2軸パラメータ空間: なぜスイープの大部分はほぼ無料であるべきなのか
「幻想なきバックテスト」シリーズの一部。
次元の呪いは、たいてい警告として語られる。パラメータを1つ加えるごとに探索空間は掛け算式に増えていくので、18次元の戦略をスイープするのは絶望的だ、と。だがこの語り口は、どの次元も評価コストが同じであることを暗黙のうちに前提にしている。実際にはそうではない。私たちのデュアル/トリプルタイムフレームエンジンでは、パラメータの3分の2はスイープしてもほぼ無料であり、残りの3分の1が計算コストのほぼすべてを背負っている。この分割が見えてしまえば、「探索空間が大きすぎる」はもはや正しい不満ではなくなる。正しい問いはこうだ。あなたが実際にコストを支払っているのはどの次元なのか?
再計算が不要だった次元
私たちのパラメータ探索ベンチマークが対象とするのは、マルチタイムフレームのモメンタム戦略だ。2つまたは3つの時間軸上でハル移動平均とその三重平滑化版(HMA3)を計算し、クロスオーバーが仕掛けるに値するほど「きれい」かどうかを判定する方向性乖離ゲートを備える。トリプルTF版のフルサーチ空間は18次元 — 各時間軸ごとの期間とHMA長に加え、各時間軸のエントリーとエグジット用の乖離閾値の集合からなる。
これを愚直にスイープする方法は単一ループだ。パラメータベクトルを選び、すべてのインジケーターをゼロから構築し、シミュレーションを実行してスコアを付け、繰り返す。私たちも最初はそこから始めた。遅かった。そして、その遅さの原因は、名前を付けてみると恥ずかしくなるようなものだった。隣り合う試行の圧倒的多数において、変化していないインジケーターを再計算していたのだ。
エントリー乖離閾値を0.03から0.035に少しだけ動かして再実行してみる。1年分の1分足にわたるハルMAは、前回の試行とビット単位で完全に一致する — 閾値はその定義のどこにも現れないからだ。それにもかかわらず、愚直なループは毎回、50万本を超えるバー、3つの時間軸ぶん、律儀に作り直す。私たちは計算予算のほぼすべてを、実際に動かしているパラメータに対して不変な量の再導出に費やしていたのだ。
この気づきが、この記事のすべてだ。あるパラメータはインジケーターを変え、別のあるパラメータは固定されたインジケーターに適用される判断ルールだけを変える — そう気づいた瞬間、パラメータ空間はフラットな次元の雲であることをやめ、価格帯がまるで違う2つの入れ子構造の層になる。
1軸ではなく2軸

