feat(live): esecuzione REALE su Deribit testnet (shadow) per i 6 fade sui lineari USDC
- ExecutionClient: notional->amount (lineare USDC + inverse), open/close_amount reduce-only, verifica sul trade (order_id), fee reali lette dai trades[] - CerberoClient: place_order market + reduce_only, get_trade_history - StrategyWorker: shadow (REAL_OPEN/REAL_CLOSE accanto al sim), ledger reale parallelo persistito, confronto slippage/fee sim-vs-reale - runner+portfolios.yml: config execution (6 fade MR01/MR02/MR07 x BTC/ETH su BTC_USDC/ETH_USDC-PERPETUAL), capitale 2000 - smoke: live_exec_smoke (layer) + live_shadow_smoke (catena worker), provati su testnet Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+120
-1
@@ -10,6 +10,7 @@ import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.live.telegram_notifier import notify_event
|
||||
from src.live.execution import ExecutionClient
|
||||
|
||||
FEE_RT = 0.002
|
||||
|
||||
@@ -28,6 +29,8 @@ class StrategyWorker:
|
||||
hold_bars: int = 3,
|
||||
params: dict | None = None,
|
||||
data_dir: Path = Path("data/paper_trades"),
|
||||
executor: ExecutionClient | None = None,
|
||||
exec_instrument: str | None = None,
|
||||
):
|
||||
self.strategy = strategy
|
||||
self.asset = asset
|
||||
@@ -38,6 +41,21 @@ class StrategyWorker:
|
||||
self.hold_bars = hold_bars
|
||||
self.params = params or {}
|
||||
|
||||
# --- Esecuzione REALE (shadow): se attiva, ogni open/close sim e' affiancato
|
||||
# da un ordine reale su Deribit (lineare USDC), con ledger reale parallelo. ---
|
||||
self.executor = executor
|
||||
self.exec_instrument = exec_instrument
|
||||
self.execution_enabled = bool(executor and exec_instrument)
|
||||
self.real_capital = capital
|
||||
self.real_in_position = False
|
||||
self.real_side = "" # "buy" | "sell" dell'apertura reale
|
||||
self.real_amount = 0.0 # amount Deribit (base-coin) da richiudere
|
||||
self.real_entry_price = 0.0
|
||||
self.real_entry_fee_usd = 0.0
|
||||
self.real_entry_notional = 0.0 # USD effettivi esposti all'entrata
|
||||
self.real_order_id = ""
|
||||
self.real_trades = 0
|
||||
|
||||
self.worker_id = f"{strategy.name}__{asset}__{tf}"
|
||||
self.work_dir = data_dir / self.worker_id
|
||||
self.work_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -89,9 +107,21 @@ class StrategyWorker:
|
||||
self.sl = state.get("sl", 0.0)
|
||||
self.max_bars = state.get("max_bars", 0)
|
||||
|
||||
self.real_capital = state.get("real_capital", self.initial_capital)
|
||||
self.real_in_position = state.get("real_in_position", False)
|
||||
self.real_side = state.get("real_side", "")
|
||||
self.real_amount = state.get("real_amount", 0.0)
|
||||
self.real_entry_price = state.get("real_entry_price", 0.0)
|
||||
self.real_entry_fee_usd = state.get("real_entry_fee_usd", 0.0)
|
||||
self.real_entry_notional = state.get("real_entry_notional", 0.0)
|
||||
self.real_order_id = state.get("real_order_id", "")
|
||||
self.real_trades = state.get("real_trades", 0)
|
||||
|
||||
self._log("RESUME", {"capital": round(self.capital, 2),
|
||||
"total_trades": self.total_trades,
|
||||
"in_position": self.in_position})
|
||||
"in_position": self.in_position,
|
||||
"real_capital": round(self.real_capital, 2),
|
||||
"real_in_position": self.real_in_position})
|
||||
|
||||
def _save_state(self):
|
||||
state = {
|
||||
@@ -108,6 +138,15 @@ class StrategyWorker:
|
||||
"tp": self.tp,
|
||||
"sl": self.sl,
|
||||
"max_bars": self.max_bars,
|
||||
"real_capital": round(self.real_capital, 4),
|
||||
"real_in_position": self.real_in_position,
|
||||
"real_side": self.real_side,
|
||||
"real_amount": self.real_amount,
|
||||
"real_entry_price": self.real_entry_price,
|
||||
"real_entry_fee_usd": self.real_entry_fee_usd,
|
||||
"real_entry_notional": self.real_entry_notional,
|
||||
"real_order_id": self.real_order_id,
|
||||
"real_trades": self.real_trades,
|
||||
"last_update": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
with open(self.status_path, "w") as f:
|
||||
@@ -155,6 +194,83 @@ class StrategyWorker:
|
||||
self._log("OPEN", trade_data)
|
||||
self._notify("OPENED", trade_data)
|
||||
|
||||
if self.execution_enabled:
|
||||
self._real_open(signal.direction, current_price, notional)
|
||||
|
||||
def _real_open(self, direction: int, sim_price: float, notional: float):
|
||||
"""Apertura REALE (shadow) accanto al fill simulato. Logga il confronto
|
||||
prezzo-sim vs prezzo-eseguito e la fee reale Deribit."""
|
||||
from src.live.execution import contract_spec
|
||||
side = "buy" if direction == 1 else "sell"
|
||||
fill = self.executor.open(self.exec_instrument, side, notional, label=self.worker_id)
|
||||
|
||||
slip_bps = ((fill.fill_price / sim_price - 1) * 1e4
|
||||
if fill.fill_price and sim_price else None)
|
||||
data = {
|
||||
"instrument": self.exec_instrument,
|
||||
"side": side,
|
||||
"order_id": fill.order_id,
|
||||
"amount": fill.amount,
|
||||
"sim_price": round(sim_price, 2),
|
||||
"real_fill": fill.fill_price,
|
||||
"slippage_bps": round(slip_bps, 2) if slip_bps is not None else None,
|
||||
"fee_usd": round(fill.fee_usd, 5),
|
||||
"verified": fill.verified,
|
||||
}
|
||||
if fill.verified:
|
||||
linear = contract_spec(self.exec_instrument).get("linear")
|
||||
self.real_in_position = True
|
||||
self.real_side = side
|
||||
self.real_amount = fill.amount
|
||||
self.real_entry_price = fill.fill_price or sim_price
|
||||
self.real_entry_fee_usd = fill.fee_usd
|
||||
self.real_entry_notional = (fill.amount * self.real_entry_price
|
||||
if linear else fill.amount)
|
||||
self.real_order_id = fill.order_id or ""
|
||||
self._log("REAL_OPEN", data)
|
||||
else:
|
||||
self._log("REAL_OPEN_FAIL", {**data, "note": fill.notes})
|
||||
|
||||
def _real_close(self, sim_exit: float, reason: str, sim_pnl: float):
|
||||
"""Chiusura REALE (reduce-only della quota worker) + confronto col sim."""
|
||||
if not self.real_in_position:
|
||||
return
|
||||
fill = self.executor.close_amount(self.exec_instrument, self.real_side,
|
||||
self.real_amount, label=self.worker_id)
|
||||
exit_price = fill.fill_price or sim_exit
|
||||
rdir = 1 if self.real_side == "buy" else -1
|
||||
price_change = (exit_price - self.real_entry_price) / self.real_entry_price \
|
||||
if self.real_entry_price else 0.0
|
||||
real_gross = rdir * price_change * self.real_entry_notional
|
||||
real_fees = self.real_entry_fee_usd + fill.fee_usd
|
||||
real_pnl = real_gross - real_fees
|
||||
self.real_capital += real_pnl
|
||||
self.real_trades += 1
|
||||
|
||||
slip_bps = ((exit_price / sim_exit - 1) * 1e4
|
||||
if exit_price and sim_exit else None)
|
||||
self._log("REAL_CLOSE", {
|
||||
"reason": reason,
|
||||
"order_id": fill.order_id,
|
||||
"sim_exit": round(sim_exit, 2),
|
||||
"real_fill": fill.fill_price,
|
||||
"slippage_bps": round(slip_bps, 2) if slip_bps is not None else None,
|
||||
"entry_fee_usd": round(self.real_entry_fee_usd, 5),
|
||||
"exit_fee_usd": round(fill.fee_usd, 5),
|
||||
"real_pnl_usd": round(real_pnl, 4),
|
||||
"sim_pnl_usd": round(sim_pnl, 4),
|
||||
"real_capital": round(self.real_capital, 4),
|
||||
"verified": fill.verified,
|
||||
})
|
||||
|
||||
self.real_in_position = False
|
||||
self.real_side = ""
|
||||
self.real_amount = 0.0
|
||||
self.real_entry_price = 0.0
|
||||
self.real_entry_fee_usd = 0.0
|
||||
self.real_entry_notional = 0.0
|
||||
self.real_order_id = ""
|
||||
|
||||
def _close_position(self, current_price: float, reason: str):
|
||||
if not self.in_position:
|
||||
return
|
||||
@@ -189,6 +305,9 @@ class StrategyWorker:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user