Bot CEX 開発ログ

🛠️開発記録#487(2026/3/17)multi_market_probe開発ログ ― 観測機の機能を極限まで絞って、歪み回帰の白黒判定器を作った話

こんにちは、ぼっちbotterよだかです。

今回は、multi_market_probe の観測機をあえて多機能化せず、歪み回帰の白黒判定だけに極限まで絞って実装を進めた話を書きます。狙いは、賢い判定器を作ることではなく、「今この条件で構造があるのかないのか」を低コストで返せるようにすることでした。HOLDを捨ててYES/NOに倒し、判定条件とパネル表示も最小構成へ整理した結果、少なくとも現条件では「構造なし」がかなり分かりやすくなってきました。今回は、その設計意図と実装内容、そして今何が見えていて何がまだ見えていないのかを整理します。

前回の話
🛠️開発記録#484(2026/3/12)「multi_market_probe開発ログ ― J系統パネルを“読める判定機”に削った話」

続きを見る

1. 今回のゴール:歪み回帰だけを見て、白黒判定を返す観測機に絞る

今回の実装で最初に決めたのは、「観測できるものを増やす」ことではなく、「今この条件で歪み回帰構造があるのかないのかを、低コストで返す」ことを最優先にする、という方針でした。multi_market_probe はもともと複数の見方を持てる観測機ですが、そのぶん解釈の余地も多くなります。今回ほしかったのは、賢い総合判定器ではなく、まずは歪み回帰だけを対象にして、YES か NO かを返す最小構成の判定器でした。設定上も J-Reversion は base_market: bf_fxref_market: binance_perp に固定され、しきい値は 10bps、主判定窓は 30 秒というかなり限定的な条件で切られています。

この「絞る」という判断には、はっきりした理由がありました。歪み回帰を見たいのに、途中で HOLD や補助判定が増えていくと、結局「今はあるのか、ないのか」が読めなくなるからです。実際、今回の exporter とダッシュボードは J-Reversion を NO=0 / YES=2 を前面に出す表示へ寄せていて、方向別の判定と、long/short をまとめた最終 OR 判定をシンプルに見られる構成へ整理されています。互換のために LEGACY_HOLD の名残は残っていますが、表示の主役は明確に NO と YES です。

今回のゴールをもう少し正確に言うと、「歪み回帰の執行戦略を完成させる」ことでも、「複数市場の構造を全部説明する」ことでもありませんでした。そうではなく、bf_fx × binance_perp の組み合わせについて、一定以上の歪みが発生したあとに、30 秒以内で回帰構造が見えているのかどうかを、最低限のゲートだけで白黒化することが目的でした。コード上でも J-Reversion のクラスは threshold、horizon、corr 条件、サンプル下限、YES/NO の判定閾値など、今回必要な要素だけを抱える形になっています。

このとき、歪みの定義には premium_exec_best_bps 系を使う前提を取りました。これは「見かけ上きれいに見える関係」ではなく、少なくとも実行可能性に寄った土俵で観測したかったからです。Prometheus 側にも best executable spread の分布ヒストグラムが用意されていて、実際にどの帯域にどれだけサンプルがあるのかを後から見直せるようになっています。つまり今回の観測機は、理論上の歪みを広く拾う装置ではなく、「取れるかもしれない歪みだけを、まずは白黒で見る」方向へ重心を寄せています。

なお、今回の実装では対数リターンや log spread を前提座標として使っていますが、その説明は本筋ではありません。そこは別記事で整理したので、本記事では「歪みとその後の変化を同じ座標で見るために、その表現を採用している」程度に留めます。ここで重要なのは、数学的な美しさではなく、その座標の上で観測機の責務をどこまで削るかでした。

結果として、今回のスタート地点で明確にしたのは三つです。見る対象は歪み回帰だけ、見る市場は bf_fx × binance_perp だけ、返す答えは YES か NO だけ。このくらいまで責務を削ってようやく、「今この条件で構造があるのかないのか」を真正面から確かめる観測機になったと思っています。

2. なぜ削る必要があったのか:解釈器ではなく判定器にしたかった