パラメータを、それらを再評価したときに何を再計算せざるを得ないかで分類する。
-
高価な軸 — インジケーターパラメータ。 時間軸の期間とHMA長。どちらか一方を変えるだけで、価格系列全体にわたってインジケーターを作り直さなければならない — 上位時間軸をリサンプルし、ハル加重を計算し、クロスオーバー系列と各クロス時点の乖離を再計算する。これは履歴の各バーごとにO(n)のスイープであり、異なる組み合わせごとにその全額を支払うことになる。トリプルTF戦略では、この軸は6個のパラメータになる — 高位・中位・低位それぞれの時間軸について
(period, hma_length)。 -
安価な軸 — 判断の閾値。 すでに計算済みのクロスオーバー系列と乖離系列をもとに、エントリーするかエグジットするかを決める方向性乖離ゲート。閾値を変えても、インジケーターは一切動かない。事前計算済みシグナルに対する単一のO(n)パスを再実行し、ゲートを確認して約定を記録するだけだ。トリプルTF戦略では、この軸は12個のパラメータになる(エントリー買い・エントリー売り・エグジット買い・エグジット売りの4つの乖離閾値 × 3つの時間軸)。デュアルTF版は8個(4つの閾値 × 2つの時間軸)。
したがって、この空間の実際の形はこうなる。トリプルTFでは高価6 + 安価12 = 18、デュアルTFでは高価4 + 安価8 = 12。次元数の3分の2が安価な軸に存在する。そして2つの軸は個数が違うだけではない — 単位コストにおいて3桁以上の差がある。これがこの記事の残りの部分で扱う数字だ。
この構造は厳密に入れ子になっている。高価な軸上の各点 — 時間軸ごとに1つの具体的な(period, hma_length) — は、固定されたシグナル配列の集合を決める。その固定された土台の上に、安価な軸のシート全体が乗っている。何千もの閾値ベクトルが、それぞれ同じ配列に対する高速なパスとして存在する。土台への支払いは一度で済み、そのコストはシート全体に償却される。
なぜインジケーターは閾値に対して不変なのか
このキャッシュが機能するのは、幸運な実装上の偶然ではなく、数学的な事実によるものであり、これはこの記事の土台となる前提なので正確に述べておく価値がある。インジケーター配列は(period, hma_length)のみの関数である。乖離閾値は、その定義のどこにも登場しない。
具体的には、各時間軸についてエンジンはベースインデックスに揃えられた4つの配列を事前計算する — ハルMAのhma、その三重平滑化版hma3、クロスオーバー系列cross(+1が買い / −1が売り / 0)、そして各クロス時点におけるseparationのパーセンテージだ。これらはすべて価格と2つのインジケーターパラメータから導出される。閾値 — separationを比較する相手となる数値 — は、後になって判断の段階で適用される。配列には一切触れない。
これは自明に安全な並べ替えではない。安全なのは、私たちが情報がいつ利用可能になるかに注意を払っていたからに他ならない。これはこのシリーズの別記事、先読みバイアスの分類が扱っているテーマだ。ベースバーiにおける上位時間軸のインジケーターは、確定済みの上位TFローソク足に加え、進行中のローソク足(その途中の終値は現在のベース足の終値close[i]に等しい — つまりバーiの時点で既知の値)から計算される。未来の情報は一切漏れ込まない。したがって、バーiで事前計算されたシグナルは、実運用のボットがバーiの時点でまさに見ていたであろうものと一致し、後でどの閾値と照合しても有効であり続ける。リークのあるシグナルをキャッシュすれば、そのリークをキャッシュするだけになる。因果的に正しいシグナルをキャッシュしてこそ、実際に取引できるものをキャッシュしたことになる。
この不変性が、因数分解を可能にする。1つの完全なパラメータベクトルθ = (indicator_params, threshold_params)の評価を、次のように書ける。
signals = build_indicators(indicator_params) # EXPENSIVE, depends only on indicator_params
score = simulate(signals, threshold_params) # CHEAP, reuses signals across all thresholds
build_indicatorsはthreshold_paramsを読み取らない。この一つの事実こそが、キャッシュを正当化する。indicator_paramsを固定し、threshold_paramsを自由に変化させれば、signalsは一度計算すれば済む定数になる。
アーキテクチャ: 一度計算し、多数をスイープする

エンジン(私たちのバックテスターにおけるscripts/engine_multitf.py、コミットbfc8aaa)は、この因数分解を、名前がそのまま働きを表す2つの部品で実装している。
SignalCache — メモ化された高価な軸。 (period_bars, hma_length)をキーとする辞書だ。ある時間軸のシグナルを要求すると、そのインジケーターの組み合わせが以前に構築済みであればキャッシュされたTFSignalsを返し、そうでなければ一度だけ構築して保存する。キーがインジケーターパラメータのみであるため、同じインジケーターの組み合わせを共有するすべての閾値設定 — 密な閾値スイープでは何千もの設定になる — は、同じキャッシュエントリにヒットする。特に上位時間軸はこの恩恵を大きく受ける。たとえば候補期間4つ×HMA長数個という粗いグリッドなら、異なる高価な構築はごく少数で済み、それぞれがその上に積み重なる閾値シート全体で再利用される。
sweep_separations — バッチ化された安価な軸。 キャッシュ済みのシグナル配列と、閾値ベクトルの行列(sps、形状[m, 12])を受け取り、それらすべてを単一のコンパイル済みカーネルに通す。各行は1回のO(n)パスだ。バーを走査し、ゲートを適用し、open[i+1]でリークのない約定を記録し、PnLとポジション保有時間を集計する。このループの内部でインジケーターが再構築されることはなく、crossとseparationをキャッシュから直接読み取るだけだ。内側のシミュレーションはJITコンパイル(Numba)されているため、ウォームアップ後は設定ごとのコストは、Pythonのオーバーヘッドではなく、バーに対する単一の線形スキャンが支配的になる。
この2つの部品は、自然な入れ子構造の探索として組み合わさる。外側のループが高価な軸を歩き(各反復が1つのインジケーターの組み合わせを構築してキャッシュし)、内側のループがそのキャッシュ済み配列に対して安価な軸全体に幅広いバッチを展開する。コード上の形もまさにそれだ — 小さな高価なループが、幅広い安価なバッチを包み込む。
cache = SignalCache(base_close, base_ts) # keyed by (period, hma_length)
for htf_p, htf_h, mtf_p, mtf_h, ltf_p, ltf_h in indicator_grid: # EXPENSIVE axis (coarse)
htf = cache.get(htf_p, htf_h) # built once, then a cache hit forever
mtf = cache.get(mtf_p, mtf_h)
ltf = cache.get(ltf_p, ltf_h)
sps = sample_thresholds(m=4000) # CHEAP axis: [m, 12] threshold vectors
pnl, n_trades, bars_in_pos = sweep_separations( # one compiled batch, no indicator work
base_close, base_open, htf, ltf, sps, mtf=mtf)
高価なビルダーには、インジケーターグリッドが許す限り最小限しか触れず、量をこなすのは安価なスイープに任せる — 何千ものspsの行を、決して動かない配列に対して処理する。それがこの最適化のすべてだ。近似はなく、忠実度の低下もない。変化していないものを再計算するのを拒んでいるだけだ。
「無料」が実際にかかるコスト: 数字で見る

