同じ相場を二度見たトレーディングBot
4時間ごとに、私の暗号通貨トレーディングBotが起動し、BTCとETHを分析してDiscordにレポートを投稿する。ある日、気になることに気づいた。4時間間隔で連続した2つのレポートが、全く同じ価格を示していたのだ。
2026-03-04T20:02 BTC=$73,644.29 ETH=$2,176.69
2026-03-05T00:02 BTC=$73,644.29 ETH=$2,176.694時間でBTCが1セントも動かなかった。四捨五入の差でも、惜しくもない。_ちょうど_同じ数字だ。これは市場の状態ではない——バグだ。
データの流れを追う
BotはBinanceからOHLCV(始値・高値・安値・終値・出来高)のローソク足データを取得し、APIへの過度なアクセスを避けるためローカルのSQLiteキャッシュを使用している。キャッシュのロジックは一見合理的に見えた:
now = datetime.now(timezone.utc)
tf_ms = TF_MINUTES[timeframe] * 60 * 1000 # 例: 4hなら 240 * 60 * 1000
# 現在形成中の4hローソク足の開始時刻
current_candle_start = int(now.timestamp() * 1000) // tf_ms * tf_ms
cache_min, cache_max = self._get_cached_range(symbol, timeframe)
# 直前の完成したローソク足があればキャッシュは「新鮮」
cache_is_fresh = cache_max >= current_candle_start - tf_ms
if cache_is_fresh:
return self._load_from_cache(...)コメントには「直前の完成したローソク足があればキャッシュは新鮮」と書かれている。一見問題ないが、微妙な罠がある。
時間のオフ・バイ・ワン
cronが16:03 UTCに実行されたとき、実際に何が起きているか追ってみよう:
current_candle_start= 16:00:00(現在形成中のローソク足)current_candle_start - tf_ms= 12:00:00(直前の完成したローソク足)- 12:03の前回実行でBinanceからデータを取得し、12:00のローソク足まですべてキャッシュした
- つまり
cache_max= 12:00:00 - チェック:
12:00 >= 12:00→ ✅ キャッシュは「新鮮」
BotはキャッシュからデータをロードしてBinanceではなく_前回の実行_のデータを返す。同じ価格。毎回。ずっと。
閾値が1期間分甘かった。このロジックは「直前のローソク足があるか?」を確認していたが、本当に確認すべきは「現在のローソク足があるか?」だった——形成中の現在のローソク足はキャッシュには_決して_存在しない。
修正
比較演算子を一文字変えるだけ:
# 修正前:直前の完成したローソク足があれば新鮮
cache_is_fresh = cache_max >= current_candle_start - tf_ms
# 修正後:現在の期間のデータがある場合のみ新鮮
cache_is_fresh = cache_max >= current_candle_start現在の4hローソク足は形成中なので、cache_maxは常にcurrent_candle_startより過去になる。条件は常にFalse → 常にBinanceから取得 → 常に最新データ。
2つ目の修正:レポートで表示される価格がindicators['close'](最後にキャッシュされたローソク足の終値)だった。これをティッカーエンドポイントを直接呼び出すfetcher.get_latest_price()に置き換えた:
# 修正前
prices[symbol] = indicators['close']
# 修正後:リアルタイムティッカー、最終ローソク足の終値ではない
live_price = fetcher.get_latest_price(symbol)
prices[symbol] = live_price見落としやすい理由
元の閾値は_過去データ_のユースケースでは直感的に理にかなっている。「インジケーター計算のために200本のローソク足が欲しい——最後の完成したローソク足があれば、過去のシリーズは十分有効だ。」この推論はバックテストでは正しい。
_ライブトレーディング_では壊れる、なぜなら:
- cronの間隔がローソク足の間隔と完全に一致している(4h cron → 4hローソク足)
- 毎回の実行で閾値がちょうど1期間分進む
- キャッシュは前回の実行で満たされたばかりなので、常に条件を満たす
期間のミスマッチ(例:1h cronで4hローソク足を取得)があればバグが隠れていた——4回中4回ではなく3回が古いデータになるはずだ。
教訓
更新間隔と一致するキャッシュ鮮度閾値は見えないバグだ。 条件 cache_max >= current_candle_start - tf_ms は安全マージン(1期間分の許容度)を追加しているように見えるが、ジョブが期間の境界でちょうど実行されるとき、それは古いデータを保証するものになる。
キャッシュ期間とジョブ期間が同じ場合、「歴史データとして十分新鮮」≠「ライブ価格として十分新鮮」だ。修正方法はOHLCVキャッシュとは独立した価格ソース(ティッカーAPI)を使用することだ。
