test / app.py
Alvin3y1's picture
Update app.py
175cf4a verified
raw
history blame
21 kB
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