feat(live): REAL-TRUTH ledger — capital aggiornato dal PnL dei fill reali, sim ridotto a diagnostica
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ class PairsWorker:
|
||||
data_dir: Path = Path("data/paper_trades"),
|
||||
executor=None, # PairsExecutionClient: esecuzione REALE shadow a 2 gambe
|
||||
exec_instruments: dict | None = None, # {asset: instrument USDC}
|
||||
real_truth: bool = False,
|
||||
):
|
||||
self.asset_a = asset_a
|
||||
self.asset_b = asset_b
|
||||
@@ -88,6 +89,9 @@ class PairsWorker:
|
||||
self.inst_a = self.exec_instruments.get(asset_a)
|
||||
self.inst_b = self.exec_instruments.get(asset_b)
|
||||
self.execution_enabled = bool(executor and self.inst_a and self.inst_b)
|
||||
# REAL-TRUTH (2026-06-10): come StrategyWorker — `capital` aggiornato dal
|
||||
# PnL dei fill reali (2 gambe, fee reali); sim solo diagnostica nel log.
|
||||
self.real_truth = bool(real_truth and self.execution_enabled)
|
||||
self.real_capital = capital
|
||||
self.real_in_position = False
|
||||
self.real_dir = 0
|
||||
@@ -231,11 +235,15 @@ class PairsWorker:
|
||||
self._notify("REAL_OPEN_FAIL", {**data, "note": pf.notes})
|
||||
self._save_state() # persisti subito il ledger reale (resume-safe sui crash)
|
||||
|
||||
def _real_close_pair(self, sim_a: float, sim_b: float, reason: str, sim_pnl: float):
|
||||
def _real_close_pair(self, sim_a: float, sim_b: float, reason: str,
|
||||
sim_pnl: float) -> tuple[float | None, bool]:
|
||||
"""Chiusura REALE shadow: richiude entrambe le gambe reduce-only, riconcilia
|
||||
PnL reale (per gamba) e fee, aggiorna il ledger reale parallelo."""
|
||||
PnL reale (per gamba) e fee, aggiorna il ledger reale parallelo.
|
||||
|
||||
Ritorna (real_pnl, applied): applied=True se almeno una gamba ha chiuso con
|
||||
fill verificato (PnL reale utilizzabile come verita' in real-truth)."""
|
||||
if not self.real_in_position:
|
||||
return
|
||||
return None, False
|
||||
pf = self.executor.close_pair(self.inst_a, self.inst_b, self.real_side_a,
|
||||
self.real_side_b, self.real_amount_a, self.real_amount_b,
|
||||
label=self.worker_id)
|
||||
@@ -264,6 +272,7 @@ class PairsWorker:
|
||||
self.real_notional_a = self.real_notional_b = 0.0
|
||||
self.real_entry_fee = 0.0
|
||||
self._save_state()
|
||||
return real_pnl, bool(pf.verified or pf.leg_a.verified or pf.leg_b.verified)
|
||||
|
||||
def _close(self, ca: float, cb: float, z: float, reason: str):
|
||||
if not self.in_position:
|
||||
@@ -273,9 +282,17 @@ class PairsWorker:
|
||||
gross = (ret_a - ret_b) * self.direction * self.leverage
|
||||
fee = 2 * self.fee_rt * self.leverage # 2 gambe
|
||||
net = gross - fee
|
||||
pnl = self.capital * self.position_size * net
|
||||
sim_pnl = self.capital * self.position_size * net
|
||||
|
||||
# REAL-TRUTH: chiusura reale PRIMA dell'update ledger (come StrategyWorker)
|
||||
real_pnl, real_applied = (None, False)
|
||||
if self.execution_enabled:
|
||||
real_pnl, real_applied = self._real_close_pair(ca, cb, reason, sim_pnl)
|
||||
use_real = self.real_truth and real_applied
|
||||
pnl = real_pnl if use_real else sim_pnl
|
||||
|
||||
self.capital = max(self.capital + pnl, 0.0)
|
||||
is_win = net > 0
|
||||
is_win = pnl > 0
|
||||
self.total_trades += 1
|
||||
self.total_wins += is_win
|
||||
acc = self.total_wins / self.total_trades * 100 if self.total_trades else 0
|
||||
@@ -284,9 +301,12 @@ class PairsWorker:
|
||||
"net_return": round(net * 100, 3), "pnl": round(pnl, 2),
|
||||
"capital": round(self.capital, 2), "bars_held": self.bars_held,
|
||||
"win": bool(is_win), "total_trades": self.total_trades, "accuracy": round(acc, 1)}
|
||||
if self.real_truth:
|
||||
data["pnl_source"] = "real" if use_real else "sim_fallback"
|
||||
data["sim_pnl"] = round(sim_pnl, 2)
|
||||
if real_pnl is not None:
|
||||
data["real_pnl"] = round(real_pnl, 4)
|
||||
self._log("CLOSE", data); self._notify("CLOSED", data)
|
||||
if self.execution_enabled:
|
||||
self._real_close_pair(ca, cb, reason, pnl)
|
||||
self.in_position = False
|
||||
self.direction = 0
|
||||
self.entry_a = self.entry_b = self.entry_z = 0.0
|
||||
|
||||
@@ -38,6 +38,7 @@ class StrategyWorker:
|
||||
data_dir: Path = Path("data/paper_trades"),
|
||||
executor: ExecutionClient | None = None,
|
||||
exec_instrument: str | None = None,
|
||||
real_truth: bool = False,
|
||||
):
|
||||
self.strategy = strategy
|
||||
self.asset = asset
|
||||
@@ -53,6 +54,11 @@ class StrategyWorker:
|
||||
self.executor = executor
|
||||
self.exec_instrument = exec_instrument
|
||||
self.execution_enabled = bool(executor and exec_instrument)
|
||||
# REAL-TRUTH (2026-06-10): il ledger che guida il portafoglio (`capital`) si
|
||||
# aggiorna col PnL dei FILL REALI (fee reali incluse); il sim resta solo
|
||||
# diagnostica nel log CLOSE. Fallback al sim SOLO se il trade reale non e'
|
||||
# mai esistito/fillato (REAL_OPEN_FAIL, fill zero) — flag pnl_source nel log.
|
||||
self.real_truth = bool(real_truth and self.execution_enabled)
|
||||
self.real_capital = capital
|
||||
self.real_in_position = False
|
||||
self.real_side = "" # "buy" | "sell" dell'apertura reale
|
||||
@@ -316,7 +322,8 @@ class StrategyWorker:
|
||||
else:
|
||||
self._log("REAL_DSL_FAIL", {**data, "note": rest.notes})
|
||||
|
||||
def _real_close(self, sim_exit: float, reason: str, sim_pnl: float):
|
||||
def _real_close(self, sim_exit: float, reason: str,
|
||||
sim_pnl: float) -> tuple[float | None, bool]:
|
||||
"""Chiusura REALE (reduce-only della quota worker) + confronto col sim.
|
||||
|
||||
Prima riconcilia l'eventuale LIMIT resting al TP: lo cancella (innocuo
|
||||
@@ -324,9 +331,13 @@ class StrategyWorker:
|
||||
legge i fill reali dal trade history per order_id; solo la quota residua
|
||||
viene chiusa a mercato (fallback, o exit non-TP: stop-loss/time_limit).
|
||||
L'uscita take-profit reale avviene cosi' AL livello come nel backtest,
|
||||
non al poll post-rimbalzo."""
|
||||
non al poll post-rimbalzo.
|
||||
|
||||
Ritorna (real_pnl, applied): applied=True se il PnL reale e' basato su
|
||||
fill effettivi (o chiusura verificata) e puo' fare da verita' del ledger
|
||||
in modalita' real-truth; False = nessuna posizione/fill reale."""
|
||||
if not self.real_in_position:
|
||||
return
|
||||
return None, False
|
||||
from src.live.execution import contract_spec
|
||||
step = contract_spec(self.exec_instrument)["step"]
|
||||
|
||||
@@ -434,6 +445,8 @@ class StrategyWorker:
|
||||
self.real_order_id = ""
|
||||
self.real_tp_order_id = ""
|
||||
self.real_dsl_order_id = ""
|
||||
# applied: fill reali presenti (parts) o chiusura comunque verificata
|
||||
return real_pnl, bool(parts) or verified
|
||||
|
||||
def _close_position(self, current_price: float, reason: str):
|
||||
if not self.in_position:
|
||||
@@ -442,9 +455,17 @@ class StrategyWorker:
|
||||
price_change = (current_price - self.entry_price) / self.entry_price
|
||||
trade_return = price_change * self.direction
|
||||
net = trade_return * self.leverage - self.fee_rt * self.leverage
|
||||
pnl = self.capital * self.position_size * net
|
||||
sim_pnl = self.capital * self.position_size * net
|
||||
|
||||
is_win = net > 0 # win = profitto NETTO dopo fee (non il lordo trade_return)
|
||||
# REAL-TRUTH: chiusura reale PRIMA dell'update ledger; se i fill reali
|
||||
# esistono il loro PnL (fee reali incluse) e' la verita' del capitale.
|
||||
real_pnl, real_applied = (None, False)
|
||||
if self.execution_enabled:
|
||||
real_pnl, real_applied = self._real_close(current_price, reason, sim_pnl)
|
||||
use_real = self.real_truth and real_applied
|
||||
pnl = real_pnl if use_real else sim_pnl
|
||||
|
||||
is_win = pnl > 0 # win = profitto NETTO dopo fee (reali se real-truth)
|
||||
self.capital += pnl
|
||||
self.capital = max(self.capital, 0)
|
||||
self.total_trades += 1
|
||||
@@ -466,12 +487,15 @@ class StrategyWorker:
|
||||
"total_trades": self.total_trades,
|
||||
"accuracy": round(accuracy, 1),
|
||||
}
|
||||
if self.real_truth:
|
||||
# diagnostica: sorgente del PnL applicato + sim a confronto
|
||||
trade_data["pnl_source"] = "real" if use_real else "sim_fallback"
|
||||
trade_data["sim_pnl"] = round(sim_pnl, 2)
|
||||
if real_pnl is not None:
|
||||
trade_data["real_pnl"] = round(real_pnl, 4)
|
||||
self._log("CLOSE", trade_data)
|
||||
self._notify("CLOSED", trade_data)
|
||||
|
||||
if self.execution_enabled:
|
||||
self._real_close(current_price, reason, pnl)
|
||||
|
||||
self.in_position = False
|
||||
self.direction = 0
|
||||
self.entry_price = 0
|
||||
|
||||
+13
-5
@@ -57,18 +57,21 @@ def pos_for_spec(sid: str, global_ps: float, family_overrides: dict[str, float],
|
||||
def build_worker_for(spec: SleeveSpec, alloc_capital: float, leverage: float,
|
||||
data_dir: Path = DATA_DIR, position_size: float = 0.15,
|
||||
executor=None, exec_instrument: str | None = None,
|
||||
pairs_executor=None, exec_instruments: dict | None = None):
|
||||
pairs_executor=None, exec_instruments: dict | None = None,
|
||||
real_truth: bool = False):
|
||||
"""Costruisce il worker esecutore per uno sleeve con capitale = quota allocata.
|
||||
|
||||
executor/exec_instrument: per i fade single-leg, StrategyWorker affianca al fill sim
|
||||
un ordine REALE (shadow). pairs_executor/exec_instruments: idem per i PairsWorker
|
||||
(esecuzione reale a 2 gambe)."""
|
||||
(esecuzione reale a 2 gambe). real_truth: il ledger `capital` si aggiorna col PnL
|
||||
dei FILL REALI (sim ridotto a diagnostica) — inerte senza executor."""
|
||||
if spec.kind == "pairs":
|
||||
return PairsWorker(
|
||||
asset_a=spec.a, asset_b=spec.b, tf=spec.tf, params=spec.params,
|
||||
capital=alloc_capital, position_size=position_size, leverage=leverage,
|
||||
fee_rt=0.001, name="PR01_pairs_reversion", data_dir=data_dir,
|
||||
executor=pairs_executor, exec_instruments=exec_instruments,
|
||||
real_truth=real_truth,
|
||||
)
|
||||
if spec.kind == "basket":
|
||||
pr = spec.params
|
||||
@@ -110,7 +113,7 @@ def build_worker_for(spec: SleeveSpec, alloc_capital: float, leverage: float,
|
||||
return StrategyWorker(
|
||||
strategy=strategy, asset=spec.asset, tf=spec.tf, capital=alloc_capital,
|
||||
position_size=position_size, leverage=leverage, params=spec.params, data_dir=data_dir,
|
||||
executor=executor, exec_instrument=exec_instrument,
|
||||
executor=executor, exec_instrument=exec_instrument, real_truth=real_truth,
|
||||
)
|
||||
|
||||
|
||||
@@ -277,6 +280,9 @@ def run(config_path: str = "portfolios.yml"):
|
||||
exec_sleeves = set(_exec_cfg.get("sleeves", []))
|
||||
exec_instr = _exec_cfg.get("instruments", {}) or {}
|
||||
pairs_exec_enabled = bool(_exec_cfg.get("pairs_enabled")) # esecuzione reale 2 gambe
|
||||
# REAL-TRUTH (2026-06-10): il capitale degli sleeve eseguiti si aggiorna col PnL
|
||||
# dei fill reali (sim = diagnostica). Default False: va acceso esplicitamente in yml.
|
||||
real_truth = bool(_exec_cfg.get("real_truth", False))
|
||||
executor = None
|
||||
pairs_executor = None
|
||||
if exec_enabled:
|
||||
@@ -285,7 +291,8 @@ def run(config_path: str = "portfolios.yml"):
|
||||
# disaster-bracket on-book (~-30%): assicurazione outage sui fade reali
|
||||
executor.disaster_sl_pct = float(_exec_cfg.get("disaster_sl_pct", 0.30) or 0) or None
|
||||
print(f"[runner] ESECUZIONE REALE attiva (shadow) — sleeve={sorted(exec_sleeves)} "
|
||||
f"strumenti={exec_instr} disaster_sl={executor.disaster_sl_pct}")
|
||||
f"strumenti={exec_instr} disaster_sl={executor.disaster_sl_pct} "
|
||||
f"real_truth={real_truth}")
|
||||
if pairs_exec_enabled:
|
||||
from src.live.execution import PairsExecutionClient
|
||||
pairs_executor = PairsExecutionClient(leg=executor)
|
||||
@@ -318,7 +325,8 @@ def run(config_path: str = "portfolios.yml"):
|
||||
workers[s.sid] = build_worker_for(s, alloc[s.sid], p.leverage,
|
||||
position_size=pos_for_spec(s.sid, position_size, ps_family, s.params.get("position_size")),
|
||||
executor=ex, exec_instrument=inst,
|
||||
pairs_executor=pex, exec_instruments=pinst)
|
||||
pairs_executor=pex, exec_instruments=pinst,
|
||||
real_truth=real_truth)
|
||||
if ps_family:
|
||||
print(f"[runner] position_size globale={position_size} override famiglia={ps_family}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user