04cb572535
Book, pesi, cron, config INVARIATI. VRP01 tiene fino a scadenza (S1 = px[i+tn], nessuna gestione infra-settimana) e il profit-take era l'unico grado di liberta' non misurato: i 4 overlay del 03/07 erano tutti sul lato del RISCHIO, questo e' sul lato del PROFITTO. Replica del sleeve bit-exact (max|diff| = 0.0) prima di ogni delta. VERDETTO con fee reali per gamba: canonico ShFULL 1.32 -> PT25 0.22 / PT50 -0.18 / PT75 -0.45. Null del de-levering REFUTED 6/6: a PT25 basta k=0.818 per lo stesso DD con Sharpe 1.32 invece di 0.22. MECCANISMO (confronto appaiato per ingresso): scatta sull'86-91% dei VINCENTI e sul 19-25% dei perdenti -> tronca i vincenti del 27% e salva un perdente su cinque. La peggior settimana e' IDENTICA (-7.27%) in ogni variante: nelle settimane brutte lo spread non tocca mai +50%, quindi e' pura troncatura dell'upside. IPOTESI MIA REFUTATA IN SESSIONE: 'su 7g chi tocca +50% e' chi sarebbe scaduto senza valore, quindi a scadenze lunghe paga'. Testata su 7/10/14/18/21/28g: delta Sharpe negativo a tutti e sei. A 18g (il tenore di cerbero-bite) il DD migliora 7.1->3.8% ma lo Sharpe crolla = firma del de-levering. CORREZIONE a un numero pubblicato oggi: il sleeve modella le fee come 12.5% del credito netto, il listino vero e' 0.03% del sottostante per gamba (cap 12.5% del premio della singola opzione, che quasi mai morde) -> sovrastima di ~2x. Il numero onesto di VRP01 con f=0.73 e' ShFULL 0.47, non 0.31. fee_frac NON cambiato: il forfait e' conservativo e i numeri di ammissione di questo progetto si tengono conservativi. A $3.000 la domanda non e' il rendimento: BTC min 0.1 = $6.210 di collaterale per lotto -> FUORI; ETH min 1 contratto = $1.832 -> 1 lotto. Il sleeve 50/50 diventa ETH-only e al peso di book (12% = $360) sono 0 lotti. REGOLE: (a) un'uscita si giudica appaiata per INGRESSO, mai allineando le serie sulla data di USCITA — e' quella che la variante cambia, e l'inner-join tiene solo i casi in cui non e' successo niente (errore commesso e corretto in sessione, congelato in un test); (b) una regola d'uscita che scatta piu' spesso sui vincenti che sui perdenti non e' protezione, e' troncatura; (c) prima del rendimento a un capitale dato, misurare il lotto minimo del venue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
179 lines
8.0 KiB
Python
179 lines
8.0 KiB
Python
"""Test della misura del profit-take su VRP01 (scripts/research/r0730_vrp_profit_take.py).
|
|
|
|
Tre cose vanno congelate qui, e una sola e' il risultato:
|
|
|
|
* **La replica bit-exact.** Tutto il confronto poggia su una re-implementazione del sleeve:
|
|
se qualcuno tocca `_vrp_weekly_asset`, il test deve rompersi invece di far passare un delta
|
|
misurato fra due cose diverse.
|
|
* **La trappola dell'inner-join.** Le due varianti escono in date DIVERSE, quindi un
|
|
`join="inner"` fra le due serie confronta solo le settimane senza uscita anticipata e
|
|
risponde "identiche" con potenza zero. E' l'errore che ho commesso in sessione; il test lo
|
|
rende irripetibile.
|
|
* **Il verdetto** (PT50 non batte il canonico) — con il **controllo positivo** obbligatorio sul
|
|
null del de-levering: un test che dice sempre REFUTED e' indistinguibile da uno rotto.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"r0730_pt", ROOT / "scripts" / "research" / "r0730_vrp_profit_take.py")
|
|
PT = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(PT)
|
|
|
|
from src.portfolio.sleeves import VRP_CFG, _vrp_weekly_asset # noqa: E402
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# cache: ogni corsa rilegge 7 anni di storia
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.fixture(scope="module")
|
|
def canon():
|
|
return PT.combo(pt=None, f=1.0, real_fee=True)[0]
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def pt50():
|
|
return PT.combo(pt=0.50, f=1.0, real_fee=True)[0]
|
|
|
|
|
|
# ===========================================================================
|
|
# la fondazione: senza questa nessun delta significa niente
|
|
# ===========================================================================
|
|
@pytest.mark.parametrize("asset", ["BTC", "ETH"])
|
|
def test_la_replica_e_bit_exact_col_sleeve(asset):
|
|
mine, _ = PT.run_asset(asset, pt=None, f=VRP_CFG["f"], real_fee=False)
|
|
ref = _vrp_weekly_asset(asset)
|
|
j = pd.concat({"m": mine, "r": ref}, axis=1, join="inner")
|
|
assert len(j) == len(ref) == len(mine) # nessuna data persa da nessuna delle due
|
|
assert float((j["m"] - j["r"]).abs().max()) == 0.0
|
|
|
|
|
|
# ===========================================================================
|
|
# l'errore di metodo commesso in sessione, congelato
|
|
# ===========================================================================
|
|
def test_confrontare_per_data_di_uscita_da_una_risposta_falsa(canon, pt50):
|
|
"""Un inner-join fra canonico e PT tiene solo le settimane in cui il PT NON e' scattato,
|
|
dove per costruzione i due valori coincidono: la conclusione sarebbe 'nessuna differenza'."""
|
|
a = canon[canon != 0.0]
|
|
b = pt50[pt50 != 0.0]
|
|
j = pd.concat({"a": a, "b": b}, axis=1, join="inner")
|
|
assert len(j) < 0.9 * min(len(a), len(b)) # il join butta via la maggior parte dei dati
|
|
# ...e su cio' che resta le due sono IDENTICHE: la risposta sarebbe "nessuna differenza"
|
|
assert float((j["a"] - j["b"]).abs().max()) == 0.0
|
|
# mentre la differenza vera, misurata appaiata, e' enorme
|
|
assert abs(PT.stats(a)["sh"] - PT.stats(b)["sh"]) > 1.0
|
|
|
|
|
|
def test_il_confronto_appaiato_per_ingresso_vede_la_differenza():
|
|
"""La lente giusta: stesso ingresso, due esiti. Qui la differenza c'e' ed e' grande."""
|
|
R = PT.paired_trades("ETH", pt=0.50)
|
|
assert len(R) > 50
|
|
assert R["early"].mean() > 0.5 # il trigger e' raggiungibile
|
|
assert R["pt"].sum() < R["hold"].sum() # e costa
|
|
|
|
|
|
# ===========================================================================
|
|
# il meccanismo
|
|
# ===========================================================================
|
|
@pytest.mark.parametrize("asset", ["BTC", "ETH"])
|
|
def test_il_profit_take_scatta_sui_vincenti_non_sui_perdenti(asset):
|
|
"""E' il motivo per cui perde: tronca i trade che sarebbero finiti bene e non salva
|
|
quelli che finiscono male."""
|
|
R = PT.paired_trades(asset, pt=0.50)
|
|
W, L = R[R["hold"] > 0], R[R["hold"] <= 0]
|
|
assert W["early"].mean() > 0.75 # >75% dei vincenti escono prima
|
|
assert L["early"].mean() < 0.40 # <40% dei perdenti
|
|
assert W["early"].mean() > 2.0 * L["early"].mean()
|
|
assert W["pt"].mean() < W["hold"].mean() # i vincenti vengono troncati
|
|
|
|
|
|
# ===========================================================================
|
|
# il trigger non dipende da f — proprieta', non coincidenza
|
|
# ===========================================================================
|
|
def test_il_trigger_e_invariante_in_f():
|
|
"""`f*debit <= (1-pt)*f*credito` non contiene f: cambiare il prezzo del modello sposta il
|
|
PnL ma non QUANDO si esce. Se qualcuno applicasse f asimmetricamente, questo si rompe."""
|
|
a = PT.paired_trades("ETH", pt=0.50, f=1.00)
|
|
b = PT.paired_trades("ETH", pt=0.50, f=0.73)
|
|
assert len(a) == len(b)
|
|
assert (a["early"].values == b["early"].values).all()
|
|
|
|
|
|
# ===========================================================================
|
|
# fee: la forma del listino, non solo il livello
|
|
# ===========================================================================
|
|
def test_la_fee_di_gamba_usa_il_sottostante_quando_il_premio_e_alto():
|
|
# opzione cara: 12.5% del premio (12.5) e' piu' del 3bps del sottostante (0.57) -> vince il 3bps
|
|
assert PT.leg_fee(1900.0, 100.0) == pytest.approx(0.0003 * 1900.0)
|
|
|
|
|
|
def test_la_fee_di_gamba_e_tappata_al_premio_quando_l_opzione_e_quasi_senza_valore():
|
|
# opzione da 1 dollaro: il cap al 12.5% (0.125) morde, altrimenti si pagherebbe piu' del premio
|
|
assert PT.leg_fee(1900.0, 1.0) == pytest.approx(0.125)
|
|
|
|
|
|
def test_la_fee_non_e_mai_negativa_su_premio_negativo():
|
|
assert PT.leg_fee(1900.0, -5.0) == 0.0
|
|
|
|
|
|
# ===========================================================================
|
|
# il verdetto, e il controllo positivo che lo rende credibile
|
|
# ===========================================================================
|
|
def test_pt50_non_batte_il_canonico(canon, pt50):
|
|
for match in ("vol", "dd"):
|
|
r = PT.delever_null(canon, pt50, match)
|
|
assert not r["batte"], f"match={match}: il verdetto e' cambiato, va rivisto il diario"
|
|
assert r["d_sh"] < -0.5
|
|
|
|
|
|
def test_controllo_positivo_il_null_sa_dire_di_si(canon):
|
|
"""Obbligatorio: un null che risponde sempre REFUTED e' indistinguibile da uno rotto.
|
|
Una variante COSTRUITA migliore (stesso rendimento, meta' della volatilita' di rumore)
|
|
deve passare."""
|
|
better = canon.copy()
|
|
better[better < 0] *= 0.5 # perdite dimezzate = davvero meglio
|
|
r = PT.delever_null(canon, better, "vol")
|
|
assert r["batte"] and r["d_sh"] > 0
|
|
|
|
|
|
# ===========================================================================
|
|
# eseguibilita': a $3.000 la domanda viene prima del rendimento
|
|
# ===========================================================================
|
|
@pytest.fixture(scope="module")
|
|
def ex3k():
|
|
return PT.executability(3000.0)
|
|
|
|
|
|
def test_a_3000_le_opzioni_btc_sono_fuori(ex3k):
|
|
"""min_trade_amount 0.1 BTC = ~$6.000 di collaterale cash-secured per lotto."""
|
|
assert ex3k["BTC"]["lots"] == 0
|
|
assert ex3k["BTC"]["collat"] > 3000.0
|
|
|
|
|
|
def test_a_3000_eth_entra_per_un_solo_lotto(ex3k):
|
|
assert ex3k["ETH"]["lots"] == 1
|
|
assert ex3k["ETH"]["collat"] < 3000.0 < 2 * ex3k["ETH"]["collat"]
|
|
|
|
|
|
def test_al_peso_di_book_lo_sleeve_non_e_collocabile(ex3k):
|
|
"""VRP01 pesa il 12%: $360 su $3.000, sotto il collaterale di un solo lotto ETH.
|
|
E' il vincolo che decide, non lo Sharpe."""
|
|
assert int(3000.0 * 0.12 // ex3k["ETH"]["collat"]) == 0
|
|
|
|
|
|
def test_il_lotto_minimo_e_quello_letto_dal_venue():
|
|
"""Congelato dal listino Deribit del 2026-07-30: se cambia, i numeri di questo studio
|
|
cambiano e il test deve dirlo."""
|
|
assert PT.MIN_TRADE == {"BTC": 0.1, "ETH": 1.0}
|
|
assert PT.FEE_UNDERLYING == 0.0003 and PT.FEE_PREMIUM_CAP == 0.125
|