自动化每日观察列表:一个获取数据、计算信号并生成报告的定时脚本,以及 API 密钥与速率限制的陷阱
把笔记本代码改造成一个稳健的命令行脚本,包含配置、日志、带退避的重试、安全的 API 密钥管理和定时调度,并写出每日报告文件。
你将学到什么
- 如何把笔记本单元格重构为一个包含函数、
main()入口和配置文件的脚本。 - 如何让 API 密钥远离代码,并通过等待和指数退避来应对速率限制。
- 如何写出一份在早上 07:30 真正有用的报告:一张排序表、几个标记和一个时间戳。
- 如何用 cron 或 Windows 任务计划程序调度脚本,以及应该记录哪些日志,才不会让静默失败被忽视。
讲解
笔记本用于探索;脚本则在无人看管的情况下每天重复同样的工作。指标(第 6 课)和信号逻辑(第 7 课和第 8 课)保持不变;它们周围的一切都要改变。
脚本的结构
把工作拆分成各司其职的函数:load_config()、fetch(ticker)、compute_signals(df)、build_report(rows) 和 main()。保护语句 if __name__ == '__main__': main() 让你既能直接运行该文件,又能把其中的函数导入测试中。把观察列表、参数和输出文件夹保存在 config.yaml 或 config.json 中,这样修改它们时无需编辑代码。
API 密钥与机密信息
永远不要把密钥写进源代码,或写进你可能分享或提交到 Git 的笔记本中。请从环境变量或 .env 文件中读取它,即 os.environ['DATA_API_KEY'](python-dotenv 软件包负责加载 .env),并把 .env 列入 .gitignore。像对待密码一样对待密钥,一旦泄露就立即更换。
速率限制、重试与退避
速率限制是数据提供商每分钟或每天允许的最大请求次数;超出限制会返回 HTTP 429 错误,或者更糟:返回空数据却不报任何错误。用 time.sleep(60 / max_per_minute) 控制循环的节奏。对于临时故障(429、500、超时),请使用指数退避进行重试:依次等待 1 秒、2 秒、4 秒、8 秒,然后放弃,并把该代码记录为缺失。永远不要在紧密循环中重试;密钥就是这样被封禁的。
def fetch_with_retry(ticker, tries=4, base=1.0):for n in range(tries):try:return fetch(ticker)except TransientError as e:wait = base * 2 ** nlog.warning('%s: %s,%.0f 秒后重试', ticker, e, wait)time.sleep(wait)log.error('%s: 放弃重试', ticker)return None
缓存原始下载数据(data/raw/TICKER_YYYY-MM-DD.parquet):同一天的第二次运行会读取该文件而不是消耗额度,你还能保留一份脚本所见数据的审计记录。
生成信号之前先验证
数据出错的频率比你想象的更高:缺少最后一根 K 线、日期过时、收盘价为 0、未复权的拆股。请检查最后日期是否等于预期的交易日、最后 20 行中是否没有 NaN,以及今天收益率的绝对值是否低于一个合理性阈值(例如 50%)。未通过检查的品种会被列入报告的“数据问题”部分,而不是因一个错误报价而产生买入信号。
报告
写出一个 Markdown 或 HTML 文件,其中包含运行时间戳和数据日期、按你的排序标准排列的表格(例如以 ATR 为单位衡量的与 50 日 SMA 的距离)、自昨天以来新出现的信号(与前一天保存的信号文件进行比较)以及数据问题。将其保存为 reports/watchlist_2026-10-30.md,以便保留历史记录;为每个被标记的代码绘制图表(第 10 课)则是可选的。
调度与日志
在 Linux 和 macOS 上,cron 按时间表运行命令:30 6 * * 1-5 /path/venv/bin/python /path/watchlist.py >> /path/logs/cron.log 2>&1 会在工作日的 06:30 运行;在 Windows 上,任务计划程序通过对话框完成同样的工作。请使用虚拟环境中 Python 的完整路径(第 1 课),因为调度程序不会激活环境。用 logging 模块把日志写入文件:开始、已获取的代码、发现的信号、结束。如果今天没有日志条目,就说明调度失败了。电脑会休眠,网络会中断,接口会变更;一个你从不检查的脚本算不上自动化。
实例演算
假设示例:一个包含 40 只欧洲和美国股票的观察列表,使用一个每分钟限 5 次请求、每天限 500 次的免费数据套餐(数字为编造;请查看你的数据提供商的当前条款)。
- 节奏控制。40 个代码、每分钟 5 次 = 40 / 5 = 至少 8 分钟。脚本在两次调用之间休眠 12 秒。安排在 06:30 运行,意味着数据大约在 06:40 准备就绪。
- 额度。一次运行使用每日 500 次调用中的 40 次(按 22 个交易日计每月 880 次)。一个每 10 分钟重复运行一次的程序错误每小时会消耗 240 次调用,大约两小时就会达到上限;磁盘缓存可以消除这一风险。
- 退避。有两个代码在第一次尝试时返回 HTTP 429。在 1 秒、2 秒和 4 秒后的重试中,它们在第二次尝试时成功;最坏情况下每个代码增加的时间为 1 + 2 + 4 + 8 = 15 秒,因此运行仍能在 10 分钟的预算内完成。
- 报告。规则:“收盘价向上穿越 50 日 SMA,且 RSI(14) 低于 60”。昨天的文件中有 3 个有效信号;今天的运行发现了 5 个,其中 2 个是新的。报告先列出这 2 个新代码,再列出 3 个延续的代码,另有 1 个代码因最后一根 K 线已是两天前的数据而被列入“数据问题”。
未涵盖的内容:这并不能说明信号是否能盈利;它只是让每天的准备工作变得可靠且可重复。
常见错误
练习活动
在纸上完成:你的数据提供商允许每分钟 30 次请求、每天 2,000 次。你想在 06:00 对一个包含 150 个代码的观察列表运行一次,并在 09:00 到 17:30(CET,中欧时间)之间每 30 分钟刷新一次 20 个优先代码。(a) 为遵守每分钟的限制,两次调用之间的最短休眠时间是多少?(b) 一整天会使用多少次调用?是否在每日额度之内?(c) 06:00 的运行报告已获取 150 个代码,但所有信号都与昨天完全相同。说出两项能判断数据是否为最新的检查。
显示答案
(a) 60 / 30 = 两次调用之间间隔 2 秒(为安全起见使用 2.1 秒)。
(b) 早间运行:150 次调用。日内:09:00 到 17:30 共 8.5 小时 = 17 个半小时时段,再加上 09:00 这个时段本身 = 18 次刷新 × 20 个代码 = 360 次调用。合计 510 次调用,远低于 2,000 次。即使每次调用都完整重试一次(1,020 次),额度也足够。
(c) 检查每个数据框中最后一个索引日期是否等于预期的上一个交易日(考虑交易所假期),并把今天下载数据的哈希值或最后收盘价与昨天缓存的文件进行比较;如果全部 150 个最后收盘价都与昨天相同,说明数据提供商提供的是过时数据。
局限性:额度、接口和免费套餐规则都会变化;这里的数字仅作说明之用,你必须阅读数据提供商当前的文档。
要点回顾
- 脚本需要函数、配置文件、
main()保护语句和日志;笔记本则不需要。 - 把密钥保存在环境变量或
.env中;按限制控制请求节奏,用指数退避进行重试,然后放弃并记录日志。 - 在计算信号之前验证日期和行数;缓存原始下载数据。
- 使用虚拟环境中 Python 的完整路径,通过 cron 或任务计划程序进行调度,并每天检查日志。
下一步
第 13 课把所有内容组装成一个从数据到报告的完整研究笔记本项目,并用一份针对前视偏差和过度拟合的清单对其进行审查。
自测
回答问题后核对答案。你可以随意重试多次。
供您自行复习的自动自测。您的答案在浏览器中核对,我们不会查看或评估。这不是考试,也不是资格证书。
您的进度
“已完成”仅表示您将本课标记为已学完,并不是对您理解程度的测试。 学习进度和笔记仅保存在此设备的此浏览器中,而非账户中。