こんにちは、ぼっちbotterよだかです。
前回は、J-Reversion の後段に 30 秒の価格プローブをつなぎ、歪み回帰が bitFlyer FX の executable 優位に本当に結びつくのかを観測し始めました。ただ、その作業を進めるほど、今の J-Reversion には「構造を見たい判定器」のはずなのに、執行寄りの条件や解釈が前段に混ざっている、という違和感も強くなってきました。そこで今回は、歪み回帰をいきなり「取れるシグナル」として磨くのではなく、まずは構造判定と執行判定を分け、J-Reversion を構造観測器として立て直す方向へ舵を切りました。主系列を exec から mid へ寄せ、Structure Probe と専用ダッシュボードも切り出したことで、「何が起きているか」と「それをどう使うか」を少しずつ分けて読める状態ができ始めています。

-
-
🛠️開発記録#489(2026/3/19)multi_market_probe開発ログ ― 歪み回帰が本当にbf_fxで有利に働くのかを、価格ベースで見始めた話
続きを見る
1. 今回のゴールと問題意識:構造判定の中に、執行寄りの条件が混ざっていた
今回のゴールは、J-Reversion をもっと賢くすることではありませんでした。やりたかったのはむしろその逆で、いったん役割を絞り直すことでした。前回は、J-Reversion の後段に 30 秒の価格プローブをつなぎ、YES / NO のあとに bitFlyer FX の executable がどう反応するのかを見られるようにしました。ただ、その観測を進めるほど、前段の J-Reversion 自体が「構造を見たい判定器」なのか、「執行寄りの条件まで含めて判断する器」なのかが少し曖昧になっている、という違和感が強くなってきました。今回まずやりたかったのは、この混線をほどくことです。
現状の J-Reversion は、設定の名前やダッシュボード上の見え方だけ見ると、かなり「構造判定器」っぽく見えます。実際、構造専用ダッシュボード側でも [J-Reversion Minimal] Gate Snapshot や [J-Reversion Minimal] Final Verdict OR + Reason といった形で、relation、samples、hit_rate、reason をまとめて読めるようにしてあります。見た目としては、「この条件で歪み回帰構造があるのかないのかを返す最小判定器」にかなり近いです。
ただ、実装の中身を見ると、完全にそうとも言い切れません。たとえば _JReversionStructureTracker.on_spread_sample() では、quality 判定に stale や内部更新時刻の整合性を使い、そのうえで return corr の ready / points / corr 値から relation を見て、そこを通ったものだけを structure 側の白黒判定へ流しています。ここでやっていること自体は不自然ではないのですが、少なくとも「純粋な価格関係の構造だけを見ている」というよりは、「構造候補を、運用品質や関係性のゲートを通したうえで見る」形になっていました。つまり、構造の有無と、その構造を読むための前提条件が、同じ器の中にかなり近い距離で入っていたわけです。
さらに気になったのは、歪みと回帰を観測する主系列の置き方です。今回の修正後の _PairPremiumForwardTracker は「mid spread ベースの distortion / reversion」を追う構造に変わっていますが、逆に言えば、それ以前は歪み回帰の中心に executable 系の情報がかなり強く入っていた、ということでもあります。執行可能差を使うこと自体は、後段で「本当に取れるか」を見るなら正しいです。ただ、今回やりたいのは、まず 2 市場間に歪み回帰構造があるのかないのかを構造レベルで観測することでした。そこに executable の条件が前段から混ざりすぎると、「構造がない」のか「取れないだけ」なのかが分かりにくくなります。
この問題意識は、価格プローブの設定を見直しているときにかなりはっきりしました。今回の設定では、price_probe 側に「mid閾値到達アンカー方式」「母集団は判定ゲート非依存」「anchor_state=ALL を一次集計、YES/NO は参考分類」と明記しています。しかも require_ready_quality: false にしてあり、少なくとも Probe 側では「判定器が自分で通したサンプルだけ」を見るのではなく、「まず生のアンカー母集団を持つ」方向へ寄せています。これは単なる実装テクニックではなく、何を前段で見て、何を後段で見るかを整理し直した結果でした。
実際、_JReversionPriceProbeTracker の docstring も今はかなりはっきりしていて、Price-response probe anchored by raw mid-distortion threshold touches. となっています。さらに内部では ALL / YES / NO の3系列を持ち、ALL を一次母集団として先に確定させるようになっています。つまり今回の実装では、構造判定器が返した YES / NO をそのまま世界の事実とみなすのではなく、その手前に「まず mid 閾値に到達した生のイベント群がある」という構図に戻そうとしていました。今回の記事でいちばん言いたい問題意識は、まさにここです。J-Reversion を使って何かを見たいなら、その前に J-Reversion 自体が見ているものを整理しなければいけない、ということでした。
だから今回は、いきなり「YES の精度を上げる」方向には行きませんでした。やったのはむしろ、構造判定の器に混ざっていた執行寄りの論点を一度外へ逃がし、J-Reversion をまず構造観測器として立て直すことです。今回のゴールは、歪み回帰をすぐに取れるシグナルへ昇格させることではありません。まずは、「2市場間の歪み回帰を、構造として本当に見ているのか」を、自分で納得できる形に戻すことでした。そこが曖昧なままでは、後段の executable をどれだけ丁寧に見ても、前提ごと濁ってしまうからです。
2. ここで言う歪み回帰とは何か:歪みと回帰をいったん定義し直した
今回の作業で最初にやったのは、実装をいじることではなく、まず自分が何を見たいのかを言葉で定義し直すことでした。前章で書いた通り、今回の違和感は「構造を見たいはずなのに、執行寄りの条件が前段に混ざっているのではないか」というものでした。こういう違和感が出たときに、そのままコードへ入ってしまうと、たいていは配線だけを直して終わります。でも今回は、配線より前に「そもそもここで言う歪みとは何か」「回帰とは何を指しているのか」をいったん自分の中で固定する必要がありました。手書きメモでも、最初にこの定義を曖昧なままにしないことをかなり意識していました。

