TL;DR
- TradingView デスクトップアプリを CDP 経由で操作する MCP サーバーを機能強化した
- バックテスト系のツールを増やすうちに、機能追加より「都合の良い結果を採用してしまう経路を塞ぐ」ほうが重要であることに気づいた
- リポジトリ: https://github.com/tedorigawa001/TradingView-MCP (npm:
bushido-tradingview-mcp)
なぜAIを「騙さない」が主題になったか
バックテストのツールを AI に持たせると、最初はうまくいきます。
AI は喜んで条件を変え、良い数字が出た構成を報告してきます。
問題は、その報告が一つ一つは正直だということです。
手元の台帳で試したことがあります。
200件・全体の平均が -1.96bps・PF 0.857 という、はっきり負けている記録です。
ここに「銘柄を絞る」「方向を絞る」「期間を絞る」を組み合わせて 12回呼んだだけで、こういうスライスが出ます。
台帳全体 : records=200 mean=-1.96bps PF=0.857
12スライス中の最良 : records=34 mean=+3.09bps PF=1.293
34件は「少数標本」の閾値(30件)を超えているので警告も出ません。
この結果だけ見ると、残り(166件)の存在を一言も述べません。嘘は一つもないのに、読んだ人は勝てる手法を見つけたと思います。
機能を増やすほどこの穴は広がります。
フィルタが増えれば探索空間が増えるからです。それで方針を変えました。
1. 台帳をハッシュで束縛する
get_strategy_trade_ledger は Strategy Tester の全取引を500件ずつページングし、SHA-256 の ID に束縛します。
ページをまたいだ再計算は拒否されます。
地味ですが効きます。バックテストの数字は「どの実行の、どのページの話か」が曖昧になりがちで、そこが曖昧だと後から検証できません。
含み損益の行(最後の未決済ポジション)を分離しているのも同じ理由です。
決済済みの成績に未決済の評価益が混ざると、都合の良い方向にだけ数字が動きます。
2. 成果物を不変にする
summarize_backtest_ledger は、ローカルに登録された内容アドレスの成果物を読みます。
npm run import:backtest-ledger -- --input /abs/path/normalized.json --confirm-local-import
# => { "artifact_id": "sha256:8d22d028...", "records": 200 }
同じ内容を再インポートすると同じ ID が返り、既存の証跡は置き換わりません。公開は一時ファイルを fsync してから link() で行い、直後に読み戻してハッシュを照合します。壊れた宛先があれば置換ではなく拒否です。
ただしここには限界があって、応答にもそう書いてあります。
content_hash_is_integrity_not_source_authentication
内容ハッシュが保証するのは「後から書き換えられていない」ことだけです。その数字が正しく作られたことは保証しません。
3. スライスの分母を返す
冒頭の 34/200 の問題への直接の答えです。応答に2つのフィールドを足しました。
| フィールド | 意味 |
|---|---|
ledger_records |
絞り込み前の全件数 |
selected_fraction |
選択件数 ÷ 全件数 |
34件が「34件」ではなく「200件中の0.17」として出ます。数字を一つ足しただけですが、読む側の判断が変わります。
4. 探索回数を記録する
分母だけでは足りません。何回切ったかは、単発の応答には原理的に書けないからです。
そこで research_id を渡すと、追記型のローカル台帳に記録してから指標を返すようにしました。実際に動かすとこうなります。
whole ledger calls=1 distinct=1 cells=0 frac=1.000
XAUUSD only calls=2 distinct=2 cells=0 frac=0.330
XAUUSD only (repeat) calls=3 distinct=2 cells=0 frac=0.330 ← 回数は増え、条件数は据え置き
XAUUSD long calls=4 distinct=3 cells=0 frac=0.165
group_by month calls=5 distinct=4 cells=9 frac=1.000 ← 1回で9セル提示
実装で気を使ったのは3点です。
条件の正規化。 [EURUSD, USDJPY] と [USDJPY, EURUSD] は同じ条件です。整列と重複除去をしてからハッシュを取らないと、distinct_conditions が水増しされて探索を実際より広く見せます。過小に見せるより厄介です。
記録が先、指標が後。 記録に失敗したら指標を返さずエラーにします。逆順だと「数字だけ見て記録は失われた」が起こります。
グループセルを別に数える。 group_by: "month" は1回の呼び出しで12セル提示することがあります。呼び出し回数だけでは探索の広さを表せません。
そして research_id を渡さない場合は status: "untracked" が明示されます。追跡していないことが黙って隠れないのが要点です。
5. train でだけ選ぶ
run_strategy_walk_forward は 2〜8 個の候補を 2〜12 の train/embargo/test 窓に分割し、train でのみ選抜して、選ばれた候補の OOS だけを返します。
全候補の OOS を並べて返すと、人間(と AI)は必ず一番良いものを見ます。それは選抜を OOS でやったのと同じことです。返さないのが唯一の対策でした。
6. 事前に protocol を検査する
validate_research_protocol は Pine のバージョンと凍結済みプロトコルを突き合わせ、以下を blocked / warning に分類します。
- IS/OOS の重複
- 未来を含む窓
- 未確定足の混入
- 試行回数
- 最小取引数
- コスト未設定
- OOS 確定後の変更
「後から気づく」のではなく、走らせる前に止まります。
実装の評価結果
M5/M15/M30 × 6銘柄 × 手法/閾値の 243構成を回しました。
| コスト | 平均の中央値 | 陽性 |
|---|---|---|
| 0bps(グロス) | -0.150 | 93/235 |
| 2bps | -2.150 | 11/235 |
コスト適用は全構成で厳密な定数シフトでした。つまりコストが edge を消したのではなく、グロスの時点で edge が無い。235構成のうち、記述的な95%区間がゼロを正の側に外したものは 0本です(負の側に外したものは185本)。
唯一「良さそう」に見えたセルも、同じ手法・同じ銘柄の 3×3 グリッドで見ると9セル中1つだけの孤立点で、合計の48%が7取引(全体の3.3%)から来ていました。
採用したものはありません。 この結論が正しく出たことが重要な実績です。
作り方の話: ミューテーションを通す
テストの受け入れ基準を「通ること」から「欠陥を戻したら落ちること」に変えました。
このセッションだけで、こういうものが見つかっています。
- limitations の配列を丸ごと消してもテストが全部緑だった(読む人が最も寄りかかる部分です)
- 監視機構が自分の失敗時に通知しないコードで、テストは通っていた
- 排他作成のシンボリックリンク検査を消しても、902件が緑のままだった
一つ実例を出すと、価格レベルの正規化(浮動小数の丸め誤差だけを回収し、本物の半ティック価格は拒否する)では、厳しすぎる変異と緩すぎる変異の両方を確かめました。片側だけだと、緩める変異を見逃します。
### M1: strict integers only → Expected 4 BUY 3 3 6 1 1, got
### M2: accept anything that rounds → Expected , got 4 BUY 3 3 6 1 1
npm test が pretest で再ビルドするので、build/ を書き換えて npm test すると変異が消えて全部通ります。これに一度騙されました。変異検証は node --test <file> を直接使う必要があります。
使い方
npm install -g bushido-tradingview-mcp
open -a TradingView --args --remote-debugging-port=9222
MCP クライアント側の設定はリポジトリの README にあります。
npm 利用者に Java は不要です(Bookmap アドオンのビルド時のみ JDK が要ります)。
正直に書いておくこと
- MCPでは勝てる手法は提供しません。 このサーバーが提供するのは、負けを負けと認識しやすくする道具です
- バックテストの結果は約定を証明しません。スプレッド・スリッページ・資金調達・通貨換算は測定していません
-
candidateEligibleは常にfalseです。採用判断は人間が別途行うもので、このツールは出しません - 探索回数の記録は、有効化前の探索も、このサーバー外の探索も数えられません