观潮过节记:教 cron 学会放假

观潮过节记:教 cron 学会放假

接上回:观潮托管记——让 AI 引擎学会自己醒来。

那篇讲的是让系统每天收盘后自动扫描持仓、生成信号、回填胜率。8 秒变 0 秒,因为数据已经在那里等我了。

这篇讲的是下一课:什么时候不该醒来。

体检

4 月 3 日,清明前最后一个交易日。

打开信号复盘看板,例行体检。cron 已经连续两天稳定执行——前天修了 baostock 三层超时防护后,昨天和今天都正常跑完了,回填边界也对:

信号日期5d 回填说明
03-26✅今天刚回填的最新日期
03-27⏳第 5 个交易日是今天(04-03),但 cron 用的是昨日收盘价
03-28+⏳还没到 5 个交易日

03-27 的信号会在清明后回填。完全正确。

一切正常。但我突然想到一件事:

周一是清明假期。cron 会跑吗?

0 9 * * 1-5

答案是会。

0 9 * * 1-5 /path/to/tidewatch-daily.sh

这行 cron 表达式的意思是:每周一到周五,UTC 09:00(北京时间 17:00)执行。它只看星期几,不看农历,不看国务院放假通知,不知道清明是什么。

周一它会准时醒来,然后:

  • scan_market 拉到的还是 04-03(今天)的收盘价——没有新数据
  • analyze_stock 基于过期数据给三只持仓各生成一条信号——垃圾信号
  • update_signal_outcomes 回填需要的 K 线还没出来——做不了任何事

不会崩,不会丢数据。但 signals 表里会多三条基于旧数据的噪音,后续回填时可能产生误导。

一条 cron 不需要知道屈原是谁。但它得知道今天有没有开市。

一行比万年历靠谱

最直觉的方案是维护一张中国假日表。但中国的调休是出了名的不可预测——五一放五天、中秋连国庆、偶尔还有周六上班。硬编码假日表意味着每年年初更新一次,漏了就出事,多了也出事。

更优雅的方案是:问数据源自己。

baostock 已经是观潮的日 K 线主力数据源,A 股休市时它自然没有数据。所以只需要查一下沪深 300 今天有没有 K 线——有就是交易日,没有就不是:

TODAY_BJ=$(TZ=Asia/Shanghai date +%Y-%m-%d)
IS_TRADING=$(cd "$PROJECT_ROOT" && poetry run python3 -c "
import baostock as bs
bs.login()
rs = bs.query_history_k_data_plus('sh.000300', 'date,close',
    start_date='$TODAY_BJ', end_date='$TODAY_BJ', frequency='d')
rows = []
while rs.error_code == '0' and rs.next():
    rows.append(rs.get_row_data())
bs.logout()
print('YES' if rows else 'NO')
" 2>/dev/null)

零维护。清明、春节、国庆、中秋调休、周末——全自动跳过,不需要硬编码任何一天。baostock 知道的比我多。

把它插进 tidewatch-daily.sh 最前面,在所有业务逻辑之前做 early exit:

if [ "$IS_TRADING" = "NO" ]; then
    echo "$TODAY_BJ is not a trading day, skipping" >> $LOG
    exit 0
fi

写完,commit,push。感觉挺完美。

然后小龙虾开始 Review。

9/10 扣掉的那 1 分

🦞 代码本身 9/10 ✅,部署有一个问题 ❌

先说扣分的地方。

如果 baostock 自己挂了呢? 网络超时、login 失败、服务器维护——print() 不会执行,IS_TRADING 变量为空字符串。空字符串 != "YES",走 skip 路径。

结果就是:交易日 baostock 宕机 → 静默跳过整天扫描。没有任何告警。

这比清明空跑严重多了。清明空跑最多多三条垃圾信号;交易日漏跑意味着持仓信号断档、回填链中断。

修复很简单——空值单独处理,宁可空跑不漏跑:

if [ -z "$IS_TRADING" ]; then
    echo "Trading day check failed (baostock error?), running anyway" >> $LOG
elif [ "$IS_TRADING" = "NO" ]; then
    echo "$TODAY_BJ is not a trading day, skipping" >> $LOG
    exit 0
fi

三种状态,三种策略:

  • YES → 正常执行
  • NO → 确认非交易日,安全跳过
  • 空值 → 不确定,保守策略,照跑

然后小龙虾说了第二个问题,这个更隐蔽。

指错地方的 crontab

🦞 cron 跑的不是 repo 里的文件

这个更隐蔽。

最初部署 cron 时图省事,把脚本拷贝到了一个独立目录,crontab 指向那份副本。后来所有改动都在 git repo 里做——包括刚才的交易日检测。

git pull 更新的是 repo 里的文件,cron 读的是旧拷贝。如果不是小龙虾提出来,清明之后 cron 跑的还是没有交易日检测的老版本。代码 commit 了,部署却没到位——最经典的「改了等于没改」。

改 crontab 直接指向 repo 路径,一劳永逸:

# 从独立副本 → 直接指向 repo 内脚本
0 9 * * 1-5 /path/to/repo/scripts/tidewatch-daily.sh

从此 git pull 就是部署,不需要再 cp 同步。

放假了

三件小事:

  1. 一行 baostock 查询,让 cron 知道今天有没有开市
  2. 空值保守策略,宁可空跑不漏跑
  3. crontab 指向 repo,git pull 即部署

我提出问题,Polly 写代码,小龙虾 Review——三方各司其职,20 行改动,半小时收工。

这 20 行的意义在于:从"能自动跑"到"知道什么时候不该跑"——这是系统成熟度的一个小台阶。

上一篇让观潮学会了每天自己醒来。这一篇教它学会了一件更难的事:

放假。

清明安康 🌿

Comments