今回ここで言う「歪み」は、かなり素直に置いています。2市場間の価格差、もっと言えば bf_fx と binance_perp のあいだで観測される乖離幅です。重要なのは、これをいきなり「取れる鞘」と呼ばないことでした。取れるかどうかは後段で決まる話であって、この段階ではまだ「2市場の価格関係がどれだけ開いているか」という観測対象として置いています。言い換えると、ここで見たいのは市場間の力学そのものであって、注文可能性やコスト控除後の収益性ではありません。まず必要なのは、「2市場間で一定以上の乖離が発生している」という事実を、構造として観測できることでした。
そのうえで「回帰」は、その乖離が将来に向かって縮小する動きです。今回かなり大事だったのは、ここを「価格が戻ること」ではなく、「歪みとして観測されたズレがゼロ方向へ寄ること」として置いたことです。たとえば base と ref の価格がそれぞれ上がっていても、2市場間の差が縮んでいれば、それは歪みの回帰です。逆に、片方だけが動いて nominal price としては大きく変化していても、2市場間の差が広がるなら、それは回帰ではありません。つまり今回見ているのは絶対的な価格方向ではなく、あくまで市場間の関係そのものです。この視点を先に固定しておかないと、途中から「上がった・下がった」と「戻った・戻っていない」が混ざりやすくなります。
ここで重要なのは、歪み回帰を binance が先で bf_fx が後、という因果方向の話とまだ同一視しないことでした。感覚的には、「binance_perp を先行市場と想定し、その歪みが bf_fx 側へどう返ってくるか」を見たくなるのですが、これは本来 lead-lag に近い問いです。今回やりたいのは、その一段手前で「そもそも 2 市場間に、乖離が発生してから縮小する構造が存在するのか」を見ることでした。つまりこの段階では、「どちらが先に動いたか」まではまだ問わない。まずは「差が開いて、後で縮むという流れが統計的に存在するか」を見る。方向因果を混ぜないことで、最初の構造判定をできるだけ単純に保つ意図がありました。
実装上の定義も、この考え方に沿う形に寄せています。今回の _PairPremiumForwardTracker は docstring からして Track pair distortion persistence/reversion from mid spread for a fixed pair. に変わっていて、入力にも premium_mid_bps / spread_bps_mid を使うようになっています。つまり、いま forward 系で見ている歪みは「mid ベースの市場間乖離幅」です。ここではまだ fee も slippage も引いていませんし、より有利な約定経路を取る exec_best 的な選び方も入れていません。まず構造を見るために、主系列をいったん mid に戻しています。今回の「歪み」の定義は、まさにこの系列に対応しています。
一方で、「回帰」の実装は将来時点での縮小として置いています。アンカー時点で一定以上の歪みが発生したら、その後の horizon 内で絶対値が縮んだかを見る。かなり素直な定義ですが、今回の段階ではこれで十分でした。ここで大事なのは、回帰をいきなり「ゼロへ完全に戻ること」や「反対側まで抜けること」にしなかった点です。まず見たいのは、歪みとして観測された価格差が、将来に向かって縮小する傾向を持つかどうかです。完全な解消や十分条件まで最初から求めると、構造の有無を観測する前にハードルが高くなりすぎます。今回の回帰定義は、あくまで最初の存在判定のための最低限のものとして置いています。
この定義を先に言葉にしておいたことで、今回の実装変更の意味もかなりはっきりしました。もし歪みを「取れるエッジ」として定義してしまうなら、最初から executable やスプレッド、手数料、entry/exit の持ち方まで前段に混ぜるのが正しいかもしれません。でも今回はそうしませんでした。歪みはまず市場間の乖離幅、回帰はその縮小。そこまでは構造の話であって、後段で初めて「その間に bf_fx で入って抜けても利益が残るか」という執行判定へ進む。この順番を守るために、今回わざわざ定義をいったん自分の言葉に戻しました。
要するに今回確定させたかったのは、歪み回帰を「市場間価格差の発生と、その後の縮小」としてまず観測する、という立場です。ここではまだ、それが取れるかどうかも、どちらが先行市場かも、主戦場でどれだけ利益化できるかも決めません。最初に置くのはあくまで、「2市場間にそういう構造があるのか」という問いです。今回の改修全体は、この定義に合わせて前段を整理し直す作業でした。
3. 実装変更①:歪み・回帰の主系列を exec から mid へ寄せた

