Update app.py
Browse files
app.py
CHANGED
|
@@ -4,6 +4,7 @@ import logging
|
|
| 4 |
import time
|
| 5 |
import bisect
|
| 6 |
import math
|
|
|
|
| 7 |
from aiohttp import web
|
| 8 |
import websockets
|
| 9 |
|
|
@@ -14,6 +15,10 @@ BROADCAST_RATE = 0.1
|
|
| 14 |
DECAY_LAMBDA = 100.0
|
| 15 |
IMPACT_SENSITIVITY = 0.5
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
|
| 18 |
|
| 19 |
market_state = {
|
|
@@ -28,6 +33,49 @@ market_state = {
|
|
| 28 |
|
| 29 |
connected_clients = set()
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
def analyze_structure(diff_x, diff_y, current_mid):
|
| 32 |
if not diff_y or len(diff_y) < 5:
|
| 33 |
return None
|
|
@@ -65,8 +113,19 @@ def process_market_data():
|
|
| 65 |
|
| 66 |
mid = market_state['current_mid']
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
d_b_x, d_b_y, cum = [], [], 0
|
| 72 |
for p, q in raw_bids:
|
|
@@ -119,7 +178,11 @@ def process_market_data():
|
|
| 119 |
"depth_net": diff_y,
|
| 120 |
"depth_bids": chart_bids,
|
| 121 |
"depth_asks": chart_asks,
|
| 122 |
-
"analysis": analysis
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
|
| 125 |
HTML_PAGE = f"""
|
|
@@ -166,10 +229,13 @@ HTML_PAGE = f"""
|
|
| 166 |
.stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }}
|
| 167 |
.stat-label {{ font-size: 11px; color: #666; display: block; margin-bottom: 4px; }}
|
| 168 |
.stat-value {{ font-size: 24px; font-weight: bold; }}
|
|
|
|
| 169 |
.green {{ color: var(--accent-green); }}
|
| 170 |
.red {{ color: var(--accent-red); }}
|
| 171 |
|
| 172 |
#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); }}
|
|
|
|
|
|
|
| 173 |
</style>
|
| 174 |
</head>
|
| 175 |
<body>
|
|
@@ -181,7 +247,7 @@ HTML_PAGE = f"""
|
|
| 181 |
|
| 182 |
<div class="grid-container">
|
| 183 |
<div id="p-price" class="panel">
|
| 184 |
-
<div class="panel-header"><span>BTC/USD Price
|
| 185 |
<div id="tv-price"></div>
|
| 186 |
</div>
|
| 187 |
|
|
@@ -207,6 +273,11 @@ HTML_PAGE = f"""
|
|
| 207 |
<span class="stat-label" style="color:var(--accent-green);">IMPACT PROJECTION</span>
|
| 208 |
<span id="proj-val" class="stat-value">---</span>
|
| 209 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
</div>
|
| 211 |
</div>
|
| 212 |
</div>
|
|
@@ -218,7 +289,8 @@ HTML_PAGE = f"""
|
|
| 218 |
status: document.getElementById('loading-status'),
|
| 219 |
price: document.getElementById('live-price'),
|
| 220 |
scoreVal: document.getElementById('score-val'),
|
| 221 |
-
projVal: document.getElementById('proj-val')
|
|
|
|
| 222 |
}};
|
| 223 |
|
| 224 |
const chartCommon = {{
|
|
@@ -265,6 +337,9 @@ HTML_PAGE = f"""
|
|
| 265 |
lineWidth: 2
|
| 266 |
}});
|
| 267 |
|
|
|
|
|
|
|
|
|
|
| 268 |
const resizeObserver = new ResizeObserver(entries => {{
|
| 269 |
for(let entry of entries) {{
|
| 270 |
const {{width, height}} = entry.contentRect;
|
|
@@ -288,6 +363,7 @@ HTML_PAGE = f"""
|
|
| 288 |
if (data.error) return;
|
| 289 |
dom.loader.style.display = 'none';
|
| 290 |
|
|
|
|
| 291 |
const cleanHistory = [];
|
| 292 |
const seen = new Set();
|
| 293 |
data.history.forEach(d => {{
|
|
@@ -326,6 +402,46 @@ HTML_PAGE = f"""
|
|
| 326 |
}}
|
| 327 |
}}
|
| 328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
if (data.depth_x && data.depth_x.length) {{
|
| 330 |
const netData = [];
|
| 331 |
const rawBids = [], rawAsks = [];
|
|
|
|
| 4 |
import time
|
| 5 |
import bisect
|
| 6 |
import math
|
| 7 |
+
import statistics
|
| 8 |
from aiohttp import web
|
| 9 |
import websockets
|
| 10 |
|
|
|
|
| 15 |
DECAY_LAMBDA = 100.0
|
| 16 |
IMPACT_SENSITIVITY = 0.5
|
| 17 |
|
| 18 |
+
# Wall Detection Parameters
|
| 19 |
+
Z_SCORE_THRESHOLD = 3.0 # Statistical significance threshold (3 sigma)
|
| 20 |
+
WALL_LOOKBACK = 200 # How many order book levels to scan
|
| 21 |
+
|
| 22 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
|
| 23 |
|
| 24 |
market_state = {
|
|
|
|
| 33 |
|
| 34 |
connected_clients = set()
|
| 35 |
|
| 36 |
+
def detect_anomalies(orders, scan_depth):
|
| 37 |
+
"""
|
| 38 |
+
Mathematically detects liquidity walls using Z-Score outlier detection.
|
| 39 |
+
Returns the top significant walls.
|
| 40 |
+
"""
|
| 41 |
+
if len(orders) < 10:
|
| 42 |
+
return []
|
| 43 |
+
|
| 44 |
+
# Get volumes for the scan depth
|
| 45 |
+
relevant_orders = orders[:scan_depth]
|
| 46 |
+
volumes = [q for p, q in relevant_orders]
|
| 47 |
+
|
| 48 |
+
if not volumes:
|
| 49 |
+
return []
|
| 50 |
+
|
| 51 |
+
# Calculate Distribution Statistics
|
| 52 |
+
try:
|
| 53 |
+
avg_vol = statistics.mean(volumes)
|
| 54 |
+
stdev_vol = statistics.stdev(volumes)
|
| 55 |
+
except statistics.StatisticsError:
|
| 56 |
+
return []
|
| 57 |
+
|
| 58 |
+
if stdev_vol == 0:
|
| 59 |
+
return []
|
| 60 |
+
|
| 61 |
+
walls = []
|
| 62 |
+
|
| 63 |
+
for price, qty in relevant_orders:
|
| 64 |
+
# Calculate Z-Score
|
| 65 |
+
z_score = (qty - avg_vol) / stdev_vol
|
| 66 |
+
|
| 67 |
+
# If Z-Score > Threshold, it is a mathematically significant wall
|
| 68 |
+
if z_score > Z_SCORE_THRESHOLD:
|
| 69 |
+
walls.append({
|
| 70 |
+
"price": price,
|
| 71 |
+
"vol": qty,
|
| 72 |
+
"z_score": z_score
|
| 73 |
+
})
|
| 74 |
+
|
| 75 |
+
# Sort by Z-Score descending (strongest walls first) and take top 3
|
| 76 |
+
walls.sort(key=lambda x: x['z_score'], reverse=True)
|
| 77 |
+
return walls[:3]
|
| 78 |
+
|
| 79 |
def analyze_structure(diff_x, diff_y, current_mid):
|
| 80 |
if not diff_y or len(diff_y) < 5:
|
| 81 |
return None
|
|
|
|
| 113 |
|
| 114 |
mid = market_state['current_mid']
|
| 115 |
|
| 116 |
+
# Sort orders for processing
|
| 117 |
+
# Bids: High to Low
|
| 118 |
+
sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
|
| 119 |
+
# Asks: Low to High
|
| 120 |
+
sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
|
| 121 |
+
|
| 122 |
+
raw_bids = sorted_bids[:300]
|
| 123 |
+
raw_asks = sorted_asks[:300]
|
| 124 |
+
|
| 125 |
+
# --- WALL DETECTION LOGIC ---
|
| 126 |
+
bid_walls = detect_anomalies(sorted_bids, WALL_LOOKBACK)
|
| 127 |
+
ask_walls = detect_anomalies(sorted_asks, WALL_LOOKBACK)
|
| 128 |
+
# ----------------------------
|
| 129 |
|
| 130 |
d_b_x, d_b_y, cum = [], [], 0
|
| 131 |
for p, q in raw_bids:
|
|
|
|
| 178 |
"depth_net": diff_y,
|
| 179 |
"depth_bids": chart_bids,
|
| 180 |
"depth_asks": chart_asks,
|
| 181 |
+
"analysis": analysis,
|
| 182 |
+
"walls": {
|
| 183 |
+
"bids": bid_walls,
|
| 184 |
+
"asks": ask_walls
|
| 185 |
+
}
|
| 186 |
}
|
| 187 |
|
| 188 |
HTML_PAGE = f"""
|
|
|
|
| 229 |
.stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }}
|
| 230 |
.stat-label {{ font-size: 11px; color: #666; display: block; margin-bottom: 4px; }}
|
| 231 |
.stat-value {{ font-size: 24px; font-weight: bold; }}
|
| 232 |
+
.stat-sub {{ font-size: 10px; color: #888; margin-top: 5px; }}
|
| 233 |
.green {{ color: var(--accent-green); }}
|
| 234 |
.red {{ color: var(--accent-red); }}
|
| 235 |
|
| 236 |
#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); }}
|
| 237 |
+
|
| 238 |
+
.wall-item {{ display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 4px; padding: 4px; background: rgba(0,0,0,0.2); }}
|
| 239 |
</style>
|
| 240 |
</head>
|
| 241 |
<body>
|
|
|
|
| 247 |
|
| 248 |
<div class="grid-container">
|
| 249 |
<div id="p-price" class="panel">
|
| 250 |
+
<div class="panel-header"><span>BTC/USD Price + Liquidity Walls</span><span id="live-price">---</span></div>
|
| 251 |
<div id="tv-price"></div>
|
| 252 |
</div>
|
| 253 |
|
|
|
|
| 273 |
<span class="stat-label" style="color:var(--accent-green);">IMPACT PROJECTION</span>
|
| 274 |
<span id="proj-val" class="stat-value">---</span>
|
| 275 |
</div>
|
| 276 |
+
|
| 277 |
+
<div class="stat-box">
|
| 278 |
+
<span class="stat-label">DETECTED WALLS (Z-SCORE > 3)</span>
|
| 279 |
+
<div id="wall-list">Waiting for data...</div>
|
| 280 |
+
</div>
|
| 281 |
</div>
|
| 282 |
</div>
|
| 283 |
</div>
|
|
|
|
| 289 |
status: document.getElementById('loading-status'),
|
| 290 |
price: document.getElementById('live-price'),
|
| 291 |
scoreVal: document.getElementById('score-val'),
|
| 292 |
+
projVal: document.getElementById('proj-val'),
|
| 293 |
+
wallList: document.getElementById('wall-list')
|
| 294 |
}};
|
| 295 |
|
| 296 |
const chartCommon = {{
|
|
|
|
| 337 |
lineWidth: 2
|
| 338 |
}});
|
| 339 |
|
| 340 |
+
// Refs for Price Lines (Walls)
|
| 341 |
+
let activePriceLines = [];
|
| 342 |
+
|
| 343 |
const resizeObserver = new ResizeObserver(entries => {{
|
| 344 |
for(let entry of entries) {{
|
| 345 |
const {{width, height}} = entry.contentRect;
|
|
|
|
| 363 |
if (data.error) return;
|
| 364 |
dom.loader.style.display = 'none';
|
| 365 |
|
| 366 |
+
// --- History & Prediction Plotting ---
|
| 367 |
const cleanHistory = [];
|
| 368 |
const seen = new Set();
|
| 369 |
data.history.forEach(d => {{
|
|
|
|
| 402 |
}}
|
| 403 |
}}
|
| 404 |
|
| 405 |
+
// --- Wall Visualization ---
|
| 406 |
+
if (data.walls) {{
|
| 407 |
+
// Clear old lines
|
| 408 |
+
activePriceLines.forEach(line => priceSeries.removePriceLine(line));
|
| 409 |
+
activePriceLines = [];
|
| 410 |
+
|
| 411 |
+
let wallHtml = "";
|
| 412 |
+
|
| 413 |
+
// Draw Bid Walls
|
| 414 |
+
data.walls.bids.forEach(w => {{
|
| 415 |
+
const line = priceSeries.createPriceLine({{
|
| 416 |
+
price: w.price,
|
| 417 |
+
color: 'rgba(0, 230, 118, 0.8)',
|
| 418 |
+
lineWidth: 1,
|
| 419 |
+
lineStyle: 2,
|
| 420 |
+
axisLabelVisible: true,
|
| 421 |
+
title: `BUY WALL (Z: ${{w.z_score.toFixed(1)}})`,
|
| 422 |
+
}});
|
| 423 |
+
activePriceLines.push(line);
|
| 424 |
+
wallHtml += `<div class="wall-item"><span class="green">BUY ${{w.price}}</span><span>Z: ${{w.z_score.toFixed(1)}}</span></div>`;
|
| 425 |
+
}});
|
| 426 |
+
|
| 427 |
+
// Draw Ask Walls
|
| 428 |
+
data.walls.asks.forEach(w => {{
|
| 429 |
+
const line = priceSeries.createPriceLine({{
|
| 430 |
+
price: w.price,
|
| 431 |
+
color: 'rgba(255, 23, 68, 0.8)',
|
| 432 |
+
lineWidth: 1,
|
| 433 |
+
lineStyle: 2,
|
| 434 |
+
axisLabelVisible: true,
|
| 435 |
+
title: `SELL WALL (Z: ${{w.z_score.toFixed(1)}})`,
|
| 436 |
+
}});
|
| 437 |
+
activePriceLines.push(line);
|
| 438 |
+
wallHtml += `<div class="wall-item"><span class="red">SELL ${{w.price}}</span><span>Z: ${{w.z_score.toFixed(1)}}</span></div>`;
|
| 439 |
+
}});
|
| 440 |
+
|
| 441 |
+
dom.wallList.innerHTML = wallHtml || "No significant walls.";
|
| 442 |
+
}}
|
| 443 |
+
|
| 444 |
+
// --- Depth Charts ---
|
| 445 |
if (data.depth_x && data.depth_x.length) {{
|
| 446 |
const netData = [];
|
| 447 |
const rawBids = [], rawAsks = [];
|