今回、観測機の機能をあえて削ったのは、情報が足りなかったからではありません。むしろ逆で、見えるものや補助指標が増えるほど、「今この条件で歪み回帰構造があるのか、ないのか」が曖昧になっていたからです。もともとの multi_market_probe は、相関、乖離幅、Forward、J判定と、複数の層を重ねて読む前提の観測機でした。これは探索段階では有用ですが、白黒を付けたい局面では、かえって判断を保留しやすくなります。設定上も J-Reversion には 10/30/60 秒の複数 horizon、relation weak の持続判定用パラメータ、YES/NO の閾値などが並んでおり、そのままでは「構造がない」のではなく「まだ解釈できる余地が残っている」状態を生みやすい形でした。

特に大きかったのは、HOLD の存在です。HOLD は一見すると丁寧な判定に見えますが、実際には「YESでもNOでもない理由を、さらに読みに行く」ことを要求します。Prometheus exporter 側にも、J-Reversion の state code はまだ NO=0, HOLD=1, YES=2 の互換形で残っていて、reason code にも HOLD_NOT_READYHOLD_NO_RELATION_PENDING のような中間状態が定義されています。これは開発途中の探索器としては自然ですが、「今の条件下で歪み回帰構造があるのか」を見たいときには、解釈コストを押し上げる要因になります。だから今回は、内部互換は残しつつも、表示と運用の主役を NO/YES に寄せる必要がありました。

その意図はダッシュボード側にもはっきり出ています。今回の J-Reversion セクションは、[J-Reversion Minimal] Verdict by DirectionFinal Verdict OR + ReasonGate SnapshotEvidence (30s) の4パネルまで削られています。方向別 verdict と最終 OR 判定を前面に出し、残りは「その判定の前提」と「30秒判定の根拠」に限定しました。つまり、補助情報を増やして理解を深めるより先に、まず判定器として読む順番を固定したわけです。ここでは、観測機を賢くしたというより、迷える余地を減らしたと考えた方が正確だと思います。

実際、今回の J-Reversion の判定ロジックも、その思想に合わせてかなり単純化されています。quality が悪ければ NO_QUALITY_ISSUE、相関の準備が足りなければ NO_NOT_READY、relation が弱ければ NO_NO_RELATION、30秒サンプルが足りなければ NO_LOW_SAMPLE、歪みイベントが足りなければ NO_NO_DISTORTION、30秒 hit rate と方向性が不足していれば NO_NO_REVERSION_30S という形で、基本はすべて NO に倒しています。ここには「60秒ならいけるかもしれないから一旦保留する」といった逃げ道はありません。観測機としてはかなり厳しめですが、そのぶん「いま構造が立っていない」ことを素直に返せるようになります。

この方針を取った理由は単純で、今回必要だったのが“よく考えれば読める装置”ではなく、“今はないを返せる装置”だったからです。探索フェーズでは、つい「条件を足せば見えるかもしれない」「別の horizon なら拾えるかもしれない」と考えがちです。しかしその状態のままだと、観測機はいつまでも解釈器のままで、白黒判定器になりません。だから今回は、機能を広げるのではなく、歪み回帰だけに責務を絞り、その代わり判定は厳しく単純にする、という方向を選びました。

要するに、今回削ったのは情報そのものではなく、迷うための余白です。J系統を解釈器のまま育てるのではなく、まずは「この条件ではあるのか、ないのか」を返す判定器へ寄せたかった。今回の実装は、そのための削減だったと思っています。

3. 今回固定した仕様:対象ペア・歪み定義・ゲート条件をどう決めたか

今回の実装で先にやったのは、ロジックを増やすことではなく、「どこまでを今回の判定器の責務にするのか」を先に固定することでした。観測機を作っていると、どうしても「あれも見たい」「この条件も残したい」と広がりやすいのですが、今回はそこを逆に絞っています。J-Reversion は設定上も base_market: bf_fxref_market: binance_perp に固定されていて、まずはこの1ペアだけで白黒を返す前提です。pair_return_corr や pair_premium_eval には bybit や coinbase も並んでいますが、J-Reversion 自体は bf_fx × binance_perp に限定して動くように切ってあります。

歪みの定義も、今回はかなり意図的に寄せています。採用したのは premium_exec_best_bps 系で、mid ベースではなく executable-best ベースです。Prometheus 側でも obx_mmarb_premium_exec_best_bps は「premium executable-best bps alias」として定義されていて、さらに obx_mmarb_total_cost_bpsobx_mmarb_edge_net_bps が並んでいます。つまり今回見たいのは、見かけ上の価格差ではなく、少なくとも実行可能性に寄った土俵での歪みです。ここは「構造をきれいに見る」より、「後段でノイズを膨らませない」ことを優先した部分でした。

