From eaf4800b6dc935944b1e277d87f54314a434e62f Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Fri, 29 May 2026 15:58:33 +0200 Subject: [PATCH] feat(portfolio): definizioni PORT01-06 + report run() (default PORT06) Co-Authored-By: Claude Sonnet 4.6 --- scripts/portfolios/PORT01_honest.py | 28 +++++++++++++++ scripts/portfolios/PORT02_fade.py | 28 +++++++++++++++ scripts/portfolios/PORT03_master.py | 28 +++++++++++++++ scripts/portfolios/PORT04_master_pairs.py | 28 +++++++++++++++ scripts/portfolios/PORT05_master_esteso.py | 28 +++++++++++++++ scripts/portfolios/PORT06_master_shape.py | 28 +++++++++++++++ scripts/portfolios/__init__.py | 0 scripts/portfolios/_defs.py | 40 ++++++++++++++++++++++ tests/portfolio/test_definitions.py | 17 +++++++++ 9 files changed, 225 insertions(+) create mode 100644 scripts/portfolios/PORT01_honest.py create mode 100644 scripts/portfolios/PORT02_fade.py create mode 100644 scripts/portfolios/PORT03_master.py create mode 100644 scripts/portfolios/PORT04_master_pairs.py create mode 100644 scripts/portfolios/PORT05_master_esteso.py create mode 100644 scripts/portfolios/PORT06_master_shape.py create mode 100644 scripts/portfolios/__init__.py create mode 100644 scripts/portfolios/_defs.py create mode 100644 tests/portfolio/test_definitions.py diff --git a/scripts/portfolios/PORT01_honest.py b/scripts/portfolios/PORT01_honest.py new file mode 100644 index 0000000..ab1b10f --- /dev/null +++ b/scripts/portfolios/PORT01_honest.py @@ -0,0 +1,28 @@ +"""PORT01 — Honest (default). Report backtest del portafoglio.""" +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from scripts.portfolios._defs import PORTFOLIOS # noqa: E402 + +CODE = "PORT01" + + +def run(): + p = PORTFOLIOS[CODE] + r = p.backtest() + print("=" * 80) + print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x") + print("=" * 80) + print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% " + f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}") + print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}") + print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())}) + print(" rischio % per sleeve:", {k: round(v, 1) for k, v in + sorted(r.risk.items(), key=lambda x: -x[1])}) + + +if __name__ == "__main__": + run() diff --git a/scripts/portfolios/PORT02_fade.py b/scripts/portfolios/PORT02_fade.py new file mode 100644 index 0000000..b89beaf --- /dev/null +++ b/scripts/portfolios/PORT02_fade.py @@ -0,0 +1,28 @@ +"""PORT02 — Fade master (default). Report backtest del portafoglio.""" +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from scripts.portfolios._defs import PORTFOLIOS # noqa: E402 + +CODE = "PORT02" + + +def run(): + p = PORTFOLIOS[CODE] + r = p.backtest() + print("=" * 80) + print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x") + print("=" * 80) + print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% " + f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}") + print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}") + print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())}) + print(" rischio % per sleeve:", {k: round(v, 1) for k, v in + sorted(r.risk.items(), key=lambda x: -x[1])}) + + +if __name__ == "__main__": + run() diff --git a/scripts/portfolios/PORT03_master.py b/scripts/portfolios/PORT03_master.py new file mode 100644 index 0000000..cde426f --- /dev/null +++ b/scripts/portfolios/PORT03_master.py @@ -0,0 +1,28 @@ +"""PORT03 — Master (default). Report backtest del portafoglio.""" +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from scripts.portfolios._defs import PORTFOLIOS # noqa: E402 + +CODE = "PORT03" + + +def run(): + p = PORTFOLIOS[CODE] + r = p.backtest() + print("=" * 80) + print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x") + print("=" * 80) + print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% " + f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}") + print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}") + print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())}) + print(" rischio % per sleeve:", {k: round(v, 1) for k, v in + sorted(r.risk.items(), key=lambda x: -x[1])}) + + +if __name__ == "__main__": + run() diff --git a/scripts/portfolios/PORT04_master_pairs.py b/scripts/portfolios/PORT04_master_pairs.py new file mode 100644 index 0000000..dbdc861 --- /dev/null +++ b/scripts/portfolios/PORT04_master_pairs.py @@ -0,0 +1,28 @@ +"""PORT04 — Master + pairs (default). Report backtest del portafoglio.""" +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from scripts.portfolios._defs import PORTFOLIOS # noqa: E402 + +CODE = "PORT04" + + +def run(): + p = PORTFOLIOS[CODE] + r = p.backtest() + print("=" * 80) + print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x") + print("=" * 80) + print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% " + f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}") + print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}") + print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())}) + print(" rischio % per sleeve:", {k: round(v, 1) for k, v in + sorted(r.risk.items(), key=lambda x: -x[1])}) + + +if __name__ == "__main__": + run() diff --git a/scripts/portfolios/PORT05_master_esteso.py b/scripts/portfolios/PORT05_master_esteso.py new file mode 100644 index 0000000..05aa311 --- /dev/null +++ b/scripts/portfolios/PORT05_master_esteso.py @@ -0,0 +1,28 @@ +"""PORT05 — Master esteso (default). Report backtest del portafoglio.""" +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from scripts.portfolios._defs import PORTFOLIOS # noqa: E402 + +CODE = "PORT05" + + +def run(): + p = PORTFOLIOS[CODE] + r = p.backtest() + print("=" * 80) + print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x") + print("=" * 80) + print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% " + f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}") + print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}") + print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())}) + print(" rischio % per sleeve:", {k: round(v, 1) for k, v in + sorted(r.risk.items(), key=lambda x: -x[1])}) + + +if __name__ == "__main__": + run() diff --git a/scripts/portfolios/PORT06_master_shape.py b/scripts/portfolios/PORT06_master_shape.py new file mode 100644 index 0000000..29ad8f1 --- /dev/null +++ b/scripts/portfolios/PORT06_master_shape.py @@ -0,0 +1,28 @@ +"""PORT06 — Master + shape (default). Report backtest del portafoglio.""" +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from scripts.portfolios._defs import PORTFOLIOS # noqa: E402 + +CODE = "PORT06" + + +def run(): + p = PORTFOLIOS[CODE] + r = p.backtest() + print("=" * 80) + print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x") + print("=" * 80) + print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% " + f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}") + print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}") + print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())}) + print(" rischio % per sleeve:", {k: round(v, 1) for k, v in + sorted(r.risk.items(), key=lambda x: -x[1])}) + + +if __name__ == "__main__": + run() diff --git a/scripts/portfolios/__init__.py b/scripts/portfolios/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/portfolios/_defs.py b/scripts/portfolios/_defs.py new file mode 100644 index 0000000..6404a86 --- /dev/null +++ b/scripts/portfolios/_defs.py @@ -0,0 +1,40 @@ +"""Definizioni canoniche dei portafogli (tutti i tipi visti finora).""" +from __future__ import annotations + +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.portfolio.base import Portfolio, SleeveSpec # noqa: E402 + +FADE = [SleeveSpec(kind="single", name=c, sid=f"{c}_{a}", asset=a, cluster=f"{a}-rev") + for a in ("BTC", "ETH") for c in ("MR01", "MR02", "MR07")] +HONEST = [ + SleeveSpec(kind="single", name="DIP01", sid="DIP01_BTC", asset="BTC", cluster="BTC-rev"), + SleeveSpec(kind="single", name="TR01", sid="TR01_basket", cluster="trend"), + SleeveSpec(kind="single", name="ROT02", sid="ROT02_rot", cluster="rotation"), +] +PAIRS = [ + SleeveSpec(kind="pairs", name="PR01", sid="PR_ETHBTC", a="ETH", b="BTC", cluster="ETH-rev"), + SleeveSpec(kind="pairs", name="PR01", sid="PR_LTCETH", a="LTC", b="ETH", cluster="ETH-rev"), + SleeveSpec(kind="pairs", name="PR01", sid="PR_ADAETH", a="ADA", b="ETH", cluster="ETH-rev"), + SleeveSpec(kind="pairs", name="PR01", sid="PR_BTCLTC", a="BTC", b="LTC", cluster="BTC-rev"), + SleeveSpec(kind="pairs", name="PR01", sid="PR_ETHSOL", a="ETH", b="SOL", cluster="ETH-rev"), +] +TSM = [SleeveSpec(kind="single", name="TSM01", sid="TSM01", cluster="trend")] +SHAPE = [SleeveSpec(kind="ml", name="SH01", sid=f"SH_{a}", asset=a, cluster="shape") + for a in ("BTC", "ETH")] + +PORTFOLIOS = { + "PORT01": Portfolio("PORT01", "Honest", HONEST, weighting="equal"), + "PORT02": Portfolio("PORT02", "Fade master", FADE, weighting="equal"), + "PORT03": Portfolio("PORT03", "Master", FADE + HONEST, weighting="equal"), + "PORT04": Portfolio("PORT04", "Master + pairs", FADE + HONEST + PAIRS, + weighting="cap", caps={"PAIRS": 0.33}), + "PORT05": Portfolio("PORT05", "Master esteso", FADE + HONEST + PAIRS + TSM, + weighting="cap", caps={"PAIRS": 0.33}), + "PORT06": Portfolio("PORT06", "Master + shape", FADE + HONEST + PAIRS + TSM + SHAPE, + weighting="cap", caps={"PAIRS": 0.33}, leverage=2.0), +} diff --git a/tests/portfolio/test_definitions.py b/tests/portfolio/test_definitions.py new file mode 100644 index 0000000..fa7d2a6 --- /dev/null +++ b/tests/portfolio/test_definitions.py @@ -0,0 +1,17 @@ +from scripts.portfolios._defs import PORTFOLIOS + + +def test_six_portfolios_defined(): + assert set(PORTFOLIOS) == {"PORT01", "PORT02", "PORT03", "PORT04", "PORT05", "PORT06"} + + +def test_port06_is_master_shape_cap(): + p = PORTFOLIOS["PORT06"] + sids = set(p.sleeve_ids) + assert {"SH_BTC", "SH_ETH", "TSM01", "PR_ETHBTC"} <= sids + assert len(sids) == 17 + assert p.weighting == "cap" and p.caps == {"PAIRS": 0.33} + + +def test_default_leverage_sober(): + assert PORTFOLIOS["PORT06"].leverage == 2.0