diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-20 22:59:00 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-20 22:59:00 +0800 |
| commit | c60905e9e7f992a7f8c79c3812e92a44b2d606f5 (patch) | |
| tree | a895bc6f0ebe669407ffe4d1a11439e03a6fe0d4 /docs/implementation-plan.md | |
| download | fund-lab-c60905e9e7f992a7f8c79c3812e92a44b2d606f5.tar.gz | |
feat(core): 建立 fund-lab 可运行基线
[变更性质]
- 本提交冻结当前可构建、可测试的应用基线,不包含 PostgreSQL 持久化。
[新增功能]
- 建立 F# Domain、API、Worker、Web 及测试项目。
- 增加 Bearer 认证、健康检查、账本领域模型和中文空状态页面。
[实现方案]
- 使用环境变量模板注入认证配置,并排除数据、凭证和构建产物。
- 保留 19 个 Domain 测试和 5 个 API 测试作为后续变更基准。
[影响范围]
- 为后续 3a PostgreSQL FOF 创建/读取切片提供可回滚基线。
- 当前仍不接入真实基金数据、真实交易或数据库。
Diffstat (limited to 'docs/implementation-plan.md')
| -rw-r--r-- | docs/implementation-plan.md | 329 |
1 files changed, 329 insertions, 0 deletions
diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md new file mode 100644 index 0000000..9bd3c4f --- /dev/null +++ b/docs/implementation-plan.md @@ -0,0 +1,329 @@ +# fund-lab 实施计划 + +状态:已获 Hermes 方案批准,进入阶段 1 骨架实施;后续业务阶段仍需按本计划逐项验收。 + +## 1. 执行原则 + +- 先完成可验证的最小垂直切片,再扩展股票和债券。 +- 任何数据源不确定性都进入数据质量状态,不用假数据掩盖。 +- 交易、估值、来源修订和采集运行都可追溯。 +- Git 托管是最终硬条件;远端写入路径未确认时不阻塞本地开发,但不能通过最终部署验收。 +- 不修改其他项目、共享服务、Ingress、DNS 或 `strategy-lab`。 +- 不调用或记录任何凭证;所有需要认证的步骤使用实施环境已经提供的 secret 机制。 + +### 阶段 1 认证约定 + +- 使用无 Cookie 的 `Authorization: Bearer` token,token 来自 `FUND_LAB_AUTH_TOKEN`,缺失或不匹配的业务请求返回 `401`。 +- `/health` 匿名可读,但不暴露数据库连接串、token 或代理身份。 +- 首版不使用 cookie,因此不引入 CSRF;若以后使用 cookie,必须先加入 SameSite、Origin/Referer 和 anti-forgery 校验。 +- 只信任 `FUND_LAB_TRUSTED_PROXY_CIDRS` 中明确配置的代理网络;默认不信任转发身份,不接受任意 `X-Forwarded-*`。 +- 订单/交易写入必须在同一 DB 事务内完成;同一幂等键同一请求哈希返回原结果,不同请求哈希返回 `409 IDEMPOTENCY_CONFLICT`。 + +## 2. 阶段 0:仓库与工具前置 + +### 目标 + +让本地工作树具备独立 `fund-lab` 源码仓库的边界,同时完成远端写入机制的事实确认。 + +### 工作项 + +1. 在服务配置或明确运维入口中只读确认 Git 服务的写入协议、仓库创建步骤、bare repository 实际写入路径、权限和 cgit 刷新方式。 +2. 根据已有顶层命名证据准备独立仓库名 `fund-lab`;不得复用 `strategy-lab` 或其他仓库。 +3. 在本地初始化 `main`(若服务端确认的默认分支不同,以服务端证据为准)。 +4. 创建 `.gitignore`,加入数据库、行情缓存、凭证、环境文件和构建产物规则;保留 `.env.example`,不得保留真实值。 +5. 只在远端写入机制确认后配置 `origin`。远端 URL 由确认结果注入,不从 cgit 页面猜测。 + +### 证明命令 + +```sh +git status --short --branch +git check-ignore -v .env data/example cache/example bin/example +git ls-files +``` + +### 单一验收条件 + +本地仓库是独立 `fund-lab` 工作树,初始 staged 内容只包含源码/文档/模板;服务端写入路径和准确 remote URL 已有非秘密证据。若写入路径仍未知,阶段 0 的远端子项标记为 `BLOCKED`,但后续本地阶段可继续。 + +## 3. 阶段 1:可运行骨架(本轮立即实施) + +### 目标 + +建立 F# 全栈骨架、健康检查、实际认证边界、中文空状态页和最小测试,不接入真实交易或数据库业务。 + +### 工作项 + +- 创建 `FundLab.Domain`、`FundLab.Api`、`FundLab.Worker`、`FundLab.Web` 和测试项目。 +- 建立 Giraffe API、Fable/Feliz/Elmish 页面和统一错误响应。 +- 建立 `/health`,只报告应用和依赖状态,不回显连接串。 +- 建立单用户授权中间件,并在不满足认证时返回明确的未授权状态。 +- 建立配置读取约定:环境变量/secret 注入,仓库只有 `.env.example`。 +- 建立 F# Domain 的最小类型边界,显式区分净资产、单位净值、空基金和订单状态,暂不实现持久化。 +- 建立前端构建入口和中文空状态页,显示“尚未创建基金/尚未选择投资”,不预选基金、不自动配置资金。 +- 添加 API 认证拒绝测试、health 测试、Domain 单元测试和前端构建脚本。 + +### 验收 + +```sh +dotnet test +curl -fsS "$APP_BASE_URL/health" +``` + +浏览器能看到中文空状态页面;未认证请求不能读取组合数据;没有真实券商或下单依赖。 + +本轮阶段 1 文件预计包括:`src/FundLab.Domain`、`src/FundLab.Api`、`src/FundLab.Worker`、`src/FundLab.Web`、`tests/FundLab.Domain.Tests`、`tests/FundLab.Api.Tests`、`.gitignore` 和不含秘密的 `.env.example`。运行时数据库、缓存、凭证和构建输出不在工作树提交范围内。 + +## 4. 阶段 2:交易账本与合成数据隔离 + +### 目标 + +先用明确标记的合成数据验证记账不变量,避免在真实数据未通前构建错误估值。 + +### 工作项 + +- 实现入金、出金、买入、卖出、分红、费用和冲正的领域类型。 +- 为每个写命令要求 `idempotency_key`。 +- 实现现金、份额和交易明细的纯函数对账。 +- 所有合成交易写入 `is_synthetic=true`,默认查询与真实交易隔离。 +- 为重复请求、负数/精度、跨组合交易和冲正添加测试。 + +### 验收 + +```sh +dotnet test tests/FundLab.Domain.Tests +``` + +同一幂等键重复提交只产生一条交易;入金不会增加收益;买卖后现金和份额守恒;合成数据在页面有显式标识且不会混入真实收益图。 + +### 4.1 阶段 2a 当前进度 + +状态:纯 F# 内存账本切片已实现并通过当前 Domain 测试;阶段 2 整体尚未完成。 + +已完成: + +- `LedgerState`、多基金隔离、现金/持仓/冻结项/应收应付款和净资产恒等式。 +- 外部入金、外部赎回确认、外部赎回付款及同幂等键重放/冲突。 +- 底层申购冻结/确认/取消,以及底层赎回冻结/确认/到账。 +- 固定现金 2 位、单位/净值 8 位、舍入残差和残差归属状态。 +- synthetic provenance、零份额状态、typed domain errors 和基础 TWR 计算。 +- 当前验证命令: + + ```sh + dotnet test tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj + ``` + + 当前切片测试数为 19 个;全量解决方案测试和 .NET 构建已通过;前端 npm 构建和运行时检查仍需执行。 + +待完成: + +- `submitted -> frozen -> pending_nav -> confirmed -> settled` 的完整订单入口和非法转换矩阵。 +- 分红现金/再投资、费用、冲正/更正和更完整的现金流时间点模型。 +- 领域不变量/property tests,以及未来 PostgreSQL 事务锁和幂等约束测试。 +- API/数据库持久化、真实基金数据、默认合成数据隔离查询和页面 provenance 展示。 + +## 5. 阶段 3:PostgreSQL 和迁移 + +### 目标 + +将账本、来源审计和运行状态持久化,同时保持运行数据在仓库外。 + +### 工作项 + +- 创建 `portfolio`、`instrument`、`portfolio_transaction`、`collector_run`、`source_payload`、`data_quality_event` 和估值相关表。 +- 增加 `fund`、`fund_unit_ledger`、`fund_cash_flow`、`order`、`cash_reservation`、`fund_target_weight` 和 `investment_plan`;单位净值、净资产和总资产使用不同字段。 +- 所有金额、份额、价格和净值使用 PostgreSQL `numeric`。 +- 添加唯一约束、外键、时间索引、幂等键/请求哈希索引和来源 payload hash 索引;published_at nullable,first_seen_at 单列。 +- 迁移文件入库;数据库实例、数据目录和备份目录放到部署外部卷。 +- 测试空库迁移、重复迁移、失败回滚和从备份恢复。 + +### 验收 + +```sh +dotnet test +docker compose config +``` + +空数据库可重复迁移;迁移失败不会留下半套业务结构;`git ls-files` 不包含数据库文件、缓存或导出的数据。 + +## 6. 阶段 4:AKShare 真实基金数据探针 + +### 目标 + +在扩大产品范围前确认真实基金数据的字段、日期、缺失、修订和限流行为。 + +### 工作项 + +- 选择少量代表性基金代码,覆盖有净值、周末/节假日、缺失区间和历史修订场景。 +- Python adapter 输出 schema-versioned JSON,不直接写业务估值表。 +- Worker 记录请求范围、响应 hash、来源版本、耗时、失败原因和重试次数。 +- 对 `nav_date`、`published_at`、净值精度、重复记录和异常跳变做校验。 +- 记录哪些基金字段能支持 FOF 展示,哪些只能作为未知状态。 + +### 验收 + +一次成功采集可从原始来源索引重建规范化净值;一次失败采集保留失败记录并可重试;缺失日期在 API 和页面显示为缺失/未知,不被填成当天。 + +## 7. 阶段 5:真实基金估值和 FOF 垂直切片 + +### 目标 + +用真实基金数据完成端到端闭环。 + +### 工作项 + +- 实现基于 cutoff 的有效净值选择。 +- 实现现金加基金份额估值、用户自建基金单位发行/赎回、期间收益、收益率和数据日期展示。 +- 实现 submitted/frozen/pending_nav/confirmed/cancelled/failed/settled 状态机;未确认订单不得出现在已成交持仓。 +- 实现 available_cash、frozen_cash、redemption_receivable、in_transit_assets/liabilities 的恒等式和确认/到账分离。 +- 实现估值快照,保存使用的来源 revision 集合。 +- 对净值冲突、过期、未发布和交易日非营业日返回明确状态。 +- look-through 延后,不作为本阶段核心;没有来源时禁止推断。 +- API 返回金额、价格和净值字符串,前端按声明口径显示。 + +### 验收 + +用固定交易账本和固定来源快照重算,结果可重复;把入金日期向前移动不能改变其“盈利”分类;请求未来日期不会读取未来发布的净值;页面显示基金净值日期、来源状态和是否含合成交易。 + +首版品种白名单只接受已经验证规则的人民币普通开放式基金;QDII、货币基金等没有各自规则前必须在搜索和订单 API 中拒绝。 + +## 8. 阶段 6:用户基金、基金搜索和前端仪表盘 + +### 目标 + +让用户可以创建多支空基金并明确选择基金代码、持仓金额和目标权重,同时不替用户做投资决定。 + +### 工作项 + +- `POST /api/funds` 接受名称、人民币初始资金和首次单位净值;无选择时只保留现金,不生成持仓。 +- `GET /api/funds` 返回每支基金的净资产、单位数、单位净值状态和空/零份额状态。 +- `GET /api/instruments/search` 支持代码/名称搜索,只返回规则验证通过的基金。 +- `PUT /api/funds/{id}/target-weights` 保存用户选择的基金与权重,并校验总权重。 +- `POST /api/funds/{id}/orders` 接受基金代码及持仓金额或份额,生成订单意图,不能直接写成交持仓。 +- 空状态、搜索结果、持仓金额输入、目标权重和待净值状态全部有中文页面。 + +### 验收 + +创建两支基金后互不混淆;不选择任何基金时持仓为空;搜索必须先选择代码;输入持仓金额只创建待处理意图/订单;未确认订单不增加已成交持仓;资产曲线和单位净值曲线标签不同。 + +## 9. 阶段 7:定投与目标权重再平衡 + +### 9.1 定投 + +建立 `investment_plan(kind=scheduled_contribution)`,包含用户选择的基金、金额、频率、执行日、起止时间、失败重试和暂停状态。到期只生成待确认订单;没有可用净值、交易日或资金时显示跳过原因,不自动替换基金。每次执行使用唯一的 plan/run key,重复 Worker 不得重复冻结资金。 + +验收:模拟月度/周度计划、节假日、缺失净值、余额不足、暂停和重试;每次运行可追踪,未确认不变成持仓。 + +### 9.2 目标权重再平衡 + +建立用户明确选择的目标权重、容差和执行窗口。系统计算当前权重偏离并展示 proposed orders;只有用户确认后才进入订单状态机。禁止自动选新基金、自动改变目标权重或忽略冻结/在途资金。 + +验收:权重和校验、现金不足、待净值订单、舍入残差、零份额和确认后权重回算;同一再平衡 run key 幂等;取消不会留下冻结资产。 + +## 10. 阶段 8:股票和债券完整里程碑 + +### 10.1 股票 + +1. 探针:代码映射、交易日、实际/复权价格、公司行动、停牌和缺失修订。 +2. 规则:交易截止、结算、费用、最小交易单位、在途资产/负债。 +3. 账本:价格类型、公司行动、分红现金/再投资和订单状态测试。 +4. 估值:point-in-time 重算、复权分析分离、无前视验收。 +5. UI/API:搜索、选择、持仓金额/份额输入、不可用状态和审计信息。 +6. 上线:数据、规则、领域、API、浏览器、恢复和部署验收全部通过。 + +### 10.2 债券 + +1. 探针:人民币债券代码、收盘价/估值价、应计利息、到期日、付息日和数据发布时间。 +2. 规则:交易日历、报价/成交口径、最小单位、费用、结算和利息现金流。 +3. 账本:净价/全价、应计利息、付息、到期和再投资事件可审计。 +4. 估值:实际持仓价值与收益分析分离,历史回填标记限制,严格模拟只用 point-in-time 来源。 +5. UI/API:搜索、选择、金额输入、利息/估值日期和数据质量状态。 +6. 上线:同股票里程碑,且必须完成债券专属规则审阅;不能复用普通基金或通用 T+1。 + +look-through 在股票/债券和基金数据稳定后另立里程碑,不阻塞首版基金单位和订单闭环。 + +## 11. 阶段 11:前端仪表盘 + +### 目标 + +提供足以审计而不是只展示漂亮数字的中文界面。 + +### 工作项 + +- 总资产、现金、基金持仓、收益和数据更新时间卡片。 +- 净值/资产曲线、收益曲线、现金流和资产配置图表。 +- 交易录入和幂等错误提示。 +- 缺失、过期、冲突、部分数据和采集中状态。 +- 移动端布局和键盘/屏幕阅读器可用的状态标签。 + +### 验收 + +浏览器测试覆盖:未认证、空组合、一次入金、一次基金买入、缺失净值和采集失败;所有页面状态与 API 状态一致,没有把 `0` 当作缺失数据。 + +## 12. 阶段 12:部署准备与安全复核 + +### 工作项 + +- 构建 API、Worker、Web 和 Python adapter 的可重复构建流程。 +- PostgreSQL 使用仓库外部数据卷;行情缓存和原始响应使用仓库外部卷或数据库。 +- 当前阶段不改共享 ingress 配置;获得明确授权并确认精确变更后,增加 `fund.somhairle.bid` 到本项目服务的独立路由,并验收 DNS/TLS/认证/health/回滚。 +- 配置 secret 注入、最小权限、备份和恢复演练。 +- 扫描 staged 文件和 Git 历史中的凭证模式,检查构建产物没有被跟踪。 +- 通过健康检查、迁移检查和浏览器冒烟测试。 + +### 验收 + +删除并重建构建目录不会影响数据库;重启 Worker 不会重复记账;恢复数据库后来源索引和估值快照可用;日志不含凭证;公网只能到达认证后的业务接口。 + +## 13. 阶段 13:Git 提交、推送和最终远端回读 + +这是“源码已托管”的唯一判定流程,不能用本地仓库替代。 + +### 工作项 + +1. 提交前检查 staged 文件、忽略规则、Git 历史和构建输出。 +2. 使用服务配置确认的准确 remote URL 和准确分支创建/更新独立远端 `fund-lab` 仓库。 +3. 完成实施提交并推送;不推送数据库、行情缓存、凭证、环境文件或构建产物。 +4. 记录本地分支和 `HEAD` 哈希。 +5. 使用远端 Git 协议读取同一分支,比较远端哈希和本地哈希。 +6. 使用确认过的 cgit 浏览页读取该提交,比较页面中的分支、提交哈希和仓库名。 + +### 验收命令模板 + +以下变量必须来自已确认的服务信息,不能自行填写猜测值: + +```sh +CONFIRMED_REMOTE_URL='operator-confirmed-value' +CONFIRMED_BRANCH='operator-confirmed-value' +CONFIRMED_CGIT_REPO_URL='operator-confirmed-value' + +LOCAL_SHA=$(git rev-parse HEAD) +REMOTE_SHA=$(git ls-remote "$CONFIRMED_REMOTE_URL" "refs/heads/$CONFIRMED_BRANCH" | cut -f1) +test -n "$REMOTE_SHA" +test "$LOCAL_SHA" = "$REMOTE_SHA" +git status --short +curl -fsS "$CONFIRMED_CGIT_REPO_URL/commit/?id=$LOCAL_SHA" +``` + +命令输出需保存为审阅证据;最后一个页面响应必须人工或脚本确认包含准确仓库、分支和 `$LOCAL_SHA`。若远端协议、路径、分支或页面不能被准确确认,结果为 `BLOCKED`,不能声称托管成功。 + +## 14. 交付顺序和提交边界 + +建议每个阶段形成可回滚提交,提交信息说明行为变化,不把数据快照混入提交: + +| 顺序 | 提交内容 | 证明 | +| --- | --- | --- | +| 1 | 骨架、忽略规则、配置模板、文档 | 测试可启动,敏感文件未跟踪 | +| 2 | 领域账本和测试 | 幂等与现金/份额不变量通过 | +| 3 | 数据库迁移和审计表 | 空库迁移与回滚通过 | +| 4 | AKShare 探针和 Worker | 成功/失败/重试/修订可追踪 | +| 5 | 基金估值和 FOF 垂直切片 | 固定快照重算一致,禁止前视 | +| 6 | UI、部署模板和安全检查 | 浏览器、健康检查和恢复演练通过 | +| 7 | 最终 Git 推送与远端回读证据 | 三方哈希一致,cgit 页面可读 | + +## 15. 需 Hermes 审阅的取舍 + +1. Python AKShare 采用由 F# Worker 调用的薄 CLI,而不是独立长期服务:部署更小、领域逻辑集中,但需要明确 Python 运行时和进程失败重试边界。 +2. API 对金额等精确数值返回 JSON 字符串,而不是 JSON number:避免前端精度损失,但前端需要统一解析和格式化。 +3. FOF look-through 只接受有日期、有来源的持仓数据:牺牲覆盖率,换取不编造底层资产。 +4. cgit 写入路径未确认不阻塞本地开发,但作为部署前置和最终验收硬门槛:可以先完成代码和测试,不能跳过远端创建、推送和回读。 +5. 初始默认分支倾向使用已观察到的 `main`,但最终以服务端确认结果为准,不在方案中猜测 remote URL 或仓库创建 API。 |
