摘要
策略账本需要回答三件事:资金经历了什么、当前净值如何形成、退出金额怎样到账。本文为 LUMENOVA 提出一套事件驱动的账本设计,把成交、费用、估值与结算拆成可独立核对的记录,再通过同一组分录生成账户视图。重点在于保持资金、份额和损益之间的计算关系,让一次余额变化能够追溯到具体业务事件。
01 / 一笔成交,为什么会出现多个数字
订单金额代表交易意图,成交金额代表实际交换,账户权益还包含未卖出资产的估值。把这三者混在一张余额表中,容易把买入支出当作亏损,把提现当作收益下降,也难以解释费用何时扣除。研究设计应从业务事实出发:订单不直接改变持仓;每笔成交改变现金和资产数量;价格更新改变估值;资金进出改变资本与份额。
因此,账本需要同时保留“发生时间”和“记账时间”。前者确定业务归属,后者说明系统何时获知;一个账户内再用连续序号固定处理顺序。展示层可以即时刷新,但每个账户快照都应带上已处理的事件序号和估值时点,让两份报表具有可比较的边界。
02 / 事件保存事实,分录形成账户
Martin Fowler 的 Event Sourcing 讨论了以事件序列保存状态变化,并据此重建历史状态。本文借用这一思路,将原始成交凭据、业务事件和记账分录分层保存:凭据说明来源,事件说明含义,分录描述各科目的增减。账户余额则是分录的计算结果。
事件公共字段包括账户、策略、来源、来源事件编号、发生时间、记账时间与规则版本。一个订单产生多次成交时,必须按成交编号分别入账。数量与金额使用固定精度,保留资产单位;不能把不同币种的原始余额直接相加。
| 事件 | 关键字段 | 影响 |
|---|---|---|
| 资金流入 | 资产、实收金额、来源凭据 | 增加现金与投入资本 |
| 份额变动 | 份额数量、确认净值、关联资金事件 | 记录申购或赎回权益 |
| 成交 | 方向、数量、价格、成交编号 | 改变现金、持仓及已实现损益 |
| 交易费用 | 费用资产、金额、关联成交 | 单独归集成本,避免重复计入 |
| 估值 | 价格、来源、估值时点 | 更新未实现损益,不改变数量 |
| 结算 | 赎回份额、定价时点、应付金额 | 形成应付款,关联后续支付凭据 |
本设计把交易费即时费用化,持仓成本只记录成交本金。现金、持仓成本、损益和费用由配套分录连接;每个资产单位分别检查借贷平衡。估值差额进入独立视图,保留价格依据,不回写原始成交价格。
每个结算周期还应生成一份轻量快照,列出现金、资产数量、选用价格、累计费用、应付款和有效份额,并记录覆盖到的事件序号。核对人员据此使用同一批事实复算,避免用稍后的市场价格去解释较早的净值。跨币种费用需同时保留原币金额和换算价格;精度舍入产生的尾差单列,结算金额与内部高精度计算各有明确位置。
03 / 同一事实只记一次,纠正留下路径
成交推送和历史补拉可能返回同一条记录。幂等键采用“来源+账户+来源事件编号”,由数据库唯一约束确定是否已处理;写入事件、追加分录和更新账户视图应处于同一事务。重复接收返回已有结果,不能再次增加持仓。
post(event):
key = (event.source, event.account, event.source_id)
with transaction():
stored = append_event_if_absent(key, event)
if not stored.is_new:
return stored.result
entries = derive_entries(event, event.rule_version)
assert balanced_by_asset(entries)
append_entries(entries)
update_account_view(entries, event.sequence)
return recorded_result(key)
若费用从 0.40 更正为 0.45,不覆盖原费用记录。追加一条冲回 0.40 的纠正分录,再记入 0.45,两条都关联原事件及更正原因;账户净影响是再减少 0.05。重建历史时只重算账本和视图,不重新下单或付款。规则版本使同一事件始终按当时确认的口径解释。
04 / 从 1,000 USDT 到可核对的净值
下面是一组教学算例:初始投入 1,000 USDT,按单位净值 1 生成 1,000 份;以每枚 100 USDT 买入 4 枚示例资产,随后分别卖出 2 枚。所有成交按金额收取 0.1% 交易费,费用均以 USDT 支付。本例只核算交易过程,产品收益提取与提现收费另按产品规则处理。
| 事件 | 现金 | 持仓估值 | 账户净资产 | 单位净值 |
|---|---|---|---|---|
| 初始入金 | 1,000 | 0 | 1,000 | 1.000000 |
| 买入 4 × 100,费用 0.400 | 599.600 | 4 × 100 = 400 | 999.600 | 0.999600 |
| 估值价格升至 110 | 599.600 | 4 × 110 = 440 | 1,039.600 | 1.039600 |
| 卖出 2 × 110,费用 0.220 | 819.380 | 2 × 110 = 220 | 1,039.380 | 1.039380 |
| 卖出 2 × 108,费用 0.216 | 1,035.164 | 0 | 1,035.164 | 1.035164 |
第一次卖出后,已实现毛利为 20,剩余持仓浮盈为 20,累计交易费为 0.620,因此净收益为 39.380。第二次卖出后,累计毛利为 36,累计交易费为 0.836,净收益为 35.164。现金也可独立复算:1,000 − 400 − 0.400 + 220 − 0.220 + 216 − 0.216 = 1,035.164。
这一口径下,账户净资产等于现金加持仓估值减应付款;单位净值等于账户净资产除以有效份额。交易损益不改变份额数,申购和赎回才改变份额,因此资金进出不会被误读成策略表现。
05 / 结算与到账分步核对
若在全部卖出后赎回 500 份,并以 1.035164 定价,教学模型中的应付金额为 517.582 USDT。请求阶段先锁定份额;确认结算后注销这 500 份并建立应付款,直到支付确认才同时减少现金与应付款。
| 状态 | 现金资产 | 应付款 | 有效份额 | 净资产 |
|---|---|---|---|---|
| 请求锁定 | 1,035.164 | 0 | 1,000 | 1,035.164 |
| 结算确认 | 1,035.164 | 517.582 | 500 | 517.582 |
| 到账确认 | 517.582 | 0 | 500 | 517.582 |
结算确认后,现金资产仍含待支付资金,其中 517.582 被划为支付专用,剩余可用现金为 517.582。到账后,剩余 500 份的单位净值仍为 1.035164;已付 517.582 加剩余净资产 517.582,等于结算前的 1,035.164,资金关系闭合。
对于链上支付,Ethereum 官方交易回执文档提供执行状态、区块位置及日志等字段,待处理交易尚无回执。核对时,应将退出请求关联至支付记录,再检查执行状态、目标资产、收款地址和实际转移数量;一次执行成功本身不能替代金额核对。满足该链的确认规则后,再形成到账事件。
06 / 把核对边界做清楚
核对流程可以按成交、账户、支付三个层次展开:先比较来源成交与费用记录,再重算现金、持仓和净值,最后逐项匹配应付款与到账凭据。差异应定位到事件和科目,避免直接修改最终余额来消除表面偏差。
事件账本增加了存储和规则维护成本,却能减少结算解释中的歧义。这里建议只对资金、成交、费用和份额等关键事实采用事件记录;日常查询使用增量视图与周期快照。历史重建保留原估值价格,新的分析口径另建视图。这样可以在查询速度和历史可解释性之间建立明确边界,也为后续多市场接入保留一致的账户语言。
← 返回研究论文