技术博客

股票、基金、债券、期权的最小必要知识:只讲系统建模需要的部分

第 2 / 141 篇 · 2026-03-09 · secfin · 证券品种数据建模领域知识

刚入行的开发最容易犯的错误,是把"证券"当成一种东西建一张表,然后在上面叠几百个字段。真实系统里,股票、债券、基金、期权的计量方式、定价逻辑、生命周期完全不同,表的骨架就得按品种分叉。这篇不讲课本上那套定义,只讲建表和写清算逻辑时绕不开的那些差异。

三个建模维度

面对一个陌生品种,先问三个问题,答案基本决定了它在系统里的形态:

  1. 怎么计量:按"股/份",还是按"面值"?这决定持仓字段的语义。
  2. 怎么定价:单一价格,还是"净价 + 应计利息"两段式?这决定成交金额的算法。
  3. 有没有生命周期事件:永久存续,还是有到期日、行权日、赎回日?这决定要不要批量到期处理。

四类品种的建模维度对比计量单位定价方式生命周期股票股(整数)单一价格永久存续,只有分红送转债券面值(100 元)净价+应计利息有到期日,期间付息基金份(可有小数)未知价(按净值)开放式无到期,可申赎期权张(×合约乘数)权利金报价到期行权,每日换月

股票:最简单的品种,藏着除权这个坑

股票在系统里是最干净的一类:持仓按整数股计,成交价就是单价,买入金额 = 价格 × 数量 + 费用。没有到期日,理论上永久存续。

但股票有一个别处没有的事件:除权除息。分红送转之后,持仓数量会变(10 送 3 就是持仓 × 1.3),成本价要跟着摊薄,昨收价也要被交易所修正成除权参考价。这意味着股票的"数量"和"单价"都不是稳定字段,任何把它们当常量缓存的代码都会在公司行动日翻车。这个坑留到公司行动那篇细拆,这里只要记住:股票表必须有股本变更的批量处理能力。

债券:按面值建模,成交金额是两段式

债券和股票最大的区别是计量单位是面值而不是数量。柜台界面显示"10 张",记账记的是面值 1000 元(每张面值 100)。持仓字段里存 face_value 而不是 quantity,两者差一个固定倍率,混用就是经典的清算差错来源。

第二个区别是净价交易、全价交收。债券报价是净价(不含利息的价格),但买方实际付的钱要加上卖方持有期间积累的应计利息:

$$ \text{全价} = \text{净价} + \text{应计利息}, \qquad \text{应计利息} = \text{面值} \times \frac{\text{票面利率}}{365} \times \text{已计息天数} $$

已计息天数是从上一付息日到交收日的实际天数(ACT/365)。为什么这样设计?因为利息是按持有期间归属的:你持有债券 100 天就该拿 100 天的利息,否则在付息日前一天买入的人白赚全年利息。净价报价把利息因素剥离,价格变动才纯粹反映利率变化——这是"为什么这么设计"的标准答案。

系统落点:债券的清算金额计算必须调用计息天数函数,而且这个函数依赖交易日历(起算日、截止日、是否含首尾)。写一个通用的 accrued_interest(security_id, settle_date) 比在每个清算任务里内联算要安全得多。

第三个区别是生命周期。债券有到期日和付息日,到期要兑付本金,期间要派息。所以债券品种表必须有 maturity_date、coupon_rate、interest_frequency,而且系统要有到期批处理——到期日之后持仓要清零、本金要入账,这是股票完全没有的一类任务。

基金:不走撮合的品种

场内基金(ETF、LOF)的交易和股票一样走撮合,这部分没有新东西。真正的差异在场外基金的申购赎回:

  • 未知价原则。你今天 15:00 前申购,按今晚算出的净值成交,下单时你不知道成交价。这意味着委托表里的 price 字段对申购单是无效的,成交确认要等管理人(基金公司)返回。
  • 按金额申购、按份额赎回。申购下单给的是"我要买 1 万元",确认回来的是份额 = 金额 ÷ 净值(扣费后);赎回下单给份额,回来的是金额。方向和股票完全相反,接口字段不能复用。
  • 份额可以有小数。净值四位小数,份额通常两位小数,取整规则和舍入方向必须在品种参数里配死。

场外业务的对手方是基金公司而不是交易所,确认数据走 TA(注册登记)系统文件交互,T 日申购 T+1 确认。所以基金模块本质是一套文件驱动的异步流程,和股票回报的实时推送是两个物种。

期权:维度爆炸的品种

期权的复杂不在单个字段,而在合约数量。一只 50ETF 期权,同一时刻挂着的合约 = 4 个到期月 × 9~13 个行权价 × 认购认沽两个方向,上百个合约。行情表、持仓表、风控指标都要按这个量级设计容量。

期权建模的关键字段:

  • contract_multiplier(合约乘数,50ETF 期权是 10000):1 张期权对应 10000 份标的。权利金报价 0.0500 元,买一张实际付 $0.05 \times 10000 = 500$ 元。所有金额计算都要过乘数。
  • strike_price + call_put + expire_month:合约的唯一标识是这四元组加标的,不是一个证券代码了事。
  • 买卖双方的不对称:买方付权利金、无保证金;卖方收权利金、冻结保证金。同一张合约,两个方向的账户处理完全不同——这是期权柜台复杂度的根源,组合保证金那篇专门讲。
  • 每日换月与到期行权:到期日(每月第四个周三)当天要处理行权指派,到期合约摘牌、新合约加挂。期权系统每天开盘前的合约初始化任务比股票重得多。

落到表结构:公共骨架 + 品种扩展

实战里的做法是一张 security 主表放公共字段(代码、名称、市场、品种类型、状态),再按品种拆扩展表 security_bond、security_option、security_fund,用 security_id 一对一关联。所有公共逻辑(行情、撮合、清算主流程)只读主表;品种特有的计算(应计利息、合约乘数、净值确认)通过品种类型分发到各自的处理器。

为什么不搞一张大宽表?因为 NULL 字段会爆炸,而且"这个字段对哪个品种有效"会变成口口相传的隐性知识,新人改清算代码时漏判一个品种就是生产事故。扩展表把品种差异显式化,加新品种(比如 REITs、存托凭证)时是加表加处理器,而不是在主流程里插 if。

这个"公共骨架 + 品种扩展"的结构,后面账户、清算、估值各篇都会反复用到——证券系统建模的一半功力,就在对品种差异的克制表达上。