test / app.py
Alvin3y1's picture
Update app.py
f95b3cf verified
raw
history blame
27.6 kB
import asyncio
import json
import logging
import time
import bisect
import math
import statistics
from datetime import datetime
from aiohttp import web
import websockets
# --- CONFIGURATION ---
SYMBOL_KRAKEN = "BTC/USD"
PORT = 7860
HISTORY_LENGTH = 300
BROADCAST_RATE = 0.1
# Mathematical Constants
DECAY_LAMBDA = 50.0
IMPACT_SENSITIVITY = 2.0
WALL_DAMPENING = 0.8
Z_SCORE_THRESHOLD = 3.0
WALL_LOOKBACK = 200
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
# --- GLOBAL STATE ---
market_state = {
"bids": {},
"asks": {},
"history": [],
"pred_history": [],
"trade_vol_history": [], # New: Store trade volume history
"current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
"current_mid": 0.0,
"ready": False
}
connected_clients = set()
# --- QUANTITATIVE METHODS ---
def detect_anomalies(orders, scan_depth):
if len(orders) < 10: return []
relevant_orders = orders[:scan_depth]
volumes = [q for p, q in relevant_orders]
if not volumes: return []
try:
avg_vol = statistics.mean(volumes)
stdev_vol = statistics.stdev(volumes)
except statistics.StatisticsError:
return []
if stdev_vol == 0: return []
walls = []
for price, qty in relevant_orders:
z_score = (qty - avg_vol) / stdev_vol
if z_score > Z_SCORE_THRESHOLD:
walls.append({"price": price, "vol": qty, "z_score": z_score})
walls.sort(key=lambda x: x['z_score'], reverse=True)
return walls[:3]
def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, best_ask, walls):
if not diff_x or len(diff_x) < 5: return None
# Weighted Imbalance Calculation
weighted_imbalance = 0.0
total_weight = 0.0
for i in range(len(diff_x)):
dist = diff_x[i]
net_vol = diff_y_net[i]
weight = math.exp(-dist / DECAY_LAMBDA)
weighted_imbalance += net_vol * weight
total_weight += weight
rho = weighted_imbalance / total_weight if total_weight > 0 else 0
# Base Projection
spread = best_ask - best_bid
theoretical_delta = (spread / 2) * rho * IMPACT_SENSITIVITY
projected_price = current_mid + theoretical_delta
# Wall Friction
final_delta = theoretical_delta
if final_delta > 0 and walls['asks']:
nearest_wall = walls['asks'][0]
if projected_price >= nearest_wall['price']:
damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
final_delta *= damp_factor
elif final_delta < 0 and walls['bids']:
nearest_wall = walls['bids'][0]
if projected_price <= nearest_wall['price']:
damp_factor = 1.0 / (1.0 + (nearest_wall['z_score'] * 0.2))
final_delta *= damp_factor
return {
"projected": current_mid + final_delta,
"rho": rho
}
def process_market_data():
if not market_state['ready']: return {"error": "Initializing..."}
mid = market_state['current_mid']
# Process Trade Volume Window (Reset every 1 second)
now = time.time()
if now - market_state['current_vol_window']['start'] >= 1.0:
market_state['trade_vol_history'].append({
't': now,
'buy': market_state['current_vol_window']['buy'],
'sell': market_state['current_vol_window']['sell']
})
if len(market_state['trade_vol_history']) > 60: # Keep last 60 seconds
market_state['trade_vol_history'].pop(0)
# Reset window
market_state['current_vol_window'] = {"buy": 0.0, "sell": 0.0, "start": now}
# Order Book Processing
sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
if not sorted_bids or not sorted_asks: return {"error": "Empty Book"}
best_bid = sorted_bids[0][0]
best_ask = sorted_asks[0][0]
bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
d_b_x, d_b_y, cum = [], [], 0
for p, q in sorted_bids[:300]:
d = mid - p
if d >= 0:
cum += q
d_b_x.append(d); d_b_y.append(cum)
d_a_x, d_a_y, cum = [], [], 0
for p, q in sorted_asks[:300]:
d = p - mid
if d >= 0:
cum += q
d_a_x.append(d); d_a_y.append(cum)
diff_x, diff_y_net = [], []
chart_bids, chart_asks = [], []
if d_b_x and d_a_x:
max_dist = min(d_b_x[-1], d_a_x[-1])
step_size = max_dist / 100
steps = [i * step_size for i in range(1, 101)]
for s in steps:
idx_b = bisect.bisect_right(d_b_x, s)
vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
idx_a = bisect.bisect_right(d_a_x, s)
vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
diff_x.append(s)
diff_y_net.append(vol_b - vol_a)
chart_bids.append(vol_b)
chart_asks.append(vol_a)
analysis = calculate_micro_price_structure(
diff_x, diff_y_net, mid, best_bid, best_ask,
{"bids": bid_walls, "asks": ask_walls}
)
if analysis:
if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
market_state['pred_history'].append({'t': now, 'p': analysis['projected']})
if len(market_state['pred_history']) > HISTORY_LENGTH:
market_state['pred_history'].pop(0)
return {
"mid": mid,
"history": market_state['history'],
"pred_history": market_state['pred_history'],
"trade_history": market_state['trade_vol_history'],
"depth_x": diff_x,
"depth_net": diff_y_net,
"depth_bids": chart_bids,
"depth_asks": chart_asks,
"analysis": analysis,
"walls": {"bids": bid_walls, "asks": ask_walls}
}
# --- FRONTEND ---
HTML_PAGE = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{SYMBOL_KRAKEN}</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;
--blue: #2979ff;
--yellow: #ffeb3b;
}}
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;
box-sizing: border-box;
}}
.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;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
z-index: 50;
}}
.status-left {{ display: flex; gap: 20px; align-items: center; }}
.status-right {{ color: var(--text-dim); letter-spacing: 1px; }}
.live-dot {{ width: 8px; height: 8px; background-color: var(--green); border-radius: 50%; display: inline-block; box-shadow: 0 0 8px var(--green); }}
.ticker-val {{ font-weight: 700; color: #fff; font-size: 13px; }}
#p-chart {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
#p-depth {{
grid-column: 1 / 2; grid-row: 3 / 4;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
background: var(--border);
}}
.depth-sub {{ background: var(--bg-panel); display: flex; flex-direction: column; position: relative; }}
#p-sidebar {{
grid-column: 2 / 3;
grid-row: 2 / 4;
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px; /* Tighter gap to fit the new chart */
border-left: 1px solid var(--border);
overflow-y: hidden;
}}
.chart-header {{
height: 24px;
min-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;
letter-spacing: 0.5px;
}}
.data-group {{ display: flex; flex-direction: column; gap: 4px; }}
.label {{ font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }}
.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-green {{ color: var(--green); }}
.c-red {{ color: var(--red); }}
.c-dim {{ color: var(--text-dim); }}
.list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; max-height: 120px; }}
.list-item {{
display: flex; justify-content: space-between;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
border-bottom: 1px solid #151515;
padding-bottom: 4px;
}}
.list-item span:first-child {{ color: #e0e0e0; }}
.list-item:last-child {{ border: none; }}
#sidebar-chart {{
flex: 1;
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 4px;
min-height: 100px;
}}
</style>
</head>
<body>
<div class="layout">
<div class="status-bar">
<div class="status-left">
<span class="live-dot"></span>
<span style="font-weight:700; color:#fff;">{SYMBOL_KRAKEN}</span>
<span id="price-ticker" class="ticker-val">---</span>
</div>
<div class="status-right" id="clock">00:00:00 UTC</div>
</div>
<div id="p-chart" class="panel">
<div class="chart-header">PRICE ACTION (BLUE) // PREDICTION (YELLOW)</div>
<div id="tv-price" style="flex: 1; width: 100%;"></div>
</div>
<div id="p-depth">
<div class="depth-sub">
<div class="chart-header">LIQUIDITY DENSITY</div>
<div id="tv-raw" style="flex: 1; width: 100%;"></div>
</div>
<div class="depth-sub">
<div class="chart-header">ORDER FLOW IMBALANCE</div>
<div id="tv-net" style="flex: 1; width: 100%;"></div>
</div>
</div>
<div id="p-sidebar" class="panel">
<div class="data-group">
<span class="label">Micro-Price Delta</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">---</span>
</div>
</div>
<div class="divider"></div>
<div class="data-group">
<span class="label">OFI Imbalance Ratio</span>
<span id="score-val" class="value">0.00</span>
</div>
<div class="divider"></div>
<div class="data-group">
<span class="label">Detected Walls (Z > 3.0)</span>
<div id="wall-list" class="list-container">
<span class="c-dim" style="font-size: 11px;">Scanning...</span>
</div>
</div>
<!-- NEW CHART UNDER WALLS -->
<div class="data-group" style="flex: 1; display:flex; flex-direction:column;">
<span class="label">Real-time Volume (Ticks)</span>
<div id="sidebar-chart"></div>
</div>
</div>
</div>
<script>
setInterval(() => {{
const now = new Date();
document.getElementById('clock').innerText = now.toISOString().split('T')[1].split('.')[0] + ' UTC';
}}, 1000);
document.addEventListener('DOMContentLoaded', () => {{
const dom = {{
ticker: document.getElementById('price-ticker'),
score: document.getElementById('score-val'),
projVal: document.getElementById('proj-val'),
projPct: document.getElementById('proj-pct'),
wallList: document.getElementById('wall-list')
}};
const chartOpts = {{
layout: {{ background: {{ type: 'solid', color: '#0a0a0a' }}, textColor: '#888', fontFamily: 'JetBrains Mono' }},
grid: {{ vertLines: {{ color: '#151515' }}, horzLines: {{ color: '#151515' }} }},
rightPriceScale: {{ borderColor: '#222', scaleMargins: {{ top: 0.1, bottom: 0.1 }} }},
timeScale: {{ borderColor: '#222', timeVisible: true, secondsVisible: true }},
crosshair: {{ mode: 1, vertLine: {{ color: '#444', labelBackgroundColor: '#444' }}, horzLine: {{ color: '#444', labelBackgroundColor: '#444' }} }}
}};
// 1. MAIN PRICE CHART
const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }}); // BLUE
const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Forecast' }}); // YELLOW
// 2. DEPTH CHARTS
const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{
...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
}});
const bidSeries = rawChart.addAreaSeries({{ lineColor: '#00ff9d', topColor: 'rgba(0, 255, 157, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
const askSeries = rawChart.addAreaSeries({{ lineColor: '#ff3b3b', topColor: 'rgba(255, 59, 59, 0.15)', bottomColor: 'rgba(0,0,0,0)', lineWidth: 1 }});
const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{
...chartOpts, localization: {{ timeFormatter: t => '$' + t.toFixed(2) }}
}});
const netSeries = netChart.addHistogramSeries({{ color: '#2979ff' }});
// 3. SIDEBAR VOLUME CHART
const volChart = LightweightCharts.createChart(document.getElementById('sidebar-chart'), {{
...chartOpts,
grid: {{ vertLines: {{ visible: false }}, horzLines: {{ visible: false }} }},
rightPriceScale: {{ visible: false }},
timeScale: {{ visible: false }},
handleScroll: false,
handleScale: false
}});
const volBuySeries = volChart.addHistogramSeries({{ color: '#00ff9d' }});
const volSellSeries = volChart.addHistogramSeries({{ color: '#ff3b3b' }});
let activeLines = [];
// RESIZE OBSERVER
new ResizeObserver(entries => {{
for(let entry of entries) {{
const {{width, height}} = entry.contentRect;
if(entry.target.id === 'tv-price') priceChart.applyOptions({{width, height}});
if(entry.target.id === 'tv-raw') rawChart.applyOptions({{width, height}});
if(entry.target.id === 'tv-net') netChart.applyOptions({{width, height}});
if(entry.target.id === 'sidebar-chart') volChart.applyOptions({{width, height}});
}}
}}).observe(document.body);
['tv-price', 'tv-raw', 'tv-net', 'sidebar-chart'].forEach(id => {{
new ResizeObserver(e => {{
const t = document.getElementById(id);
if(id === 'tv-price') priceChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
if(id === 'tv-raw') rawChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
if(id === 'tv-net') netChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
if(id === 'sidebar-chart') volChart.applyOptions({{ width: t.clientWidth, height: t.clientHeight }});
}}).observe(document.getElementById(id));
}});
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);
const lastP = cleanHist[cleanHist.length-1].value;
dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
if (data.analysis) {{
const proj = data.analysis.projected;
const rho = data.analysis.rho;
predSeries.setData([
cleanHist[cleanHist.length-1],
{{ time: cleanHist[cleanHist.length-1].time + 60, value: proj }}
]);
const pct = ((proj - lastP) / lastP) * 100;
const sign = pct >= 0 ? "+" : "";
dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
dom.score.innerText = rho.toFixed(3);
dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
}}
}}
// WALLS
if (data.walls) {{
activeLines.forEach(l => priceSeries.removePriceLine(l));
activeLines = [];
let html = "";
const addWall = (w, type) => {{
const color = type === 'BID' ? '#00ff9d' : '#ff3b3b';
activeLines.push(priceSeries.createPriceLine({{ price: w.price, color: color, lineWidth: 1, lineStyle: 2, axisLabelVisible: false }}));
html += `<div class="list-item">
<span style="color:${{color}}">${{type}} ${{w.price}}</span>
<span class="c-dim">Z:${{w.z_score.toFixed(1)}}</span>
</div>`;
}};
data.walls.asks.forEach(w => addWall(w, 'ASK'));
data.walls.bids.forEach(w => addWall(w, 'BID'));
dom.wallList.innerHTML = html || '<span class="c-dim" style="font-size:11px">Scanning...</span>';
}}
// VOLUME CHART IN SIDEBAR
if (data.trade_history && data.trade_history.length) {{
const buyData = [];
const sellData = [];
data.trade_history.forEach(t => {{
const time = Math.floor(t.t);
buyData.push({{ time: time, value: t.buy }});
sellData.push({{ time: time, value: t.sell }});
}});
// Ensure unique time points for LW Charts
const uniqueBuys = [...new Map(buyData.map(i => [i.time, i])).values()];
const uniqueSells = [...new Map(sellData.map(i => [i.time, i])).values()];
volBuySeries.setData(uniqueBuys);
volSellSeries.setData(uniqueSells);
}}
// DEPTH
if (data.depth_x.length) {{
const bids = [], asks = [], nets = [];
for(let i=0; i<data.depth_x.length; i++) {{
const t = data.depth_x[i];
bids.push({{ time: t, value: data.depth_bids[i] }});
asks.push({{ time: t, value: data.depth_asks[i] }});
nets.push({{ time: t, value: data.depth_net[i], color: data.depth_net[i] > 0 ? '#00ff9d' : '#ff3b3b' }});
}}
bidSeries.setData(bids);
askSeries.setData(asks);
netSeries.setData(nets);
}}
}};
ws.onclose = () => setTimeout(connect, 2000);
}}
connect();
}});
</script>
</body>
</html>
"""
# --- SERVER ---
async def kraken_worker():
global market_state
while True:
try:
async with websockets.connect("wss://ws.kraken.com/v2") as ws:
logging.info(f"πŸ”Œ Connected to Kraken ({SYMBOL_KRAKEN})")
# SUBSCRIBE TO BOOK AND TRADES
await ws.send(json.dumps({
"method": "subscribe",
"params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
}))
await ws.send(json.dumps({
"method": "subscribe",
"params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}
}))
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', []):
q, p = float(bid['qty']), float(bid['price'])
if q == 0: market_state['bids'].pop(p, None)
else: market_state['bids'][p] = q
for ask in item.get('asks', []):
q, p = float(ask['qty']), float(ask['price'])
if q == 0: market_state['asks'].pop(p, None)
else: market_state['asks'][p] = q
if market_state['bids'] and market_state['asks']:
best_bid = max(market_state['bids'].keys())
best_ask = min(market_state['asks'].keys())
mid = (best_bid + best_ask) / 2
market_state['prev_mid'] = market_state['current_mid']
market_state['current_mid'] = mid
market_state['ready'] = True
now = time.time()
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)
elif channel == "trade":
# Process trades for volume history
for trade in data:
# Kraken Trade format: [price, qty, time, side, order_type, misc]
# side: 'buy' or 'sell'
try:
qty = float(trade['qty'])
side = trade['side'] # 'buy' or 'sell'
if side == 'buy':
market_state['current_vol_window']['buy'] += qty
else:
market_state['current_vol_window']['sell'] += qty
except:
pass
except Exception as e:
logging.warning(f"⚠️ Reconnecting: {e}")
await asyncio.sleep(3)
async def broadcast_worker():
while True:
if connected_clients and market_state['ready']:
payload = process_market_data()
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()
try: await app['kraken_task']; await app['broadcast_task']
except: pass
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