diff --git a/src/live/dashboard.py b/src/live/dashboard.py index 7f4c42e..bd8e24c 100644 --- a/src/live/dashboard.py +++ b/src/live/dashboard.py @@ -210,46 +210,58 @@ def build_state() -> dict: }) if st.get("in_position") and not paper_only: is_pair = "entry_a" in st - asset = wid.split("__")[1] - # entry REALE se eseguito (il mark live è il prezzo reale USDC: confrontare - # con l'entry SIM mischierebbe le basi — il feed di decisione testnet inverse - # è dislocato dal lineare, vedi xex_divergence_research). Fallback al sim. + # SEMPRE prezzi REALI (entry reale di esecuzione vs mark reale USDC): il feed + # di decisione SIM (testnet inverse) è dislocato e non va mostrato come prezzo. executed = st.get("real_in_position") - if is_pair: - real_e, sim_e = (st.get("real_entry_a") if executed else None), st.get("entry_a") or 0.0 - else: - real_e, sim_e = (st.get("real_entry_price") if executed else None), st.get("entry_price") or 0.0 - entry = real_e or sim_e - if not is_pair: - open_assets.add(asset) - active.append({ + row = { "id": _short(wid), "family": _family_of(wid), "dir": "LONG" if st.get("direction", 0) > 0 else "SHORT", - "entry": round(entry, 4), "sim_entry": round(sim_e, 4), - "bars": st.get("bars_held", 0), - "max_bars": st.get("max_bars", 0), - "tp": st.get("tp", 0.0), "sl": st.get("sl", 0.0), - "age_min": round(_age_min(sp), 1), - "stale": _age_min(sp) > 15, - "asset": None if is_pair else asset, - "pair": is_pair, - "notional": st.get("real_entry_notional") or round(cap * 0.5 * 3, 1), - "real": bool(st.get("real_in_position")), - }) + "bars": st.get("bars_held", 0), "max_bars": st.get("max_bars", 0), + "tp": st.get("tp", 0.0), "age_min": round(_age_min(sp), 1), + "stale": _age_min(sp) > 15, "pair": is_pair, + "real": bool(executed), + } + if is_pair: + a_, b_ = wid.split("__")[1].split("_") # es. ETH_SOL + open_assets.update({a_, b_}) + row.update({ + "asset": None, "leg_a": a_, "leg_b": b_, + "entry_a": st.get("real_entry_a") if executed else st.get("entry_a"), + "entry_b": st.get("real_entry_b") if executed else st.get("entry_b"), + "notional_a": st.get("real_notional_a") or 0.0, + "notional_b": st.get("real_notional_b") or 0.0, + "dirnum": 1 if st.get("direction", 0) > 0 else -1, + "entry": round(st.get("real_entry_a") or st.get("entry_a") or 0.0, 4), + }) + else: + asset = wid.split("__")[1] + open_assets.add(asset) + entry = (st.get("real_entry_price") if executed else None) or st.get("entry_price") or 0.0 + row.update({"asset": asset, "entry": round(entry, 4), + "notional": st.get("real_entry_notional") or round(cap * 0.5 * 3, 1)}) + active.append(row) - # PnL non realizzato live per i trade attivi single-leg (mark best-effort) + # valore di mercato REALE + PnL non realizzato reale (mark USDC live, best-effort) marks = _marks(open_assets) for a in active: - if a["asset"] and marks.get(a["asset"]) and a["entry"]: + if not a["pair"] and a.get("asset") and marks.get(a["asset"]) and a["entry"]: mark = marks[a["asset"]] sign = 1 if a["dir"] == "LONG" else -1 pct = sign * (mark - a["entry"]) / a["entry"] a["mark"] = round(mark, 4) a["unreal_pct"] = round(pct * 100, 2) a["unreal_eur"] = round(a["notional"] * pct, 2) - # distanza al TP (%) if a["tp"]: a["to_tp_pct"] = round(abs(a["tp"] - mark) / mark * 100, 2) + elif a["pair"]: + ma, mb = marks.get(a["leg_a"]), marks.get(a["leg_b"]) + if ma and mb and a["entry_a"] and a["entry_b"]: + s = a["dirnum"] # +1 long_ratio: long A / short B + gain_a = a["notional_a"] * s * (ma - a["entry_a"]) / a["entry_a"] + gain_b = a["notional_b"] * (-s) * (mb - a["entry_b"]) / a["entry_b"] + a["mark"] = round(ma, 4) + a["mark_b"] = round(mb, 4) + a["unreal_eur"] = round(gain_a + gain_b, 2) # curve EQUITY per FAMIGLIA: PnL cumulato di ogni famiglia su asse-tempo comune # (ad ogni CLOSE di una qualsiasi famiglia, il cumulato di TUTTE avanza step-wise). @@ -427,10 +439,10 @@ HTML = r"""

Equity

+

Trade attivi — stato in tempo reale

Equity per famiglia (PnL cumulato realizzato)

Strategie REALI per famiglia (realizzato, netto fee)

-

Trade attivi — stato in tempo reale

Trade chiusi (ultimi 50)

@@ -593,14 +605,13 @@ function renderDescs(d){const wrap=document.getElementById('descs');if(!wrap)ret wrap.append(box);} function renderActive(a){const wrap=document.getElementById('active');wrap.innerHTML=''; if(!a.length){wrap.innerHTML='
nessuna posizione aperta
';return;} - const t=E('table');t.innerHTML='StrategiaLatoEntryMarkPnL non realizz.Barre→TP%Età'; + const t=E('table');t.innerHTML='StrategiaLatoEntry realeMercato realePnL non realizz.Barre→TP%Età'; a.forEach(p=>{const tr=E('tr'); - const side=`${p.dir}`; - const ue=p.unreal_eur!=null?`${eur(p.unreal_eur)} (${p.unreal_pct}%)`:(p.pair?'pairs (z)':''); - const gap=p.sim_entry&&Math.abs(p.sim_entry-p.entry)>0.01; - const ecell=gap?`${p.entry} ⚠sim ${p.sim_entry}`:`${p.entry}`; - tr.innerHTML=`${p.id}${p.real?'':' sim'}${side} - ${ecell}${p.mark||'—'}${ue} + const side=`${p.dir}${p.pair?' ratio':''}`; + const ue=p.unreal_eur!=null?`${eur(p.unreal_eur)}${p.unreal_pct!=null?' ('+p.unreal_pct+'%)':''}`:''; + const mkt=p.pair?(p.mark!=null?`${p.mark} / ${p.mark_b}`:'—'):(p.mark||'—'); + tr.innerHTML=`${p.id}${p.real?'':' sim'}${p.pair?' '+p.leg_a+'/'+p.leg_b+'':''}${side} + ${p.entry}${mkt}${ue} ${p.bars}/${p.max_bars||'∞'}${p.to_tp_pct!=null?p.to_tp_pct:'—'} ${p.age_min}m${p.stale?' ⚠':''}`; t.append(tr);});wrap.append(t);}