cronでPython株価分析スクリプトを毎日自動実行する方法【VPS・Linux】

分析スクリプトを毎日自動実行する 自動化・運用

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

「手動で実行したら動くのに、cronに登録すると動かない」——Linuxでの自動実行に挑戦した人が、ほぼ全員が一度は通る道です。しかもエラーメッセージすら出ないので、何が悪いのかまったく分かりません。

原因はほぼ決まっています。cronは、あなたが普段使っているシェルとはまったく別の環境で動くからです。この一点を理解すれば、トラブルの9割は自力で解決できるようになります。

この記事では、株価分析スクリプトを毎日確実に自動実行させる設定を、つまずきポイントを潰しながら解説します。設定の書き方だけでなく、動かないときの原因の切り分け方に重点を置きました。

cronの基本と書式

cronはLinuxに標準で入っている定期実行の仕組みです。設定は crontab -e で編集します。

# 編集する
crontab -e

# 現在の設定を表示する
crontab -l

# 設定をファイルに保存しておく(バックアップ)
crontab -l > ~/crontab.bak

書式は5つの数字+実行コマンドです。左から「分・時・日・月・曜日」を指定します。

┌───── 分   (0-59)
│ ┌─── 時   (0-23)
│ │ ┌─ 日   (1-31)
│ │ │ ┌─ 月 (1-12)
│ │ │ │ ┌─ 曜日 (0-7  0と7が日曜)
│ │ │ │ │
30 15 * * 1-5  実行したいコマンド

株価分析でよく使う指定をまとめておきます。

指定意味用途
30 15 * * 1-5平日15:30大引け後のデータ取得
50 8 * * 1-5平日8:50寄り付き前の準備
*/30 9-15 * * 1-5平日9〜15時の30分ごとザラ場中の監視
0 6 * * 6土曜6時週次のレポート作成
0 0 1 * *毎月1日0時月次の集計

曜日指定の 1-5(月〜金)を必ず入れてください。市場が閉まっている土日に実行しても意味がないうえ、データが取得できずエラーログだけが溜まります。

書式で迷ったときは、crontab.guru のようなオンラインツールが便利です。指定を入力すると「次にいつ実行されるか」を日本語ではなく英語で教えてくれるので、意図と合っているかを確認できます。

もう1つ、cronを触るときの心得を書いておきます。設定を編集する前に、必ず現在の内容をバックアップしてください。crontab -e の操作を誤ると、登録済みの設定がまるごと消えることがあります。

# 編集前に必ず退避する(習慣にする)
crontab -l > ~/crontab_$(date +%Y%m%d).bak

# 万一消してしまったら、バックアップから戻せる
# crontab ~/crontab_20260820.bak

📘 外部参考Ubuntu Server Documentationcrontab.guru(書式の確認ツール)

なぜcronでPythonが動かないのか

ここが本記事の核心です。原因を理解するために、まず自分の環境とcronの環境の違いを目で見てみましょう。

# 手元のシェルでの環境変数
echo $PATH
# → /home/trader/.local/bin:/usr/local/bin:/usr/bin:/bin:...

# cronでの環境変数を確認する(一時的に登録して確認)
# * * * * * env > /tmp/cron_env.txt
# 1分待ってから
cat /tmp/cron_env.txt

実行すると衝撃を受けるはずです。cronのPATHは /usr/bin:/bin しかありません。普段使っている環境変数のほとんどが存在しないのです。

項目普段のシェルcron
PATH豊富/usr/bin:/bin のみ
環境変数.bashrc が読まれる読まれない
カレントディレクトリいまいる場所ホームディレクトリ
仮想環境activate済み無効
標準出力画面に表示どこにも出ない

この表を見れば、なぜ動かないかが分かります。手動実行のつもりで書いたコマンドは、cronでは前提がすべて崩れているのです。

つまずきの原因トップ5

  • pythonが見つからない: PATHが違うので python というコマンドが存在しない
  • 仮想環境が効いていない: source venv/bin/activate はcronでは意味がない
  • 相対パスが解決できない: カレントディレクトリがホームなので ./data.csv が見つからない
  • 環境変数が読めない: APIキーを .bashrc に書いていると取得できない
  • エラーが見えない: ログを取っていないので、失敗しても気づけない

