株式ランキングやスコアリングモデルを作っていると、いちばん簡単なのは因子を追加することです。

モメンタム、RSI、相対強度、52週高値からの距離、ボラティリティ、出来高。バックテストで少しでも関係がありそうに見えれば、列を1つ増やして重みを付けることはすぐできます。難しいのはその後です。その因子は本当に将来リターンの順位付けに効いているのか、それとも特定の期間でたまたま良く見えただけなのか。

自分のクオンツ研究フローを整理していて、以前は「勝率」や「単一ホライズンのリターン」に引っ張られやすかったと気づきました。5日後には良く見える因子でも、1日後や3日後では同じとは限りません。同じシグナルを毎日評価すれば行数はすぐ増えますが、forward return は大きく重複します。行数が多いからといって、それだけ独立した情報が増えたとは限りません。

そこで、内部ノートに残っていた昔の IC や spread をそのまま使うのではなく、小さな公開再現実験をやり直しました。データは公開リポジトリの5つのETFの調整後終値です:sector_prices.csv。ファイルは blob SHA 0a442a9260c2ad64e6cd8c01af49328eb7619367 に固定し、SPY、XLK、XLF、XLE、XLVで単純な20日価格因子をいくつか作り、1D / 3D / 5D の forward return を見ました。

結果は、きれいな「最良因子」にはなりませんでした。むしろそこが重要でした。

20日モメンタムは、疎なアンカーサンプルでは1Dと5Dの High-Low median spread がプラスなのに、3Dではわずかにマイナスへ反転しました。低ボラ因子もホライズンごとに方向が安定しません。さらに「SPYに対する20日相対強度」を作ってみると、元の20日モメンタムと横断面ランキングが完全に同じになりました。

こういう不揃いな結果の方が、全部が緑色の因子レポートより実際の研究に近いと思います。

シグナル時点の因子値を固定する

因子検証でいま最も重視しているのは式そのものではなく、時間境界です。

ある日にシステムが複数銘柄をスコアリングし、momentum_20d を記録したとします。保存すべきなのは意思決定時点で実際に見えていた値です。その後で1D、3D、5Dの outcome が確定してから、その値に順位付け能力があったかを評価します。

つまり:

decision / snapshot -> factor value at t -> realized return at t + N

3日後になってから、今日のデータと今日のコードで「当時の因子」を再計算するのではありません。

見た目は似ていますが、意味はかなり違います。データベンダーが過去データを修正することもあります。株式分割や配当の調整方法が変わることもあります。ユニバースも因子定義も変わり得ます。横断面の標準化対象も当時とは違うかもしれません。あとから再計算すると、「当時システムが見ていた過去」ではなく「今日の定義で作り直した過去」を検証してしまう可能性があります。

Alphalens も基本構造は同じで、factor value と後続の forward returns を揃え、quantile ごとのリターンや Information Coefficient を計算します。ドキュメントには具体的な look-ahead の注意点もあります。将来リターンそのものから z-score を作って外れ値を除去すると、クリーニング段階ですでに未来情報を使ってしまいます。

将来自分の本番研究システムで自動的に重みを調整するなら、この境界は普通の研究 notebook より厳しくしたいです。factor snapshot が outcome tracking に入った時点で、後のモデルバージョンが上書きできないようにします。

今回の公開再現では本番DBは使っていません。固定した過去価格ファイルから signal-time factor と forward return を作っています。計算フローの再現には十分ですが、本番モデルが point-in-time データ問題を持っていないことの証明にはなりません。そこは別の主張です。

Bucket Spread と Rank IC は別のものを見る

因子を主にランキングに使うなら、最初に「勝率はいくつか」と聞くのはあまり好きではありません。

勝率は多くの情報を真偽値に圧縮します。+0.01%も+8%も勝ちです。1位と5位も同じ「勝ち」に入ることがあります。戦略結果の説明には使えますが、「因子値が高いほど将来結果も良いか」を直接見るには弱いです。

今回は2つの基本指標を使いました。

