feat(skyhook): pos_fn introspection for SKH01 sleeve (current open trade / flat)
_skyhook_positions(): replays the non-overlap entry+exit logic (TP/SL/max_bars) to the last closed 230m bar and reports, per asset, the current OPEN trade (dir/entry/sl/tp/bars_in) or 'flat'. Wired into skyhook_sleeve(pos_fn=...) so the Deribit book report & web dashboard show Skyhook's live position. Causal (closed bars only). +1 test. Currently flat/flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -236,8 +236,41 @@ def _skyhook_returns() -> pd.Series:
|
|||||||
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
|
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
|
||||||
|
|
||||||
|
|
||||||
|
def _skyhook_positions() -> dict:
|
||||||
|
"""Stato corrente del book Skyhook per asset (introspezione live): se c'e' un trade APERTO ORA
|
||||||
|
-> dir/entry/sl/tp/barre-trascorse; altrimenti 'flat'. Replica la logica non-overlap di
|
||||||
|
entry+exit (TP/SL/max_bars) fino all'ultima barra 230m chiusa. Causale: usa solo barre chiuse."""
|
||||||
|
out = {}
|
||||||
|
for a in ASSETS:
|
||||||
|
ltf, htf = build_frames(load_data(a, "5m"))
|
||||||
|
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
|
||||||
|
H = ltf["high"].values; L = ltf["low"].values; Cc = ltf["close"].values
|
||||||
|
n = len(ltf); i = 0; open_pos = "flat"
|
||||||
|
while i < n:
|
||||||
|
e = ent[i]
|
||||||
|
if e is None:
|
||||||
|
i += 1; continue
|
||||||
|
d, sl, tp, mb = e["dir"], e["sl"], e["tp"], e["max_bars"]
|
||||||
|
exit_idx = None
|
||||||
|
for s in range(1, mb + 1):
|
||||||
|
j = i + s
|
||||||
|
if j >= n: # non ancora uscito: posizione APERTA ora
|
||||||
|
break
|
||||||
|
hit = (L[j] <= sl or H[j] >= tp) if d == 1 else (H[j] >= sl or L[j] <= tp)
|
||||||
|
if hit or s == mb:
|
||||||
|
exit_idx = j; break
|
||||||
|
if exit_idx is None:
|
||||||
|
open_pos = dict(dir="LONG" if d == 1 else "SHORT", entry=round(float(Cc[i]), 2),
|
||||||
|
sl=round(float(sl), 2), tp=round(float(tp), 2),
|
||||||
|
bars_in=int((n - 1) - i), max_bars=int(mb))
|
||||||
|
break
|
||||||
|
i = exit_idx + 1
|
||||||
|
out[a] = open_pos
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def skyhook_sleeve(weight: float = 0.25) -> Sleeve:
|
def skyhook_sleeve(weight: float = 0.25) -> Sleeve:
|
||||||
return Sleeve("SKH01_skyhook", weight, _skyhook_returns)
|
return Sleeve("SKH01_skyhook", weight, _skyhook_returns, pos_fn=_skyhook_positions)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------- REGISTRY -----------------------------
|
# ----------------------------- REGISTRY -----------------------------
|
||||||
|
|||||||
@@ -129,6 +129,16 @@ def test_short_override_changes_only_shorts():
|
|||||||
assert longs_same > 0 and shorts_diff > 0
|
assert longs_same > 0 and shorts_diff > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_skyhook_pos_fn_structure():
|
||||||
|
"""pos_fn introspettiva: dict BTC/ETH, ciascuno 'flat' o dict con dir/entry/sl/tp coerenti."""
|
||||||
|
from src.portfolio.sleeves import _skyhook_positions
|
||||||
|
pos = _skyhook_positions()
|
||||||
|
assert set(pos.keys()) == {"BTC", "ETH"}
|
||||||
|
for a, p in pos.items():
|
||||||
|
assert p == "flat" or (isinstance(p, dict) and p["dir"] in ("LONG", "SHORT")
|
||||||
|
and p["sl"] > 0 and p["tp"] > 0 and 0 <= p["bars_in"] <= p["max_bars"])
|
||||||
|
|
||||||
|
|
||||||
def test_v2dd_robust_both_assets():
|
def test_v2dd_robust_both_assets():
|
||||||
"""SKH01-V2-DD: PASS netto fee su BTCÐ, hold-out forte, e maxDD standalone <30%."""
|
"""SKH01-V2-DD: PASS netto fee su BTCÐ, hold-out forte, e maxDD standalone <30%."""
|
||||||
import skyhooklib as sk
|
import skyhooklib as sk
|
||||||
|
|||||||
Reference in New Issue
Block a user