market metrics —— 一个只读的、每日美股收盘后自动更新的指标看板。
观测 QQQ 与 16 只纳斯达克个股(共 17 个标的),计算 RSI(14)、EMA(60)、 半年 alpha & beta(基准 QQQ)、财报与分红的前后距离, 并给出当日三强股及其强度衡量参数。
架构:GitHub(代码 + Actions 调度)→ Supabase(Postgres 存储)→ Vercel(Next.js 前端)
状态:M0–M7 已实现并对生产库实跑。管道每个交易日收盘约一小时后先出临时结果 (收盘价取自分钟线,页面右上角会标明),当晚或次日换成正式收盘价; 前一天及更早的数据都是定稿(见
docs/provisional-close.md)。
uv sync # Python 管道(需要 Python 3.13)
uv run pytest # 全部测试
uv run ruff check . && uv run mypy
cd web && npm ci && npm run dev # 前端跑一次管道需要 MARKME_DB_URL(见 .env.example):
uv run python -m pipeline.run_daily # 每日增量(带四重闸门)
uv run python -m pipeline.backfill # 全量回填整个窗口
uv run python -m pipeline.check_invariants # 线上数据库的 22 条不变式整套设计围绕一类特定的故障:看起来完全正常的错数。 几个具体例子,每一条都在实现过程中真实发生过:
| 如果不管它 | 会怎样 |
|---|---|
| 只重写当天一行价格 | 供应商在除息日追溯改写全部历史复权因子,跨接缝的 mom_20 误差恰好等于股息率 —— 足以让第 3、4 名换位,而所有闸门全绿 |
| 降级时只补缺的那一行 | 那一行会落进 60 根 EMA 窗口与 126 日收益窗口,污染其后半年的 β 与 α |
| 闸门只断言基准有新 bar | 某只个股拿到昨天的 bar,于是三强榜是一个混合日期的横截面,并被「在榜 N 天」永久烤进历史 |
| 事件表用日期做主键 | 财报改期会留下一条作废的预告行,倒计时走向一个不存在的日子,到期后播报一场从未发生的财报 |
事件抓取失败记成 partial |
条件重试不再跳过,放行的那几跑全部重跑,而降级到备源的那一跑会静默覆盖好数据 |
所以库里有 22 条不变式,每天连库跑一次;所有非有限值一律转 NULL
(绝不用 0 或上一日的值冒充);任何一个指标窗口内的数据源必须唯一。
| 文档 | 说明 |
|---|---|
| AGENTS.md | 仓库约定:目录规范、skills 位置、review 闸门、独立性 |
| docs/create-project.md | 项目从零到上线的建设计划(指标口径、架构、数据库与 RLS、前端设计、里程碑) |
| docs/reviews/ | 各里程碑的 review 闸门记录 |
| skills/ | 可复用的 agent skill(与具体 agent 无关,不放 .claude/) |
计划文档一律放在 docs/,每份用一个说明其内容的具体文件名
(小写 kebab-case,如 create-project.md、add-sector-metrics.md),
不使用 workplan.md / plan.md 这类通用名 —— 仓库里迟早会有多份计划,
通用名会立刻失去指向性。新增计划后在下表登记一行。
| 文档 | 说明 | 状态 | 更新 |
|---|---|---|---|
| create-project.md | 项目建设计划;§7.2 闸门 4 的第二意见必须比到被标记的每一天(M15,闸门记录 M15) | v3 + 实现期按实测修正 | 2026-10-14 |
| provisional-close.md | 收盘后先出临时结果、次日定稿(M13);当天半根日线改走分钟线(M14,§7.3);闸门记录 M13、M14 | 已确认,实现中 | 2026-10-14 |
站点前面有一道象征性的密码。它不是安全措施,是合规与成本措施。
- 数据源的再分发条款。 行情数据来自 Yahoo(经
yfinance)。 Yahoo 的条款禁止再分发其市场数据,yfinance自身也把用途定位为个人研究。 一个可被公开检索的数据站,和一个只有我自己打开的私人页面, 在这件事上不是一回事。加一道密码让页面不被搜索引擎索引, 这个(概率不高但真实的)暴露面就显著下降了。 - 免费额度。 爬虫进不来,Vercel 与 Supabase 的免费额度就不会被刷爆。
需要说清楚的三件事,免得误解:
- 这不是安全边界。 真正的访问控制在数据库侧。
- 密码不在仓库里,它是 Vercel 的环境变量
SITE_PASSWORD;.env.example里只有占位符。 - 如果将来去掉密码、或站点产生实质流量,就该换到许可允许再分发的数据源 (Tiingo / Polygon / EODHD),而不是继续靠「没人看得到」来规避。
本站展示的是公开行情数据的统计派生量,不构成任何投资建议。 数据来源与口径见站内方法论页。
config/ 四个 YAML —— 标的池、指标、强度、运行参数(前端构建期直读)
pipeline/ Python 管道:抓取 → 闸门 → 计算 → 三事务写入
supabase/ 0001_init.sql + 回滚脚本 + invariants.sql(20 条)
web/ Next.js 前端(Server Components + ISR)
.github/ 五个 workflow:daily / backfill / ci / keepalive / heartbeat
这个项目的每个里程碑都过两道 review 闸门(见 docs/reviews/):
一组 Claude review agent,再加一轮外部 codex CLI 复核,修到干净为止才推进。
M0–M8 一共抓到 45 条 SERIOUS,逐条记在 docs/reviews/。
其中很大一部分不在功能代码里,而在验证手段里 —— 断言换个角色跑就失明、
守卫写在 WHERE 里对无 GROUP BY 的聚合是死代码、缺凭证时整组测试静默跳过
而 CI 全绿、解析器答错而不是报错、测试断言命中了注释里的解释文字而不是代码。
还有三条是上一轮的修复自己制造的:修复本身对,它与另一条已经成立的约束的 交互错了。单看 diff 看不出来 —— 这就是每一轮都重新全量复核、而不是只看新 diff 的理由。
它们的共同点是:坏掉时的症状是「一切正常」。 这也是本项目为什么在不变式、闸门和诚实标注上花掉了远多于「画图表」的篇幅。
markme (market metrics) is a read-only US-equity indicator dashboard that publishes a preliminary result about an hour after each market close (the close taken from the last 1-minute bar, flagged on the page) and replaces it with the official close that night or the next morning; history is always final.
It tracks QQQ plus 16 Nasdaq stocks (17 symbols) and computes RSI(14), EMA(60), half-year alpha & beta against QQQ, and days to/since the next and last earnings and dividend, along with a daily top-three ranking and the parameters that describe how solid that ranking is.
Stack: GitHub (code + scheduled Actions) → Supabase (Postgres) → Vercel (Next.js).
The whole design is organised around one failure class: numbers that are wrong but look completely normal. Every example below actually happened during implementation, not in theory:
- Vendors retroactively rewrite adjustment factors on every dividend, so an incremental pipeline that only writes today's row produces a seam whose error equals the dividend yield — enough to swap ranks 3 and 4 with every gate green.
- A single row patched in from the fallback source lands inside a 60-bar EMA window and a 126-day return window, corrupting beta and alpha for the next six months.
- yfinance's
Adj Closeis only stable to float32, so naive change-detection rewrites ~8% of rows forever and the adjustment-factor alarm fires every run. Ticker.calendarandTicker.earnings_datesdescribe the same next earnings one day apart; storing both makes the countdown announce an earnings that never happens.
Hence 20 database invariants run against production daily, a hard rule that
non-finite values become NULL (never 0, never yesterday's value), and a
requirement that any one metric window uses exactly one data source.
The site sits behind a symbolic password. It is not a security boundary — the repository is public and the real boundary is Postgres GRANT + RLS. It buys two concrete things: the page is not indexable (which matters because the data vendor's terms forbid redistribution) and crawlers cannot burn the free tier.
MIT — see LICENSE.