この2つの軸を、デモのワークロードで測定した。1年分のETHUSDT 1分足(約52.7万本)、トリプルTF、インジケーターはウォームアップ済みでJITコンパイルの時間は計測から除外している。
安価な軸では、sweep_separationsは毎秒約5,600件の閾値設定を持続的にこなす。これは各設定について、50万本を超えるバーにわたる完全なシミュレーション — ゲート、約定、PnL、エクスポージャー — であり、1設定あたりおよそ180マイクロ秒だ。これほど速くできる理由は、インジケーター関連の作業をまったく行わないからだ。すべての設定が、同じキャッシュ済みのcrossとseparationの配列を読み取るだけである。
では代替案のコストを見積もってみよう。トリプルTFのインジケーター一式を一度構築するのには、数百ミリ秒のオーダー(約0.3秒)かかる — 3つの時間軸のリサンプル、ハル加重、1年分にわたるクロスオーバーと乖離の抽出だ。もし設定ループの内側でインジケーターを再計算していたら — つまり愚直な単一ループの設計では — 毎秒5,600件だったはずの設定のそれぞれが、代わりにインジケーター構築のフルコストを支払うことになる。設定あたりのコストは約180マイクロ秒から約0.3秒へと膨れ上がる。
| 設定あたりのコスト | 設定数/秒 | |
|---|---|---|
| 安価な軸(キャッシュ済みシグナル) | ~180 µs | ~5,600 |
| 設定ごとにインジケーターを再計算 | ~0.3 s | ~3.4 |
比率は約1,600倍だ。キャッシュ済みシグナルに対して安価な軸をスイープするのは、すべての閾値ベクトルについてインジケーターを作り直す愚直な設計に比べて、およそ3桁安い。具体的に言うと、キャッシュされた経路なら1秒足らずで終わる数千件の閾値設定のバッチが、各設定でインジケーターを作り直す場合には1時間近くかかることになる。結果は同じ、忠実度も同じ、数学に近道はない — 唯一の違いは、一方は不変量を再計算し、もう一方はしないという点だけだ。
これは最後に振りかけるだけの微細な最適化ではない。どのような探索が実行可能かそのものを変える。毎秒5,600設定という速度があれば、閾値軸はきちんと探索できるほど密にできる — インジケーターの組み合わせごとに、細かいグリッドや長いランダム/QMCサンプルを使う余裕が生まれる — 一方で高価な軸は意図的に少数の粗いビルドのままにしておける。計算予算は、パラメータが実際にコストを持つ場所へと流れていく。
次元の呪いを、再評価する

