diff --git a/src/portfolio/runner.py b/src/portfolio/runner.py index 9184d07..298eed6 100644 --- a/src/portfolio/runner.py +++ b/src/portfolio/runner.py @@ -17,7 +17,7 @@ _STRAT_MODULE = { "MR07": "MR07_return_reversal", "SH01": "SH01_shape_ml", # 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, @@ -50,11 +50,15 @@ def _worker_equity(w) -> float: 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).""" + 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()) alloc = ledger.allocate(weights) for sid, w in workers.items(): inner = getattr(w, "worker", w) + if getattr(inner, "in_position", False): + continue inner.capital = alloc.get(sid, inner.capital) ledger.save() @@ -74,6 +78,10 @@ def run(config_path: str = "portfolios.yml"): 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): return s.kind == "pairs" or s.name in _STRAT_MODULE 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) last_day = "" - poll = 60 while True: try: keys = set() diff --git a/tests/portfolio/test_backtest_parity_cap.py b/tests/portfolio/test_backtest_parity_cap.py new file mode 100644 index 0000000..e1102f2 --- /dev/null +++ b/tests/portfolio/test_backtest_parity_cap.py @@ -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) diff --git a/tests/portfolio/test_runner_rebalance.py b/tests/portfolio/test_runner_rebalance.py index 064184e..7dfa2ad 100644 --- a/tests/portfolio/test_runner_rebalance.py +++ b/tests/portfolio/test_runner_rebalance.py @@ -2,12 +2,24 @@ from src.portfolio.runner import rebalance_allocations 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: + def __init__(self, cap, in_position=False): + self.capital = cap + self.in_position = in_position - class FakeWorker: - def __init__(self, cap): self.capital = cap - workers = {"a": FakeWorker(700.0), "b": FakeWorker(500.0)} + +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}) assert L.total_capital == 1200.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 diff --git a/tests/portfolio/test_weighting.py b/tests/portfolio/test_weighting.py index 0281a69..9c83b83 100644 --- a/tests/portfolio/test_weighting.py +++ b/tests/portfolio/test_weighting.py @@ -39,3 +39,15 @@ def test_inverse_vol_prefers_low_vol(): w = W.inverse_vol(["lo", "hi"], df, lookback=90) assert w["lo"] > w["hi"] 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