|
|
import asyncio |
|
|
import json |
|
|
import logging |
|
|
import time |
|
|
import math |
|
|
import aiohttp |
|
|
from collections import deque |
|
|
from aiohttp import web |
|
|
import websockets |
|
|
import statistics |
|
|
|
|
|
SYMBOL_KRAKEN = "BTC/USD" |
|
|
PORT = 7860 |
|
|
HISTORY_LENGTH = 300 |
|
|
BROADCAST_RATE = 0.1 |
|
|
MICROPRICE_DECAY = 0.05 |
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s') |
|
|
|
|
|
class OnlineStats: |
|
|
def __init__(self): |
|
|
self.count = 0 |
|
|
self.mean = 0.0 |
|
|
self.M2 = 0.0 |
|
|
|
|
|
def update(self, value): |
|
|
self.count += 1 |
|
|
delta = value - self.mean |
|
|
self.mean += delta / self.count |
|
|
delta2 = value - self.mean |
|
|
self.M2 += delta * delta2 |
|
|
|
|
|
@property |
|
|
def variance(self): |
|
|
if self.count < 2: return 0.0 |
|
|
return self.M2 / self.count |
|
|
|
|
|
@property |
|
|
def std_dev(self): |
|
|
return math.sqrt(self.variance) |
|
|
|
|
|
class KalmanVelocity: |
|
|
def __init__(self, R=0.001, Q=0.0001): |
|
|
self.z = 0.0 |
|
|
self.v = 0.0 |
|
|
self.P = 1.0 |
|
|
self.R = R |
|
|
self.Q = Q |
|
|
self.last_ts = time.time() |
|
|
|
|
|
def update(self, price): |
|
|
now = time.time() |
|
|
dt = now - self.last_ts |
|
|
self.last_ts = now |
|
|
if dt <= 0: return |
|
|
|
|
|
pred_z = self.z + self.v * dt |
|
|
pred_v = self.v |
|
|
p_cov = self.P + self.Q |
|
|
|
|
|
y = price - pred_z |
|
|
K = p_cov / (p_cov + self.R) |
|
|
|
|
|
self.z = pred_z + K * y |
|
|
self.v = pred_v + (K / dt) * y |
|
|
self.P = (1 - K) * p_cov |
|
|
|
|
|
market_state = { |
|
|
"bids": {}, |
|
|
"asks": {}, |
|
|
"history": [], |
|
|
"trade_history": deque(maxlen=2000), |
|
|
"ohlc_history": [], |
|
|
"current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()}, |
|
|
"current_mid": 0.0, |
|
|
"ready": False, |
|
|
"kalman": KalmanVelocity(), |
|
|
"stats": OnlineStats(), |
|
|
"walls": {"bids": [], "asks": []} |
|
|
} |
|
|
|
|
|
connected_clients = set() |
|
|
|
|
|
def detect_walls(order_book, side_name): |
|
|
if not order_book: return [] |
|
|
|
|
|
sorted_book = sorted(order_book.items(), key=lambda x: x[0], reverse=(side_name == 'bids')) |
|
|
relevant = sorted_book[:50] |
|
|
|
|
|
volumes = [q for p, q in relevant] |
|
|
if not volumes: return [] |
|
|
|
|
|
avg_vol = statistics.mean(volumes) |
|
|
std_vol = statistics.stdev(volumes) if len(volumes) > 1 else 0 |
|
|
|
|
|
walls = [] |
|
|
for p, q in relevant: |
|
|
if std_vol > 0: |
|
|
z = (q - avg_vol) / std_vol |
|
|
if z > 2.5: |
|
|
walls.append({'p': p, 'q': q, 'z': z}) |
|
|
|
|
|
return walls[:3] |
|
|
|
|
|
def calculate_weighted_micro_price(mid_price): |
|
|
bids = sorted(market_state['bids'].items(), reverse=True)[:50] |
|
|
asks = sorted(market_state['asks'].items())[:50] |
|
|
|
|
|
if not bids or not asks: return mid_price |
|
|
|
|
|
sum_wb = 0.0 |
|
|
sum_wa = 0.0 |
|
|
|
|
|
for p, q in bids: |
|
|
distance = abs(mid_price - p) |
|
|
weight = q * math.exp(-MICROPRICE_DECAY * distance) |
|
|
sum_wb += weight |
|
|
|
|
|
for p, q in asks: |
|
|
distance = abs(p - mid_price) |
|
|
weight = q * math.exp(-MICROPRICE_DECAY * distance) |
|
|
sum_wa += weight |
|
|
|
|
|
total_w = sum_wb + sum_wa |
|
|
if total_w == 0: return mid_price |
|
|
|
|
|
imbalance = (sum_wb - sum_wa) / total_w |
|
|
spread = asks[0][0] - bids[0][0] |
|
|
micro_price = mid_price + (imbalance * (spread / 2)) |
|
|
return micro_price |
|
|
|
|
|
def calculate_vwap_1m(): |
|
|
cutoff = time.time() - 60 |
|
|
v_sum = 0.0 |
|
|
pv_sum = 0.0 |
|
|
|
|
|
for trade in reversed(market_state['trade_history']): |
|
|
if trade['t'] < cutoff: break |
|
|
pv_sum += trade['p'] * trade['q'] |
|
|
v_sum += trade['q'] |
|
|
|
|
|
return pv_sum / v_sum if v_sum > 0 else market_state['current_mid'] |
|
|
|
|
|
def calculate_kyle_lambda(volatility, volume_window): |
|
|
if volume_window <= 0: return 0 |
|
|
return (volatility * 1000) / (math.sqrt(volume_window) + 1) |
|
|
|
|
|
def process_market_data(): |
|
|
if not market_state['ready']: return {"error": "Initializing..."} |
|
|
|
|
|
mid = market_state['current_mid'] |
|
|
now = time.time() |
|
|
|
|
|
market_state['stats'].update(mid) |
|
|
volatility = market_state['stats'].std_dev |
|
|
if volatility == 0: volatility = 1.0 |
|
|
|
|
|
market_state['kalman'].update(mid) |
|
|
|
|
|
micro_price = calculate_weighted_micro_price(mid) |
|
|
vwap = calculate_vwap_1m() |
|
|
|
|
|
ofi_buy = 0.0 |
|
|
ofi_sell = 0.0 |
|
|
ofi_window = 10.0 |
|
|
|
|
|
for t in reversed(market_state['trade_history']): |
|
|
if t['t'] < (now - ofi_window): break |
|
|
if t['side'] == 'buy': ofi_buy += t['q'] |
|
|
else: ofi_sell += t['q'] |
|
|
|
|
|
net_ofi = ofi_buy - ofi_sell |
|
|
total_vol_10s = ofi_buy + ofi_sell |
|
|
|
|
|
k_lambda = calculate_kyle_lambda(volatility, total_vol_10s) |
|
|
impact_term = net_ofi * k_lambda |
|
|
|
|
|
mean_reversion_alpha = 0.1 |
|
|
reversion_term = (vwap - mid) * mean_reversion_alpha |
|
|
|
|
|
micro_alpha = (micro_price - mid) * 0.8 |
|
|
|
|
|
trend_term = market_state['kalman'].v * 60.0 |
|
|
|
|
|
predicted_delta = impact_term + micro_alpha + trend_term + reversion_term |
|
|
pred_close = mid + predicted_delta |
|
|
|
|
|
sigma_1m = volatility * math.sqrt(60) |
|
|
|
|
|
pred_candle = { |
|
|
'time': int(now) + 60, |
|
|
'open': mid, |
|
|
'close': pred_close, |
|
|
'high': max(mid, pred_close) + (2 * sigma_1m), |
|
|
'low': min(mid, pred_close) - (2 * sigma_1m) |
|
|
} |
|
|
|
|
|
if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5): |
|
|
market_state['history'].append({'t': now, 'p': mid}) |
|
|
if len(market_state['history']) > HISTORY_LENGTH: |
|
|
market_state['history'].pop(0) |
|
|
|
|
|
bid_walls = detect_walls(market_state['bids'], 'bids') |
|
|
ask_walls = detect_walls(market_state['asks'], 'asks') |
|
|
|
|
|
analysis = { |
|
|
"projected": pred_close, |
|
|
"rho": (micro_price - mid), |
|
|
"vwap": vwap, |
|
|
"lambda": k_lambda |
|
|
} |
|
|
|
|
|
return { |
|
|
"mid": mid, |
|
|
"history": market_state['history'], |
|
|
"ohlc": market_state['ohlc_history'], |
|
|
"pred_candle": pred_candle, |
|
|
"analysis": analysis, |
|
|
"walls": {"bids": bid_walls, "asks": ask_walls} |
|
|
} |
|
|
|
|
|
HTML_PAGE = f""" |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<title>{SYMBOL_KRAKEN} Quant</title> |
|
|
<script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
:root {{ --bg-base: #000000; --bg-panel: #0a0a0a; --border: #252525; --text-main: #FFFFFF; --text-dim: #999999; --green: #00ff9d; --red: #ff3b3b; --purple: #d500f9; }} |
|
|
body {{ margin: 0; padding: 0; background-color: var(--bg-base); color: var(--text-main); font-family: 'Inter', sans-serif; overflow: hidden; height: 100vh; width: 100vw; }} |
|
|
.layout {{ display: grid; grid-template-rows: 34px 1fr 1fr; grid-template-columns: 3fr 1fr; gap: 1px; background-color: var(--border); height: 100vh; }} |
|
|
.panel {{ background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; }} |
|
|
.status-bar {{ grid-column: 1 / 3; grid-row: 1 / 2; background: var(--bg-panel); display: flex; align-items: center; justify-content: space-between; padding: 0 12px; font-family: 'JetBrains Mono', monospace; font-size: 12px; border-bottom: 1px solid var(--border); }} |
|
|
.live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; margin-right: 8px; box-shadow: 0 0 8px var(--green); }} |
|
|
#p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }} |
|
|
#p-bottom {{ grid-column: 1 / 2; grid-row: 3 / 4; display: grid; grid-template-columns: 1fr; gap: 1px; background: var(--border); }} |
|
|
#p-sidebar {{ grid-column: 2 / 3; grid-row: 2 / 4; padding: 15px; display: flex; flex-direction: column; gap: 20px; border-left: 1px solid var(--border); }} |
|
|
.chart-header {{ height: 24px; display: flex; align-items: center; padding-left: 12px; font-size: 10px; font-weight: 700; color: var(--text-dim); background: #050505; border-bottom: 1px solid #151515; }} |
|
|
.data-group {{ display: flex; flex-direction: column; gap: 4px; }} |
|
|
.label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; }} |
|
|
.value {{ font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 700; color: #fff; }} |
|
|
.value-lg {{ font-size: 26px; }} |
|
|
.value-sub {{ font-family: 'JetBrains Mono', monospace; font-size: 11px; margin-top: 2px; color: #666; }} |
|
|
.divider {{ height: 1px; background: var(--border); width: 100%; }} |
|
|
.c-purple {{ color: var(--purple); }} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="layout"> |
|
|
<div class="status-bar"> |
|
|
<div><span class="live-dot"></span><span style="font-weight:700;">{SYMBOL_KRAKEN} MATH MODEL</span></div> |
|
|
<div id="price-ticker">---</div> |
|
|
</div> |
|
|
<div id="p-chart" class="panel"> |
|
|
<div class="chart-header">PRICE (BLUE) vs PREDICTION (YELLOW) vs VWAP (WHITE)</div> |
|
|
<div id="tv-price" style="flex: 1;"></div> |
|
|
</div> |
|
|
<div id="p-bottom" class="panel"> |
|
|
<div class="chart-header">1M KLINE + WALLS + GHOST PREDICTION (PURPLE)</div> |
|
|
<div id="tv-candles" style="flex: 1;"></div> |
|
|
</div> |
|
|
<div id="p-sidebar" class="panel"> |
|
|
<div class="data-group"> |
|
|
<span class="label">Predicted Close (1m)</span> |
|
|
<div style="display:flex; align-items: baseline; gap: 10px;"> |
|
|
<span id="proj-pct" class="value value-lg">--%</span> |
|
|
<span id="proj-val" class="value-sub c-purple">---</span> |
|
|
</div> |
|
|
<span class="label" style="margin-top:4px;">MicroPrice + OFI Impact</span> |
|
|
</div> |
|
|
<div class="divider"></div> |
|
|
<div class="data-group"> |
|
|
<span class="label">VWAP Divergence</span> |
|
|
<span id="vwap-div" class="value">0.00</span> |
|
|
</div> |
|
|
<div class="divider"></div> |
|
|
<div class="data-group"> |
|
|
<span class="label">Kyle's Lambda (Liq. Cost)</span> |
|
|
<span id="lambda-val" class="value">0.00</span> |
|
|
<span class="value-sub">Impact per Volume Unit</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<script> |
|
|
const dom = {{ ticker: document.getElementById('price-ticker'), projVal: document.getElementById('proj-val'), projPct: document.getElementById('proj-pct'), vwapDiv: document.getElementById('vwap-div'), lambdaVal: document.getElementById('lambda-val') }}; |
|
|
const chartOpts = {{ layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }}, grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }}, crosshair: {{ mode: 1 }} }}; |
|
|
|
|
|
const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts); |
|
|
const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2 }}); |
|
|
const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2 }}); |
|
|
const vwapSeries = priceChart.addLineSeries({{ color: 'rgba(255,255,255,0.3)', lineWidth: 1, lineStyle: 0 }}); |
|
|
|
|
|
const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), chartOpts); |
|
|
const candleSeries = candleChart.addCandlestickSeries({{ upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false }}); |
|
|
const ghostSeries = candleChart.addCandlestickSeries({{ upColor: 'rgba(213, 0, 249, 0.5)', downColor: 'rgba(213, 0, 249, 0.5)', borderVisible: true, borderColor: '#d500f9' }}); |
|
|
|
|
|
let activeWallLines = []; |
|
|
|
|
|
new ResizeObserver(e => {{ |
|
|
const t1 = document.getElementById('tv-price'); |
|
|
const t2 = document.getElementById('tv-candles'); |
|
|
priceChart.applyOptions({{ width: t1.clientWidth, height: t1.clientHeight }}); |
|
|
candleChart.applyOptions({{ width: t2.clientWidth, height: t2.clientHeight }}); |
|
|
}}).observe(document.body); |
|
|
|
|
|
function connect() {{ |
|
|
const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws'); |
|
|
ws.onmessage = (e) => {{ |
|
|
const data = JSON.parse(e.data); |
|
|
if (data.error) return; |
|
|
|
|
|
if (data.history.length) {{ |
|
|
const hist = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }})); |
|
|
const cleanHist = [...new Map(hist.map(i => [i.time, i])).values()]; |
|
|
priceSeries.setData(cleanHist); |
|
|
dom.ticker.innerText = cleanHist[cleanHist.length-1].value.toFixed(2); |
|
|
|
|
|
if(data.analysis) {{ |
|
|
predSeries.setData([ |
|
|
cleanHist[cleanHist.length-1], |
|
|
{{ time: cleanHist[cleanHist.length-1].time + 60, value: data.analysis.projected }} |
|
|
]); |
|
|
|
|
|
vwapSeries.setData([ |
|
|
{{ time: cleanHist[0].time, value: data.analysis.vwap }}, |
|
|
{{ time: cleanHist[cleanHist.length-1].time, value: data.analysis.vwap }} |
|
|
]); |
|
|
|
|
|
dom.lambdaVal.innerText = (data.analysis.lambda * 1000).toFixed(4); |
|
|
const vwapDiff = cleanHist[cleanHist.length-1].value - data.analysis.vwap; |
|
|
dom.vwapDiv.innerText = vwapDiff.toFixed(2); |
|
|
dom.vwapDiv.style.color = vwapDiff > 0 ? '#ff3b3b' : '#00ff9d'; |
|
|
}} |
|
|
}} |
|
|
|
|
|
if (data.ohlc && data.ohlc.length) {{ |
|
|
candleSeries.setData(data.ohlc.map(c => ({{ time: c.time, open: c.open, high: c.high, low: c.low, close: c.close }}))); |
|
|
}} |
|
|
|
|
|
if (data.walls) {{ |
|
|
activeWallLines.forEach(l => candleSeries.removePriceLine(l)); |
|
|
activeWallLines = []; |
|
|
data.walls.bids.forEach(w => {{ |
|
|
activeWallLines.push(candleSeries.createPriceLine({{ price: w.p, color: '#00ff9d', lineWidth: 2, lineStyle: 2, axisLabelVisible: true, title: 'BID WALL' }})); |
|
|
}}); |
|
|
data.walls.asks.forEach(w => {{ |
|
|
activeWallLines.push(candleSeries.createPriceLine({{ price: w.p, color: '#ff3b3b', lineWidth: 2, lineStyle: 2, axisLabelVisible: true, title: 'ASK WALL' }})); |
|
|
}}); |
|
|
}} |
|
|
|
|
|
if (data.pred_candle) {{ |
|
|
ghostSeries.setData([data.pred_candle]); |
|
|
const currentP = parseFloat(dom.ticker.innerText); |
|
|
const pClose = data.pred_candle.close; |
|
|
dom.projVal.innerText = pClose.toFixed(2); |
|
|
const pct = ((pClose - currentP) / currentP) * 100; |
|
|
dom.projPct.innerText = (pct >= 0 ? "+" : "") + pct.toFixed(3) + "%"; |
|
|
dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)"; |
|
|
}} |
|
|
}}; |
|
|
ws.onclose = () => setTimeout(connect, 2000); |
|
|
}} |
|
|
connect(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
async def kraken_worker(): |
|
|
global market_state |
|
|
try: |
|
|
async with aiohttp.ClientSession() as session: |
|
|
url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1" |
|
|
async with session.get(url) as response: |
|
|
if response.status == 200: |
|
|
data = await response.json() |
|
|
if 'result' in data: |
|
|
raw = list(data['result'].values())[0] |
|
|
market_state['ohlc_history'] = [ |
|
|
{'time': int(c[0]), 'open': float(c[1]), 'high': float(c[2]), 'low': float(c[3]), 'close': float(c[4])} |
|
|
for c in raw[-120:] |
|
|
] |
|
|
except Exception as e: |
|
|
logging.error(f"Init Error: {e}") |
|
|
|
|
|
while True: |
|
|
try: |
|
|
async with websockets.connect("wss://ws.kraken.com/v2") as ws: |
|
|
logging.info(f"Connected to Kraken ({SYMBOL_KRAKEN})") |
|
|
|
|
|
await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 100}})) |
|
|
await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}})) |
|
|
await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "ohlc", "symbol": [SYMBOL_KRAKEN], "interval": 1}})) |
|
|
|
|
|
async for message in ws: |
|
|
payload = json.loads(message) |
|
|
channel = payload.get("channel") |
|
|
data = payload.get("data", []) |
|
|
|
|
|
if channel == "book": |
|
|
for item in data: |
|
|
for bid in item.get('bids', []): |
|
|
market_state['bids'][float(bid['price'])] = float(bid['qty']) |
|
|
for ask in item.get('asks', []): |
|
|
market_state['asks'][float(ask['price'])] = float(ask['qty']) |
|
|
|
|
|
market_state['bids'] = {k: v for k, v in market_state['bids'].items() if v > 0} |
|
|
market_state['asks'] = {k: v for k, v in market_state['asks'].items() if v > 0} |
|
|
|
|
|
if market_state['bids'] and market_state['asks']: |
|
|
best_bid = max(market_state['bids'].keys()) |
|
|
best_ask = min(market_state['asks'].keys()) |
|
|
market_state['current_mid'] = (best_bid + best_ask) / 2 |
|
|
market_state['ready'] = True |
|
|
|
|
|
elif channel == "trade": |
|
|
for trade in data: |
|
|
try: |
|
|
t_obj = { |
|
|
't': time.time(), |
|
|
'p': float(trade['price']), |
|
|
'q': float(trade['qty']), |
|
|
'side': trade['side'] |
|
|
} |
|
|
market_state['trade_history'].append(t_obj) |
|
|
except: pass |
|
|
|
|
|
elif channel == "ohlc": |
|
|
for c in data: |
|
|
c_data = { |
|
|
'time': int(float(c['endtime'])), |
|
|
'open': float(c['open']), |
|
|
'high': float(c['high']), |
|
|
'low': float(c['low']), |
|
|
'close': float(c['close']) |
|
|
} |
|
|
if market_state['ohlc_history'] and market_state['ohlc_history'][-1]['time'] == c_data['time']: |
|
|
market_state['ohlc_history'][-1] = c_data |
|
|
else: |
|
|
market_state['ohlc_history'].append(c_data) |
|
|
if len(market_state['ohlc_history']) > 100: market_state['ohlc_history'].pop(0) |
|
|
|
|
|
except Exception as e: |
|
|
logging.warning(f"Reconnecting: {e}") |
|
|
await asyncio.sleep(2) |
|
|
|
|
|
async def broadcast_worker(): |
|
|
while True: |
|
|
if connected_clients and market_state['ready']: |
|
|
payload = process_market_data() |
|
|
if "error" not in payload: |
|
|
msg = json.dumps(payload) |
|
|
for ws in list(connected_clients): |
|
|
try: await ws.send_str(msg) |
|
|
except: pass |
|
|
await asyncio.sleep(BROADCAST_RATE) |
|
|
|
|
|
async def websocket_handler(request): |
|
|
ws = web.WebSocketResponse() |
|
|
await ws.prepare(request) |
|
|
connected_clients.add(ws) |
|
|
try: |
|
|
async for msg in ws: |
|
|
pass |
|
|
finally: |
|
|
connected_clients.remove(ws) |
|
|
return ws |
|
|
|
|
|
async def handle_index(request): |
|
|
return web.Response(text=HTML_PAGE, content_type='text/html') |
|
|
|
|
|
async def start_background(app): |
|
|
app['kraken_task'] = asyncio.create_task(kraken_worker()) |
|
|
app['broadcast_task'] = asyncio.create_task(broadcast_worker()) |
|
|
|
|
|
async def cleanup_background(app): |
|
|
app['kraken_task'].cancel() |
|
|
app['broadcast_task'].cancel() |
|
|
|
|
|
async def main(): |
|
|
app = web.Application() |
|
|
app.router.add_get('/', handle_index) |
|
|
app.router.add_get('/ws', websocket_handler) |
|
|
app.on_startup.append(start_background) |
|
|
app.on_cleanup.append(cleanup_background) |
|
|
runner = web.AppRunner(app) |
|
|
await runner.setup() |
|
|
site = web.TCPSite(runner, '0.0.0.0', PORT) |
|
|
await site.start() |
|
|
print(f"Quant Dashboard: http://localhost:{PORT}") |
|
|
await asyncio.Event().wait() |
|
|
|
|
|
if __name__ == "__main__": |
|
|
try: asyncio.run(main()) |
|
|
except KeyboardInterrupt: pass |