発生条件と時間幅も、今回は暫定値をはっきり置いています。pair_premium_eval 側には 5/10/15/20/30/40/50bps や 10/30/60 秒の追加観測がすでに入っていますが、J-Reversion の主判定は threshold_bps: 10horizon_main_sec: 30 です。短期・補助として 10 秒と 60 秒は残しつつも、最終的な白黒判定はまず 30 秒を中心に返す、という構成にしています。いきなり最適値を探しに行くのではなく、まず 10bps × 30s という一つの土俵を固定したうえで、「ここで構造が見えるのか」を見るためです。

連動性ゲートも、今回は最小構成に寄せて決めました。pair_return_corr は window_sec: 60min_points: 20 で回しており、J-Reversion 側では corr_window_sec: 60corr_min_points: 20relation_min_abs_corr: 0.15 を使います。コード上でも、まず corr_readycorr_points が足りているかを見て、そのうえで abs(corr_val) >= relation_min_abs_corr を満たしたときだけ relation OK とみなしています。ここで見ているのは、「この2市場に最低限の連動がある局面だけを歪み回帰判定の対象にする」という足場です。relation weak の持続判定パラメータは設定には残っていますが、今回の最小運用では主役ではなく、単発の relation 条件をまず優先しています。

品質ゲートも同じ発想で決めています。今回の quality は、複雑な quality score を作ることではなく、「少なくとも今のサンプルが壊れていないか」を見るための最低限の条件です。J-Reversion の実装では、stale_flag < max_stale_flag、signed bps が finite、そして internal_update_timeout_sec: 10 以内で内部更新が継続していることを使って quality_ok を作っています。設定でも max_stale_flag: 0.5internal_update_timeout_sec: 10 が明示されていて、解釈を増やすより先に「止まっていない・壊れていない」ことを確認する方向です。

白黒判定そのものの下限も、今回はかなりはっきり置いています。min_samples_main: 80min_samples_aux: 30min_distortion_events_total: 10yes_hit_rate_pct: 55no_hit_rate_pct: 45directional_delta_min_abs_bps: 0.5 です。もっとも、今回の運用では HOLD を主役にしないので、実際の判定ロジックはかなり単純です。quality が悪ければ NO、相関の準備が足りなければ NO、relation が弱ければ NO、30 秒サンプルが足りなければ NO、歪みイベントが足りなければ NO、そして 30 秒 hit rate と方向性が足りなければ NO。逆に、30 秒 hit rate が 55%以上で方向整合も通るときだけ YES にします。つまり今回固定した仕様は、条件を細かく分けて丁寧に読むためのものではなく、「この土俵で立っているなら YES、そうでなければ NO」と返すための境界線でした。

要するに、今回固定した仕様は最適化の結果ではありません。むしろ逆で、最適化に入る前に、まず一つの土俵を明示的に決めたという意味合いが強いです。対象ペアを固定し、歪みの定義を executable-best に寄せ、30 秒主判定と最低限の relation / quality ゲートだけを残す。そうやって責務を狭く切ったことで、ようやく「今この条件で歪み回帰構造があるのか」を見に行ける状態になったと思っています。

4. 実装で何を変えたのか:within-window判定とYES/NO二値化を入れた

今回の実装で変えたことを一言で言うと、J-Reversion を「解釈のための観測器」から「白黒を返す判定器」へ寄せるために、判定ロジックそのものをかなり単純化した、ということです。設定ファイルを見ると、J-Reversion は threshold_bps: 10horizon_short_sec: 10horizon_main_sec: 30horizon_slow_sec: 60 を持ちながら、主判定は 30 秒を中心に返す構成になっています。さらに、品質側には internal_update_timeout_sec: 10 が追加され、止まったデータをそのまま読むのではなく、内部更新が継続していることも最低条件に入れています。

