# 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。