Update app.py
Browse files
app.py
CHANGED
|
@@ -58,7 +58,7 @@ def analyze_structure(diff_x, diff_y, current_mid):
|
|
| 58 |
"net_score": net_total
|
| 59 |
}
|
| 60 |
|
| 61 |
-
# --- HTML Frontend
|
| 62 |
HTML_PAGE = f"""
|
| 63 |
<!DOCTYPE html>
|
| 64 |
<html lang="en">
|
|
@@ -66,9 +66,8 @@ HTML_PAGE = f"""
|
|
| 66 |
<meta charset="UTF-8">
|
| 67 |
<title>AI Liquidity Dashboard | {SYMBOL_KRAKEN}</title>
|
| 68 |
|
| 69 |
-
<!--
|
| 70 |
<script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
|
| 71 |
-
<script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
|
| 72 |
|
| 73 |
<style>
|
| 74 |
:root {{
|
|
@@ -90,7 +89,8 @@ HTML_PAGE = f"""
|
|
| 90 |
|
| 91 |
.panel-header {{ padding: 8px 12px; background: #0f1116; border-bottom: 1px solid var(--border); font-size: 12px; font-weight: bold; display: flex; justify-content: space-between; color: var(--accent-green); }}
|
| 92 |
|
| 93 |
-
#tv-
|
|
|
|
| 94 |
.stats-content {{ padding: 15px; overflow-y: auto; flex: 1; }}
|
| 95 |
.stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }}
|
| 96 |
.stat-label {{ font-size: 11px; color: #666; display: block; margin-bottom: 4px; }}
|
|
@@ -120,14 +120,19 @@ HTML_PAGE = f"""
|
|
| 120 |
</div>
|
| 121 |
|
| 122 |
<div class="grid-container">
|
|
|
|
| 123 |
<div id="p-price" class="panel">
|
| 124 |
<div class="panel-header"><span>BTC/USD Price Action</span><span id="live-price">---</span></div>
|
| 125 |
-
<div id="tv-
|
| 126 |
</div>
|
|
|
|
|
|
|
| 127 |
<div id="p-depth" class="panel">
|
| 128 |
-
<div class="panel-header"><span>Liquidity
|
| 129 |
-
<div id="depth
|
| 130 |
</div>
|
|
|
|
|
|
|
| 131 |
<div id="p-stats" class="panel">
|
| 132 |
<div class="panel-header">ANALYTICS ENGINE</div>
|
| 133 |
<div class="stats-content">
|
|
@@ -154,7 +159,7 @@ HTML_PAGE = f"""
|
|
| 154 |
</div>
|
| 155 |
|
| 156 |
<script>
|
| 157 |
-
//
|
| 158 |
window.onerror = function(msg, url, lineNo, columnNo, error) {{
|
| 159 |
const errDiv = document.getElementById('error-log');
|
| 160 |
errDiv.innerHTML += `ERROR: ${{msg}} (Line: ${{lineNo}})<br>`;
|
|
@@ -176,40 +181,64 @@ HTML_PAGE = f"""
|
|
| 176 |
logs: document.getElementById('term-logs')
|
| 177 |
}};
|
| 178 |
|
| 179 |
-
// ---
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
const chartContainer = document.getElementById('tv-chart');
|
| 183 |
-
const chart = LightweightCharts.createChart(chartContainer, {{
|
| 184 |
layout: {{ background: {{ type: 'solid', color: '#12141a' }}, textColor: '#888' }},
|
| 185 |
grid: {{ vertLines: {{ color: '#1f2833' }}, horzLines: {{ color: '#1f2833' }} }},
|
| 186 |
-
timeScale: {{ timeVisible: true, secondsVisible: true, borderColor: '#2d3842' }},
|
| 187 |
rightPriceScale: {{ borderColor: '#2d3842' }},
|
| 188 |
-
|
|
|
|
| 189 |
|
| 190 |
-
|
| 191 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
// Resize Logic
|
| 197 |
new ResizeObserver(entries => {{
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
}}, {{ responsive: true, displayModeBar: false }});
|
| 211 |
-
|
| 212 |
-
// --- 3. HELPERS ---
|
| 213 |
function log(msg, type='neutral') {{
|
| 214 |
const div = document.createElement('div');
|
| 215 |
div.className = 'log-line';
|
|
@@ -219,20 +248,19 @@ HTML_PAGE = f"""
|
|
| 219 |
if (dom.logs.children.length > 20) dom.logs.removeChild(dom.logs.lastChild);
|
| 220 |
}}
|
| 221 |
|
| 222 |
-
// ---
|
| 223 |
async function fetchData() {{
|
| 224 |
try {{
|
| 225 |
const res = await fetch('/data?t=' + Date.now());
|
| 226 |
const data = await res.json();
|
| 227 |
|
| 228 |
if (data.error) {{
|
| 229 |
-
dom.status.innerText = "
|
| 230 |
return;
|
| 231 |
}}
|
| 232 |
-
|
| 233 |
dom.loader.style.display = 'none';
|
| 234 |
|
| 235 |
-
//
|
| 236 |
const uniqueHistory = [];
|
| 237 |
const seen = new Set();
|
| 238 |
data.history.forEach(d => {{
|
|
@@ -241,38 +269,37 @@ HTML_PAGE = f"""
|
|
| 241 |
}});
|
| 242 |
|
| 243 |
if (uniqueHistory.length > 0) {{
|
| 244 |
-
|
| 245 |
const last = uniqueHistory[uniqueHistory.length - 1];
|
| 246 |
-
|
| 247 |
dom.price.innerText = last.value.toLocaleString(undefined, {{minimumFractionDigits: 2}});
|
| 248 |
|
| 249 |
if (data.analysis) {{
|
| 250 |
const {{ projected, support, resistance, net_score }} = data.analysis;
|
| 251 |
|
| 252 |
-
//
|
| 253 |
predSeries.setData([last, {{ time: last.time + 60, value: projected }}]);
|
| 254 |
dom.projVal.innerText = projected.toFixed(0);
|
| 255 |
|
| 256 |
-
// S/R Lines
|
| 257 |
if (support) {{
|
| 258 |
dom.supVal.innerText = support.toFixed(0);
|
| 259 |
-
if (!supportLine) supportLine =
|
| 260 |
else supportLine.applyOptions({{ price: support }});
|
| 261 |
}} else {{
|
| 262 |
dom.supVal.innerText = '---';
|
| 263 |
-
if (supportLine) {{
|
| 264 |
}}
|
| 265 |
|
| 266 |
if (resistance) {{
|
| 267 |
dom.resVal.innerText = resistance.toFixed(0);
|
| 268 |
-
if (!resistanceLine) resistanceLine =
|
| 269 |
else resistanceLine.applyOptions({{ price: resistance }});
|
| 270 |
}} else {{
|
| 271 |
dom.resVal.innerText = '---';
|
| 272 |
-
if (resistanceLine) {{
|
| 273 |
}}
|
| 274 |
|
| 275 |
-
//
|
| 276 |
dom.scoreVal.innerText = net_score.toFixed(1);
|
| 277 |
dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red";
|
| 278 |
let barW = Math.min(Math.abs(net_score)*2, 50);
|
|
@@ -280,29 +307,46 @@ HTML_PAGE = f"""
|
|
| 280 |
dom.scoreBar.style.left = net_score > 0 ? '50%' : (50 - barW) + '%';
|
| 281 |
dom.scoreBar.style.background = net_score > 0 ? '#66fcf1' : '#ff3b3b';
|
| 282 |
|
| 283 |
-
// Random Logs
|
| 284 |
if (Math.random() > 0.98) {{
|
| 285 |
-
if (net_score >
|
| 286 |
-
else if (net_score < -
|
| 287 |
}}
|
| 288 |
}}
|
| 289 |
}}
|
| 290 |
|
| 291 |
-
//
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
}} catch (e) {{
|
| 302 |
-
console.error(
|
| 303 |
}}
|
| 304 |
}}
|
| 305 |
-
|
| 306 |
setInterval(fetchData, 1000);
|
| 307 |
}});
|
| 308 |
</script>
|
|
@@ -346,8 +390,7 @@ async def kraken_worker():
|
|
| 346 |
market_state['ready'] = True
|
| 347 |
|
| 348 |
now = time.time()
|
| 349 |
-
|
| 350 |
-
if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.25):
|
| 351 |
market_state['history'].append({'t': now, 'p': mid})
|
| 352 |
if len(market_state['history']) > HISTORY_LENGTH:
|
| 353 |
market_state['history'].pop(0)
|
|
@@ -365,7 +408,6 @@ async def handle_data(request):
|
|
| 365 |
|
| 366 |
mid = market_state['current_mid']
|
| 367 |
|
| 368 |
-
# Snapshot
|
| 369 |
raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
|
| 370 |
raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
|
| 371 |
|
|
|
|
| 58 |
"net_score": net_total
|
| 59 |
}
|
| 60 |
|
| 61 |
+
# --- HTML Frontend ---
|
| 62 |
HTML_PAGE = f"""
|
| 63 |
<!DOCTYPE html>
|
| 64 |
<html lang="en">
|
|
|
|
| 66 |
<meta charset="UTF-8">
|
| 67 |
<title>AI Liquidity Dashboard | {SYMBOL_KRAKEN}</title>
|
| 68 |
|
| 69 |
+
<!-- Pinned Version for Stability -->
|
| 70 |
<script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
|
|
|
|
| 71 |
|
| 72 |
<style>
|
| 73 |
:root {{
|
|
|
|
| 89 |
|
| 90 |
.panel-header {{ padding: 8px 12px; background: #0f1116; border-bottom: 1px solid var(--border); font-size: 12px; font-weight: bold; display: flex; justify-content: space-between; color: var(--accent-green); }}
|
| 91 |
|
| 92 |
+
#tv-price, #tv-depth {{ flex: 1; width: 100%; position: relative; }}
|
| 93 |
+
|
| 94 |
.stats-content {{ padding: 15px; overflow-y: auto; flex: 1; }}
|
| 95 |
.stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }}
|
| 96 |
.stat-label {{ font-size: 11px; color: #666; display: block; margin-bottom: 4px; }}
|
|
|
|
| 120 |
</div>
|
| 121 |
|
| 122 |
<div class="grid-container">
|
| 123 |
+
<!-- Price Chart Panel -->
|
| 124 |
<div id="p-price" class="panel">
|
| 125 |
<div class="panel-header"><span>BTC/USD Price Action</span><span id="live-price">---</span></div>
|
| 126 |
+
<div id="tv-price"></div>
|
| 127 |
</div>
|
| 128 |
+
|
| 129 |
+
<!-- Depth Chart Panel (Now using Lightweight Charts) -->
|
| 130 |
<div id="p-depth" class="panel">
|
| 131 |
+
<div class="panel-header"><span>Net Liquidity (Green=Bids, Red=Asks)</span><span>DEPTH 300</span></div>
|
| 132 |
+
<div id="tv-depth"></div>
|
| 133 |
</div>
|
| 134 |
+
|
| 135 |
+
<!-- Analytics Panel -->
|
| 136 |
<div id="p-stats" class="panel">
|
| 137 |
<div class="panel-header">ANALYTICS ENGINE</div>
|
| 138 |
<div class="stats-content">
|
|
|
|
| 159 |
</div>
|
| 160 |
|
| 161 |
<script>
|
| 162 |
+
// --- ERROR HANDLING ---
|
| 163 |
window.onerror = function(msg, url, lineNo, columnNo, error) {{
|
| 164 |
const errDiv = document.getElementById('error-log');
|
| 165 |
errDiv.innerHTML += `ERROR: ${{msg}} (Line: ${{lineNo}})<br>`;
|
|
|
|
| 181 |
logs: document.getElementById('term-logs')
|
| 182 |
}};
|
| 183 |
|
| 184 |
+
// --- CHART CONFIGURATION ---
|
| 185 |
+
const chartOptionsCommon = {{
|
|
|
|
|
|
|
|
|
|
| 186 |
layout: {{ background: {{ type: 'solid', color: '#12141a' }}, textColor: '#888' }},
|
| 187 |
grid: {{ vertLines: {{ color: '#1f2833' }}, horzLines: {{ color: '#1f2833' }} }},
|
|
|
|
| 188 |
rightPriceScale: {{ borderColor: '#2d3842' }},
|
| 189 |
+
timeScale: {{ borderColor: '#2d3842' }},
|
| 190 |
+
}};
|
| 191 |
|
| 192 |
+
// 1. PRICE CHART
|
| 193 |
+
const priceContainer = document.getElementById('tv-price');
|
| 194 |
+
const priceChart = LightweightCharts.createChart(priceContainer, {{
|
| 195 |
+
...chartOptionsCommon,
|
| 196 |
+
timeScale: {{ timeVisible: true, secondsVisible: true }},
|
| 197 |
+
}});
|
| 198 |
+
const priceSeries = priceChart.addLineSeries({{ color: '#2962FF', lineWidth: 2 }});
|
| 199 |
+
const predSeries = priceChart.addLineSeries({{ color: '#ff9800', lineWidth: 2, lineStyle: 2 }});
|
| 200 |
+
let supportLine = null, resistanceLine = null;
|
| 201 |
+
|
| 202 |
+
// 2. DEPTH CHART (Hacked for Non-Time Series)
|
| 203 |
+
const depthContainer = document.getElementById('tv-depth');
|
| 204 |
+
const depthChart = LightweightCharts.createChart(depthContainer, {{
|
| 205 |
+
...chartOptionsCommon,
|
| 206 |
+
timeScale: {{
|
| 207 |
+
// Override formatted date to just show raw distance
|
| 208 |
+
tickMarkFormatter: (time) => parseFloat(time).toFixed(0),
|
| 209 |
+
}},
|
| 210 |
+
localization: {{
|
| 211 |
+
// Override tooltip formatter
|
| 212 |
+
timeFormatter: (time) => 'Dist: $' + parseFloat(time).toFixed(2),
|
| 213 |
+
}}
|
| 214 |
+
}});
|
| 215 |
|
| 216 |
+
// We use two Area series: one for Positive (Green), one for Negative (Red)
|
| 217 |
+
const bullSeries = depthChart.addAreaSeries({{
|
| 218 |
+
topColor: 'rgba(102, 252, 241, 0.4)',
|
| 219 |
+
bottomColor: 'rgba(102, 252, 241, 0.0)',
|
| 220 |
+
lineColor: '#66fcf1', lineWidth: 2
|
| 221 |
+
}});
|
| 222 |
+
const bearSeries = depthChart.addAreaSeries({{
|
| 223 |
+
topColor: 'rgba(255, 59, 59, 0.4)',
|
| 224 |
+
bottomColor: 'rgba(255, 59, 59, 0.0)',
|
| 225 |
+
lineColor: '#ff3b3b', lineWidth: 2
|
| 226 |
+
}});
|
| 227 |
|
| 228 |
// Resize Logic
|
| 229 |
new ResizeObserver(entries => {{
|
| 230 |
+
for (let entry of entries) {{
|
| 231 |
+
if (entry.target === priceContainer) priceChart.applyOptions({{ width: entry.contentRect.width, height: entry.contentRect.height }});
|
| 232 |
+
if (entry.target === depthContainer) depthChart.applyOptions({{ width: entry.contentRect.width, height: entry.contentRect.height }});
|
| 233 |
+
}}
|
| 234 |
+
}}).observe(priceContainer);
|
| 235 |
+
new ResizeObserver(entries => {{
|
| 236 |
+
for (let entry of entries) {{
|
| 237 |
+
if (entry.target === depthContainer) depthChart.applyOptions({{ width: entry.contentRect.width, height: entry.contentRect.height }});
|
| 238 |
+
}}
|
| 239 |
+
}}).observe(depthContainer);
|
| 240 |
+
|
| 241 |
+
// --- HELPERS ---
|
|
|
|
|
|
|
|
|
|
| 242 |
function log(msg, type='neutral') {{
|
| 243 |
const div = document.createElement('div');
|
| 244 |
div.className = 'log-line';
|
|
|
|
| 248 |
if (dom.logs.children.length > 20) dom.logs.removeChild(dom.logs.lastChild);
|
| 249 |
}}
|
| 250 |
|
| 251 |
+
// --- DATA LOOP ---
|
| 252 |
async function fetchData() {{
|
| 253 |
try {{
|
| 254 |
const res = await fetch('/data?t=' + Date.now());
|
| 255 |
const data = await res.json();
|
| 256 |
|
| 257 |
if (data.error) {{
|
| 258 |
+
dom.status.innerText = "Synchronizing Market State...";
|
| 259 |
return;
|
| 260 |
}}
|
|
|
|
| 261 |
dom.loader.style.display = 'none';
|
| 262 |
|
| 263 |
+
// -- UPDATE PRICE CHART --
|
| 264 |
const uniqueHistory = [];
|
| 265 |
const seen = new Set();
|
| 266 |
data.history.forEach(d => {{
|
|
|
|
| 269 |
}});
|
| 270 |
|
| 271 |
if (uniqueHistory.length > 0) {{
|
| 272 |
+
priceSeries.setData(uniqueHistory);
|
| 273 |
const last = uniqueHistory[uniqueHistory.length - 1];
|
|
|
|
| 274 |
dom.price.innerText = last.value.toLocaleString(undefined, {{minimumFractionDigits: 2}});
|
| 275 |
|
| 276 |
if (data.analysis) {{
|
| 277 |
const {{ projected, support, resistance, net_score }} = data.analysis;
|
| 278 |
|
| 279 |
+
// Projection
|
| 280 |
predSeries.setData([last, {{ time: last.time + 60, value: projected }}]);
|
| 281 |
dom.projVal.innerText = projected.toFixed(0);
|
| 282 |
|
| 283 |
+
// S/R Lines (Using createPriceLine)
|
| 284 |
if (support) {{
|
| 285 |
dom.supVal.innerText = support.toFixed(0);
|
| 286 |
+
if (!supportLine) supportLine = priceSeries.createPriceLine({{ price: support, color: '#00e676', title: 'SUP' }});
|
| 287 |
else supportLine.applyOptions({{ price: support }});
|
| 288 |
}} else {{
|
| 289 |
dom.supVal.innerText = '---';
|
| 290 |
+
if (supportLine) {{ priceSeries.removePriceLine(supportLine); supportLine = null; }}
|
| 291 |
}}
|
| 292 |
|
| 293 |
if (resistance) {{
|
| 294 |
dom.resVal.innerText = resistance.toFixed(0);
|
| 295 |
+
if (!resistanceLine) resistanceLine = priceSeries.createPriceLine({{ price: resistance, color: '#ff1744', title: 'RES' }});
|
| 296 |
else resistanceLine.applyOptions({{ price: resistance }});
|
| 297 |
}} else {{
|
| 298 |
dom.resVal.innerText = '---';
|
| 299 |
+
if (resistanceLine) {{ priceSeries.removePriceLine(resistanceLine); resistanceLine = null; }}
|
| 300 |
}}
|
| 301 |
|
| 302 |
+
// Meter & Logs
|
| 303 |
dom.scoreVal.innerText = net_score.toFixed(1);
|
| 304 |
dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red";
|
| 305 |
let barW = Math.min(Math.abs(net_score)*2, 50);
|
|
|
|
| 307 |
dom.scoreBar.style.left = net_score > 0 ? '50%' : (50 - barW) + '%';
|
| 308 |
dom.scoreBar.style.background = net_score > 0 ? '#66fcf1' : '#ff3b3b';
|
| 309 |
|
|
|
|
| 310 |
if (Math.random() > 0.98) {{
|
| 311 |
+
if (net_score > 40) log("Strong Buyers Detected", 'bull');
|
| 312 |
+
else if (net_score < -40) log("Strong Sellers Detected", 'bear');
|
| 313 |
}}
|
| 314 |
}}
|
| 315 |
}}
|
| 316 |
|
| 317 |
+
// -- UPDATE DEPTH CHART --
|
| 318 |
+
// Map diff_x (distance) to 'time', diff_y to 'value'
|
| 319 |
+
// Split into Bull (Positive) and Bear (Negative) series
|
| 320 |
+
if (data.diff && data.diff.x.length > 0) {{
|
| 321 |
+
const bullData = [];
|
| 322 |
+
const bearData = [];
|
| 323 |
+
|
| 324 |
+
for (let i = 0; i < data.diff.x.length; i++) {{
|
| 325 |
+
const xVal = data.diff.x[i]; // Distance
|
| 326 |
+
const yVal = data.diff.y[i]; // Net Volume
|
| 327 |
+
|
| 328 |
+
// Ensure strictly unique and ascending (already guaranteed by logic, but safe to cast)
|
| 329 |
+
// We use xVal as 'time'
|
| 330 |
+
|
| 331 |
+
if (yVal >= 0) {{
|
| 332 |
+
bullData.push({{ time: xVal, value: yVal }});
|
| 333 |
+
bearData.push({{ time: xVal, value: 0 }}); // Clamp other series to 0
|
| 334 |
+
}} else {{
|
| 335 |
+
bullData.push({{ time: xVal, value: 0 }});
|
| 336 |
+
bearData.push({{ time: xVal, value: yVal }});
|
| 337 |
+
}}
|
| 338 |
+
}}
|
| 339 |
+
|
| 340 |
+
bullSeries.setData(bullData);
|
| 341 |
+
bearSeries.setData(bearData);
|
| 342 |
+
depthChart.timeScale().fitContent();
|
| 343 |
+
}}
|
| 344 |
|
| 345 |
}} catch (e) {{
|
| 346 |
+
console.error(e);
|
| 347 |
}}
|
| 348 |
}}
|
| 349 |
+
|
| 350 |
setInterval(fetchData, 1000);
|
| 351 |
}});
|
| 352 |
</script>
|
|
|
|
| 390 |
market_state['ready'] = True
|
| 391 |
|
| 392 |
now = time.time()
|
| 393 |
+
if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
|
|
|
|
| 394 |
market_state['history'].append({'t': now, 'p': mid})
|
| 395 |
if len(market_state['history']) > HISTORY_LENGTH:
|
| 396 |
market_state['history'].pop(0)
|
|
|
|
| 408 |
|
| 409 |
mid = market_state['current_mid']
|
| 410 |
|
|
|
|
| 411 |
raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
|
| 412 |
raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
|
| 413 |
|