確実に動く書き方

原因が分かれば対処は簡単です。すべてを明示的に指定する——これに尽きます。

# 仮想環境のPythonの絶対パスを確認する
cd ~/stock
source venv/bin/activate
which python
# → /home/trader/stock/venv/bin/python

# ログ用のディレクトリを作っておく
mkdir -p ~/stock/logs
# crontab -e で以下を記述する

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

# 平日15:30 に株価を取得する
30 15 * * 1-5 cd /home/trader/stock && /home/trader/stock/venv/bin/python fetch.py >> /home/trader/stock/logs/fetch.log 2>&1

この1行に、対策がすべて詰まっています。分解して見ていきます。

部分役割
cd /home/trader/stock作業場所を固定し、相対パスを使えるようにする
/home/.../venv/bin/python仮想環境のPythonを直接指定。activateは不要になる
>> ...log標準出力をログに追記する
2>&1エラーも同じログに記録する。これが最重要
MAILTO=""実行のたびにメールが飛ぶのを止める

特に2>&1 は絶対に省略しないでください。これがないとエラー内容が消え、「動かないけど理由が分からない」という最悪の状態になります。

また、source venv/bin/activate を書く必要はありません。仮想環境のPythonを直接指定すれば、その環境のライブラリが自動的に使われます。activateは単にPATHを書き換えているだけなので、絶対パスで呼べば同じことです。

動作確認は「数分後」で試す

設定したら、翌日まで待たずにすぐ確認します。待ってから動いていないと分かるのは、時間の無駄です。

# 現在時刻を確認する
date
# → 水 8月 20 16:33:12 JST

# 2〜3分後を指定してテスト用の行を追加する
# 36 16 * * * cd /home/trader/stock && /home/trader/stock/venv/bin/python fetch.py >> /home/trader/stock/logs/test.log 2>&1

# 時刻が過ぎたら結果を確認する
cat ~/stock/logs/test.log

# cron自体が起動したかを確認する
grep CRON /var/log/syslog | tail -5

確認できたら、テスト用の行は必ず削除してください。毎日不要な実行が走り続けることになります。

切り分けの手順

それでも動かない場合、上から順に確認すれば原因は必ず特定できます。

# ① cronサービス自体が動いているか
systemctl status cron

# ② 設定が正しく登録されているか
crontab -l

# ③ cronが実行を試みたか
grep CRON /var/log/syslog | tail -20

# ④ ログに何が書かれているか
cat ~/stock/logs/fetch.log

# ⑤ 同じコマンドを手で叩いて動くか
cd /home/trader/stock && /home/trader/stock/venv/bin/python fetch.py
症状原因対処
③にログがないcronが起動していないsudo systemctl start cron
③にあるが④が空コマンドが即座に失敗パスの誤りを確認
④に command not foundPATHの問題絶対パスで指定する
④に ModuleNotFoundError仮想環境が違うvenvのPythonを指定
④に FileNotFoundErrorカレントディレクトリcd を先頭に付ける
⑤も動かないスクリプト自体のバグcronは無関係。コードを修正

この表があれば、ほとんどのトラブルは5分で解決できます。特に⑤を先に試すと、cronの問題かコードの問題かがすぐ分かるので効率的です。

環境変数(APIキー)をどう渡すか

通知用のWebhook URLや証券会社のAPIキーは、cronでは読み込まれません。.bashrc に書いてある環境変数は、cronからは見えないのです。

対処法は3つあり、それぞれ一長一短があります。

方法安全性手軽さ
crontabに直接書くcrontab -l で見える
スクリプト内で.envを読む◎ 権限600で保護できる
ラッパースクリプト経由

おすすめは2番目です。スクリプト側で読み込む形にしておけば、cronでも手動でも同じように動きます。

