0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Celeryの朝バッチが寄り付きに間に合わない問題を「開始時刻の前倒し+スケジュールのテスト固定」で解決した話 〜Windowsのsoloプールとbeat UTC換算の落とし穴〜

0
Last updated at Posted at 2026-09-25

締め切りが「相場の開始時刻」で決まっているバッチを、書いたことはありますか。

日本株のAI銘柄スクリーニング&継続学習端末「ALPHA FORGE(アルファフォージ)」を個人で開発・運用しています(開発には Claude Code を使っています)。朝の銘柄抽出バッチは、東証が開く9時までに終わっていなければ意味がありません。この記事では、そのバッチが間に合わなかったときの原因の切り分けと、対処の手順をまとめます。

【要約】

  1. Windows の Celery は --pool=solo(直列実行)になるため、2つのタスクを数分ずらしても順番待ちになるだけ。08:50 開始で中長期の抽出に実測 23分24秒 かかり、9時に間に合っていなかった
  2. 性能改善より先に開始時刻の前倒しで解決した(08:50 → 07:40 → 07:30 → 07:00)
  3. beat の crontab を UTC で書くときは換算後の JST をコメントに残し、時刻と実行順はテストで固定する

※本記事はnote連載[ALPHA FORGE 開発秘話 #04] の技術的な部分を、エンジニア向けに再構成したものです。

環境

項目 内容
OS Windows 11
バックエンド Python 3.11 / FastAPI
非同期処理 Celery(beat でスケジュール発火、ワーカーは --pool=solo)+ Redis
朝のバッチ 中長期の抽出 → 短期の抽出(それぞれ LLM による分析を含む)
.venv\Scripts\celery.exe -A backend.celery_app worker --pool=solo --loglevel=info

Windows では Celery の prefork プールが使えないため、--pool=solo で動かしています。solo はタスクを1件ずつ直列に処理します。beat 側で2つのタスクを2分ずらして登録しても、先のタスクが終わるまで後のタスクは始まりません。

症状と実測

2026年9月16日の9時7分に「市場がひらくのに間に合わないね」と気づき、Claude Code にタスクの完了まで見届けてもらいました。

タスク 開始 完了 所要時間
中長期の抽出 08:50 09:13:22 約23分24秒(1403秒)
短期の抽出 中長期の完了待ち 09:15:03 約1分31秒(91秒)

中長期は、全銘柄の価格取得と LLM による分析が直列に並ぶため時間がかかります。短期は直前に取得したデータを使えたので短く済みましたが、完了は寄り付きの15分後でした。

対処1:速くする前に、開始時刻を前倒しする

並列化やキャッシュで速くすることもできますが、変更が大きく、毎朝のバッチを壊すリスクが増えます。「間に合わせる」ことが目的なら、開始時刻を早めるのが最小の変更です。Claude Code が実測をもとに余裕を見た時刻を提案し、人間(私)が採用しました。

朝の銘柄抽出バッチの開始時刻の変遷
朝の銘柄抽出バッチの開始時刻の変遷(バーの長さは9/16の実測23分半を当てはめた目安)

  1. 〜9月16日:08:50 開始(寄り付きに間に合わない)
  2. 9月17日:07:40(短期は 07:42)へ前倒し
  3. 9月17日:07:30 へ前倒し(08:05 の note 下書き自動生成が抽出結果を使うようになったため)
  4. 9月25日:07:00 へ前倒し(対処3を参照)

対処2:crontab は UTC で書き、JST をコメントに残す

このプロジェクトの beat スケジュールは UTC で評価しているので、JST から9時間引いて書きます(JST は夏時間なし)。hour=22 が「JST 7時」を意味することは、コメントがないと誤読されます。

backend/celery_app.py(抜粋)
_BEAT_SCHEDULE: dict[str, dict[str, object]] = {
    "run-picks-mid-term": {
        "task": "backend.tasks.run_picks_task",
        "args": ("mid_term",),
        "schedule": crontab(hour=22, minute=0),  # JST 07:00(寄り付き前、記事作成より前)
    },
    "run-picks-short-term": {
        "task": "backend.tasks.run_picks_task",
        "args": ("short_term",),
        "schedule": crontab(hour=22, minute=2),  # JST 07:02(solo worker で直列実行のため実質は順番待ち)
    },
    # ...
    "sync-trends-morning": {
        "task": "backend.tasks.sync_trends_task",
        "schedule": crontab(hour=21, minute=45),  # JST 06:45(ピック生成 07:00 の前)
    },
}

対処3:手動実行と定期実行の食い違いを、実行順で防ぐ

9月25日には、次の順番で「公開済みの記事と台帳の食い違い」が起きました。

  1. 06:52 に抽出を手動実行。中長期は全件見送りで0件
  2. その結果で朝の記事を書いて公開
  3. 07:30 の定期実行が「今日の中長期は採用0件=まだ出ていない」と判定して再実行
  4. 1回目は AI の自信度38%で見送られた銘柄が、2回目は40%で採用ラインを越えた

定期実行には二重作成を防ぐガードがありますが、判定しているのは採用済みの件数のため、採用0件の日はやり直しが走ります。そこで「記事を書く前に定期実行を終える」よう、開始を 07:00 に前倒ししました(この判断は人間)。

前倒しの副作用として、抽出の材料になる朝のトレンド同期(07:13)が抽出より後になってしまいます。朝の分だけ sync-trends-morning(06:45、実測約1分)として切り出しました。なお「採用0件の日は再実行する」ふるまい自体は変えておらず、時刻の順番で起きにくくしただけです。

対処4:時刻と実行順をテストで固定する

締め切りのあるバッチの時刻は、うっかり変わると翌朝まで気づけません。テストで固定します。

backend/tests/test_beat_schedule.py(抜粋)
_JST_OFFSET_HOURS = 9


def _jst_hour_minute(entry_name: str) -> tuple[int, int]:
    schedule = celery_app.conf.beat_schedule[entry_name]["schedule"]
    (utc_hour,) = schedule.hour
    (minute,) = schedule.minute
    return (utc_hour + _JST_OFFSET_HOURS) % 24, minute


def test_mid_term_picks_run_at_0700_jst() -> None:
    assert _jst_hour_minute("run-picks-mid-term") == (7, 0)


def test_morning_trend_sync_runs_before_picks() -> None:
    assert _jst_hour_minute("sync-trends-morning") == (6, 45)
    assert _jst_hour_minute("sync-trends-morning") < _jst_hour_minute("run-picks-mid-term")

また、根拠になった実測値はコミットメッセージに残しています。

コミットメッセージ(2026-09-17)
fix: AI銘柄ピック生成の開始時刻を寄り付き前に間に合うよう前倒し

08:50 JST開始では実測23分半(中長期のみ)かかり、solo workerの直列実行と
合わせて寄り付き09:00に完了が間に合っていなかった。実測を基に安全マージンを
確保し、07:40/07:42 JST開始に変更する。

まとめ

  1. 外部の時計(東証の9時)が締め切りのバッチは、まず実測する
  2. 間に合わないなら、性能改善より先に開始時刻の前倒しで止血する
  3. --pool=solo では、タスクをずらしても直列の順番待ちになるだけ
  4. crontab を UTC で書くなら、換算後の JST をコメントに書く
  5. 時刻と実行順はテストで固定し、根拠の実測値はコミットメッセージに残す

気づいたのは人間、実測して案を出したのは Claude Code、採用して線を引いたのは人間、という分担でした。

次回は、事件が起きるたびに CLAUDE.md(AI の行動ルール)の条文が増えていった話を書きます。

☕ note.comで開発秘話と毎朝の検証ログを連載中

note.comでは、ALPHA FORGE の開発秘話と、毎朝のアルゴリズム検証ログを公開しています。

👉 元記事(note連載第4話):
【ALPHA FORGE 開発秘話 #04】寄り付きに間に合わない! 23分半かかった朝の銘柄抽出


免責事項
本記事は個人開発およびAIエージェントを活用したシステム開発の技術記録です。特定の銘柄の売買推奨や投資助言を行うものではありません。本ツールは証券会社等への発注機能を持ちません。記事中のコードは抜粋であり、パスや環境固有の値は省略しています。

⚠️ 【システム検証記録に関する免責事項・注意事項】

  • 本記事は、独自開発アルゴリズム「ALPHA FORGE」の動作検証およびデータ分析過程を公開する技術・運用の個人的な記録ログです。
  • 掲載されているすべてのデータ(各スコア、基準観測値、統計的変動上限、シナリオ無効化水準など)は、過去の市場データに基づき数式(ATR等)により機械的に算出されたバックテスト・シミュレーション用のパラメータであり、特定の有価証券の売買勧誘、取引の推奨、投資助言・代理行為を目的としたものではありません。
  • また、将来の株価変動や運用成果を保証するものではありません。実際の投資判断および最終決定は、必ずご自身の責任と判断において行っていただけますようお願いいたします。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?