俺のbot CSV は-240円分嘘ついてた話 — 50代エンジニアが broker reconciliation して気付いたこと

botter journal
副業 物販 https://ffpytaro.com/wp-admin/upload.php?item=2680

イントロ ― 朝、見覚えのないshortポジが立っていた

2026年5月26日、いつものようにK1 botを動かしてた朝、bitFlyer FXの画面を開いて手が止まった。

short 0.006 BTC。

なんでだ。bot のlot sizeは0.001のはずなのに、なぜ6発分積み上がってる。前夜のlogを見直しても、main / v2 両方とも lot=0.001 のままだし、エントリー回数も6回じゃない。誰かが手で発注した形跡もない。

50代エンジニアとして「自動売買とは数字が嘘をつかない世界だ」と信じてた俺の自尊心が、その朝10分でガラガラと崩れた。結論から書くと、自作botの中に2つ別々の構造バグが同居していて、CSVに記録されているPnLは累計で約240円分嘘をついていた。今回はその発覚と修正、そして「自分の数字を二度と信じないために何を作ったか」を書く。


バグA:close-size が他botの建玉まで巻き込む

最初の手がかりは、bitFlyer の /me/getexecutions だった。同時刻 (2026-05-22 14:33:12) に MARKET CLOSE 0.006 が 2回 入っている。発注したのはK1 main と K1 v2。どちらも別プロセス、別 lot、別ロジックで動かしていたはずだった。

コードを追うと、close 処理がこうなっていた。

# 修正前
real_pos = bf_client.get_position(symbol='FX_BTC_JPY')
size_to_close = real_pos['size']        # ← これが諸悪の根源
bf_client.order_close(side, size=size_to_close)

real_pos['size']bitFlyer 口座全体の集約サイズ。K1 main の 0.003 と K1 v2 の 0.003 が同一口座に乗っていれば、ここは合算で 0.006 を返す。それを「自分の建玉のサイズ」と勘違いして MARKET CLOSE に渡していた。

main と v2 がほぼ同時にエグジットシグナルを出した瞬間、お互いに相手の建玉まで MARKET でクローズし合った。残ったのは反対側のショート 0.006 — つまり、両 bot が相手のロングを刈り取り合った結果のショートだった。

修正は単純で、bot ごとに自分の建玉だけを記録した内部レジャー (my_position_size) を持たせて、close もそこを参照させる。

# 修正後
size_to_close = my_position_size  # 自 bot lot のみ
if real_pos['size'] != size_to_close:
    log.warning(f'ANOMALY: bf={real_pos["size"]} mine={size_to_close}')
bf_client.order_close(side, size=size_to_close)

ANOMALY warning を出す副作用 check も同時に入れた。同一口座で複数 bot を動かす設計を取る限り、この check は今後も必須になる。


バグB:CSVの exit_price が実約定VWAPじゃなかった

A を直して 1 日眺めて、それでも違和感が消えなかった。bitFlyer の証拠金残高の変動と、bot CSV の PnL 合計が毎日 50〜100円ズレている

ここで初めて reconciliation script を書いた。やったのは1つだけ。

CSV の entry_price / exit_price を捨てて、bitFlyer /me/getexecutions から trade_id ごとに実約定価格を取り、約定数量で実約定VWAPを計算する。それを bot lot で再計算した PnL と、CSV PnL を突き合わせる。

結果は痛烈だった。

期間 CSV PnL 累計 実約定 VWAP PnL 乖離
5/22 – 5/25 +210円 -30円 +240円 (CSVが過大)

72% も嘘をついていた。原因はコードのこの 1 行だった。

# 修正前
trade_log['exit_price'] = tp_trigger_price  # ← トリガ水準を記録してしまっていた

TP/SL 発火条件の水準をそのまま exit_price として CSV に書き込んでいた。当然、実際の MARKET 約定はスリッページ込みで 50〜80円ほど不利な側に走るのに、そこが反映されない。スリッページ分だけ毎回 PnL が「見かけ上の勝ち」に上振れし、累積で 240円の嘘になっていた。

修正後はこう。

# 修正後
executions = bf_client.get_executions(child_order_id=close_order_id)
vwap = sum(e['price'] * e['size'] for e in executions) / sum(e['size'] for e in executions)
trade_log['exit_price'] = vwap
trade_log['exit_price_trigger'] = tp_trigger_price  # 参考用に残す

exit_price は約定 VWAP、exit_price_trigger を別カラムに分離。これで「TP水準と実約定のスリップ幅」も後追いで分析できる構造になった。


再構築:framework v2 を一晩で組んだ

「CSV を信用しない世界」をどう作るか。参考にしたのは yodaka 氏の戦略ログ #4 で示されていた12種セーフティテスト + 3指標ログ + 5段階フォワードの枠組みだった。今すぐ全部は無理なので、最初の version では3つだけ実装した。

① broker reconciliation cron化

毎朝5時に bitFlyer execution を取得し、当日 CSV と突合。乖離率 > 10% で Discord に ALERT を投げる。yodaka basis B-2「乖離率 < 10%」を合格ラインとして採用。

# reconciliation_cron.py 抜粋
def reconcile_day(date):
    csv_pnl = read_csv_pnl(date)
    exec_pnl = compute_exec_vwap_pnl(date)
    diff_rate = abs(csv_pnl - exec_pnl) / max(abs(exec_pnl), 1)
    if diff_rate > 0.10:
        discord_alert(f'RECONCILE FAIL {date}: csv={csv_pnl} exec={exec_pnl} diff={diff_rate:.1%}')

② 3指標ログ (Slippage / RTT / MissRate) の埋め込み

各 trade で以下を強制記録する schema を追加した。

  • Slippage: trigger_price と exec_vwap の差 (JPY)
  • RTT: 発注 → 約定 までの round-trip ms
  • MissRate: 発注のうち未約定で取り消した割合

③ ANOMALY warning

bot 内部レジャー ≠ broker 集約サイズの瞬間に warning を出す。同一口座 multi-bot 設計の安全弁。


教訓 ― 50代でbotを書く者として

今回学んだことは3つ。

1. CSV PnL を信用するな。 bot が自分で書く数字は、bot の世界観に閉じている。broker の現実と突き合わせない限り、それは観測ではなく祈りだ。

2. broker reconciliation 無しのbotは数字が嘘をついている可能性が常にある。 動いている、勝っているように見える、それだけで満足してはいけない。

3. 失敗を記録する場所を持つ。 自分の bot が嘘をついていた事実を、自分の口で、自分の名前で書き残す。これは技術記事である前に、自分への戒めだ。

このブログ pytaro.log は、その記録のために再起動する。今までの記事 (AI 生成の副業・働き方系) は資産として残すが、これからは 自分の bot の失敗譚と発見 をメインに書いていく。

次回は、上に書いた 3指標ログ (Slippage / RTT / MissRate) の実装詳細編 — bitFlyer execution API の落とし穴と、metrics CSV を翌朝 5 時 aggregator で Discord に流すまでの構成を書く。framework v2 のコードも、整理がついたら順次公開する予定。


おわりに

  • 質問・bot audit の相談は Twitter DM で受付予定 (アカウント準備中)。
  • 次回 → 3指標ログ実装編 (Slippage / RTT / MissRate)。
  • プロフィール → サイドバー参照。

50代でこの世界に居続けるための、地味で堅実な記録です。よろしくお願いします。

― とっちゃん