一番大きい変更は、Forward 側の回帰判定を 「horizon 時点の一点比較」から「window 内に一度でも縮んだか」へ変えた ことです。これは _PairPremiumForwardTracker の中で、アンカー発生後のサンプルをただ horizon 到達時点で比較するのではなく、その間に観測された最小 absolute premium を deque で追跡する形で実装しています。実際、_append_min_abs_sample() で最小値候補を単調キューに積み、_resolve_future() では horizon 到達後に min_abs_seen < abs(anchor_premium) なら hit とみなしています。つまり今回の hit は「30秒後にたまたま近かったか」ではなく、「30秒以内のどこかで一度でも回帰したか」に変わりました。これは、今回のようにまず構造の有無を見たいフェーズではかなり重要な変更でした。

実装の該当部分を抜くと、考え方はかなり素直です。

def _resolve_future(self, *, ts_ms: int, premium_exec_best_bps: float) -> None:
horizon_ms = int(self.future_horizon_sec * 1000.0)
while self._anchors and (ts_ms - self._anchors[0][0]) >= horizon_ms:
anchor_ts, anchor_premium = self._anchors.popleft()
while self._anchors_min_abs_q and self._anchors_min_abs_q[0][0] < int(anchor_ts):
self._anchors_min_abs_q.popleft()
delta = premium_exec_best_bps - anchor_premium
self._future_delta_latest = float(delta)
self._future_samples_total += 1
min_abs_seen = abs(premium_exec_best_bps)
if self._anchors_min_abs_q:
min_abs_seen = float(self._anchors_min_abs_q[0][1])
if min_abs_seen < abs(anchor_premium):
self._future_reversion_hits += 1

ここで見ているのは、終点の値そのものではなく、「その窓の中で一度でも元のゼロ方向に寄ったか」です。今回の開発意図とかなり噛み合っている部分だと思います。

もう一つの大きな変更は、J-Reversion の最終判定を 実質 YES/NO の二値運用にした ことです。Prometheus exporter には後方互換のため NO=0, HOLD=1, YES=2 という state code 自体は残っていますが、reason code には今回の最小運用向けに NO_NOT_READYNO_LOW_SAMPLENO_QUALITY_ISSUE が追加され、NO 側の理由が明示されるようになりました。つまり「一旦保留する」よりも、「何が足りないから NO なのか」を返す方向へ寄せたわけです。

その実際の判定は _JReversionStructureTracker._judge_direction() にまとまっています。ここは今回かなり重要で、quality が悪ければ NO、相関準備が足りなければ NO、relation が弱ければ NO、30秒サンプルが足りなければ NO、歪みイベントが足りなければ NO、30秒 hit rate と方向性が通ったときだけ YES、というかなり単純な流れです。コードで見るとこうなっています。

def _judge_direction(
self,
*,
direction: str,
quality_ok: bool,
ready_ok: bool,
relation_ok: bool,
total_distortion_events: float,
facts_30s: dict[str, float],
) -> tuple[str, str]:
if not quality_ok:
return "NO", "NO_QUALITY_ISSUE"
if not ready_ok:
return "NO", "NO_NOT_READY"
if not relation_ok:
return "NO", "NO_NO_RELATION" samples_30 = float(facts_30s.get("samples_total", 0.0))
if samples_30 < float(self.min_samples_main):
return "NO", "NO_LOW_SAMPLE" hit_30 = float(facts_30s.get("hit_rate_pct", 0.0))
delta_latest_30 = float(facts_30s.get("delta_latest", 0.0))
delta_mean_30 = float(facts_30s.get("delta_mean", 0.0))
edge_ok = self._directional_edge_ok(
direction=direction,
samples_30s=samples_30,
delta_mean_30s=delta_mean_30,
delta_latest_30s=delta_latest_30,
) if total_distortion_events < float(self.min_distortion_events_total):
return "NO", "NO_NO_DISTORTION" if hit_30 >= self.yes_hit_rate_pct and edge_ok:
return "YES", "YES_REVERSION_CONFIRMED" if not edge_ok:
return "NO", "NO_NO_DIRECTIONAL_EDGE"
return "NO", "NO_NO_REVERSION_30S"

ここには、以前のような「60秒だけ良いから HOLD」や「relation が弱いけど pending」みたいな逃げ道はありません。今回の目的が「今この条件で構造があるのか」を返すことだったので、そこは意図的に削っています。

品質ゲートも、今回かなり整理しました。以前のように confidence を重ねて複雑に見るのではなく、J-Reversion 側では stale_flag、signed bps の finite 判定、そして 10 秒以内に内部更新が継続しているかだけを使って quality_ok を作っています。内部更新チェックも _internal_update_ok() として切り出されていて、時系列が逆行していないことまで確認しています。要するに今回の quality は「うまく見えているか」ではなく、「止まっていないか・壊れていないか」を見る最低限のゲートです。

