Update app.py
Browse files
app.py
CHANGED
|
@@ -2,34 +2,22 @@ import asyncio
|
|
| 2 |
import json
|
| 3 |
import logging
|
| 4 |
import time
|
| 5 |
-
import bisect
|
| 6 |
import math
|
| 7 |
-
import statistics
|
| 8 |
import aiohttp
|
| 9 |
from collections import deque
|
| 10 |
from aiohttp import web
|
| 11 |
import websockets
|
|
|
|
| 12 |
|
| 13 |
SYMBOL_KRAKEN = "BTC/USD"
|
| 14 |
PORT = 7860
|
| 15 |
HISTORY_LENGTH = 300
|
| 16 |
BROADCAST_RATE = 0.1
|
| 17 |
-
|
| 18 |
-
# --- MATH CONSTANTS ---
|
| 19 |
-
# DECAY for Micro-Price weights (focus on near-price liquidity)
|
| 20 |
MICROPRICE_DECAY = 0.05
|
| 21 |
-
# Lookback for VWAP and Volatility
|
| 22 |
-
WINDOW_SIZE = 60
|
| 23 |
|
| 24 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
|
| 25 |
|
| 26 |
-
# --- MATHEMATICAL HELPERS ---
|
| 27 |
-
|
| 28 |
class OnlineStats:
|
| 29 |
-
"""
|
| 30 |
-
Welford's Online Algorithm for calculating Mean and Variance
|
| 31 |
-
in a single pass (O(1) complexity).
|
| 32 |
-
"""
|
| 33 |
def __init__(self):
|
| 34 |
self.count = 0
|
| 35 |
self.mean = 0.0
|
|
@@ -52,17 +40,12 @@ class OnlineStats:
|
|
| 52 |
return math.sqrt(self.variance)
|
| 53 |
|
| 54 |
class KalmanVelocity:
|
| 55 |
-
"""
|
| 56 |
-
Kalman Filter specifically tuned for tracking Velocity (Trend)
|
| 57 |
-
rather than Position.
|
| 58 |
-
Model: Constant Velocity
|
| 59 |
-
"""
|
| 60 |
def __init__(self, R=0.001, Q=0.0001):
|
| 61 |
-
self.z = 0.0
|
| 62 |
-
self.v = 0.0
|
| 63 |
-
self.P = 1.0
|
| 64 |
-
self.R = R
|
| 65 |
-
self.Q = Q
|
| 66 |
self.last_ts = time.time()
|
| 67 |
|
| 68 |
def update(self, price):
|
|
@@ -71,48 +54,56 @@ class KalmanVelocity:
|
|
| 71 |
self.last_ts = now
|
| 72 |
if dt <= 0: return
|
| 73 |
|
| 74 |
-
# Predict
|
| 75 |
pred_z = self.z + self.v * dt
|
| 76 |
pred_v = self.v
|
| 77 |
p_cov = self.P + self.Q
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
K = p_cov / (p_cov + self.R) # Kalman Gain
|
| 82 |
|
| 83 |
self.z = pred_z + K * y
|
| 84 |
-
# Velocity update derived from residual
|
| 85 |
self.v = pred_v + (K / dt) * y
|
| 86 |
self.P = (1 - K) * p_cov
|
| 87 |
|
| 88 |
-
# --- STATE MANAGEMENT ---
|
| 89 |
-
|
| 90 |
market_state = {
|
| 91 |
"bids": {},
|
| 92 |
"asks": {},
|
| 93 |
"history": [],
|
| 94 |
-
"trade_history": deque(maxlen=2000),
|
| 95 |
"ohlc_history": [],
|
| 96 |
"current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
|
| 97 |
"current_mid": 0.0,
|
| 98 |
"ready": False,
|
| 99 |
"kalman": KalmanVelocity(),
|
| 100 |
-
"stats": OnlineStats(),
|
| 101 |
-
"
|
| 102 |
-
"vwap_denominator": 0.0
|
| 103 |
}
|
| 104 |
|
| 105 |
connected_clients = set()
|
| 106 |
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
def calculate_weighted_micro_price(mid_price):
|
| 110 |
-
|
| 111 |
-
Calculates the 'Micro-Price' (Stoikov).
|
| 112 |
-
The mid-price is adjusted by the imbalance of liquidity
|
| 113 |
-
weighted by distance from the mid.
|
| 114 |
-
"""
|
| 115 |
-
bids = sorted(market_state['bids'].items(), reverse=True)[:50] # Top 50 levels
|
| 116 |
asks = sorted(market_state['asks'].items())[:50]
|
| 117 |
|
| 118 |
if not bids or not asks: return mid_price
|
|
@@ -120,9 +111,7 @@ def calculate_weighted_micro_price(mid_price):
|
|
| 120 |
sum_wb = 0.0
|
| 121 |
sum_wa = 0.0
|
| 122 |
|
| 123 |
-
# Calculate Volume-Weighted Imbalance
|
| 124 |
for p, q in bids:
|
| 125 |
-
# Weight decays exponentially as distance from mid increases
|
| 126 |
distance = abs(mid_price - p)
|
| 127 |
weight = q * math.exp(-MICROPRICE_DECAY * distance)
|
| 128 |
sum_wb += weight
|
|
@@ -135,23 +124,16 @@ def calculate_weighted_micro_price(mid_price):
|
|
| 135 |
total_w = sum_wb + sum_wa
|
| 136 |
if total_w == 0: return mid_price
|
| 137 |
|
| 138 |
-
# Imbalance Ratio
|
| 139 |
imbalance = (sum_wb - sum_wa) / total_w
|
| 140 |
-
|
| 141 |
-
# Adjust spread based on imbalance
|
| 142 |
spread = asks[0][0] - bids[0][0]
|
| 143 |
-
|
| 144 |
-
# Formula: MicroPrice = Mid + (Imbalance * (Spread / 2))
|
| 145 |
micro_price = mid_price + (imbalance * (spread / 2))
|
| 146 |
return micro_price
|
| 147 |
|
| 148 |
def calculate_vwap_1m():
|
| 149 |
-
"""Calculates Volume Weighted Average Price over the last 60 seconds."""
|
| 150 |
cutoff = time.time() - 60
|
| 151 |
v_sum = 0.0
|
| 152 |
pv_sum = 0.0
|
| 153 |
|
| 154 |
-
# Iterate trades in reverse (newest first)
|
| 155 |
for trade in reversed(market_state['trade_history']):
|
| 156 |
if trade['t'] < cutoff: break
|
| 157 |
pv_sum += trade['p'] * trade['q']
|
|
@@ -160,13 +142,7 @@ def calculate_vwap_1m():
|
|
| 160 |
return pv_sum / v_sum if v_sum > 0 else market_state['current_mid']
|
| 161 |
|
| 162 |
def calculate_kyle_lambda(volatility, volume_window):
|
| 163 |
-
"""
|
| 164 |
-
Kyle's Lambda: A measure of market impact (Liquidity Cost).
|
| 165 |
-
Lambda ~ sigma / Volume
|
| 166 |
-
Quantifies how much price moves per $1 of order flow.
|
| 167 |
-
"""
|
| 168 |
if volume_window <= 0: return 0
|
| 169 |
-
# Scaling factor (heuristic normalization for BTC/USD typical volumes)
|
| 170 |
return (volatility * 1000) / (math.sqrt(volume_window) + 1)
|
| 171 |
|
| 172 |
def process_market_data():
|
|
@@ -175,19 +151,15 @@ def process_market_data():
|
|
| 175 |
mid = market_state['current_mid']
|
| 176 |
now = time.time()
|
| 177 |
|
| 178 |
-
# 1. Update Volatility Stats (Welford)
|
| 179 |
market_state['stats'].update(mid)
|
| 180 |
volatility = market_state['stats'].std_dev
|
| 181 |
-
if volatility == 0: volatility = 1.0
|
| 182 |
|
| 183 |
-
# 2. Update Kalman Filter (Velocity Trend)
|
| 184 |
market_state['kalman'].update(mid)
|
| 185 |
|
| 186 |
-
# 3. Calculate Micro-Structure Features
|
| 187 |
micro_price = calculate_weighted_micro_price(mid)
|
| 188 |
vwap = calculate_vwap_1m()
|
| 189 |
|
| 190 |
-
# 4. Calculate Order Flow Imbalance (OFI) - Last 10 seconds
|
| 191 |
ofi_buy = 0.0
|
| 192 |
ofi_sell = 0.0
|
| 193 |
ofi_window = 10.0
|
|
@@ -200,36 +172,19 @@ def process_market_data():
|
|
| 200 |
net_ofi = ofi_buy - ofi_sell
|
| 201 |
total_vol_10s = ofi_buy + ofi_sell
|
| 202 |
|
| 203 |
-
# 5. PREDICTION ENGINE (The Math Part)
|
| 204 |
-
|
| 205 |
-
# A. Kyle's Impact Term
|
| 206 |
-
# How much should the price move based on recent net buying/selling?
|
| 207 |
-
# Impact = Net_Volume * Lambda
|
| 208 |
k_lambda = calculate_kyle_lambda(volatility, total_vol_10s)
|
| 209 |
impact_term = net_ofi * k_lambda
|
| 210 |
|
| 211 |
-
|
| 212 |
-
# Prices tend to revert to VWAP in the short term.
|
| 213 |
-
# Pull = (VWAP - Price) * alpha
|
| 214 |
-
mean_reversion_alpha = 0.1 # Reversion strength coefficient
|
| 215 |
reversion_term = (vwap - mid) * mean_reversion_alpha
|
| 216 |
|
| 217 |
-
|
| 218 |
-
# The divergence between MicroPrice and MidPrice predicts immediate tick direction.
|
| 219 |
-
micro_alpha = (micro_price - mid) * 0.8 # 0.8 is a sensitivity weight
|
| 220 |
|
| 221 |
-
# D. Trend Term (Kalman)
|
| 222 |
-
# Project current velocity 60 seconds out
|
| 223 |
trend_term = market_state['kalman'].v * 60.0
|
| 224 |
|
| 225 |
-
# TOTAL PREDICTED CHANGE (Delta)
|
| 226 |
-
# We combine Market Impact (OFI), Micro-structure pressure (MP), Trend, and Mean Reversion.
|
| 227 |
predicted_delta = impact_term + micro_alpha + trend_term + reversion_term
|
| 228 |
-
|
| 229 |
pred_close = mid + predicted_delta
|
| 230 |
|
| 231 |
-
# Calculate Confidence Interval (2 Sigma) for the ghost candle
|
| 232 |
-
# Volatility scales with square root of time
|
| 233 |
sigma_1m = volatility * math.sqrt(60)
|
| 234 |
|
| 235 |
pred_candle = {
|
|
@@ -240,31 +195,17 @@ def process_market_data():
|
|
| 240 |
'low': min(mid, pred_close) - (2 * sigma_1m)
|
| 241 |
}
|
| 242 |
|
| 243 |
-
# Data management for charts
|
| 244 |
if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
|
| 245 |
market_state['history'].append({'t': now, 'p': mid})
|
| 246 |
if len(market_state['history']) > HISTORY_LENGTH:
|
| 247 |
market_state['history'].pop(0)
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
depth_x, depth_net, depth_bids, depth_asks = [], [], [], []
|
| 253 |
-
|
| 254 |
-
if bids and asks:
|
| 255 |
-
center_price = mid
|
| 256 |
-
for i in range(min(len(bids), len(asks))):
|
| 257 |
-
dist = (asks[i][0] - bids[i][0]) / 2
|
| 258 |
-
p_level = dist
|
| 259 |
-
depth_x.append(p_level)
|
| 260 |
-
depth_bids.append(bids[i][1])
|
| 261 |
-
depth_asks.append(asks[i][1])
|
| 262 |
-
depth_net.append(bids[i][1] - asks[i][1])
|
| 263 |
-
|
| 264 |
-
# Analysis Object for Frontend
|
| 265 |
analysis = {
|
| 266 |
"projected": pred_close,
|
| 267 |
-
"rho": (micro_price - mid),
|
| 268 |
"vwap": vwap,
|
| 269 |
"lambda": k_lambda
|
| 270 |
}
|
|
@@ -273,17 +214,11 @@ def process_market_data():
|
|
| 273 |
"mid": mid,
|
| 274 |
"history": market_state['history'],
|
| 275 |
"ohlc": market_state['ohlc_history'],
|
| 276 |
-
"trade_history": [], # Reduced payload, handled by client cumulative logic
|
| 277 |
"pred_candle": pred_candle,
|
| 278 |
-
"depth_x": depth_x,
|
| 279 |
-
"depth_net": depth_net,
|
| 280 |
-
"depth_bids": depth_bids,
|
| 281 |
-
"depth_asks": depth_asks,
|
| 282 |
"analysis": analysis,
|
| 283 |
-
"walls": {"bids":
|
| 284 |
}
|
| 285 |
|
| 286 |
-
# --- HTML FRONTEND (Unchanged visual structure, updated data mapping) ---
|
| 287 |
HTML_PAGE = f"""
|
| 288 |
<!DOCTYPE html>
|
| 289 |
<html lang="en">
|
|
@@ -323,7 +258,7 @@ HTML_PAGE = f"""
|
|
| 323 |
<div id="tv-price" style="flex: 1;"></div>
|
| 324 |
</div>
|
| 325 |
<div id="p-bottom" class="panel">
|
| 326 |
-
<div class="chart-header">1M KLINE + GHOST PREDICTION (PURPLE)</div>
|
| 327 |
<div id="tv-candles" style="flex: 1;"></div>
|
| 328 |
</div>
|
| 329 |
<div id="p-sidebar" class="panel">
|
|
@@ -361,7 +296,14 @@ HTML_PAGE = f"""
|
|
| 361 |
const candleSeries = candleChart.addCandlestickSeries({{ upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false }});
|
| 362 |
const ghostSeries = candleChart.addCandlestickSeries({{ upColor: 'rgba(213, 0, 249, 0.5)', downColor: 'rgba(213, 0, 249, 0.5)', borderVisible: true, borderColor: '#d500f9' }});
|
| 363 |
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
function connect() {{
|
| 367 |
const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
|
|
@@ -376,23 +318,20 @@ HTML_PAGE = f"""
|
|
| 376 |
dom.ticker.innerText = cleanHist[cleanHist.length-1].value.toFixed(2);
|
| 377 |
|
| 378 |
if(data.analysis) {{
|
| 379 |
-
// Plot Prediction Line
|
| 380 |
predSeries.setData([
|
| 381 |
cleanHist[cleanHist.length-1],
|
| 382 |
{{ time: cleanHist[cleanHist.length-1].time + 60, value: data.analysis.projected }}
|
| 383 |
]);
|
| 384 |
|
| 385 |
-
// Plot VWAP Line (simple point for now)
|
| 386 |
vwapSeries.setData([
|
| 387 |
{{ time: cleanHist[0].time, value: data.analysis.vwap }},
|
| 388 |
{{ time: cleanHist[cleanHist.length-1].time, value: data.analysis.vwap }}
|
| 389 |
]);
|
| 390 |
|
| 391 |
-
|
| 392 |
-
dom.lambdaVal.innerText = (data.analysis.lambda * 1000).toFixed(4); // Scaled for display
|
| 393 |
const vwapDiff = cleanHist[cleanHist.length-1].value - data.analysis.vwap;
|
| 394 |
dom.vwapDiv.innerText = vwapDiff.toFixed(2);
|
| 395 |
-
dom.vwapDiv.style.color = vwapDiff > 0 ? '#ff3b3b' : '#00ff9d';
|
| 396 |
}}
|
| 397 |
}}
|
| 398 |
|
|
@@ -400,6 +339,17 @@ HTML_PAGE = f"""
|
|
| 400 |
candleSeries.setData(data.ohlc.map(c => ({{ time: c.time, open: c.open, high: c.high, low: c.low, close: c.close }})));
|
| 401 |
}}
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
if (data.pred_candle) {{
|
| 404 |
ghostSeries.setData([data.pred_candle]);
|
| 405 |
const currentP = parseFloat(dom.ticker.innerText);
|
|
@@ -420,8 +370,6 @@ HTML_PAGE = f"""
|
|
| 420 |
|
| 421 |
async def kraken_worker():
|
| 422 |
global market_state
|
| 423 |
-
|
| 424 |
-
# Initial fetch for OHLC
|
| 425 |
try:
|
| 426 |
async with aiohttp.ClientSession() as session:
|
| 427 |
url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
|
|
@@ -429,7 +377,6 @@ async def kraken_worker():
|
|
| 429 |
if response.status == 200:
|
| 430 |
data = await response.json()
|
| 431 |
if 'result' in data:
|
| 432 |
-
# Extract OHLC
|
| 433 |
raw = list(data['result'].values())[0]
|
| 434 |
market_state['ohlc_history'] = [
|
| 435 |
{'time': int(c[0]), 'open': float(c[1]), 'high': float(c[2]), 'low': float(c[3]), 'close': float(c[4])}
|
|
@@ -441,7 +388,7 @@ async def kraken_worker():
|
|
| 441 |
while True:
|
| 442 |
try:
|
| 443 |
async with websockets.connect("wss://ws.kraken.com/v2") as ws:
|
| 444 |
-
logging.info(f"
|
| 445 |
|
| 446 |
await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 100}}))
|
| 447 |
await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}}))
|
|
@@ -459,7 +406,6 @@ async def kraken_worker():
|
|
| 459 |
for ask in item.get('asks', []):
|
| 460 |
market_state['asks'][float(ask['price'])] = float(ask['qty'])
|
| 461 |
|
| 462 |
-
# Cleanup zero qty
|
| 463 |
market_state['bids'] = {k: v for k, v in market_state['bids'].items() if v > 0}
|
| 464 |
market_state['asks'] = {k: v for k, v in market_state['asks'].items() if v > 0}
|
| 465 |
|
|
@@ -482,7 +428,6 @@ async def kraken_worker():
|
|
| 482 |
except: pass
|
| 483 |
|
| 484 |
elif channel == "ohlc":
|
| 485 |
-
# Update OHLC array
|
| 486 |
for c in data:
|
| 487 |
c_data = {
|
| 488 |
'time': int(float(c['endtime'])),
|
|
@@ -541,7 +486,7 @@ async def main():
|
|
| 541 |
await runner.setup()
|
| 542 |
site = web.TCPSite(runner, '0.0.0.0', PORT)
|
| 543 |
await site.start()
|
| 544 |
-
print(f"
|
| 545 |
await asyncio.Event().wait()
|
| 546 |
|
| 547 |
if __name__ == "__main__":
|
|
|
|
| 2 |
import json
|
| 3 |
import logging
|
| 4 |
import time
|
|
|
|
| 5 |
import math
|
|
|
|
| 6 |
import aiohttp
|
| 7 |
from collections import deque
|
| 8 |
from aiohttp import web
|
| 9 |
import websockets
|
| 10 |
+
import statistics
|
| 11 |
|
| 12 |
SYMBOL_KRAKEN = "BTC/USD"
|
| 13 |
PORT = 7860
|
| 14 |
HISTORY_LENGTH = 300
|
| 15 |
BROADCAST_RATE = 0.1
|
|
|
|
|
|
|
|
|
|
| 16 |
MICROPRICE_DECAY = 0.05
|
|
|
|
|
|
|
| 17 |
|
| 18 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
|
| 19 |
|
|
|
|
|
|
|
| 20 |
class OnlineStats:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
def __init__(self):
|
| 22 |
self.count = 0
|
| 23 |
self.mean = 0.0
|
|
|
|
| 40 |
return math.sqrt(self.variance)
|
| 41 |
|
| 42 |
class KalmanVelocity:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
def __init__(self, R=0.001, Q=0.0001):
|
| 44 |
+
self.z = 0.0
|
| 45 |
+
self.v = 0.0
|
| 46 |
+
self.P = 1.0
|
| 47 |
+
self.R = R
|
| 48 |
+
self.Q = Q
|
| 49 |
self.last_ts = time.time()
|
| 50 |
|
| 51 |
def update(self, price):
|
|
|
|
| 54 |
self.last_ts = now
|
| 55 |
if dt <= 0: return
|
| 56 |
|
|
|
|
| 57 |
pred_z = self.z + self.v * dt
|
| 58 |
pred_v = self.v
|
| 59 |
p_cov = self.P + self.Q
|
| 60 |
|
| 61 |
+
y = price - pred_z
|
| 62 |
+
K = p_cov / (p_cov + self.R)
|
|
|
|
| 63 |
|
| 64 |
self.z = pred_z + K * y
|
|
|
|
| 65 |
self.v = pred_v + (K / dt) * y
|
| 66 |
self.P = (1 - K) * p_cov
|
| 67 |
|
|
|
|
|
|
|
| 68 |
market_state = {
|
| 69 |
"bids": {},
|
| 70 |
"asks": {},
|
| 71 |
"history": [],
|
| 72 |
+
"trade_history": deque(maxlen=2000),
|
| 73 |
"ohlc_history": [],
|
| 74 |
"current_vol_window": {"buy": 0.0, "sell": 0.0, "start": time.time()},
|
| 75 |
"current_mid": 0.0,
|
| 76 |
"ready": False,
|
| 77 |
"kalman": KalmanVelocity(),
|
| 78 |
+
"stats": OnlineStats(),
|
| 79 |
+
"walls": {"bids": [], "asks": []}
|
|
|
|
| 80 |
}
|
| 81 |
|
| 82 |
connected_clients = set()
|
| 83 |
|
| 84 |
+
def detect_walls(order_book, side_name):
|
| 85 |
+
if not order_book: return []
|
| 86 |
+
|
| 87 |
+
sorted_book = sorted(order_book.items(), key=lambda x: x[0], reverse=(side_name == 'bids'))
|
| 88 |
+
relevant = sorted_book[:50]
|
| 89 |
+
|
| 90 |
+
volumes = [q for p, q in relevant]
|
| 91 |
+
if not volumes: return []
|
| 92 |
+
|
| 93 |
+
avg_vol = statistics.mean(volumes)
|
| 94 |
+
std_vol = statistics.stdev(volumes) if len(volumes) > 1 else 0
|
| 95 |
+
|
| 96 |
+
walls = []
|
| 97 |
+
for p, q in relevant:
|
| 98 |
+
if std_vol > 0:
|
| 99 |
+
z = (q - avg_vol) / std_vol
|
| 100 |
+
if z > 2.5:
|
| 101 |
+
walls.append({'p': p, 'q': q, 'z': z})
|
| 102 |
+
|
| 103 |
+
return walls[:3]
|
| 104 |
|
| 105 |
def calculate_weighted_micro_price(mid_price):
|
| 106 |
+
bids = sorted(market_state['bids'].items(), reverse=True)[:50]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
asks = sorted(market_state['asks'].items())[:50]
|
| 108 |
|
| 109 |
if not bids or not asks: return mid_price
|
|
|
|
| 111 |
sum_wb = 0.0
|
| 112 |
sum_wa = 0.0
|
| 113 |
|
|
|
|
| 114 |
for p, q in bids:
|
|
|
|
| 115 |
distance = abs(mid_price - p)
|
| 116 |
weight = q * math.exp(-MICROPRICE_DECAY * distance)
|
| 117 |
sum_wb += weight
|
|
|
|
| 124 |
total_w = sum_wb + sum_wa
|
| 125 |
if total_w == 0: return mid_price
|
| 126 |
|
|
|
|
| 127 |
imbalance = (sum_wb - sum_wa) / total_w
|
|
|
|
|
|
|
| 128 |
spread = asks[0][0] - bids[0][0]
|
|
|
|
|
|
|
| 129 |
micro_price = mid_price + (imbalance * (spread / 2))
|
| 130 |
return micro_price
|
| 131 |
|
| 132 |
def calculate_vwap_1m():
|
|
|
|
| 133 |
cutoff = time.time() - 60
|
| 134 |
v_sum = 0.0
|
| 135 |
pv_sum = 0.0
|
| 136 |
|
|
|
|
| 137 |
for trade in reversed(market_state['trade_history']):
|
| 138 |
if trade['t'] < cutoff: break
|
| 139 |
pv_sum += trade['p'] * trade['q']
|
|
|
|
| 142 |
return pv_sum / v_sum if v_sum > 0 else market_state['current_mid']
|
| 143 |
|
| 144 |
def calculate_kyle_lambda(volatility, volume_window):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
if volume_window <= 0: return 0
|
|
|
|
| 146 |
return (volatility * 1000) / (math.sqrt(volume_window) + 1)
|
| 147 |
|
| 148 |
def process_market_data():
|
|
|
|
| 151 |
mid = market_state['current_mid']
|
| 152 |
now = time.time()
|
| 153 |
|
|
|
|
| 154 |
market_state['stats'].update(mid)
|
| 155 |
volatility = market_state['stats'].std_dev
|
| 156 |
+
if volatility == 0: volatility = 1.0
|
| 157 |
|
|
|
|
| 158 |
market_state['kalman'].update(mid)
|
| 159 |
|
|
|
|
| 160 |
micro_price = calculate_weighted_micro_price(mid)
|
| 161 |
vwap = calculate_vwap_1m()
|
| 162 |
|
|
|
|
| 163 |
ofi_buy = 0.0
|
| 164 |
ofi_sell = 0.0
|
| 165 |
ofi_window = 10.0
|
|
|
|
| 172 |
net_ofi = ofi_buy - ofi_sell
|
| 173 |
total_vol_10s = ofi_buy + ofi_sell
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
k_lambda = calculate_kyle_lambda(volatility, total_vol_10s)
|
| 176 |
impact_term = net_ofi * k_lambda
|
| 177 |
|
| 178 |
+
mean_reversion_alpha = 0.1
|
|
|
|
|
|
|
|
|
|
| 179 |
reversion_term = (vwap - mid) * mean_reversion_alpha
|
| 180 |
|
| 181 |
+
micro_alpha = (micro_price - mid) * 0.8
|
|
|
|
|
|
|
| 182 |
|
|
|
|
|
|
|
| 183 |
trend_term = market_state['kalman'].v * 60.0
|
| 184 |
|
|
|
|
|
|
|
| 185 |
predicted_delta = impact_term + micro_alpha + trend_term + reversion_term
|
|
|
|
| 186 |
pred_close = mid + predicted_delta
|
| 187 |
|
|
|
|
|
|
|
| 188 |
sigma_1m = volatility * math.sqrt(60)
|
| 189 |
|
| 190 |
pred_candle = {
|
|
|
|
| 195 |
'low': min(mid, pred_close) - (2 * sigma_1m)
|
| 196 |
}
|
| 197 |
|
|
|
|
| 198 |
if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
|
| 199 |
market_state['history'].append({'t': now, 'p': mid})
|
| 200 |
if len(market_state['history']) > HISTORY_LENGTH:
|
| 201 |
market_state['history'].pop(0)
|
| 202 |
|
| 203 |
+
bid_walls = detect_walls(market_state['bids'], 'bids')
|
| 204 |
+
ask_walls = detect_walls(market_state['asks'], 'asks')
|
| 205 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
analysis = {
|
| 207 |
"projected": pred_close,
|
| 208 |
+
"rho": (micro_price - mid),
|
| 209 |
"vwap": vwap,
|
| 210 |
"lambda": k_lambda
|
| 211 |
}
|
|
|
|
| 214 |
"mid": mid,
|
| 215 |
"history": market_state['history'],
|
| 216 |
"ohlc": market_state['ohlc_history'],
|
|
|
|
| 217 |
"pred_candle": pred_candle,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
"analysis": analysis,
|
| 219 |
+
"walls": {"bids": bid_walls, "asks": ask_walls}
|
| 220 |
}
|
| 221 |
|
|
|
|
| 222 |
HTML_PAGE = f"""
|
| 223 |
<!DOCTYPE html>
|
| 224 |
<html lang="en">
|
|
|
|
| 258 |
<div id="tv-price" style="flex: 1;"></div>
|
| 259 |
</div>
|
| 260 |
<div id="p-bottom" class="panel">
|
| 261 |
+
<div class="chart-header">1M KLINE + WALLS + GHOST PREDICTION (PURPLE)</div>
|
| 262 |
<div id="tv-candles" style="flex: 1;"></div>
|
| 263 |
</div>
|
| 264 |
<div id="p-sidebar" class="panel">
|
|
|
|
| 296 |
const candleSeries = candleChart.addCandlestickSeries({{ upColor: '#00ff9d', downColor: '#ff3b3b', borderVisible: false }});
|
| 297 |
const ghostSeries = candleChart.addCandlestickSeries({{ upColor: 'rgba(213, 0, 249, 0.5)', downColor: 'rgba(213, 0, 249, 0.5)', borderVisible: true, borderColor: '#d500f9' }});
|
| 298 |
|
| 299 |
+
let activeWallLines = [];
|
| 300 |
+
|
| 301 |
+
new ResizeObserver(e => {{
|
| 302 |
+
const t1 = document.getElementById('tv-price');
|
| 303 |
+
const t2 = document.getElementById('tv-candles');
|
| 304 |
+
priceChart.applyOptions({{ width: t1.clientWidth, height: t1.clientHeight }});
|
| 305 |
+
candleChart.applyOptions({{ width: t2.clientWidth, height: t2.clientHeight }});
|
| 306 |
+
}}).observe(document.body);
|
| 307 |
|
| 308 |
function connect() {{
|
| 309 |
const ws = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/ws');
|
|
|
|
| 318 |
dom.ticker.innerText = cleanHist[cleanHist.length-1].value.toFixed(2);
|
| 319 |
|
| 320 |
if(data.analysis) {{
|
|
|
|
| 321 |
predSeries.setData([
|
| 322 |
cleanHist[cleanHist.length-1],
|
| 323 |
{{ time: cleanHist[cleanHist.length-1].time + 60, value: data.analysis.projected }}
|
| 324 |
]);
|
| 325 |
|
|
|
|
| 326 |
vwapSeries.setData([
|
| 327 |
{{ time: cleanHist[0].time, value: data.analysis.vwap }},
|
| 328 |
{{ time: cleanHist[cleanHist.length-1].time, value: data.analysis.vwap }}
|
| 329 |
]);
|
| 330 |
|
| 331 |
+
dom.lambdaVal.innerText = (data.analysis.lambda * 1000).toFixed(4);
|
|
|
|
| 332 |
const vwapDiff = cleanHist[cleanHist.length-1].value - data.analysis.vwap;
|
| 333 |
dom.vwapDiv.innerText = vwapDiff.toFixed(2);
|
| 334 |
+
dom.vwapDiv.style.color = vwapDiff > 0 ? '#ff3b3b' : '#00ff9d';
|
| 335 |
}}
|
| 336 |
}}
|
| 337 |
|
|
|
|
| 339 |
candleSeries.setData(data.ohlc.map(c => ({{ time: c.time, open: c.open, high: c.high, low: c.low, close: c.close }})));
|
| 340 |
}}
|
| 341 |
|
| 342 |
+
if (data.walls) {{
|
| 343 |
+
activeWallLines.forEach(l => candleSeries.removePriceLine(l));
|
| 344 |
+
activeWallLines = [];
|
| 345 |
+
data.walls.bids.forEach(w => {{
|
| 346 |
+
activeWallLines.push(candleSeries.createPriceLine({{ price: w.p, color: '#00ff9d', lineWidth: 2, lineStyle: 2, axisLabelVisible: true, title: 'BID WALL' }}));
|
| 347 |
+
}});
|
| 348 |
+
data.walls.asks.forEach(w => {{
|
| 349 |
+
activeWallLines.push(candleSeries.createPriceLine({{ price: w.p, color: '#ff3b3b', lineWidth: 2, lineStyle: 2, axisLabelVisible: true, title: 'ASK WALL' }}));
|
| 350 |
+
}});
|
| 351 |
+
}}
|
| 352 |
+
|
| 353 |
if (data.pred_candle) {{
|
| 354 |
ghostSeries.setData([data.pred_candle]);
|
| 355 |
const currentP = parseFloat(dom.ticker.innerText);
|
|
|
|
| 370 |
|
| 371 |
async def kraken_worker():
|
| 372 |
global market_state
|
|
|
|
|
|
|
| 373 |
try:
|
| 374 |
async with aiohttp.ClientSession() as session:
|
| 375 |
url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
|
|
|
|
| 377 |
if response.status == 200:
|
| 378 |
data = await response.json()
|
| 379 |
if 'result' in data:
|
|
|
|
| 380 |
raw = list(data['result'].values())[0]
|
| 381 |
market_state['ohlc_history'] = [
|
| 382 |
{'time': int(c[0]), 'open': float(c[1]), 'high': float(c[2]), 'low': float(c[3]), 'close': float(c[4])}
|
|
|
|
| 388 |
while True:
|
| 389 |
try:
|
| 390 |
async with websockets.connect("wss://ws.kraken.com/v2") as ws:
|
| 391 |
+
logging.info(f"Connected to Kraken ({SYMBOL_KRAKEN})")
|
| 392 |
|
| 393 |
await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 100}}))
|
| 394 |
await ws.send(json.dumps({"method": "subscribe", "params": {"channel": "trade", "symbol": [SYMBOL_KRAKEN]}}))
|
|
|
|
| 406 |
for ask in item.get('asks', []):
|
| 407 |
market_state['asks'][float(ask['price'])] = float(ask['qty'])
|
| 408 |
|
|
|
|
| 409 |
market_state['bids'] = {k: v for k, v in market_state['bids'].items() if v > 0}
|
| 410 |
market_state['asks'] = {k: v for k, v in market_state['asks'].items() if v > 0}
|
| 411 |
|
|
|
|
| 428 |
except: pass
|
| 429 |
|
| 430 |
elif channel == "ohlc":
|
|
|
|
| 431 |
for c in data:
|
| 432 |
c_data = {
|
| 433 |
'time': int(float(c['endtime'])),
|
|
|
|
| 486 |
await runner.setup()
|
| 487 |
site = web.TCPSite(runner, '0.0.0.0', PORT)
|
| 488 |
await site.start()
|
| 489 |
+
print(f"Quant Dashboard: http://localhost:{PORT}")
|
| 490 |
await asyncio.Event().wait()
|
| 491 |
|
| 492 |
if __name__ == "__main__":
|