1つ目は Bucket Spread。各 signal date で横断面の因子値を順位で Low / Mid / High に分け、High と Low の後続リターン差を見る方法です。

Bucket Spread = median(return_high) - median(return_low)

「高いほど良い」因子に本当に順位情報があるなら、High が Low より強いことを期待します。ただし spread がプラスでも、このサンプルで両端が離れたというだけです。統計的有意性や取引コスト超過を自動的に意味しませんし、中間バケットまで単調とは限りません。

2つ目は Rank IC です。Spearman rank correlation で因子順位と将来リターン順位を直接比較します。

Rank IC = Spearman(factor_rank, forward_return_rank)

SciPy の spearmanr は単調な関係を測るノンパラメトリックな指標で、-1から1の範囲です。多くの因子では、因子値と将来リターンの線形関係より「順位に情報があるか」の方が重要なので、Spearman は自然です。

今回実行した中心部分はかなり小さいです。

mom20 = px / px.shift(20) - 1
fwd = px.shift(-horizon) / px - 1

x = mom20.loc[dt].dropna()
y = fwd.loc[dt].dropna()
common = x.index.intersection(y.index)

bucket = pd.qcut(
    x.loc[common].rank(method="first"),
    3,
    labels=["low", "mid", "high"],
)

rho, _ = spearmanr(
    x.loc[common].values,
    y.loc[common].values,
)

spread = (
    y.loc[common][bucket == "high"].median()
    - y.loc[common][bucket == "low"].median()
)

実験スクリプトは Content Lab の証拠として固定され、実行コマンドは exit code 0 で完了しています。新しいクオンツフレームワークを作ったわけではなく、因子検証の中核だけをできるだけ小さく切り出しました。

ホライズンを変えると結論も動き始める

forward return の重複を少し減らすため、主表示では5営業日ごとに1つ anchor date を取っています。14日分、各日5ETFなので、factor × horizon ごとに70件の横断面観測があります。

以下の spread は High bucket median - Low bucket median、単位はpercentage points。Rank IC は各 anchor date の横断面 Spearman IC の平均です。

FactorHorizonNMedian SpreadMean Rank IC
20D Momentum1D70+0.583 pp+0.179
20D Momentum3D70-0.071 pp+0.014
20D Momentum5D70+0.667 pp+0.214
Distance to 20D High1D70+0.249 pp+0.089
Distance to 20D High3D70+0.259 pp+0.151
Distance to 20D High5D70+0.529 pp+0.252
Low Volatility 20D1D70-0.029 pp+0.029
Low Volatility 20D3D70+0.296 pp+0.014
Low Volatility 20D5D70-0.224 pp-0.207
Relative Strength 20D vs SPY1D70+0.583 pp+0.179
Relative Strength 20D vs SPY3D70-0.071 pp+0.014
Relative Strength 20D vs SPY5D70+0.667 pp+0.214

この表で一番やりたくないのは、最大値を丸で囲んで「有効」と宣言することです。

20D Momentum を見ると、5Dだけなら spread +0.667 pp、mean Rank IC +0.214 なので重みを増やしたくなります。ところが3D spread は -0.071 pp、Rank IC もほぼ0です。「ホライズンが長いほど良い」というきれいな構造ではありません。

Low Volatility はもっと不安定です。3D median spread はプラスですが、5Dでは -0.224 pp に反転し、mean Rank IC も -0.207 です。3Dだけ切り取れば良さそうな正方向因子に見えますが、5Dでは話が変わります。

これが multi-horizon outcome tracking の実務的な価値です。1D、3D、5Dを加重してきれいな総合スコアにすることより先に、結論が horizon にどれだけ敏感かを露出させます。

毎日サンプリングする結果も残しました。そこでは66 signal dates、330 factor/outcome rows になるので、見た目のサンプル数はかなり増えます。

ただし3D、5D forward returns は大きく重複します。

実際、20D Momentum の3D median spread は日次重複サンプルでは約 +0.578 pp なのに、5営業日ごとのアンカーサンプルでは -0.071 pp でした。