ダッシュボード側も、この実装変更に合わせてかなり削りました。J-Reversion セクションは [J-Reversion Minimal] Verdict by DirectionFinal Verdict OR + ReasonGate SnapshotEvidence (30s) の4パネルへ整理されていて、方向別の state、最終 OR 判定、判定前提、30 秒 hit rate と sample だけを見る形になっています。以前のように horizon 感度や補助パネルを並べて読む構成ではなく、まず verdict を見て、次に reason と gate を見る 順番に固定したわけです。ここでも、情報を増やしたというより、読む順序を固定したという方が近いと思います。

つまり今回の実装変更は、新しい賢いロジックをたくさん足したというより、判定器として必要な形に整え直した、という性格が強いです。within-window 判定で「30秒内に一度でも戻ったか」を拾えるようにし、その上で J-Reversion の最終出力は YES か NO に倒す。そうすることで、少なくとも 今の定義では構造があるのかないのか を、かなり低コストで読めるようになったと思っています。

5. スクショで見る現在地:今この条件では「構造なし」が分かりやすくなった

今回の実装でいちばん良かったのは、何か新しい構造が見つかったことではありません。むしろ逆で、今の条件では歪み回帰構造が立っていないということを、かなり低コストで読めるようになったことです。これは地味に見えて、観測機としてはかなり大きな前進でした。以前のように補助パネルや中間判定が多い状態だと、「まだ分からない」「もう少し読めば何かあるかもしれない」に逃げやすくなります。今回の Minimal 構成では、まず verdict を見て、次に reason と gate を見るだけで、どこで落ちているのかがかなり明確になります。

まず、[J-Reversion Minimal] Verdict by Direction の時点で、long / short ともに NO です。つまり、方向別に見ても、現時点ではどちらにも歪み回帰構造ありとは言っていません。

さらに Final Verdict OR + Reason でも最終判定は NO で、long / short の reason はどちらも NO_NO_RELATION になっています。ここで重要なのは、「単にダメだった」ではなく、relation が足りないから NO なのだと理由まで一発で見えることです。今回の判定器は、YES/NO を返すだけでなく、最低限どこで落ちたのかまで読めるようにしてあるので、少なくとも“壊れているのか”“条件が弱いのか”を混同しにくくなりました。

次に Gate Snapshot を見ると、ready_both_dirquality_both_dircorr_ready_60s はすべて 1.00 で通っています。corr_points_60s も 289 あるので、サンプル不足で相関が計算できていないわけではありません。つまり、データ取得が死んでいるわけでも、内部更新が止まっているわけでも、相関計算の準備ができていないわけでもない、ということです。一方で abs_corr_60s は 0.01 しかなく、ここが明確に赤で落ちています。今回の relation 条件は relation_min_abs_corr: 0.15 なので、この時点で 「品質は通っているが、2市場が回帰判定の土台になるほど連動していない」 と読めます。設定上も J-Reversion は corr_min_points: 20relation_min_abs_corr: 0.15 を使っており、相関の準備ができた上で最低限の連動性がある局面だけを対象にする設計です。

Evidence (30s) も、この読みを補強しています。short 側の hit_rate_30s_pct は 100% に張り付いていますが、short_samples_30s はまだ 10 本しかありません。long 側は sample も 0 のままです。ここで見えているのは、「一部ではたまたま 30 秒内回帰が起きている」ことではあっても、それを構造ありと呼べるだけの母数には全く達していないということです。設定では min_samples_main: 80min_distortion_events_total: 10 が入っているので、数本当たったこと自体では YES になりません。今回のスクショは、まさにその設計意図どおりの状態を示しています。目先の 100% に引っ張られず、母数と relation の両方を通ったときだけ構造ありと言う。その意味で、今回の判定器はかなり健全に動いています。

