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"""