Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ETF 每日持仓日报

抓取 Global X HSCEI Covered Call Active ETF 页面的完整持仓,生成摘要并邮件发送。

关键点:该基金页面的 "FULL HOLDINGS (.CSV)" 按钮是纯前端(table2CSV.js), 把页面里已有的持仓表格在浏览器转成 CSV,没有后端下载链接。 所以本工具直接抓页面 HTML 解析那张表 —— 结果与点按钮导出的 CSV 等价,且更稳定。

它做了什么

  1. 抓页面(超时 + 重试 + 内容校验)

  2. 解析完整持仓表 + 期权敞口表(按表头列名认表,不依赖易变的 id),拆分正股 / 衍生品; 基金净值 NAV 与 HSCEI 收盘点位直接取页面公布值,「市值 ÷ 权重」反推值仅作兜底与校验

  3. 邮件发送:HTML 正文 + 当日完整持仓 CSV 附件

    邮件主题:YYYYMMDD 3416.HK 持仓(日期为持仓截止日,代码取自 FUND_NAME) 数据不全时加前缀 【数据不全】,补发的更新版加 【更新】

    正文内容:

    • 抬头:持仓截止日期(数据不全 / 补发时,上方另有一条醒目告警条)
    • a. 指数现价 → 行权价距离(含剩余到期天数,按到期日排序)
    • b. 风险暴露(同时给「名义口径」与「Delta 调整后」两列):
      • 正股敞口 ≈ 总正股市值 / 基金净值
      • 期货多头敞口 = (合约张数 × 指数点 × 50) / 基金净值
      • 期权空头敞口:名义 = Σ(合约张数 × 指数点 × 50) / 基金净值; Delta 调整 = 逐腿「该腿名义占比 × N(d1)」再求和, 每条腿的波动率各自由它自己的市价反解(见下文)
      • 净方向性敞口 = 正股 + 期货 + 期权(Delta 调整)
    • c. 前五大重仓占比 XX%(标题即合计占比,下接明细表)
    • 较上一交易日变化:新增 / 剔除 / 权重变化 Top5(仅正股)
  4. 全程日志;失败发告警邮件;按持仓截止日期幂等去重(详见下文「调度与幂等」)

敞口与 Delta 怎么算的

  • 敞口来源以「完整持仓表」的合约行为准,官网那张「期权敞口表」只用于交叉校验 (两者名义占比差 >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-Scholes N(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 分钟跑)

官网持仓是「隔日更新」,且当天具体几点更新不固定。为尽快收到、又不重复发, 采用「高频触发 + 按日期去重」:

  • 触发:工作日每 10 分钟跑一次(见部署)。
  • 每次跑都读页面上的 As of <日期> 作为「持仓截止日」,与状态文件 data/state.jsonlast_as_of 比对:
    • 日期没更新 → 直接跳过:不发邮件、不存快照、工作流仍成功(nothing to commit)。
    • 日期推进到新交易日 → 发一封(带「较上一交易日变化」),并把新日期写回 state.json
  • 净效果:数据更新后最多 10 分钟内收到,且每个交易日最多一封
  • 调试强制重发:FORCE_SEND=1(忽略去重)。

「数据未就绪」等待与补发(为什么 2026-07-31 那封是残的)

官网存在分阶段发布: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 那封残缺日报留下的 —— 默认成「完整」的话, 这套补发机制对它唯一要修的那一天完全失效。现在部署后会自动补发一封 【更新】

失败通知(两层兜底)

高频触发下,故障也不会刷屏 —— 两层告警都按天去重:

  1. 脚本层(SMTP 告警):任一步异常 → 发一封失败邮件;同一天只发一次 (state.jsonlast_alert_date)。
  2. 工作流层(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

Foxmail / QQ 邮箱配置

SMTP_PASS 填的是授权码,不是登录密码: QQ邮箱 → 设置 → 账号 → 开启 IMAP/SMTP → 生成授权码。 SMTP_HOST=smtp.qq.com,SMTP_PORT=465(SSL)。

部署:GitHub Actions(已上线,免运维免费)

当前部署在 公开仓库(公开仓库的 Actions 额度无限,故可每 10 分钟跑; 私有仓库免费额度 2,000 分钟/月,高频会不够)。授权码等放加密 Secret,公开不泄露

  1. 推到一个 公开 GitHub 仓库。
  2. 仓库 Settings → Secrets and variables → Actions,加这 6 个 Secret: SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASS MAIL_FROM MAIL_TO (FUND_NAMEFUND_URL 非敏感,直接写在 workflow 的 env: 里,改基金只改这两行。)
  3. .github/workflows/daily.yml 已配好:
    • 定时 */10 3-15 * * 1-5(UTC)= 工作日 港时 11:00–23:50 每 10 分钟;
    • 也可在 Actions 页面点 "Run workflow" 手动触发;
    • 跑完把当日快照(data/holdings_*.csvstate.json)提交回仓库,供次日对比。
  4. 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

About

Global X HSCEI Covered Call ETF (3416.HK) 每日持仓抓取 + 风险暴露邮件日报 · GitHub Actions 定时自动运行

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages