chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""Ledger aggregato del portafoglio: capitale, allocazioni, equity, PnL, peak/DD, persistenza."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class PortfolioLedger:
|
||||
def __init__(self, code: str, total_capital: float = 1000.0,
|
||||
data_dir: Path = Path("data/portfolios")):
|
||||
self.code = code
|
||||
self.initial_capital = total_capital
|
||||
self.total_capital = total_capital
|
||||
self.work_dir = Path(data_dir) / code
|
||||
self.work_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.status_path = self.work_dir / "status.json"
|
||||
self.equity_path = self.work_dir / "equity.jsonl"
|
||||
self.events_path = self.work_dir / "events.jsonl"
|
||||
self.equity = total_capital
|
||||
self.peak = total_capital
|
||||
self.max_dd = 0.0
|
||||
self.weights: dict[str, float] = {}
|
||||
self.alloc: dict[str, float] = {}
|
||||
self.last_rebalance = ""
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
if not self.status_path.exists():
|
||||
return
|
||||
s = json.loads(self.status_path.read_text())
|
||||
self.total_capital = s.get("total_capital", self.total_capital)
|
||||
self.equity = s.get("equity", self.equity)
|
||||
self.peak = s.get("peak", self.peak)
|
||||
self.max_dd = s.get("max_dd", self.max_dd)
|
||||
self.weights = s.get("weights", {})
|
||||
self.alloc = s.get("alloc", {})
|
||||
self.last_rebalance = s.get("last_rebalance", "")
|
||||
|
||||
def allocate(self, weights: dict[str, float],
|
||||
reserved: dict[str, float] | None = None) -> dict[str, float]:
|
||||
"""alloc per sid = peso × total_capital. `reserved` {sid: capitale trattenuto}
|
||||
per i worker con posizione APERTA: quel capitale e' deployato, non
|
||||
ridistribuibile -> i flat si dividono (total − Σreserved) per peso
|
||||
RINORMALIZZATO sui soli flat. Cosi' Σalloc == total_capital ed equity e'
|
||||
CONSERVATA dal ribilancio. Senza reserved (default): comportamento storico
|
||||
(alloc = peso×total per tutti), corretto solo se tutti i worker sono flat
|
||||
(allocazione iniziale). Fix 2026-06-13: prima i flat si dividevano l'INTERO
|
||||
total includendo il capitale degli in-position -> doppio conteggio, equity
|
||||
gonfiata di Σ(capital_in_pos − alloc_in_pos) (caso MR02_BTC 15m seedato e in
|
||||
posizione al ribilancio: +4.77). Vedi docs/diary/2026-06-13-rebalance-conservation.md."""
|
||||
self.weights = dict(weights)
|
||||
reserved = reserved or {}
|
||||
distributable = self.total_capital - sum(reserved.values())
|
||||
flat = {sid: w for sid, w in weights.items() if sid not in reserved}
|
||||
wsum = sum(flat.values())
|
||||
self.alloc = {sid: round(cap, 6) for sid, cap in reserved.items()}
|
||||
for sid, w in flat.items():
|
||||
share = (w / wsum) if wsum > 0 else (1.0 / len(flat) if flat else 0.0)
|
||||
self.alloc[sid] = round(distributable * share, 6)
|
||||
self.last_rebalance = datetime.now(timezone.utc).isoformat()
|
||||
self._append(self.events_path, {"event": "rebalance", "weights": self.weights,
|
||||
"total_capital": self.total_capital,
|
||||
"reserved": sorted(reserved)})
|
||||
return self.alloc
|
||||
|
||||
def update_equity(self, sleeve_equity: dict[str, float], pnl_day: float = 0.0):
|
||||
self.equity = float(sum(sleeve_equity.values()))
|
||||
if self.equity > self.peak:
|
||||
self.peak = self.equity
|
||||
dd = (self.peak - self.equity) / self.peak * 100 if self.peak > 0 else 0.0
|
||||
self.max_dd = max(self.max_dd, dd)
|
||||
self._append(self.equity_path, {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"equity": round(self.equity, 2), "dd": round(dd, 3),
|
||||
"pnl_day": round(pnl_day, 2),
|
||||
"pnl_total": round(self.equity - self.initial_capital, 2),
|
||||
})
|
||||
|
||||
def save(self):
|
||||
self.status_path.write_text(json.dumps({
|
||||
"code": self.code, "total_capital": round(self.total_capital, 2),
|
||||
"equity": round(self.equity, 2), "peak": round(self.peak, 2),
|
||||
"max_dd": round(self.max_dd, 3), "weights": self.weights,
|
||||
"alloc": self.alloc, "last_rebalance": self.last_rebalance,
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
}, indent=2))
|
||||
|
||||
@staticmethod
|
||||
def _append(path: Path, row: dict):
|
||||
with open(path, "a") as f:
|
||||
f.write(json.dumps(row) + "\n")
|
||||
Reference in New Issue
Block a user