fix(portfolio): runner data_dir dedicata, no resize posizioni aperte, poll da config, +test cap/cluster_rp

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 16:22:33 +02:00
parent d02bc10ab5
commit 0f582db265
4 changed files with 49 additions and 8 deletions
+10 -3
View File
@@ -17,7 +17,7 @@ _STRAT_MODULE = {
"MR07": "MR07_return_reversal", "SH01": "SH01_shape_ml", "MR07": "MR07_return_reversal", "SH01": "SH01_shape_ml",
# DIP01/TR01/ROT02 sono honest a sé: vedi nota nel design (worker dedicati in fase 2) # DIP01/TR01/ROT02 sono honest a sé: vedi nota nel design (worker dedicati in fase 2)
} }
DATA_DIR = Path("data/paper_trades") DATA_DIR = Path("data/portfolio_paper")
def build_worker_for(spec: SleeveSpec, alloc_capital: float, leverage: float, def build_worker_for(spec: SleeveSpec, alloc_capital: float, leverage: float,
@@ -50,11 +50,15 @@ def _worker_equity(w) -> float:
def rebalance_allocations(ledger: PortfolioLedger, workers: dict, weights: dict[str, float]): def rebalance_allocations(ledger: PortfolioLedger, workers: dict, weights: dict[str, float]):
"""Ribilancio: total_capital = Σ equity sleeve; riallinea il capitale-base di ogni worker """Ribilancio: total_capital = Σ equity sleeve; riallinea il capitale-base di ogni worker
a peso×total. Le posizioni APERTE restano sul loro notional (approssimazione dichiarata).""" a peso×total. I worker con posizione APERTA NON vengono ritoccati (la posizione mantiene
il suo notional, come da approssimazione dichiarata): il nuovo capitale-base si applica
alla prossima posizione, quando il worker è flat."""
ledger.total_capital = sum(_worker_equity(w) for w in workers.values()) ledger.total_capital = sum(_worker_equity(w) for w in workers.values())
alloc = ledger.allocate(weights) alloc = ledger.allocate(weights)
for sid, w in workers.items(): for sid, w in workers.items():
inner = getattr(w, "worker", w) inner = getattr(w, "worker", w)
if getattr(inner, "in_position", False):
continue
inner.capital = alloc.get(sid, inner.capital) inner.capital = alloc.get(sid, inner.capital)
ledger.save() ledger.save()
@@ -74,6 +78,10 @@ def run(config_path: str = "portfolios.yml"):
p: Portfolio = load_active_portfolio(config_path) p: Portfolio = load_active_portfolio(config_path)
import yaml as _yaml
_ov = (_yaml.safe_load(__import__("pathlib").Path(config_path).read_text()) or {}).get("overrides", {})
poll = int(_ov.get("poll_seconds", 60))
def _supported(s): def _supported(s):
return s.kind == "pairs" or s.name in _STRAT_MODULE return s.kind == "pairs" or s.name in _STRAT_MODULE
live_specs = [s for s in p.sleeves if _supported(s)] live_specs = [s for s in p.sleeves if _supported(s)]
@@ -94,7 +102,6 @@ def run(config_path: str = "portfolios.yml"):
inst_map = dict(INSTRUMENT_MAP) inst_map = dict(INSTRUMENT_MAP)
last_day = "" last_day = ""
poll = 60
while True: while True:
try: try:
keys = set() keys = set()
@@ -0,0 +1,10 @@
import pytest
from scripts.portfolios._defs import PORTFOLIOS
def test_port06_cap_backtest_numbers_locked():
r = PORTFOLIOS["PORT06"].backtest()
# regression-lock dei numeri del default (cap pairs 0.33) — vedi report_families
assert r.full["sharpe"] == pytest.approx(6.07, abs=0.15)
assert r.oos["sharpe"] == pytest.approx(8.19, abs=0.25)
assert r.full["dd"] == pytest.approx(4.9, abs=0.5)
+17 -5
View File
@@ -2,12 +2,24 @@ from src.portfolio.runner import rebalance_allocations
from src.portfolio.ledger import PortfolioLedger from src.portfolio.ledger import PortfolioLedger
def test_rebalance_resizes_to_total(tmp_path):
L = PortfolioLedger("PX", total_capital=1000.0, data_dir=tmp_path)
class FakeWorker: class FakeWorker:
def __init__(self, cap): self.capital = cap def __init__(self, cap, in_position=False):
workers = {"a": FakeWorker(700.0), "b": FakeWorker(500.0)} self.capital = cap
self.in_position = in_position
def test_rebalance_resizes_flat_workers(tmp_path):
L = PortfolioLedger("PX", total_capital=1000.0, data_dir=tmp_path)
workers = {"a": FakeWorker(700.0), "b": FakeWorker(500.0)} # equity 1200, both flat
rebalance_allocations(L, workers, {"a": 0.5, "b": 0.5}) rebalance_allocations(L, workers, {"a": 0.5, "b": 0.5})
assert L.total_capital == 1200.0 assert L.total_capital == 1200.0
assert workers["a"].capital == 600.0 and workers["b"].capital == 600.0 assert workers["a"].capital == 600.0 and workers["b"].capital == 600.0
def test_rebalance_skips_in_position_worker(tmp_path):
L = PortfolioLedger("PX", total_capital=1000.0, data_dir=tmp_path)
workers = {"a": FakeWorker(700.0, in_position=True), "b": FakeWorker(500.0)}
rebalance_allocations(L, workers, {"a": 0.5, "b": 0.5})
assert L.total_capital == 1200.0
assert workers["a"].capital == 700.0 # invariato: posizione aperta
assert workers["b"].capital == 600.0 # flat: riallineato
+12
View File
@@ -39,3 +39,15 @@ def test_inverse_vol_prefers_low_vol():
w = W.inverse_vol(["lo", "hi"], df, lookback=90) w = W.inverse_vol(["lo", "hi"], df, lookback=90)
assert w["lo"] > w["hi"] assert w["lo"] > w["hi"]
assert pytest.approx(sum(w.values())) == 1.0 assert pytest.approx(sum(w.values())) == 1.0
def test_cluster_rp_equal_across_clusters():
idx = pd.date_range("2024-01-01", periods=100, freq="D", tz="UTC")
rng = np.random.default_rng(1)
cols = ["MR01_BTC", "MR02_BTC", "PR_ETHBTC"]
df = pd.DataFrame({c: rng.normal(0, 0.02, 100) for c in cols}, index=idx)
clusters = {"MR01_BTC": "BTC-rev", "MR02_BTC": "BTC-rev", "PR_ETHBTC": "ETH-rev"}
w = W.cluster_rp(cols, clusters, df, lookback=90)
assert pytest.approx(sum(w.values())) == 1.0
# due cluster equipesati: il cluster con 1 solo sleeve (ETH-rev) prende ~0.5
assert pytest.approx(w["PR_ETHBTC"], abs=1e-9) == 0.5