今回の改修で最初に手を入れたのは、歪みと回帰を追う主系列です。前章で整理した通り、今回まず見たいのは「この2市場間に歪み回帰構造があるのかどうか」であって、「今この瞬間に取れるかどうか」ではありません。ところが、以前の forward 系は exec 系の系列にかなり寄っていました。実行可能差は実運用では重要ですが、その中には bid / ask、スプレッド、経路選択、板形状といった執行寄りの条件が最初から混ざります。構造の有無を先に見たい段階では、この情報量の多さがかえって解釈を濁らせます。そこで今回、歪み・回帰の主系列を exec から mid へ寄せました。
その変更がいちばん分かりやすく出ているのが _PairPremiumForwardTracker です。まずクラスの docstring 自体を、今回の責務に合わせて書き換えています。
class _PairPremiumForwardTracker:
"""Track pair distortion persistence/reversion from mid spread for a fixed pair."""
この1行がかなり象徴的です。ここで言っている通り、この tracker はもう「exec ベースで有利な差を追うもの」ではなく、「mid spread から distortion と reversion を追うもの」として置き直されています。今回の3章で言いたいことは、まずこの責務変更そのものです。forward 系が何を見ているのかを、クラスの役割からして構造寄りに戻しました。
入力の取り方もかなり変わっています。いまのコードでは、歪み量の主系列として premium_mid_bps を優先し、fallback で spread_bps_mid を使うようになっています。
premium_mid_signed_bps = self._safe_float(
sample.get("premium_mid_bps", sample.get("spread_bps_mid", 0.0)),
)
premium_mid_abs_bps = abs(premium_mid_signed_bps)
ここで重要なのは、構造判定の入口に置く歪み量が、もう premium_exec_best_bps ではないことです。以前の exec ベース系列では、「どちらの経路で抜けると有利か」という執行寄りの情報が最初から入っていました。今回そこをやめて、まずは「2市場間の mid 差がどれだけ開いているか」を前段の歪み量として採用しています。つまり今回の forward 系は、取れる差を見ているのではなく、まず市場間の価格関係そのものを見ています。
この変更は、単に入力値を差し替えただけではありません。アンカー生成も mid ベースへ揃えています。歪みイベントを開く条件は、いまは次のようになっています。
need_min_track = bool(
or self._anchors_dir
or premium_mid_abs_bps >= self.threshold_bps
)if premium_mid_abs_bps >= self.threshold_bps:
direction = self._direction_from_signed(premium_mid_signed_bps)
self._anchors.append((ts_ms, premium_mid_abs_bps))
if direction:
self._anchors_dir.append((ts_ms, premium_mid_signed_bps, direction))
ここでやっているのはかなり素直で、「mid ベースの歪み絶対値が threshold を超えたらアンカーを積む」というだけです。つまり、どの時点を「歪み発生」と数えるかという定義も、今回から mid 系列に統一されています。ここが大きいです。回帰判定の計算だけを mid にして、アンカーは昔の exec 条件のままだと、入口の時点でまだ執行寄りの選別が残ってしまいます。今回はそこまで含めて、前段の土台を mid 側へ寄せました。
その後の future 解決も、同じ文脈で整理されています。_resolve_future() はいま premium_abs_bps を受け取るようになっていて、アンカー時点との差分をそのまま積んでいます。
def _resolve_future(self, *, ts_ms: int, premium_abs_bps: float) -> None:
...
delta = premium_abs_bps - anchor_premium
self._future_delta_latest = float(delta)
...
min_abs_seen = abs(premium_abs_bps)
方向付きの _resolve_future_directional() も同様で、signed な mid 差の変化量をそのまま見ています。
def _resolve_future_directional(self, *, ts_ms: int, premium_signed_bps: float) -> None:
...
delta = float(premium_signed_bps - anchor_signed_bps)
self._future_dir_delta_latest[direction] = delta
self._future_dir_delta_sum[direction] += delta
ここで重要なのは、回帰の定義自体は大きく変えていないことです。見ているのは「将来時点で歪みの絶対値が縮んだか」「signed な差分がゼロ方向へ寄ったか」という、かなり素直なものです。ただ、その対象がもう exec ではなく mid である、という点が今回の改修の本質です。つまり Forward の hit_rate や delta は、現在は「実行可能差が改善したか」ではなく、「市場間の価格差が縮んだか」を表すものとして読み直せるようになっています。
この変更に合わせて、J-Reversion 側が参照する signed spread も mid ベースへ寄せています。実際、_JReversionStructureTracker.on_spread_sample() の入口でも、いまは次のように読んでいます。
signed_bps = self._safe_float(
sample.get("premium_mid_bps", sample.get("spread_bps_mid", 0.0)),
0.0,
)
ここも地味ですが大事です。前段の forward facts が mid になっていても、J-Reversion 本体が見る現在値が exec のままだと、structure 判定の内部でまた執行寄りの系列が混ざります。今回はそこも揃えています。つまり少なくとも J-Reversion の構造判定経路に入る「歪みの現在値」は、前よりかなり素直に mid ベースと読めるようになりました。
もちろん、この変更で万能になるわけではありません。mid ベースにしたことで、今度は逆に「mid では回帰しているように見えるが、bf_fx の entry_ask → future_bid では負ける」というズレがはっきり出てきます。実際、前回から見えていた問題はそこでした。ただ、今回のフェーズではそのズレこそ後段で観測したいので、前段から executable を混ぜ込まない方が筋がいいと判断しています。構造判定の時点で executable まで抱え込むと、「構造が無い」のか「構造はあるけど取れない」のかが潰れてしまうからです。今回はその二つを分けるために、あえて主系列を mid に戻しました。
設定側も、この変更に合わせてそのまま使えるようになっています。pair_premium_eval は threshold 10bps、main horizon 30 秒を維持したまま、60 / 90 / 120 秒も additional pairs として回しています。つまり今回やったのは「threshold を最適化した」とか「horizon をいじった」とかではありません。同じ 10bps / 30s / 60s / 90s / 120s の枠組みの中で、何を歪みとして見るのか を整理し直しただけです。チューニングを先送りにして、まず系列の意味を揃えた、という順番でした。
要するに今回の実装変更①でやったのは、歪み回帰を「実行可能差の改善」としてではなく、「市場間の mid 差の発生と縮小」として捉え直すことでした。そのために _PairPremiumForwardTracker の入力、アンカー、future 解決を mid ベースへ寄せ、J-Reversion 本体も同じ系列で current spread を読むように揃えました。まだこれで構造があると証明できたわけではありませんし、主戦場で利益化できるとも限りません。ただ少なくとも、前段の構造判定器が何を見ているのかは、今回の変更でかなり明確になったと思っています。
4. 実装変更②:Structure Probe と構造判定専用ダッシュボードを作り直した

