「AIに株の売買を任せたらどうなるか」は、多くの人が一度は考えるテーマだと思います。そこで今回、TypeSafe の汎用判断モデル Jev に米国株ETFの SPY を 1秒ごと に判断させ、その判断どおりに 模擬売買(シャドートレード) する仕組みを Python で作って試しました。
3行まとめ
- 約4時間で 14,285回 判断させ、模擬売買は 11往復。結果は 1勝1分け9敗、−1.68ドル(1株ずつの売買。実際の注文は出していません)。一晩でこれだけ負けたって理解でOKです。
- 損失の約6割(0.97ドル)は買値と売値の差(スプレッド)。残る値動きの部分も「同じ時間だけ株を持っていた場合」とほぼ同じで、この設定では予測力の証拠は見つかりませんでした
- 一番の収穫は、AIに数値を渡すときの 書き方 で判断が偏るとわかったこと。
7.8e-05を+0.78 bpと書き直すだけで、下落側への偏りが −12.7 → −1.7ポイント に縮みました
最初にお断り
- 投資助言ではありません。 特定の銘柄や売買を勧めるものでもありません。
- 今回の実験は 2セッション分だけ です。1秒モードでは実際の注文を一切出していません。模擬約定は IEX という1つの取引所の気配値で計算しており、実際の売買より有利な結果が出やすい条件です。
- Jev は相場予測のために作られたモデルではなく、今回はあえて 本来の用途の外 で使っています。「Jev は役に立たない」という話ではなく、「この使い方・この設定では、腕前の証拠が出なかった」 という話として読んでください。
1. 何をやったか
| 項目 | 内容 |
|---|---|
| 銘柄 | SPY(S&P 500 に連動するETF。米国株全体の値動きの代表格) |
| データ | Alpaca の無料リアルタイムデータ(IEX 取引所の気配と約定のみ) |
| 判断 | TypeSafe の Jev に、毎秒「この先30〜120秒で上がりそうか、下がりそうか、どちらでもないか」を確率で答えてもらう(2回目の設定。1回目は10〜60秒) |
| 売買 | Jev の答えをコードの 固定ルール に通し、模擬約定(その瞬間の気配値で売買したことにして帳簿をつける) |
| お金 | 一切動かない。注文も出さない |
実際に回したのは次の2セッションです。
| 1回目(v1) | 2回目(v2) | |
|---|---|---|
| 日時(米東部) | 2026/10/20 13:00〜15:50 | 2026/10/23 11:50〜15:50 |
| 日時(日本時間) | 10/21 2:00〜4:50 | 10/24 0:50〜4:50 |
| 長さ | 約169分 | 約240分 |
| Jev のバージョン | 1.13.0 | 1.14.0 |
| 判断回数 | 10,146回 | 14,285回 |
| 判断の内訳(下落/中立/上昇) | 39% / 55% / 6% | 40% / 35% / 25% |
| 模擬売買 | 0回 | 11往復 |
| 模擬損益 | 0ドル | −1.68ドル |
1回目は1万回判断して取引ゼロ。そこから原因を調べて作り直したのが2回目です。以下、その経緯を順に書いていきます。
2. この記事に出てくる用語
金融の用語がいくつか出てくるので、先にまとめておきます。
| 用語 | 一言でいうと |
|---|---|
| bp(ベーシスポイント) | 0.01%のこと。SPY が約760ドルなら、1bp は約7.6セント |
| bid / ask | bid は買い手が出している最高値、ask は売り手が出している最安値。すぐ買うなら ask、すぐ売るなら bid で約定する |
| スプレッド | ask と bid の差。すぐ買ってすぐ売ると、それだけで毎回この差の分だけ損をする |
| mid | bid と ask のちょうど真ん中の値 |
| IEX | 米国に数多くある株式取引所の1つ。無料データでは IEX の分しか見えないため、市場全体のごく一部の気配と約定しか見ていないことになる |
| シャドートレード | 実際には注文を出さず、その瞬間の気配値で売買したことにして帳簿をつける方式。今回の1秒モードはこれのみ |
| ペーパートレード | 証券会社の模擬口座に、本物と同じ形式で注文を出す方式(お金は動かない)。今回は別の1分足ボットで使用 |
| VWAP | 出来高で重みをつけた平均約定価格 |
| IC(情報係数) | 「シグナルが強いときほど、その後の値動きも大きいか」を順位相関で表した数字。予測どおりならプラス、無関係なら0付近、逆ならマイナス |
| キャリブレーション | 「70%と言ったとき、本当に70%の割合でそうなるか」という確率の正確さ |
| ミラーテスト | 入力の上下を反転させて同じAIに聞き直し、判断に「好み」があるかを調べる方法(6章で説明) |
| ルックアヘッド | 判断の時点ではまだ知り得ない未来のデータが、うっかり入力に混ざること。バックテストの成績が良く見えすぎる典型的な原因 |
3. 構成:「判断はAI、決定はコード」
全体は4つの層に分かれています。
③のポリシーとリスク管理は、次の固定ルールです。
| ルール | 内容 |
|---|---|
| 買い | 上昇の確率 68%以上 かつ 買い場スコア 2以上 かつ 不安定さ 35%以下 |
| 売り | 下落の確率 68%以上(または不安定・引け前) |
| 最低保有時間 | 30秒 |
| 売買の間隔 | 10秒以上 |
| 売買回数の上限 | 60回 |
| 損失の上限 | 25ドルで停止 |
| 引け | 引けの10分前に手仕舞い |
| 資金の上限 | 1,000ドル(SPY は約760ドルなので、毎回1株) |
Jev に聞いていること
Jev は、状態(JSON)と「型の決まった質問」を渡すと、選択肢ごとの確率やスコアを返してくれる汎用の判断モデルです。毎秒、次の4つを聞いています(実際の質問はもちろん英語です。Jevって日本語確か怪しかった記憶)。
| 質問 | 答えの形 |
|---|---|
| この状態に一番合う、次の30〜120秒の値動きの方向は? | 上昇 / 下落 / 中立 それぞれの確率 |
| 今の相場の状態は? | 通常 / 高ボラティリティ / レンジ / トレンド |
| 慎重な30〜120秒の戦略にとって、この状態は不安定か? | 「はい」の確率 |
| 慎重な買いエントリーにとって、この状態はどのくらい好ましいか? | 0〜3のスコア(避ける/弱い/普通/好ましい) |
方向の質問の原文は次のとおりです。6章の実験で、この文言が効いてきます。
Which next-30-to-120-second price direction is most consistent with this compact state?
Do not infer certain profits.
up: "Up bias" / down: "Down bias" / neutral: "No directional edge"
Jev に「任せない」こと
ここが設計の肝です。何株買うか、いつ止めるか、何回まで売買するか、損失がいくらになったら止めるか は、すべてコードの固定ルールが決めます。Jev が担当するのは「上がりそうか、下がりそうか」という狭い判断だけで、注文の大きさを決めたり、緊急停止を覆したりすることはできません。
資金の上限を1,000ドルにしているので、売買は毎回1株です。この記事の損益の金額が小さいのはそのためです。
参考にした設計
「状態の計算 → Jev → ポリシー → 執行」という分け方は、Roan さんが公開の場で紹介していた構成[リンク: Roan さんの投稿]を参考にしました。また、Jarrod Watts さんの jev-trader からは、モック(ルールだけの偽AI)と Jev の切り替え、遅れて届いた判断を捨てる仕組み、ドライラン、イベントログといった設計のアイデアをもらっています。
なお、今回のコードは jev-trader のフォークでも移植でもありません。jev-trader は TypeScript/Bun 製で、Monad 上の Kuru の MON-USDC を対象にしたものです。こちらは Alpaca と米国株向けに、Python で独自に書いています。
4. 安全設計:お金が動かない仕組みを先に作る
AIに売買をさせる実験で一番怖いのは、「バグで本物のお金が動くこと」です。そこで、戦略を考えるより先に、次の仕組みを作りました。
お金が動かない
- 注文先は、Alpaca のペーパー(模擬)口座のURLだけをコードに固定しています。 本番口座へ注文を出す経路は、コードのどこにも存在しません。
- 既定は観察モード(注文ゼロ)です。 ペーパー注文モードは、明示的なフラグを付け、しかも中身が空の専用ペーパー口座でなければ起動しません。
- 今回の1秒モードは、さらに厳しくしています。 注文の送信・取り消し・照会という機能自体を持たず、模擬約定だけを行います。
未来のデータを混ぜない(ルックアヘッド対策)
- 判断時刻より前のタイムスタンプを持つデータだけで状態を作ります。
- 「今」の時刻も、PCの時計ではなくデータのタイムスタンプから決めます。Mac の時計がずれていても影響を受けません。
- 模擬約定には、判断が終わった 後 の気配を使います。
遅れたら・壊れたら、売買しない
- Jev の答えが600ミリ秒以内に届かなければ捨てます。データが2秒以上古ければ判断しません。
- エラー時は止まる側に倒します(fail closed)。 1分足のペーパー注文モードでは、注文の前に識別子つきの「注文意図」を記録し、結果がはっきりしない注文は再送せずに照会したうえで、そのセッションの新規注文を止めます。
その他
- APIキーや口座情報はログに出しません。
- 戦略はセッション中に変えません。変えるのは次に実行する前だけで、同じデータでルールだけの基準と比べてから変えます。
地味な話ですが、実際に役に立った場面もありました。10/20 に別の1分足ボットをペーパー口座で動かしたところ、ネットワークエラーが断続的に発生しました。ボットはエラーの間は判断を止めて待ち、1件も注文を出さないままセッションを終えています。わからないときは何もしない が、設計どおりに働いた例です。
5. 1回目(v1):1万回判断して、取引ゼロ
v1 の入力
最初のバージョンでは、IEX の 気配(bid/ask)を中心に状態を作っていました。mid の1秒/5秒/30秒/60秒のリターン、板の偏り(買い注文と売り注文の量の差)、スプレッド、約定件数などです。数値は生の小数のまま渡し、時刻や株価そのものも状態に含めていました。
結果
- 判断は10,146回。応答時間は中央値0.15秒、90パーセンタイルで0.20秒で、1秒ごとのループには十分な速さでした。
- 判断の内訳は 下落39%、中立55%、上昇6%。
- 「上昇の確率68%以上」は約2時間50分でわずか 3回(「下落68%以上」は1,138回)。買いの条件を一度も満たさず、取引はゼロ でした。
この時間帯の SPY はほぼ横ばい(IEX の mid で −0.04%)です。それなのに、判断の多くが下落寄りでした。この時点で「何かおかしい」と思い始めます。
評価そのものが壊れていた
事後に、判断と10秒後の値動きの関係(IC)を測ると −0.18(95%区間 −0.24〜−0.12)でした。これだけ見ると「Jev の逆をやれば当たる」ように思えます。
ところが、Jev を使わない単純な「直近5秒のリターン」も同じく −0.18、板の偏りも −0.17 でした。原因は IEX の気配のチラつき です。1秒ごとの mid の変化について「前の1秒の変化」と「次の1秒の変化」の相関(1次の自己相関)をとると −0.32。上がったら次は下がる、の繰り返しになっていました。正解ラベル自体が跳ね返るので、直近の動きに沿うシグナルは何でも「逆に当たる」ように見えてしまいます。
実際、正解ラベルを mid ではなく 実際の約定価格 にすると、Jev の10秒IC は +0.08(+0.01〜+0.14)と符号まで変わりました。物差しの選び方で結論が逆になる ほど、1秒単位の IEX データはノイズが大きかったわけです。v1 からは何も結論を出せません。
6. 寄り道:AIの「下落びいき」を測る(ミラーテスト)
方法
横ばいの相場で下落判断が多い。これは相場を読んだ結果なのか、それとも Jev の「好み」なのか。これを切り分けるために使ったのが ミラーテスト です。
- 実際に記録した状態を200個取り出す
- 方向の意味を持つ入力(リターン、VWAP乖離、売買フローなど)の 符号をすべて反転 させた「鏡像」を作る
- 元の状態と鏡像の両方を Jev に聞く
偏りのない判定者なら、元の状態で「上昇70%、下落10%」と答えたものは、鏡像では「上昇10%、下落70%」になるはずです。そこで「上昇確率 − 下落確率」をスコアとし、元のスコアと鏡像のスコアを足して2で割った値 を偏りとしました。好みがなければ0になります。単位は、確率の差を100倍した「ポイント」です。
考え方をコードにすると、次のようになります(実際のコードではなく、説明用に簡略化したものです)。
# 方向の意味を持つ特徴量(符号を反転させる対象)
DIRECTIONAL = {
"ret_tr_30s", "ret_tr_60s", "ret_tr_300s",
"vwap_dev_30s_bps", "vwap_dev_300s_bps",
"flow_60s", "flow_300s",
}
def mirror(state: dict) -> dict:
"""方向の意味を持つ特徴量だけ符号を反転した「鏡像」を作る"""
return {k: (-v if k in DIRECTIONAL else v) for k, v in state.items()}
def score(probs: dict) -> float:
"""上昇確率 − 下落確率(−1〜+1)"""
return probs["up"] - probs["down"]
def mirror_bias(judge, states: list[dict]) -> float:
"""偏り(ポイント)。偏りのない判定者なら 0 になる"""
pair_bias = [(score(judge(s)) + score(judge(mirror(s)))) / 2 for s in states]
return 100 * sum(pair_bias) / len(pair_bias)
おまけとして、方向の入力をすべて0にした状態も20個聞きました。こちらはどの条件でも 20個すべて「中立」 で、材料がないときに下落を選ぶ「事前の好み」はありませんでした。偏りは、数値が入ったときに生まれていたことになります。
結果:書き方を変えるだけで偏りがほぼ消えた
1回目に保存したデータから、v2 で使う特徴量の一部を計算し直した状態を作り、条件ごとに200ペアずつ測りました。表は、方向の質問に絞って条件を比べたものです。
| 条件(変えたのは1か所だけ) | 偏り(ポイント) | 偏りがマイナスだったペアの割合 | 中立以外を答えた割合 |
|---|---|---|---|
| 元の設定(生の小数、元の文言) | −12.8 | 97.5% | 25% |
| A:文言から “Do not infer certain profits.” を削除 | −9.0 | 90.5% | 35% |
B:数値を符号付きbpで表記(+0.78 bp) |
−1.7 | 62% | 69% |
| C:選択肢を「高くなりそう/低くなりそう」に変更 | −0.8 | 58% | 0.5% |
| A+B+C をすべて適用 | +5.9 | 18.5% | 59% |
本番と同じ4問の構成で測っても、生の小数では −12.7、符号付きbpでは −1.7 でした。v1 の気配ベースの状態(生の小数)では −19.2 で、200ペアすべてがマイナスでした。
1回目の記録から10秒おきに取り出した961個の状態を、表記だけ変えて聞き直した結果にも、違いがはっきり出ています。
| 表記 | 上昇 | 中立 | 下落 |
|---|---|---|---|
| 生の小数 | 0 | 729 | 232 |
| 符号付きbp | 304 | 324 | 333 |
生の小数のままでは、961回聞いて 「上昇」が一度も出ませんでした。
なぜ?(仮説)
確かめたわけではありませんが、有力な仮説が1つあります。
Python の float は、絶対値が 1e-4(=1bp)未満になると、repr() や json.dumps() で 2.6e-05 のような指数表記になります。つまり 1bp 未満の小さな正の数は、文字列の中にマイナス記号を含む ことになります。一方、負の数は -0.0001366 のように先頭にマイナスが付きます。結果として、生の小数では正でも負でも、ほとんどの数値に「-」が含まれていた わけです。
実際の v1 の入力の一部はこうでした。
ret_1s: 2.6e-05 ret_5s: -0.0001366 ret_30s: -0.0002277 ret_60s: -0.0002537
符号付きbpにすると、こうなります。
ret_1s: +0.26 bp ret_5s: -1.37 bp ret_30s: -2.28 bp ret_60s: -2.54 bp
変換はこれだけです(こちらも説明用の簡略版です)。
def to_signed_bp(x: float) -> str:
"""リターン(小数)を符号付きbpの文字列にする。例: 7.8e-05 -> '+0.78 bp'"""
return f"{x * 1e4:+.2f} bp"
落とし穴:C案は「偏りが消えた」のではなく「答えなくなった」
C案の偏りは −0.8 で、一見いちばん良い結果です。ところが、中立以外を答えた割合はわずか0.5%でした。ほぼ常に「どちらでもない」と答える ようになっただけで、判定者としての役目を果たしていません。偏りだけを見ていると、この罠にはまります。
また、A・B・C をすべて入れると、今度は 上昇側に +5.9 と行き過ぎました。変更は1つずつ試すべき、という教訓です。本番には A・B・C のうち B(符号付きbp)だけを採用しました。2回目のセッション当日の状態で測り直した偏りは −2.8(95%区間 −3.3〜−2.3)です。ゼロではありませんが、−12.7 と比べればかなり小さくなっています。
万能薬ではない
同じ処方を1分足ボットの状態(株価そのものや移動平均の水準を含むもの)に試したところ(77ペア)、偏りは生の小数で −15.2、符号付きbpで −15.4 と、まったく改善しませんでした。株価の水準を消して相対値だけにすると −10.8 まで減りましたが、それでも残っています。状態の設計が違えば効く処方も違う ので、毎回測るしかありません。
AIに数値を渡すすべての人へ
この章の教訓は、相場に限らず、LLM や判断モデルに数値を渡すあらゆる場面に当てはまると思います(別記事で詳しく書く予定です)。
- ミラーテストで偏りを測る。 200ペアなら、ゼロ状態20件を足しても420回の呼び出しで済む、安い健康診断です。
- 符号を明示し、単位をそろえる。 指数表記の小数をそのまま渡さない。
- 偏りと同時に「ちゃんと答えているか」も見る。 中立ばかりになっていないか(C案の罠)。
- 変更は1つずつ。 まとめて変えると、逆方向に行き過ぎることがある。
- 状態の設計を変えたら測り直す。 前に効いた処方が次も効くとは限らない。
7. v1 → v2 で変えたこと
| 項目 | v1 | v2 |
|---|---|---|
| 状態の材料 | IEX の 気配(mid、板の偏り) | 実際の 約定(約定価格のリターン30秒/1分/5分、VWAP乖離、売買フロー、約定件数、5分の値幅)+スプレッド |
| Jev に見せる数値 | 生の小数(2.6e-05) |
符号付きbp(+0.26 bp) |
| 時刻・株価そのもの | 含む | 含まない(日付や価格水準から何かを「思い出す」余地をなくす) |
| 質問の時間軸 | 次の10〜60秒 | 次の30〜120秒 |
| 記録 | 判断のログ | 生のストリームも保存し、あとから完全に再生できる |
| Jev のバージョン | 1.13.0 | 1.14.0(※比較を難しくする要因。12章参照) |
「売買フロー」は、各約定が買い手主導(ask で約定)か売り手主導(bid で約定)かを分類し、出来高で重み付けした −1〜+1 の値です。+1 に近いほど、買い手が積極的に買っている状態を表します。
2回目のセッションで最初に「買い」になった瞬間、Jev にはこう見えていました。方向の意味を持つ値には符号を付け、値幅やスプレッドのように向きのない値はそのままにしています。
ret_tr_30s: +0.66 bp ret_tr_60s: +2.62 bp ret_tr_300s: +4.98 bp
vwap_dev_30s_bps: -0.13 bp vwap_dev_300s_bps: +3.16 bp
flow_60s: +0.889 flow_300s: +0.044
trades_30s: 10 trades_60s: 18 trades_300s: 117 volume_30s: 223
range_300s_bps: 6.95 trade_age_s: 3.0 spread_bps: 1.048
Jev の答えは「上昇79%、中立17%、下落4%」、相場の状態は「トレンド」、買い場スコアは2.0、不安定さは26%。すべての条件を満たしたので、模擬で1株買いました(この取引が次章の表の #1 で、結果は −0.27ドルでした)。
8. 2回目(v2):4時間・1.4万回の判断と11往復の模擬売買
実行の数字
| 項目 | 値 |
|---|---|
| 時間 | 米東部 10/23 11:50〜15:50(日本時間 10/24 0:50〜4:50)、約240分 |
| Jev の判断 | 14,285回 |
| 応答時間 | 中央値0.15秒、90パーセンタイル0.20秒、99パーセンタイル0.36秒 |
| 締め切り超過などで使えなかった判断 | 85回(約0.6%) |
| 判断の内訳 | 下落40%、中立35%、上昇25% |
| 強い判断(確率68%以上) | 下落1,757回、上昇979回 |
| 受信データ | 気配の更新 約100万件、約定 7,494件(約0.5件/秒) |
| IEX のスプレッド | 中央値0.66bp(約5セント)、90パーセンタイル2.49bp |
| IEX の mid の自己相関(1次) | −0.35(v1 と同じく、mid のチラつきは残っていた) |
| SPY の動き(IEX の mid) | 762.61 → 760.26ドル(−0.31%) |
全取引
時刻は米東部時間です(日本時間は+13時間)。買いは ask、売りは bid での模擬約定です。「mid基準」は、スプレッドを払わずに mid で売買できたと仮定した場合の損益です。
| # | 買い | 売り | 保有 | 買値 | 売値 | 損益 | mid基準 | 売った理由 |
|---|---|---|---|---|---|---|---|---|
| 1 | 11:57:38 | 12:01:28 | 3.8分 | 763.02 | 762.75 | −0.27 | −0.235 | Jev が下落判断 |
| 2 | 12:30:37 | 12:39:22 | 8.7分 | 761.79 | 761.91 | +0.12 | +0.165 | Jev が下落判断 |
| 3 | 12:45:51 | 12:48:35 | 2.7分 | 762.63 | 762.43 | −0.20 | −0.105 | Jev が下落判断 |
| 4 | 13:30:11 | 13:35:19 | 5.1分 | 760.39 | 760.36 | −0.03 | +0.075 | Jev が下落判断 |
| 5 | 13:48:06 | 13:52:28 | 4.4分 | 760.99 | 760.79 | −0.20 | +0.005 | Jev が下落判断 |
| 6 | 13:57:56 | 14:00:44 | 2.8分 | 760.94 | 760.79 | −0.15 | +0.040 | Jev が下落判断 |
| 7 | 14:15:45 | 14:28:25 | 12.7分 | 760.87 | 760.78 | −0.09 | −0.005 | Jev が下落判断 |
| 8 | 14:53:00 | 14:59:21 | 6.3分 | 760.72 | 760.28 | −0.44 | −0.400 | Jev が下落判断 |
| 9 | 15:02:56 | 15:11:07 | 8.2分 | 760.65 | 760.65 | ±0.00 | +0.050 | Jev が下落判断 |
| 10 | 15:29:58 | 15:31:30 | 1.5分 | 760.52 | 760.32 | −0.20 | −0.145 | Jev が下落判断 |
| 11 | 15:45:02 | 15:50:00 | 5.0分 | 760.29 | 760.07 | −0.22 | −0.160 | 引け前の手仕舞い |
| 計 | 61.3分 | −1.68 | −0.715 |
(単位:ドル、すべて1株)
- 1勝1分け9敗、−1.68ドル。
- mid基準では −0.715ドル。差額の 0.965ドル(損失の約57%)がスプレッドの負担 です。1往復あたりに直すと、損益は −2.0bp、そのうちスプレッドが約1.15bpです。
- mid基準ではプラスが5回ありましたが、スプレッドを払うと、そのうち4回がマイナスか±0になりました(#4・#5・#6・#9)。
- 平均保有時間は5.6分。株を持っていた時間は、セッション全体の約26%でした。
9. 同じデータで比べる
1回の損益だけでは、良いのか悪いのか判断できません。そこで、同じ日の同じデータで基準と比べました。
| 方式 | 往復 | 損益(スプレッド込み) | mid基準 | 保有時間の割合 |
|---|---|---|---|---|
| Jev(実際のシャドー運用) | 11 | −1.68 | −0.715 | 26% |
| Jev(記録から再生したオフライン再現) | 11 | −1.56 | −0.94 | 26% |
| ルールだけの基準(60秒の約定モメンタム+売買フロー、Jev なし) | 30 | −7.31 | −2.38 | 5% |
| 1株を最初に買って最後まで保有 | 1 | −2.42 | −2.355 | 100% |
| 保有時間を Jev にそろえた買い持ち(概算) | — | 約−0.62 | 約−0.60 | 26% |
(単位:ドル、すべて1株)
- ルールだけの基準 は30往復もして、スプレッドで大きく負けました。Jev 版は売買回数が少なかった分、損失も小さく済んでいます。
- 買い持ち はこの日 −2.42ドル。SPY が −0.31% 下げた日なので、買いしかしない戦略にはもともと逆風でした。
- 最後の行は、買い持ちの損益を Jev の保有時間の割合(26%)で単純に割り引いた概算です。下げた日に26%の時間だけ1株持っていれば、それだけで約0.6ドルは負ける計算 になります。Jev の mid基準の損益(−0.715ドル)はこれとほぼ同じで、値動きを当てて稼いだ形跡も、大きく外した形跡もありません。
- オフライン再現(−1.56ドル)が実際の運用(−1.68ドル)とほぼ一致したのは、仕組みの検証として良いニュースです。差は、約定に使った気配が数ミリ秒ずれていることによるものです。
10. Jev は何を見ていたか
判断は「直近1分の値動き」とほぼ連動していた
Jev のスコア(上昇確率 − 下落確率)と、入力した各特徴量との順位相関を測りました。
| 特徴量 | Jev スコアとの相関 |
|---|---|
| 直近60秒の約定価格リターン | +0.81 |
| 5分VWAPからの乖離 | +0.79 |
| 直近30秒のリターン | +0.67 |
| 直近5分のリターン | +0.58 |
| 売買フロー(60秒/5分) | +0.33 / +0.34 |
| スプレッド | ±0.00 |
つまり Jev は、「最近上がっていれば上昇、下がっていれば下落」 という素直なモメンタム(勢い)型の判断をしていました。判断として不自然なものではありません。
ところが、この日の相場は逆向きだった
判断とその後の値動きとの IC は、次のとおりです(重ならない区間ごとに数えたサンプル。[ ]は95%区間)。
| 何秒後を予測 | IC(約定価格で評価) | IC(IEX の mid で評価) |
|---|---|---|
| 10秒 | −0.10[−0.16, −0.05] | −0.05[−0.11, +0.00] |
| 30秒 | −0.14[−0.23, −0.05] | −0.10[−0.19, −0.01] |
| 60秒 | −0.14[−0.27, −0.01] | −0.08[−0.21, +0.05] |
| 120秒 | −0.20[−0.37, −0.02] | −0.17[−0.34, +0.01] |
どの時間軸でもマイナスで、セッションを前半と後半に分けても同じ傾向でした。Jev を使わない単純な「60秒モメンタム」も、約定価格で評価すると −0.07〜−0.12 とマイナスです。反対に「30秒VWAPより下なら買い(平均回帰)」は、30〜120秒で +0.07〜+0.12 とプラス寄りでした(ただし多くは区間が0をまたいでいます)。
この日は「上がったら戻る」平均回帰寄りの相場で、勢いに乗る判断とは相性が悪かった というのが、一番素直な解釈です。とはいえ1日分のデータでは、Jev の性質なのか、その日の相場の性質なのかは区別できません。
確率は「当たる確率」ではない
Jev が「上がりそう」と言ったとき、実際にはどうだったのか。上昇と下落の確率だけで比べた値(上昇 ÷(上昇+下落))を5段階に分け、10秒後に mid が上がっていた割合を見ました。
| Jev の上昇確率(上昇 ÷(上昇+下落)、各段階の平均) | 実際に上がった割合 | 件数 |
|---|---|---|
| 11% | 53% | 350 |
| 29% | 54% | 300 |
| 50% | 47% | 211 |
| 70% | 47% | 229 |
| 90% | 47% | 215 |
90%と言ったときも、実際に上がったのは半分以下でした。予測の良し悪しを測るブライアスコア(小さいほど良い)は0.35で、毎回「50%」と答え続けた場合の0.25より悪い結果です。Jev の確率をそのまま売買の自信度として使うのは危険で、使うなら別の日のデータで較正(キャリブレーション)する必要があります。
「じゃあ逆をやれば?」も勝てない
IC がマイナスなら、Jev の逆を張れば勝てるのでしょうか。「Jev が下落68%以上と言ったら買う」というルールも検証しましたが、スプレッドを払うと30秒で1取引あたり −0.78bp で、どの時間軸でもマイナスでした。実は、試したルールは(Jev に従うもの、逆を張るもの、Jev を使わない単純なものも含めて)すべて、スプレッドを払うとマイナス でした。1秒〜2分の時間軸で約1bpのスプレッドを毎回超えるのは、それだけ難しいということです。
11. なぜ負けたのか
- スプレッド負け。 損失1.68ドルのうち0.97ドルは、買値と売値の差を払った分です。数分で売り買いする設計では、値動きの予測が当たっても、このコストを超えるのは簡単ではありません。
- 下げた日に、買いだけで勝負した。 SPY は −0.31%。持っている時間が長いほど不利な日でした。
- 判断の向きと相場の性質が逆だった。 Jev は勢いに乗る判断をしましたが、この日は平均回帰寄りでした。
- 確率が過信気味だった。 「90%」が実際には47%。確率のしきい値(68%)に、見た目ほどの意味はありませんでした。
- そもそもサンプルが小さい。 11往復は運の範囲に収まる数です。良くも悪くも、ここから実力は語れません。
12. 反省点
- データのノイズを先に調べなかった。 v1 で IEX の mid を1秒単位で使い、チラつき(自己相関 −0.32)に振り回されました。セッションの前に、記録データで自己相関を見ておくべきでした。
- AIの偏りを本番前に検査しなかった。 v1 は「下落びいきのAI」のまま約3時間回していました。ミラーテストは数百回の呼び出しで済むので、本番前の必須チェックにすべきでした。
- 評価の物差しを事前に決めていなかった。 mid と約定価格のどちらで評価するかで、IC の符号まで変わりました。
- v1 と v2 の間で、Jev のバージョンも変わっていた(1.13.0 → 1.14.0)。 そのため、v1 と v2 の違いをすべて設計変更の効果とは言えません。
- 模擬約定は楽観的。 注文の順番待ち(キュー)、通信の遅れ、市場全体の価格を考慮していません。実際に売買していれば、結果はもっと悪かった可能性があります。
- 2セッションだけ、しかも同じデータでの事後診断。 9〜10章の比較はすべてインサンプル(答えを知っているデータでの検証)です。
- 1分足ボットは、ネットワークエラーで実験として不完全に終わった。 安全側に止まったのは良かったものの、データとしてはほとんど使えません。
13. 収穫
- 安全に回る土台ができた。 2回のセッションで実際の注文はゼロ。遅れた判断は捨て(約0.6%)、エラー時には止まる。この土台があったので、安心して中身の検証に集中できました。
- Jev は十分に速い。 応答は中央値0.15秒、90パーセンタイル0.20秒。1秒ループの判断役として、工学的には問題なく使えます。
- ミラーテストという安い検査方法を手に入れた。 数値の書き方ひとつで −12.7 → −1.7。この知見は相場以外にも使えます。
- 損益を分解して「どこで負けたか」を言えるようになった。 スプレッド・値動き・保有時間に分けると、「予測が外れた」ではなく「コストと相場環境で負けた。予測力の証拠はない」と正確に言えます。
- 再現できる。 生データを保存しておいたので、オフラインでほぼ同じ結果(−1.56ドル対−1.68ドル)を再現できました。次の改善も、同じデータで公平に比べられます。
- 汎用AIの「性格」が見えた。 書式に敏感で、素直に勢いに乗るタイプ。こういう性質は、実際に動かして測ってみないとわかりません。
14. 次にやること
研究として、次の順で進める予定です(売買の推奨ではありません)。
- 同じ設定を固定したまま、セッションを重ねる。 しきい値などは事前に決めて記録し、途中で動かさない。
- Jev のバージョンが変わるたびにミラーテストを実行する。 偏りと「答えているか」の両方を測る健康診断として。
- 約定モデルを現実に近づける。 注文の順番待ちや通信遅延を考慮し、可能なら IEX だけでなく市場全体(SIP)のデータで検証する。
- スプレッドに対して値動きが十分大きい時間軸を検証する。 秒単位にこだわらず、取引回数を減らす設計も含めて比べる。
- 確率を較正する。 別の日のデータで、「Jev の70%」が実際には何%に当たるかを測る。
- 1分足ペーパーモードで、約定通知と注文の突き合わせを検証する。
- Jev が本来得意そうなタスクを試す。 数値の羅列ではなく、ニュースや適時開示の文章を読んで判断させる実験を予定しています。
おわりに
AIに毎秒相場を判断させると聞くと、派手な結果を期待したくなります。けれども、実際に手元に残ったのは「スプレッドは重い」「数値の書き方でAIは偏る」「1日の結果からは何も言えない」という、地味で確かな学びでした。個人的には、それで十分に面白い実験だったと思っています。
免責事項
- 本記事は個人の技術実験の記録であり、投資助言ではありません。特定の金融商品の売買を勧めるものではありません。
- 1秒モードは実際の注文を一切出さないシャドートレードで、損益は IEX の気配値にもとづく模擬計算です。手数料、注文の順番待ち、通信遅延、市場全体の価格は考慮していません。
- 結果は2セッション分だけのもので、統計的には「仮説を立てる段階」です。将来の成績を示すものではありません。
- Jev は汎用の判断モデルであり、今回は本来の用途の外で使っています。結果は Jev 1.13.0 / 1.14.0 と、この記事に書いた設定でのものです。
お疲れ様でした。
わからないところ、間違っているところ、もっといい方法がある場合は、コメントでもDMでも教えてください。

