
A 股中隔夜端可能強烈反轉日內端可能偏動量且收益/波動很大部分來自隔夜跳空。T1、漲跌停、集合競價、散戶占比高等制度與行為特征使得總收益有必要拆成隔夜和日內。這時嘗試梳理如何拆分以及如何構造隔夜/日內累計動量因子并通過python代碼進行示例。1 隔夜/日內累計動量因子首先分析為什么拆分隔夜和日內拆分的背景然后進一步給出相關因子定義及驗證方式。1.1 為什么拆隔夜和日內這里首先把股票總收益精確拆成其中- 隔夜段昨收到今開close → open- 日內段今開到今收open → close但這兩個部分的形成機制完全不同- 隔夜段(on)非交易時段信息、隔夜風險、散戶情緒、集合競價、外盤影響、公告效應。- 日內段(id)連續(xù)競價、機構交易、流動性提供、訂單流、日內動量/反轉。Lou-Polk-Skouras 的核心發(fā)現是隔夜和日內收益對未來的預測方向可能相反。在 A 股版本中常見結論是- 隔夜端反轉尤其是短期。- 日內端動量。- 總收益中隔夜波動/跳空占比很高。A 股中隔夜端可能強烈反轉日內端可能偏動量且收益/波動很大部分來自隔夜跳空。A 股 T1、漲跌停、集合競價、散戶占比高等制度與行為特征使這種分解尤其有意義。所以 mom_on_12m可能ICIR為負mom_id_12m可能 ICIR 為正。也就是說隔夜和日內收益的預測方向可能完全不同拆開后可以針對不同因子采用不同方向比如用凍結樣本內 ICIR 決定符號這是穩(wěn)健做法。1.2 A 股背景T1當天買入不能當天賣出隔夜風險無法日內平倉。隔夜風險溢價和隔夜情緒更容易累積。- 漲跌停開盤漲停/跌停會導致無法成交隔夜跳空可能被放大。- 集合競價開盤價由 9:15–9:25 集合競價產生反映隔夜信息與情緒。- 散戶占比高隔夜持倉受情緒、彩票偏好、過度反應影響容易導致隔夜反轉。- 機構日內交易機構拆單、執(zhí)行、流動性提供可能使日內收益呈現動量。- 做空受限所以回測用純多頭 zpos再通過市場對沖近似中性。1.3 因子定義這里進一步給出隔夜、日內相關因子的定義示例如下ret_12m總 12 月動量目的是做對照mom_on_12m過去 12 月隔夜累計可能反轉ICIR負mom_id_12m過去 12 月日內累計可能動量ICIR 正mom_on_1m過去 1 月隔夜累計短期隔夜反轉skewness偏度因子偏度因子表現較穩(wěn)健這里作為基準idio_vol特質波動率特質波動率相對穩(wěn)健這里作為對照ret_12m mom_on_12m mom_id_12m在收益層面成立但因子層面不一定完全可加因為復權、缺失值、窗口和 shift 處理可能略有差異。如果沒有做正交化隔夜、日內、總動量之間可能高度相關。1.4 驗證與貨幣化這里進一步給出如何驗證和貨幣化的對應參數和過程。ICIR信息系數均值 / 標準差衡量預測穩(wěn)定性。Rank ICIR秩相關抗極端值。spread_sharpe分組多空組合夏普衡量單調性和收益。recent_icir2023 后表現檢查衰減。recent_sign_flip方向是否翻轉。zpos純多頭倉位適合 A 股。rolling_hedge_beta60 日滾動 beta因果對沖避免前視。hedged_causal_sharpe對沖后夏普更接近可交易市場中性策略。最終adopt要求為統計上有預測力、分組上有收益、貨幣化后能打、近期不翻車。2 隔夜/日內因子實現示例以下代碼是一個因子研究流水線核心目標是把股票總收益精確拆成隔夜收益和日內收益。然后構造隔夜/日內累計動量因子檢驗它們是否具有預測能力并在嚴格凍結樣本內符號后做 2023–2025 的樣本外貨幣化回測。最后與基準因子skewness做成本端 貨幣化端對比決定是否采納。# ******** AI生成僅供教學和測試 ******** 隔夜(close→open) vs 日內(open→close) 收益分解. A 股文獻: 收益幾乎全部來自隔夜段, 且隔夜端強烈反轉、日內端動量 (Lou-Polk-Skouras 2019 的 A 股版本)。把總收益做精確對數分解: logret_total log(close/prev_close) log(open/prev_close) log(close/open) logret_on logret_id (恒等, 無交叉項) 因子 (與 ret_12m 同口徑, 12m250d 累計, 1m21d): * ret_12m : 總 12 月動量 (registry, pct_change(250).shift(1)) 對照 * mom_on_12m: 隔夜 12 月累計 * mom_id_12m: 日內 12 月累計 * mom_on_1m : 隔夜 1 月累計 (A 股短周期反轉主要在隔夜) 符號由凍結樣本內 ICIR 決定??趶? horizon20/rebal20, 凍結 ≤2022-12-31 → OOS 2023-01-01~2025-12-31, 純多頭 zpos 60d 因果對沖。FACTOR_REGISTRY 不修改。 import argparse import json import os import sys import h5py import numpy as np import pandas as pd sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import ic_weighted_test as base # noqa: E402 import beta_neutral_test as btn # noqa: E402 from idio_vol_test import idio_volatility_factor # noqa: E402 from mfm.combine import combine_factors_icir_weight from mfm.data import load_data, load_members, universe_mask from mfm.factors import compute_factors, cross_sectional_normalize from mfm.pipelines.csi300_screen import CSI300ScreenConfig from mfm.validation import validate_factors NAMES (skewness, idio_vol, ret_12m, mom_on_12m, mom_id_12m, mom_on_1m) STOCK (ret_12m, mom_on_12m, mom_id_12m, mom_on_1m) def load_adjusted_open(h5_path: str, start: str, end: str) - pd.DataFrame: date x stock 后復權開盤價 $open / $factor (與 load_data 的 close 同口徑). with h5py.File(h5_path, r) as f: grp f[data] dates pd.to_datetime(grp[axis1_level0][:]) stocks [s.decode() if isinstance(s, (bytes, np.bytes_)) else str(s) for s in grp[axis1_level1][:]] cols [c.decode() if isinstance(c, (bytes, np.bytes_)) else str(c) for c in grp[axis0][:]] vals grp[block0_values][:] label0 grp[axis1_label0][:] label1 grp[axis1_label1][:] mi pd.MultiIndex.from_arrays([dates[label0], [stocks[i] for i in label1]], names[date, stock]) df pd.DataFrame(vals, indexmi, columnscols) open_raw df[$open].unstack(levelstock).sort_index() factor df[$factor].unstack(levelstock).reindex_like(open_raw) open_adj open_raw / factor return open_adj.loc[(open_adj.index start) (open_adj.index end)] def main() - None: ap argparse.ArgumentParser(description__doc__.splitlines()[0]) ap.add_argument(--tag, requiredTrue) ap.add_argument(--instruments, defaultdata/cn_data/instruments/csi300.txt) ap.add_argument(--start, requiredTrue) ap.add_argument(--end, requiredTrue) ap.add_argument(--freeze, default2022-12-31) ap.add_argument(--oos-start, default2023-01-01) ap.add_argument(--outdir, default./tmp/on_id) args ap.parse_args() os.makedirs(args.outdir, exist_okTrue) cfg CSI300ScreenConfig( startargs.start, endargs.end, instruments_pathargs.instruments, out_dirargs.outdir, ) print(f[1/5] loading ({args.tag})...) prices_full, volumes_full load_data( sourceqlib, h5_pathcfg.h5_path, startcfg.start, endcfg.end, max_stockscfg.max_stocks ) segs load_members(cfg.instruments_path) mask_full universe_mask(segs, prices_full.index, prices_full.columns) member_cols mask_full.columns[mask_full.any(axis0)] prices prices_full[member_cols] volumes volumes_full.reindex(columnsmember_cols) mask mask_full[member_cols] returns prices.pct_change(fill_methodNone) dates prices.index print(f Universe avg members: {mask_full.sum(axis1).mean():.0f}; fever-member cols: {len(member_cols)}) print( loading adjusted $open...) open_adj load_adjusted_open(cfg.h5_path, cfg.start, cfg.end).reindex( indexprices.index, columnsmember_cols) print([2/5] session decomposition...) logret_on np.log(open_adj / prices.shift(1)) logret_id np.log(prices / open_adj) m_on logret_on.abs().stack().mean() m_id logret_id.abs().stack().mean() on_frac float(m_on / (m_on m_id)) print(f overnight share of daily |return| (magnitude): {on_frac:.1%}) mom compute_factors(prices, volumes, factor_names[skewness, ret_12m]) facs { skewness: mom[skewness].where(mask), idio_vol: idio_volatility_factor(prices).where(mask), ret_12m: mom[ret_12m].where(mask), mom_on_12m: logret_on.rolling(250, min_periods125).sum().shift(1).where(mask), mom_id_12m: logret_id.rolling(250, min_periods125).sum().shift(1).where(mask), mom_on_1m: logret_on.rolling(21, min_periods11).sum().shift(1).where(mask), } print([3/5] four-lens (full window 2023 flip)...) val validate_factors(facs, prices, horizoncfg.horizon, n_groups5) hold dates[dates 2023-01-01] val_h validate_factors( {n: facs[n].loc[hold] for n in NAMES}, prices.loc[hold], horizoncfg.horizon, n_groups5, ) four {} for n in NAMES: s val[ic_summary][n] r val[rank_ic_summary][n] sp val[spread_summary][n] h val_h[ic_summary][n] four[n] { icir: float(s[icir]), t_stat: float(s[t_stat]), rank_icir: float(r[icir]), spread_annualized: float(sp[annualized]), spread_sharpe: float(sp[sharpe]), recent_icir: float(h[icir]), recent_sign_flip: bool(np.sign(s[icir]) ! np.sign(h[icir])), } print(f {n:12s} ICIR{four[n][icir]:.4f} t{four[n][t_stat]:.2f} frankICIR{four[n][rank_icir]:.4f} | spread{four[n][spread_annualized]:.2%} fsharpe{four[n][spread_sharpe]:.2f} | 2023 {four[n][recent_icir]:.4f} fflip{four[n][recent_sign_flip]}) print(f[4/5] frozen OOS monetization (sign {args.freeze})...) cut dates[dates args.freeze] val_is validate_factors( {n: facs[n].loc[cut] for n in NAMES}, prices.loc[cut], horizoncfg.horizon, n_groups5, ) signs {n: 1.0 if val_is[ic_summary][n][icir] 0 else -1.0 for n in NAMES} oos_idx dates[dates args.oos_start] bm returns.where(mask).mean(axis1).shift(-1).loc[oos_idx] ret_next returns.shift(-1).loc[oos_idx] rebal oos_idx[:: cfg.rebal] zero pd.Series(0.0, indexbm.index) money {} for n in NAMES: norm cross_sectional_normalize(facs[n].loc[oos_idx]) comp combine_factors_icir_weight({n: norm}, {n: signs[n]}).where(mask.loc[oos_idx]) book btn.build_scheme_returns(comp, ret_next, rebal, cfg, zpos) bep btn.realized_beta(book, bm) broll btn.rolling_hedge_beta(book, bm) hc base.calc_metrics(btn.hedge(book, bm, broll), zero) money[n] { sign: signs[n], book_beta_expost: bep, hedged_causal_ann: float(hc[annual_return]), hedged_causal_sharpe: float(hc[sharpe_ratio]), hedged_causal_maxdd: float(hc[max_drawdown]), } print(f [{n}] sign {signs[n]:.0f} book_beta{bep:.2f} fhedged_caus ann{hc[annual_return]:.2%} sharpe{hc[sharpe_ratio]:.2f} fmaxDD{hc[max_drawdown]:.1%}) print([5/5] head-to-head vs skewness (P1 rule)...) sk four[skewness] skm money[skewness] verdict {} for n in STOCK: f four[n] fm money[n] cost_win abs(f[icir]) abs(sk[icir]) and abs(f[rank_icir]) abs(sk[rank_icir]) spread_adj f[spread_sharpe] * fm[sign] money_win spread_adj sk[spread_sharpe] and fm[hedged_causal_sharpe] skm[hedged_causal_sharpe] adopt cost_win and money_win and not f[recent_sign_flip] verdict[n] {cost_win: cost_win, money_win: money_win, flip: f[recent_sign_flip], adopt: bool(adopt)} print(f {n:12s} cost{cost_win} money{money_win} flip{f[recent_sign_flip]} f- adopt{YES if adopt else NO}) out { tag: args.tag, freeze: args.freeze, oos_window: [str(oos_idx[0].date()), str(oos_idx[-1].date())], overnight_share: on_frac, four_lens: four, money: money, verdict: verdict, } path os.path.join(args.outdir, fon_id_{args.tag}.json) with open(path, w) as fh: json.dump(out, fh, indent1, ensure_asciiFalse) print(f\nSaved - {path}) if __name__ __main__: main()這里是進一步的代碼結構梳理。2.1 導入與全局定義首先導入如下因子skewness、idio_vol是對照/基準因子。ret_12m是總 12 月動量。mom_on_12m、mom_id_12m是隔夜/日內 12 月累計。mom_on_1m是隔夜 1 月累計用于捕捉 A 股短周期隔夜反轉。STOCK是四個真正要與skewness做頭對頭比較的股票收益分解因子。NAMES (skewness, idio_vol, ret_12m, mom_on_12m, mom_id_12m, mom_on_1m)STOCK (ret_12m, mom_on_12m, mom_id_12m, mom_on_1m)2.2 數據讀取數據讀取函數具體為load_adjusted_open作用是從 qlib 的 h5 數據中讀取后復權開盤價。open_adj open_raw / factor它假設項目里 load_data得到的close與$open / $factor同口徑。這是整個隔夜/日內分解能否成立的關鍵如果開盤價和收盤價復權口徑不一致log(open/prev_close)就會混入除權除息噪聲。2.3 數據加載與股票池使用歷史成分股segs構造動態(tài)股票池mask。member_cols是曾經進入過成分股的股票避免只保留當前成分股造成生存偏差。mask標記某日某股是否在成分內。后續(xù)因子大多.where(mask)表示只在當時成分股內有效。prices_full, volumes_full load_data(...)segs load_members(...)mask_full universe_mask(segs, prices_full.index, prices_full.columns)member_cols mask_full.columns[mask_full.any(axis0)]2.4 隔夜/日內收益分解這里計算logret_on昨收 → 今開logret_id今開 → 今收然后計算隔夜絕對收益均值占隔夜日內絕對收益均值的比例。logret_on np.log(open_adj / prices.shift(1))logret_id np.log(prices / open_adj)m_on logret_on.abs().stack().mean()m_id logret_id.abs().stack().mean()on_frac float(m_on / (m_on m_id))注意這里衡量的是絕對波動/跳空幅度占比不是累計收益貢獻占比。所以overnight_share不能直接證明收益幾乎全部來自隔夜只能說明隔夜段的絕對變動更大。2.5 因子構造因子構造關鍵點如下1rolling(250).sum()過去 250 個交易日累計。2shift(1)避免前視t 日使用截至 t-1 的累計收益。3mom_on_12m過去 12 月隔夜累計收益。4mom_id_12m過去 12 月日內累計收益。5mom_on_1m過去 1 月隔夜累計收益。6ret_12m作為總動量對照。由于對數收益可加隔夜累計和日內累計分別刻畫了兩個交易時段的長期價格壓力。pythonmom compute_factors(prices, volumes, factor_names[skewness, ret_12m])facs {skewness: mom[skewness].where(mask),idio_vol: idio_volatility_factor(prices).where(mask),ret_12m: mom[ret_12m].where(mask),mom_on_12m: logret_on.rolling(250, min_periods125).sum().shift(1).where(mask),mom_id_12m: logret_id.rolling(250, min_periods125).sum().shift(1).where(mask),mom_on_1m: logret_on.rolling(21, min_periods11).sum().shift(1).where(mask),}2.6 四透鏡驗證對每個因子收集1ICIR2t_stat3rank_icir4分組多空 spread_annualized5spread_sharpe62023 年后recent_icir7是否近期符號翻轉 recent_sign_flip這是四透鏡統計顯著性、秩穩(wěn)健性、分組收益、近期穩(wěn)定性。val validate_factors(facs, prices, horizoncfg.horizon, n_groups5)hold dates[dates 2023-01-01]val_h validate_factors(...)2.7 符號好貨幣化具體為凍結樣本內符號樣本外貨幣化cut dates[dates args.freeze]val_is validate_factors(...)signs {n: 1.0 if val_is[ic_summary][n][icir] 0 else -1.0 for n in NAMES}用 ≤2022-12-31 的樣本內 ICIR 決定因子方向- ICIR 為正 → 因子值越大越好sign 1- ICIR 為負 → 反向使用sign -1然后 OOS 為 2023-01-01 到 2025-12-31。bm returns.where(mask).mean(axis1).shift(-1).loc[oos_idx]ret_next returns.shift(-1).loc[oos_idx]rebal oos_idx[:: cfg.rebal]bm等權成分股下一日收益作為市場基準。ret_next下一日個股收益。rebal每 20 日再平衡與horizon20對應。對每個因子做截面標準化等處理。norm cross_sectional_normalize(facs[n].loc[oos_idx])comp combine_factors_icir_weight({n: norm}, {n: signs[n]}).where(mask.loc[oos_idx])book btn.build_scheme_returns(comp, ret_next, rebal, cfg, zpos)bep btn.realized_beta(book, bm)broll btn.rolling_hedge_beta(book, bm)hc base.calc_metrics(btn.hedge(book, bm, broll), zero)這里做了1橫截面標準化cross_sectional_normalize。2根據凍結符號生成單因子組合combine_factors_icir_weight3用 zpos方案構建純多頭組合符合 A 股做空受限現實。build_scheme_returns4計算事后beta beprealized_beta。5用滾動 60 日 beta broll做因果對沖即只用歷史信息估計 betarolling_hedge_beta。6計算對沖后的年化收益、夏普、最大回撤具體為calc_metrics。2.8 與skewness頭對頭比較與skewness頭對頭比較代碼如下cost_win abs(f[icir]) abs(sk[icir]) and abs(f[rank_icir]) abs(sk[rank_icir])spread_adj f[spread_sharpe] * fm[sign]money_win spread_adj sk[spread_sharpe] and fm[hedged_causal_sharpe] skm[hedged_causal_sharpe]adopt cost_win and money_win and not f[recent_sign_flip]決策規(guī)則為1成本端勝|ICIR| 和 |Rank ICIR| 都超過skewness。2貨幣化端勝調整方向后的 spread Sharpe 超過skewness且對沖后因果夏普也超過。3無近期符號翻轉2023 后沒有出現方向不穩(wěn)定。三者同時滿足才adopt YES最后保存 JSON 結果。3 潛在問題3.1 隔夜占比指標隔夜占比指標不等于收益貢獻on_frac用的是logret_on.abs().mean() / (logret_on.abs().mean() logret_id.abs().mean())這是絕對變動幅度占比不是累計收益占比。若要驗證收益幾乎全部來自隔夜應計算或對指數/組合收益做累計分解。3.2 復權口徑需嚴格驗證必須檢查open_adj open_raw / factor是否與load_data的close完全一致。如果$factor的定義是raw * factor adjusted那這里可能反了。一旦口徑不一致隔夜收益會混入除權除息因子含義被污染。3.3 缺失值等處理不足缺失值、停牌、漲跌停處理不足代碼直接np.log沒有處理1價格為 0 或負2停牌導致 NaN3漲跌停無法成交4開盤集合競價無成交這會讓logret_on、logret_id出現inf或異常值進而污染rolling().sum()。3.4. 交易成本與可交易性未建模回測沒有扣- 傭金、印花稅、沖擊成本- 漲跌停無法買入- T1 賣出限制- 股指期貨基差、保證金、展期成本- 融券成本所以hedged_causal_sharpe是理想化結果。3.5 未做行業(yè)/風格中性只對沖了市場 beta沒有控制- 行業(yè)暴露- 規(guī)模- 流動性- 波動率- 估值隔夜/日內因子可能與這些風格高度相關導致收益來自風格暴露而非獨立 alpha。3.6 多重檢驗與數據窺探同時比較 4 個因子與skewness沒有做多重檢驗校正。recent_sign_flip使用全窗口 vs 2023全窗口包含 OOS若用于決策存在前視嫌疑。3.7 隔夜/日內/總動量未正交化mom_on_12m mom_id_12m ≈ ret_12m三者高度相關。頭對頭比較時應做正交化或至少報告相關性否則難以判斷增量信息。3.8 樣本外窗口較短OOS 為 2023–2025只有約 3 年且包含特定市場環(huán)境。結論可能對市場狀態(tài)敏感。reference---隔夜動量因子的增量檢驗探索https://blog.csdn.net/liliang199/article/details/166599863