Windows のタスクスケジューラで Python のジョブを毎日動かす|PowerShell 5.1・uv・Docker でつまずいた4点

Windows のタスクスケジューラで Python のジョブを毎日動かす|PowerShell 5.1・uv・Docker でつまずいた4点 開発記録

開発記録(第3回)。AI に日本株の売買判断をさせる投資システム investTool を個人で開発しています。全体像は目次記事にまとめています。

この記事の位置づけ:開発の段階のうち「段階1 データ収集」の時期の話

この記事の要点
  • 株価・適時開示・ニュースの取得や仮想売買の計算など、22個のジョブを自宅の Windows PC のタスクスケジューラで毎日動かしています
  • 時刻の定義は Python 側の1か所にまとめ、PowerShell のスクリプトがそれを読んでタスクを登録します
  • PowerShell 5.1 の文字コード、uv run の自動同期、繰り返しのトリガー、Docker の速度の4点でつまずきました

動作環境

項目 内容
OS Windows 10 Pro
シェル Windows PowerShell 5.1
Python の実行 uv 0.12、Python 3.12
データベース SQLite

やりたかったこと

前回作ったデータの取得を、毎日決まった時刻に自動で動かすことです。平日の時間割は次のとおりです。

平日の定時ジョブの時間割。朝6時半から夜23時まで、ニュースは2時間ごと、適時開示は1時間ごと、日足は16時半に取得し、21時台に絞り込み・仮想売買・通知を行う。Bot の見張りは5分ごと

investTool は自宅の PC で動かしています。ブログを置いているレンタルサーバーでも Python の実行や cron による定時実行はできますが、標準の Python がシステムの要件(3.12)より古い 3.6 系でした。また、画面用の API サーバーや Discord の Bot のように、常に動き続けるプログラムを置く用途には向いていないため、自宅の PC を選びました。PC を24時間つけっぱなしにはしないので、予定時刻に PC が止まっていても、後で取りこぼしを実行できることも条件にしました。

手順

時刻の定義を1か所にまとめる

タスクスケジューラに1つずつ手で登録すると、どの時刻に何を動かしているかがコードから分からなくなります。そこで、時刻の定義は Python のコード(schedule.py)の1か所だけに書き、PowerShell のスクリプトがそれを読んで登録する形にしました。

スケジュールの登録から実行まで。schedule.py の定義を register_tasks.ps1 が読んでタスクスケジューラに登録し、予定時刻に PowerShell から uv run --no-sync で invest job を実行する。ログオン時とスリープ復帰時には invest catchup で取りこぼしを実行する

Python 側の定義は、次のような形です(一部)。

SCHEDULE: list[ScheduleEntry] = [
    ScheduleEntry("backup-db", "backup-db", _times("03:00"), "daily"),
    ScheduleEntry("fetch-news", "fetch-news", _times(*[f"{h:02d}:00" for h in range(7, 24, 2)]), "daily"),
    ScheduleEntry("fetch-daily-prices", "fetch-daily-prices", _times("16:30"), "weekdays"),
    # ……
]

登録用の PowerShell スクリプトは、これを JSON で受け取ってタスクを作ります。

$json = & $Uv run --project $BackendDir invest schedule list --json
$tasks = $json | ConvertFrom-Json

同じ定義を、取りこぼしの判定と、画面に出す「次回の実行予定」にも使っています。

タスクの設定

各タスクには、次の設定を入れています。

設定 値 理由
StartWhenAvailable 有効 予定時刻に PC が止まっていたら、次に起動したときに実行する
MultipleInstances IgnoreNew 前回の実行が終わっていなければ、新しい実行を始めない
ExecutionTimeLimit 3時間 止まったままのジョブを打ち切る
実行ユーザー ログオン中のユーザー 管理者の権限なしで登録できる
スリープの解除 しない 夜中に PC を起こさない

取りこぼしを実行する

