Imported from oMygpt/faro (
AGENTS.md). Install upstream withnpx skills add oMygpt/faro. Copyright stays with the author.
AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
项目目标
构建一个 FOF 系统:通过持有国内公募基金(QDII / 港股通 ETF / 主动型)来间接配置跨市场目标股票篮子,并按规则动态再平衡。
单用户系统(owner 自用)。无角色体系、不开放任何 Web 入口给非 owner。分享给特定朋友通过 Excel 导出完成。
不分阶段直接奔终局:曾考虑过 P1/P2/P3 演进,已废弃;改为按模块成熟度(v0/v1)演进。建造顺序见 docs/BUILD_ORDER.md。
系统数据流(核心)
基金代码池 → [穿透] → W (funds × tickers) 权重矩阵
目标篮子 t (ticker → 目标权重) ↓
↓ [反求] → FOF 权重 x: min ‖Wᵀx − t‖² s.t. 1ᵀx=1, x≥0
价格 + 汇率 → [当日估值] → 基金/FOF 估值
↓
[再平衡规则] → 操作建议
实时性:当日(EOD)级别,不做盘中实时。A 股 16:00 后批跑;美股次日早晨补。
五个核心抽象(封闭集合)
PRD_Overall.md §3 定义,不允许引入第六个:
- Basket — 目标股票篮子,跨市场,三种来源(白名单 / tag 规则 / UI 编辑),版本化只追加
- FundUniverse — 候选基金池 + 元数据,一个 owner 可有多个
- W — 穿透矩阵
(funds × tickers),每行权重和 ≤ 1,差额是未披露部分 - Solver — cvxpy QP 反求引擎
- Rebalancer + Valuator — 再平衡 + EOD 估值
数据模型一次定型在 PRD_Overall.md §5(终局版,不为简化场景预留缩减)。
仓库结构
baskets/ # YAML basket 定义(参数化主题)
ai_global.yaml # AI 全球龙头 v1(universe + target + 约束)
screening/ # akshare 一次性数据 pipeline
build_pool.py # 候选池:QDII场外 + LOF + ETF + 主动股票/混合
screen_qdii_funds.py # 4-worker 并行扫前十大持仓
build_report.py # 按子板块生成 md 报告
candidate_pool.md # 6 选 FOF 候选(手工 curate)
_funds_pool.csv / fund_screening.csv
scripts/
quarterly_refresh.sh # 一键季度刷新(4 步串起来)
backend/ # FastAPI + SQLModel + SQLite
pyproject.toml # uv-managed
app/
main.py # FastAPI app + lifespan + CORS
config.py # Settings (env: FOF_*)
db.py # SQLModel engine + init_db
models/ # 14 张表,按域分组
market.py / fund.py / basket.py / portfolio.py / event.py
routes/ # 8 个 router,对应前端 8 个页面
dashboard.py / baskets.py / funds.py / matrix.py
solver.py / rebalance.py / performance.py / audit.py
services/ # 业务逻辑
exposure.py # compute_exposure / residual / coverage
seed/seed_demo.py # 把设计稿 mock 数据灌进 SQLite
seed/seed_from_screening.py # 把真实扫描结果(328 funds)灌进 SQLite ★用这个
services/basket_yaml.py # YAML basket loader(screening + solver 共用)
data/fof.db # SQLite 文件(gitignored)
.venv/ # uv venv(gitignored)
frontend/ # Vite + React 18 + TypeScript
package.json # pnpm-managed
index.html
src/
main.tsx / App.tsx # entry + shell(侧栏 + topbar + ⌘K)
styles.css # 设计系统 tokens(直接来自设计稿)
api/ # client.ts + types.ts(手写匹配后端契约)
lib/ # format.ts + heat.ts
components/
Icon.tsx # 26 个 SVG 图标
ui/ # Sparkline / Donut / LineChart / StackBar / Pill / MarketTag
pages/ # 8 个页面(Dashboard 完整,其他 Wave N 占位)
docs/
PRD_Overall.md # 整体 PRD(已与 owner 对齐)
BUILD_ORDER.md # 12 个 Wave,按页面分组并行交付
UPDATE_FREQUENCY.md # 季度/月度/每日 更新频率 + cron 模板
DEPLOY.md # Docker compose 部署清单
archive/PRD_Phase1.md # 已废弃留底
design_drop/ # 设计稿原始包(HTML/JSX/CSS 原型)
research/ # qlib / PyPortfolioOpt / Riskfolio / skfolio 浅克隆
AI_FOF/ # 早期 Excel 原型,过渡期保留
Makefile # dev 命令入口
仓库不是 git 仓库。Wave 0 已完工,前后端能握手;Wave 1(Dashboard)已写完,其他 7 个页面是占位。
Excel 现状
AI_FOF_穿透模型.xlsx 是早期工件。系统建好后会被 M11 Excel 导出 替代(生成的 Excel 包含 5 个 sheet:当前组合 / 净值历史 / 再平衡历史 / 篮子定义 / 方法论)。新代码不要再依赖这个 xlsx——它仅在过渡期内由 refresh_holdings_via_tushare.py 维护。
「穿透矩阵」工作表的硬约定(旧脚本依赖):
- A 列:基金代码不带后缀(
513300.SH匹配513300) - 第 1 行:美股 ticker 大写不带交易所后缀(
NVDA而非NVDA.O) - 单元格:权重,
>1视为百分数自动除以 100 - 颜色:绿色斜体 = 真实披露;黄底 = 抓取失败/未披露
数据源
- 持仓:Tushare
fund_portfolio主源(owner 账户 5200 积分够用),AKSharefund_portfolio_hold_em兜底 - 价格:A/H 用 Tushare/AKShare;美股用 Tushare(QDII ticker)+ Yahoo/AKShare 兜底
- 汇率:USD/CNY、HKD/CNY,每日一次
主动型基金披露不全(≥50% 持仓不公开)是已知坑,未披露部分按"分类指数/行业平均"近似(在 Valuator 里实现,不在 W 里硬填)。
常用命令
# 一次性安装
make install # backend (uv) + frontend (pnpm)
# 季度刷新(推荐)— 一键串起 4 步:池更新 → 扫持仓 → 报告 → 写 SQLite
./scripts/quarterly_refresh.sh # 默认 baskets/ai_global.yaml
./scripts/quarterly_refresh.sh baskets/another_theme.yaml # 换主题
# 单步操作
cd screening && python build_pool.py # 拉候选池
cd screening && python screen_qdii_funds.py --basket ../baskets/ai_global.yaml --workers 4
cd screening && python build_report.py --basket ../baskets/ai_global.yaml
cd backend && .venv/bin/python -m app.seed.seed_from_screening # CSV → SQLite
# 旧 mock 数据库重置(仅 demo 演示用,会丢真实数据)
make seed # 用 backend/app/seed/seed_demo.py
# 开发(两个终端)
make dev-backend # FastAPI on :8000,--reload
make dev-frontend # Vite on :5173,proxy /api → :8000
# 浏览器
# http://localhost:5173 — Workbench UI
# http://127.0.0.1:8000/docs — FastAPI Swagger
# 停服
make stop # 干掉 :8000 / :5173
# 旧 Excel 刷新脚本(过渡期)
cd AI_FOF && python refresh_holdings_via_tushare.py
# 副作用:备份 → AI_FOF_穿透模型.bak.xlsx;日志 → refresh_log.csv
# Tushare token 硬编码在脚本顶部 TUSHARE_TOKEN
后端 venv 路径:backend/.venv。直接调 Python:backend/.venv/bin/python -m app.seed.seed_demo。
开源借鉴决策
| 模块 | 决策 | 理由 |
|---|---|---|
| 反求 Solver | 自写 ~30 行 cvxpy QP | 三家库都没现成 ‖Wᵀx − t‖² API |
| Walk-forward 回测 | 直接 import skfolio.model_selection | sklearn 风格 CV 免费拿 |
| 约束 DSL(tag 暴露) | 借鉴 skfolio linear_constraints 字符串语法 |
借语法不借实现 |
| 风险预算 / HRP | 不上 Riskfolio | 依赖过重(C++/vectorbt/astropy) |
| qlib | 不作基础 | 单标的视角,无基金穿透概念 |
"借鉴能力,不抄代码"原则:要么 import 用、要么读完用自己语言重写。禁止"抄一段+改两个变量名"。research/ 下的 4 个 repo 只读不改。
工作约定
- 复杂任务先确认方案再动手;一句话够用时不写文档
- 任何模块的 v0 → v1 必须有 done criteria 验收,不允许半成品永久滞留
- 持仓矩阵以季报披露为准,未披露部分是已知盲区,不要假装能算准
- 跨市场字段(汇率、计价币种)在 Tushare 里语义不统一,写代码前先验证字段含义
- 不可变性:basket_version / W / rebalance_event / valuation_daily 只追加,不更新不删除
- 单用户契约:任何模块都不假设"多 owner"
当前状态 — 所有 12 个 Wave 已写完
- ✅ 整体 PRD 冻结(
docs/PRD_Overall.md) - ✅ BUILD_ORDER 冻结(
docs/BUILD_ORDER.md) - ✅ DEPLOY 文档(
docs/DEPLOY.md) - ✅ Wave 0 — 项目骨架(前后端握手)
- ✅ Wave 1 — Dashboard(实时从后端拿组合 / 暴露 / 漂移 / NAV)
- ✅ Wave 2 — 目标篮子(CRUD + 版本时间线 + diff 视图 + 拖拽编辑权重)
- ✅ Wave 3 — 反求引擎(真 cvxpy CLARABEL QP,约束实时滑条触发重算,约束放松降级)
- ✅ Wave 4 — 再平衡(5 触发 + before/after diff + 操作清单 + execute/reject)
- ✅ Wave 5 — 穿透矩阵 W(热力图 + 披露完整度 + ticker 跨基金分布)
- ✅ Wave 6 — 基金池(筛选 / 详情 / 持仓侧栏)
- ✅ Wave 7 — 净值与归因(多区间 + KPI + 贡献度)
- ✅ Wave 8 — 审计日志(事件流)
- ✅ Wave 9 — 数据接入脚手架(Tushare + AKShare 适配,cron 入口;FOF_INGEST_ENABLED 闸门保护)
- ✅ Wave 10 — 回测(walk-forward over historical W snapshots)
- ✅ Wave 11 — Excel 导出(6 sheet:当前组合 / 合成暴露 / 净值历史 / 再平衡历史 / 篮子定义 / 方法论)
- ✅ Wave 12 — Docker compose(backend + frontend nginx + nightly backup)+ 部署清单
后端 API 总览
| 路径 | 方法 | Wave | 用途 |
|---|---|---|---|
/health |
GET | 0 | 健康探针 |
/api/funds、/api/funds/{code}、/api/funds/{code}/holdings |
GET | 6 | 基金 CRUD |
/api/universes |
GET | 6 | universe 列表 |
/api/baskets、/api/baskets/{id} |
GET | 2 | 篮子查询 |
/api/baskets |
POST | 2 | 新建篮子 |
/api/baskets/{id}/versions |
GET / POST | 2 | 版本列表 / 新建版本 |
/api/baskets/{id}/versions/{vid} |
GET | 2 | 版本详情 |
/api/baskets/{id}/diff?a=&b= |
GET | 2 | 版本 diff |
/api/portfolio/{id} |
GET | 1 | 组合总览 |
/api/portfolio/{id}/exposure |
GET | 1 | 合成暴露 |
/api/portfolio/{id}/drift |
GET | 1 | 偏差 |
/api/portfolio/{id}/nav |
GET | 1 | NAV 历史 |
/api/w-matrix?universe_id= |
GET | 5 | 穿透矩阵 |
/api/solver/run |
POST | 3 | cvxpy QP 求解 |
/api/rebalance/evaluate |
POST | 4 | 跑评估 + 写事件 |
/api/rebalance/events、/api/rebalance/events/{id} |
GET | 4 | 事件查询 |
/api/rebalance/events/{id}/execute |
POST | 4 | 标记已执行 |
/api/rebalance/events/{id}/reject |
POST | 4 | 拒绝建议 |
/api/backtest/run |
POST | 10 | walk-forward 回测 |
/api/performance/{id}/nav |
GET | 7 | 净值时间序列 |
/api/audit |
GET | 8 | 审计日志 |
/api/export/snapshot/{id} |
GET | 11 | 下载 .xlsx |
当前已知缺陷(不影响验收,列在这便于后续修)
datetime.utcnow在 Python 3.12 deprecated(用datetime.now(UTC)后续替换)- Dashboard 残差 L1 大(72%):原始 mock 中候选基金对目标 ticker 覆盖不全的真实数学反映;不是 bug,求解器跑完会降到 ~64%
- 历史 basket version v1..v6 只有版本元数据,没有 items;v7 才有完整 items —— 用 UI 做新版本就有 diff
- Wave 9 真实网络调用未连(默认关闭),种子数据仍是 mock;要切真数据时设
FOF_INGEST_ENABLED=1+ 安装[ingest]extras + 跑python -m app.ingest.jobs quarterly - backtest 当前只有 1 个 report_date(mock 只灌了 2026-03-31 一份),所以 walk-forward 只有 1 帧;真实数据接入后自然填满