前章では、歪みと回帰の主系列を exec から mid へ寄せた話を書きました。ただ、それだけではまだ十分ではありません。系列を mid ベースに変えても、どのイベントを観測対象として数えるかが判定器自身の state 遷移や ready / quality 条件に依存していると、「市場で起きた事実」を見ているのか、「判定器が通した事実だけ」を見ているのかが曖昧なまま残るからです。今回そこをもう一段整理するために、Structure Probe のアンカー起点そのものを作り直し、Grafana 側もそれに合わせて切り直しました。
まず象徴的なのが、価格プローブ側のクラス定義です。今回の _JReversionPriceProbeTracker は、冒頭の docstring からして、以前とかなり意味が変わっています。
class _JReversionPriceProbeTracker:
"""Price-response probe anchored by raw mid-distortion threshold touches."""
ここで明示している通り、今回の Probe は J-Reversion の状態遷移にアンカーする装置 ではなく、raw な mid 歪みの閾値到達にアンカーする装置 です。つまり主語は「判定器が YES / NO を返した瞬間」ではなく、「市場間の mid 差が threshold を跨いだ瞬間」に置き換わっています。今回の修正のいちばん大きい部分は、まさにこの主語の変更でした。
この方針は設定ファイルにもかなりそのまま出ています。config.yaml 側の price_probe コメントはいま、次のように書かれています。
# J-Reversion価格応答プローブ(mid閾値到達アンカー方式, 30s)
# - アンカー起点: spread_bps_mid(abs) が threshold_bps を跨いだ時点
# - 母集団は判定ゲート非依存(ready/quality/state遷移で除外しない)
# - anchor_state=ALL を一次集計、YES/NO は参考分類
# - directional executable は理論値(fee/slippage控除なし)
price_probe:
enabled: true
main_horizon_sec: 30
timeout_sec: 30
touch_thresholds_bps: [1, 3, 5, 7, 10]
snapshot_seconds: [1, 3, 5, 10, 15, 30]
require_ready_quality: false
max_anchor_price_lag_sec: 2.0
max_pending_anchors: 20000
今回かなり重要なのは、ここで require_ready_quality: false にしていることです。以前のように ready / quality をアンカー採用条件にしてしまうと、Probe 自体がすでに「判定器が通したイベントだけ」を見に行く構造になります。今回はそこを外し、「まず市場で起きた歪みイベントを拾う。そのうえで、YES / NO は参考ラベルとして重ねる」という順番に戻しました。これによって、Structure Probe はかなり“生データ寄り”の観測器になっています。
実際のアンカー生成は on_spread_sample() で行っています。ここでやっていることはかなり素直で、mid ベースの signed spread を取り、threshold を跨いだ瞬間だけアンカーを打つようにしています。
signed_mid_bps = self._safe_float(
sample.get("premium_mid_bps", sample.get("spread_bps_mid", 0.0)),
0.0,
)
direction = self._direction_from_signed(signed_mid_bps)
above = bool(direction) and (abs(signed_mid_bps) >= self.threshold_bps)crossed = False
if above:
crossed = (not self._prev_above_threshold) or (direction != self._prev_direction)
self._prev_above_threshold = True
self._prev_direction = direction
else:
self._prev_above_threshold = False
self._prev_direction = ""if not crossed:
return
ここで大事なのは二つあります。ひとつは、使っている系列が premium_mid_bps / spread_bps_mid であること。もうひとつは、アンカー条件が「threshold を跨いだ瞬間」になっていることです。つまり今回の Structure Probe は、J-Reversion の state 更新を待たずに、mid の歪みイベントそのものを観測対象として持ちます。そのうえで、threshold 付近で出入りを繰り返して同種イベントを過剰カウントしないよう、_prev_above_threshold と _prev_direction で簡単なヒステリシスを持たせています。
ただし、今回の修正が面白いのは「判定結果を完全に捨てた」わけではない点です。YES / NO は消していません。代わりに、それを一次母集団ではなく参考分類に落とした形にしています。その役割が見えるのが、次の部分です。
def _resolve_anchor_groups(self, *, direction: str) -> tuple[str, ...]:
groups: list[str] = ["ALL"]
state_group = self._last_state_group_by_direction.get(direction, "UNKNOWN")
if state_group in {"YES", "NO"}:
groups.append(state_group)
return tuple(groups)
ここでは必ず ALL を先に入れ、その時点で直近の state が YES か NO なら、そのラベルも追加しています。つまり集計の順番は、
- まず市場の事実として mid 歪みアンカーがある
- そのアンカーを
ALLに数える - そのとき判定器がたまたま YES / NO なら、参考的にそこにも数える
という形です。今回の設計でいちばん大事なのは、この順番です。前は「YES / NO のあとに何が起きたか」を見ていたのに対し、今は「市場で起きたことの上に、YES / NO をラベルとして重ねる」形に戻しています。これで、少なくとも Structure Probe 側は「判定器が作った世界」を見る装置ではなくなりました。
そして、集計された事実は update_multi_market_j_reversion_probe_state() に流されます。ここでは anchors 総数、directional mid / exec mean、touch 系、snapshot 系をまとめて Prometheus へ渡しています。
update_multi_market_j_reversion_probe_state(
base_market=self.base_market,
ref_market=self.ref_market,
threshold_bps=self.threshold_bps,
horizon_main_sec=float(self.horizon_main_sec),
anchor_state=str(state_group).upper(),
anchors_total=float(n),
directional_mid_bps_mean=float(mean_mid),
directional_mid_win_rate_pct=float(win_mid_pct),
directional_exec_bps_mean=float(mean_exec),
directional_exec_win_rate_pct=float(win_exec_pct),
touch_facts=touch_facts,
snapshot_facts=snapshot_facts,
)
この構造によって、Probe は単なる1本の mean だけでなく、
- 何本アンカーが確定したか
- 方向付き mid / exec が平均でどうだったか
- 1/3/5/7/10bps に一度でも届いた割合
- そこに最初に届くまで何秒かかったか
- 1 / 3 / 5 / 10 / 15 / 30 秒時点の値
まで、一括で出せるようになっています。今回やりたかったのは、「判定がどうだったか」ではなく、「そのあと何が起きたか」を事実として見ることだったので、この粒度はかなり重要でした。