StartWhenAvailable はタスクスケジューラ側の仕組みです。これとは別に、investTool 自身の実行記録を見て取りこぼしを探す仕組みも用意しました。ログオン時とスリープ復帰時に invest catchup を実行するタスクを登録しています。このコマンドは、予定時刻を過ぎているのに実行記録がないジョブを探して実行します。

スリープからの復帰は、Windows のイベントログ(Microsoft-Windows-Power-Troubleshooter のイベント ID 1)をきっかけにしています。

$eventClass = Get-CimClass -ClassName MSFT_TaskEventTrigger -Namespace Root/Microsoft/Windows/TaskScheduler
$resume = New-CimInstance -CimClass $eventClass -ClientOnly
$resume.Enabled = $true
$resume.Subscription = '<QueryList><Query Id="0" Path="System"><Select Path="System">*[System[Provider[@Name=''Microsoft-Windows-Power-Troubleshooter''] and EventID=1]]</Select></Query></QueryList>'
$resume.Delay = 'PT2M'  # ネットワークの復帰を待つ

復帰の直後はネットワークがつながっていないことがあるため、2分待ってから実行します。

つまずいた点と解決

1. PowerShell 5.1 がスクリプトを読めない

症状
登録用のスクリプトの日本語が文字化けし、正しく動かない
原因
PowerShell 5.1 は、BOM のない UTF-8 のファイルを Shift_JIS として読む
対処
スクリプトを BOM 付きの UTF-8 で保存する

登録用のスクリプトを実行すると、日本語の部分が文字化けし、スクリプトが正しく動きませんでした。当時のエラーメッセージの原文は残していません。

Windows に標準で入っている PowerShell 5.1 は、BOM(ファイルの先頭に付ける、文字コードを示す印)のない UTF-8 のファイルを、UTF-8 として読みません。日本語版の Windows では Shift_JIS として読むため、UTF-8 で書いたコメントや文字列が化けます。

ポイント

PowerShell 7 は BOM がなくても UTF-8 として読みます。ただ、Windows に最初から入っているのは 5.1 のため、スクリプトは BOM 付きにそろえています。

2. Docker で動かすと、SQLite が極端に遅い

症状
Docker で PC のフォルダをマウントすると、API の応答が2.2秒から約11秒に遅くなる
原因
Windows とコンテナの間のファイルの読み書きが遅い
対処
Docker は動作確認だけに使い、日々の運用は PC で直接動かす

最初は、将来クラウドに移すことを考えて Docker で動かすことも検討しました。しかし Windows の Docker Desktop で、PC 側のデータのフォルダをコンテナにつなぐ(マウントする)と、SQLite の読み込みが大幅に遅くなりました。2026年9月15日に、銘柄を検索する API で比べた結果は次のとおりです。

実行のしかた 応答時間
PC で直接実行 2.2秒
Docker(PC のフォルダをマウント) 約11秒。キャッシュが空のときは60秒を超えてタイムアウト
Docker(同じ DB をコンテナの中のディスクに置く) 0.1〜0.5秒

コードの問題ではなく、Windows とコンテナの間でファイルを読み書きする部分が遅いことが原因です。Docker はクラウドに移すときのための動作確認だけに使い、日々の運用は PC で直接動かすことにしました。

3. API の起動中に、定時ジョブが失敗する

症状
画面用の API を起動していると、定時ジョブが失敗する
原因
uv run が実行前に環境を同期し、実行中の invest.exe を置き換えようとする
対処
uv run --no-sync で起動し、実行のたびに同期しない

タスクからは、uv run でジョブを起動していました。uv run は、実行の前にプロジェクトの環境を自動で同期します。git pull などでファイルが更新されていると、uv はこのプロジェクト自体のパッケージ(invest)を入れ直そうとします。

このとき、画面用の API サーバー(invest api)が動いていると、実行中の invest.exe を置き換えられず、ジョブが失敗しました。こちらも、当時のエラーメッセージの原文は残していません。

