Files
PythagorasGoal/src/live/basket_trend_worker.py
T
Adriano Dal Pastro f5173fba06 fix(live): 4 hardening da code-review — REAL_DIVERGENCE, outage su feed vuoto, bars.py condiviso, DSL cancel retry
Review multi-agente 7-angoli su 8c4e1cd..HEAD + check trades live:

1. Alert Telegram REAL_DIVERGENCE quando |slippage sim/reale| >= 100bps a
   open/close. Causa scatenante: spike print testnet 10:37 (candela 10:00
   H=65618, O/C ~62400) -> 3 fade BTC short su close fantasma 65266.5, reale
   fillato a 62395 (-440bps), sim +2.26 mai esistiti — passato in silenzio.
2. FEED_OUTAGE anche su feed degradato SENZA eccezione (HTTP 200 + candles
   vuote: i worker saltavano il tick in silenzio, streak a 0). Helper unico
   _outage_tick; chiavi payload uniformate (minuti su start e RIPRESO).
3. src/live/bars.py: detection forming-bar unificata (bar_ms_of /
   last_bar_is_forming / last_settled_idx) — era copiata in 4 punti
   (strategy_worker, basket, pairs, _check_stale_feed hardcoded 1h).
   E' l'invariante di sicurezza EXIT-16: ora una sola implementazione testata.
4. DSL cancel hardening in _real_close: retry su errore transitorio + alert
   REAL_DSL_CANCEL_FAIL se lo stop resta forse orfano sul book (prima l'id
   veniva dimenticato in silenzio); order_not_found = probabile trigger in
   outage -> solo log (il close a valle esce gia' verified=False).

Refutato il finding top dei finder ("stop_market senza trigger"): cerbero-mcp
traduce price->trigger_price+mark, e in produzione 2 DSL armati + 1 ciclo
completo pulito (MR07_BTC).

Test: 83/83 (9 nuovi: bars helper + DSL/divergence con executor finto).
Smoke testnet 4 scenari verdi, conto flat, zero falsi allarmi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:59:58 +00:00

103 lines
4.4 KiB
Python

"""BasketTrendWorker (TR01): EMA20>EMA100 long/flat su un paniere, equal-weight.
Replica live di honest_improve2._tr_basket_daily."""
from __future__ import annotations
import json
import time
from datetime import datetime, timezone
from pathlib import Path
import numpy as np
import pandas as pd
FEE_RT, LEV, POS = 0.001, 3.0, 0.15
def _ema(x, n):
return pd.Series(x).ewm(span=n, adjust=False).mean().values
class BasketTrendWorker:
def __init__(self, universe, tf="4h", capital=1000.0, position_size=POS,
leverage=LEV, fee_rt=FEE_RT, name="TR01_basket",
data_dir=Path("data/portfolio_paper")):
self.universe = list(universe)
self.tf = tf
self.initial_capital = capital
self.capital = capital
self.position_size = position_size
self.leverage = leverage
self.fee_rt = fee_rt
self.worker_id = f"{name}__{'-'.join(self.universe)}__{tf}"
self.work_dir = Path(data_dir) / self.worker_id
self.work_dir.mkdir(parents=True, exist_ok=True)
self.status_path = self.work_dir / "status.json"
self.trades_path = self.work_dir / "trades.jsonl"
self.positions = {a: 0.0 for a in self.universe}
self.last_bar_ts = {a: 0 for a in self.universe}
self.in_position = False
self._load()
def _load(self):
if self.status_path.exists():
s = json.loads(self.status_path.read_text())
self.capital = s.get("capital", self.capital)
self.positions = {**self.positions, **s.get("positions", {})}
self.last_bar_ts = {**self.last_bar_ts, **s.get("last_bar_ts", {})}
self.in_position = any(v > 0 for v in self.positions.values())
def _save(self):
self.status_path.write_text(json.dumps({
"capital": round(self.capital, 2), "positions": self.positions,
"in_position": self.in_position, # per hourly_report (osservabilita')
"last_bar_ts": self.last_bar_ts,
"ts": datetime.now(timezone.utc).isoformat()}, indent=2))
def tick(self, data: dict):
now_ms = int(time.time() * 1000)
rets = []
for a in self.universe:
df = data.get(a)
if df is None or len(df) < 111:
continue
# Scarta la barra 4h IN FORMAZIONE: crossover EMA e booking del return
# valutati SOLO su barre COMPLETE, come il reference
# honest_improve2._tr_basket_daily (lezione EXIT-16; evidenza live: flip
# SOL 0->1->0 in 59min nella stessa finestra 4h, -9.3% di glitch).
from src.live.bars import last_bar_is_forming
ts_arr = df["timestamp"].values.astype("int64")
c = df["close"].values
if last_bar_is_forming(ts_arr, now_ms):
c, ts_arr = c[:-1], ts_arr[:-1]
if len(c) < 110:
continue
ef, es = _ema(c, 20)[-1], _ema(c, 100)[-1]
target = 1.0 if ef > es else 0.0
bar_ts = int(ts_arr[-1])
prev = self.positions[a]
if self.last_bar_ts[a] and bar_ts > self.last_bar_ts[a] and prev > 0:
r = (c[-1] - c[-2]) / c[-2]
rets.append(self.position_size * self.leverage * r * prev)
if target != prev:
# fee = FEE_RT/2 * LEV come il reference (honest_improve2.py:150):
# il notional e' leveraged, la fee si paga sul notional
self.capital -= self.capital * self.position_size * self.leverage * (self.fee_rt / 2) * abs(target - prev) / len(self.universe)
self._log(a, prev, target, float(c[-1]))
self.positions[a] = target
self.last_bar_ts[a] = bar_ts
if rets:
self.capital = max(self.capital * (1 + float(np.mean(rets))), 10.0)
self.in_position = any(v > 0 for v in self.positions.values())
self._save()
def _log(self, asset, frm, to, price):
with open(self.trades_path, "a") as f:
f.write(json.dumps({"ts": datetime.now(timezone.utc).isoformat(),
"asset": asset, "from": frm, "to": to,
"price": round(price, 6), "capital": round(self.capital, 2)}) + "\n")
@property
def status_summary(self):
longs = [a for a, v in self.positions.items() if v > 0]
return f"{self.worker_id}: cap={self.capital:.0f} long={longs}"