Grafana 側も、この思想に合わせて切り直しています。まず、パネル名の時点でかなり分かりやすくしていて、[Structure Probe 30s] Finalized Anchors (ALL/YES/NO) という名前に変えています。
"title": "[Structure Probe 30s] Finalized Anchors (ALL/YES/NO)",
"description": "mid閾値到達アンカー方式。ALLが生母集団、YES/NOは判定状態の参考分類。"
この説明文はかなり重要です。今回のパネルは、YES / NO の差を見るためのものではありますが、それ以前に「ALL が生母集団である」とはっきり書いています。つまり読み手に対しても、「YES / NO を一次の真実だと思わないでください」というガードを最初から入れているわけです。これはただの文言変更ではなく、今回の設計思想そのものです。
Touch Any Rate と First Touch Delay も同じ思想で並べています。
"expr": "obx_mmarb_j_reversion_probe_touch_any_rate_pct{...,anchor_state=\"YES\",...}",
"legendFormat": "YES_touch{{touch_bps}}bps""expr": "obx_mmarb_j_reversion_probe_touch_any_rate_pct{...,anchor_state=\"NO\",...}",
"legendFormat": "NO_touch{{touch_bps}}bps""expr": "obx_mmarb_j_reversion_probe_touch_any_rate_pct{...,anchor_state=\"ALL\",...}",
"legendFormat": "ALL_touch{{touch_bps}}bps"
"expr": "obx_mmarb_j_reversion_probe_first_touch_sec_mean{...,anchor_state=\"YES\",...}",
"legendFormat": "YES_first_touch_{{touch_bps}}bps""expr": "obx_mmarb_j_reversion_probe_first_touch_sec_mean{...,anchor_state=\"NO\",...}",
"legendFormat": "NO_first_touch_{{touch_bps}}bps""expr": "obx_mmarb_j_reversion_probe_first_touch_sec_mean{...,anchor_state=\"ALL\",...}",
"legendFormat": "ALL_first_touch_{{touch_bps}}bps"
ここでも、YES / NO と同列に ALL を置いています。つまりダッシュボードの読解順としては、
- まず ALL で市場全体の反応を見る
- そのあと YES / NO がその上でどうズレているかを見る
が自然に入るようになっています。前のように YES / NO だけを見る構造だと、「判定器の世界の中での優劣」しか見えませんでした。今回はそこを崩して、「市場で起きたこと」と「判定器の分類」を分けて読めるようにしています。


