清结算与对账是交易链路的下游,也是最容易被忽视但最不能出错的一环。策略赚了多少、持仓有多少、可用资金还剩多少——这些数字必须和券商、交易所完全一致。一个持仓对不上的 bug,可能意味着你的策略在「看不见的持仓」上承担着风险。
真正难的地方在于多方数据的对齐。你的系统、券商系统、交易所系统是三个独立的事实来源,它们的时间粒度、字段定义、更新时机都不同。对账的本质是:在承认差异的前提下,快速定位差异的根因。
本文按「流程 → 时序 → 盯市 → 核算 → 对账 → 差错 → 实现 → 一致性 → 报送」的顺序展开。风控层面的实时限额在 交易风控与实时限额 中讨论,本文聚焦成交之后的账务处理。
目录
- 清结算的流程与参与者
- 交易日、结算日与资金可用性
- 逐日盯市与保证金
- 持仓与资金核算
- 三方对账体系
- 差错处理与调账
- 对账的技术实现
- 数据一致性与幂等
- 报表与监管报送
1. 清结算的流程与参与者
一笔交易从成交到最终交收,要经过多个环节:
成交(Trade)
│
├─► 清算(Clearing):计算应收应付、净额、保证金
│
├─► 交收(Settlement):资金与证券的实际划转
│
└─► 存管(Custody):证券登记与托管
参与者与职责:
| 角色 | 职责 | 数据来源 |
|---|---|---|
| 交易所 | 撮合、发布成交 | 成交流水 |
| 中国结算 | 证券登记、交收 | 结算数据 |
| 期货公司/券商 | 客户资金与持仓管理 | 柜台数据 |
| 托管银行 | 资金划转 | 银行流水 |
| 你(投资者) | 内部账务 | 自己的系统 |
每一层都有自己的账本,你的系统账本必须与上一层对齐。对齐的方式就是「对账」。
2. 交易日、结算日与资金可用性
时间是清结算里最容易出错的地方,因为存在多个「日期」概念:
| 概念 | 含义 | 例子 |
|---|---|---|
| 自然日 | 日历日 | 10 月 1 日 |
| 交易日 | 可交易的日子 | 夜盘归属前一交易日 |
| 结算日 | 完成清算的日期 | 通常 T 日收盘后 |
| 交收日 | 资金证券划转日 | A 股 T+1 |
| 资金可用日 | 资金可用于交易 | A 股 T+0 |
| 资金可取日 | 资金可提取 | A 股 T+1 |
A 股的资金规则最绕:当日卖出股票的资金当日可用于买入,但次日才能提取。这被称为「可用不可取」。
class CashAccount:
def __init__(self):
self.total = 0.0 # 总资金
self.withdrawable = 0.0 # 可取资金
self.frozen = 0.0 # 冻结(挂单占用)
def sell_stock(self, amount):
self.total += amount # 卖出所得立即可用,但不可取
def settle_end_of_day(self):
self.withdrawable = self.total - self.frozen # 日终:可用转为可取
期货是 T+0 且逐日盯市,当日盈亏当日结算,资金当日可用可取(在保证金充足的前提下)。不同品种的结算规则必须在系统里显式建模,不能用一套逻辑硬套。
3. 逐日盯市与保证金
期货采用逐日盯市(Mark-to-Market),每天按结算价重估持仓盈亏并调整保证金:
当日盈亏 = (当日结算价 − 昨日结算价) × 持仓量 × 合约乘数 × 方向
新保证金 = 当日结算价 × 持仓量 × 合约乘数 × 保证金率
def mark_to_market(position, prev_settle, cur_settle, multiplier=300): # 乘数 300 元/点
direction = 1 if position.side == 'LONG' else -1
pnl = (cur_settle - prev_settle) * position.qty * multiplier * direction
new_margin = cur_settle * position.qty * multiplier * position.margin_rate
return pnl, new_margin
保证金分几档,交易所收基础保证金,期货公司在交易所基础上加收:
| 层级 | 保证金率 | 说明 |
|---|---|---|
| 交易所 | 8% | 基础要求 |
| 期货公司 | 10% | 加收 2% |
| 风控线 | 12% | 触发追保 |
追加保证金(Margin Call) 的触发条件是「可用资金 < 0」:
def check_margin_call(account):
available = account.cash - account.margin_occupied
if available < 0: # 触发追保,需在规定时间内补足
return {'action': 'margin_call', 'shortfall': -available}
return None
强平(强制平仓)是期货公司在你无法补足保证金时的处置手段,通常按「先平亏损大的、后平盈利的」顺序执行。你的风控应该在期货公司强平之前主动减仓,因为强平的时机和价格都不由你控制。
4. 持仓与资金核算
持仓核算要处理多个维度,每个维度都可能出错:
| 维度 | 说明 |
|---|---|
| 总持仓 | 该标的的全部持仓 |
| 可用持仓 | 可卖出的部分(T+1 限制) |
| 冻结持仓 | 挂单卖出占用的部分 |
| 昨仓/今仓 | 期货区分,影响平仓手续费 |
| 多头/空头 | 期货双向持仓 |
class Position:
def __init__(self, symbol):
self.symbol = symbol
self.long_qty = 0 # 多头持仓
self.short_qty = 0 # 空头持仓
self.yd_long = 0 # 昨多(可平)
self.yd_short = 0 # 昨空(可平)
self.frozen_long = 0 # 冻结的多头(挂卖单)
self.frozen_short = 0 # 冻结的空头(挂买单)
self.avg_cost = 0.0 # 持仓均价
@property
def available_long(self):
return self.yd_long - self.frozen_long # 可平多头 = 昨多 − 已冻结
def on_fill(self, side, qty, price):
if side == 'BUY':
self.long_qty += qty
total_cost = self.avg_cost * (self.long_qty - qty) + price * qty # 重算均价
self.avg_cost = total_cost / self.long_qty
else:
self.short_qty += qty
持仓均价的计算方式会影响盈亏统计。两种口径:
移动加权平均:每笔成交都重算均价(常用)
先进先出(FIFO):按买入顺序配对(用于税务)
选择哪种取决于用途:策略归因用移动加权,税务申报用 FIFO。两套口径必须分别维护,不能混用。
5. 三方对账体系
对账是清结算的核心工作。三方对账指:你的系统、券商/期货公司、交易所三份数据互相核对。
| 对账项 | 数据源 A | 数据源 B | 差异容忍 |
|---|---|---|---|
| 成交流水 | 你的回报 | 券商对账单 | 0 |
| 持仓 | 你的持仓表 | 券商持仓 | 0 |
| 资金 | 你的资金表 | 券商资金 | 0 |
| 手续费 | 你的计算 | 券商扣收 | 0 |
| 保证金 | 你的计算 | 期货公司 | 小(四舍五入) |
def reconcile_trades(my_trades, broker_trades):
my_map = {t.trade_id: t for t in my_trades}
broker_map = {t.trade_id: t for t in broker_trades}
diffs = []
for tid in set(my_map) | set(broker_map):
mine, theirs = my_map.get(tid), broker_map.get(tid)
if mine is None:
diffs.append(('missing_local', tid, theirs))
elif theirs is None:
diffs.append(('missing_broker', tid, mine))
elif not same_trade(mine, theirs):
diffs.append(('field_mismatch', tid, mine, theirs))
return diffs
对账的黄金标准是零差异。任何差异都必须被解释——要么是数据延迟(稍后会补齐),要么是真实差错(需要调账)。
6. 差错处理与调账
发现差异后,处理流程要标准化:
1. 定位:差异出现在哪个环节(回报丢失 / 券商错误 / 计算错误)
2. 分类:是延迟差异(自愈)还是真实差异(需调账)
3. 处置:以权威数据源为准修正本地账
4. 记录:留痕,供审计
权威数据源的优先级:
交易所 > 券商/期货公司 > 你的系统
你自己的系统永远是「被修正方」。当你和券商的数据不一致时,以券商为准,然后回头查为什么你的系统算错了。
def adjust_position(local_pos, broker_pos, reason):
diff = broker_pos.qty - local_pos.qty
if diff == 0:
return None
adjustment = { # 生成调账记录,而不是直接改数
'symbol': local_pos.symbol,
'before': local_pos.qty,
'after': broker_pos.qty,
'diff': diff,
'reason': reason,
'ts': time.time(),
}
audit_log.append(adjustment)
local_pos.qty = broker_pos.qty
return adjustment
调账必须留痕,且不能直接改数据库——要生成一条调整记录,让账本可追溯。这与财务系统的「红冲蓝补」是同一个思路。
7. 对账的技术实现
对账系统的实现有三个关键点:
1. 幂等:同一份对账单重复导入不产生重复数据
2. 可重跑:对账任务失败后能重新执行
3. 可追溯:每个差异都有处理记录
对账单的导入用唯一键去重:
-- 对账单表,用 (trade_date, trade_id) 作为唯一键
INSERT INTO broker_trades (trade_date, trade_id, symbol, qty, price)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (trade_date, trade_id) DO NOTHING;
日终对账的任务编排:
16:00 券商发布对账单
16:30 导入对账单(幂等)
17:00 执行对账(流水 / 持仓 / 资金)
17:30 生成差异报告
18:00 差异处理(自动 + 人工)
对账任务的调度要考虑失败重试和依赖顺序。如果对账单还没到就执行对账,会产生大量假差异。相关设计参考 分布式幂等与可靠性 。
对账数据的量级可能很大(每天数万笔成交 × 数年),查询性能与归档策略都需要提前规划,索引设计要围绕「按日期 + 账户」这个主查询模式展开。
8. 数据一致性与幂等
清结算的数据一致性要求是最终一致,但中间状态必须可解释:
| 时刻 | 你的系统 | 券商 | 一致性 |
|---|---|---|---|
| 成交瞬间 | 已记录 | 已记录 | 一致 |
| 回报延迟 | 未收到 | 已记录 | 暂时不一致 |
| 日终 | 已对齐 | 已对齐 | 一致 |
回报丢失是最常见的差异来源。解法是主动查询而不是被动等待:
def sync_orders_from_broker(broker_api, local_orders): # 主动拉取,补齐丢失回报
broker_orders = broker_api.query_orders()
for bo in broker_orders:
lo = local_orders.get(bo.order_id)
if lo is None or lo.filled != bo.filled:
apply_broker_state(lo, bo) # 本地状态落后,以券商为准
幂等性要求每个操作都能安全重放。成交记录用「成交编号」做唯一键,资金变动用「流水号」做唯一键,任何重复写入都被忽略。
def apply_fill(fill, processed_set):
if fill.trade_id in processed_set:
return False # 已处理,幂等跳过
processed_set.add(fill.trade_id)
update_position(fill)
update_cash(fill)
return True
9. 报表与监管报送
清结算的输出是各种报表,它们同时服务于内部管理和监管报送:
| 报表 | 频率 | 用途 |
|---|---|---|
| 成交流水 | 每日 | 内部核对 |
| 持仓明细 | 每日 | 风险监控 |
| 资金流水 | 每日 | 财务核算 |
| 盈亏报表 | 每日/每月 | 业绩归因 |
| 风险指标 | 每日 | 监管报送 |
| 大额交易报告 | 触发式 | 反洗钱 |
盈亏报表的两种口径必须区分:
def daily_pnl(positions, prev_close, cur_close, trades):
realized = sum(t.pnl for t in trades if t.is_closing) # 已实现盈亏
unrealized = sum( # 浮动盈亏:持仓部分的市值变化
(cur_close[p.symbol] - prev_close[p.symbol]) * p.qty * p.multiplier
for p in positions
)
return {'realized': realized, 'unrealized': unrealized,
'total': realized + unrealized}
已实现盈亏与浮动盈亏分开统计是必须的:只有已实现盈亏是「落袋」的,浮动盈亏会随市场波动。业绩展示时混淆两者是常见的误导。
监管报送的数据必须与内部账完全一致,任何不一致都可能在检查时被质疑。报送的口径要与合规部门对齐后再固化进系统,避免临时取数导致的偏差。
权衡取舍
| 维度 | 实时对账 | 日终对账 | 定期对账 |
|---|---|---|---|
| 时效性 | 秒级 | 日级 | 周/月级 |
| 实现复杂度 | 高 | 中 | 低 |
| 覆盖度 | 部分 | 完整 | 完整 |
| 成本 | 高 | 中 | 低 |
实践中的组合是**「实时对持仓 + 日终对全量」**:持仓和资金变化实时与柜台比对(能立即发现异常),成交流水和手续费在日终做全量核对(数据量大、时效要求低)。
调账策略上也有取舍:自动调账效率高但有误调风险,人工调账安全但慢。折中方案是按差异金额分级:小于阈值的自动调账并记录,超过阈值的转人工审核。
常见坑清单
- 混淆可用与可取资金:卖出资金当日可用不可取,误判会导致提款失败。
- 夜盘日期归属错误:夜盘按自然日归属,导致结算日错位。
- 保证金按最新价而非结算价:交易所按结算价盯市,口径不一致。
- 持仓均价用错口径:移动加权与 FIFO 混用,盈亏统计错误。
- 对账不幂等:对账单重复导入产生重复成交记录。
- 以本地数据为准:与券商不一致时应以券商为准,反了会掩盖真实错误。
- 调账不留痕:直接改数据库,审计时无法解释。
- 不主动查询订单:回报丢失后本地状态永久错误。
- 手续费口径不一致:自己算的和券商扣的不一样,逐笔核对才发现。
- 报表口径混用:已实现与浮动盈亏混在一起,业绩展示失真。
小结
清结算的核心是让每一分钱、每一股持仓都能被解释。它不产生收益,但一旦出错,轻则报表失真,重则资金损失或监管处罚。工程上的关键是三点:权威数据源明确(以交易所和券商为准)、处理流程幂等(可重放)、差异处理留痕(可审计)。
判断一套清结算系统是否可靠,最快的检验是差异注入测试:人为制造回报丢失、重复推送、金额偏差等场景,看系统能否正确识别并处理。没有经过差异测试的对账系统,在真实差错面前往往束手无策。
下一步建议阅读 实盘运维与策略监控 ,看这些账务数据如何转化为运维指标;监管报送的口径要求可以延伸阅读 交易合规与监管要求 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。