つまり、今のスクショから言えることはかなりはっきりしています。少なくとも現時点では、**「歪み回帰がない」のではなく、「歪み回帰構造があると言えるだけの relation が立っていない」**ということです。しかもこれは quality 不足や ready 不足ではなく、relation 条件で落ちていると明示されています。以前なら、こういう状態でも「もう少し見れば拾えるかも」と曖昧に解釈していたかもしれません。今回はそこを NO と返してくれるので、今やるべきこともはっきりします。つまり、コードが壊れていないかを疑う段階ではなく、時間幅や歪み定義、あるいは relation の土台そのものを動かして、別の条件下で構造が立つかを見に行く段階に入った、ということです。

今回のスクショは、派手さはありませんが、観測機としてはかなり良い状態です。YES が出ていないこと自体よりも、なぜ YES でないのかが分かること、そして 「今はない」を素直に返せること の方が重要でした。そこが、今回の Minimal 化でいちばん見えるようになった部分だと思っています。

6. 今見えていることと、まだ見えていないこと

今回の実装とスクショから、少なくとも一つはかなり明確に言えるようになりました。今の条件では、bf_fx × binance_perp の歪み回帰構造を「ある」とは言えないということです。しかもその理由は、quality 不足でも ready 不足でもなく、relation が足りていないからです。Gate Snapshot では ready_both_dirquality_both_dircorr_ready_60s は通っていて、corr_points_60s も十分あります。それでも abs_corr_60s は 0.01 しかなく、設定している relation_min_abs_corr: 0.15 を大きく下回っています。つまり今見えているのは、「観測機が壊れている」のではなく、「少なくともこの時間幅・この歪み定義・この relation 条件では、回帰判定の土台になる連動性が立っていない」という事実です。

もう一つ見えているのは、NO の意味がかなり具体化されたことです。以前なら NO や HOLD が出ても、それが quality の問題なのか、母数不足なのか、relation の弱さなのかを追加で読みに行く必要がありました。今は Final Verdict OR + ReasonNO_NO_RELATION がそのまま出るので、少なくとも今回の NO は「構造が弱い」というより、「判定対象として扱えるだけの市場連動がない」という意味だと読めます。これは地味ですが大きいです。観測機の役割が「全部を説明すること」ではなく、「どこで落ちているかをはっきりさせること」に近づいたからです。

さらに、Evidence (30s) からは、サンプルがまだ全然足りていないことも見えています。short 側では hit_rate_30s_pct が 100% に見えていても、short_samples_30s はまだ 10 本です。long 側はサンプル 0 のままです。設定では min_samples_main: 80min_distortion_events_total: 10 を要求しているので、この段階で何か期待値があると読むのは危険です。ここから分かるのは、今のところ「部分的に window 内回帰は起きているかもしれない」が、「それを構造と呼べるだけの分布や母数はまだ見えていない」ということです。つまり、現時点では“兆し”と“構造”を混同しなくて済むようになったとも言えます。

一方で、まだ見えていないこともかなり多いです。まず大きいのは、30 秒という時間幅自体が妥当なのかどうかです。今回の主判定は horizon_main_sec: 30 ですが、これはまだ仮置きに近い値です。もし実際の歪み回帰がもっと短い時間で終わるなら、30 秒で見た relation や hit rate は薄まって見えるかもしれません。逆に、もっと長い時間でじわじわ戻る構造なら、30 秒では早すぎる可能性もあります。今見えているのは、あくまで「30 秒で切ったときには relation が弱い」ということだけであって、歪み回帰という現象そのものが存在しないとまではまだ言えません。

次に、歪みの定義そのものが今の市場に合っているのかもまだ見えていません。今回は premium_exec_best_bps を使い、しかも threshold_bps: 10 以上を歪み発生条件にしています。これは「実行可能性を優先し、後段でノイズを膨らませない」という意味では合理的ですが、その一方で、今の市場で実際に構造が出るのがもっと大きい帯なのか、あるいは逆に 10bps ではまだ微妙すぎるのかは分かっていません。pair_premium_eval 側には 5/10/15/20/30/40/50bps の観測設定がすでにあるので、本当はこの閾値帯ごとに relation や回帰率の分布を見ないと、「10bps で構造なし」という結論の意味も固まりません。いま見えているのは、10bps を起点にした土俵では、まだ YES を返せる状態ではないということまでです。