タスクからの起動を uv run --no-sync に変え、実行のたびに同期しないようにして解決しました。

$command = "& '$Uv' run --no-sync --project '$BackendDir' invest $quoted"
注意

その代わり、依存するパッケージや pyproject.toml を変えたときは、API を止めてから手動で uv sync を実行する必要があります。

4. 「プロパティ ‘Interval’ が見つかりません」で登録が止まる

症状
タスクの登録が「このオブジェクトにプロパティ ‘Interval’ が見つかりません」で止まる
原因
New-ScheduledTaskTrigger -Daily のトリガーは Repetition が空で、繰り返しの間隔を後から設定できない
対処
-Once で作ったトリガーの Repetition を、毎日のトリガーに差し込む

これは少し後の、2026年9月18日の出来事です。Discord の Bot が止まっていないかを5分ごとに確かめるタスクを追加したところ、登録用のスクリプトが次のエラーで止まりました。

このオブジェクトにプロパティ 'Interval' が見つかりません。

スクリプトはこのタスクの登録で異常終了し、このタスクと、その後に登録する2つを合わせた3つ(Bot の見張り、取りこぼしの実行、Bot 本体)が登録されないままになっていました。

原因は、New-ScheduledTaskTrigger -Daily が返すトリガーの Repetition が空(null)で、後から繰り返しの間隔(Interval)を設定できないことでした(Windows 10、PowerShell 5.1)。繰り返しの仕組みはこの前に入れていましたが、繰り返しを使うタスクがなかったため、この処理は一度も実行されていませんでした。

-Once に -RepetitionInterval を付けて作ったトリガーには Repetition があるので、それを毎日のトリガーに差し込む形で解決しました。

$at = [datetime]::ParseExact($t.times[0], 'HH:mm', $null)
$repeatTrigger = New-ScheduledTaskTrigger -Daily -At $at
$repeatTrigger.Repetition = (New-ScheduledTaskTrigger -Once -At $at `
    -RepetitionInterval (New-TimeSpan -Minutes $t.repeat_minutes) `
    -RepetitionDuration (New-TimeSpan -Days 1)).Repetition
無期限の繰り返しは指定できない

繰り返しの期間に [TimeSpan]::MaxValue(無期限)を渡す方法もありますが、P99999999DT23H59M59S という値になり、タスクスケジューラが受け付けませんでした。「毎日その時刻に始めて、24時間繰り返す」という形にしています。

修正の後、22個のタスクがすべて登録され、見張りのタスクが「毎日 0:00 開始・5分ごと・1日間」になっていることを確認しました。

実際にどう使っているか

登録したタスクは、タスクスケジューラの「investTool」フォルダにまとめています。登録し直すときは、フォルダの中の既存のタスクをすべて消してから作り直すので、時刻を変えたときもスクリプトを1回実行するだけです。

ジョブの実行結果はデータベースに記録し、失敗したときは Discord に通知します。実行記録があるので、取りこぼしの判定もこの記録と予定時刻を突き合わせるだけで済んでいます。

まとめ

今回の4点は、どれも Windows で動かしていることに関係する問題でした。実行中のファイルを置き換えられない点(3点目)も、Windows のファイルの扱いによるものです。Linux のサーバーで cron を使えば避けられるものが多いのですが、自宅の PC で動かすと決めた以上、ここを通る必要がありました。


前回:Python で日本株の日足・開示・ニュースを毎日集める|無料のデータ源でつまずいた5点

次回:日足だけでバックテストの約定を決める|損切りと利確が同じ日に届いたら、損切りを先にする

当サイトは情報提供を目的としており、投資助言を行うものではありません。掲載している売買記録は仮想売買(ペーパートレード)であり、実際の取引の結果ではありません。投資判断はご自身の責任で行ってください。詳しくは免責事項をご覧ください。

タイトルとURLをコピーしました