160ad300be
- deribit_book_sleeves(): TP01 75% + SKH01 25% — the two directional BTC/ETH legs on ONE venue (Deribit), both since 2019. Excludes XS01 (Hyperliquid/stat-mode) & VRP01 (modeled options). FULL Sharpe 1.78 / HOLD 1.17 / DD 9.4% (research). - rebalance_sim(): realistic PERIODIC rebalancing (drift between dates, turnover cost at Deribit-taker ~5bps/side) vs the idealized continuous rebalance of combined_daily. period=1 + cost=0 reduces to continuous (tested). - run_deribit_book.py: report — continuous vs weekly/biweekly/monthly rebal, per-year, accumulation €2k & $600-real, min-order $5 note. Finding: turnover is LOW (0.2-0.4x/yr), so monthly rebal (€7,919) ~= continuous (€7,938) — cost is negligible; daily would be sub-min-order fiction at $600 -> use >= weekly. - +2 tests (rebalance_sim continuity & cost). Full suite green. TP01 is the only live-armed leg; SKH01 is the candidate 2nd leg (validate execution code first). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
4.1 KiB
Python
101 lines
4.1 KiB
Python
"""Test del contenitore portafoglio estensibile."""
|
|
import sys
|
|
from pathlib import Path
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics, rebalance_sim
|
|
|
|
|
|
def _const_sleeve(name, weight, val, n=400):
|
|
idx = pd.date_range("2020-01-01", periods=n, freq="1D", tz="UTC")
|
|
return Sleeve(name, weight, lambda: pd.Series(val, index=idx))
|
|
|
|
|
|
def _ret_series(vals):
|
|
idx = pd.date_range("2020-01-01", periods=len(vals), freq="1D", tz="UTC")
|
|
return pd.Series(vals, index=idx)
|
|
|
|
|
|
def test_rebalance_sim_no_cost_period1_matches_continuous():
|
|
"""period=1 + cost=0 deve coincidere col rebalance-continuo (weighted-return giornaliero)."""
|
|
rng = np.random.default_rng(0)
|
|
A = _ret_series(rng.normal(0.001, 0.02, 300))
|
|
B = _ret_series(rng.normal(0.000, 0.03, 300))
|
|
w = {"A": 0.6, "B": 0.4}
|
|
sim = rebalance_sim({"A": A, "B": B}, w, period_days=1, cost_rate=0.0)
|
|
cont = 0.6 * A + 0.4 * B
|
|
assert np.allclose(sim["daily"].values, cont.values, atol=1e-12)
|
|
assert sim["n_rebalances"] == 300
|
|
|
|
|
|
def test_rebalance_sim_cost_reduces_return_and_counts():
|
|
"""Il costo del turnover abbassa il rendimento; ribilanci meno frequenti = meno costo."""
|
|
rng = np.random.default_rng(1)
|
|
A = _ret_series(rng.normal(0.001, 0.02, 360))
|
|
B = _ret_series(rng.normal(0.001, 0.04, 360))
|
|
w = {"A": 0.5, "B": 0.5}
|
|
free = rebalance_sim({"A": A, "B": B}, w, period_days=7, cost_rate=0.0)["daily"]
|
|
weekly = rebalance_sim({"A": A, "B": B}, w, period_days=7, cost_rate=0.001)
|
|
monthly = rebalance_sim({"A": A, "B": B}, w, period_days=30, cost_rate=0.001)
|
|
assert weekly["daily"].sum() < free.sum() # il costo morde
|
|
assert monthly["n_rebalances"] < weekly["n_rebalances"] # mensile ribilancia meno
|
|
assert weekly["turnover_per_year"] > 0
|
|
|
|
|
|
def test_single_sleeve_equals_itself():
|
|
s = _const_sleeve("A", 1.0, 0.001)
|
|
pf = StrategyPortfolio([s])
|
|
combo = pf.combined_daily()
|
|
assert np.allclose(combo.values, s.daily().values)
|
|
assert pf.weights() == {"A": 1.0}
|
|
|
|
|
|
def test_weights_normalize():
|
|
pf = StrategyPortfolio([_const_sleeve("A", 3.0, 0.001), _const_sleeve("B", 1.0, 0.002)])
|
|
w = pf.weights()
|
|
assert abs(sum(w.values()) - 1.0) < 1e-12
|
|
assert abs(w["A"] - 0.75) < 1e-12 and abs(w["B"] - 0.25) < 1e-12
|
|
|
|
|
|
def test_equal_weight_combine():
|
|
a, b = _const_sleeve("A", 1.0, 0.001), _const_sleeve("B", 1.0, 0.003)
|
|
pf = StrategyPortfolio([a, b])
|
|
combo = pf.combined_daily()
|
|
assert np.allclose(combo.values, 0.5 * 0.001 + 0.5 * 0.003) # 0.002
|
|
|
|
|
|
def test_to_daily_compounds_intraday():
|
|
# due barre da +1% nello stesso giorno -> +2.01% giornaliero
|
|
idx = pd.to_datetime(["2020-01-01T00:00", "2020-01-01T12:00"], utc=True)
|
|
d = to_daily(pd.Series([0.01, 0.01], index=idx))
|
|
assert len(d) == 1 and abs(d.iloc[0] - (1.01 * 1.01 - 1)) < 1e-12
|
|
|
|
|
|
def test_metrics_basic():
|
|
idx = pd.date_range("2020-01-01", periods=730, freq="1D", tz="UTC")
|
|
m = metrics(pd.Series(0.0005, index=idx)) # ritorno costante positivo
|
|
assert m["ret"] > 0 and m["maxdd"] == 0.0 and m["n"] == 730
|
|
|
|
|
|
def test_outer_join_renormalizes_late_sleeve():
|
|
# sleeve con date d'inizio diverse: prima parte A da solo (peso rinormalizzato a 1),
|
|
# poi A+B (pesi 0.7/0.3). Il portafoglio NON si tronca alla finestra comune.
|
|
idxA = pd.date_range("2020-01-01", periods=120, freq="1D", tz="UTC")
|
|
idxB = pd.date_range("2020-02-15", periods=60, freq="1D", tz="UTC")
|
|
A = Sleeve("A", 0.7, lambda: pd.Series(0.001, index=idxA))
|
|
B = Sleeve("B", 0.3, lambda: pd.Series(0.003, index=idxB))
|
|
combo = StrategyPortfolio([A, B]).combined_daily()
|
|
assert abs(combo.iloc[0] - 0.001) < 1e-12 # solo A -> 100% A
|
|
both = combo[combo.index >= idxB[0]]
|
|
assert abs(both.iloc[0] - (0.7 * 0.001 + 0.3 * 0.003)) < 1e-12 # blend rinormalizzato
|
|
assert len(combo) == 120 # span completo di A, non tronca
|
|
|
|
|
|
def test_empty_portfolio_raises():
|
|
with pytest.raises(ValueError):
|
|
StrategyPortfolio([])
|