基金基线切换实战:DCA 引擎的"新旧共存"设计
一、背景:DCA 模拟 → 实际持仓
venxine.vip 的基金数据管线最初基于 DCA 模拟:从某个起始日期(如 2026-06-06)开始,按定投计划逐日累加份额,用每日净值计算收益。
7 月 1 日,用户完成了实际建仓,提供了真实持仓数据。需求很明确:
7 月 1 日的历史数据不能改。从 7 月 2 日开始,用实际持仓作为新基线计算收益。
这意味着基金计算引擎需要同时维护两份基线:
0606 基线(DCA 模拟) → 7/1 及之前
0701 基线(实际持仓) → 7/2 及之后
二、DCA 引擎的基线选择逻辑
核心函数是 _demo2_calc_fund_pl(),它接收一个日期,选择对应的基线文件,然后计算该日的持仓和收益。
基线选择逻辑概览:
# app.py — _demo2_calc_fund_pl() 内部
wn = date.isocalendar()[1]
if month == 7 and day >= 2:
baseline_file = 'baseline-0701.json' # ← 新基线(实际持仓)
elif month > 7:
baseline_file = 'baseline-0701.json' # ← 7 月以后也用新基线
elif wn >= 24:
baseline_file = 'baseline-0606.json' # ← 旧基线(DCA 模拟)
else:
baseline_file = 'baseline-0530.json' # ← 更早的基线
关键设计点:if month == 7 and day >= 2 而不是 if month >= 7。
如果写成 if month >= 7,7 月 1 日也会用新基线,但 7/1 的历史数据应该保持不变。所以必须是 == 7 and >= 2,精确到天。
先说清楚这段逻辑的局限:if month == 7 and day >= 2 / elif month > 7 / elif wn >= 24 把月、日、ISO 周号混在一起判断,是为这一次迁移硬写的,跨到明年就会错乱(明年 6 月的周号又会 >= 24)。它现在能用,是因为这个项目的基线切换是低频事件,一年切一两回,写死反而直观。
真要频繁切换,更稳的做法是把"生效日期 → 基线文件"做成一张有序表,按查询日期往前找最近的一条:
from datetime import date
BASELINES = [
("2026-05-30", "baseline-0530.json"),
("2026-06-06", "baseline-0606.json"),
("2026-07-02", "baseline-0701.json"), # 7/2 起生效,7/1 仍走 0606
]
def pick_baseline(d): # d 是 date 对象
chosen = BASELINES[0][1]
for eff, f in BASELINES:
if d >= date.fromisoformat(eff):
chosen = f
return chosen
这样加一次基线只是往表里追一行,不用再碰 if/elif,也不会明年就失效。本次没上这套,纯粹是就切这一回、杀鸡不必用牛刀,但值得记一笔。
三、基线文件的结构
baseline-0701.json 包含每只基金的实际持仓:
{
"008086": {
"name": "华夏5G",
"shares": 8538.00,
"cost_basis": 3.7709,
"total_cost": 32195.94
},
"017853": {
"name": "云计算",
"shares": 19519.07,
"cost_basis": 1.6547,
"total_cost": 32298.21
}
}
DCA 引擎读取基线后,从基线日期开始逐日累加定投份额,计算每日收益:
当日收益 = (基线份额 + 累计定投份额) × (当日净值 - 前日净值)
这个公式确保: - 7/1:用 0606 基线 + 累积到 7/1 的定投 → 历史数据不变 - 7/2:用 0701 基线 + 7/2 当天的定投 → 起点改为实际持仓
四、数据验证:新旧两套数据的一致性
迁移完成后必须验证,两套基线在同一天(7/1)的数据必须完全一致:
验证项:
├── 份额对齐? → baseline-0606 累积至 7/1 的份额 = 用户实际建仓份额
├── 总金额对齐? → funddata 页面金额 = 用户 MD 文件中的金额
├── funddata 不变? → 7/1 页面刷新后数据完全不变
└── fundreturns 不变? → /api/fund-pl 返回 7/1 数据不变
验证结果:
| 基金 | 基线累积份额 | 用户实际份额 | 偏差 |
|---|---|---|---|
| 008086·5G | 8,538.00 | 8,538.00 | ✅ 0 |
| 017853·云 | 19,519.07 | 19,519.07 | ✅ 0 |
| 016370·信澳5G | 2,455.32 | 2,455.32 | ✅ 0 |
| ... | ... | ... | ✅ 全部一致 |
全部 7 只 A 股基金的 DCA 累积份额与用户实际持仓完全一致,说明 DCA 模拟的参数(起始日期、定投金额)精准还原了真实交易。
五、四条输出路径的一致性
基线切换影响 4 条数据输出路径,全部需要验证:
daily-pl.json → _demo2_calc_fund_pl() → ┌→ funddata 月视图
├→ funddata 周视图
├→ funddata 日视图
└→ /api/fund-pl → fundreturns 图表
修改后逐页验证:
| 路径 | 7/1 数据 | 7/2 数据 | 状态 |
|---|---|---|---|
| funddata/月视图 | 不变(旧基线) | 新基线计算 | ✅ |
| funddata/周视图 | 不变 | 新基线计算 | ✅ |
| funddata/日视图 | 不变 | 新基线计算 | ✅ |
| fundreturns 图表 | /api/fund-pl 不变 | 新基线计算 | ✅ |
六、设计原则总结
原则 1:历史数据不可变
任何基线切换都不能修改已展示过的历史数据。用户看到的 7/1 数字,在切换前后必须完全一致。
原则 2:条件判断精确到天
# ❌ 粗暴:7/1 也会被改
if month >= 7:
baseline = '0701'
# ✅ 精确:7/1 不动,7/2+ 切新
if month == 7 and day >= 2:
baseline = '0701'
原则 3:统一计算引擎,多路径输出
所有视图(月/周/日/图表)共享同一个 _demo2_calc_fund_pl() 引擎。不允许多个地方各自实现 DCA 逻辑,那是数据不一致的温床。
原则 4:验证先于部署
改完基线逻辑后,先跑验证脚本确认份额对齐、金额对齐、历史数据不变,再重启服务。不要"部署了再看对不对"。