From 002117c33e6cfe93fe4333ec8a6bcff1bc730af5 Mon Sep 17 00:00:00 2001 From: zhaoli Date: Thu, 20 Aug 2026 23:52:20 +0000 Subject: [PATCH] start experiment 59 (exp/59-signal-quality-gate-walk-forward-test-wi) --- book/workflows/sq_gate/sq_gate_2021.yaml | 9 +- book/workflows/sq_gate/sq_gate_2023.yaml | 9 +- book/workflows/sq_gate/sq_gate_2024.yaml | 9 +- book/workflows/sq_gate/sq_gate_2025.yaml | 9 +- book/workflows/sq_gate/sq_gate_2026.yaml | 9 +- code/MANIFEST.txt | 6 +- .../signal_quality_gate.cpython-312.pyc | Bin 10218 -> 15030 bytes .../contrib/strategy/signal_quality_gate.py | 102 +++++++++++++++++- 8 files changed, 136 insertions(+), 17 deletions(-) diff --git a/book/workflows/sq_gate/sq_gate_2021.yaml b/book/workflows/sq_gate/sq_gate_2021.yaml index b7c8c40..03507d1 100644 --- a/book/workflows/sq_gate/sq_gate_2021.yaml +++ b/book/workflows/sq_gate/sq_gate_2021.yaml @@ -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: "" - 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 diff --git a/book/workflows/sq_gate/sq_gate_2023.yaml b/book/workflows/sq_gate/sq_gate_2023.yaml index ea132b8..6104b31 100644 --- a/book/workflows/sq_gate/sq_gate_2023.yaml +++ b/book/workflows/sq_gate/sq_gate_2023.yaml @@ -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: "" - 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 diff --git a/book/workflows/sq_gate/sq_gate_2024.yaml b/book/workflows/sq_gate/sq_gate_2024.yaml index fcb6f0c..85c82e3 100644 --- a/book/workflows/sq_gate/sq_gate_2024.yaml +++ b/book/workflows/sq_gate/sq_gate_2024.yaml @@ -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: "" - 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 diff --git a/book/workflows/sq_gate/sq_gate_2025.yaml b/book/workflows/sq_gate/sq_gate_2025.yaml index 80eccec..fbcc0e8 100644 --- a/book/workflows/sq_gate/sq_gate_2025.yaml +++ b/book/workflows/sq_gate/sq_gate_2025.yaml @@ -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: "" - 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 diff --git a/book/workflows/sq_gate/sq_gate_2026.yaml b/book/workflows/sq_gate/sq_gate_2026.yaml index 518eaa2..12e09bb 100644 --- a/book/workflows/sq_gate/sq_gate_2026.yaml +++ b/book/workflows/sq_gate/sq_gate_2026.yaml @@ -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: "" - 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 diff --git a/code/MANIFEST.txt b/code/MANIFEST.txt index e60be5f..21b3699 100644 --- a/code/MANIFEST.txt +++ b/code/MANIFEST.txt @@ -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 diff --git a/code/tac-qlib/tac_qlib/contrib/strategy/__pycache__/signal_quality_gate.cpython-312.pyc b/code/tac-qlib/tac_qlib/contrib/strategy/__pycache__/signal_quality_gate.cpython-312.pyc index ebcbd48b5de9bf6da84af4d6916c97d03bb6acfa..a6a1c21ab71b62080830df45c4784b73c1531036 100644 GIT binary patch delta 5732 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 delta 1793 zcmY*aZ)_Ar6rZ`8OVidOv=j+}7^+E2#8X_iw{vyN-XCW7 zq{SnJ*a#$GB%@-Ci7^_Jh8Q)C#)MDm7o&avKaq(a_*y?02tg;@y*~={0=Y&Q$x@qSLpIHZ?GSw7 z4?`D8p4!S{JgeAITU}JLsvQetkwq;Vxk%<{HabZRjYq&p{I#QuJ1=?N+So{h?oDUAGhI zn~7bwlf4%YUO6i6Cw6VS*)VuFLQFeLr^|%9p4!KZlgM$j(*aRq`ezJ zCPQUgYEzIQo^KV(QVnuQhFy$*x>Rzd;<%-pq3~ugn%YI%gp(Si1LC99>!DwWNT!pa z*|7L>Yr7~V+d{Ld_$<9coJu!|JLx*Ai9gb-OT(2_OKE=+s|q z-v+!9k`d1Q3NP{wRNaBHn0QamEUd?Q;H?`tS+|OQj=8RB8nosLvt@tH3wjM|EeVXb z1<0dCaxbK*vAZp#ci_jVKZ&Mx-fxMj%?}$@b?hDi!Z8R5@E)67K>$`%AT!X3LCBr4 zGx6Ph{ewMy{XKmH$nPHN>3g!Le+05cbFGuiwUVvh06An26ZLCd!eajU=r>oR^69s%bFzBOD24KdzgH%pZj%Kp5J zOoAKc1)z%?Ey-q{MyqvTBY1~`ZQ}2ielgIx<$zQQ%qkk~QY&+2n-~`#0e!v)36?O& z+vWR^;G4jDo#U*K7BNwZxGeUVbODJxa&LjltuYGbkMf zB56*lIk)67qlq6uDVmiLHs^T07o43xBSwcd)8isL)IEyl;1hug)T%R6l$ytUcmoiJ z94@{D#MtoI0UnE3{4jKwc8PVvn>GZgJB1sNd_k56hnMI=9tG!-1ONnFfI`3_6f*1- z{Td!uQ4c|pY91aLzlKeq82}V;y|8{pE8Cgcl^E;!0PcvJX1)T2!Az6m|46FEN}()H zjMUT3!X0^KXAoK)tOPk6U@2pJ3njbavQhp4EKum)x8lJ_cl|axc2ggCKw!N4G8Mgh J-cT^}{{hYRwK4zz diff --git a/code/tac-qlib/tac_qlib/contrib/strategy/signal_quality_gate.py b/code/tac-qlib/tac_qlib/contrib/strategy/signal_quality_gate.py index 0101bc8..16ab80b 100644 --- a/code/tac-qlib/tac_qlib/contrib/strategy/signal_quality_gate.py +++ b/code/tac-qlib/tac_qlib/contrib/strategy/signal_quality_gate.py @@ -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 (````) 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):