test(skyhook): demo anchors + dual-TF alignment + causality + V1 robustness (5 pass)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""Test della strategia SKH01 (Skyhook) — dual-timeframe regime+breakout su BTC/ETH.
|
||||
|
||||
Coprono: fedelta' al brief (ancore demo BuzVola/BuzVolume), allineamento dual-TF, assenza di
|
||||
look-ahead (causalita'), e robustezza onesta del config V1 su entrambi gli asset.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "skyhook"))
|
||||
|
||||
from src.data.downloader import load_data
|
||||
from src.strategies.skyhook import (
|
||||
HTF_MIN, LTF_MIN, SkyhookParams, build_frames, chande01, skyhook_entries)
|
||||
|
||||
# config V1 (vincente del lever-scout/grid; vedi diario 2026-06-23-skyhook)
|
||||
V1 = dict(ptn_n=55, sl_atr=2.5, tp_atr=6.0, vola_lo=35, vola_hi=95, vol_lo=0.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fedelta' al brief: indicatori tipo-Chande, normalizzati 0-100.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_chande01_anchors():
|
||||
"""Semantica del brief: volatilita'/volume STEADY -> 50 (neutro); in RAMPA -> 100; in CALO -> 0."""
|
||||
n = 100
|
||||
assert abs(chande01(np.full(n, 7.0), 13)[-1] - 50.0) < 1e-9 # costante -> neutro
|
||||
assert abs(chande01(np.arange(n, dtype=float), 13)[-1] - 100.0) < 1e-9 # rampa su -> 100
|
||||
assert abs(chande01(np.arange(n, 0, -1, dtype=float), 13)[-1] - 0.0) < 1e-9 # rampa giu' -> 0
|
||||
|
||||
|
||||
def test_demo_buzvola_buzvolume():
|
||||
"""Ancore della demo: ATR costante (vol steady) -> BuzVola 50; volume in rampa -> BuzVolume 100."""
|
||||
n = 100
|
||||
buz_vola = chande01(np.full(n, 2.0), 13) # ATR steady
|
||||
buz_volume = chande01(np.linspace(1000, 5000, n), 13) # volume in rampa
|
||||
assert abs(buz_vola[-1] - 50.0) < 1e-9
|
||||
assert abs(buz_volume[-1] - 100.0) < 1e-9
|
||||
# oscillatori sempre in [0,100]
|
||||
assert chande01(np.random.default_rng(0).normal(size=500).cumsum() + 100, 13)[20:].min() >= -1e-9
|
||||
assert chande01(np.random.default_rng(1).normal(size=500).cumsum() + 100, 13)[20:].max() <= 100 + 1e-9
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Allineamento dual-timeframe: 690 = 3 x 230, confini HTF subset dei confini LTF.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_dual_tf_alignment():
|
||||
assert HTF_MIN == 3 * LTF_MIN
|
||||
ltf, htf = build_frames(load_data("BTC", "5m"))
|
||||
# ogni timestamp (open) HTF e' anche un open LTF (stessa griglia epoch)
|
||||
ltf_opens = set(ltf["timestamp"].astype("int64").tolist())
|
||||
htf_opens = htf["timestamp"].astype("int64").tolist()
|
||||
inside = sum(t in ltf_opens for t in htf_opens)
|
||||
assert inside / len(htf_opens) > 0.99, "i confini HTF devono essere un sottoinsieme dei confini LTF"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Causalita': gli ingressi su un prefisso devono coincidere con la run completa.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_no_lookahead_entries():
|
||||
p = SkyhookParams(**V1)
|
||||
ltf, htf = build_frames(load_data("BTC", "5m"))
|
||||
full = skyhook_entries(ltf, htf, p)
|
||||
n = len(ltf)
|
||||
cut = int(n * 0.85)
|
||||
cut_ts = int(ltf["timestamp"].iloc[cut - 1])
|
||||
htf_cut = htf[htf["timestamp"] <= cut_ts].reset_index(drop=True)
|
||||
sub = skyhook_entries(ltf.iloc[:cut].reset_index(drop=True), htf_cut, p)
|
||||
for i in range(cut - 200, cut):
|
||||
a, b = full[i], sub[i]
|
||||
assert (a is None) == (b is None)
|
||||
if a is not None:
|
||||
assert a["dir"] == b["dir"]
|
||||
assert abs(a["sl"] - b["sl"]) < 1e-6 and abs(a["tp"] - b["tp"]) < 1e-6
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Robustezza onesta del config V1: PASS su BTC E ETH, netto fee, OOS.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_v1_robust_both_assets():
|
||||
import skyhooklib as sk
|
||||
p = SkyhookParams(**V1)
|
||||
for a in ("BTC", "ETH"):
|
||||
r = sk.run_asset(a, p, sk.FEE_RT)
|
||||
assert r["full"]["sharpe"] >= 0.5, f"{a} FULL Sharpe basso: {r['full']['sharpe']}"
|
||||
assert r["holdout"]["sharpe"] >= 0.2, f"{a} HOLD-OUT Sharpe basso: {r['holdout']['sharpe']}"
|
||||
assert r["full"]["n_trades"] >= 20, f"{a} troppo pochi trade: {r['full']['n_trades']}"
|
||||
assert sk.causality(p, "BTC")["ok"] and sk.causality(p, "ETH")["ok"]
|
||||
Reference in New Issue
Block a user