定期実行(Task Scheduler)で株価を毎日自動取得
Python経験者向けの投資分析シリーズ第14回。1回起動したら取って・SQLiteに入れて・ログを残して終わるスクリプトを書き、「毎日」はOS側(タスクスケジューラ/cron)に丸投げします。2回走っても行が増えないUPSERT、休場日カレンダーを持たない設計、スケジューラの中で print() は見えない——「毎日動かす」ときだけ出る問題を実測ログつきで並べます。
- 1. この記事のゴールと連載の位置づけ
- 2. 何をためるか — テーブル設計と、日付キーの決め方
- 3. 1回走らせたら必ず終わるスクリプトにする
- 4. つまずき実演①:2回走らせたら、同じ行が2つ入った
- 5. つまずき実演②:休場日に走ったら、何が返ってくるのか
- 6. ログと終了コード — スケジューラの中では print() は誰にも見えない
- 7. Windows:タスクスケジューラにGUIを開かずに登録する(schtasks)
- 8. つまずき実演③:schtasks に無いスイッチが3つある
- 9. Linux(Mint):crontab -e で登録する
- 10. 常駐を選ばなかった理由/この仕組みで分からないこと
- 11. よくある質問(FAQ)
1. この記事のゴールと連載の位置づけ
本記事は「Python経験者が、投資ポートフォリオを管理・可視化するアプリを作る」連載の第14回です。第13回で記録を1画面に束ねたので、次はその画面に出すものを毎日供給する側です。第1回の「取りに行く回数を減らす」4箇条には、こう書きました。
取得した終値を SQLite に保存し、「当日ぶんは再取得しない」設計にする(保存設計は連載の後半で扱います)
その「後半」がここです。第1回は取ってきて終わり、第8回はSQLiteに手で入力して貯める回。今回は誰も操作していない時間に、勝手に取ってきて貯まる状態を作ります。
作るのは「毎日決まった時刻に値を取ってきて、自分のPCのファイルに積むだけの仕組み」です。売買はしません。売買の判断もしません。ためた値が高いのか安いのか、上がったのか下がったのかも一切扱いません。注文を出す機能に繋ぐ話も出ません(10章)。登場する SMPL-01 〜 SMPL-03 はすべて架空の銘柄コードです。当サイトは投資助言業(金融商品取引業)の登録をしておらず、個別銘柄の推奨や売買タイミングの助言は行いません。
できあがるのは1本のスクリプトと、1つのDBファイルと、1日1行ずつ伸びるログです。スクリプトは1回起動すると必ず終わり(while True を書きません)、いつ呼ばれても同じ結果になることだけに責任を持ちます。
実行の確認はLinux Mint 22.3(Python 3.12.3 / yfinance 1.7.0)で行い、cron登録も走らせてログを採りました。一方のWindows側は、この記事のために新しいタスクを作っていません。掲載する schtasks の出力は、同じPCで採取したヘルプ全文と、別タスクの照会結果です。7章・8章で書けるのは「コマンドの仕様」までで、「Windowsでも同じように動きました」とは書けません。yfinanceの版もWindowsが 1.4.1、Mintが 1.7.0 と違います。同日に両方で走らせ、戻り値の形と既定値が両OS・両バージョンで一致することは確認しました(値そのものは採取時刻で動きます)——この断りは各章でくり返しません。
2. 何をためるか — テーブル設計と、日付キーの決め方
第1回では分足の取得制限を説明し「日足に落とすか、毎日取得して SQLite などに自分で蓄積していく設計へ切り替えてください」と書きました。テーブルは1つだけです。
CREATE TABLE IF NOT EXISTS prices (
code TEXT NOT NULL, date TEXT NOT NULL,
open REAL, high REAL, low REAL, close REAL NOT NULL, volume INTEGER,
fetched_at TEXT NOT NULL,
PRIMARY KEY (code, date)
)
PRIMARY KEY (code, date) の1行が、この記事のほぼ全部を決めています。「同じ銘柄の同じ取引日は1行だけ」をDB側に持たせれば、何回呼ばれても結果は同じです。定期実行では同じ日に2回走ることが普通に起きるので、この保証がないと数字が壊れます(4章)。
date は「実行した日」ではなく「取引日」
date.today() を入れたくなりますが、それはスクリプトが動いた日であって値がついた日ではありません。土曜に走らせて金曜の終値が返ると、「土曜の終値」という存在しない行ができます。キーはyfinanceが返したDataFrameのindex(=取引日)です。これだけで休場日を判定するコードが要らなくなります(5章)。
fetched_at は「いつの値か」と「いつ取ったか」を分けるためです。SQLiteの datetime('now') はUTCなので、日本時間は datetime('now','localtime') と書きます。
DBは新しいファイルにする(第8回のDBには1行も書かない)
貯め先は prices.db という新規ファイル。第8回で作った dividends.db にはテーブルも足さず、1行も書き込みません。手で入力した記録と、機械が毎日書き足すものを混ぜない——バグったときの被害をファイル1つで止めるためです。prices.db は「自分がどの銘柄を毎日見ているか」の記録なので、クラウド同期や共有フォルダには置かないでください(.gitignore に *.db が最低限の防波堤)。
UPSERT(4章)が使えるのは SQLite 3.24.0(2018-06-04)以降です(公式・2026年9月10日確認。実測はMint 3.45.1/Windows 3.49.1)。
3. 1回走らせたら必ず終わるスクリプトにする
Pythonの側に「毎日」を持たせません。起動したら、取って、入れて、ログを書いて、終了コードを返して終わり。常駐しないので、常駐型の悩み(10章)は起きません。
from pathlib import Path
# カレントに依存しないよう、置き場所を基準に絶対パス化する
BASE_DIR = Path(__file__).resolve().parent
DEFAULT_DB = BASE_DIR / "prices.db"
DEFAULT_LOG = BASE_DIR / "logs" / "fetch.log"
この3行が両OSのつまずきを同時に潰します。タスクスケジューラもcronもスクリプトの置き場所をカレントにしません(9章)。手で叩くと動くのにスケジューラから呼ぶとDBが変な場所に——この事故はここで消えます。
取得は第1回の関数をそのまま持ってくる
1銘柄ぶんを取る関数は、第1回の fetch_latest_price() と同じ形。返すものを「最新の終値」から「DataFrame」に変えただけで、①空データと例外を分ける ②例外は握りつぶさずログに残す ③失敗は None に揃える、はそのままです。
df = yf.Ticker(code).history(period=period, timeout=timeout)
if df is not None and not df.empty:
return df
log.warning("%s: 空のデータが返りました(銘柄コードを確認)", code)
return None # 空はリトライしても変わらない。例外のときだけ 2**attempt 秒待って再試行
違いは print() が log.warning() になったところだけ(理由は6章)。複数銘柄は yf.download(codes, period=period, group_by="ticker", progress=False, threads=False) でまとめて1リクエストにし、取れなかったぶんだけ上の関数で取り直します。
download() と Ticker.history() は返す形が違う実測では download() の列はMultiIndex(('Close', '1306.T'))でindexはタイムゾーンなし、Ticker.history() の列は単層でindexは Asia/Tokyo 付き。両方の戻り値を同じ関数に流すと、どちらかで壊れます。
毎回「直近5営業日」を取り直す(period="5d")
この記事でいちばん効いている設計判断です。1日1行だけ取ると、走らなかった日が穴のまま残ります。毎回5営業日ぶんを取って主キーで潰せば、数日PCを落としても次の1回で埋まります。スケジューラ側の「取りこぼしを後から実行する」機能に頼らずに済み、両OSで同じ挙動です(取りに行く回数は1日1リクエストのまま、増えるのは受け取る行数だけ)。
第1回の4箇条を、正直に突き合わせます。3つは実装し、1つは設計を変えました。
| 第1回の助言 | この記事での扱い |
|---|---|
| 一括取得を使う | 実装した。1リクエストにまとめ、失敗分だけ個別に取り直す |
| 結果をキャッシュ(当日ぶんは再取得しない) | 設計を変えた。毎回 period="5d" を取り直してUPSERTする。理由は穴埋めと、過去の値が書き換わること(4章) |
軽い属性を選ぶ(fast_info) | 使っていない。現在値まわりの約20項目だけで、過去日足の系列は返らない |
間隔を空ける(time.sleep()) | 実装した。個別に取り直すループに --sleep(既定1.0秒)を挟む |
以下は完成版 stock_daily_fetch.pyの使い方。
python stock_daily_fetch.py --demo # 通信せず架空データで動かす
python stock_daily_fetch.py --codes ^N225,1306.T # ここだけ外部と通信する(cmdでは引用符で囲む・7章)
python stock_daily_fetch.py --show 4 # DBの件数と直近の行を表示
python stock_daily_fetch.py --print-cron # crontab に貼る行を表示
終了コードは0(1件以上保存)と1(保存0件)の2つだけを sys.exit(main()) で返します。--demo は完全にオフライン。この記事の画面はすべてこれです。通信するのは --codes のときだけ、送るのは銘柄コードだけです。
4. つまずき実演①:2回走らせたら、同じ行が2つ入った
素直に INSERT を書くと何が起きるか、メモリ上のDBで実演します(同じ2行を2回)。
# p1 は主キーを付けていないテーブル
rows = [("SMPL-01", "2026-09-09", 1000.0), ("SMPL-01", "2026-09-10", 1010.0)]
for _ in range(2):
c.executemany("INSERT INTO p1 VALUES (?,?,?)", rows)
print(c.execute("SELECT COUNT(*) FROM p1").fetchone()[0]) # → 4
2行のはずが4行、エラーは出ません。1か月放置すれば同じ日の終値が何十行も並びます。手動テスト・時刻の変更・取りこぼしの再実行と、2回走る理由はいくらでも。複合主キーで重複は防げますが、今度は2回目が丸ごと落ちます。
IntegrityError -> UNIQUE constraint failed: p2.code, p2.date
答えはUPSERT(ON CONFLICT ... DO UPDATE)
UPSERT is a clause added to INSERT that causes the INSERT to behave as an UPDATE or a no-op if the INSERT would violate a uniqueness constraint.(UPSERT・2026年9月10日確認)
配布コードのSQLです。excluded. は「いま入れようとした値」を指す修飾子で、付けないと既存行の値を指します。
INSERT INTO prices (code, date, open, high, low, close, volume, fetched_at)
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now','localtime'))
ON CONFLICT(code, date) DO UPDATE SET
open = excluded.open, high = excluded.high, low = excluded.low,
close = excluded.close, volume = excluded.volume,
fetched_at = excluded.fetched_at
同じ行を投げても件数は増えず、値だけ更新されます。「同じ日を何回取っても壊れない」——これが定期実行の前提です。
fetched_at だけが 15:33:15 → 15:33:34 に変わった(架空データ)なぜ INSERT OR IGNORE ではなく DO UPDATE なのか
重複を無視するだけなら INSERT OR IGNORE で足ります。更新にするのは過去の行の値が後から変わることがあるからです。実測では Ticker.history() も download() も auto_adjust=True が既定で(警告0件)、調整後の価格は株式分割や配当で過去側にさかのぼって変わります。無視すると、古い値が永久に残ります。
もう1つの候補 INSERT OR REPLACE は行を消してから入れ直す動きです。既定値 'memo' の列にメモを入れ、その列を省略して試すと、OR REPLACE では既定値に戻り、UPSERTでは残りました。UPSERTは指定した列しか触りません。あとからメモやフラグを足すときに効く差です。ログの「新規n件・更新m件」はexecutemany() からは取れない(changes() は最後の1文ぶんだけ)ので、配布コードは前後の COUNT(*) の差を取っています。
5. つまずき実演②:休場日に走ったら、何が返ってくるのか
毎日走らせるなら土日と祝日にも走ります。何が返るか確かめないと静かに壊れるので、土日をまたぐ期間で実測しました(2026年9月5日は土曜、6日は日曜)。
d = yf.Ticker("1306.T").history(start="2026-09-04", end="2026-09-10")
print(d.shape, [str(x) for x in d.index])
# shape (4, 8)
# ['2026-09-04 00:00:00+09:00', '2026-09-07 …', '2026-09-08 …', '2026-09-09 …']
土日の行は最初から返ってきません。9月4日(金)の次は9月7日(月)です。ついでに2つ分かります。end は排他的(end="2026-09-10" だと9月10日の行は入らない=4行しかない理由)、そしてperiod="5d" は暦の5日ではなく直近5営業日でした。
だから、休場日カレンダーを持たない
祝日一覧を持って「今日は休場だから走らない」と判定する必要はありません。取引が無かった日は行そのものが返らず、DBには何も足されません。「返ってきたindexをキーにする」(2章)と決めた時点で、休場日の判定は実装から消えます。祝日リストの更新も要りません。
上の実測は平日(木曜)に、土日を含む期間を指定して行いました。土曜や日曜に実行したとき当日の行がどう扱われるか、period="1d" が空になるかは確認できていません。だからこそ本記事は「休場日には何も返らないはず」に頼らず、何が返ってきても主キーで潰れる形(4章)にしてあります。
実行時刻は、取引時間が終わってから
同じ日の15時13分(Mint・yfinance 1.7.0)と15時15分(Windows・1.4.1)に1回ずつ取得すると、当日の終値が違いました(指数で 64,892.14 と 64,943.89。取得時点の参考値で、推奨ではありません)。別のPC・別の版なので、原因を時刻差だけには切り分けていませんが、まだ場が動く時間帯の値を「終値」として拾っているのは確かです。取引時間の終了時刻は取引所が決めるもので、変更されることもあります。一次情報で確認できていないため具体的な時刻は書きません。日本取引所グループの営業時間・休業日一覧を見て、取引が終わってから、余裕を持った時刻を選んでください(本記事の18:30は一例)。cronの曜日欄を 1-5(平日)にするのは無駄な実行を減らす最適化で、正しさの担保ではありません——祝日は除外できないからです。
6. ログと終了コード — スケジューラの中では print() は誰にも見えない
手で叩くうちは print() で十分です。スケジューラから呼ばれた瞬間、その出力を見る人間がいなくなります。昨日動いたか確かめる方法が無いと、欠落に気づけません。やることは logging でファイルに書くだけ、encoding="utf-8" を省かないこと(Windowsで日本語ログが化ける一因)。
fh = logging.FileHandler(log_path, encoding="utf-8") # 文字コードは明示する
fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"))
log.addHandler(fh)
外部から取得したログです(^N225 と 1306.T をMintで続けて2回)。
2026-09-10 15:27:04 [INFO] 開始 db=real.db demo=False period=5d
2026-09-10 15:27:04 [INFO] 対象 2件: ^N225, 1306.T
2026-09-10 15:27:06 [INFO] 保存 10件(新規 10 / 更新 0)
2026-09-10 15:27:06 [INFO] 終了 exit=0
2026-09-10 15:27:11 [INFO] 保存 10件(新規 0 / 更新 10)← 4秒後の2回目・合計は10件のまま
「開始・対象・保存の内訳・終了コード」の4行で、「昨日の件数」を追えます(この回はDB合計10件)。
終了コードは、スケジューラへの返事
存在しない銘柄コードを渡すと、リトライののち [ERROR] 終了 保存0件 exit=1 で終了。この値はWindowsなら Last Result、Linuxならcronの実行結果に出ます。0以外を返さないと、スケジューラからは「毎日成功している」ようにしか見えません。
アプリのログとは別に、リダイレクトも要る
logging があれば >> log 2>&1 は不要に思えますが、両方要ります。1つ目はPythonが起動する前に落ちた場合——インタプリタが見つからなければ、アプリのログには1文字も残りません。2つ目はライブラリが直接標準出力に書く行です。上の失敗では 1 Failed download: ['SMPL-01']: No data found, symbol may be delisted という logging を通らない行が端末に出ました。この行がリダイレクトしないと消えます。
ついでに、グラフも画面なしで描ける
第9回の「matplotlib.use("Agg") を先に呼ぶ」が効きます。--chart はDBを読んで折れ線PNGを書き出すだけで、画面のないSSH越しでも同じPNGができます。
--chart の出力。値はすべて架空のサンプルデータで、実在の銘柄とは無関係7. Windows:タスクスケジューラにGUIを開かずに登録する(schtasks)
ウィザードを1画面ずつ進める解説が主流ですが、ここではコマンドだけで登録・確認・テスト・削除まで通します。
継続行の記号がcmdは ^、PowerShellは `と違い、cmdでは ^ 自体がエスケープ文字です。^N225 のような引数は必ず引用符で囲んでください(囲まないと N225 になります)。
まず .bat を1枚かませる
いきなり python.exe を登録せず、バッチを1枚挟むのが確実です。作業ディレクトリの固定と文字コード指定をここで済ませます。
@echo off
cd /d "%~dp0"
"C:\Users\ユーザー名\stockdb\.venv\Scripts\python.exe" -X utf8 "stock_daily_fetch.py" --codes "^N225,1306.T"
exit /b %ERRORLEVEL%
%~dp0 はこのバッチが置かれたフォルダ。cd /d で移動すれば、カレントが何であっても同じ結果です。-X utf8 は環境変数を汚さない文字化け対策、exit /b %ERRORLEVEL% はPythonの終了コードをタスクへ返します。仮想環境でもactivate は不要——Python公式が「呼び出すときにフルパスを指定すればよいので有効化の必要は特にない」と明記しています(venv・2026年9月10日確認)。
登録・確認・テスト・削除
schtasks /create /tn "\PythonLand\StockDaily" /tr "'C:\Users\ユーザー名\stockdb\run_fetch.bat'" /sc DAILY /st 18:30 /f
schtasks /query /tn "\PythonLand\StockDaily" /v /fo LIST ← 確認(/run で即時テスト・/delete /f で削除)
--print-schtasks を付けると、上の .bat と登録コマンドが実パス入りで出ます。
押さえどころは公式から(Microsoft Learn「schtasks create」と schtasks /create /? の出力・いずれも2026年9月10日確認)。
| 項目 | 内容 |
|---|---|
/tr はフルパス | 「パスを書かないと System32 のものとみなす」「262文字以内」と明記 |
| 引用符の入れ子 | 空白入りパスは外側 "・内側 ' か \"(ヘルプに "one set for CMD.EXE and one for SchTasks.exe") |
/rl・/f・/tn | /rl は逐語 "The default is LIMITED."=管理者権限は不要。/f は作り直し、/tn の \ はフォルダ分け。/delay はDAILYでは不可(ONSTART・ONLOGON・ONEVENT 専用) |
| パスワード | 公式に "always prompts for a password unless you provide one"=聞かれても壊れていない |
登録できたかは /query /v /fo LIST で読む
/v はLIST か CSV でしか効きません。見るのはNext Run Time・Last Run Time・Last Result・Start In・Power Management。同じPCの別タスク(月1回実行)では Last Result: 0・Start In: N/A・Power Management: Stop On Battery Mode, No Start On Batteries でした。Start In: N/A は作業ディレクトリ未設定(だから .bat で cd /d)、Power Management は8章で効きます。なおこのPCの schtasks は英語表示で、日本語の項目名は未採取です。
Last Result が 0 以外だったら
公式の定数表(Task Scheduler Error and Success Constants・2026年9月10日確認)で引けます。0x80041320="…the user is not logged on."(ログオンしていない時刻だった)、0x80041324="…one of the constraints in the task definition."(バッテリー駆動・既に実行中など=8章)。0x1 がスクリプト自身の終了コードなのかは確認できていません。まずはアプリのログ(6章)。何も無ければPythonが起動する前の問題——パスか、引用符か、権限です。
/RU SYSTEM と /NP の扱い(言い切れないこと)「ログオフ中も動かしたい」に /RU SYSTEM を勧める記事がありますが、SYSTEMは公式に "a highly privileged account" とある特権アカウントで過剰です。/NP はヘルプの "Only local resources are available." が通信できないようにも読めますが、このPCでは同じ方式(S4U)の別タスクが毎日インターネット経由のAPI取得に成功しており、「/NP では取れない」とは言い切れません。取得タスク自体は未実測なので、本記事はログオン中に走らせる形を既定にします。
8. つまずき実演③:schtasks に無いスイッチが3つある
GUIの解説が「ここにチェックを入れましょう」と案内する項目のうち、schtasks のコマンドラインから指定できないものが3つあります。このPCで schtasks /create /? を実行し、スイッチを数えました。
/S /U /P /RU /RP /SC /MO /D /M /I /TN /TR /ST /RI /ET /DU /K /SD /ED /EC
/IT /NP /Z /XML /V1 /F /RL /DELAY /HRESULT /?
30個あります。この中に①作業ディレクトリ(GUIの「開始(オプション)」)②「開始できなかった場合、すぐにタスクを開始する」③バッテリー駆動時の条件に相当するものは1つもありません。ただしタスクスケジューラ自体に機能が無いわけではなく、GUIにもXMLにもあります。無いのはスイッチだけで、埋め方は①.bat ラッパー(①だけなら十分・本記事の既定)②/XML ③PowerShellのコマンドレットの三択です。
②「開始できなかった場合はすぐ開始」= StartWhenAvailable
スケジュール時刻にPCがスリープや電源オフだと、そのタスクは既定では実行されません。公式(StartWhenAvailable Elementほか・2026年9月10日確認)から分かるのは3つ。スキーマ定義が default="false"=既定は無効、有効にしても "The default delay is 10 minutes."=すぐには走らない、適用範囲にも "applies only to time-based tasks with an end boundary or …set to repeat infinitely" という条件付きです。
この条件があるため、単純な「毎日1回」で期待どおり効くのかは文面だけでは判断できません。当サイトはPCをスリープさせて時刻をまたぐ確認をしていませんので、「XMLに1行足せば遅れた分も走ります」とは書けません。
③バッテリー駆動だと、そもそも走らない
影響が大きいのに知られていない既定です。DisallowStartIfOnBatteries の公式Remarksは "The default setting for this element is True."——ノートPCで18:30に設定しても、その時刻にACアダプタが繋がっていなければ実行されません。7章で引いた月次タスクの Power Management: Stop On Battery Mode, No Start On Batteries が、この既定の姿です(Last Result が 0x80041324 になる典型例)。
3つとも指定するならPowerShellです。New-ScheduledTaskSettingsSet に StartWhenAvailable・AllowStartIfOnBatteries(肯定形)・WakeToRun、New-ScheduledTaskAction に WorkingDirectory。実在はこのPCで確認しました(ScheduledTasks 1.0.0.0・PowerShell 5.1)。-WakeToRun は勧めません——閉じたはずのノートPCが勝手に起きるのは事故のもとです。
ただし、この3つは「あれば嬉しい保険」です。本命は3章の period="5d" + 4章のUPSERT——OS側の再実行機能に頼ると、それが動くかどうかにデータの完全性が乗ります。
9. Linux(Mint):crontab -e で登録する
ここからは実際に走らせた側。環境はLinux Mint 22.3(Ubuntu 24.04 noble)。systemctl is-active cron は active、dpkg -l には cron 3.0pl1-184ubuntu2 と anacron 2.3-39ubuntu2。登録は crontab -e で、初回は使うエディタを選ぶプロンプトが出ます。書く内容は1行だけです。
30 18 * * 1-5 /home/ユーザー名/stockdb/.venv/bin/python /home/ユーザー名/stockdb/stock_daily_fetch.py --codes '^N225,1306.T' >> /home/ユーザー名/stockdb/logs/cron.log 2>&1
左から分・時・日・月・曜日、1-5 は月〜金。貼る前に mkdir -p /home/ユーザー名/stockdb/logs を1回だけ。フォルダが無いとシェルがリダイレクト先を開けずに終わり、Pythonは起動せず6章のログにも残りません。--print-cron を付けるといま動いているPythonの実パスで埋めた行が出ます。
なぜ全部フルパスなのか — cronが渡す環境を実測した
cronから起動されたプロセスに何が渡るのか、/usr/bin/env を1分だけ仕掛けて採りました。たった6個。
HOME=/home/ユーザー名 LOGNAME=ユーザー名 LANG=ja_JP.UTF-8
SHELL=/bin/sh PWD=/home/ユーザー名 (umask)0002
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:…:/snap/bin
①PATHは、この環境ではログインシェルと同じでした。「cronのPATHは /usr/bin:/bin だけ」という説明を見かけますが、このMintでは違います。それでも python と書いてはいけません——仮想環境の bin はPATHに入りません。②シェルは /bin/sh(ログインシェルは /bin/bash)でbash固有の書き方は通りません。③カレントは $HOME、.bashrc も読まれません(3章の Path(__file__).resolve().parent が効くのはここ)。
つまずき実演④:% を書いた瞬間に壊れる
crontabではコマンド中の % は改行に化け、最初の % より後ろは標準入力に渡されます(crontab(5) の man に明記)。ログ名に日付を入れると必ず踏むので、同じ内容の2行を1分ジョブで実測しました。
* * * * * /bin/date +%Y-%m-%d >> /tmp/pl_demo/pct_ng.log 2>&1
* * * * * /bin/date +\%Y-\%m-\%d >> /tmp/pl_demo/pct_ok.log 2>&1
エスケープしないほう(pct_ng.log)はファイルすら作られず、pct_ok.log だけに 2026-09-10 が2行入りました。リダイレクト記号ごと標準入力に流れたからです。\% と書くか、日付の整形はPython側でやる(本記事はこちら)かの二択です。出力先を MAILTO に頼るのも避けます。manは「未設定ならオーナー宛にメールする」と言いますが、家庭のLinuxにその仕組みは無いことが多く、書かないと出力はどこにも残りません。
実際に走らせた結果と、止まっていた時刻の扱い
1分ごとのジョブで2回走らせたところ(架空データの --demo・1章の画像)、1回目は「新規 90 / 更新 0」、2回目は「新規 0 / 更新 90」で、DB合計は90件のままでした。確認が済んだら crontab -r で消します(-e と -r は隣同士、-r は確認なしにその利用者のcron設定を全部消します)。
止まっていた時刻の扱いは、出典に注意が要ります。cron(8) のmanには「システムが停止していた時刻のジョブがどうなるか」は書かれていません。書かれているのは anacron 側です。
Unlike cron, it does not assume that the machine is running continuously.(anacron(8)・2026年9月10日確認)
「連続稼働を前提にしない」ためのツールが別にある——ここから、cron自身はその面倒を見ないと読むのが正確です。このMintでanacronが呼ぶのは /etc/cron.daily/ などシステム側のジョブで、crontab -e で自分が書いた行は現れません。この非対称——Windowsには「遅れた分を後で走らせる」設定がある(8章・効き方は未確認)/ユーザーのcronには無い——も、本記事の設計なら結果は同じです。毎回5営業日ぶんを取り直すので、3日止めていても次の1回で埋まります。
10. 常駐を選ばなかった理由/この仕組みで分からないこと
「毎日9時に実行」はPythonの中でも書けます。よく使うPythonライブラリ51選の schedule がそれです。採らなかったのは用途が違うからで、優劣ではありません。
| 観点 | 常駐(schedule など) | OSのスケジューラ(本記事) |
|---|---|---|
| 動く条件 | そのプロセスが動き続けていること | その時刻にPCが動いていること |
| 落ちたら・再起動したら | 止まる。気づく仕組みも再起動も自前 | 次の時刻にOSがまた起動する。登録も残る |
| 向いている用途 | アプリが動いている間の定期処理 | 電源やアプリの生死に依存しない日次バッチ |
「毎日1回、値を取ってファイルに積む」は後者。この仕組みは注文には繋ぎません。出口は prices.db というファイル1つ。証券会社のAPIを叩く・条件に合ったら発注する、といった方向へは本連載は一歩も進みません。
ここでやったのは「1本のスクリプトを、毎日確実に走らせる」土台だけです。次に効くのは「何を自動化するか」の引き出し——Excel・PDF・メールからSQLiteまで、手作業を1本のスクリプトに落とす題材が並ぶのが退屈なことはPythonにやらせよう 第3版(当サイトのおすすめ本ランキング総合3位・全800ページ)。本記事はOS側に登録して毎日走らせる話、同書は中身のカタログという住み分けです。リンク先は当サイトの書籍紹介ページで、アフィリエイト広告を含みます。
毎日走るからこそ、相手のサーバーへの配慮が要る
自動実行の怖さは忘れていても毎日走ることです。だから1日1回にする・銘柄を絞る・1リクエストにまとめて個別取得には sleep を挟む・分足を毎日回さない——この4つは設計時に決めます。yfinanceは429(Too Many Requests)で YFRateLimitError を送出し、メッセージは "Too Many Requests. Rate limited. Try after a while." でした。「何回までなら大丈夫」という公表基準は確認できていませんので、推測して詰めず減らす方向で組んでください。第2回の判断軸——負荷をかけない、規約を読む——は、自動化するほど重くなります。
ためたデータで分かること・分からないこと
| DBの中身 | 答えられること | 答えられないこと |
|---|---|---|
date・close | その取引日に、その値が記録されていたこと | その値が高いのか安いのか |
| 行数 | 何営業日ぶん貯まったか | 取りこぼした日(後から必ず埋まるとは限らない) |
fetched_at | いつ取得した値か | その時点の取引所の値と一致しているか |
| 複数銘柄 | 同じ日付の行が揃っているか | 銘柄同士の関係(第15回) |
右の列がこの道具の限界です。前提も2つ。①yfinanceが返すのは米国 Yahoo, Inc. の Yahoo! Finance の値で、リアルタイム配信を保証しません(自動取得を禁じている日本の Yahoo!ファイナンス/LINEヤフーとは別サービスです)。遅延の有無や幅は取引所・銘柄で異なり、一律の値は確認できていない(第1回と同じ立場)ので、秒単位の判断に使う設計にはしないでください。②yfinanceは配布ページに「研究・教育目的」「Yahoo! Finance のAPIは個人利用のみを想定」と明記されています。
Yahooの API 利用規約には取得したデータの保持期間に関する条項があります。その条項が株価データにどこまで及ぶかを判断できないため、「何年ぶん貯めても問題ありません」とは書きません。貯め始める前にYahoo Developer API Terms of Use(保持期間の条項はこちら)とYahoo Terms of Service、yfinance公式ページをご自身で確認してください。ためたデータの再配布(.db をそのまま公開リポジトリに置く等)はしないでください。
第15回は、貯まった日足で値動きのばらつきを測る回の予定です。全体像は投資×Python シリーズ一覧にあります。
本連載は「自分の資産を自分で管理・可視化するツールを、Pythonの学習題材として自作する」ことが目的です。特定銘柄の売買や投資手法を勧めるものではありません。画面・グラフ・DBに入るサンプルデータ(SMPL-01〜03)はすべて架空の値、実在の銘柄コード(^N225・1306.T)と実測値は動作確認のための例示で、推奨でも予測でもありません。金融商品取引法38条2号は不確実な事項について断定的判断を提供することを禁じており、金融庁の監督指針も「有価証券等の価格、数値、対価の額の動向を断定的に表現したり、確実に利益を得られるように誤解させて、投資意欲を不当に刺激するような表示」を戒めています(金融商品取引業者等向けの総合的な監督指針)。当サイトは投資助言業(金融商品取引業)の登録を行っておらず、個別銘柄の推奨・売買タイミングの助言・税務相談には応じられません。投資判断はご自身の責任で(詳細は免責事項)。
11. よくある質問(FAQ)
Q. PCがスリープや電源オフだった時刻のぶんは、あとから実行されますか?
既定では実行されません。Windowsの StartWhenAvailable は既定で無効・起動まで既定10分の遅延・適用範囲にも条件があり、スリープをまたぐ起動は確認できていません(8章)。cronにも、ユーザーのジョブに当たる仕組みはありません。本記事はどちらにも頼らず、毎回 period="5d" を取り直してUPSERTで穴を埋めます。
Q. 同じ日に2回実行したり、土日・祝日に走ったりしても大丈夫ですか?
どちらも壊れません。2回実行しても PRIMARY KEY (code, date) と ON CONFLICT … DO UPDATE で1行に潰れます(実測では2回目が「新規 0 / 更新 90」・合計90件のまま・4章)。主キーなしの INSERT なら2回で4行です。休場日は行そのものが返らないので(実測では金曜の次が月曜・5章)、カレンダーも要りません。ただし土曜・日曜当日に実行したときの挙動は確認できていないため、何が返っても主キーで守る形にしています。
Q. タスクが失敗します。Last Result はどう読めばいいですか?
まずアプリのログです。記録があればPythonは起動しています。無ければパス・引用符・権限のどれか。0x80041324 はバッテリー駆動などタスクの条件で走らなかった合図(8章)、0x80041320 は「ログオンしていなかった」。0x1 がスクリプトの終了コードかどうかは確認できていません。
Q. cronに書いたのに動きません。仮想環境(venv)のPythonを使うときは?
systemctl is-active cron で確認し、次にすべて絶対パスかを見ます(cronが渡す環境の実測は9章)。仮想環境の bin はPATHに入らず、venv も activate は不要です。インタプリタを絶対パスで指定します(Windowsは .venv\Scripts\python.exe、Linuxは .venv/bin/python)。あとは9章の % とリダイレクトの2点。
Q. schedule ライブラリではダメなのですか?
ダメではなく、用途が違います。プロセスが動き続ける前提の schedule と、電源やログオン状態に依存しない日次バッチ向けのOSスケジューラの住み分けです(10章)。
Q. 第8回の dividends.db は書き換わりますか? このまま自動売買に発展させられますか?
書き換わりません。触るのは新規の prices.db だけです(第8回・第11回のDBには接続すらしません)。そして自動売買には発展させません——出口はファイルで、注文を出す機能には繋ぎません(10章)。
完成版 stock_daily_fetch.py を保存し、--demo を2回叩くところから。通信せずに2回目が「新規 0 / 更新 90」になれば動いています。次に --print-cron(または --print-schtasks)で登録コマンドを出します(手で書き写すと引用符かパスで詰まります)。最後に --codes で1回だけ実通信し、ログの保存件数を確かめてから登録します。
取得は第1回、SQLiteの土台は第8回、見せ方は第13回、エラーはPythonエラー一覧22種と直し方へ。全体像は投資×Python シリーズ一覧か新着記事のRSSで追えます。
本記事は、生成AIを活用して下書きし、運営者が確認・編集して公開しています。掲載のコード・ログ・画面は Linux Mint 22.3 / Python 3.12.3 / yfinance 1.7.0 / pandas 3.0.5 / SQLite 3.45.1 で 2026年9月10日に確認しました。schtasks とPowerShellの出力は同日に Windows 11 / Python 3.12.10 / yfinance 1.4.1 / pandas 3.0.3 で採取しましたが、本記事のためにWindowsで新しいタスクは作成していません(7章・8章に明記)。グラフと SMPL-01〜03 の値はすべて架空、実在の銘柄コード(^N225・1306.T)は動作確認のための例示で、推奨銘柄ではありません。本ツールが行うのは値の取得と保存だけで、資産の査定でも投資判断でもありません。本記事はプログラミングの情報提供が目的で、銘柄の売買・投資手法の推奨でも税務上の助言でもありません。投資判断はご自身の責任で。AIの利用方針は免責事項をご覧ください。