この違いから「どちらが真実か」を決めるつもりはありません。14アンカーはまだ小さく、5日間隔でも依存が完全に消えるわけではありません。重要なのは別の点です。妥当なサンプリング方法を変えただけで結論が反転するなら、研究レポートは大きな総行数だけを見せるべきではありません。

因子を増やす前に、同じ順位情報ではないかを見る

今回、最初はコードのバグかと思った結果もありました。

同時に作ったのが:

momentum_20d
relative_strength_20d_vs_spy

後者は:

asset_20d_return - SPY_20d_return

です。

解析すると、bucket、spread、Rank IC がすべて完全に一致しました。

コードの問題ではありませんでした。

同じ日なら SPY の20日リターンは全資産に対して同じ定数です。

RS_i = Momentum_i - C_t

横断面のすべての資産から同じ C_t を引いても順位は変わりません。Bucket と Rank IC が横断面 rank だけに依存するなら、この構成では2つは同じ順位情報を持ちます。

これは「IC = 0.21」のような数字より自分には価値がありました。

多因子モデルでは簡単に「見かけの複雑さ」が増えます。名前も式も違って見えるのに、実際は同じ方向情報を重複しているケースです。強く冗長な2因子に別々の重みを付けても、証拠が2つになるわけではありません。同じ証拠が2票持つだけです。

もちろん「relative strength は momentum と同じ」という一般論ではありません。業種基準、動的 benchmark、残差リターン、rolling beta 調整、異なる lookback などを使えば順位は変わり得ます。今回同じになった理由は非常に限定的で、同じ日にすべての資産から同じ SPY 20D return を引いたからです。

今後は重みを調整する前に factor rank、相関、bucket assignment を比較し、新しい因子が本当に新しい情報を持つのか、名前だけ違うのかを先に確認したいです。

1回の正の IC で本番重みは変えない

研究システムが将来モデルを自動調整するなら、「因子を見つけた」と「本番に影響させる」の間に距離を置きたいです。

候補因子はまず observe に入れます。シグナル時点の値は保存するが意思決定には使わない。outcome が確定したら、複数 horizon の bucket spread、Rank IC、coverage、サンプル分布を見る。

まだ面白ければ shadow へ進めます。Shadow は「有効にしたら重みがどう変わるか」という候補結果を作れても、active model は変更しません。

少なくとも次を見ます。

  • サンプル数と非欠損 coverage が十分か。
  • High / Mid / Low に基本的な順位構造があるか。両端だけ偶然離れていないか。
  • 1D / 3D / 5D の方向を説明できるか。互いに矛盾していないか。
  • 別期間や別 regime でも方向が残るか。
  • 既存因子と強く重複していないか。
  • サンプリングや重複除去ルールを変えると結論が反転しないか。
  • 研究中に何個の因子、窓、パラメータを試したか。

最後の点は特に重要です。因子研究は大量の仮説検定になりがちです。50指標、6つのlookback、4つのhorizonを試して最良だけを選べば、基礎シグナルが弱くても「きれいな結果」が見つかる確率は上がります。Harvey、Liu、Zhu は資産価格研究の多重検定問題を … and the Cross-Section of Expected Returns で直接扱っています。

だから Bucket Spread と Rank IC を因子の入場許可証とは考えていません。基礎診断に近いです。順位情報の有無、方向の安定性、horizon 反転や因子冗長を見つけるのには便利ですが、「実資金で取引できる因子」との間には、安定性、取引コスト、サンプル外検証、regime、ポートフォリオ曝露、リスク管理が残っています。

今回の公開実験も、5ETFと14個の疎なアンカー日しか使っていません。本番 alpha の証明ではありません。証明できたのは、この検証チェーンが小さな形で再現できることと、horizon、サンプリング、rank redundancy を表に出すだけで、きれいに見えていた因子結論がかなり不確かになることです。

その不確かさは残しておきたいと思っています。

モデルに入れる価値がある因子なら、1つの研究レポートで良い数字が出たからではなく、outcome tracking を続けたあとでも、いくつもの厳しいチェックにまだ耐えている方がいいです。