抓取 Global X HSCEI Covered Call Active ETF 页面的完整持仓,生成摘要并邮件发送。
关键点:该基金页面的 "FULL HOLDINGS (.CSV)" 按钮是纯前端(
table2CSV.js), 把页面里已有的持仓表格在浏览器转成 CSV,没有后端下载链接。 所以本工具直接抓页面 HTML 解析那张表 —— 结果与点按钮导出的 CSV 等价,且更稳定。
-
抓页面(超时 + 重试 + 内容校验)
-
解析完整持仓表 + 期权敞口表(按表头列名认表,不依赖易变的 id),拆分正股 / 衍生品; 基金净值 NAV 与 HSCEI 收盘点位直接取页面公布值,「市值 ÷ 权重」反推值仅作兜底与校验
-
邮件发送:HTML 正文 + 当日完整持仓 CSV 附件
邮件主题:
YYYYMMDD 3416.HK 持仓(日期为持仓截止日,代码取自FUND_NAME) 数据不全时加前缀【数据不全】,补发的更新版加【更新】。正文内容:
- 抬头:持仓截止日期(数据不全 / 补发时,上方另有一条醒目告警条)
- a. 指数现价 → 行权价距离(含剩余到期天数,按到期日排序)
- b. 风险暴露(同时给「名义口径」与「Delta 调整后」两列):
- 正股敞口 ≈ 总正股市值 / 基金净值
- 期货多头敞口 = (合约张数 × 指数点 × 50) / 基金净值
- 期权空头敞口:名义 = Σ(合约张数 × 指数点 × 50) / 基金净值; Delta 调整 = 逐腿「该腿名义占比 × N(d1)」再求和, 每条腿的波动率各自由它自己的市价反解(见下文)
- 净方向性敞口 = 正股 + 期货 + 期权(Delta 调整)
- c. 前五大重仓占比 XX%(标题即合计占比,下接明细表)
- 较上一交易日变化:新增 / 剔除 / 权重变化 Top5(仅正股)
-
全程日志;失败发告警邮件;按持仓截止日期幂等去重(详见下文「调度与幂等」)
- 敞口来源以「完整持仓表」的合约行为准,官网那张「期权敞口表」只用于交叉校验
(两者名义占比差 >1 个百分点会告警)。原因:两张表更新不同步,新加的腿
(如 2026-08 新纳入的
CALL HSCEI WEEKLY OPTION)可能晚一步才进敞口表, 拿它当主源会静默漏算;持仓表则同时给出张数和期权市价。 - 工具识别集中在三条规则(
EQUITY_TICKER_RE/FUTURE_RE/OPTION_RE), 基金新增工具类型只改这三行。行权价与到期日一律从合约名称解析,已覆盖... WEEKLY OPTION 08/07/26 8750、... 08/28/26 C8700 OTC(页面会把C8700渲染断开成C870 0)、... HANG SENG CHINA ENT 08/28/26 8700等写法。 - Delta 不用拍脑袋的假设波动率:持仓表里的
Market Price就是期权的指数点报价, 先由它反解隐含波动率(二分法),再算 Black-ScholesN(d1)。反解不出来才退回IMPLIED_VOL(默认 30%),且会在日志和正文里标明。实测差异不小:2026-07-31 那天市价反解出的 IV 是 21.1% / 21.6%, 而非假设的 30%,「Delta 调整后」由 -45.04% 修正为 -42.72%。
- 算不出剩余期限的腿一律不参与计算,并标红。
T<=0时 BS Delta 会退化成 「实值 1 / 虚值 0」的阶跃值,所以下面这些情况都必须先拦住,否则那一格会静默变成 0.00% 或直接等于名义值:合约已过期却仍挂在页面上、到期日解析不出、持仓截止日期解析不出。 - 任何一条腿被丢掉都算「数据不全」(张数缺失、行权价解析失败、行权价与指数明显不匹配、
看跌期权……)。丢腿只会让敞口偏小,而偏小的百分比长得和正常日报一模一样 ——
所以它必须进邮件告警条,而不是只进
run.log(CI 的日志随 runner 销毁,且被 gitignore)。
官网持仓是「隔日更新」,且当天具体几点更新不固定。为尽快收到、又不重复发, 采用「高频触发 + 按日期去重」:
- 触发:工作日每 10 分钟跑一次(见部署)。
- 每次跑都读页面上的
As of <日期>作为「持仓截止日」,与状态文件data/state.json里last_as_of比对:- 日期没更新 → 直接跳过:不发邮件、不存快照、工作流仍成功(
nothing to commit)。 - 日期推进到新交易日 → 发一封(带「较上一交易日变化」),并把新日期写回
state.json。
- 日期没更新 → 直接跳过:不发邮件、不存快照、工作流仍成功(
- 净效果:数据更新后最多 10 分钟内收到,且每个交易日最多一封。
- 调试强制重发:
FORCE_SEND=1(忽略去重)。
官网存在分阶段发布:As of 日期已经翻到新交易日,但完整持仓表里
只有正股,期货/期权合约行要过一会儿才补上。2026-07-31 就撞上了这个 ——
脚本在那一刻抓到数据发了信,期货/期权全是 N/A,又因为幂等标记再也不会重发。
现在的处理:
| 情况 | 行为 |
|---|---|
| 期权/期货合约行整类缺失,或条数少于基准快照,或与官网敞口表差 >0.3 个百分点,或有腿被丢掉 | 判为数据不全;若「等一等可能会好」(分阶段发布)→ 不发信、不写幂等标记,等下次触发(10 分钟后再看) |
等待超过 INCOMPLETE_GRACE_HOURS(默认 3 小时)仍不全 |
先发一封 【数据不全】 兜底,正文顶部红色告警条逐条列明问题 |
| 问题属于「等也没用」(如官网到期日写法改版) | 不白等,立即发带红色告警条的日报 |
| 之后官网补齐 / 解析修好 | 自动补发一封 【更新】,每个截止日最多补一次 |
| 官网「期权敞口表」缺失 | 照常发信(敞口本来就由持仓表算),只在正文黄色告警条提示无法交叉校验 |
页面解析不出 As of 日期,或截止日期比已发过的还早 |
不发日报(去重键会失效 → 会按 cron 频率刷屏);前者走按天去重的失败告警邮件,后者静默跳过 |
两个容易踩的坑,已经绕开:
- 基准快照不能取"上一份",要取最近一份真正含衍生品的快照。
否则昨天那封兜底存下的残缺 CSV(
data/holdings_20260731.csv就是)会成为今天的基准, 护栏自己把自己解除。 state.json里读不到last_complete时按「不完整」处理。旧版从不写这个键, 而线上那份 state 恰恰是 2026-07-31 那封残缺日报留下的 —— 默认成「完整」的话, 这套补发机制对它唯一要修的那一天完全失效。现在部署后会自动补发一封【更新】。
高频触发下,故障也不会刷屏 —— 两层告警都按天去重:
- 脚本层(SMTP 告警):任一步异常 → 发一封失败邮件;同一天只发一次
(
state.json的last_alert_date)。 - 工作流层(GitHub Issue):
if: failure()时自动建一个 Issue(每天最多一个), 覆盖"连告警邮件都发不出去 / Runner 自身挂了"等脚本兜不住的情况; GitHub 会就该 Issue 通知你。需issues: write权限(workflow 已声明)。
pip install -r requirements.txt
cp .env.example .env # 填好邮箱授权码和收件人
set -a; source .env; set +a # 加载环境变量
python daily_holdings.py快照、日志、状态都在 data/ 下。调试想强制重发:FORCE_SEND=1 python daily_holdings.py
SMTP_PASS 填的是授权码,不是登录密码:
QQ邮箱 → 设置 → 账号 → 开启 IMAP/SMTP → 生成授权码。
SMTP_HOST=smtp.qq.com,SMTP_PORT=465(SSL)。
当前部署在 公开仓库(公开仓库的 Actions 额度无限,故可每 10 分钟跑; 私有仓库免费额度 2,000 分钟/月,高频会不够)。授权码等放加密 Secret,公开不泄露。
- 推到一个 公开 GitHub 仓库。
- 仓库 Settings → Secrets and variables → Actions,加这 6 个 Secret:
SMTP_HOSTSMTP_PORTSMTP_USERSMTP_PASSMAIL_FROMMAIL_TO(FUND_NAME、FUND_URL非敏感,直接写在 workflow 的env:里,改基金只改这两行。) .github/workflows/daily.yml已配好:- 定时
*/10 3-15 * * 1-5(UTC)= 工作日 港时 11:00–23:50 每 10 分钟; - 也可在 Actions 页面点 "Run workflow" 手动触发;
- 跑完把当日快照(
data/holdings_*.csv、state.json)提交回仓库,供次日对比。
- 定时
- 用
gh一键创建并配置(参考)::gh repo create <name> --public --source=. --push set -a; source .env; set +a for k in SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASS MAIL_FROM MAIL_TO; do gh secret set "$k" --body "${!k}"; done gh workflow run daily-holdings # 手动触发一次验证
注:GitHub 定时任务不保证准点,高峰期可能延迟几分钟或偶尔跳过;配合幂等去重不影响结果。
想改用自己的服务器(如 Oracle Cloud 永久免费机)也行,等价 crontab:
*/10 11-23 * * 1-5 cd /path/app && set -a && . ./.env && set +a && /path/venv/bin/python daily_holdings.py