この記事の冒頭で示した枠組みに戻ろう。次元の呪いは、探索空間がパラメータ数に対して指数関数的に増大するため、次元が多いほど一律に悪いと言う。これはグリッドのサイズについては正しい。しかし、それを網羅するコストについては誤解を招く。なぜなら、すべての次元を同じ値段で評価してしまっているからだ。
軸を分離してみると、次元数の読み方が変わる。トリプルTF戦略は18次元だが、そのうち高価なのはわずか6次元だけだ。残りの12は安価な軸の次元であり、計算コストを実質的に増やすことなくグリッドを拡張する — わずかな費用で何千もの設定を投入できる。デュアルTF戦略は12次元で、高価な次元は4つだけ、安価な次元が8つだ。どちらの場合も、「呪い」の大部分は、スイープにほとんどコストがかからない軸に集中している。
したがって、探索コストについて正直に考える方法は「パラメータがいくつあるか」ではなく、「高価なパラメータがいくつあり、そのグリッドをどれだけ粗くできるか」だ。指数関数が実際に痛みをもたらすのは高価な軸であり、そこでは小さく吟味されたグリッド — 数個の候補期間、控えめな範囲のHMA長 — が欲しい。場合によっては、私たちが他の記事で使っている適応解像度ドリルダウンの精神に則って、粗いものから細かいものへと段階的に精緻化してもよい。安価な軸では贅沢にしてよい。追加の閾値設定1つあたり180マイクロ秒だからだ。
この再評価は、私たちのHMA戦略をはるかに超えて一般化できる。一部のパラメータは特徴量を変え、大半のパラメータは固定された特徴量に適用されるルールを変えるというパターンは、システムトレードのあらゆる場面で繰り返し現れる。インジケーターの長さ、リサンプリングの頻度、ルックバックウィンドウは高価であり、エントリー/エグジット閾値、ストップ幅、ポジションサイジングの倍率、確認ゲートは安価だ。あるパラメータが、すでに計算済みのシグナルに対して判断境界を作り直すだけであれば、それは安価な軸に属し、そこでスイープされるべきだ。同じ系統の関連するキャッシュの恩恵は、マルチタイムフレームParquetキャッシュや、より広いエンジン速度の梯子にも見られる。
タダ飯にも請求書が来る場所
安価な軸は計算コストにおいて安価だ。しかし統計においては安価ではなく、この2つを混同することが、パフォーマンス面での勝利をオーバーフィッティング製造機に変えてしまう原因となる。
評価する閾値設定の1つ1つが1回の試行であり、1つのデータセットに対して何千回もの試行を行えば、その中の最良のものは部分的には運の良さによって良く見える。試行を1,600倍安くしても、その運の要素は消えない — むしろ蓄積しやすくなるだけだ。密な閾値スイープはまさに、多重検定によるインフレーションが最も深刻に効いてくる状況だ。多数の候補、1つの履歴、そして最大値を報告する選択ルール。世界最速のエンジンであっても、テストウィンドウのノイズに見事にフィットし、アウトオブサンプルでは失敗する閾値ベクトルを、喜んで差し出してくることになる。
したがって、規律は速度に見合ってスケールさせなければならない。試行の計算コストがゼロに近づいた瞬間、統計的な会計こそが拘束条件になり、それを明示的に支払う必要がある。
- 試行回数でデフレートする。 勝者を、ゼロと比較するのではなく、何個の設定を試したかに照らしてスコアリングする。デフレーテッド・シャープレシオやバックテストオーバーフィッティングの確率(PBO)は、まさにこのために存在する — 「4,000個の閾値ベクトルを試した」という事実を、報告されたエッジに対する割り引きへと変換してくれる。
- フォールドごとにアウトオブサンプルで検証する。 安価なスイープであっても、正直なウォークフォワード分割の内側で実行しなければならない。インサンプルでしか勝てない閾値は、それをどれだけ速く見つけたとしても無価値だ。私たちのエンジンが約定をリークなし(
open[i+1])に保っているのは、まさに安価な軸が覗き見によってパフォーマンスを買えないようにするためだ。 - ピークよりプラトーを選ぶ。 閾値軸は密で高速なので、argmaxだけでなく応答曲面全体をマッピングできる。広い範囲の閾値がすべて機能するなら、それは本物のエッジだ。単一のとがった極大値は、フィッティングによる人工物にすぎない — このプラトー対ピークの区別は、まさにここで説明している速度によって手が届くようになる。
2軸構造は正しく読み解く必要がある。それが買ってくれるのはより多くの確信ではなく、試行あたり同じ確信コストで、より多くの試行数だ。これは本当に価値がある — 安価な軸を密にカバーすることで、プラトーを見つけ出し、曲面の特徴を捉えられるようになる — ただし、統計的な帳簿を正直に保っている限りにおいてだ。速度は、徹底的に探索しないことへの計算上の言い訳を取り除く。しかし、徹底的な探索が見つけたものを割り引く義務までは取り除かない。
自分の戦略の探索をどう構造化するか
これを自分の戦略に適用するための作業は、大部分が分類だ — パラメータを正しい軸に振り分けること — そのあとに入れ子ループが続く。
- すべてのパラメータに、それが何を再計算させるかでラベルを付ける。 それを変えるとインジケーター/特徴量配列が変わるなら高価だ。固定された配列に適用される比較・閾値・サイジングルールだけを変えるなら安価だ。迷ったら、こう問うとよい — このパラメータはインジケーターの定義のどこかに現れるか?現れないなら、それは安価だ。
- キャッシュのキーには高価な軸だけを使う。 特徴量構築を、そのインジケーターパラメータでメモ化する(
SignalCacheが(period, hma_length)で行っているように)。同じインジケーターの組み合わせを共有する隣接した試行は、同じ配列を無料で再利用できるようになる。 - キャッシュ済みの特徴量に対して、安価な軸をバッチ化する。 閾値設定を、完全な再評価としてではなく、事前計算済みシグナルに対する引き締まったコンパイル済みループとして実行する。スループット — 私たちの場合は約5,600設定/秒 — はここから生まれる。
- ループを入れ子にする: 高価な軸が外側、安価な軸が内側。 高価なグリッドは小さく、意図的なものにしておく(粗い、あるいは粗いものから細かいものへ)。安価なスイープは密にしてよい。予算は、パラメータが実際にコストを持つ場所に使う。
- 試行回数の予算は、時計ではなくオーバーフィッティングに対して組む。 時計がもはや制約ではなくなった以上、デフレーテッド・シャープやPBOにその役割を委ねよう。安価な軸の試行を統計的に何回まで許容できるかを決め、勝者をアウトオブサンプルで検証する。
エンジニアリング上の見返りと統計的なガードレールは、1つの考えの表裏だ。軸を分離することで安価な次元を徹底的に探索できるようになる — そしてまさにそれゆえに、どれだけ徹底的に探索したかを割り引かなければならない。
まとめ
- すべての次元が同じコストとは限らない。 戦略のパラメータは、高価な軸(インジケーター — 系列全体で再計算が必要)と安価な軸(閾値 — 事前計算済みシグナルに対するO(n)のパス)に分かれる。私たちのエンジンでは、トリプルTFで高価6 + 安価12、デュアルTFで高価4 + 安価8だ。
- インジケーターは閾値に対して不変であり、その不変性こそが最適化のすべてだ。 特徴量構築はインジケーターパラメータのみに依存する。だからこそ一度だけ構築し、
(period, hma_length)でキャッシュし、同じ組み合わせを共有するすべての閾値設定にわたって再利用できる。 - 安価な軸は約1,600倍安く動く。 キャッシュ済みシグナルに対して約5,600設定/秒(1件あたり約180マイクロ秒)に対し、毎回インジケーターを作り直す場合は1設定あたり約0.3秒。忠実度は同じ — 唯一の違いは、不変量の再計算を拒否しているかどうかだけだ。
- 次元の呪いとは、実際には高価な次元数の呪いである。 パラメータ数の大半は、スイープがほぼ無料な軸に存在する。高価なグリッドは粗く保ち、安価な方は贅沢に使う。
- 速度は拘束条件を計算から統計へと移す。 密で高速なスイープは、多重検定製造機だ。試行回数でデフレートし、フォールドごとに検証し、ピークよりプラトーを選ぶ — タダ飯は本物だが、統計的な請求書は避けられない。
エンジンの全体 — SignalCache、sweep_separations、リークのないマルチTFシミュレーション、そしてそれをライブの進行中ローソク足のセマンティクスに結び付けるパリティテスト — は、私たちのバックテスターのscripts/engine_multitf.py(コミットbfc8aaa)にある。次に誰かが「18パラメータの戦略は探索するには大きすぎる」と言ってきたら、そのうちいくつのパラメータが実際にインジケーターを動かすのかを尋ねてみるとよい。たいていはその3分の1で、残りはほぼ無料だ。
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.