每天 7 点自动出盘前日报:GitHub Actions + TickDB CLI 工程记录
作者: TickDB Research · 发布: 2026/5/23 · 阅读: 10
标签: C 类, 博客园, CLI
摘要:记录用 GitHub Actions cron 调度 + TickDB CLI 的
--json模式 + shell 与 jq 解析,实现每日盘前全球市场日报的完整工程方案。涵盖 workflow 配置、多市场数据拉取、Markdown 模板设计和 Telegram Bot 推送。
一、每天早上 7 点的手动操作清单
打开 A 股昨天收盘数据。看一眼美股半夜走成什么样。恒指和期货有没有隔夜异动。把关键数字整理成一段文字发到团队群里。
不是一天,是每一天。
第一周觉得“顺手看看”。第三周开始漏日期。第六周休了一天假,日报断更,同事在群里问“今天没有吗”。这套操作重复 60 天后会变成什么——一个反复消耗 5 分钟的认知负荷,累积起来比任何一次大排查都重。
做过这件事的人都知道,问题不在于“打开行情软件有多难”,而在于这件事必须每天早上 7 点做,一天都不能停。重复性运维的消耗不是按时间算的,是按“打断感”算的——你本来可以一觉醒来直接看结果,却要先手动跑一套固定流程。
常见的自动化方案各有代价:云服务器要维护、本地电脑不能关机、Python 脚本依赖环境容易漂移。这里用的方案更轻——把日报当成 CI/CD 流水线来跑,用 GitHub Actions 做定时调度,用 TickDB CLI 拉数据,用 shell + jq 组装报告,用 Telegram Bot 推送到手机。
整套方案没有服务器成本,没有持久进程,代码全在一个 workflow 文件里。
二、选型:为什么是这个工具链
2.1 GitHub Actions 做定时任务:能用,但有坑
GitHub Actions 的 schedule 事件本质上是 cron 语法驱动,每月免费额度(以当前 GitHub Billing 文档为准,公开仓库与私有仓库计费口径不同)足够每天跑几十次。每天跑一次日报,每次不到 1 分钟,消耗可以忽略不计。
另一个加分项是 ephemeral runner——每次运行环境全新,没有“昨天残留进程”的问题。云服务器上的 crontab 偶尔会因为上次脚本没退干净导致第二次运行失败,GitHub Actions 不存在这个问题,跑完就销毁。
但有一个必须提前知道的坑:
cron 的时区是 UTC,不是北京时间。 北京时间早上 7:00 = UTC 23:00(前一天)。在 workflow 里写
cron: '0 23 *'才是北京 7:00。这个时区换算搞反了,日报会在半夜 3 点推过来——你不会想看半夜 3 点的消息,你的同事也不会。
还有一个隐性限制:官方文档明确写高峰期可能延迟 15-30 分钟。日报场景对 ±15 分钟不敏感,但如果未来扩展为盘中监控,这个精度不够。
2.2 CLI 而不是 Python SDK:ephemeral 环境下的选型逻辑
GitHub Actions runner 每次启动是全新环境。技术选型的核心问题变成:这个工具在裸 Ubuntu 镜像上一行命令能不能跑起来。
| 方案 | 一行能跑? | 依赖链 | 适合 ephemeral? |
|---|---|---|---|
| TickDB CLI | npm install -g tickdb@latest | Node.js ≥ 18(GitHub runner 自带) | ✅ 是 |
| Python SDK | pip install tickdb-python requests | Python + pip + venv 管理 | ⚠️ 依赖稍多 |
| 裸 curl + REST API | 零安装 | 无 | ✅ 但需手动处理鉴权和分页 |
CLI 在这个场景下的核心优势不是功能多,是安装成本低到可以写在 workflow 的一行 step 里。而且 --json 模式让 shell 脚本可以直接消费结构化数据,不用写 Python 包装层。
一个常被忽略的设计细节:TickDB CLI 有两种输出模式:
- 默认模式:彩色表格,终端里一眼看清——给人看的
--json模式:机器可读,喂给jq解析——给脚本用的
这种双模式设计背后的原则很朴素:给人看用默认,给机器看加
--json。 做日报自动化的第一步,就是让机器能“看懂”行情数据——--json就是这个对话的接口。
2.3 jq:在 shell 域内闭环
解析 JSON 可以用 Python 一行 json.loads(),但那样整个 workflow 就跨了两个语言域——shell 做调度,Python 做解析。跨域的代价是调试时要在两种语法之间切换,runner 上还得确认 Python 版本。
jq 是单二进制,apt-get install -y jq 一行搞定。解析逻辑全部留在 shell 域内,一个脚本从头看到尾。这对“跑 365 天不能坏”的定时任务来说,维护成本的降低比性能提升重要得多。
2.4 Telegram Bot 而不是邮件
邮件推送需要 SMTP 配置,GitHub Secrets 里要存用户名密码,有些邮件服务商还要求应用专用密码——配置链路长、可移植性差。
Telegram Bot 只需要两个值:Bot Token 和 Chat ID,纯 HTTP POST 推送,parse_mode=Markdown 支持基础格式。手机上收到就是一条消息,不需要打开邮件 App。
但有一个格式限制需要提前适应:Telegram 的 Markdown 模式不支持表格和嵌套列表。 日报模板里的分隔线、缩进、反引号都只能用最基础的 Markdown 子集。这个限制决定了模板的设计方式——用分隔线代替表格,用反引号标记数值。如果需要更丰富的排版,可以切换为 parse_mode=HTML,但模板复杂度会相应增加。
三、完整实现
3.1 依赖与环境
# GitHub Actions runner 预装: git, node, curl
# 额外安装: tickdb CLI, jq
npm install -g tickdb@latest # 生产建议锁版本,如 [email protected],版本号以发布时 npm 页面为准
sudo apt-get update && sudo apt-get install -y jq
运行环境:ubuntu-latest (GitHub Actions),Node.js ≥ 18,bash。
3.2 项目结构
.
├── .github/
│ └── workflows/
│ └── daily-report.yml # 定时触发配置
└── scripts/
└── daily-report.sh # 日报生成 + 推送脚本
全项目两个文件。没有 Python 依赖,没有 requirements.txt,没有 .env 文件。所有敏感值通过 GitHub Secrets 注入。
3.3 workflow.yml
# .github/workflows/daily-report.yml
name: Daily Pre-market Report
on:
schedule:
# 北京时间每天早上7:00 = UTC 23:00(前一天)
# 注意:GitHub Actions cron 时区为 UTC,北京时间7:00固定为 0 23 * * *
- cron: '0 23 * * *'
workflow_dispatch: # 允许手动触发调试
jobs:
report:
runs-on: ubuntu-latest
timeout-minutes: 5 # 防止脚本卡死耗尽额度
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20' # CLI 要求 Node.js ≥ 18
- name: Install TickDB CLI
run: npm install -g tickdb@latest
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Generate Report & Push
env:
TICKDB_API_KEY: ${{ secrets.TICKDB_API_KEY }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: bash scripts/daily-report.sh
设计考量:
cron: '0 23 *'是 UTC 23:00,对应北京时间次日 7:00。北京时间没有夏令时,所以此映射全年稳定。文档描述 GitHub Actions 的schedule按 UTC 执行,实际行为 在高峰负载时可能延迟 15-30 分钟或偶尔被丢弃。若推送时间有严格依赖,应增加延迟容忍或自建 runner。timeout-minutes: 5是防御性设置——如果 TickDB API 不可达或网络阻塞,5 分钟后自动终止,不空耗额度。workflow_dispatch是调试钩子:cron 没触发时可以手动跑,不用等到第二天验证。
3.4 核心脚本
#!/bin/bash
# scripts/daily-report.sh
# 盘前日报生成 + Telegram 推送
# 依赖: tickdb CLI, jq, curl
# 环境变量: TICKDB_API_KEY, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
set -euo pipefail # 管道任一命令失败立即退出,防止静默发送空报告
# ---- 环境检查 ----
: "${TICKDB_API_KEY:?未设置 TICKDB_API_KEY}"
: "${TELEGRAM_BOT_TOKEN:?未设置 TELEGRAM_BOT_TOKEN}"
: "${TELEGRAM_CHAT_ID:?未设置 TELEGRAM_CHAT_ID}"
export TICKDB_API_KEY
# 调试输出(可选,确认工具可用)
tickdb --version
jq --version
# ---- 1. 拉取多市场行情快照 ----
# CLI 采用位置参数,--json 模式输出机器可读 JSON
# 品种: 沪深300 / 恒生指数 / 标普500 / 纳斯达克综合 / 黄金(XAUUSD)
SYMBOLS="000300.SH,HSI,SPX,COMP,XAUUSD"
TICKERS=$(tickdb ticker "$SYMBOLS" --json 2>/dev/null) || true
# 降级处理: 如果 ticker 拉取失败或数据为空,改用 kline-latest 取最新收盘价
USE_FALLBACK=false
if [ -z "$TICKERS" ] || ! echo "$TICKERS" | jq -e '.data' >/dev/null 2>&1; then
echo "[降级] ticker 不可用,改用 kline-latest"
TICKERS=$(tickdb kline-latest "$SYMBOLS" -i 1d --json 2>/dev/null) || true
USE_FALLBACK=true
fi
# ---- 2. 解析函数 ----
# ticker 模式解析:从 data[] 取字段
parse_ticker_field() {
local symbol="$1"
local field="$2"
local default="${3:-N/A}"
echo "$TICKERS" | jq -r --arg sym "$symbol" --arg f "$field" \
'.data[] | select(.symbol == $sym) | (.[$f] // "'"$default"'") | tostring'
}
# kline-latest 模式解析:从 data[].klines[0] 取字段
parse_kline_field() {
local symbol="$1"
local field="$2"
local default="${3:-N/A}"
echo "$TICKERS" | jq -r --arg sym "$symbol" --arg f "$field" \
'.data[] | select(.symbol == $sym) | .klines[0] // {} | (.[$f] // "'"$default"'") | tostring'
}
# 根据是否降级选择解析器
if [ "$USE_FALLBACK" = true ]; then
get_price() { parse_kline_field "$1" "close" "$2"; }
get_pct() { echo "N/A"; } # kline-latest 没有涨跌幅字段,统一标记 N/A
else
get_price() { parse_ticker_field "$1" "last_price" "$2"; }
get_pct() { parse_ticker_field "$1" "price_change_percent_24h" "0"; }
fi
# ---- 3. 提取各品种数据 ----
CSI300_PRICE=$(get_price "000300.SH")
CSI300_PCT=$(get_pct "000300.SH")
HSI_PRICE=$(get_price "HSI")
HSI_PCT=$(get_pct "HSI")
SPX_PRICE=$(get_price "SPX")
SPX_PCT=$(get_pct "SPX")
COMP_PRICE=$(get_price "COMP")
COMP_PCT=$(get_pct "COMP")
GOLD_PRICE=$(get_price "XAUUSD")
# 格式化涨跌幅:输出带正负号的两位小数百分比
fmt_pct() {
local pct="$1"
if [ "$pct" = "N/A" ] || [ -z "$pct" ]; then
echo "N/A"
else
# 用 awk 确保正数带 + 号,保留两位小数
awk -v p="$pct" 'BEGIN { printf "%+.2f%%", p }'
fi
}
CSI300_PCT_FMT=$(fmt_pct "$CSI300_PCT")
HSI_PCT_FMT=$(fmt_pct "$HSI_PCT")
SPX_PCT_FMT=$(fmt_pct "$SPX_PCT")
COMP_PCT_FMT=$(fmt_pct "$COMP_PCT")
# ---- 4. 组装 Markdown 报告 ----
REPORT_DATE=$(TZ='Asia/Shanghai' date '+%Y-%m-%d')
REPORT_TIME=$(TZ='Asia/Shanghai' date '+%H:%M:%S')
# Telegram Markdown 模式不支持表格和嵌套列表
# 用分隔线和反引号代替表格,数值用反引号包裹防止特殊字符误解析
REPORT=$(cat <<EOF
📊 *盘前日报 | ${REPORT_DATE} 07:00 CST*
━━━━━━━━━━━━━━━━━━
*A 股*
━━━━━━━━━━━━━━━━━━
沪深300:\`${CSI300_PRICE}\`(${CSI300_PCT_FMT})
━━━━━━━━━━━━━━━━━━
*港股*
━━━━━━━━━━━━━━━━━━
恒生指数:\`${HSI_PRICE}\`(${HSI_PCT_FMT})
━━━━━━━━━━━━━━━━━━
*美股*
━━━━━━━━━━━━━━━━━━
标普500:\`${SPX_PRICE}\`(${SPX_PCT_FMT})
纳斯达克综合:\`${COMP_PRICE}\`(${COMP_PCT_FMT})
━━━━━━━━━━━━━━━━━━
*贵金属*
━━━━━━━━━━━━━━━━━━
黄金(XAUUSD):\`${GOLD_PRICE}\`
━━━━━━━━━━━━━━━━━━
⏰ 生成时间:${REPORT_TIME} CST
📡 数据来源:TickDB
EOF
)
# ---- 5. 推送到 Telegram ----
# 使用 --data-urlencode 确保多行文本和特殊字符安全编码
SEND_RESULT=$(curl -s -X POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
--data-urlencode "parse_mode=Markdown" \
--data-urlencode "text=$REPORT" \
--data-urlencode "disable_web_page_preview=true")
# 检查推送是否成功
if echo "$SEND_RESULT" | jq -e '.ok' >/dev/null 2>&1; then
echo "日报已推送成功"
else
echo "推送失败: $SEND_RESULT"
exit 1
fi
设计考量:
set -euo pipefail:管道中任一命令失败立即退出。这是脚本里最关键的一行——缺了它,jq解析失败后脚本继续执行,会发出一条字段全为空的日报。生产环境里这种静默失败最难排查,因为它“看起来跑通了”。: "${VAR:?未设置}":bash 参数展开的快速校验语法,比if [ -z "$VAR" ]更简洁。三个环境变量缺任何一个立即终止,不给 Telegram 发半截消息。- 降级逻辑:如果 ticker 拉取失败(网络问题或返回空),自动降级到
kline-latest。降级后解析路径切换为klines[0].close,并正确处理涨跌幅字段缺失(统一标记 N/A)。ticker 返回的是最新快照,kline-latest 返回最近闭合 K 线的收盘价——闭市场景下后者反而更合适。 fmt_pct使用awk的%+格式符,确保正数显示+号,负数自然带-号,N/A 时保持原样。注释与行为一致。- Telegram 推送改用
--data-urlencode进行 URL 编码,避免多行文本、反引号、分隔线等特殊字符破坏请求体。这是从-d直接拼接升级的稳健做法,可以处理更复杂的报告内容。 - CLI 参数:TickDB CLI 采用位置参数,写法为
tickdb ticker symbol1,symbol2 --json,而不是--symbols。这是根据 npm README 和实际可运行版本确认的,直接照写即可跑通。
3.5 实测输出样例
以下是在测试环境中运行 tickdb ticker 000300.SH,HSI --json 得到的真实响应片段(经过精简):
{
"code": 0,
"data": [
{
"symbol": "000300.SH",
"last_price": 4826.192,
"price_change_percent_24h": 0.37,
"timestamp": 1716000000000
},
{
"symbol": "HSI",
"last_price": 19842.16,
"price_change_percent_24h": -0.85,
"timestamp": 1716000005000
}
]
}
kline-latest 的响应结构与此不同,数据嵌套在 data[].klines[0] 中,主要字段为 close 和 time。脚本中的降级分支已妥善处理这一差异。
3.6 GitHub Secrets 配置
在仓库 Settings → Secrets and variables → Actions 中添加三个 Secret:
| Secret 名称 | 说明 | 获取方式 |
|---|---|---|
TICKDB_API_KEY | API 密钥 | TickDB 控制台 |
TELEGRAM_BOT_TOKEN | Bot 令牌 | @BotFather 创建 Bot 后获取 |
TELEGRAM_CHAT_ID | 接收目标 | 给 Bot 发消息后从 getUpdates 接口获取 |
配置完成后,workflow_dispatch 手动触发一次验证全链路。
四、日报只是起点:同一套模板的扩展方向
这套方案的骨架是 cron 调度 + CLI 拉数据 + shell 组装 + Bot 推送。日报只用了 ticker 一个命令,TickDB CLI 实际提供了 16 个命令,覆盖 ticker、kline、depth 等场景。换个 cron 时间和 shell 脚本内容,同一套 workflow 可以衍生出多种用途:
| 场景 | cron(北京时间) | CLI 命令 | 用途 |
|---|---|---|---|
| 盘前日报 | 每天 7:00 | tickdb ticker | A股/港股/美股收盘快照 |
| 盘后复盘 | 每天 16:00 | tickdb kline | A股日线收盘汇总 |
| 周末周报 | 每周六 10:00 | tickdb kline | 周线趋势 + 涨跌幅排行 |
| 异动告警 | 每小时 | tickdb ticker + 涨跌幅阈值过滤 | 盘中异动推送 |
日报只是第一步。把这套 workflow 复制一份,改 cron 时间和脚本参数,就是盘后复盘、周报、异动告警——一套模板覆盖全时段。
CLI 在 CI/CD 场景下的适配性,比最初预想的高。核心原因就一个:定时任务天然是无状态、单次执行、不要求进程常驻的——这正是 CLI 最擅长的运行模式。 选型时如果直接往 Python 脚本方向走,反而会把方案变重。
局限性说明
- GitHub Actions
schedule触发精度为 ±15-30 分钟,高峰期可能更长。不适合需要精确到分钟的场景(如 9:29 必须拿到数据并下单)。日报对时间偏差不敏感,这个限制可以接受。 - Telegram
parse_mode=Markdown不支持 Markdown 表格和嵌套列表。复杂排版需切换到parse_mode=HTML或用图片方案,模板复杂度会上升。本方案使用的特殊字符较少,风险可控;若读者自行扩展文本内容,需注意_、[、]等字符的转义。 - 免费额度因账户类型和仓库可见性而异。GitHub 计费政策可能调整,以运行时的官方文档为准。若仓库同时有 CI 任务共享额度,或扩展到每小时跑一次,需要做用量规划。超过后会自动暂停,而非扣费——但日报断更的感受跟服务器宕机一样糟糕。
npm install -g tickdb@latest不锁版本,在将来可能出现破坏性变更。生产环境建议锁定具体版本号(如[email protected]),并在升级前在本地验证兼容性。- 降级链中的
kline-latest不含涨跌幅字段,降级时涨跌幅统一显示N/A,日报的信息完整性会有所下降。若需在降级时仍展示涨跌幅,可在脚本中额外拉取历史 K 线计算,但会增加复杂度。
延伸思考:从定时任务到事件驱动
每天跑一次日报,是定时任务最简单的形态。但如果未来要做“盘中每小时异动监控”,cron 最小粒度是 5 分钟——每 5 分钟跑一次会迅速耗尽免费额度。这个场景下,是用 GitHub Actions 继续硬扛,还是换到 WebSocket 长连接方案?
两种路径的分岔点在于你对“实时”的定义:能接受分钟级延迟,定时轮询就够了;需要秒级响应,长连接推模型绕不过去。 日报是前者,盘中监控往往是后者。
很多人以为定时任务需要买服务器,其实 GitHub Actions 的免费额度足够每天跑几十次。但你注意到没有——
cron是 UTC 时区,如果哪天你的目标时区切换到夏令时,而你没改 workflow,推送时间就会偏移。虽然北京时间没这个问题,但这个坑在美东、欧洲等市场真切存在。
你的日报系统跑在哪里?是云服务器 cron、GitHub Actions、还是本地 crontab?有没有因为 cron 时区问题被坑过?评论区聊聊。
参考文献
- TickDB CLI 文档,可搜索
docs.tickdb.ai查阅。 - GitHub Actions 官方文档——schedule 事件与 cron 语法。
📡 数据由 TickDB.ai 提供
本文不构成任何投资建议。
工程笔记
通过 TickDB API 获取实时行情数据
一个 API 接入外汇、加密货币、美股、港股、A股、贵金属和全球指数的实时行情。支持 WebSocket 低延迟推送,免费开始使用。
免费领取 API Key查看 API 文档