# 多因子モデルを安全にリウェイトするには？Regime・Shadow・Weight Driftを比較した

> Canonical: https://vxi.me/ja/posts/adaptive-factor-weighting-regime-shadow-drift/
> Locale: ja-jp
> Published: 2026-10-08
> Updated: 2026-10-08
> Markdown: https://vxi.me/ja/posts/adaptive-factor-weighting-regime-shadow-drift.md

因子の IC がプラスになったあと、いちばん危ない次の一手は、そのウェイトをすぐ上げることかもしれない。

前回は、signal-time snapshot を固定し、Bucket Spread、Rank IC、複数 horizon の outcome を見ることで「その因子にランキング情報があるのか」を検証した。ところが自動リウェイトを始めると、問題は別の形になる。

**直近 20 period で強く見えた因子が、なぜ今すぐ active model のウェイトを書き換えてよいのか。**

この二つは簡単に混同される。

候補因子に本当に情報があっても、直近ウィンドウの推定がたまたま高すぎることはある。逆に regime が本当に切り替わったのに、保護を強くしすぎてモデルが追従できないこともある。IC から新しい weight vector を作れば自動調整が完成するわけではない。難しいのは、その新しい状態をいつ本番に反映するかだ。

今回は、以前の本番ウェイトや Notion に残していた閾値を証拠には使わなかった。代わりに、ground truth が分かっている synthetic regime-switching experiment を作った。因子関係が本当に変わる時点を自分で決めた上で、三つの activation policy を比較した。

- 新しい candidate weights を毎回すぐ active にする。
- 毎回 active にするが、各因子の最大 weight change に cap をかける。
- candidate をまず shadow で走らせ、active に対して連続で勝った場合だけ promotion し、その際にも delta cap を適用する。

12 個の固定 random seed を使い、各 seed は 240 の連続 period、各 period 200 synthetic assets、4 factors で構成した。regime は period 80 と 160 で明示的に切り替える。candidate は 5 period ごとに作り、推定には直前 20 period しか使わない。評価は必ずその後の 5 period だけで行った。

この時間境界は意図的に厳しくした。scikit-learn の [TimeSeriesSplit](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html) のドキュメントも、時系列データで通常の cross-validation を使うと、将来を train に使って過去を評価するような不正な分割が起こり得ると説明している。

candidate weight が未来を見ているなら、shadow、regime、drift budget を後からいくら足しても意味はない。

## 「速く調整する」と「安定して調整する」はどれくらい違うのか

candidate weight の推定自体はあえて単純にした。trailing window 内で各 factor の cross-sectional Rank IC を計算し、負の値を 0 に切り、残った正の evidence を正規化して candidate weights にする。

変えるのは activation policy だけだ。

記録した指標は次のとおり。

- future block の combined-score Rank IC
- 累積 L1 weight turnover
- 一回の最大 weight jump
- promotion 回数
- promotion の直後の evaluation block で、新 profile が以前の active profile に負けた割合
- 本当の regime switch 後、active profile の最大ウェイトが新 regime の最強 factor に移るまでの時間

最後の指標を adaptation lag と呼んだ。金融業界の標準指標ではなく、この実験で「モデルが regime change を認めるまでどれだけ遅れるか」を測るための定義だ。

12 seeds の平均は次のようになった。

