summaryrefslogtreecommitdiff
path: root/docs/implementation-plan.md
blob: 9bd3c4f46f4bafc12c4c597e8fcc95aa79a0a2a1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
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。