feat(pairs): position_size per-famiglia, PAIRS 0.5->0.20 (gate PORT06)

Punto 8 roadmap sweep. La famiglia pairs (SENZA stop, validata a pos 0.15 lev 3 =
esposizione 0.45) girava col position_size globale 0.5 a leva 2 = esposizione 1.0,
~2.2x il validato (ETH/BTC DD grezzo 78% a quella taglia; live: ADA/ETH -4.26%
sleeve in un trade il 2026-06-05).

Gate pairspos_port06_impact.py (pairs_sim alla leva live 2x, parita' canonica
1.00000 5/5, PORT06 pesi cap): griglia pos {0.5, 0.25, 0.20, 0.15}. Il gate
meccanico boccia ogni riduzione (costa OOS Sharpe — il compounding in-sample a 0.5
e' fantasia: +2.6e9% ETH/BTC), ma 0.20 e' il ginocchio del trade-off: OOS DD
3.40->1.26% (meglio anche di 0.15), famiglia DD 14->5.8%, costo OOS Sharpe
9.05->8.43. Decisione assicurativa (utente), come il precedente cap SHAPE.

- runner.pos_for_spec(sid, global, family_overrides): override per-famiglia
  (chiave weighting.family_of), plumbato in build_worker_for.
- portfolios.yml: position_size_family {PAIRS: 0.20} (globale 0.5 invariato).
- Test: pos_for_spec mapping + PairsWorker.position_size (74/74 verdi).

Nota transizione: la posizione LTC/ETH aperta verra' chiusa col pos nuovo
(one-time). Diario docs/diary/2026-06-07-pairspos-gate.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-07 10:44:15 +00:00
parent 0aaab08612
commit 6097205694
6 changed files with 242 additions and 2 deletions
+16 -1
View File
@@ -40,6 +40,13 @@ _LOOKBACK_DAYS = {"1h": 90, "4h": 220, "1d": 440}
_ML_LOOKBACK_DAYS = 365
def pos_for_spec(sid: str, global_ps: float, family_overrides: dict[str, float]) -> float:
"""position_size effettivo di uno sleeve: override per-famiglia (chiave =
weighting.family_of: PAIRS/FADE/HONEST/SHAPE/TSM) o globale."""
from src.portfolio.weighting import family_of
return family_overrides.get(family_of(sid), global_ps)
def build_worker_for(spec: SleeveSpec, alloc_capital: float, leverage: float,
data_dir: Path = DATA_DIR, position_size: float = 0.15,
executor=None, exec_instrument: str | None = None):
@@ -187,6 +194,12 @@ def run(config_path: str = "portfolios.yml"):
# Con leva 2x: notional = capital * position_size * 2. A 0.5 ogni sleeve in posizione
# impegna il 100% della sua fetta (max impiego senza debito di margine); DD scala ~lineare.
position_size = float(_ov.get("position_size", 0.15))
# Override PER-FAMIGLIA (improvement-sweep punto 8): la chiave e' la famiglia di
# weighting.family_of (PAIRS/FADE/HONEST/SHAPE/TSM). Nato per i pairs: tutta la
# validazione PR01 e' a pos 0.15 e la famiglia e' SENZA stop -> il pos globale 0.5
# la faceva girare a ~2.2x l'esposizione validata. Gate: pairspos_port06_impact.py.
ps_family = {str(k).upper(): float(v)
for k, v in (_ov.get("position_size_family") or {}).items()}
def _supported(s):
return s.kind in ("pairs",) + _MULTI_KINDS or s.name in _STRAT_MODULE
@@ -229,8 +242,10 @@ def run(config_path: str = "portfolios.yml"):
for s in live_specs:
ex, inst = _exec_for(s)
workers[s.sid] = build_worker_for(s, alloc[s.sid], p.leverage,
position_size=position_size,
position_size=pos_for_spec(s.sid, position_size, ps_family),
executor=ex, exec_instrument=inst)
if ps_family:
print(f"[runner] position_size globale={position_size} override famiglia={ps_family}")
# lookback (giorni) richiesto per ogni asset = max sui worker che lo usano
asset_days: dict[str, int] = {}