| Policy | Cap | Shadow confirmations | Mean OOS Rank IC | L1 turnover | Max weight jump | False promotion rate | Adaptation lag |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| Immediate | — | — | **0.2199** | **7.305** | 0.333 | **39.0%** | **11.67** |
| Delta capped | 5% | — | 0.2092 | 5.591 | 0.050 | 27.8% | 22.08 |
| Shadow first | 5% | 1 | 0.2001 | 4.197 | 0.050 | 20.6% | 29.79 |
| Shadow first | 5% | 2 | 0.1882 | 2.409 | 0.050 | 15.0% | 52.29 |
| Shadow first | 5% | 3 | 0.1850 | **1.659** | 0.050 | **10.2%** | **75.83** |
| Delta capped | 10% | — | 0.2172 | 6.884 | 0.100 | 36.2% | 14.58 |
| Shadow first | 10% | 1 | 0.2077 | 5.015 | 0.100 | 27.6% | 22.71 |
| Shadow first | 10% | 2 | 0.1937 | 3.472 | 0.100 | 13.7% | 37.08 |
| Shadow first | 10% | 3 | 0.1869 | 2.411 | 0.100 | 11.2% | 47.71 |

この表は「最も安全な設定」を一つ示してくれなかった。

Immediate は mean OOS Rank IC が最も高く、regime switch への追従も最速で、平均 adaptation lag は 11.67 period しかない。その代わり累積 turnover は最大で、44 checkpoint のほぼすべてでウェイトを変え、false promotion rate は約 39% だった。

本当に 5% を守る delta cap を入れると、mean OOS Rank IC は 0.2199 から 0.2092 に下がり、追従も遅くなる。一方で L1 turnover は 7.305 から 5.591、false promotion rate は 39.0% から 27.8% に下がった。

Shadow をさらに加えると変化はもっと大きい。5% cap で 3 回連続の shadow win を要求すると、false promotion rate は約 10.2%、turnover は 1.659 まで下がる。非常に安定して見えるが、adaptation lag は 75.83 period に伸びた。

「誤った promotion を減らす」だけを目的にすれば最後の設定は魅力的だ。しかし本当に regime が変わったときまで、モデルをほとんど動けないほど保護してしまう可能性がある。

この実験で明確になったのは、**guardrail は無料の安全機構ではなく、モデルそのものの一部になる**ということだった。

