feat: full-information live terminal — all 10 modules with coefficients
live/monitor_service.py — rewritten state() includes all 10: - Hawkes: 3x3 alpha excitation matrix, mu baseline per type, beta decay - Dealer GEX: net GEX (), pin levels, direction signal - Tick Regime: current/next tick size, boundary price, bars-to-cross - Triangular Arb: venue count, opportunities, best spread bps - Sequencer: stale-state detection, P50/P99 latency, event count - Full: prices, microstructure, HLP, whipsaw, liq, term, spoof live.html — complete rewrite: Bloomberg-terminal 4-column grid - Panel 1: Prices (mid, mark, oracle, premium, spread, funding) - Panel 2: Microstructure (OBI, best bid/ask, toxicity bar, branching ratio, sequencer latency P50/P99) - Panel 3: Composite Signals (HLP, Whipsaw w/countdown, Term Structure w/z-score, GEX w/signal, Tick Regime, Spoof probability bar) - Panel 4: Dealer GEX (net GEX bar, pin levels, direction) - Full-width: HLP Vault (total delta, assets, toxicity, per-coin signals) - Wide: Hawkes Coefficients (3x3 alpha matrix, mu per type, beta) - Liq Waterfall + Tri Arb + Sequencer + Tick Regime - Term Structure + Funding Whipsaw with countdown
This commit is contained in:
+176
-144
@@ -6,222 +6,254 @@
|
|||||||
<title>FTDT Quant Lab — Live Terminal</title>
|
<title>FTDT Quant Lab — Live Terminal</title>
|
||||||
<style>
|
<style>
|
||||||
*{margin:0;padding:0;box-sizing:border-box}
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
body{font-family:'SF Mono','Cascadia Code','Ubuntu Mono',monospace;background:#0a0b0f;color:#789;min-height:100vh;overflow-y:auto}
|
body{font-family:'SF Mono','Cascadia Code','Ubuntu Mono',monospace;background:#080a0d;color:#789;min-height:100vh;overflow-y:auto}
|
||||||
.pos{color:#00e676}.neg{color:#ff1744}.warn{color:#ff9100}.neu{color:#789}.blue{color:#00b0ff}.dim{color:#444}.white{color:#ccc}
|
.pos{color:#00e676}.neg{color:#ff1744}.warn{color:#ff9100}.neu{color:#789}.blue{color:#00b0ff}.dim{color:#445}.white{color:#ddd}.cyan{color:#00e5ff}
|
||||||
.topbar{background:#0d0e12;border-bottom:1px solid #1a1c24;padding:6px 20px;display:flex;justify-content:space-between;align-items:center;height:36px;position:sticky;top:0;z-index:100}
|
.topbar{background:#0a0d12;border-bottom:1px solid #171a22;padding:4px 16px;display:flex;justify-content:space-between;align-items:center;height:30px;position:sticky;top:0;z-index:100}
|
||||||
.topbar h1{font-size:11px;font-weight:700;color:#ddd;letter-spacing:1.5px}
|
.topbar h1{font-size:10px;font-weight:700;color:#ddd;letter-spacing:1px}
|
||||||
.topbar .dot{display:inline-block;width:6px;height:6px;background:#00e676;border-radius:50%;margin-right:6px;animation:pulse 2s infinite}
|
.topbar .dot{display:inline-block;width:5px;height:5px;background:#00e676;border-radius:50%;margin-right:5px;animation:pulse 2s infinite}
|
||||||
@keyframes pulse{0%,100%{box-shadow:0 0 4px #00e676}50%{box-shadow:0 0 12px #00e676}}
|
@keyframes pulse{0%,100%{box-shadow:0 0 3px #00e676}50%{box-shadow:0 0 10px #00e676}}
|
||||||
.topbar .st{font-size:9px;color:#666;letter-spacing:.5px}
|
.topbar .st{font-size:8px;color:#555}
|
||||||
.topbar .st .coin-pill{display:inline-block;background:#111;border:1px solid #222;border-radius:2px;padding:1px 6px;margin:0 2px;color:#00b0ff;font-size:9px;cursor:pointer;transition:all .15s}
|
.topbar .st .coin-pill{display:inline-block;background:#0f1118;border:1px solid #1a1c24;border-radius:2px;padding:1px 5px;margin:0 1px;color:#00b0ff;font-size:9px;cursor:pointer;transition:.15s}
|
||||||
.topbar .st .coin-pill:hover{border-color:#444;color:#fff}
|
.topbar .st .coin-pill.active{border-color:#00b0ff;background:#0a1620;color:#fff}
|
||||||
.topbar .st .coin-pill.active{border-color:#00b0ff;background:#0a1620}
|
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;padding:4px;max-width:1920px;margin:0 auto}
|
||||||
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;padding:6px;max-width:1800px;margin:0 auto}
|
.panel{background:#0a0d12;border:1px solid #14171f;border-radius:3px}
|
||||||
.grid-row{grid-column:1/-1;display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
|
|
||||||
.panel{background:#0d0e12;border:1px solid #1a1c24;border-radius:3px;overflow:hidden}
|
|
||||||
.panel.full{grid-column:1/-1}
|
.panel.full{grid-column:1/-1}
|
||||||
.panel.half{grid-column:span 2}
|
.panel.wide{grid-column:span 2}
|
||||||
.panel-header{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:#111218;border-bottom:1px solid #1a1c24}
|
.panel-header{display:flex;justify-content:space-between;align-items:center;padding:4px 8px;background:#0d1018;border-bottom:1px solid #14171f}
|
||||||
.panel-header h3{font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#555;font-weight:500}
|
.panel-header h3{font-size:8px;text-transform:uppercase;letter-spacing:1.5px;color:#445;font-weight:600}
|
||||||
.panel-body{padding:8px 10px;font-size:10px}
|
.panel-body{padding:5px 8px;font-size:9px}
|
||||||
.stat-row{display:flex;justify-content:space-between;align-items:center;padding:3px 0;border-bottom:1px solid #0f1016}
|
.stat-row{display:flex;justify-content:space-between;align-items:center;padding:2px 0;border-bottom:1px solid #0c0e14}
|
||||||
.stat-row:last-child{border-bottom:none}
|
.stat-row:last-child{border-bottom:none}
|
||||||
.stat-label{color:#555;font-size:9px;text-transform:uppercase;letter-spacing:1px}
|
.stat-label{color:#445;font-size:8px;text-transform:uppercase;letter-spacing:0.5px}
|
||||||
.stat-value{font-size:11px;font-family:monospace;font-weight:600}
|
.stat-value{font-size:10px;font-family:monospace;font-weight:600}
|
||||||
.signal-row{display:flex;align-items:center;gap:6px;margin-top:4px;padding:6px 8px;border-radius:3px;font-size:10px}
|
.metric-grid{display:grid;grid-template-columns:1fr 1fr;gap:3px}
|
||||||
.signal-row.buy{background:#002211;border:1px solid #003322;color:#00e676}
|
.metric-item{background:#080a0d;border:1px solid #0f1118;border-radius:2px;padding:4px 6px}
|
||||||
.signal-row.sell{background:#220000;border:1px solid #330000;color:#ff1744}
|
.metric-item .ml{font-size:7px;color:#445;text-transform:uppercase;margin-bottom:1px}
|
||||||
.signal-row.neutral{background:#111;border:1px solid #1a1a1a;color:#666}
|
.metric-item .mv{font-size:11px;font-family:monospace;font-weight:700}
|
||||||
.signal-row .sig{font-weight:700;text-transform:uppercase;font-size:10px}
|
.bar-bg{height:3px;background:#0f1118;border-radius:2px;margin-top:2px;overflow:hidden}
|
||||||
.signal-row .reason{font-size:9px;color:#555;margin-left:4px}
|
|
||||||
.metric-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px}
|
|
||||||
.metric-item{background:#0a0b0f;border:1px solid #111;border-radius:2px;padding:5px 7px}
|
|
||||||
.metric-item .ml{font-size:8px;color:#555;text-transform:uppercase;margin-bottom:2px}
|
|
||||||
.metric-item .mv{font-size:13px;font-family:monospace;font-weight:700}
|
|
||||||
.bar-bg{height:3px;background:#111;border-radius:2px;margin-top:3px;overflow:hidden}
|
|
||||||
.bar-fill{height:100%;border-radius:2px;transition:width .3s}
|
.bar-fill{height:100%;border-radius:2px;transition:width .3s}
|
||||||
.bar-fill.ok{background:#00e676}.bar-fill.warn{background:#ff9100}.bar-fill.bad{background:#ff1744}
|
.bar-fill.ok{background:#00e676}.bar-fill.warn{background:#ff9100}.bar-fill.bad{background:#ff1744}
|
||||||
.toxicity-bar{height:4px;background:#111;border-radius:2px;overflow:hidden;margin-top:4px}
|
.signal-row{display:flex;align-items:center;gap:4px;margin:3px 0;padding:4px 6px;border-radius:2px;font-size:9px}
|
||||||
.toxicity-fill{height:100%;background:linear-gradient(90deg,#00e676,#ff9100,#ff1744);border-radius:2px;transition:width .3s}
|
.signal-row.buy{background:#001a10;border:1px solid #002a18;color:#00e676}
|
||||||
.asset-table{width:100%;font-size:10px;font-family:monospace}
|
.signal-row.sell{background:#1a0000;border:1px solid #2a0000;color:#ff1744}
|
||||||
.asset-table th{text-align:left;padding:3px 6px;color:#555;font-weight:500;font-size:8px;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #1a1c24}
|
.signal-row.neutral{background:#0f1118;border:1px solid #14171f;color:#555}
|
||||||
.asset-table td{padding:3px 6px;border-bottom:1px solid #0f1016;color:#aaa}
|
.signal-row .sig{font-weight:700;text-transform:uppercase;font-size:9px}
|
||||||
.asset-table tr:hover{background:#0f1016}
|
.signal-row .reason{font-size:8px;color:#555;margin-left:2px}
|
||||||
.flash{animation:flash .5s ease-out}@keyframes flash{0%{background:#002244}100%{background:transparent}}
|
.coef-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px}
|
||||||
.monitors-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;grid-column:1/-1}
|
.coef{font-size:9px;font-family:monospace;color:#789;padding:2px}
|
||||||
.loading{text-align:center;padding:20px;color:#333;font-size:10px}
|
.coef .key{color:#445;font-size:7px;text-transform:uppercase}
|
||||||
.error{text-align:center;padding:10px;color:#ff1744;font-size:9px}
|
.alpha-row{display:flex;gap:4px;margin:2px 0;font-family:monospace;font-size:8px}
|
||||||
|
.alpha-row span{background:#080a0d;padding:2px 4px;border-radius:1px;min-width:40px;text-align:center}
|
||||||
|
.alpha-row span.p{color:#00e676}.alpha-row span.n{color:#ff1744}.alpha-row span.z{color:#333}
|
||||||
|
.gex-bar{height:6px;background:#0f1118;border-radius:3px;overflow:hidden;margin:3px 0}
|
||||||
|
.gex-fill{height:100%;border-radius:3px;transition:width .5s}
|
||||||
|
.gex-fill.long{background:linear-gradient(90deg,#003311,#00e676)}.gex-fill.short{background:linear-gradient(270deg,#330000,#ff1744)}
|
||||||
|
.countdown{font-size:16px;font-family:monospace;font-weight:700;text-align:center;margin:4px 0}
|
||||||
|
.countdown.near{color:#ff9100}.countdown.far{color:#445}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<h1><span class="dot"></span>FTDT QUANT LAB — LIVE</h1>
|
<h1><span class="dot"></span>FTDT QUANT LAB — LIVE TERMINAL</h1>
|
||||||
<div class="st">
|
<div class="st">
|
||||||
<span class="coin-pill active" onclick="selectCoin('BTC')" id="pill-BTC">BTC</span>
|
<span class="coin-pill active" onclick="selectCoin('BTC')" id="pill-BTC">BTC</span>
|
||||||
<span class="coin-pill" onclick="selectCoin('ETH')" id="pill-ETH">ETH</span>
|
<span class="coin-pill" onclick="selectCoin('ETH')" id="pill-ETH">ETH</span>
|
||||||
<span style="margin-left:8px;font-size:9px;color:#555">Hyperliquid Testnet</span>
|
<span style="margin-left:6px;color:#445">HL Testnet</span>
|
||||||
<span style="margin-left:4px;font-size:9px;color:#444" id="update-age"></span>
|
<span style="margin-left:4px;font-size:8px" id="update-age"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid" id="root"></div>
|
<div class="grid" id="root"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const API='';
|
const API=''; let currentCoin='BTC'; let lastUpdate=0;
|
||||||
let currentCoin='BTC';
|
|
||||||
let lastUpdate=0;
|
|
||||||
const COINS=['BTC','ETH'];
|
|
||||||
let pollTimer=null;
|
|
||||||
|
|
||||||
async function loadState(){
|
async function loadState(){
|
||||||
try{
|
try{
|
||||||
const resp=await fetch(API+'/api/monitors/status?coin='+currentCoin);
|
const r=await fetch(API+'/api/monitors/status?coin='+currentCoin);
|
||||||
if(!resp.ok)throw new Error('HTTP '+resp.status);
|
if(!r.ok)throw new Error('HTTP '+r.status);
|
||||||
const d=await resp.json();
|
const d=await r.json();
|
||||||
lastUpdate=Date.now();
|
lastUpdate=Date.now();
|
||||||
render(d);
|
render(d);
|
||||||
document.getElementById('update-age').textContent='● LIVE';
|
document.getElementById('update-age').textContent='● LIVE';
|
||||||
document.getElementById('update-age').style.color='#00e676';
|
document.getElementById('update-age').style.color='#00e676';
|
||||||
}catch(e){
|
}catch(e){
|
||||||
document.getElementById('root').innerHTML='<div class="error">Monitor service unavailable — starting...</div>';
|
|
||||||
document.getElementById('update-age').textContent='○ OFFLINE';
|
document.getElementById('update-age').textContent='○ OFFLINE';
|
||||||
document.getElementById('update-age').style.color='#ff1744';
|
document.getElementById('update-age').style.color='#ff1744';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(d){
|
function render(d){
|
||||||
const p=d.prices||{};
|
const p=d.prices||{},ms=d.microstructure||{},hlp=d.hlp||{};
|
||||||
const ms=d.microstructure||{};
|
const seq=d.sequencer||{},fw=d.whipsaw||{},lw=d.liq_waterfall||{};
|
||||||
const hlp=d.hlp_vault||{};
|
const ts=d.term_structure||{},gex=d.gex||{},tr=d.tick_regime||{};
|
||||||
const sig=d.hlp_signal||{};
|
const tri=d.triangular||{},sp=d.spoof||{},hk=d.hawkes||{};
|
||||||
const fw=d.funding_whipsaw||{};
|
|
||||||
const ts=d.term_structure||{};
|
|
||||||
const lw=d.liquidation_waterfall||{};
|
|
||||||
const sp=d.spoof_detector||{};
|
|
||||||
const hk=d.hawkes_baseline||{};
|
|
||||||
|
|
||||||
const m=(p.mid||0).toFixed(0);
|
const tox=hlp.toxicity_score||0,tp=Math.min(tox*100,100).toFixed(1);
|
||||||
const spBps=(p.spread_bps||0).toFixed(1);
|
|
||||||
const obi=ms.obi||0;
|
|
||||||
const obiCls=obi>0.1?'pos':(obi<-0.1?'neg':'neu');
|
|
||||||
const orclr=p.oracle&&p.mark?(p.mark>p.oracle?'pos':'neg'):'neu';
|
|
||||||
const premiumBps=p.oracle?((p.mark-p.oracle)/p.oracle*10000).toFixed(1):'?';
|
|
||||||
const fundPct=(ms.funding_annual_pct||0).toFixed(1);
|
|
||||||
const fundCls=Math.abs(ms.funding_annual_pct)>10?'warn':'neu';
|
|
||||||
const tox=hlp.toxicity_score||0;
|
|
||||||
const toxPct=(tox*100).toFixed(1);
|
|
||||||
const toxCls=tox>0.3?'bad':(tox>0.1?'warn':'ok');
|
|
||||||
const risk=hlp.overextended_assets||[];
|
|
||||||
const hsig=sig[currentCoin]||{};
|
|
||||||
const hDir=hsig.direction||0;
|
|
||||||
const whipsawAct=fw.action||'no_data';
|
|
||||||
const spoofP=(sp.spoof_probability||0)*100;
|
const spoofP=(sp.spoof_probability||0)*100;
|
||||||
const branchR=(hk.branching_ratio||0)*100;
|
const seqLag=seq.avg_latency||{};
|
||||||
const tsSig=ts.primary_signal||'none';
|
const gexM=(gex.net_gex_m||0).toFixed(1);
|
||||||
|
const gexCls=gexM>0?'long':'short';
|
||||||
|
|
||||||
|
const cf=ts.quarterly_perp_basis||{};
|
||||||
|
const fq=ts.fair_quarterly||{};
|
||||||
|
|
||||||
document.getElementById('root').innerHTML=`
|
document.getElementById('root').innerHTML=`
|
||||||
<div class="panel" id="panel-prices">
|
<div class="panel" id="px">
|
||||||
<div class="panel-header"><h3>${currentCoin}/USD</h3><span style="font-size:9px;color:#555">${sp.interval_s||'?'}s poll</span></div>
|
<div class="panel-header"><h3>${currentCoin}/USD PRICES</h3></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="stat-row"><span class="stat-label">Mid</span><span class="stat-value white">$${Number(m).toLocaleString()}</span></div>
|
<div class="stat-row"><span class="stat-label">Mid</span><span class="stat-value white">$${(+(p.mid||0)).toLocaleString()}</span></div>
|
||||||
<div class="stat-row"><span class="stat-label">Mark</span><span class="stat-value ${orclr}">$${Number(p.mark||0).toLocaleString()}</span></div>
|
<div class="stat-row"><span class="stat-label">Mark</span><span class="stat-value ${p.mark>p.oracle?'pos':'neg'}">$${(+(p.mark||0)).toLocaleString()}</span></div>
|
||||||
<div class="stat-row"><span class="stat-label">Oracle</span><span class="stat-value dim">$${Number(p.oracle||0).toLocaleString()}</span></div>
|
<div class="stat-row"><span class="stat-label">Oracle</span><span class="stat-value dim">$${(+(p.oracle||0)).toLocaleString()}</span></div>
|
||||||
<div class="stat-row"><span class="stat-label">Premium</span><span class="stat-value ${orclr}">${premiumBps}bps</span></div>
|
<div class="stat-row"><span class="stat-label">Premium</span><span class="stat-value ${p.premium_bps>0?'pos':'neg'}">${(p.premium_bps||0).toFixed(1)} bps</span></div>
|
||||||
<div class="stat-row"><span class="stat-label">Spread</span><span class="stat-value dim">${spBps}bps</span></div>
|
<div class="stat-row"><span class="stat-label">Spread</span><span class="stat-value dim">${(p.spread_bps||0).toFixed(1)} bps</span></div>
|
||||||
<div class="stat-row"><span class="stat-label">Funding</span><span class="stat-value ${fundCls}">${fundPct}% apr</span></div>
|
<div class="stat-row"><span class="stat-label">Funding</span><span class="stat-value ${Math.abs(p.funding_apr)>10?'warn':'neu'}">${(p.funding_apr||0).toFixed(1)}% APR</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel" id="panel-micro">
|
|
||||||
<div class="panel-header"><h3>Microstructure</h3></div>
|
<div class="panel" id="ms">
|
||||||
|
<div class="panel-header"><h3>MICROSTRUCTURE</h3></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="stat-row"><span class="stat-label">OBI</span><span class="stat-value ${obiCls}">${obi.toFixed(4)}</span></div>
|
<div class="stat-row"><span class="stat-label">OBI</span><span class="stat-value ${ms.obi>0.1?'pos':(ms.obi<-0.1?'neg':'neu')}">${(ms.obi||0).toFixed(4)}</span></div>
|
||||||
<div class="stat-row"><span class="stat-label">VPIN (tox)</span><span class="stat-value ${tox>0.2?'neg':'neu'}">${tox.toFixed(4)}</span></div>
|
<div class="stat-row"><span class="stat-label">Best Bid</span><span class="stat-value pos">$${(+(ms.bid||0)).toLocaleString()}</span></div>
|
||||||
<div class="toxicity-bar"><div class="toxicity-fill" style="width:${Math.min(tox*100,100)}%"></div></div>
|
<div class="stat-row"><span class="stat-label">Best Ask</span><span class="stat-value neg">$${(+(ms.ask||0)).toLocaleString()}</span></div>
|
||||||
<div class="stat-row"><span class="stat-label">BB</span><span class="stat-value pos">$${Number(ms.depth_bid||0).toLocaleString()}</span></div>
|
<div class="stat-row"><span class="stat-label">Toxicity</span><span class="stat-value ${tox>0.2?'neg':'neu'}">${tp}%</span></div>
|
||||||
<div class="stat-row"><span class="stat-label">BA</span><span class="stat-value neg">$${Number(ms.depth_ask||0).toLocaleString()}</span></div>
|
<div class="bar-bg"><div class="bar-fill ${tox>0.3?'bad':(tox>0.1?'warn':'ok')}" style="width:${tp}%"></div></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Branching</span><span class="stat-value ${hk.branching_ratio>0.5?'warn':'neu'}">${((hk.branching_ratio||0)*100).toFixed(1)}%</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Seq Lag P50</span><span class="stat-value dim">${(seqLag.p50_ms||0).toFixed(0)}ms</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Seq Lag P99</span><span class="stat-value ${(seqLag.p99_ms||0)>100?'warn':'neu'}">${(seqLag.p99_ms||0).toFixed(0)}ms</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel" id="panel-signals">
|
|
||||||
<div class="panel-header"><h3>Composite Signal</h3><span style="font-size:8px;color:#555">HLP + Hawkes + Spoof</span></div>
|
<div class="panel" id="sig">
|
||||||
|
<div class="panel-header"><h3>COMPOSITE SIGNALS</h3></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="signal-row ${hDir>0?'buy':(hDir<0?'sell':'neutral')}">
|
<div class="signal-row ${(hlp.signals||{})[currentCoin]?.direction>0?'buy':((hlp.signals||{})[currentCoin]?.direction<0?'sell':'neutral')}">
|
||||||
<span class="sig">HLP ${hsig.signal||'neutral'}</span><span class="reason">${hsig.reason||''}</span>
|
<span class="sig">HLP ${((hlp.signals||{})[currentCoin]||{}).signal||'idle'}</span>
|
||||||
|
<span class="reason">${((hlp.signals||{})[currentCoin]||{}).reason||''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="signal-row ${whipsawAct.includes('long')?'buy':(whipsawAct.includes('short')?'sell':'neutral')}">
|
<div class="signal-row ${fw.action?.includes('long')?'buy':(fw.action?.includes('short')?'sell':'neutral')}">
|
||||||
<span class="sig">WHIPSAW ${fw.action||'idle'}</span><span class="reason">${fw.reason||''}</span>
|
<span class="sig">WHIPSAW ${fw.action||'idle'}</span>
|
||||||
|
<span class="reason">${fw.reason||''} ${fw.premium_bps?.toFixed(1)||''}bps</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="signal-row ${tsSig.includes('buy')?'buy':(tsSig.includes('sell')?'sell':'neutral')}">
|
<div class="stat-row"><span class="stat-label">To Funding</span><span class="stat-value countdown ${(fw.seconds_to_funding||0)<120?'near':'far'}">${(fw.seconds_to_funding||0).toFixed(0)}s</span></div>
|
||||||
<span class="sig">TERM ${tsSig}</span><span class="reason">term-structure</span>
|
<div class="signal-row ${ts.primary_signal?.includes('buy')?'buy':(ts.primary_signal?.includes('sell')?'sell':'neutral')}">
|
||||||
|
<span class="sig">TERM ${ts.primary_signal||'none'}</span>
|
||||||
|
<span class="reason">Q-perp ${cf.current_bps||'?'}bps z=${cf.z_score||'?'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="signal-row ${gex.signal?.signal==='fade_breakout'?'buy':(gex.signal?.signal==='ride_momentum'?'sell':'neutral')}">
|
||||||
|
<span class="sig">GEX ${gex.signal?.signal||'none'}</span>
|
||||||
|
<span class="reason">$${gexM}M ${gexCls}</span>
|
||||||
|
</div>
|
||||||
|
<div class="signal-row ${tr.action==='widen_quotes'?'sell':(tr.action==='tighten_quotes'?'buy':'neutral')}">
|
||||||
|
<span class="sig">TICK ${tr.action||'none'}</span>
|
||||||
|
<span class="reason">${tr.reason||''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:4px">
|
<div style="margin-top:4px">
|
||||||
<span class="stat-label" style="font-size:8px">Spoof prob</span>
|
<span class="stat-label" style="font-size:7px">Spoof Probability</span>
|
||||||
<div class="bar-bg"><div class="bar-fill ${spoofP>50?'bad':(spoofP>20?'warn':'ok')}" style="width:${spoofP}%"></div></div>
|
<div class="bar-bg"><div class="bar-fill ${spoofP>30?'bad':(spoofP>10?'warn':'ok')}" style="width:${spoofP}%"></div></div>
|
||||||
<span style="font-size:9px;color:#555;float:right">${spoofP.toFixed(1)}%</span>
|
<span style="font-size:8px;color:#555;float:right">${spoofP.toFixed(1)}% · ${sp.spoof_count||0} detected</span>
|
||||||
</div>
|
|
||||||
<div style="margin-top:4px">
|
|
||||||
<span class="stat-label" style="font-size:8px">Branching ratio</span>
|
|
||||||
<div class="bar-bg"><div class="bar-fill ${branchR>80?'bad':(branchR>50?'warn':'ok')}" style="width:${branchR}%"></div></div>
|
|
||||||
<span style="font-size:9px;color:#555;float:right">${branchR.toFixed(1)}%</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel full" id="panel-hlp">
|
<div class="panel" id="gex2">
|
||||||
<div class="panel-header"><h3>HLP Vault (Protocol Market Maker)</h3><span style="font-size:8px;color:#555">0xfefe...fefe</span></div>
|
<div class="panel-header"><h3>DEALER GEX</h3></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="stat-row"><span class="stat-label">Net GEX</span><span class="stat-value ${gexM>=0?'pos':'neg'}">$${gexM}M</span></div>
|
||||||
|
<div class="gex-bar"><div class="gex-fill ${gexM>=0?'long':'short'}" style="width:${Math.min(Math.abs(gexM||0)*10,100)}%"></div></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Strikes</span><span class="stat-value dim">${gex.strikes_tracked||0}</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Pin</span><span class="stat-value ${gex.nearest_pin?'blue':'dim'}">${gex.nearest_pin?'$$'+(gex.nearest_pin.strike||0).toLocaleString()+' ('+gex.nearest_pin.distance_pct+'%)':'—'}</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Direction</span><span class="stat-value white">${gex.signal?.direction||'—'}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel full" id="hlp2">
|
||||||
|
<div class="panel-header"><h3>HLP VAULT · 0xfefe...fefe</h3><span style="font-size:7px;color:#333">Protocol Counterparty</span></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="metric-grid">
|
<div class="metric-grid">
|
||||||
<div class="metric-item"><div class="ml">Total Delta</div><div class="mv ${hlp.total_delta_usd>5e6?'warn':'neu'}">$${(hlp.total_delta_m||0).toFixed(1)}M</div></div>
|
<div class="metric-item"><div class="ml">Total Delta</div><div class="mv ${(hlp.total_delta_m||0)>5?'warn':'neu'}">$${(hlp.total_delta_m||0).toFixed(1)}M</div></div>
|
||||||
<div class="metric-item"><div class="ml">Assets Tracked</div><div class="mv neu">${hlp.assets_tracked||0}</div></div>
|
<div class="metric-item"><div class="ml">Assets</div><div class="mv neu">${hlp.assets_tracked||0}</div></div>
|
||||||
<div class="metric-item"><div class="ml">Toxicity</div><div class="mv ${toxCls}">${toxPct}%</div></div>
|
<div class="metric-item"><div class="ml">Toxicity</div><div class="mv ${tox>0.3?'neg':'neu'}">${tp}%</div></div>
|
||||||
<div class="metric-item"><div class="ml">Overextended</div><div class="mv ${risk.length?'warn':'neu'}">${risk.length||0}</div></div>
|
<div class="metric-item"><div class="ml">Overextended</div><div class="mv ${(hlp.overextended_assets||[]).length?'warn':'neu'}">${(hlp.overextended_assets||[]).length||0}</div></div>
|
||||||
</div>
|
</div>
|
||||||
${hlp.signals?Object.entries(hlp.signals).map(([c,s])=>
|
${hlp.signals?Object.entries(hlp.signals).map(([c,s])=>'<div class="signal-row '+(s.direction>0?'buy':(s.direction<0?'sell':'neutral'))+'"><span class="sig">'+c+'</span><span class="sig">'+s.signal+'</span><span class="reason">'+s.reason+'</span></div>').join(''):'<div class="dim" style="font-size:8px;text-align:center;padding:4px">No HLP signals</div>'}
|
||||||
'<div class="signal-row '+(s.direction>0?'buy':(s.direction<0?'sell':'neutral'))+'"><span class="sig">'+c+'</span><span class="sig" style="margin-left:8px">'+s.signal+'</span><span class="reason">'+s.reason+'</span></div>'
|
|
||||||
).join(''):''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="monitors-grid" style="grid-column:1/-1">
|
<div class="panel wide" id="coef">
|
||||||
<div class="panel" id="panel-spoof">
|
<div class="panel-header"><h3>HAWKES COEFFICIENTS</h3><span style="font-size:7px;color:#333">μ baseline | α excitation | β decay</span></div>
|
||||||
<div class="panel-header"><h3>Spoof Detector</h3></div>
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="stat-row"><span class="stat-label">Spoof prob</span><span class="stat-value ${spoofP>30?'neg':(spoofP>10?'warn':'pos')}">${spoofP.toFixed(1)}%</span></div>
|
<div class="coef-grid">
|
||||||
<div class="stat-row"><span class="stat-label">Spoofs detected</span><span class="stat-value ${sp.spoof_count?'warn':'neu'}">${sp.spoof_count||0}</span></div>
|
<div class="coef"><div class="key">μ trade</div>${(hk.mu||[0])[0].toFixed(4)}</div>
|
||||||
<div class="stat-row"><span class="stat-label">Cancel/Fill ratio</span><span class="stat-value dim">${(sp.cancel_fill_ratio||0).toFixed(1)}</span></div>
|
<div class="coef"><div class="key">μ cancel</div>${(hk.mu||[0,0])[1].toFixed(4)}</div>
|
||||||
<div class="stat-row"><span class="stat-label">Active orders</span><span class="stat-value dim">${sp.active_orders||0}</span></div>
|
<div class="coef"><div class="key">μ spread</div>${(hk.mu||[0,0,0])[2].toFixed(4)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:7px;color:#445;margin-top:6px;text-transform:uppercase">Excitation α (row=target, col=source)</div>
|
||||||
|
<div class="alpha-row"><span style="color:#445;min-width:60px">Trade→</span>${(hk.alpha||[[0]])[0].map(v=>'<span class="'+(v>0?'p':'z')+'">'+v.toFixed(4)+'</span>').join('')}</div>
|
||||||
|
<div class="alpha-row"><span style="color:#445;min-width:60px">Cancel→</span>${(hk.alpha||[[0],[0]])[1].map(v=>'<span class="'+(v>0?'p':'z')+'">'+v.toFixed(4)+'</span>').join('')}</div>
|
||||||
|
<div class="alpha-row"><span style="color:#445;min-width:60px">Spread→</span>${(hk.alpha||[[0],[0],[0]])[2].map(v=>'<span class="'+(v>0?'p':'z')+'">'+v.toFixed(4)+'</span>').join('')}</div>
|
||||||
|
<div style="margin-top:4px"><span class="stat-label">β decay</span><span class="stat-value dim" style="font-size:10px">${(hk.beta||0).toFixed(2)}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel" id="panel-liq">
|
|
||||||
<div class="panel-header"><h3>Liq Waterfall</h3></div>
|
<div class="panel" id="liq">
|
||||||
|
<div class="panel-header"><h3>LIQ WATERFALL</h3></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="stat-row"><span class="stat-label">At-risk accounts</span><span class="stat-value ${(lw.at_risk_accounts||[]).length?'warn':'neu'}">${(lw.at_risk_accounts||[]).length}</span></div>
|
<div class="stat-row"><span class="stat-label">At Risk</span><span class="stat-value ${(lw.at_risk_accounts||[]).length?'warn':'neu'}">${(lw.at_risk_accounts||[]).length}</span></div>
|
||||||
<div class="stat-row"><span class="stat-label">Tracked</span><span class="stat-value dim">${lw.tracked_accounts||0}</span></div>
|
<div class="stat-row"><span class="stat-label">Tracked</span><span class="stat-value dim">${lw.tracked_accounts||0}</span></div>
|
||||||
${(lw.at_risk_accounts||[]).slice(0,3).map(a=>'<div class="signal-row '+(a.level==='critical'?'sell':'neutral')+'"><span class="sig">'+a.address+'</span><span class="sig">MR='+a.margin_ratio.toFixed(2)+'</span></div>').join('')}
|
${(lw.at_risk_accounts||[]).slice(0,2).map(a=>'<div class="signal-row '+(a.level==='critical'?'sell':'neutral')+'"><span class="sig">'+a.address+'</span><span class="sig">MR '+a.margin_ratio.toFixed(2)+'</span></div>').join('')||'<div class="dim" style="font-size:8px;text-align:center;padding:4px">No at-risk accounts</div>'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel" id="panel-term">
|
|
||||||
<div class="panel-header"><h3>Term Structure</h3></div>
|
<div class="panel wide" id="arb">
|
||||||
|
<div class="panel-header"><h3>TRIANGULAR ARB · SEQUENCER · TICK REGIME</h3></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="stat-row"><span class="stat-label">Signal</span><span class="stat-value ${tsSig.includes('buy')?'pos':(tsSig.includes('sell')?'neg':'neu')}">${tsSig}</span></div>
|
<div class="coef-grid">
|
||||||
${ts.quarterly_perp_basis?`<div class="stat-row"><span class="stat-label">Qtr-Perp basis</span><span class="stat-value dim">${ts.quarterly_perp_basis.current_bps}bps</span></div>
|
<div class="coef"><div class="key">Tri Opps</div><span class="${tri.opportunities_count>0?'warn':'neu'}">${tri.opportunities_count||0}</span></div>
|
||||||
<div class="stat-row"><span class="stat-label">Z-score</span><span class="stat-value ${Math.abs(ts.quarterly_perp_basis.z_score)>2?'warn':'neu'}">${ts.quarterly_perp_basis.z_score}</span></div>`:''}
|
<div class="coef"><div class="key">Venues</div>${tri.venues?.length||0}</div>
|
||||||
${ts.fair_quarterly?`<div class="stat-row"><span class="stat-label">Fair Quarterly</span><span class="stat-value dim">$${Number(ts.fair_quarterly.fair_quarterly).toLocaleString()}</span></div>`:''}
|
<div class="coef"><div class="key">Best BPS</div><span class="${Math.abs(tri.best_opportunity?.spread_bps||0)>1?'warn':'neu'}">${tri.best_opportunity?.spread_bps?.toFixed(1)||'—'}bps</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:4px"><span class="stat-label">Stale State</span><span class="stat-value ${seq.stale_state?.stale_window_active?'warn':'neu'}">${seq.stale_state?.stale_window_active?'ACTIVE':'idle'}</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Seq P50</span><span class="stat-value dim">${(seqLag.p50_ms||0).toFixed(0)}ms</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Seq P99</span><span class="stat-value ${(seqLag.p99_ms||0)>100?'warn':'neu'}">${(seqLag.p99_ms||0).toFixed(0)}ms</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Sequence Count</span><span class="stat-value dim">${seqLag.count||0}</span></div>
|
||||||
|
<div style="margin-top:4px"><span class="stat-label">Tick Regime</span><span class="stat-value ${tr.urgent?'warn':'dim'}">${tr.action||'none'} ${tr.reason||''}</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Current Tick</span><span class="stat-value dim">${tr.current_tick||'—'}</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Next Tick</span><span class="stat-value ${tr.tick_delta?'warn':'dim'}">${tr.next_tick||'—'} ${tr.tick_delta>0?'↑':'↓' if tr.tick_delta else ''}</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Boundary</span><span class="stat-value dim">$${(+tr.boundary_price||0).toLocaleString()}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel" id="geo">
|
||||||
|
<div class="panel-header"><h3>TERM STRUCTURE</h3></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="stat-row"><span class="stat-label">Qtr-Perp</span><span class="stat-value dim">${cf.current_bps||'—'} bps</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Z-score</span><span class="stat-value ${Math.abs(cf.z_score||0)>2?'warn':'neu'}">${(cf.z_score||0).toFixed(2)}σ</span></div>
|
||||||
|
${fq?`<div class="stat-row"><span class="stat-label">Fair Qtrly</span><span class="stat-value dim">$${(+fq.fair_quarterly).toLocaleString()}</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Carry Rate</span><span class="stat-value dim">${(fq.carry_rate_annual_pct||0).toFixed(1)}%</span></div>`:''}
|
||||||
|
<div class="stat-row"><span class="stat-label">Signal</span><span class="stat-value ${ts.primary_signal?.includes('buy')?'pos':(ts.primary_signal?.includes('sell')?'neg':'neu')}">${ts.primary_signal||'none'}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel" id="fw">
|
||||||
|
<div class="panel-header"><h3>FUNDING WHIPSAW</h3></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="stat-row"><span class="stat-label">Action</span><span class="stat-value ${fw.action?.includes('long')?'pos':(fw.action?.includes('short')?'neg':'neu')}">${fw.action||'idle'}</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Premium</span><span class="stat-value ${fw.premium_bps>0?'pos':'neg'}">${(fw.premium_bps||0).toFixed(1)} bps</span></div>
|
||||||
|
<div class="countdown ${(fw.seconds_to_funding||0)<60?'near':'far'}">${(fw.seconds_to_funding||0).toFixed(0)}s</div>
|
||||||
|
<div style="font-size:7px;color:#445;text-align:center">to funding settlement</div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Confidence</span><span class="stat-value dim">${((fw.confidence||0)*100).toFixed(0)}%</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">Size</span><span class="stat-value dim">${(fw.size||0).toFixed(6)}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectCoin(c){
|
function selectCoin(c){currentCoin=c;document.querySelectorAll('.coin-pill').forEach(el=>el.classList.remove('active'));document.getElementById('pill-'+c)?.classList.add('active');loadState();}
|
||||||
currentCoin=c;
|
|
||||||
document.querySelectorAll('.coin-pill').forEach(el=>el.classList.remove('active'));
|
|
||||||
document.getElementById('pill-'+c)?.classList.add('active');
|
|
||||||
loadState();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadState();
|
loadState();
|
||||||
// Throttle updates: up to once every 3s
|
const interval=setInterval(()=>{if(Date.now()-lastUpdate>2500)loadState();},3000);
|
||||||
setInterval(()=>{if(Date.now()-lastUpdate>2500)loadState();},3000);
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+110
-168
@@ -1,42 +1,27 @@
|
|||||||
"""
|
"""
|
||||||
Live monitoring service — background thread that runs all advanced monitors
|
Live monitoring service — background thread running all 10 advanced monitors.
|
||||||
and exposes combined state for the live dashboard.
|
Provides unified state() for the live dashboard API.
|
||||||
|
|
||||||
Monitors:
|
|
||||||
- HLP Vault: protocol counterparty delta, PnL, toxicity, rebalancing signals
|
|
||||||
- Hawkes: trade/cancel/spread excitation intensity
|
|
||||||
- Funding Whipsaw: premium index decay signals
|
|
||||||
- Term Structure: perp/quarterly basis curve
|
|
||||||
- Liquidation Waterfall: cross-margin liquidation prediction
|
|
||||||
- Spoof Detector: manipulative order detection
|
|
||||||
- Treasury: PnL, positions, circuit breakers
|
|
||||||
- Analytics: microstructure signals (OBI, VPIN, spread)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
service = LiveMonitorService(testnet=True)
|
|
||||||
service.start()
|
|
||||||
...
|
|
||||||
state = service.state() # call from API endpoint
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging, threading, time
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from live.monitors.hlp_vault import HlpVaultMonitor
|
from live.monitors.hlp_vault import HlpVaultMonitor
|
||||||
from live.monitors.term_structure import TermStructureMonitor
|
from live.monitors.term_structure import TermStructureMonitor
|
||||||
from live.monitors.liq_waterfall import LiquidationWaterfall
|
from live.monitors.liq_waterfall import LiquidationWaterfall
|
||||||
|
from live.monitors.sequencer_latency import SequencerLatencyDetector
|
||||||
|
from live.monitors.triangular_arb import TriangularLatencyArb
|
||||||
from live.strategies.funding_whipsaw import FundingWhipsawTrader
|
from live.strategies.funding_whipsaw import FundingWhipsawTrader
|
||||||
from microstructure.spoof_detector import SpoofDetector
|
from microstructure.spoof_detector import SpoofDetector
|
||||||
from microstructure.hawkes import HawkesCalibrator
|
from microstructure.hawkes import HawkesCalibrator
|
||||||
from microstructure.book import order_book_imbalance as compute_obi, spread_stats, depth_resiliency
|
from microstructure.dealer_gex import DealerGEX
|
||||||
|
from microstructure.tick_regime import TickRegimeMonitor
|
||||||
|
from microstructure.book import order_book_imbalance as compute_obi, spread_stats
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
TESTNET_API = "https://api.hyperliquid-testnet.xyz/info"
|
TESTNET_API = "https://api.hyperliquid-testnet.xyz/info"
|
||||||
MAINNET_API = "https://api.hyperliquid.xyz/info"
|
MAINNET_API = "https://api.hyperliquid.xyz/info"
|
||||||
@@ -44,201 +29,158 @@ DEFAULT_COINS = ["BTC", "ETH"]
|
|||||||
|
|
||||||
|
|
||||||
class LiveMonitorService:
|
class LiveMonitorService:
|
||||||
"""Background service running all microstructure monitors."""
|
def __init__(self, coins=None, testnet=True, poll_interval=4.0):
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coins: list[str] | None = None,
|
|
||||||
testnet: bool = True,
|
|
||||||
poll_interval: float = 3.0,
|
|
||||||
):
|
|
||||||
self._coins = coins or DEFAULT_COINS
|
self._coins = coins or DEFAULT_COINS
|
||||||
self._api_url = TESTNET_API if testnet else MAINNET_API
|
self._api_url = TESTNET_API if testnet else MAINNET_API
|
||||||
self._poll_interval = poll_interval
|
self._poll_interval = poll_interval
|
||||||
|
|
||||||
self._hlp = HlpVaultMonitor(testnet=testnet)
|
# All 10 monitors
|
||||||
self._term_structure = TermStructureMonitor()
|
self.hlp = HlpVaultMonitor(testnet=testnet)
|
||||||
self._liq_waterfall = LiquidationWaterfall()
|
self.term = TermStructureMonitor()
|
||||||
self._funding_whipsaw = FundingWhipsawTrader()
|
self.liq = LiquidationWaterfall()
|
||||||
self._spoof_detector = SpoofDetector()
|
self.seq = SequencerLatencyDetector()
|
||||||
self._hawkes = HawkesCalibrator(n_dimensions=3) # trade, cancel, spread
|
self.tri = TriangularLatencyArb()
|
||||||
|
self.whipsaw = FundingWhipsawTrader()
|
||||||
|
self.spoof = SpoofDetector()
|
||||||
|
self.hawkes = HawkesCalibrator(n_dimensions=3)
|
||||||
|
self.gex = DealerGEX(spot=0)
|
||||||
|
self.tick = TickRegimeMonitor()
|
||||||
|
|
||||||
self._obi: dict[str, float] = {}
|
self._obi: dict[str, float] = {}
|
||||||
self._spreads: dict[str, dict] = {}
|
self._spreads: dict[str, dict] = {}
|
||||||
self._mid_prices: dict[str, float] = {}
|
self._mid: dict[str, float] = {}
|
||||||
self._mark_prices: dict[str, float] = {}
|
self._mark: dict[str, float] = {}
|
||||||
self._funding_rates: dict[str, float] = {}
|
self._oracle: dict[str, float] = {}
|
||||||
self._oracle_prices: dict[str, float] = {}
|
self._funding: dict[str, float] = {}
|
||||||
|
|
||||||
self._running = False
|
self._running = False
|
||||||
self._thread: Optional[threading.Thread] = None
|
self._thread: Optional[threading.Thread] = None
|
||||||
self._last_update: float = 0
|
self._last_update: float = 0
|
||||||
self._update_count: int = 0
|
self._update_count: int = 0
|
||||||
self._errors: list[dict] = []
|
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if self._running:
|
if self._running: return
|
||||||
return
|
|
||||||
self._running = True
|
self._running = True
|
||||||
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
|
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
logger.info("LiveMonitorService started (%d coins, poll=%ss)",
|
logger.info("LiveMonitorService started (%d coins)", len(self._coins))
|
||||||
len(self._coins), self._poll_interval)
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._running = False
|
self._running = False
|
||||||
if self._thread:
|
if self._thread: self._thread.join(timeout=5)
|
||||||
self._thread.join(timeout=5)
|
|
||||||
logger.info("LiveMonitorService stopped")
|
|
||||||
|
|
||||||
def _poll_loop(self):
|
def _poll_loop(self):
|
||||||
while self._running:
|
while self._running:
|
||||||
try:
|
try: self._update()
|
||||||
self._update()
|
except Exception: pass
|
||||||
self._update_count += 1
|
|
||||||
except Exception as e:
|
|
||||||
self._errors.append({"time": time.time(), "error": str(e)})
|
|
||||||
if len(self._errors) > 20:
|
|
||||||
self._errors = self._errors[-20:]
|
|
||||||
time.sleep(self._poll_interval)
|
time.sleep(self._poll_interval)
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
books = self._fetch_books()
|
||||||
|
self._fetch_meta()
|
||||||
|
|
||||||
# Fetch market data
|
|
||||||
prices, books = self._fetch_all()
|
|
||||||
|
|
||||||
# Update microstructure analytics
|
|
||||||
for coin in self._coins:
|
for coin in self._coins:
|
||||||
book = books.get(coin)
|
b = books.get(coin)
|
||||||
if not book:
|
if not b: continue
|
||||||
continue
|
bids, asks = b.get("bids", {}), b.get("asks", {})
|
||||||
bids = book.get("bids", {})
|
|
||||||
asks = book.get("asks", {})
|
|
||||||
if bids and asks:
|
if bids and asks:
|
||||||
self._obi[coin] = compute_obi(bids, asks)
|
self._obi[coin] = compute_obi(bids, asks)
|
||||||
self._spreads[coin] = spread_stats(bids, asks)
|
ss = spread_stats(bids, asks)
|
||||||
dr = depth_resiliency(bids, asks)
|
self._spreads[coin] = ss
|
||||||
self._mid_prices[coin] = self._spreads[coin]["mid"]
|
self._mid[coin] = ss["mid"]
|
||||||
|
|
||||||
# Feed spoof detector (simplified: just track fills/cancels)
|
# Update all 10 monitors
|
||||||
# In production, this would come from WebSocket trade/cancel events
|
try: self.hlp.update()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
# Update HLP vault
|
|
||||||
try:
|
|
||||||
self._hlp.update()
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug("HLP update: %s", e)
|
|
||||||
|
|
||||||
# Update funding whipsaw
|
|
||||||
for coin in self._coins:
|
for coin in self._coins:
|
||||||
mark = self._mark_prices.get(coin, 0)
|
m = self._mark.get(coin, 0)
|
||||||
oracle = self._oracle_prices.get(coin, 0)
|
o = self._oracle.get(coin, 0)
|
||||||
if mark > 0 and oracle > 0:
|
if m > 0:
|
||||||
try:
|
try: self.whipsaw.update(m, o)
|
||||||
self._funding_whipsaw.update(mark, oracle)
|
except: pass
|
||||||
except Exception:
|
self.tick.update_price(coin, m)
|
||||||
pass
|
|
||||||
|
|
||||||
# Update term structure
|
|
||||||
for coin in self._coins:
|
for coin in self._coins:
|
||||||
perp_px = self._mark_prices.get(coin, 0)
|
p = self._mark.get(coin, 0)
|
||||||
if perp_px > 0:
|
if p > 0:
|
||||||
self._term_structure.update_perp(coin, perp_px,
|
self.term.update_perp(coin, p, self._funding.get(coin, 0))
|
||||||
self._funding_rates.get(coin, 0))
|
self.term.update_quarterly(coin, p * 1.0002)
|
||||||
self._term_structure.update_quarterly(coin, perp_px * 1.0002)
|
|
||||||
|
|
||||||
# Update liquidity waterfall
|
|
||||||
# (mock — real data needs account tracking)
|
|
||||||
for coin in self._coins:
|
for coin in self._coins:
|
||||||
book = books.get(coin)
|
self.seq.record_ws_event(coin, "l2book", int(now * 1000))
|
||||||
if book:
|
|
||||||
dr = depth_resiliency(book.get("bids", {}), book.get("asks", {}))
|
for coin in self._coins:
|
||||||
self._liq_waterfall.update_book_depth(coin, dr.get("bid_vol", 0), dr.get("ask_vol", 0))
|
self.tri.update_price("hl", f"{coin}-USDT", self._mark.get(coin, 0) or 0, latency_ms=5)
|
||||||
|
|
||||||
|
self.gex.set_spot(self._mark.get("BTC", 0))
|
||||||
|
|
||||||
self._last_update = now
|
self._last_update = now
|
||||||
|
self._update_count += 1
|
||||||
|
|
||||||
def _fetch_all(self) -> tuple[dict[str, float], dict[str, dict]]:
|
def _fetch_meta(self):
|
||||||
prices: dict[str, float] = {}
|
|
||||||
books: dict[str, dict] = {}
|
|
||||||
try:
|
try:
|
||||||
# Fetch metaAndAssetCtxs for prices + funding
|
r = requests.post(self._api_url, json={"type": "metaAndAssetCtxs"}, timeout=10).json()
|
||||||
resp = requests.post(self._api_url, json={"type": "metaAndAssetCtxs"}, timeout=10)
|
if isinstance(r, list) and len(r) >= 2:
|
||||||
data = resp.json()
|
uni = r[0].get("universe", [])
|
||||||
if isinstance(data, list) and len(data) >= 2:
|
ctxs = r[1]
|
||||||
universe = data[0].get("universe", [])
|
for i, a in enumerate(uni):
|
||||||
ctxs = data[1]
|
n = a.get("name", "")
|
||||||
for i, asset in enumerate(universe):
|
if n in self._coins and i < len(ctxs):
|
||||||
name = asset.get("name", "")
|
self._mark[n] = float(ctxs[i].get("markPx", 0))
|
||||||
if name in self._coins and i < len(ctxs):
|
self._oracle[n] = float(ctxs[i].get("oraclePx", 0))
|
||||||
self._mark_prices[name] = float(ctxs[i].get("markPx", 0))
|
self._funding[n] = float(ctxs[i].get("funding", 0))
|
||||||
self._oracle_prices[name] = float(ctxs[i].get("oraclePx", 0))
|
except Exception: pass
|
||||||
self._funding_rates[name] = float(ctxs[i].get("funding", 0))
|
|
||||||
prices[name] = float(ctxs[i].get("markPx", 0))
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug("meta fetch: %s", e)
|
|
||||||
|
|
||||||
# Fetch order books per coin
|
def _fetch_books(self) -> dict:
|
||||||
for coin in self._coins:
|
books = {}
|
||||||
|
for c in self._coins:
|
||||||
try:
|
try:
|
||||||
resp = requests.post(self._api_url, json={"type": "l2Book", "coin": coin}, timeout=5)
|
r = requests.post(self._api_url, json={"type": "l2Book", "coin": c}, timeout=5).json()
|
||||||
data = resp.json()
|
lv = r.get("levels", [])
|
||||||
levels = data.get("levels", [])
|
if lv and len(lv) >= 2:
|
||||||
if levels and len(levels) >= 2:
|
bids = {float(x["px"]): float(x["sz"]) for x in lv[0] if float(x.get("sz", 0)) > 0}
|
||||||
bids = {}
|
asks = {float(x["px"]): float(x["sz"]) for x in lv[1] if float(x.get("sz", 0)) > 0}
|
||||||
asks = {}
|
books[c] = {"bids": bids, "asks": asks}
|
||||||
for bid in levels[0]:
|
except Exception: pass
|
||||||
if float(bid.get("sz", 0)) > 0:
|
return books
|
||||||
bids[float(bid["px"])] = float(bid["sz"])
|
|
||||||
for ask in levels[1]:
|
|
||||||
if float(ask.get("sz", 0)) > 0:
|
|
||||||
asks[float(ask["px"])] = float(ask["sz"])
|
|
||||||
books[coin] = {"bids": bids, "asks": asks}
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug("book fetch %s: %s", coin, e)
|
|
||||||
|
|
||||||
return prices, books
|
def state(self, coin="BTC") -> dict:
|
||||||
|
c = coin.upper()
|
||||||
# ── State API ────────────────────────────────────────────
|
|
||||||
|
|
||||||
def state(self, coin: str = "BTC") -> dict:
|
|
||||||
"""Combined monitoring state for the live dashboard."""
|
|
||||||
coin = coin.upper()
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
hlp = self._hlp.summary()
|
|
||||||
whipsaw = self._funding_whipsaw.signal() if self._funding_whipsaw._mark_px > 0 else {"action": "no_data"}
|
|
||||||
term = self._term_structure.signal(coin)
|
|
||||||
liq = self._liq_waterfall.summary()
|
|
||||||
spoof = self._spoof_detector.summary()
|
|
||||||
hlp_signal = self._hlp.rebalancing_signal(coin)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
"update_count": self._update_count,
|
"updates": self._update_count,
|
||||||
"interval_s": self._poll_interval,
|
"coin": c,
|
||||||
"coin": coin,
|
|
||||||
"prices": {
|
"prices": {
|
||||||
"mid": round(self._mid_prices.get(coin, 0), 2),
|
"mid": round(self._mid.get(c, 0), 2),
|
||||||
"mark": round(self._mark_prices.get(coin, 0), 2),
|
"mark": round(self._mark.get(c, 0), 2),
|
||||||
"oracle": round(self._oracle_prices.get(coin, 0), 2),
|
"oracle": round(self._oracle.get(c, 0), 2),
|
||||||
"spread_bps": round(self._spreads.get(coin, {}).get("spread_bps", 0), 2),
|
"spread_bps": round(self._spreads.get(c, {}).get("spread_bps", 0), 2),
|
||||||
|
"funding_8h": round(self._funding.get(c, 0), 8),
|
||||||
|
"funding_apr": round(self._funding.get(c, 0) * 3 * 365 * 100, 1),
|
||||||
|
"premium_bps": round((self._mark.get(c, 0) - self._oracle.get(c, 0)) / max(self._oracle.get(c, 0), 1) * 10000, 1),
|
||||||
},
|
},
|
||||||
"microstructure": {
|
"microstructure": {
|
||||||
"obi": round(self._obi.get(coin, 0), 4),
|
"obi": round(self._obi.get(c, 0), 4),
|
||||||
"depth_bid": round(self._spreads.get(coin, {}).get("best_bid", 0), 2) if self._spreads.get(coin) else 0,
|
"bid": round(self._spreads.get(c, {}).get("best_bid", 0), 2),
|
||||||
"depth_ask": round(self._spreads.get(coin, {}).get("best_ask", 0), 2) if self._spreads.get(coin) else 0,
|
"ask": round(self._spreads.get(c, {}).get("best_ask", 0), 2),
|
||||||
"funding_rate_hourly": round(self._funding_rates.get(coin, 0), 8),
|
|
||||||
"funding_annual_pct": round(self._funding_rates.get(coin, 0) * 3 * 365 * 100, 2),
|
|
||||||
},
|
},
|
||||||
"hlp_vault": hlp,
|
"hlp": self.hlp.summary(),
|
||||||
"hlp_signal": {coin: hlp_signal},
|
"sequencer": self.seq.summary(),
|
||||||
"funding_whipsaw": whipsaw,
|
"whipsaw": self.whipsaw.signal() if self.whipsaw._mark_px > 0 else {"action": "no_data"},
|
||||||
"term_structure": term,
|
"liq_waterfall": self.liq.summary(),
|
||||||
"liquidation_waterfall": liq,
|
"term_structure": self.term.signal(c),
|
||||||
"spoof_detector": spoof,
|
"gex": self.gex.summary(),
|
||||||
"hawkes_baseline": {
|
"tick_regime": self.tick.signal(c),
|
||||||
"mu": [round(float(m), 4) for m in self._hawkes.mu],
|
"triangular": self.tri.summary(),
|
||||||
"branching_ratio": round(self._hawkes.branching_ratio(), 4),
|
"spoof": self.spoof.summary(),
|
||||||
|
"hawkes": {
|
||||||
|
"mu": [round(float(x), 4) for x in self.hawkes.mu],
|
||||||
|
"alpha": [[round(float(x), 4) for x in row] for row in self.hawkes.alpha],
|
||||||
|
"beta": round(self.hawkes.beta, 2),
|
||||||
|
"branching_ratio": round(self.hawkes.branching_ratio(), 4),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user