さらに、relation 指標そのものも、まだ十分に検証しきれていません。現状の relation は base_midref_mid_jpy の対数リターン相関を 60 秒ローリングで取り、その absolute 値が 0.15 以上かどうかを見ています。これは「最低限、2市場が同じ方向に動く局面だけで回帰判定をしたい」という土台としては分かりやすいですが、逆に言うと この relation が弱いからといって、歪み回帰構造が存在しないと即断してよいか はまだ別問題です。たとえば、もっと短い窓で見た方が relation が立つのか、相関ではなく別の連動性指標の方が良いのか、という論点は残っています。今はまず最小構成で relation を一つに固定しただけで、relation の設計自体が最適化されたわけではありません。

要するに、今回見えるようになったのは、「今この条件では構造ありとは言えない」「しかもその主因は relation 不足である」ということです。一方で、まだ見えていないのは、「どの時間幅なら relation が立つのか」「どの歪み閾値なら構造が出るのか」「今の relation 指標は本当に最低限として適切か」という、次の探索の核心部分です。だから今の段階でやるべきなのは、NO を見て落ち込むことではなく、どの軸を動かせばこの NO の意味が変わるのかを、順番を決めて試していくことだと思っています。

7. 次に試すこと:時間幅・閾値・回帰定義を一軸ずつ動かしていく

今回の実装で、少なくとも「今の条件では歪み回帰構造ありとは言えない」ということは、かなり分かりやすくなりました。だから次にやるべきことは、ここで慌てて判定器を複雑にすることではありません。むしろ逆で、何を動かした結果として YES / NO が変わったのかを追えるように、一回の検証で動かす軸を一つに限定することが重要だと思っています。

最初に触るべきなのは、やはり時間幅です。今回の主判定は 30 秒ですが、これはまだ仮置きに近い値です。歪み回帰がもっと短い時間で完了するなら、30 秒という窓は長すぎて、構造が薄まって見えている可能性があります。逆に、もっとゆっくり戻る現象なら、30 秒では早すぎて取りこぼしているかもしれません。だから次はまず、歪みの定義や relation 条件には手を入れずに、10 秒、30 秒、60 秒、場合によってはそれ以上という形で時間幅だけを動かし、どこで relation や hit rate が立ちやすいのかを見にいくのが自然です。

その次に触るべきなのが、歪み発生の閾値です。今回は 10bps を起点にしましたが、これも最適値として採用したわけではなく、最小構成を動かすための暫定値です。もしこの帯域がまだ細かすぎて HFT と殴り合う領域に近いなら、そもそも構造が見えにくいのは当然ですし、逆に大きすぎる閾値の方が「明確に歪んだあとに戻る」場面を拾いやすい可能性もあります。だから時間幅の次は、10bps、15bps、20bps、30bps… のように閾値だけを動かして、どの帯域で歪み回帰の分布が変わるのかを見たいです。ここでも、時間幅まで同時に動かしてしまうと、何が効いたのか分からなくなるので避けます。

そのあとで初めて、回帰定義そのものに触れる段階に入ると思っています。今回の実装では、回帰 hit を「horizon 時点でどうだったか」ではなく、「時間内に一度でも元の方向へ戻ったか」で見ています。これは最小構成としてはかなり妥当ですが、回帰をこれで定義し切ってよいかはまだ別問題です。たとえば、「一度でも戻ればよい」のか、「一定以上戻ったこと」を要求するのか、「戻ったあとに維持されたか」まで見るのかで、見える構造は変わります。ただし、ここを先に触ってしまうと、時間幅や閾値の問題と混ざってしまうので、回帰定義の変更は三番目以降に置くつもりです。

この順番で進める理由は単純で、今回は“賢いモデル”を作りたいのではなく、どの条件下で歪み回帰構造が立つのか、立たないのかを事実ベースで切り分けたいからです。一回の検証で複数の軸を動かすと、結果が良くても悪くても原因が分からなくなります。観測機を最小構成に絞った意味も薄れてしまいます。だからここから先も、実装を増やすより先に、まずは一軸ずつ動かし、そのたびに YES / NO と reason がどう変わるかを見る、という進め方を崩さないつもりです。

要するに、次のフェーズは「より高度な判定器を作る」ことではありません。まずは、時間幅を変えると何が変わるのか、閾値を変えると何が変わるのか、回帰定義を変えると何が変わるのかを、一つずつ観測していくことです。今回ようやく「今はない」を返せる装置になったので、ここから先はその装置を使って、どの条件なら「ある」に変わるのかを順番に探していく段階に入ったのだと思っています。

-Bot, CEX, 開発ログ