要するに今回の実装変更②でやったのは、Structure Probe のアンカー起点、母集団、ダッシュボードの読み順をまとめて作り直すことでした。コード上では on_spread_sample() で raw mid 閾値到達イベントにアンカーし、_resolve_anchor_groups() で ALL を一次母集団として持ち、update_multi_market_j_reversion_probe_state() でその事実を Prometheus に流す。ダッシュボード上ではそれを [Structure Probe 30s] セクションとして切り出し、ALL を先に見る構成へ変えました。今回の修正でようやく、「構造判定」と「その土台になる事実観測」を、画面の上でもコードの上でも少し分けて扱えるようになったと思っています。
5. スクショで見る現在地:観測は動いているが、J-Reversion はまだ NO のまま
今回の改修を入れたあと、最初に確認したかったのは「良い数字が出ているか」ではありませんでした。まず見たかったのは、構造判定と Structure Probe の配線がちゃんと動いていて、前段と後段の役割分離が実際にダッシュボード上でも成立しているかどうかです。そこが崩れていると、どれだけもっともらしい値が並んでいても意味がありません。今回スクショを見ていくと、少なくともこの点についてはかなりクリアに確認できました。構造判定専用ダッシュボードは正常に立ち上がっていて、C / M / F / J / Structure Probe の各セクションがそれぞれ役割を持って動いています。まずここで、「今回の改修は少なくとも配線としては死んでいない」という段階には入れました。

最上段の C 系を見ると、ready 自体は立っています。Corr Points は十分にあり、サンプル不足で何も言えない状態ではありません。ただし、Return Corr の値はかなり弱く、ここがそのまま gate の弱さに直結しています。つまり現在の NO は、「何も観測できていないから NO」というより、「観測はできているが、relation を通すほど関係が強くないので NO」という性格が強いです。今回の構造判定では、少なくとも sample 不足と relation の弱さを分けて読めるようにしたかったので、この時点で Gate Snapshot の意味はかなりはっきりしました。ready なのに verdict が立たない、ということ自体が、今の現在地を表しています。

M 系の [M] Premium Mid bps も、かなり示唆的でした。ここでは premium がゼロ近傍で細かく上下しているというより、かなり大きめの正側に張り付いている時間帯が見えています。これは重要です。もし premium が常時ある程度開いたままなら、「threshold を超えたら歪み」と定義している現在のロジックでは、歪みイベントが“特別な発生”ではなく“だいたい常時ONに近い状態”になりやすいからです。つまり今のスクショで見えているのは、瞬間的に飛び出した歪みというより、もともとある程度開いた base 差の上での微小な揺り戻しなのかもしれません。この違和感は、後段の F 系を読むとさらに強くなります。
F 系では、Forward Reversion Hit Rate がかなり高く出ています。一見すると、これは強い回帰構造があるようにも見えます。ただ、同時に Forward Premium Delta Latest はかなり小さく、縮小量そのものは大きくありません。ここから読み取れるのは、「回帰方向に動いたこと自体は多いが、その動きの大きさはまだ強いとは言いにくい」ということです。今回の回帰定義は、将来時点で歪みの絶対値が少しでも縮めば hit と数えるかなり素直なものなので、hit rate の高さをそのまま「強い構造」と読むのは危険です。ここで見えているのは、少なくとも現時点では「縮みやすい方向の反応はあるが、それがどれだけ有意な回帰量なのかは別問題」という現在地でした。


この前提で J-Reversion Minimal を見ると、全部がかなり自然につながります。state は long / short ともに NO、OR も NO のままです。そして reason は NO_NOT_READY に寄っています。ただし、ここで言う NOT_READY は、単純なデータ欠損というより、「relation gate を通すだけの ready にまだなっていない」という意味合いが強い状態です。つまり今回の構造判定器は、現状の数字を見たうえで無理に YES を出していません。この点はむしろ良いと思っています。今回のゴールは YES を出すことではなく、「構造があるなら YES、言えないなら NO を返す」ようにすることだったからです。もしここで無理に YES が立っていたら、むしろそちらの方が危なかったと思います。