AWS SageMaker の [shadow tests](https://docs.aws.amazon.com/sagemaker/latest/dg/shadow-tests.html) も似た production pattern を使う。新しい variant に production request を mirror しつつ、その response は caller に返さず、production variant と比較してから promotion を判断する。量化モデルにも借りやすい考え方だが、SageMaker のドキュメントは「何回連続で勝てば上げるべきか」も「weight cap を何%にすべきか」も決めてくれない。

それは自分の outcome、turnover cost、regime への反応速度から決める必要がある。

## Regime はラベルではなく、どの過去を信じるかを変える

動的ウェイトで起こりやすい錯覚の一つは、サンプルが増えれば推定は必ず安全になる、というものだ。

関係が本当に安定していれば observation が増えるのは有利だ。しかし factor behavior が regime に依存するなら、遠い過去の別環境を同じ trailing window に詰め込むことで、サンプル数は増えても「今」必要な関係を薄めることがある。

Shu と Mulvey が 2025 年に *Journal of Portfolio Management* で発表した [Dynamic Factor Allocation Leveraging Regime-Switching Signals](https://collaborate.princeton.edu/en/publications/dynamic-factor-allocation-leveraging-regime-switching-signals/) は、factor-specific regime inference を使った dynamic allocation を扱っている。今回の実験はそのモデルを再現していないし、その結果が私のシステムを正当化するわけでもない。ただ、regime-aware factor allocation がプロジェクト固有の思いつきではなく、正式な研究テーマであることは確認できる。

自動リウェイトでは、market / regime を後付けの label ではなく evaluation context として扱いたい。

同じ momentum factor でも、evidence は次のような文脈を持つ。

```text
momentum evidence
    + market
    + regime
    + session / universe
    + data quality
        ↓
candidate evidence
```

全部を細かい bucket に分ける必要はない。hierarchical shrinkage のように一部を pool する方法もある。重要なのは、「過去全体で良かった」をそのまま「今ウェイトを上げるべき」に変換しないことだ。

米連邦準備制度の現在の [model risk management guidance](https://www.federalreserve.gov/frrs/guidance/supervisory-guidance-on-model-risk-management.htm) は銀行モデルのガバナンス資料で、トレーディング戦略の手引きではない。ただし一般化できる原則がある。ongoing monitoring では data relevance、exposure、activity、market conditions の変化を見て、モデルが期待どおり動かなくなったなら adjustment や redevelopment を検討する。

この考え方は量化システムにも有用だ。**weight vector は一度 training した後の永久定数でも、各 window で気軽に再計算する一時変数でもない。version、context、change condition が必要だ。**

だから私は前の記事である [因子の有効性検証](/ja/posts/quant-factor-validation-bucket-spread-rank-ic/) をこの話の前に置いている。まず factor evidence を信用できるか確認し、その後で candidate weight をどう動かすか考える。順番を逆にすると、モデルがノイズに対して精密に反応し続けることになる。

## 5% の上限が normalize 後に約 9.1% まで膨らんだ

今回いちばん残す価値があったのは結果表ではなく、途中で見つけた実装ミスだった。

最初の delta cap は自然な書き方だった。

```text
target weight
    ↓
各 factor を old ± 5% に clip
    ↓
normalize して合計を 1 に戻す
```

一見すると、すでに 5% に clip したのだから、最後の normalize は合計を直すだけに見える。

ところが実際に走らせると、名目 5% cap なのに observed max weight jump は平均で約 **9.1%** まで膨らんだ。

理由は単純で、clip が制約しているのは normalize **前**の座標だけだからだ。全ウェイトを後から再スケールすると、一部の座標は再び old から遠ざかる。

つまり次のコードは安全そうに見えても、

```python
clipped = np.clip(target, old - cap, old + cap)
weights = clipped / clipped.sum()
```

これを保証しない。

```text
abs(weights[i] - old[i]) <= cap
```

この種の bug は IC の小数点を一桁間違えるより危険だ。監視画面には `cap=0.05` と出ているのに、実際のモデルはもっと大きく動いている可能性がある。

最終的には bounded simplex projection に変えた。新しい weight は「非負」「合計 1」「各次元が `old ± cap` 内」の三条件を同時に満たさなければならない。

```text
sum(w) = 1
0 <= w_i <= 1
old_i - cap <= w_i <= old_i + cap
```

実装では小さな二分探索で projection offset を求める。

```python
lower = np.maximum(0.0, old - cap)
upper = np.minimum(1.0, old + cap)

lo = np.min(target - upper) - 1.0
hi = np.max(target - lower) + 1.0

for _ in range(80):
    lam = (lo + hi) / 2.0
    w = np.clip(target - lam, lower, upper)

    if w.sum() > 1.0:
        lo = lam
    else:
        hi = lam
```

修正後は、5% cap の observed max jump が 0.05、10% cap が 0.10 に収まった。残るのは floating-point epsilon 程度だ。

この失敗から、drift budget は設定ファイルに書くだけでは足りないと感じた。

```yaml
max_weight_delta: 0.05
```

と書くなら runtime でも、

```text
max(abs(new_weight - old_weight)) <= max_weight_delta + epsilon
```

を assert したい。そうでなければ、それは constraint ではなく単なる parameter だ。

## Shadow の目的は予測精度を上げることではなく、研究結果と本番状態を分離すること

今なら adaptive factor の lifecycle を四段階に分ける。

```text
observe
   ↓
candidate
   ↓
shadow
   ↓
active
```

Observe は evidence を集めるだけで live scoring には影響しない。

Candidate は研究側が「比較する価値がある」weight profile を作った状態で、まだ production state ではない。

Shadow で初めて、同じ future outcome を使って candidate と active を並行比較する。新しいデータで推定したというだけで deployment の資格を得てはいけない。

Promotion が本当の state change だ。

この四層を分けると、次の違いが見やすくなる。

```text
factor IC が改善
≠ candidate が必ず良い

candidate が直近 1 回勝った
≠ promotion すべき

promotion を許可
≠ 無制限にウェイトを動かしてよい
```

今回の false promotion の定義は狭い。新 profile を promote した後、次の future evaluation block で以前の active profile に負けた場合を 1 回の false promotion とした。

唯一の定義ではないが、問題を可視化するには十分だった。直近 window では良く見えた candidate でも、本番に切り替えた直後に負けるケースはかなりある。

Shadow confirmation を増やせばそれは減る。しかし confirmation を増やすほど、本当の regime change への反応も遅くなる。

だから production pipeline を作るなら、

```text
3 回連続で勝つ -> deploy
```

と先に固定するのではなく、まず次を保存する。

```text
candidate_version
active_version
market / regime
evidence_window
future_eval_window
weight_delta
turnover
shadow_outcome
promotion_decision
```

そして settled outcome から、confirmation count と drift budget が実際に何を交換しているのかを見る。

以前の Notion メモには 2 回・3 回の確認や 5%、8%、12–15% といった drift threshold があった。今回の実験は、それらの数字が正しいとは証明していない。証明したのは、こうした control がモデルの挙動を確実に変え、その tradeoff が観測できるということだけだ。

## 自動リウェイトは「制約付きモデル変更フロー」として扱いたい

今回の結果をシステム設計に戻すと、最小ループはそれほど複雑ではない。

```text
1. 現在の active weights を固定
2. 過去 window だけで factor evidence を推定
3. market / regime context を付与
4. candidate weights を生成
5. hard constraint / drift check
6. candidate を shadow に送る
7. future outcome が確定するまで待つ
8. active と shadow を比較
9. promotion rule を満たした場合だけ新 active version を作る
10. decision trail をすべて保存
```

重要なのは 9 を単なる `if ic > threshold` にしないことだ。

最低でも次は知っておきたい。

- candidate はどれだけ長く勝っているか
- その勝ちは同じ regime だけで起きていないか
- promotion でどれだけ weight jump が発生するか
- 最近の累積 turnover はどの程度か
- candidate と active の差は一つの factor なのか profile 全体なのか
- 今 promotion しない場合、本当の regime change への反応がどれだけ遅れるか

「直近 Rank IC = 0.08」だけでは、このどれにも答えられない。

さらに promotion decision 自体も outcome tracking の対象にしたい。システムが「今ウェイトを変えるべき」と判断したら、後から「変えなかった場合はどうだったか」「もう 1 window 待ったらどうだったか」を戻って確認する。

記録が十分たまって初めて、shadow confirmation を 1、2、3 のどれにするか、delta cap を 5% か 10% にするかを自分のデータで議論できる。

ここまで来ると、単なる parameter tuning とはかなり違う。

adaptive weighting system が学ぶべきなのは「最近どの factor が効いているか」だけではない。**自分の model-change rule が過去に本当に役立ったか**も学ぶ必要がある。

今回の synthetic experiment はそこまでを示した。Immediate は反応が速いが churn が大きい。Shadow は慎重だが、遅すぎれば regime を逃す。delta cap は一回の動きを抑えられるが、future outcome validation の代わりにはならない。

この表から一つの数字を選んで、そのまま production config に貼るつもりはない。

次にやる価値があるのは、これらの guardrail を real settled outcome に対して shadow でしばらく走らせることだ。システムが promotion したかった candidate を全部記録し、その後も本当に勝ち続けたかを確認する。実データが十分たまってから、許容できる false promotion と、安定性のために払ってよい adaptation lag を決めればいい。

## Verification context

- Level: first-party
- Verified: 2026-10-08T00:00:00.000Z
- Environment: Python
- Environment: Synthetic regime-switching simulation
- Environment: 12 fixed seeds / 240 periods / 200 assets
- Evidence mode: generated
- Evidence receipt: https://vxi.me/evidence/adaptive-factor-weighting-regime-shadow-drift.json
