| import asyncio | |
| import json | |
| import logging | |
| import time | |
| import bisect | |
| import math | |
| from aiohttp import web | |
| import websockets | |
| SYMBOL_KRAKEN = "BTC/USD" | |
| PORT = 7860 | |
| HISTORY_LENGTH = 300 | |
| BROADCAST_RATE = 0.1 | |
| DECAY_LAMBDA = 100.0 | |
| IMPACT_SENSITIVITY = 0.5 | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s') | |
| market_state = { | |
| "bids": {}, | |
| "asks": {}, | |
| "history": [], | |
| "pred_history": [], | |
| "current_mid": 0.0, | |
| "prev_mid": 0.0, | |
| "ready": False | |
| } | |
| connected_clients = set() | |
| def analyze_structure(diff_x, diff_y, current_mid): | |
| if not diff_y or len(diff_y) < 5: | |
| return None | |
| weighted_imbalance = 0.0 | |
| prev_vol = 0.0 | |
| for i in range(len(diff_x)): | |
| dist = diff_x[i] | |
| cum_vol = diff_y[i] | |
| marginal_vol = cum_vol - prev_vol | |
| prev_vol = cum_vol | |
| weight = math.exp(-dist / DECAY_LAMBDA) | |
| weighted_imbalance += marginal_vol * weight | |
| if weighted_imbalance != 0: | |
| impact = math.sqrt(abs(weighted_imbalance)) * IMPACT_SENSITIVITY | |
| if weighted_imbalance < 0: | |
| impact = -impact | |
| else: | |
| impact = 0.0 | |
| projected_price = current_mid + impact | |
| return { | |
| "projected": projected_price, | |
| "net_score": weighted_imbalance | |
| } | |
| def process_market_data(): | |
| if not market_state['ready']: | |
| return {"error": "Initializing..."} | |
| mid = market_state['current_mid'] | |
| raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300] | |
| raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300] | |
| d_b_x, d_b_y, cum = [], [], 0 | |
| for p, q in raw_bids: | |
| 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 raw_asks: | |
| d = p - mid | |
| if d >= 0: | |
| cum += q | |
| d_a_x.append(d); d_a_y.append(cum) | |
| diff_x, diff_y = [], [] | |
| 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.append(vol_b - vol_a) | |
| chart_bids.append(vol_b) | |
| chart_asks.append(vol_a) | |
| analysis = analyze_structure(diff_x, diff_y, mid) | |
| now = time.time() | |
| 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'], | |
| "depth_x": diff_x, | |
| "depth_net": diff_y, | |
| "depth_bids": chart_bids, | |
| "depth_asks": chart_asks, | |
| "analysis": analysis | |
| } | |
| HTML_PAGE = f""" | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>HFT Liquidity Dashboard | {SYMBOL_KRAKEN}</title> | |
| <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script> | |
| <style> | |
| :root {{ | |
| --bg-color: #0b0c10; | |
| --panel-bg: #1f2833; | |
| --text-main: #c5c6c7; | |
| --accent-green: #66fcf1; | |
| --accent-red: #ff3b3b; | |
| --border: #2d3842; | |
| }} | |
| body {{ margin: 0; padding: 0; background-color: var(--bg-color); color: var(--text-main); font-family: monospace; overflow: hidden; height: 100vh; width: 100vw; }} | |
| .grid-container {{ | |
| display: grid; | |
| grid-template-columns: 3fr 1fr; | |
| grid-template-rows: 55% 45%; | |
| gap: 4px; | |
| height: 100vh; | |
| padding: 4px; | |
| box-sizing: border-box; | |
| }} | |
| .panel {{ background: #12141a; border: 1px solid var(--border); border-radius: 4px; position: relative; display: flex; flex-direction: column; overflow: hidden; }} | |
| #p-price {{ grid-column: 1 / 2; grid-row: 1 / 2; }} | |
| #p-stats {{ grid-column: 2 / 3; grid-row: 1 / 3; border-left: 2px solid #45a29e; }} | |
| #p-bottom {{ grid-column: 1 / 2; grid-row: 2 / 3; display: flex; gap: 4px; background: transparent; border: none; }} | |
| .sub-panel {{ flex: 1; background: #12141a; border: 1px solid var(--border); border-radius: 4px; display: flex; flex-direction: column; overflow: hidden; }} | |
| .panel-header {{ padding: 6px 10px; background: #0f1116; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: bold; display: flex; justify-content: space-between; color: var(--accent-green); text-transform: uppercase; }} | |
| #tv-price, #tv-raw, #tv-net {{ flex: 1; width: 100%; position: relative; }} | |
| .stats-content {{ padding: 15px; overflow-y: auto; flex: 1; }} | |
| .stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }} | |
| .stat-label {{ font-size: 11px; color: #666; display: block; margin-bottom: 4px; }} | |
| .stat-value {{ font-size: 24px; font-weight: bold; }} | |
| .green {{ color: var(--accent-green); }} | |
| .red {{ color: var(--accent-red); }} | |
| #loader {{ position: absolute; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.95); z-index: 999; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--accent-green); }} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="loader"> | |
| <div style="font-size: 24px;">ESTABLISHING UPLINK...</div> | |
| <div id="loading-status">Connecting to WebSocket Stream...</div> | |
| </div> | |
| <div class="grid-container"> | |
| <div id="p-price" class="panel"> | |
| <div class="panel-header"><span>BTC/USD Price Action</span><span id="live-price">---</span></div> | |
| <div id="tv-price"></div> | |
| </div> | |
| <div id="p-bottom"> | |
| <div class="sub-panel"> | |
| <div class="panel-header"><span>Market Depth (Bids vs Asks)</span></div> | |
| <div id="tv-raw"></div> | |
| </div> | |
| <div class="sub-panel"> | |
| <div class="panel-header"><span>Net Delta (Bids - Asks)</span></div> | |
| <div id="tv-net"></div> | |
| </div> | |
| </div> | |
| <div id="p-stats" class="panel"> | |
| <div class="panel-header">HFT ANALYTICS ENGINE</div> | |
| <div class="stats-content"> | |
| <div class="stat-box"> | |
| <span class="stat-label">WEIGHTED IMBALANCE SCORE</span> | |
| <span id="score-val" class="stat-value">0</span> | |
| </div> | |
| <div class="stat-box" style="border: 1px solid #444;"> | |
| <span class="stat-label" style="color:var(--accent-green);">IMPACT PROJECTION</span> | |
| <span id="proj-val" class="stat-value">---</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => {{ | |
| const dom = {{ | |
| loader: document.getElementById('loader'), | |
| status: document.getElementById('loading-status'), | |
| price: document.getElementById('live-price'), | |
| scoreVal: document.getElementById('score-val'), | |
| projVal: document.getElementById('proj-val') | |
| }}; | |
| const chartCommon = {{ | |
| layout: {{ background: {{ type: 'solid', color: '#12141a' }}, textColor: '#888' }}, | |
| grid: {{ vertLines: {{ color: '#1f2833' }}, horzLines: {{ color: '#1f2833' }} }}, | |
| rightPriceScale: {{ borderColor: '#2d3842' }}, | |
| timeScale: {{ borderColor: '#2d3842', timeVisible: true, secondsVisible: true }}, | |
| crosshair: {{ mode: 0 }} | |
| }}; | |
| const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartCommon); | |
| const priceSeries = priceChart.addLineSeries({{ color: '#2962FF', lineWidth: 2, title: 'Price' }}); | |
| const pastPredSeries = priceChart.addLineSeries({{ | |
| color: '#555555', | |
| lineWidth: 1, | |
| title: 'Past Prediction' | |
| }}); | |
| const futurePredSeries = priceChart.addLineSeries({{ | |
| color: '#ff9800', | |
| lineWidth: 2, | |
| lineStyle: 2, | |
| title: 'Projection' | |
| }}); | |
| const rawChart = LightweightCharts.createChart(document.getElementById('tv-raw'), {{ | |
| ...chartCommon, | |
| timeScale: {{ tickMarkFormatter: (time) => parseFloat(time).toFixed(0) }}, | |
| localization: {{ timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2) }} | |
| }}); | |
| const rawBidSeries = rawChart.addAreaSeries({{ lineColor: '#00e676', topColor: 'rgba(0, 230, 118, 0.2)', bottomColor: 'rgba(0, 230, 118, 0.0)', lineWidth: 2, title: "Bids" }}); | |
| const rawAskSeries = rawChart.addAreaSeries({{ lineColor: '#ff1744', topColor: 'rgba(255, 23, 68, 0.2)', bottomColor: 'rgba(255, 23, 68, 0.0)', lineWidth: 2, title: "Asks" }}); | |
| const netChart = LightweightCharts.createChart(document.getElementById('tv-net'), {{ | |
| ...chartCommon, | |
| timeScale: {{ tickMarkFormatter: (time) => parseFloat(time).toFixed(0) }}, | |
| localization: {{ timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2) }} | |
| }}); | |
| const netSeries = netChart.addAreaSeries({{ | |
| topColor: 'rgba(33, 150, 243, 0.56)', | |
| bottomColor: 'rgba(33, 150, 243, 0.0)', | |
| lineColor: '#2196f3', | |
| lineWidth: 2 | |
| }}); | |
| const resizeObserver = 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}}); | |
| }} | |
| }}); | |
| ['tv-price', 'tv-raw', 'tv-net'].forEach(id => resizeObserver.observe(document.getElementById(id))); | |
| function connect() {{ | |
| const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'; | |
| const url = `${{proto}}://${{window.location.host}}/ws`; | |
| const ws = new WebSocket(url); | |
| ws.onopen = () => {{ dom.status.innerText = "Receiving Data Stream..."; }}; | |
| ws.onclose = () => {{ dom.loader.style.display = 'flex'; dom.status.innerText = "Reconnecting..."; setTimeout(connect, 3000); }}; | |
| ws.onmessage = (event) => {{ | |
| const data = JSON.parse(event.data); | |
| if (data.error) return; | |
| dom.loader.style.display = 'none'; | |
| const cleanHistory = []; | |
| const seen = new Set(); | |
| data.history.forEach(d => {{ | |
| const t = Math.floor(d.t); | |
| if (!seen.has(t)) {{ seen.add(t); cleanHistory.push({{ time: t, value: d.p }}); }} | |
| }}); | |
| const predHistory = []; | |
| const seenP = new Set(); | |
| if(data.pred_history) {{ | |
| data.pred_history.forEach(d => {{ | |
| const t = Math.floor(d.t); | |
| if(!seenP.has(t)) {{ seenP.add(t); predHistory.push({{ time: t, value: d.p }}); }} | |
| }}); | |
| }} | |
| if (cleanHistory.length) {{ | |
| priceSeries.setData(cleanHistory); | |
| pastPredSeries.setData(predHistory); | |
| const last = cleanHistory[cleanHistory.length-1]; | |
| dom.price.innerText = last.value.toLocaleString(undefined, {{minimumFractionDigits: 2}}); | |
| if (data.analysis) {{ | |
| const {{ projected, net_score }} = data.analysis; | |
| futurePredSeries.setData([ | |
| last, | |
| {{ time: last.time + 60, value: projected }} | |
| ]); | |
| dom.projVal.innerText = projected.toLocaleString(undefined, {{minimumFractionDigits: 0, maximumFractionDigits: 0}}); | |
| dom.scoreVal.innerText = net_score.toFixed(2); | |
| dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red"; | |
| }} | |
| }} | |
| if (data.depth_x && data.depth_x.length) {{ | |
| const netData = []; | |
| const rawBids = [], rawAsks = []; | |
| for (let i = 0; i < data.depth_x.length; i++) {{ | |
| const x = data.depth_x[i]; | |
| const netY = data.depth_net[i]; | |
| const bidY = data.depth_bids[i]; | |
| const askY = data.depth_asks[i]; | |
| netData.push({{ time: x, value: netY }}); | |
| rawBids.push({{ time: x, value: bidY }}); | |
| rawAsks.push({{ time: x, value: askY }}); | |
| }} | |
| netSeries.setData(netData); | |
| rawBidSeries.setData(rawBids); | |
| rawAskSeries.setData(rawAsks); | |
| }} | |
| }}; | |
| }} | |
| connect(); | |
| }}); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| 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})") | |
| await ws.send(json.dumps({ | |
| "method": "subscribe", | |
| "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500} | |
| })) | |
| 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) | |
| 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"π AI Dashboard: http://localhost:{PORT}") | |
| await asyncio.Event().wait() | |
| if __name__ == "__main__": | |
| try: asyncio.run(main()) | |
| except KeyboardInterrupt: pass |