综合

每天 7 点自动出盘前日报:GitHub Actions + TickDB CLI 工程记录

作者: TickDB Research · 发布: 2026/5/23 · 阅读: 6

标签: 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 CLInpm install -g tickdb@latestNode.js ≥ 18(GitHub runner 自带)✅ 是
Python SDKpip install tickdb-python requestsPython + 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] 中,主要字段为 closetime。脚本中的降级分支已妥善处理这一差异。

3.6 GitHub Secrets 配置

在仓库 Settings → Secrets and variables → Actions 中添加三个 Secret:

Secret 名称说明获取方式
TICKDB_API_KEYAPI 密钥TickDB 控制台
TELEGRAM_BOT_TOKENBot 令牌@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:00tickdb ticker --jsonA股/港股/美股收盘快照
盘后复盘每天 16:00tickdb kline -i 1d --jsonA股日线收盘汇总
周末周报每周六 10:00tickdb kline -i 1w --json周线趋势 + 涨跌幅排行
异动告警每小时tickdb ticker --json + 涨跌幅阈值过滤盘中异动推送

日报只是第一步。把这套 workflow 复制一份,改 cron 时间和脚本参数,就是盘后复盘、周报、异动告警——一套模板覆盖全时段。

CLI 在 CI/CD 场景下的适配性,比最初预想的高。核心原因就一个:定时任务天然是无状态、单次执行、不要求进程常驻的——这正是 CLI 最擅长的运行模式。 选型时如果直接往 Python 脚本方向走,反而会把方案变重。


局限性说明

  1. GitHub Actions schedule 触发精度为 ±15-30 分钟,高峰期可能更长。不适合需要精确到分钟的场景(如 9:29 必须拿到数据并下单)。日报对时间偏差不敏感,这个限制可以接受。
  2. Telegram parse_mode=Markdown 不支持 Markdown 表格和嵌套列表。复杂排版需切换到 parse_mode=HTML 或用图片方案,模板复杂度会上升。本方案使用的特殊字符较少,风险可控;若读者自行扩展文本内容,需注意 _[] 等字符的转义。
  3. 免费额度因账户类型和仓库可见性而异。GitHub 计费政策可能调整,以运行时的官方文档为准。若仓库同时有 CI 任务共享额度,或扩展到每小时跑一次,需要做用量规划。超过后会自动暂停,而非扣费——但日报断更的感受跟服务器宕机一样糟糕。
  4. npm install -g tickdb@latest 不锁版本,在将来可能出现破坏性变更。生产环境建议锁定具体版本号(如 [email protected]),并在升级前在本地验证兼容性。
  5. 降级链中的 kline-latest 不含涨跌幅字段,降级时涨跌幅统一显示 N/A,日报的信息完整性会有所下降。若需在降级时仍展示涨跌幅,可在脚本中额外拉取历史 K 线计算,但会增加复杂度。

延伸思考:从定时任务到事件驱动

每天跑一次日报,是定时任务最简单的形态。但如果未来要做“盘中每小时异动监控”,cron 最小粒度是 5 分钟——每 5 分钟跑一次会迅速耗尽免费额度。这个场景下,是用 GitHub Actions 继续硬扛,还是换到 WebSocket 长连接方案?

两种路径的分岔点在于你对“实时”的定义:能接受分钟级延迟,定时轮询就够了;需要秒级响应,长连接推模型绕不过去。 日报是前者,盘中监控往往是后者。

很多人以为定时任务需要买服务器,其实 GitHub Actions 的免费额度足够每天跑几十次。但你注意到没有——cron 是 UTC 时区,如果哪天你的目标时区切换到夏令时,而你没改 workflow,推送时间就会偏移。虽然北京时间没这个问题,但这个坑在美东、欧洲等市场真切存在。


你的日报系统跑在哪里?是云服务器 cron、GitHub Actions、还是本地 crontab?有没有因为 cron 时区问题被坑过?评论区聊聊。


参考文献

  1. TickDB CLI 文档,可搜索 docs.tickdb.ai 查阅。
  2. GitHub Actions 官方文档——schedule 事件与 cron 语法。

📡 数据由 TickDB.ai 提供

本文不构成任何投资建议。

工程笔记

通过 TickDB API 获取实时行情数据

一个 API 接入外汇、加密货币、美股、港股、A股、贵金属和全球指数的实时行情。支持 WebSocket 低延迟推送,免费开始使用。

免费领取 API Key查看 API 文档

相关文章