start experiment 59 (exp/59-signal-quality-gate-walk-forward-test-wi)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
{%- set LAKE = TAC_LAKE_DIR %}
|
||||
{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %}
|
||||
{%- set SP_FIELDS = "sp_ret,sp_ou_zscore,sp_ou_half_life,sp_ou_revert,sp_hmm_p_regime1,sp_hmm_state,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %}
|
||||
{%- set GATE_PATH = "/app/experiments/book/data/signal_quality_gate/sq_gate_2021.pkl" %}
|
||||
{# Gate computed on-the-fly from signal + lake close prices #}
|
||||
|
||||
qlib_init:
|
||||
provider_uri: "{{ LAKE }}"
|
||||
@@ -90,7 +90,12 @@ task:
|
||||
module_path: tac_qlib.contrib.strategy.signal_quality_gate
|
||||
kwargs:
|
||||
signal: "<PRED>"
|
||||
signal_quality_gate_path: "{{ GATE_PATH }}"
|
||||
lake_root: "{{ LAKE }}"
|
||||
gate_topk: 10
|
||||
gate_lookback: 5
|
||||
gate_threshold: 0.5
|
||||
gate_start: "2015-01-03"
|
||||
gate_end: "2021-12-31"
|
||||
topk: 10
|
||||
n_drop: 1
|
||||
only_tradable: true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{%- set LAKE = TAC_LAKE_DIR %}
|
||||
{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %}
|
||||
{%- set SP_FIELDS = "sp_ret,sp_ou_zscore,sp_ou_half_life,sp_ou_revert,sp_hmm_p_regime1,sp_hmm_state,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %}
|
||||
{%- set GATE_PATH = "/app/experiments/book/data/signal_quality_gate/sq_gate_2023.pkl" %}
|
||||
{# Gate computed on-the-fly from signal + lake close prices #}
|
||||
|
||||
qlib_init:
|
||||
provider_uri: "{{ LAKE }}"
|
||||
@@ -90,7 +90,12 @@ task:
|
||||
module_path: tac_qlib.contrib.strategy.signal_quality_gate
|
||||
kwargs:
|
||||
signal: "<PRED>"
|
||||
signal_quality_gate_path: "{{ GATE_PATH }}"
|
||||
lake_root: "{{ LAKE }}"
|
||||
gate_topk: 10
|
||||
gate_lookback: 5
|
||||
gate_threshold: 0.5
|
||||
gate_start: "2015-01-03"
|
||||
gate_end: "2023-12-29"
|
||||
topk: 10
|
||||
n_drop: 1
|
||||
only_tradable: true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{%- set LAKE = TAC_LAKE_DIR %}
|
||||
{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %}
|
||||
{%- set SP_FIELDS = "sp_ret,sp_ou_zscore,sp_ou_half_life,sp_ou_revert,sp_hmm_p_regime1,sp_hmm_state,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %}
|
||||
{%- set GATE_PATH = "/app/experiments/book/data/signal_quality_gate/sq_gate_2024.pkl" %}
|
||||
{# Gate computed on-the-fly from signal + lake close prices #}
|
||||
|
||||
qlib_init:
|
||||
provider_uri: "{{ LAKE }}"
|
||||
@@ -90,7 +90,12 @@ task:
|
||||
module_path: tac_qlib.contrib.strategy.signal_quality_gate
|
||||
kwargs:
|
||||
signal: "<PRED>"
|
||||
signal_quality_gate_path: "{{ GATE_PATH }}"
|
||||
lake_root: "{{ LAKE }}"
|
||||
gate_topk: 10
|
||||
gate_lookback: 5
|
||||
gate_threshold: 0.5
|
||||
gate_start: "2015-01-03"
|
||||
gate_end: "2024-12-31"
|
||||
topk: 10
|
||||
n_drop: 1
|
||||
only_tradable: true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{%- set LAKE = TAC_LAKE_DIR %}
|
||||
{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %}
|
||||
{%- set SP_FIELDS = "sp_ret,sp_ou_zscore,sp_ou_half_life,sp_ou_revert,sp_hmm_p_regime1,sp_hmm_state,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %}
|
||||
{%- set GATE_PATH = "/app/experiments/book/data/signal_quality_gate/sq_gate_2025.pkl" %}
|
||||
{# Gate computed on-the-fly from signal + lake close prices #}
|
||||
|
||||
qlib_init:
|
||||
provider_uri: "{{ LAKE }}"
|
||||
@@ -90,7 +90,12 @@ task:
|
||||
module_path: tac_qlib.contrib.strategy.signal_quality_gate
|
||||
kwargs:
|
||||
signal: "<PRED>"
|
||||
signal_quality_gate_path: "{{ GATE_PATH }}"
|
||||
lake_root: "{{ LAKE }}"
|
||||
gate_topk: 10
|
||||
gate_lookback: 5
|
||||
gate_threshold: 0.5
|
||||
gate_start: "2015-01-03"
|
||||
gate_end: "2025-12-31"
|
||||
topk: 10
|
||||
n_drop: 1
|
||||
only_tradable: true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{%- set LAKE = TAC_LAKE_DIR %}
|
||||
{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %}
|
||||
{%- set SP_FIELDS = "sp_ret,sp_ou_zscore,sp_ou_half_life,sp_ou_revert,sp_hmm_p_regime1,sp_hmm_state,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %}
|
||||
{%- set GATE_PATH = "/app/experiments/book/data/signal_quality_gate/sq_gate_2026.pkl" %}
|
||||
{# Gate computed on-the-fly from signal + lake close prices #}
|
||||
|
||||
qlib_init:
|
||||
provider_uri: "{{ LAKE }}"
|
||||
@@ -90,7 +90,12 @@ task:
|
||||
module_path: tac_qlib.contrib.strategy.signal_quality_gate
|
||||
kwargs:
|
||||
signal: "<PRED>"
|
||||
signal_quality_gate_path: "{{ GATE_PATH }}"
|
||||
lake_root: "{{ LAKE }}"
|
||||
gate_topk: 10
|
||||
gate_lookback: 5
|
||||
gate_threshold: 0.5
|
||||
gate_start: "2015-01-03"
|
||||
gate_end: "2026-08-19"
|
||||
topk: 10
|
||||
n_drop: 1
|
||||
only_tradable: true
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
# TradeAC custom-qlib-code snapshot (auto-generated)
|
||||
# parent repo HEAD : fd5382caa4c4e96417a16d5a22006d9a7d625d00
|
||||
# parent repo HEAD : e952feed0a66a20439f4f24ad5524233429cd0c3
|
||||
# tac-qlib/tac_qlib/contrib
|
||||
# tac-qlib/tac_qlib/data
|
||||
# per-file hashes (git hash-object):
|
||||
@@ -20,12 +20,12 @@
|
||||
cd3133cfbd2556b25c106e39ae97fb128df0e326 tac-qlib/tac_qlib/contrib/strategy/__pycache__/ic_gate.cpython-312.pyc
|
||||
6dd1c568a2961842793674390d5abffd1a0e71b8 tac-qlib/tac_qlib/contrib/strategy/__pycache__/optimal_stop.cpython-312.pyc
|
||||
03b5e4d80da00800f1b108bee0735d3d18d856d1 tac-qlib/tac_qlib/contrib/strategy/__pycache__/regime_gate.cpython-312.pyc
|
||||
ebcbd48b5de9bf6da84af4d6916c97d03bb6acfa tac-qlib/tac_qlib/contrib/strategy/__pycache__/signal_quality_gate.cpython-312.pyc
|
||||
a6a1c21ab71b62080830df45c4784b73c1531036 tac-qlib/tac_qlib/contrib/strategy/__pycache__/signal_quality_gate.cpython-312.pyc
|
||||
755e3b139496a5e22b0328db45c8199c33029fbc tac-qlib/tac_qlib/contrib/strategy/__pycache__/weekly_rebalance.cpython-312.pyc
|
||||
519a1f4c05dbe0ac018ab8b779eb33d53b4dd545 tac-qlib/tac_qlib/contrib/strategy/ic_gate.py
|
||||
79aaad9e39fcc740a773f4f63c512ce1086cfde0 tac-qlib/tac_qlib/contrib/strategy/optimal_stop.py
|
||||
7bcee5f0b09cfa721440f1354f16f2dd9a112b12 tac-qlib/tac_qlib/contrib/strategy/regime_gate.py
|
||||
0101bc87b67c6b10debe0eb9c7e5fa910e919b41 tac-qlib/tac_qlib/contrib/strategy/signal_quality_gate.py
|
||||
16ab80b731aab6d8bb818525615d77c2d1fcb0c8 tac-qlib/tac_qlib/contrib/strategy/signal_quality_gate.py
|
||||
fe60bacdfedd48617863be31f24b7c7daebfac5a tac-qlib/tac_qlib/contrib/strategy/weekly_rebalance.py
|
||||
92e6e90eb0cd0a25142034560f27adb6b705b1a8 tac-qlib/tac_qlib/data/__init__.py
|
||||
316bf4aa160cc8d15929ea648be03f4b4999667d tac-qlib/tac_qlib/data/__pycache__/__init__.cpython-312.pyc
|
||||
|
||||
BIN
Binary file not shown.
@@ -28,26 +28,118 @@ __all__ = ["SignalQualityGateStrategy", "compute_signal_quality_gate"]
|
||||
class SignalQualityGateStrategy(TopkDropoutStrategy):
|
||||
"""TopkDropout with signal-quality gate overlay.
|
||||
|
||||
When ``lake_root`` is provided the gate is computed on-the-fly from the
|
||||
signal (``<PRED>``) and close prices — no precomputed gate file needed.
|
||||
This ensures the gate matches the model that is actually generating the
|
||||
predictions (critical when the model is retrained each year).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
topk, n_drop, method_sell, method_buy, hold_thresh, only_tradable,
|
||||
forbid_all_trade_at_limit : same as ``TopkDropoutStrategy``.
|
||||
signal_quality_gate : pd.Series — precomputed per-date gate (bool indexed
|
||||
by datetime). True = trade allowed, False = no orders. Missing dates
|
||||
default to open (trade allowed).
|
||||
signal_quality_gate : pd.Series — precomputed per-date gate (bool).
|
||||
signal_quality_gate_path : str — path to pickled gate Series.
|
||||
lake_root : str — lake root for on-the-fly gate computation (preferred).
|
||||
gate_topk, gate_lookback, gate_threshold : int/float — gate params.
|
||||
gate_start, gate_end : str — date window for loading close prices.
|
||||
"""
|
||||
|
||||
def __init__(self, *, signal_quality_gate=None, signal_quality_gate_path=None, **kwargs):
|
||||
def __init__(self, *, signal_quality_gate=None, signal_quality_gate_path=None,
|
||||
lake_root=None, gate_topk=10, gate_lookback=5, gate_threshold=0.5,
|
||||
gate_start="2015-01-03", gate_end="2026-08-19", **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._sq_gate_computed = False
|
||||
if signal_quality_gate is not None:
|
||||
self._sq_gate = signal_quality_gate
|
||||
self._sq_gate_computed = True
|
||||
elif signal_quality_gate_path is not None:
|
||||
import pickle
|
||||
with open(signal_quality_gate_path, "rb") as f:
|
||||
self._sq_gate = pickle.load(f)
|
||||
self._sq_gate_computed = True
|
||||
elif lake_root is not None:
|
||||
self._sq_gate = None
|
||||
self._lake_root = lake_root
|
||||
self._gate_topk = gate_topk
|
||||
self._gate_lookback = gate_lookback
|
||||
self._gate_threshold = gate_threshold
|
||||
self._gate_start = gate_start
|
||||
self._gate_end = gate_end
|
||||
else:
|
||||
self._sq_gate = None
|
||||
|
||||
def _compute_gate_on_fly(self):
|
||||
"""Compute gate from the signal (pred.pkl) and lake close prices."""
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
|
||||
signal_path = self._signal
|
||||
if not Path(signal_path).exists():
|
||||
return
|
||||
|
||||
with open(signal_path, "rb") as f:
|
||||
pred = pickle.load(f)
|
||||
|
||||
# Handle MultiIndex DataFrame -> unstack to wide
|
||||
if isinstance(pred, pd.DataFrame) and isinstance(pred.index, pd.MultiIndex):
|
||||
pred = pred.iloc[:, 0]
|
||||
pred.index = pd.MultiIndex.from_arrays([
|
||||
pd.to_datetime(pred.index.get_level_values(0)).normalize(),
|
||||
pred.index.get_level_values(1)
|
||||
])
|
||||
pred = pred.unstack(level=1)
|
||||
elif isinstance(pred, pd.DataFrame):
|
||||
pred = pred.iloc[:, 0] if pred.shape[1] >= 1 else pred.squeeze()
|
||||
pred.index = pd.to_datetime(pred.index).normalize()
|
||||
|
||||
# Load close prices from lake
|
||||
close_df = _load_close_prices(self._lake_root, "US", self._gate_start, self._gate_end)
|
||||
if close_df.empty:
|
||||
return
|
||||
|
||||
ret_df = close_df.pct_change()
|
||||
ret_df.index = pd.to_datetime(ret_df.index).normalize()
|
||||
|
||||
pred_dates = sorted(pred.index.unique())
|
||||
if len(pred_dates) < 2:
|
||||
self._sq_gate = pd.Series(True, index=pd.DatetimeIndex(pred_dates))
|
||||
self._sq_gate_computed = True
|
||||
return
|
||||
|
||||
hit_rates = {}
|
||||
for i in range(1, len(pred_dates)):
|
||||
day = pred_dates[i]
|
||||
prev_day = pred_dates[i - 1]
|
||||
try:
|
||||
prev_scores = pred.loc[prev_day]
|
||||
except KeyError:
|
||||
continue
|
||||
if isinstance(prev_scores, pd.DataFrame):
|
||||
prev_scores = prev_scores.iloc[:, 0]
|
||||
prev_scores = prev_scores.dropna().sort_values(ascending=False)
|
||||
topk_syms = list(prev_scores.index[:self._gate_topk])
|
||||
|
||||
if day not in ret_df.index:
|
||||
continue
|
||||
today_ret = ret_df.loc[day]
|
||||
topk_rets = today_ret.reindex(topk_syms).dropna()
|
||||
if len(topk_rets) == 0:
|
||||
continue
|
||||
|
||||
hit_rates[day] = (topk_rets > 0).sum() / len(topk_rets)
|
||||
|
||||
if not hit_rates:
|
||||
self._sq_gate_computed = True
|
||||
return
|
||||
|
||||
hr_series = pd.Series(hit_rates).sort_index()
|
||||
rolling_hr = hr_series.rolling(self._gate_lookback, min_periods=1).mean()
|
||||
gate = rolling_hr >= self._gate_threshold
|
||||
gate.iloc[:self._gate_lookback] = True
|
||||
|
||||
self._sq_gate = gate
|
||||
self._sq_gate_computed = True
|
||||
|
||||
def _gate_open(self, trade_start_time) -> bool:
|
||||
if self._sq_gate is None:
|
||||
return True
|
||||
@@ -58,6 +150,8 @@ class SignalQualityGateStrategy(TopkDropoutStrategy):
|
||||
return True # default open if no history yet
|
||||
|
||||
def generate_trade_decision(self, execute_result=None):
|
||||
if not self._sq_gate_computed:
|
||||
self._compute_gate_on_fly()
|
||||
trade_step = self.trade_calendar.get_trade_step()
|
||||
trade_start_time, _ = self.trade_calendar.get_step_time(trade_step)
|
||||
if not self._gate_open(trade_start_time):
|
||||
|
||||
Reference in New Issue
Block a user