ADR-018 — 自动定投与子弹池现金语义(DCA + dip-reserve)
日期: 2026-06-23 状态: Accepted 关联: 016-ledger-mutation-idempotency · 017-config-via-api
背景
不少用户在第三方平台(基金 App / 银行)做每日自动定投,扣款来自工资 / 银行卡。但 openInvest 的 buy() 一律从 portfolio.cash 扣钱(账本一致:买 X 元 = 扣 X 元现金)。若把日定投 也按这套记账,会把 portfolio cash 错误消耗——而 portfolio cash 的真实语义是"现在能立刻 拿去抄底的子弹(dip-reserve)",不是日定投的资金来源。例如 ¥30k 子弹按 ¥100/天定投扣, 约 10 个月归零,之后委员会逢跌想加仓却显示"没子弹"。
根因:buy() 把"资金来源"写死成"扣 portfolio cash",无法表达"外部新钱进场建仓、子弹池 不动"这一情形。
附带发现的数据 bug:新加 target_asset 时取价/指标只触发 5d 增量刷新 → DB 仅 5 天历史 → RSI/MA120/MA250/regime 全 N/A、REGIME=unknown(510500.SS / SPY 复现)。
决策
buy(source_type=...)资金来源参数:cash_deduct(默认):扣 portfolio cash,现有行为不变;external_funding:外部新钱建仓,portfolio cash 不动。history记funding_source审计字段。- 由此明确 portfolio cash 语义 = 抄底子弹池:只被手动抄底 / 委员会加仓动用,不被日定投消耗。
- 2026-10 复用:onboarding 补录"用系统前就持有的仓位"同样不是现金买入,CLI
buy --existing-position/ MCPrecord_existing_position走external_funding, historysource带:existing_position后缀(与日定投的dca_daily区分)。
DCAConfig+jobs/dca_daily:- 配置
dca.auto_dca_enabled/auto_dca_amount_cny/auto_dca_symbols,dataclass 默认禁用(安全模式); - 经
/api/config(白名单,ADR-017)或INVEST_DCA_*env 开启; - 每日对每个 symbol 用
get_quote取价 →units = amount_cny / price→buy(source_type="external_funding"); - 每个
(date, symbol)一把state_claim幂等闸(ADR-016),买入失败unclaim可重试。 - 休市闸(2026-10 补):行情最新 bar 日期 ≠ 今天(北京日期)→ 不动账本:行情源本次没拉到 →
skip(stale_quote)+ warning,拉到了但没今天的 bar →skip(market_closed);都unclaim。 job 平日北京 15:30 / 18:30 / 21:30 跑三次,bar 晚到 / 行情源抖动由后两次补记,已记过的走already_dca_today。此前国庆 / 中秋 / 周六按节前旧价记了幻影买入,存量差异用scripts/reconcile_dca_phantom.py对账(默认 dry-run)。 - 适用范围:北京时间 15:30 前已收盘的市场(A 股 / 亚太)。15:30 后才收盘的市场(美股、欧股) 不受支持:多数时刻最新 bar 是前一交易日 →
skip(market_closed);但 21:30 档恰在美东开盘、 欧股三档都在盘中 → 也可能按盘中价记账。要支持需按交易所时区 + 收盘时刻比对(未做:行情库只存日期)。 - 记账价:job 运行时库里当天那根 bar 的价。2026-10 起当天这根距上次刷新超过 30 分钟就重拉 5d (刷新时间记在行情库
fetch_log,各进程共用),15:30 档跑时 A 股 / 亚太已收盘,通常就是收盘价; 此前当天首次缓存进库后整天不重拉,常是早盘盘中价。对账脚本补记漏记日用的是收盘价。
- 配置
数据深度自愈:
get_history_data在 DB 历史 <_MIN_HISTORY_ROWS(60 根)时首次拉 2y 全量回填, 保证长周期指标 / regime 算得出;已有足够历史才 5d 增量。
后果 / 约束
- 对账口径:系统看不到第三方平台真实成交,按
amount_cny估算每日买入量记账,月度用真实 基金余额对账校准。智慧定投实际金额浮动,amount_cny是估算基准。 - 币种:CNY 标的直接
amount_cny / price;非 CNY 标的经utils.fx.to_base折算后再除价。 - 代理跟踪:场外联接基金(yfinance 取不到净值)按同指数的场内 ETF symbol 做价值代理记账 (如某沪深 300 联接基金 → openInvest 记同指数的场内 ETF),份额是价值代理、市值/PnL 同指数跟得准。
- 开口池模型(已实现):
wealth_context.monthly_contribution_cny—— 用户每月从外部补充进 投资池的额度。WealthContextOfficer 据此把 portfolio cash 当流量而非封闭快照:低现金更不算 流动性风险、30% 现金目标软化(铁律不变:本次加仓上限仍 = 当前 portfolio cash)。