# ~/stock/.env に秘密情報を置き、権限を絞る
cat > ~/stock/.env << 'EOF'
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/xxxx
EOF
chmod 600 ~/stock/.env
# ~/stock/env_loader.py
import os
from pathlib import Path


def load_env():
    """スクリプトと同じ場所の .env を読み込む

    cron から実行してもカレントディレクトリに依存しないよう、
    __file__ を基準にパスを解決するのがポイント
    """
    path = Path(__file__).resolve().parent / ".env"
    if not path.exists():
        return

    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        os.environ.setdefault(key.strip(), value.strip())

__file__ を基準にしているのが重要です。カレントディレクトリに依存しないので、どこから実行しても確実に読み込めます。

APIキーの管理方針はVPS契約後に最初にやるUbuntu初期設定でも扱っています。

実務で使えるスクリプトの書き方

cronで動かす前提のスクリプトには、手動実行とは違う配慮が必要です。誰も見ていない場所で動くからです。

# ~/stock/fetch.py
import logging
import sys
import datetime as dt
from pathlib import Path

import yfinance as yf
import pandas as pd

from env_loader import load_env

load_env()

BASE = Path(__file__).resolve().parent
LOG_DIR = BASE / "logs"
DATA_DIR = BASE / "data"
LOG_DIR.mkdir(exist_ok=True)
DATA_DIR.mkdir(exist_ok=True)

# ログの体裁を整える(時刻付きで残す)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(LOG_DIR / "fetch.log", encoding="utf-8"),
        logging.StreamHandler(sys.stdout),
    ],
)

TICKERS = {"7203.T": "トヨタ", "6501.T": "日立", "^N225": "日経平均"}


def main():
    logging.info("=== 処理開始 ===")
    ok, ng = 0, 0

    for code, name in TICKERS.items():
        try:
            df = yf.download(code, period="5d", auto_adjust=True,
                             progress=False)
            if df.empty:
                raise ValueError("データが空です")

            out = DATA_DIR / f"{code.replace('.', '_')}.csv"
            df.to_csv(out)
            logging.info(f"{name}: 取得成功 終値 {float(df['Close'].iloc[-1]):,.1f}")
            ok += 1
        except Exception as e:
            logging.error(f"{name}: 取得失敗 {e}")
            ng += 1

    logging.info(f"=== 処理終了 成功{ok} / 失敗{ng} ===")
    return 1 if ng else 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as e:
        logging.exception(f"想定外のエラー: {e}")
        sys.exit(1)

押さえるべき点は4つです。

  • パスは __file__ 基準: どこから実行されても壊れない
  • logging で時刻付きログ: print より情報量が多く、後から追える
  • 1銘柄失敗しても続行: try/except を個別に置き、全体が止まらないようにする
  • 終了コードを返す: 失敗時に1を返せば、後続処理で成否を判定できる

3番目が特に重要です。1銘柄の取得に失敗しただけで全体が止まる設計だと、その日のデータがまるごと欠けます。個別に例外を捕まえておけば、取れた分は保存されます。

止まっていることに気づく仕組み

自動化の最大の落とし穴は、止まっていても誰も教えてくれないことです。異常時だけ通知する設計だと、完全に停止したときに何の連絡も来ません。

対策は単純で、正常時にも1日1回「動いています」と通知することです。通知が来ない日=止まっている日、と判断できます。

# ~/stock/notify.py
import os
import sys
import datetime as dt
import requests

from env_loader import load_env

load_env()


def send(text):
    url = os.environ.get("DISCORD_WEBHOOK_URL")
    if not url:
        print("Webhook URLが未設定です")
        return False
    try:
        r = requests.post(url, json={"content": text}, timeout=10)
        return r.status_code < 300
    except Exception as e:
        print(f"通知失敗: {e}")
        return False


if __name__ == "__main__":
    # 引数で受け取った終了コードに応じてメッセージを変える
    code = int(sys.argv[1]) if len(sys.argv) > 1 else 0
    now = dt.datetime.now().strftime("%m/%d %H:%M")
    mark = "✅ 正常終了" if code == 0 else "⚠️ エラーあり"
    send(f"{mark} 株価取得 ({now})")
