research(wave-0822): TP01-SINISTRO — l'allarme e' falsificato nel verso; il rischio dell'ottimo funded e' XS01, non TP01
This commit is contained in:
@@ -0,0 +1,640 @@
|
||||
"""r0822b_depeg.py — IL FALSIFICATORE DI VENUE PER XS01-OOS (2026-08-22, ondata wave-0822).
|
||||
|
||||
IL PUNTO FRAGILE, DICHIARATO DALL'AUTORE DEL RISULTATO
|
||||
-----------------------------------------------------
|
||||
`r0822b_xs01_oos.py` (RESULTS-0822 §21) ha misurato che il meccanismo congelato di XS01 ha un edge
|
||||
FUORI dalla sua finestra di scoperta — 2021-2023 su **Binance spot USDT** — e che li' e' PIU' GRANDE
|
||||
(mediana di fase +1,12 contro +0,37, differenza appaiata +0,668 in 10/10 fasi). Quel risultato
|
||||
riscrive il gate del canale funded, quindi merita il suo falsificatore.
|
||||
|
||||
Lo stesso autore lo ha nominato:
|
||||
> "La validazione di venue Binance-vs-Hyperliquid gira SOLO sulla finestra comune 2024+, cioe'
|
||||
> proprio il periodo SENZA depeg USDT. Il fuori campione contiene il depeg di maggio 2022. Se i
|
||||
> close Binance/USDT 2021-2023 divergessero da un venue USD in modo NON COMUNE fra gli asset, il
|
||||
> ranking cambierebbe e il risultato con esso. Test che lo deciderebbe: rifare il meccanismo sui
|
||||
> close Coinbase USD per il sottoinsieme quotato li', 2021-2023, e chiedere corr fra i due sleeve
|
||||
> >= 0.99 come sul 2024+."
|
||||
|
||||
Questo script esegue esattamente quel test.
|
||||
|
||||
PERCHE' LA DOMANDA NON E' OZIOSA (il meccanismo del danno, prima dei numeri)
|
||||
---------------------------------------------------------------------------
|
||||
La regola del progetto — "Binance non e' la verita', e' USDT, ~10 bps fuori, e sotto depeg fino al
|
||||
3% off" — e' stata **correttamente** giudicata non-binding da §21: uno z-score cross-sezionale
|
||||
sottrae la media di sezione, quindi e' invariante a un fattore **COMUNE**. Ma un depeg non e'
|
||||
garantito comune: nei giorni di stress la liquidita' si sposta in modo diverso per asset (i major
|
||||
restano scambiati contro USDT, i minori no; i flussi di fuga passano per coppie diverse), e una
|
||||
divergenza **IDIOSINCRATICA** cambia il RANKING — che e' esattamente cio' che la strategia legge.
|
||||
Maggio 2022 (UST/LUNA, con USDT sceso a ~0,95) e' il caso peggiore possibile.
|
||||
Percio' la divergenza va misurata in DUE pezzi, e sono due domande diverse:
|
||||
(a) COMUNE = mediana per giorno dello scarto fra i venue -> quella che lo z-score annulla;
|
||||
(b) IDIOSINCRATICA = scarto per asset DOPO aver tolto la mediana del giorno -> quella che uccide.
|
||||
Riportare solo (a) darebbe la risposta rassicurante e sbagliata.
|
||||
|
||||
IL VINCOLO DEL DATO, E COME E' STATO RISOLTO (dichiarato PRIMA di misurare)
|
||||
--------------------------------------------------------------------------
|
||||
Su Coinbase USD, dei 13 asset di U13:
|
||||
BNB -> quotato solo dal 2025-10 = FUORI (nessun venue USD ce l'ha nel 2021-23: e' l'asset
|
||||
nativo del venue che stiamo mettendo in discussione)
|
||||
XRP -> SOSPESO su Coinbase dal 2021-01-19 al 2023-07 (causa SEC) = buco proprio sulla finestra
|
||||
NEAR/INJ -> quotati su Coinbase solo dal 2022-09
|
||||
AVAX -> dal 2021-09-30 (il vincolo che decide l'inizio della finestra)
|
||||
XRP e' recuperato da **Bitstamp USD** (l'altra referenza USD del `venue_watch` del progetto, che
|
||||
non ha mai sospeso la coppia). Restano fuori BNB, NEAR, INJ.
|
||||
Ne escono DUE test, e il secondo copre cio' che il primo non copre:
|
||||
|
||||
TEST 1 "U10-USD" = universo FISSO di 10 gambe (BTC ETH LTC LINK AAVE ADA DOGE SOL AVAX + XRP),
|
||||
2021-09-30 -> 2023-12-31, meccanismo **CONGELATO** (k=5, min_assets=10:
|
||||
dieci gambe sono ESATTAMENTE il minimo, nessun parametro toccato).
|
||||
Contiene il depeg di maggio 2022 e FTX. NON contiene il 2021.
|
||||
TEST 2 "U13-IBRIDO" = i 13 asset e la finestra INTERA 2021-01 -> 2023-12, con il close USD
|
||||
sostituito a quello USDT **dove esiste** (9-12 gambe su 13 a seconda della
|
||||
data) e Binance dove non esiste. Non e' un confronto puro di venue, e'
|
||||
la domanda operativa: *se sostituisco tutto il prezzo USD che esiste, il
|
||||
risultato di §21 si muove?*
|
||||
In entrambi i test il confronto e' **APPAIATO**: stesso sottoinsieme, stessa finestra, stesse 10
|
||||
fasi, stesso codice del motore. Confrontare "19 gambe su Binance" con "10 su Coinbase" misurerebbe
|
||||
l'UNIVERSO, non il venue (regola del progetto: un confronto appaiato eredita TUTTI i parametri
|
||||
della riga in cui compare) — e per questo il TEST 1 riporta anche cosa fa U10 **su Binance**
|
||||
rispetto a U13 su Binance, cioe' quanto del titolo di §21 questo sottoinsieme riproduce.
|
||||
|
||||
MECCANISMO: CONGELATO. Il motore e' importato da `r0822b_xs01_oos.py` (che a sua volta replica
|
||||
`sleeves._xsec_returns` a max|diff| = 0.0). Nessuna riga di strategia riscritta qui: se il motore
|
||||
fosse riscritto, una differenza di venue e una differenza di implementazione sarebbero
|
||||
indistinguibili.
|
||||
|
||||
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_depeg.py
|
||||
Rete: sola lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col
|
||||
collettore della catena opzioni; guasto del 29/07). Download in cache nello scratchpad: la
|
||||
seconda corsa e' offline.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||
|
||||
import altlib as A # noqa: E402
|
||||
import r0822b_xs01_oos as X # noqa: E402 (motore CONGELATO + cache Binance)
|
||||
from src.portfolio.sleeves import XS_CFG # noqa: E402
|
||||
|
||||
CACHE = X.CACHE.parent / "r0822b_depeg"
|
||||
CACHE.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UNIVERSI E FINESTRE — dichiarati PRIMA di guardare i numeri
|
||||
# ---------------------------------------------------------------------------
|
||||
U13 = X.U13 # l'universo di §21
|
||||
# venue USD per asset. `None` = nessun venue USD con storia sulla finestra.
|
||||
USD_SRC: dict[str, tuple[str, str] | None] = {
|
||||
"BTC": ("coinbase", "BTC/USD"), "ETH": ("coinbase", "ETH/USD"),
|
||||
"LTC": ("coinbase", "LTC/USD"), "LINK": ("coinbase", "LINK/USD"),
|
||||
"AAVE": ("coinbase", "AAVE/USD"), "ADA": ("coinbase", "ADA/USD"),
|
||||
"DOGE": ("coinbase", "DOGE/USD"), "SOL": ("coinbase", "SOL/USD"),
|
||||
"AVAX": ("coinbase", "AVAX/USD"), "NEAR": ("coinbase", "NEAR/USD"),
|
||||
"INJ": ("coinbase", "INJ/USD"),
|
||||
"XRP": ("bitstamp", "XRP/USD"), # Coinbase l'ha sospeso 2021-01 -> 2023-07
|
||||
"BNB": None, # nessun venue USD prima del 2025-10
|
||||
}
|
||||
U10 = ["BTC", "ETH", "LTC", "LINK", "AAVE", "ADA", "DOGE", "SOL", "AVAX", "XRP"]
|
||||
T1_START, T1_END = "2021-09-30", "2024-01-01" # AVAX su Coinbase e' il vincolo
|
||||
T2_START, T2_END = "2021-01-01", "2024-01-01" # la finestra fuori campione di §21
|
||||
DEPEG = ("2022-05-07", "2022-05-21") # UST/LUNA + USDT a ~0.95 (estremo incluso)
|
||||
PHASES = list(range(XS_CFG["H"])) # 10 fasi: la stima onesta e' la MEDIANA
|
||||
CORR_SOGLIA = 0.99 # soglia DICHIARATA DALL'AUTORE, non da me
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 1. FETCH USD (Coinbase / Bitstamp) + certificazione locale
|
||||
# ===========================================================================
|
||||
def _fetch_paged(ex, market: str, tf: str, start_ms: int, end_ms: int,
|
||||
step_bars: int, bar_ms: int) -> list:
|
||||
"""Paginazione DETERMINISTICA sulla finestra: avanza anche quando una pagina torna vuota.
|
||||
|
||||
⚠️ Non e' un dettaglio: XRP su Coinbase ha un buco di 2,5 anni (sospensione SEC) e un loop che
|
||||
si ferma alla prima pagina vuota — il modo ovvio di scrivere questo ciclo — troncherebbe la
|
||||
serie li', producendo un 'asset che non esiste' invece di un 'asset con un buco'. Sono due
|
||||
fatti diversi e portano a due decisioni diverse.
|
||||
"""
|
||||
rows, cur, span = [], start_ms, step_bars * bar_ms
|
||||
while cur < end_ms:
|
||||
X._outside_forbidden_window()
|
||||
batch = []
|
||||
for attempt in range(3):
|
||||
try:
|
||||
batch = ex.fetch_ohlcv(market, tf, since=cur, limit=step_bars)
|
||||
break
|
||||
except Exception as e: # noqa: BLE001
|
||||
if attempt == 2:
|
||||
print(f" [rete] {market} @ {cur}: {type(e).__name__} {str(e)[:70]}")
|
||||
time.sleep(2 ** attempt)
|
||||
time.sleep(0.55) # <= 2 req/s
|
||||
rows += [b for b in (batch or []) if cur <= b[0] < cur + span]
|
||||
cur += span
|
||||
return rows
|
||||
|
||||
|
||||
def fetch_usd_daily(asset: str) -> pd.DataFrame:
|
||||
"""Close giornalieri USD dal venue dichiarato in USD_SRC. Cache su disco."""
|
||||
src = USD_SRC.get(asset)
|
||||
if src is None:
|
||||
return pd.DataFrame(columns=["timestamp", "close", "volume"])
|
||||
venue, market = src
|
||||
f = CACHE / f"{venue[:2]}_{asset.lower()}_1d.parquet"
|
||||
if f.exists():
|
||||
return pd.read_parquet(f)
|
||||
import ccxt
|
||||
ex = getattr(ccxt, venue)({"enableRateLimit": True})
|
||||
ex.load_markets()
|
||||
if market not in ex.markets:
|
||||
d = pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"])
|
||||
d.to_parquet(f)
|
||||
return d
|
||||
start = ex.parse8601("2019-01-01T00:00:00Z")
|
||||
end = int(time.time() * 1000)
|
||||
step = 300 if venue == "coinbase" else 1000
|
||||
rows = _fetch_paged(ex, market, "1d", start, end, step, 86_400_000)
|
||||
d = (pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
|
||||
.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True))
|
||||
d.to_parquet(f)
|
||||
return d
|
||||
|
||||
|
||||
def series_of(d: pd.DataFrame) -> pd.Series:
|
||||
if not len(d):
|
||||
# indice tz-aware anche da vuoto: un asset senza venue USD (BNB) deve poter essere
|
||||
# confrontato/reindicizzato come gli altri, non far cadere lo script
|
||||
return pd.Series(dtype=float, index=pd.DatetimeIndex([], tz="UTC"))
|
||||
return pd.Series(d["close"].values.astype(float),
|
||||
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
||||
|
||||
|
||||
def certify_usd(asset: str, d: pd.DataFrame, a: str, b: str) -> dict:
|
||||
"""Stessa forma della certificazione di §21, ma valutata SULLA FINESTRA DEL TEST.
|
||||
|
||||
Certificare "tutta la serie" direbbe la cosa sbagliata: XRP su Coinbase ha 900 giorni di buco
|
||||
che sulla finestra 2021-09/2023-12 sono TUTTI dentro. Il numero utile e' la copertura della
|
||||
finestra in cui il meccanismo gira, non della serie.
|
||||
"""
|
||||
s = series_of(d)
|
||||
s = s[(s.index >= pd.Timestamp(a, tz="UTC")) & (s.index < pd.Timestamp(b, tz="UTC"))]
|
||||
if not len(s):
|
||||
return dict(sym=asset, n=0, ok=False, start="-", end="-", gap=-1,
|
||||
flat=float("nan"), zero_vol=float("nan"))
|
||||
v = series_of(d.assign(close=d["volume"]))
|
||||
v = v[(v.index >= pd.Timestamp(a, tz="UTC")) & (v.index < pd.Timestamp(b, tz="UTC"))]
|
||||
attesi = (pd.Timestamp(b, tz="UTC") - s.index.min()).days
|
||||
c = s.values
|
||||
flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan")
|
||||
zv = float(np.mean(v.values == 0.0)) if len(v) else float("nan")
|
||||
gap = int(attesi - len(s))
|
||||
return dict(sym=asset, n=len(s), start=str(s.index.min().date()),
|
||||
end=str(s.index.max().date()), gap=gap, flat=flat, zero_vol=zv,
|
||||
ok=bool(gap == 0 and flat < 0.02 and (not np.isfinite(zv) or zv < 0.01)))
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 2. pannelli
|
||||
# ===========================================================================
|
||||
def bin_panel(syms: list[str]) -> pd.DataFrame:
|
||||
"""Pannello Binance USDT — dalla cache di §21, cosi' il lato USDT del confronto e' BIT-IDENTICO
|
||||
a quello che ha prodotto il risultato che sto falsificando."""
|
||||
return pd.concat({s: series_of(X.fetch_binance_daily(s)) for s in syms}, axis=1).sort_index()
|
||||
|
||||
|
||||
def usd_panel(syms: list[str]) -> pd.DataFrame:
|
||||
return pd.concat({s: series_of(fetch_usd_daily(s)) for s in syms}, axis=1).sort_index()
|
||||
|
||||
|
||||
def splice(usd: pd.Series, usdt: pd.Series) -> pd.Series:
|
||||
"""Serie IBRIDA: USD dove esiste, USDT prima — RI-SCALANDO il tratto USDT al livello USD nel
|
||||
giorno del giunto.
|
||||
|
||||
⚠️ Senza la ri-scalatura il giunto crea un rendimento fantasma pari allo scarto fra i venue
|
||||
(pochi bps, ma esattamente il tipo di artefatto che questo script deve misurare, non fabbricare).
|
||||
Il meccanismo legge RAPPORTI di prezzo a 30 e 90 giorni: dopo la ri-scalatura la serie e'
|
||||
continua e i rapporti che attraversano il giunto sono ben definiti.
|
||||
"""
|
||||
u = usd.dropna()
|
||||
if not len(u):
|
||||
return usdt
|
||||
t0 = u.index[0]
|
||||
if t0 not in usdt.index or not np.isfinite(usdt.get(t0, np.nan)):
|
||||
return usd.reindex(usdt.index)
|
||||
k = float(u.iloc[0]) / float(usdt.loc[t0])
|
||||
pre = usdt[usdt.index < t0] * k
|
||||
return pd.concat([pre, u]).sort_index().reindex(usdt.index)
|
||||
|
||||
|
||||
def hybrid_panel(syms: list[str]) -> tuple[pd.DataFrame, dict]:
|
||||
B, U = bin_panel(syms), usd_panel(syms)
|
||||
cols, meta = {}, {}
|
||||
for s in syms:
|
||||
u = U[s] if s in U.columns else pd.Series(dtype=float)
|
||||
u = u.reindex(B.index)
|
||||
cols[s] = splice(u, B[s])
|
||||
cov = u.dropna()
|
||||
meta[s] = (str(cov.index[0].date()) if len(cov) else "mai")
|
||||
return pd.concat(cols, axis=1).sort_index(), meta
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 3. metriche e allineamento appaiato
|
||||
# ===========================================================================
|
||||
sh, dd, cagr, win, alive, line = X.sh, X.dd, X.cagr, X.win, X.alive, X.line
|
||||
|
||||
|
||||
def pair(a: pd.Series, b: pd.Series) -> tuple[pd.Series, pd.Series]:
|
||||
"""Allinea due sleeve sullo stesso indice: un confronto di venue non puo' confrontare finestre."""
|
||||
j = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna()
|
||||
return j["a"], j["b"]
|
||||
|
||||
|
||||
def ranking_scan(PB: pd.DataFrame, PU: pd.DataFrame, phases: list[int],
|
||||
label: str) -> pd.DataFrame:
|
||||
"""IL CANALE DEL DANNO, misurato dove passa: il venue puo' far male SOLO cambiando le gambe
|
||||
scelte. Scansione di TUTTE le fasi (non solo la canonica: un ribaltamento su una fase sola
|
||||
sarebbe invisibile all'ancora 0) con self-check dello specchio contro il motore."""
|
||||
from scipy.stats import spearmanr
|
||||
lb = XS_CFG["lookbacks"]
|
||||
mlb, H, k = max(lb), XS_CFG["H"], XS_CFG["k"]
|
||||
rows, sp, checked, tot = [], [], 0, 0
|
||||
for ph in phases:
|
||||
det: dict = {}
|
||||
X.xsec_engine(PB, phase=ph, detail=det)
|
||||
WB = det["W"]
|
||||
for i in range(mlb, len(PB)):
|
||||
if i % H != ph:
|
||||
continue
|
||||
iB, sB = score_at(PB, i, lb)
|
||||
iU, sU = score_at(PU, i, lb)
|
||||
if len(iB) < 10 or len(iU) < 10:
|
||||
continue
|
||||
tot += 1
|
||||
oB = np.argsort(sB)
|
||||
loB, hiB = set(iB[oB[:k]]), set(iB[oB[-k:]])
|
||||
mh, ml = set(np.flatnonzero(WB[i] > 0)), set(np.flatnonzero(WB[i] < 0))
|
||||
if mh or ml: # self-check solo dove il motore ha una posizione
|
||||
checked += 1
|
||||
if (mh, ml) != (hiB, loB):
|
||||
raise AssertionError(f"specchio != motore ({label}, fase {ph}, i={i}): "
|
||||
"misurerei la mia riscrittura invece del venue")
|
||||
oU = np.argsort(sU)
|
||||
loU, hiU = set(iU[oU[:k]]), set(iU[oU[-k:]])
|
||||
ov = (len(hiB & hiU) + len(loB & loU)) / (2 * k)
|
||||
sp.append(float(spearmanr(sB, sU).statistic))
|
||||
if ov < 1.0:
|
||||
diff = sorted(set(PB.columns[list(hiB ^ hiU)]) | set(PB.columns[list(loB ^ loU)]))
|
||||
rows.append(dict(fase=ph, data=PB.index[i], overlap=ov, gambe=",".join(diff)))
|
||||
D = pd.DataFrame(rows)
|
||||
print(f" [{label}] {tot} ribilanciamenti su {len(phases)} fasi; specchio verificato "
|
||||
f"contro il motore in {checked}/{checked} con posizione")
|
||||
print(f" [{label}] Spearman dello score fra i venue: mediana {np.median(sp):.4f} "
|
||||
f"min {np.min(sp):.4f}")
|
||||
print(f" [{label}] ribilanciamenti con gambe DIVERSE: {len(D)}/{tot} "
|
||||
f"({len(D)/max(tot,1)*100:.2f}%)")
|
||||
for _, r in D.iterrows():
|
||||
print(f" fase {int(r['fase'])} {r['data'].date()} "
|
||||
f"sovrapposizione {r['overlap']*100:3.0f}% gambe scambiate: {r['gambe']}")
|
||||
return D
|
||||
|
||||
|
||||
def score_at(P: pd.DataFrame, i: int, lookbacks) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Specchio della riga di score del motore (z-score medio sui lookback). Serve per confrontare
|
||||
i RANKING fra venue; la sezione [4] verifica che i top-k/bottom-k che ne derivano coincidano
|
||||
ESATTAMENTE con quelli scelti dal motore — se lo specchio divergesse, ogni numero di ranking
|
||||
misurerebbe la mia riscrittura invece del venue."""
|
||||
px = P.values.astype(float)
|
||||
elig = np.isfinite(px[i])
|
||||
for L in lookbacks:
|
||||
elig = elig & np.isfinite(px[i - L])
|
||||
idx = np.flatnonzero(elig)
|
||||
sc, cnt = np.zeros(len(idx)), 0
|
||||
for L in lookbacks:
|
||||
r = px[i, idx] / px[i - L, idx] - 1.0
|
||||
if r.std() > 0:
|
||||
sc += (r - r.mean()) / r.std()
|
||||
cnt += 1
|
||||
return idx, (sc / cnt if cnt else sc)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
def main() -> None:
|
||||
t0 = time.time()
|
||||
pd.set_option("display.width", 200)
|
||||
print("=" * 100)
|
||||
print("DEPEG — il close USDT 2021-2023 puo' aver cambiato il RANKING di XS01-OOS?")
|
||||
print(f"XS_CFG (CONGELATO) = {XS_CFG} soglia dichiarata dall'autore: corr >= {CORR_SOGLIA}")
|
||||
print("=" * 100)
|
||||
|
||||
# ------------------------------------------------------------- [0] sanity
|
||||
print("\n[0] SANITY — il motore importato e' quello che ha prodotto §21?")
|
||||
from src.portfolio.sleeves import _xsec_returns
|
||||
off = _xsec_returns()
|
||||
rep = X.xsec_engine(X.hl_panel(), phase=0)
|
||||
j = pd.concat({"o": off, "r": rep}, axis=1, join="inner").dropna()
|
||||
md = float(np.max(np.abs(j["o"] - j["r"])))
|
||||
print(f" motore vs sleeve UFFICIALE su HL: n={len(j)} max|diff| = {md:.2e} "
|
||||
f"-> {'IDENTICO' if md < 1e-12 else 'DIVERGE — STOP'}")
|
||||
if md >= 1e-12:
|
||||
return
|
||||
|
||||
# ------------------------------------------------------- [1] dato USD
|
||||
print("\n[1] DATO USD — quanti dei 13 asset di §21 hanno un prezzo in DOLLARI nel 2021-2023?")
|
||||
print(f" {'sym':<6}{'venue USD':>11}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}"
|
||||
f"{'flat%':>8}{'vol0%':>8} (finestra {T1_START} .. {T1_END})")
|
||||
certs = {}
|
||||
for s in U13:
|
||||
d = fetch_usd_daily(s)
|
||||
c = certify_usd(s, d, T1_START, T1_END)
|
||||
certs[s] = c
|
||||
ven = USD_SRC[s][0] if USD_SRC[s] else "—"
|
||||
flag = "" if c["ok"] else " <-- non usabile su questa finestra"
|
||||
print(f" {s:<6}{ven:>11}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap']:>5}"
|
||||
f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{flag}")
|
||||
usabili = [s for s in U13 if certs[s]["ok"]]
|
||||
print(f" -> usabili sulla finestra del TEST 1: {len(usabili)}/13 {usabili}")
|
||||
print(f" -> ESCLUSI: BNB (nessun venue USD prima del 2025-10 — e' l'asset nativo del venue")
|
||||
print(f" sotto esame), NEAR e INJ (Coinbase solo dal 2022-09).")
|
||||
print(f" ⚠️ LIMITE DEL TEST, da dire subito: 10 gambe su 13 e finestra che parte dal")
|
||||
print(f" {T1_START} (AVAX su Coinbase) -> il TEST 1 NON copre il 2021, che in §21 e'")
|
||||
print(f" l'anno piu' forte (+2,14). Copre pero' ESATTAMENTE cio' che l'obiezione nomina:")
|
||||
print(f" il depeg di maggio 2022 e FTX. Il 2021 lo copre il TEST 2 (ibrido).")
|
||||
|
||||
# controllo di ALLINEAMENTO: le barre USD sono sulla stessa griglia di quelle USDT?
|
||||
print("\n Controllo di ALLINEAMENTO — se le barre USD e USDT fossero su griglie sfasate,")
|
||||
print(" ogni numero sotto sarebbe uno sfasamento di calendario travestito da divergenza")
|
||||
print(" di venue. corr dei RITORNI giornalieri, 2024+ (finestra dove esistono entrambi):")
|
||||
B24 = bin_panel(U13)
|
||||
U24 = usd_panel(U13)
|
||||
al, shifted = [], []
|
||||
for s in U13:
|
||||
if s not in U24.columns or not U24[s].notna().any():
|
||||
continue
|
||||
k = pd.concat({"u": U24[s], "b": B24[s]}, axis=1, join="inner").dropna()
|
||||
k = k[k.index >= pd.Timestamp("2024-01-01", tz="UTC")].pct_change().dropna()
|
||||
if len(k) > 200:
|
||||
al.append((s, float(k["u"].corr(k["b"]))))
|
||||
shifted.append(float(k["u"].shift(1).corr(k["b"]))) # controllo POSITIVO
|
||||
print(" " + " ".join(f"{s}:{c:.4f}" for s, c in al))
|
||||
lo_al = min(c for _, c in al)
|
||||
# ⚠️ Un controllo che non fallisce mai non e' un controllo: il modo di fallire e' uno
|
||||
# sfasamento di UNA barra, e va mostrato che il numero se ne accorge. Con la serie USD
|
||||
# ritardata di un giorno la corr collassa: e' quella la scala del guasto che sto escludendo.
|
||||
print(f" corr minima {lo_al:.4f} | controllo POSITIVO (USD ritardato di 1 giorno): "
|
||||
f"corr mediana {np.median(shifted):+.4f}, massima {np.max(shifted):+.4f}")
|
||||
if lo_al < 0.95:
|
||||
print(" -> SFASAMENTO: i due venue non sono sulla stessa griglia. STOP.")
|
||||
return
|
||||
print(" -> allineati (uno sfasamento darebbe ~0, non 0,99). Gli asset sotto 0,999")
|
||||
print(f" ({', '.join(s for s, c in al if c < 0.999)}) sono solo piu' RUMOROSI in USD")
|
||||
print(" (book piu' sottile sul venue USD), non sfasati.")
|
||||
|
||||
# --------------------------------------------- [2] la divergenza, in DUE pezzi
|
||||
print("\n[2] LA DIVERGENZA USD-vs-USDT, scomposta — e il pezzo che conta e' il SECONDO")
|
||||
Bp = bin_panel(U10).loc[T1_START:T1_END]
|
||||
Up = usd_panel(U10).reindex(Bp.index)
|
||||
both = Bp.notna() & Up.notna()
|
||||
D = 1e4 * np.log(Up / Bp) # scarto di livello, bps
|
||||
D = D.where(both)
|
||||
common = D.median(axis=1) # (a) componente COMUNE (il peg + il venue)
|
||||
idio = D.sub(common, axis=0) # (b) componente IDIOSINCRATICA (cambia il rango)
|
||||
print(f" {len(Bp)} giorni, {len(U10)} asset, {int(both.values.sum())} osservazioni appaiate")
|
||||
print(f" (a) COMUNE |mediana per giorno|: med {common.abs().median():7.1f} bps "
|
||||
f"p95 {common.abs().quantile(.95):7.1f} max {common.abs().max():7.1f}")
|
||||
print(f" (b) IDIOSINCRATICO |scarto de-medianato|: med {idio.abs().stack().median():7.1f} bps "
|
||||
f"p95 {idio.abs().stack().quantile(.95):7.1f} max {idio.abs().stack().max():7.1f}")
|
||||
print(" -> (a) e' cio' che lo z-score cross-sezionale annulla per costruzione; (b) no.")
|
||||
|
||||
print(f"\n LA FINESTRA DEL DEPEG {DEPEG[0]} .. {DEPEG[1]} (UST/LUNA; USDT a ~0,95):")
|
||||
dm = (D.index >= pd.Timestamp(DEPEG[0], tz="UTC")) & (D.index < pd.Timestamp(DEPEG[1], tz="UTC"))
|
||||
print(f" {'giorno':<12}{'comune':>9}" + "".join(f"{s:>8}" for s in U10))
|
||||
for t in D.index[dm]:
|
||||
print(f" {str(t.date()):<12}{common.loc[t]:>9.0f}" +
|
||||
"".join(f"{idio.loc[t, s]:>8.0f}" if np.isfinite(idio.loc[t, s]) else f"{'—':>8}"
|
||||
for s in U10))
|
||||
print(" (numeri in bps; colonna 'comune' = mediana del giorno, le altre = scarto DOPO")
|
||||
print(" averla tolta, cioe' la parte che puo' spostare il ranking)")
|
||||
idm = idio[dm].abs().stack()
|
||||
print(f" idiosincratico NELLA finestra: med {idm.median():.1f} bps p95 "
|
||||
f"{idm.quantile(.95):.1f} max {idm.max():.1f} "
|
||||
f"(FUORI: med {idio[~dm].abs().stack().median():.1f}, "
|
||||
f"p95 {idio[~dm].abs().stack().quantile(.95):.1f})")
|
||||
|
||||
print("\n Ma il ranking non legge il LIVELLO: legge i rapporti a 30 e 90 giorni. Divergenza")
|
||||
print(" idiosincratica degli STESSI rendimenti che entrano nello score (bps):")
|
||||
for L in XS_CFG["lookbacks"]:
|
||||
rB = (Bp / Bp.shift(L) - 1.0) * 1e4
|
||||
rU = (Up / Up.shift(L) - 1.0) * 1e4
|
||||
dR = (rU - rB).where(both)
|
||||
dRi = dR.sub(dR.median(axis=1), axis=0)
|
||||
w = dRi[dm].abs().stack()
|
||||
print(f" r{L:<3d}: idiosincratico med {dRi.abs().stack().median():6.1f} bps "
|
||||
f"p95 {dRi.abs().stack().quantile(.95):7.1f} max {dRi.abs().stack().max():8.1f}"
|
||||
f" | nel depeg med {w.median():6.1f} max {w.max():8.1f}")
|
||||
|
||||
# ------------------------------------------- [3] IL TEST 1 — U10, k=5 congelato
|
||||
print(f"\n[3] TEST 1 — meccanismo CONGELATO su U10, {T1_START} .. {T1_END}, USD vs USDT")
|
||||
print(f" 10 gambe = ESATTAMENTE il minimo del meccanismo (min_assets=10, k=5): nessun")
|
||||
print(f" parametro toccato, e il confronto e' appaiato su TUTTO tranne il venue.")
|
||||
PB = Bp[U10].dropna()
|
||||
PU = Up[U10].dropna()
|
||||
idxc = PB.index.intersection(PU.index)
|
||||
PB, PU = PB.loc[idxc], PU.loc[idxc]
|
||||
print(f" pannello comune: {len(idxc)} giorni, {PB.index[0].date()} .. {PB.index[-1].date()}")
|
||||
|
||||
rows = []
|
||||
for ph in PHASES:
|
||||
a = alive(X.xsec_engine(PU, phase=ph))
|
||||
b = alive(X.xsec_engine(PB, phase=ph))
|
||||
a, b = pair(a, b)
|
||||
rows.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b),
|
||||
d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b), n=len(a)))
|
||||
R = pd.DataFrame(rows)
|
||||
print(f" {'fase':>5}{'corr':>9}{'Sh USD':>9}{'Sh USDT':>9}{'dSharpe':>10}"
|
||||
f"{'DD USD':>9}{'DD USDT':>9}")
|
||||
for _, r in R.iterrows():
|
||||
print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>9.2f}{r['sh_usdt']:>9.2f}"
|
||||
f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%")
|
||||
print(f" {'MEDIANA':>5}{R['corr'].median():>9.4f}{R['sh_usd'].median():>9.2f}"
|
||||
f"{R['sh_usdt'].median():>9.2f}{R['d'].median():>+10.3f}"
|
||||
f"{R['dd_usd'].median()*100:>8.1f}%{R['dd_usdt'].median()*100:>8.1f}%")
|
||||
print(f" corr MINIMA fra le 10 fasi: {R['corr'].min():.4f} (fase {int(R.loc[R['corr'].idxmin(),'ph'])})"
|
||||
f" | soglia dichiarata {CORR_SOGLIA} -> "
|
||||
f"{'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}")
|
||||
print(f" dSharpe appaiato (USD - USDT): mediana {R['d'].median():+.3f} "
|
||||
f"banda [{R['d'].min():+.3f},{R['d'].max():+.3f}] "
|
||||
f"fasi in cui USD e' MEGLIO {(R['d'] > 0).sum()}/{len(R)}")
|
||||
print(f" ⚠️ la statistica e' la MEDIANA DELLE DIFFERENZE APPAIATE, non la differenza delle")
|
||||
print(f" mediane (lezione 26/07: quest'ultima confronta fasi diverse e puo' ribaltare).")
|
||||
|
||||
# quanto di §21 riproduce questo sottoinsieme? (o si misura l'universo, non il venue)
|
||||
p13 = bin_panel(U13)[U13].dropna()
|
||||
s13_w = win(alive(X.xsec_engine(p13, phase=0)), T1_START, T1_END)
|
||||
s10_w = win(alive(X.xsec_engine(PB, phase=0)), T1_START, T1_END)
|
||||
print(f"\n Contesto obbligatorio — quanto di §21 vive in questo sottoinsieme (Binance, fase 0,")
|
||||
print(f" stessa finestra): U13 Sh {sh(s13_w):+.2f} | U10 Sh {sh(s10_w):+.2f}")
|
||||
print(f" -> il TEST 1 giudica il VENUE su U10; NON e' una replica del titolo di §21.")
|
||||
|
||||
# ------------------------------------------------ [4] i RANKING, non i P&L
|
||||
print("\n[4] IL CANALE DEL DANNO — cambia il RANKING? (e' cio' che la strategia legge)")
|
||||
print(" Un P&L simile puo' nascere da due ranking diversi che si compensano: la domanda")
|
||||
print(" diretta e' se le GAMBE SCELTE cambiano. Scansione su tutte e 10 le fasi.")
|
||||
D1 = ranking_scan(PB, PU, PHASES, "TEST 1 / U10")
|
||||
if len(D1):
|
||||
ind = D1[(D1["data"] >= pd.Timestamp(DEPEG[0], tz="UTC")) &
|
||||
(D1["data"] < pd.Timestamp(DEPEG[1], tz="UTC"))]
|
||||
print(f" di cui dentro la finestra del depeg: {len(ind)}")
|
||||
|
||||
# ------------------------------- [5] TEST 2 — U13 ibrido, finestra INTERA di §21
|
||||
print(f"\n[5] TEST 2 — U13 IBRIDO (USD dove esiste, USDT altrove), {T2_START} .. {T2_END}")
|
||||
print(" Domanda operativa: sostituendo TUTTO il prezzo in dollari che esiste, il titolo")
|
||||
print(" di §21 (mediana di fase +1,12 fuori campione) si muove?")
|
||||
HB, meta = hybrid_panel(U13)
|
||||
print(" prima data USD per asset: " + " ".join(f"{s}:{meta[s]}" for s in U13))
|
||||
PBh = p13
|
||||
PUh = HB[U13].dropna()
|
||||
ic = PBh.index.intersection(PUh.index)
|
||||
PBh, PUh = PBh.loc[ic], PUh.loc[ic]
|
||||
rows2 = []
|
||||
for ph in PHASES:
|
||||
a = win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END)
|
||||
b = win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END)
|
||||
a, b = pair(a, b)
|
||||
rows2.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b),
|
||||
d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b)))
|
||||
R2 = pd.DataFrame(rows2)
|
||||
print(f" {'fase':>5}{'corr':>9}{'Sh IBRIDO':>11}{'Sh USDT':>10}{'dSharpe':>10}"
|
||||
f"{'DD IBR':>9}{'DD USDT':>9}")
|
||||
for _, r in R2.iterrows():
|
||||
print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>11.2f}{r['sh_usdt']:>10.2f}"
|
||||
f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%")
|
||||
print(f" {'MEDIANA':>5}{R2['corr'].median():>9.4f}{R2['sh_usd'].median():>11.2f}"
|
||||
f"{R2['sh_usdt'].median():>10.2f}{R2['d'].median():>+10.3f}"
|
||||
f"{R2['dd_usd'].median()*100:>8.1f}%{R2['dd_usdt'].median()*100:>8.1f}%")
|
||||
print(f" corr minima {R2['corr'].min():.4f} -> "
|
||||
f"{'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'} (soglia {CORR_SOGLIA})")
|
||||
print(f" 📌 IL NUMERO DI TESTA DI §21, RICALCOLATO IN DOLLARI: mediana di fase fuori")
|
||||
print(f" campione {R2['sh_usdt'].median():+.2f} (USDT, replica) -> "
|
||||
f"{R2['sh_usd'].median():+.2f} (IBRIDO USD).")
|
||||
|
||||
# differenza appaiata con la macchineria del progetto (stesso gate di §21)
|
||||
dlt = A.anchor_luck_delta(
|
||||
lambda ph: win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END),
|
||||
lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END),
|
||||
PHASES, metric=sh)
|
||||
print(f" anchor_luck_delta (IBRIDO - USDT): mediana appaiata {dlt['median_paired']:+.3f} "
|
||||
f"[{dlt['lo']:+.3f},{dlt['hi']:+.3f}], fasi con IBRIDO>USDT "
|
||||
f"{dlt['n_positive']}/{dlt['n_anchors']}")
|
||||
print("\n E il ranking, sull'ibrido (la fase con la corr piu' bassa va GUARDATA, non")
|
||||
print(" riassunta: una corr di 0,97 su una fase o e' un ribaltamento vero o e' un giunto):")
|
||||
D2 = ranking_scan(PBh, PUh, PHASES, "TEST 2 / U13-IBRIDO")
|
||||
|
||||
# ------------------------------- [5b] le gambe che NESSUN venue USD puo' controllare
|
||||
print("\n[5b] LE TRE GAMBE NON CONTROLLABILI — quanto del risultato di §21 ci passa?")
|
||||
print(" BNB e' il token nativo del venue sotto esame e NON esiste in USD nel 2021-23;")
|
||||
print(" NEAR e INJ hanno un prezzo USD solo dal 2022-09. Se il titolo di §21 vivesse su")
|
||||
print(" di loro, questo falsificatore non avrebbe potuto vedere niente, e andrebbe detto.")
|
||||
base_b = A.anchor_luck_band(
|
||||
lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END),
|
||||
PHASES, canonical=0, metric=sh)
|
||||
print(f" base U13 (Binance, fuori campione, mediana di fase): {base_b['median']:+.2f}")
|
||||
for a_ in ("BNB", "NEAR", "INJ"):
|
||||
sub = p13[[c for c in U13 if c != a_]].dropna()
|
||||
bb = A.anchor_luck_band(
|
||||
lambda ph, sub=sub: win(alive(X.xsec_engine(sub, phase=ph, min_assets=10)),
|
||||
T2_START, T2_END),
|
||||
PHASES, canonical=0, metric=sh)
|
||||
print(f" senza {a_:<5s}: mediana di fase {bb['median']:+.2f} "
|
||||
f"({bb['median']-base_b['median']:+.2f}) banda [{bb['lo']:+.2f},{bb['hi']:+.2f}]")
|
||||
print(" (leave-one-out a 12 gambe: il meccanismo congelato regge, min_assets=10)")
|
||||
|
||||
# ---------------------------------------------------- [6] IL CASO PEGGIORE
|
||||
print("\n[6] IL CASO PEGGIORE — non la media: dove i due venue divergono di piu'?")
|
||||
print(f" {'anno':>6}" + "".join(f"{k:>12}" for k in
|
||||
("corr T1", "dSh T1", "corr T2", "dSh T2")))
|
||||
for y in (2021, 2022, 2023):
|
||||
a1 = b1 = a2 = b2 = None
|
||||
cs1, ds1, cs2, ds2 = [], [], [], []
|
||||
for ph in PHASES:
|
||||
a1 = win(alive(X.xsec_engine(PU, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
|
||||
b1 = win(alive(X.xsec_engine(PB, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
|
||||
if len(a1) > 60 and len(b1) > 60:
|
||||
a1, b1 = pair(a1, b1)
|
||||
cs1.append(float(a1.corr(b1))); ds1.append(sh(a1) - sh(b1))
|
||||
a2 = win(alive(X.xsec_engine(PUh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
|
||||
b2 = win(alive(X.xsec_engine(PBh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
|
||||
if len(a2) > 60 and len(b2) > 60:
|
||||
a2, b2 = pair(a2, b2)
|
||||
cs2.append(float(a2.corr(b2))); ds2.append(sh(a2) - sh(b2))
|
||||
f1 = f"{np.median(cs1):.4f}" if cs1 else "—"
|
||||
g1 = f"{np.median(ds1):+.3f}" if ds1 else "—"
|
||||
f2 = f"{np.median(cs2):.4f}" if cs2 else "—"
|
||||
g2 = f"{np.median(ds2):+.3f}" if ds2 else "—"
|
||||
print(f" {y:>6}{f1:>12}{g1:>12}{f2:>12}{g2:>12}")
|
||||
worst1 = R.loc[R["corr"].idxmin()]
|
||||
worst2 = R2.loc[R2["corr"].idxmin()]
|
||||
wd1 = R.loc[R["d"].abs().idxmax()]
|
||||
wd2 = R2.loc[R2["d"].abs().idxmax()]
|
||||
print(f" peggior FASE per corr: T1 fase {int(worst1['ph'])} corr {worst1['corr']:.4f} | "
|
||||
f"T2 fase {int(worst2['ph'])} corr {worst2['corr']:.4f}")
|
||||
print(f" peggior FASE per dSharpe: T1 fase {int(wd1['ph'])} {wd1['d']:+.3f} | "
|
||||
f"T2 fase {int(wd2['ph'])} {wd2['d']:+.3f}")
|
||||
|
||||
st = idio.abs().stack().sort_values(ascending=False)
|
||||
print(" le 8 divergenze IDIOSINCRATICHE piu' grandi della finestra (bps di livello):")
|
||||
for (t, s), v in st.head(8).items():
|
||||
indep = "DEPEG" if pd.Timestamp(DEPEG[0], tz="UTC") <= t < pd.Timestamp(DEPEG[1], tz="UTC") else ""
|
||||
print(f" {str(t.date()):<12}{s:<6}{v:8.0f} bps (comune quel giorno "
|
||||
f"{common.loc[t]:+.0f} bps) {indep}")
|
||||
print(" -> se le peggiori NON cadono nel depeg, il rumore di venue e' book sottile,")
|
||||
print(" non contaminazione da stablecoin: due cause diverse, due implicazioni diverse.")
|
||||
|
||||
# il giorno peggiore: dove il P&L dei due sleeve si stacca di piu'
|
||||
a0 = alive(X.xsec_engine(PU, phase=0)); b0 = alive(X.xsec_engine(PB, phase=0))
|
||||
a0, b0 = pair(a0, b0)
|
||||
gap = (a0 - b0)
|
||||
print(f" scarto GIORNALIERO fra i due sleeve (fase 0): med "
|
||||
f"{gap.abs().median()*1e4:.1f} bps p99 {gap.abs().quantile(.99)*1e4:.1f} bps "
|
||||
f"max {gap.abs().max()*1e4:.1f} bps il {gap.abs().idxmax().date()}")
|
||||
cum = (1 + a0).cumprod().iloc[-1] / (1 + b0).cumprod().iloc[-1] - 1
|
||||
print(f" divario CUMULATO su tutta la finestra del TEST 1: {cum*100:+.2f}% "
|
||||
f"(equity USD vs equity USDT)")
|
||||
|
||||
# --------------------------------------------------------------- [7] gate
|
||||
print("\n[7] GATE")
|
||||
print(f" soglia di venue (dichiarata dall'autore PRIMA, non da me): corr >= {CORR_SOGLIA}")
|
||||
print(f" TEST 1 U10 congelato : corr mediana {R['corr'].median():.4f}, minima "
|
||||
f"{R['corr'].min():.4f} -> {'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}")
|
||||
print(f" TEST 2 U13 ibrido : corr mediana {R2['corr'].median():.4f}, minima "
|
||||
f"{R2['corr'].min():.4f} -> {'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'}")
|
||||
print(f" riferimento 2024+ di §21 (Binance vs Hyperliquid): 0,9991 / dSharpe -0,025")
|
||||
su = alive(X.xsec_engine(PUh, phase=0))
|
||||
mg = A.marginal_vs_tp01(win(su, T2_START, T2_END))
|
||||
print(f" marginal_vs_tp01 sullo sleeve IBRIDO fuori campione: {mg.get('marginal_verdict')} "
|
||||
f"(corr {mg.get('corr_full')}, is_hedge {mg.get('is_hedge')}, "
|
||||
f"insample_edge {mg.get('has_insample_edge')})")
|
||||
imp = A.implausible_sharpe(win(su, T2_START, T2_END))
|
||||
print(f" implausible_sharpe (ibrido OOS): {imp['implausible']} Sh {imp.get('sharpe'):+.2f} "
|
||||
f"perdite su barre attive {imp.get('loss_frac', float('nan'))*100:.1f}%")
|
||||
print(" causalita': NON RI-GIRATA — il motore e' importato invariato da r0822b_xs01_oos.py,")
|
||||
print(" che la verifica (prezzi futuri randomizzati -> max|diff| = 0). Cambiare il venue")
|
||||
print(" cambia i dati, non l'ordine temporale in cui il motore li legge.")
|
||||
print(" deflated_sharpe: NON PERTINENTE — qui non si SELEZIONA una cella, si confronta due")
|
||||
print(" volte lo STESSO meccanismo su due fonti di prezzo. Nessun trial nuovo di strategia.")
|
||||
print(" null de-levering: NON PERTINENTE (nessun claim di 'meno drawdown').")
|
||||
print(" anchor: girato per costruzione — ogni numero sopra e' su tutte e 10 le fasi.")
|
||||
|
||||
print(f"\n[fine] {time.time()-t0:.1f}s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user