Update app.py
Browse files
app.py
CHANGED
|
@@ -96,6 +96,77 @@ def calculate_micro_price_structure(diff_x, diff_y_net, current_mid, best_bid, b
|
|
| 96 |
"rho": rho
|
| 97 |
}
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
def process_market_data():
|
| 100 |
if not market_state['ready']: return {"error": "Initializing..."}
|
| 101 |
|
|
@@ -160,6 +231,9 @@ def process_market_data():
|
|
| 160 |
diff_x, diff_y_net, mid, best_bid, best_ask,
|
| 161 |
{"bids": bid_walls, "asks": ask_walls}
|
| 162 |
)
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
if analysis:
|
| 165 |
if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
|
|
@@ -171,6 +245,7 @@ def process_market_data():
|
|
| 171 |
"mid": mid,
|
| 172 |
"history": market_state['history'],
|
| 173 |
"pred_history": market_state['pred_history'],
|
|
|
|
| 174 |
"trade_history": market_state['trade_vol_history'],
|
| 175 |
"ohlc": market_state['ohlc_history'],
|
| 176 |
"depth_x": diff_x,
|
|
@@ -200,6 +275,7 @@ HTML_PAGE = f"""
|
|
| 200 |
--red: #ff3b3b;
|
| 201 |
--blue: #2979ff;
|
| 202 |
--yellow: #ffeb3b;
|
|
|
|
| 203 |
}}
|
| 204 |
body {{
|
| 205 |
margin: 0; padding: 0;
|
|
@@ -284,6 +360,7 @@ HTML_PAGE = f"""
|
|
| 284 |
.c-green {{ color: var(--green); }}
|
| 285 |
.c-red {{ color: var(--red); }}
|
| 286 |
.c-dim {{ color: var(--text-dim); }}
|
|
|
|
| 287 |
|
| 288 |
.list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; height: 100px; }}
|
| 289 |
.list-item {{
|
|
@@ -322,7 +399,9 @@ HTML_PAGE = f"""
|
|
| 322 |
</div>
|
| 323 |
|
| 324 |
<div id="p-chart" class="panel">
|
| 325 |
-
<div class="chart-header">
|
|
|
|
|
|
|
| 326 |
<div id="tv-price" style="flex: 1; width: 100%;"></div>
|
| 327 |
</div>
|
| 328 |
|
|
@@ -400,7 +479,12 @@ HTML_PAGE = f"""
|
|
| 400 |
|
| 401 |
const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
|
| 402 |
const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }});
|
| 403 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
|
| 405 |
const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{
|
| 406 |
...chartOpts,
|
|
@@ -475,26 +559,38 @@ HTML_PAGE = f"""
|
|
| 475 |
priceSeries.setData(cleanHist);
|
| 476 |
|
| 477 |
const lastP = cleanHist[cleanHist.length-1].value;
|
|
|
|
| 478 |
dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
|
| 479 |
|
|
|
|
| 480 |
if (data.analysis) {{
|
| 481 |
const proj = data.analysis.projected;
|
| 482 |
const rho = data.analysis.rho;
|
| 483 |
-
|
| 484 |
predSeries.setData([
|
| 485 |
cleanHist[cleanHist.length-1],
|
| 486 |
-
{{ time:
|
| 487 |
]);
|
| 488 |
-
|
| 489 |
const pct = ((proj - lastP) / lastP) * 100;
|
| 490 |
const sign = pct >= 0 ? "+" : "";
|
| 491 |
-
|
| 492 |
dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
|
| 493 |
dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
|
| 494 |
dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
|
| 495 |
dom.score.innerText = rho.toFixed(3);
|
| 496 |
dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
|
| 497 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
}}
|
| 499 |
|
| 500 |
if (data.ohlc && data.ohlc.length) {{
|
|
@@ -505,7 +601,6 @@ HTML_PAGE = f"""
|
|
| 505 |
low: c.low,
|
| 506 |
close: c.close
|
| 507 |
}}));
|
| 508 |
-
// Deduplicate candles
|
| 509 |
const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
|
| 510 |
candleSeries.setData(uniqueCandles);
|
| 511 |
}}
|
|
@@ -567,7 +662,7 @@ HTML_PAGE = f"""
|
|
| 567 |
async def kraken_worker():
|
| 568 |
global market_state
|
| 569 |
try:
|
| 570 |
-
# Fetch initial history
|
| 571 |
async with aiohttp.ClientSession() as session:
|
| 572 |
url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
|
| 573 |
async with session.get(url) as response:
|
|
@@ -650,19 +745,19 @@ async def kraken_worker():
|
|
| 650 |
if side == 'buy': market_state['current_vol_window']['buy'] += qty
|
| 651 |
else: market_state['current_vol_window']['sell'] += qty
|
| 652 |
|
| 653 |
-
# LIVE CANDLE UPDATE
|
| 654 |
current_minute_start = int(time.time()) // 60 * 60
|
| 655 |
|
| 656 |
if market_state['ohlc_history']:
|
| 657 |
last_candle = market_state['ohlc_history'][-1]
|
| 658 |
|
| 659 |
-
# If still in the same minute
|
| 660 |
if last_candle['time'] == current_minute_start:
|
| 661 |
last_candle['close'] = price
|
| 662 |
if price > last_candle['high']: last_candle['high'] = price
|
| 663 |
if price < last_candle['low']: last_candle['low'] = price
|
| 664 |
|
| 665 |
-
# If new minute started
|
| 666 |
elif current_minute_start > last_candle['time']:
|
| 667 |
new_candle = {
|
| 668 |
'time': current_minute_start,
|
|
@@ -674,13 +769,12 @@ async def kraken_worker():
|
|
| 674 |
market_state['ohlc_history'].append(new_candle)
|
| 675 |
if len(market_state['ohlc_history']) > 200:
|
| 676 |
market_state['ohlc_history'].pop(0)
|
| 677 |
-
|
| 678 |
except: pass
|
| 679 |
|
| 680 |
elif channel == "ohlc":
|
| 681 |
for candle in data:
|
| 682 |
try:
|
| 683 |
-
# Kraken
|
| 684 |
start_time = int(float(candle['endtime'])) - 60
|
| 685 |
c_data = {
|
| 686 |
'time': start_time,
|
|
@@ -690,7 +784,6 @@ async def kraken_worker():
|
|
| 690 |
'close': float(candle['close'])
|
| 691 |
}
|
| 692 |
|
| 693 |
-
# Sync with existing history if found
|
| 694 |
if market_state['ohlc_history']:
|
| 695 |
if market_state['ohlc_history'][-1]['time'] == start_time:
|
| 696 |
market_state['ohlc_history'][-1] = c_data
|
|
|
|
| 96 |
"rho": rho
|
| 97 |
}
|
| 98 |
|
| 99 |
+
def calculate_polr(bids, asks, mid):
|
| 100 |
+
"""
|
| 101 |
+
Path of Least Resistance:
|
| 102 |
+
Simulates eating volume on both sides.
|
| 103 |
+
Returns the price path where liquidity is thinnest.
|
| 104 |
+
"""
|
| 105 |
+
if not bids or not asks: return []
|
| 106 |
+
|
| 107 |
+
sorted_bids = sorted(bids.items(), key=lambda x: -x[0]) # High to Low
|
| 108 |
+
sorted_asks = sorted(asks.items(), key=lambda x: x[0]) # Low to High
|
| 109 |
+
|
| 110 |
+
path_points = []
|
| 111 |
+
|
| 112 |
+
# We simulate volume stepping from 0.1 BTC up to 20 BTC
|
| 113 |
+
# This represents "Time" or "Intensity" on the X-axis of the projection
|
| 114 |
+
volume_steps = [i * 0.5 for i in range(1, 41)] # 0.5, 1.0 ... 20.0
|
| 115 |
+
|
| 116 |
+
current_time = time.time()
|
| 117 |
+
|
| 118 |
+
for i, target_vol in enumerate(volume_steps):
|
| 119 |
+
# 1. Find price cost to eat 'target_vol' UP
|
| 120 |
+
ask_cost_dist = 0
|
| 121 |
+
cum_vol = 0
|
| 122 |
+
target_ask_price = mid
|
| 123 |
+
for p, q in sorted_asks:
|
| 124 |
+
cum_vol += q
|
| 125 |
+
if cum_vol >= target_vol:
|
| 126 |
+
target_ask_price = p
|
| 127 |
+
break
|
| 128 |
+
ask_cost_dist = target_ask_price - mid
|
| 129 |
+
|
| 130 |
+
# 2. Find price cost to eat 'target_vol' DOWN
|
| 131 |
+
bid_cost_dist = 0
|
| 132 |
+
cum_vol = 0
|
| 133 |
+
target_bid_price = mid
|
| 134 |
+
for p, q in sorted_bids:
|
| 135 |
+
cum_vol += q
|
| 136 |
+
if cum_vol >= target_vol:
|
| 137 |
+
target_bid_price = p
|
| 138 |
+
break
|
| 139 |
+
bid_cost_dist = mid - target_bid_price
|
| 140 |
+
|
| 141 |
+
# 3. Compare Resistance
|
| 142 |
+
# If moving up 10$ costs 5 BTC, but moving down 10$ costs 20 BTC,
|
| 143 |
+
# The path of least resistance is UP (it's thinner).
|
| 144 |
+
|
| 145 |
+
# Here we compare the DISTANCE moved for the SAME volume.
|
| 146 |
+
# If for 5 BTC, Price moves UP $10 and DOWN $2.
|
| 147 |
+
# The UP side is "thinner" (less resistance to price change), so price "slips" up.
|
| 148 |
+
|
| 149 |
+
projected_p = mid
|
| 150 |
+
|
| 151 |
+
# Avoid division by zero or tiny spreads
|
| 152 |
+
if bid_cost_dist <= 0: bid_cost_dist = 0.01
|
| 153 |
+
if ask_cost_dist <= 0: ask_cost_dist = 0.01
|
| 154 |
+
|
| 155 |
+
# Logic: Price goes where the book is thinner (Distance is LARGER for same volume)
|
| 156 |
+
if ask_cost_dist > bid_cost_dist:
|
| 157 |
+
# It takes LESS liquidity to move UP (Price moved further for same vol)
|
| 158 |
+
projected_p = target_ask_price
|
| 159 |
+
else:
|
| 160 |
+
projected_p = target_bid_price
|
| 161 |
+
|
| 162 |
+
# Add slight smoothing/dampening so it connects to current price
|
| 163 |
+
path_points.append({
|
| 164 |
+
't': current_time + (i * 2), # Spread points out every 2 seconds into future
|
| 165 |
+
'p': projected_p
|
| 166 |
+
})
|
| 167 |
+
|
| 168 |
+
return path_points
|
| 169 |
+
|
| 170 |
def process_market_data():
|
| 171 |
if not market_state['ready']: return {"error": "Initializing..."}
|
| 172 |
|
|
|
|
| 231 |
diff_x, diff_y_net, mid, best_bid, best_ask,
|
| 232 |
{"bids": bid_walls, "asks": ask_walls}
|
| 233 |
)
|
| 234 |
+
|
| 235 |
+
# Calculate Path of Least Resistance
|
| 236 |
+
polr_path = calculate_polr(market_state['bids'], market_state['asks'], mid)
|
| 237 |
|
| 238 |
if analysis:
|
| 239 |
if not market_state['pred_history'] or (now - market_state['pred_history'][-1]['t'] > 0.5):
|
|
|
|
| 245 |
"mid": mid,
|
| 246 |
"history": market_state['history'],
|
| 247 |
"pred_history": market_state['pred_history'],
|
| 248 |
+
"polr": polr_path,
|
| 249 |
"trade_history": market_state['trade_vol_history'],
|
| 250 |
"ohlc": market_state['ohlc_history'],
|
| 251 |
"depth_x": diff_x,
|
|
|
|
| 275 |
--red: #ff3b3b;
|
| 276 |
--blue: #2979ff;
|
| 277 |
--yellow: #ffeb3b;
|
| 278 |
+
--purple: #d500f9;
|
| 279 |
}}
|
| 280 |
body {{
|
| 281 |
margin: 0; padding: 0;
|
|
|
|
| 360 |
.c-green {{ color: var(--green); }}
|
| 361 |
.c-red {{ color: var(--red); }}
|
| 362 |
.c-dim {{ color: var(--text-dim); }}
|
| 363 |
+
.c-purp {{ color: var(--purple); }}
|
| 364 |
|
| 365 |
.list-container {{ display: flex; flex-direction: column; gap: 8px; overflow-y: auto; height: 100px; }}
|
| 366 |
.list-item {{
|
|
|
|
| 399 |
</div>
|
| 400 |
|
| 401 |
<div id="p-chart" class="panel">
|
| 402 |
+
<div class="chart-header">
|
| 403 |
+
PRICE (BLUE) // <span class="c-purp">LEAST RESISTANCE (PURPLE)</span> // <span style="color:var(--yellow)">PRED (YELLOW)</span>
|
| 404 |
+
</div>
|
| 405 |
<div id="tv-price" style="flex: 1; width: 100%;"></div>
|
| 406 |
</div>
|
| 407 |
|
|
|
|
| 479 |
|
| 480 |
const priceChart = LightweightCharts.createChart(document.getElementById('tv-price'), chartOpts);
|
| 481 |
const priceSeries = priceChart.addLineSeries({{ color: '#2979ff', lineWidth: 2, title: 'Price' }});
|
| 482 |
+
|
| 483 |
+
// Yellow Line: Mathematical Micro-Price
|
| 484 |
+
const predSeries = priceChart.addLineSeries({{ color: '#ffeb3b', lineWidth: 2, lineStyle: 2, title: 'Math Forecast' }});
|
| 485 |
+
|
| 486 |
+
// Purple Line: Path of Least Resistance (Lowest Volume)
|
| 487 |
+
const polrSeries = priceChart.addLineSeries({{ color: '#d500f9', lineWidth: 3, lineStyle: 0, title: 'Least Resistance' }});
|
| 488 |
|
| 489 |
const candleChart = LightweightCharts.createChart(document.getElementById('tv-candles'), {{
|
| 490 |
...chartOpts,
|
|
|
|
| 559 |
priceSeries.setData(cleanHist);
|
| 560 |
|
| 561 |
const lastP = cleanHist[cleanHist.length-1].value;
|
| 562 |
+
const lastTime = cleanHist[cleanHist.length-1].time;
|
| 563 |
dom.ticker.innerText = lastP.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
|
| 564 |
|
| 565 |
+
// 1. Yellow Line (Math Forecast)
|
| 566 |
if (data.analysis) {{
|
| 567 |
const proj = data.analysis.projected;
|
| 568 |
const rho = data.analysis.rho;
|
|
|
|
| 569 |
predSeries.setData([
|
| 570 |
cleanHist[cleanHist.length-1],
|
| 571 |
+
{{ time: lastTime + 60, value: proj }}
|
| 572 |
]);
|
| 573 |
+
// Text updates
|
| 574 |
const pct = ((proj - lastP) / lastP) * 100;
|
| 575 |
const sign = pct >= 0 ? "+" : "";
|
|
|
|
| 576 |
dom.projPct.innerText = `${{sign}}${{pct.toFixed(4)}}%`;
|
| 577 |
dom.projPct.style.color = pct >= 0 ? "var(--green)" : "var(--red)";
|
| 578 |
dom.projVal.innerText = proj.toLocaleString('en-US', {{ minimumFractionDigits: 2 }});
|
| 579 |
dom.score.innerText = rho.toFixed(3);
|
| 580 |
dom.score.style.color = rho > 0 ? "var(--green)" : (rho < 0 ? "var(--red)" : "var(--text-main)");
|
| 581 |
}}
|
| 582 |
+
|
| 583 |
+
// 2. Purple Line (Path of Least Resistance)
|
| 584 |
+
if (data.polr && data.polr.length) {{
|
| 585 |
+
// Attach start of POLR to current price to make it seamless
|
| 586 |
+
const polrData = [
|
| 587 |
+
{{ time: lastTime, value: lastP }},
|
| 588 |
+
...data.polr.map(d => ({{ time: Math.floor(d.t), value: d.p }}))
|
| 589 |
+
];
|
| 590 |
+
// Deduplicate times
|
| 591 |
+
const uniquePolr = [...new Map(polrData.map(i => [i.time, i])).values()];
|
| 592 |
+
polrSeries.setData(uniquePolr);
|
| 593 |
+
}}
|
| 594 |
}}
|
| 595 |
|
| 596 |
if (data.ohlc && data.ohlc.length) {{
|
|
|
|
| 601 |
low: c.low,
|
| 602 |
close: c.close
|
| 603 |
}}));
|
|
|
|
| 604 |
const uniqueCandles = [...new Map(candles.map(i => [i.time, i])).values()];
|
| 605 |
candleSeries.setData(uniqueCandles);
|
| 606 |
}}
|
|
|
|
| 662 |
async def kraken_worker():
|
| 663 |
global market_state
|
| 664 |
try:
|
| 665 |
+
# Fetch initial history
|
| 666 |
async with aiohttp.ClientSession() as session:
|
| 667 |
url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
|
| 668 |
async with session.get(url) as response:
|
|
|
|
| 745 |
if side == 'buy': market_state['current_vol_window']['buy'] += qty
|
| 746 |
else: market_state['current_vol_window']['sell'] += qty
|
| 747 |
|
| 748 |
+
# LIVE CANDLE UPDATE
|
| 749 |
current_minute_start = int(time.time()) // 60 * 60
|
| 750 |
|
| 751 |
if market_state['ohlc_history']:
|
| 752 |
last_candle = market_state['ohlc_history'][-1]
|
| 753 |
|
| 754 |
+
# If still in the same minute
|
| 755 |
if last_candle['time'] == current_minute_start:
|
| 756 |
last_candle['close'] = price
|
| 757 |
if price > last_candle['high']: last_candle['high'] = price
|
| 758 |
if price < last_candle['low']: last_candle['low'] = price
|
| 759 |
|
| 760 |
+
# If new minute started
|
| 761 |
elif current_minute_start > last_candle['time']:
|
| 762 |
new_candle = {
|
| 763 |
'time': current_minute_start,
|
|
|
|
| 769 |
market_state['ohlc_history'].append(new_candle)
|
| 770 |
if len(market_state['ohlc_history']) > 200:
|
| 771 |
market_state['ohlc_history'].pop(0)
|
|
|
|
| 772 |
except: pass
|
| 773 |
|
| 774 |
elif channel == "ohlc":
|
| 775 |
for candle in data:
|
| 776 |
try:
|
| 777 |
+
# Kraken sends endtime, adjust to starttime
|
| 778 |
start_time = int(float(candle['endtime'])) - 60
|
| 779 |
c_data = {
|
| 780 |
'time': start_time,
|
|
|
|
| 784 |
'close': float(candle['close'])
|
| 785 |
}
|
| 786 |
|
|
|
|
| 787 |
if market_state['ohlc_history']:
|
| 788 |
if market_state['ohlc_history'][-1]['time'] == start_time:
|
| 789 |
market_state['ohlc_history'][-1] = c_data
|