# crontab: 取得の結果を受けて通知する
30 15 * * 1-5 cd /home/trader/stock && ./venv/bin/python fetch.py >> logs/fetch.log 2>&1; ./venv/bin/python notify.py $? >> logs/notify.log 2>&1

$?直前のコマンドの終了コードです。これを通知スクリプトに渡すことで、成功したか失敗したかを判別できます。

通知の実装方法はDiscordに株価シグナルを自動送信するコード実装にまとめています。

運用を続けるための整備

ログが無限に増えないようにする

毎日追記していると、数年後にはログが巨大になります。ディスクが埋まってサーバーが止まるという事故は、意外とよく起こります。

sudo tee /etc/logrotate.d/stock > /dev/null << 'EOF'
/home/trader/stock/logs/*.log {
    weekly
    rotate 8
    compress
    missingok
    notifempty
    copytruncate
}
EOF

# 設定を検証する(実際には実行しない)
sudo logrotate -d /etc/logrotate.d/stock

copytruncate を指定しているのは、実行中のプロセスがログを掴んだままでも安全にローテートできるためです。

処理の重複を防ぐ

前回の処理が終わる前に次が始まると、二重に動いてしまいます。データの取得が長引いたときに起こりがちです。

# flock を使うと、多重起動を防げる
*/30 9-15 * * 1-5 /usr/bin/flock -n /tmp/stock.lock -c "cd /home/trader/stock && ./venv/bin/python monitor.py >> logs/monitor.log 2>&1"

flock -n は、すでに実行中なら何もせず終了します。30分ごとに監視するような設定では、これを入れておくと安心です。

祝日に実行しないようにする

cronは曜日しか判定できないので、日本の祝日はスクリプト側で判定します。

import datetime as dt
import sys

# 主要な祝日を列挙しておく(jpholidayライブラリを使う方法もあります)
HOLIDAYS = {
    "01-01", "01-02", "01-03",   # 年始
    "12-31",                      # 大納会翌日以降
}


def is_market_open(date=None):
    """市場が開いているかを判定する"""
    date = date or dt.date.today()
    if date.weekday() >= 5:              # 土日
        return False
    if date.strftime("%m-%d") in HOLIDAYS:
        return False
    return True


if not is_market_open():
    print("本日は市場が休場です。処理をスキップします")
    sys.exit(0)

祝日を正確に扱いたい場合は jpholiday というライブラリが便利です。エラーログを無駄に増やさないためにも、休場日の判定は入れておくことをおすすめします。

まとめ

cronでPython株価分析スクリプトを確実に自動実行する方法をまとめました。

  • cronは普段のシェルとは別の環境で動く。PATHも環境変数も引き継がれない
  • 対策はすべてを絶対パスで書くこと。cd を先頭に付けると相対パスも使える
  • source activate は不要。venvのPythonを直接指定すればよい
  • 2>&1 は絶対に省略しない。エラーが見えないと原因を追えない
  • 動作確認は数分後の時刻を指定して即座に行う。翌日まで待たない
  • APIキーは .env に置き、__file__ 基準で読み込む
  • 正常時にも通知する。異常検知だけだと停止に気づけない

cronでつまずくのは、能力の問題ではなく「環境が違う」という前提を知らないだけです。これさえ分かってしまえば、あとは機械的に対処できます。

まずは * * * * * env > /tmp/cron_env.txt を1分だけ仕掛けて、cronの環境を自分の目で見てみてください。「これでは動かないはずだ」と腑に落ちるはずです。そこから先の設定は、驚くほどスムーズに進みます。

最後に、自動化全般に言えることを1つ。「動くようにする」より「止まったと気づけるようにする」ほうが、長期的にはずっと重要です。動かす部分は一度作れば終わりますが、止まったことに気づけない状態は何か月も続きます。

本記事の通知スクリプトは10行程度です。この10行があるかないかで、半年後にデータが揃っているかどうかが決まります。設定が終わったら、忘れないうちに入れておいてください。

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