一方で、今回いちばん価値があったのは、J が NO のままでも Structure Probe 側では事実を別に見られるようになったことでした。下段の [Structure Probe 30s] を見ると、ALL を一次母集団として anchors、touch rate、first touch delay が立っています。つまり、判定器が NO を返しているからといって、「市場では何も起きていない」わけではありません。実際には、mid 閾値到達アンカーのあとに、一定の touch は起きているし、到達までの時間も出ています。ここが今回の改修のいちばん大きな成果です。前なら「NO だからその先は何も見えない」に近かったものが、今は「NO だが、市場事実としてはこういう反応がある」と分けて読めるようになりました。
ただし、この Structure Probe の数字もまだ読みすぎてはいけません。現時点ではアンカー数がまだ少なく、touch rate 100% のような強い数字が出ていても、統計的にはかなり軽いです。ここで言えるのは、「少なくともこのサンプルではそうだった」というところまでです。つまり、Structure Probe は正しく動いているし、今回の思想通りに生母集団を見られる状態にもなっていますが、まだ「これで構造がある」と断定できるほど母数が溜まっているわけではありません。この距離感はかなり大事です。今回の現在地は、あくまで“観測器が整った段階”であって、“結論が出た段階”ではありません。
要するに、今回スクショで確認できた現在地はかなりはっきりしています。第一に、構造判定と Structure Probe の配線は通っていて、ダッシュボードとしては意図通りに動いている。第二に、M / F 系には回帰っぽい反応が多少見えるが、それをそのまま強い構造とはまだ言えない。第三に、J-Reversion は現時点で NO のままで、しかもその NO は「何も見えていない」ではなく「relation を通すほどまだ強くない」という意味を持っている。つまり今回は、観測器をちゃんと動かし、そのうえで「まだ YES とは言えない」という現在地を、以前よりずっと誤読しにくい形で読めるようになった、というのがいちばん大きな進捗でした。
6. 見え始めた論点:回帰っぽい反応と、構造あり判定はまだ別問題
今回の改修でかなり大きかったのは、「何が見えていないのか」が前よりはっきりしたことでした。前章までで見た通り、ダッシュボード自体は動いています。Structure Probe も立っていて、mid 閾値到達アンカーのあとに touch や first touch delay も観測できています。つまり、少なくとも市場側では「何も起きていない」わけではありません。ところが、その事実がそのまま J-Reversion の YES に変わるわけではないし、さらに言えば、それがそのまま主戦場での優位に変わるわけでもありません。今回見え始めた論点は、まさにこのズレでした。
最初の論点は、Forward 側で見えている「回帰っぽい反応」をどう読むかです。スクショ上では hit rate がかなり高く見える一方で、delta の大きさはそこまで強くありません。ここから分かるのは、少なくとも「ゼロ方向に少しは寄る」という反応はありそうだ、ということです。ただし、それだけで「歪み回帰構造がある」とはまだ言えません。今回の hit 判定はかなり素直で、将来時点で絶対値が少しでも縮めば hit です。つまり、いま見えている高い hit rate は、「強く鋭く回帰する構造」を意味しているのではなく、「少しは縮む方向へ揺れやすい」という程度の事実かもしれません。
二つ目の論点は、premium 自体の水準です。今回の M 系を見ると、premium_mid_bps はゼロ近傍を行き来するというより、ある程度大きな正側に張り付いている時間帯があります。ここがかなり気になります。もしこの差が、一時的な歪みというより、取引所間の恒常的なベース差や制度差、換算のズレを含んだ水準差なのだとすると、今の threshold 10bps は「歪み発生の検出」ではなく、「すでに常時開いている状態の中での小さな揺れ」を拾っているだけかもしれません。この場合、Forward hit が高くても、それは“歪みの解消”ではなく“高いベース差の上での微小な戻り”にすぎない可能性があります。
三つ目の論点は、Structure Probe の意味です。今回の改修で、ALL を一次母集団にしたことで、「市場で起きたこと」と「判定器が何と言っているか」を切り分けて見られるようになりました。これはかなり大きな前進です。ただ、そこで見えているのはあくまで touch や delay といった事実であって、それ自体が「構造あり」の証明ではありません。たとえば 30 秒以内に 1bps や 3bps へ触れたとしても、それが偶然ではなく、しかも統計的に安定して起きているのかはまだ別問題です。今の段階で言えるのは、「判定器が NO を返していても、市場事実としてはこういう反応がある」というところまでです。ここからさらに一歩進んで、「ではその反応を構造と呼んでよいのか」を決めるには、まだ母数も足りませんし、定義の精度ももう少し必要です。
四つ目の論点は、relation の扱いです。今回の J-Reversion は NO のままで、その背景には relation gate の弱さがあります。ただ、この論点も単純ではありません。relation が弱いから NO なのか、それとも今の relation 定義が、見たい構造に対してズレているのかがまだ切れていません。return corr はたしかに関係性の粗い指標にはなりますが、歪み回帰のような現象を本当に捉えるのに十分かどうかは別です。つまり今後の課題は、「relation が弱いから構造が無い」とすぐに結論することではなく、「relation の定義が今の問いに合っているのか」を含めて考えることになります。ただし、ここで重要なのは順番です。今回ようやく price facts と structure facts の分離ができたので、relation の再設計も、次からは「気になるから直す」のではなく、「直したときに本当に後段の事実が変わるか」で比較できるようになりました。
最後に、今回かなりはっきりしたのは、「回帰っぽい反応がある」ことと「構造あり判定を出せる」ことは別だし、そのさらに先に「主戦場で取れる」ことがある、という三段の分離です。これまではどうしても、この三つが少し近すぎました。前段で回帰っぽいものが見えると、そのまま YES に寄せたくなるし、YES が立つとそのまま主戦場で取れそうに見えます。でも今回のスクショは、そこが別問題だということをかなりはっきり示しています。回帰方向の touch は起きている。けれど J-Reversion はまだ NO。しかも、それを YES にしたからといって主戦場での executable 優位が立つとは限らない。
要するに今回見え始めた論点は、どれも「構造っぽさ」と「構造判定」と「取引優位」を混同しないためのものです。Forward の hit rate は高いが、縮小量はまだ小さいかもしれない。premium は動いているが、それは一時的な歪みではなくベース差かもしれない。Structure Probe は事実を見せてくれるが、それ自体が構造証明ではない。relation gate は弱いが、それは relation の定義の問題かもしれない。今回の改修で観測器がきれいになったからこそ、こうした論点が前よりずっと見えやすくなりました。ここから先は、この論点を一つずつ雑に潰さず、事実ベースで切り分けていく段階に入ったのだと思っています。
7. 次に詰めることと今日の結論:まず構造を見て、そのあと執行を考える順番に戻した
今回の改修で、すぐに新しい優位が見つかったわけではありません。むしろ逆で、「まだ言えないこと」がかなりはっきりした、という方が実態に近い。今回やったのは、J-Reversion を無理に良く見せることではなく、まず何を見ている装置なのかを整理し直すことです。その意味では、今回の改修はかなり重要でした。少なくとも今は、「構造の有無を見たい前段」と「その構造が主戦場で取れるかを見る後段」が、以前よりだいぶ混ざりにくくなっています。
次に詰めるべきことは、おおまかに4つあります。まず一つ目は、premium の baseline 差をどう扱うかです。今回の M 系を見る限り、premium_mid_bps はゼロ近傍の一時的な歪みというより、ある程度大きな水準差を持ったまま推移しているように見える時間帯があります。この状態で threshold 10bps をそのまま使うと、「歪みが発生した」というより「もともと開いている差の上で少し揺れた」イベントを拾っている可能性があります。ここは今後かなり重要になりそうです。rolling mean を引いた residual で見るのか、あるいはまず baseline 差の存在自体を別パネルとして固定するのか。いずれにせよ、「何を歪みと呼ぶか」を、もう一段だけ厳密にする余地があります。
二つ目は、回帰の“量”をどう扱うかです。いまの Forward hit は、「絶対値が少しでも縮めば hit」というかなり素直な定義です。これは存在判定の最初の一歩としては悪くないのですが、スクショを見ていると、hit rate の高さがそのまま構造の強さに見えてしまう危険があります。今後たぶん必要になるのは、「何回縮んだか」だけではなく、「どれくらい縮んだか」をもう少し前に出すことです。たとえば median delta や 3bps / 5bps 以上の縮小率を追加するだけでも、今見えている hit の意味はかなり変わるはずです。ここを詰めることで、Forward が見せているものが「なんとなく戻りやすい」なのか、「ちゃんと回帰幅を持つ構造」なのかを切り分けやすくなると思っています。
三つ目は、relation の再設計を本当にやるべきかの判断です。今回の時点では、relation はまだ NO 側へ強く効いています。ただ、前章でも書いた通り、ここはまだ「relation が弱いから構造が無い」とは言い切れません。今の return corr ベースの relation が、そもそも見たい現象に対してズレている可能性もあります。大事なのは、ここで焦って relation をいじり始めないことだと思っています。今回ようやく Structure Probe と後段事実が切り分けられたので、relation を触るなら次は「その変更で実際に Structure Probe や price facts がどう変わるか」を同じ物差しで見られます。つまり今後の relation 再設計は、前よりかなり健全な形で比較できるはずです。
そして四つ目は、執行判定を本当に後段に押し込めることです。今回の改修で、前段はかなり構造観測寄りに戻りました。ただ、それで終わりではありません。最終的に知りたいのは、もちろん主戦場で取れるかどうかです。だから執行判定は不要なのではなく、順番を守って後ろに置くことが大事です。まず市場間の歪み回帰構造があるのかを見る。そのあと、その構造のうちどれが bf_fx で executable 優位へ変換されるのかを見る。この二段を混ぜないことが、今回の作業全体のいちばん大きな意味でした。
今回の結論を一言でまとめるなら、やはりこれだと思っています。まず構造を見て、そのあと執行を考える順番に戻した、ということです。これまでの J-Reversion は、構造判定器として立てているつもりでも、実際には執行寄りの前提や判定器自身の選別が少し近い場所にありました。今回その違和感を放置せず、mid ベースへ寄せ、Structure Probe を生母集団起点に作り直し、ダッシュボード上でも ALL を一次母集団として見られるようにしたことで、少なくとも「何を事実として見ているのか」はかなりはっきりしました。まだ YES は立っていませんし、構造ありとも言い切れません。でも、その“言い切れなさ”を前よりも正しく読めるようになったこと自体が、今回の進捗だったと思っています。