feat(portfolio): PortfolioRunner live (data v2, tick, ribilancio giornaliero, ledger)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 16:05:07 +02:00
parent 2b3d3e3ff8
commit a5547fb3d2
3 changed files with 160 additions and 0 deletions
+108
View File
@@ -41,3 +41,111 @@ def build_worker_for(spec: SleeveSpec, alloc_capital: float, leverage: float,
if spec.kind == "ml": # SH01: retraining periodico
return MLWorkerWrapper(worker, {"retrain_hours": 24})
return worker
def _worker_equity(w) -> float:
inner = getattr(w, "worker", w) # smonta MLWorkerWrapper
return float(getattr(inner, "capital", 0.0))
def rebalance_allocations(ledger: PortfolioLedger, workers: dict, weights: dict[str, float]):
"""Ribilancio: total_capital = Σ equity sleeve; riallinea il capitale-base di ogni worker
a peso×total. Le posizioni APERTE restano sul loro notional (approssimazione dichiarata)."""
ledger.total_capital = sum(_worker_equity(w) for w in workers.values())
alloc = ledger.allocate(weights)
for sid, w in workers.items():
inner = getattr(w, "worker", w)
inner.capital = alloc.get(sid, inner.capital)
ledger.save()
def run(config_path: str = "portfolios.yml"):
"""Loop live a portafoglio. Data layer Cerbero v2; ribilancio a fine giornata UTC.
Gli sleeve senza worker live (honest DIP01/TR01/ROT02) vengono SALTATI con warning
(restano solo in backtest); i pesi sono rinormalizzati sugli sleeve eseguibili."""
import time
from datetime import datetime, timezone, timedelta
import pandas as pd
from src.portfolio.base import load_active_portfolio
from src.portfolio.sleeves import sleeve_returns_df
from src.portfolio import weighting as W
from src.live.cerbero_client import CerberoClient
from src.live.multi_runner import INSTRUMENT_MAP
p: Portfolio = load_active_portfolio(config_path)
def _supported(s):
return s.kind == "pairs" or s.name in _STRAT_MODULE
live_specs = [s for s in p.sleeves if _supported(s)]
skipped = [s.sid for s in p.sleeves if not _supported(s)]
if skipped:
print(f"[runner] sleeve saltati nel live (worker non disponibili): {skipped}")
live_ids = [s.sid for s in live_specs]
clusters = {s.sid: (s.cluster or s.sid) for s in live_specs}
ledger = PortfolioLedger(p.code, total_capital=p.total_capital)
client = CerberoClient()
dr = sleeve_returns_df(live_ids)
weights = W.weight_vector(p.weighting, live_ids, dr, weights=p.weights,
caps=p.caps, clusters=clusters, lookback=p.vol_lookback)
alloc = ledger.allocate(weights)
workers = {s.sid: build_worker_for(s, alloc[s.sid], p.leverage) for s in live_specs}
inst_map = dict(INSTRUMENT_MAP)
last_day = ""
poll = 60
while True:
try:
keys = set()
for s in live_specs:
if s.kind == "pairs":
keys.add((s.a, s.tf)); keys.add((s.b, s.tf))
else:
keys.add((s.asset, s.tf))
cache = {}
end = datetime.now(timezone.utc); start = end - timedelta(days=60)
for asset, tf in keys:
inst = inst_map.get(asset, f"{asset}-PERPETUAL")
candles = client.get_historical_v2(inst, start.strftime("%Y-%m-%d"),
end.strftime("%Y-%m-%d"), tf)
if candles:
df = pd.DataFrame(candles)
df["timestamp"] = df["timestamp"].astype("int64")
cache[(asset, tf)] = df.sort_values("timestamp").reset_index(drop=True)
for s in live_specs:
w = workers[s.sid]
if s.kind == "pairs":
ka, kb = (s.a, s.tf), (s.b, s.tf)
if ka in cache and kb in cache:
w.tick(cache[ka], cache[kb])
else:
key = (s.asset, s.tf)
if key in cache:
inner = getattr(w, "worker", w)
if hasattr(w, "needs_training") and w.needs_training():
w.train(cache[key], hold=inner.hold_bars)
w.tick(cache[key])
ledger.update_equity({sid: _worker_equity(wk) for sid, wk in workers.items()})
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
if today != last_day and last_day:
dr = sleeve_returns_df(live_ids)
weights = W.weight_vector(p.weighting, live_ids, dr, weights=p.weights,
caps=p.caps, clusters=clusters, lookback=p.vol_lookback)
rebalance_allocations(ledger, workers, weights)
last_day = today
ledger.save()
except KeyboardInterrupt:
ledger.save()
print("shutdown")
break
except Exception as e:
print(f"[runner] errore: {e}")
time.sleep(poll)
if __name__ == "__main__":
run()