根据上篇DDD 的思想方法, 首先我们需要分清系统的边界, 相同Domain的放一起。分解业务模块可以遵从软件设计的思想,保持高内聚、 低耦合; 相同的业务在同一个Domain 中处理,按照DDD设计的思想: 一个Domain 内业务应该只有一个Aggregate Root(聚合根) 对外提供服务, 内部可有多个子Aggregate Root,但是必须通过这个主的Aggregate Root 对外提供服务接口, 我们选择使用的CQRS 框架 AxonFramework,作为我们的架构基础, 所以下面的若干章节将结合框架的特征和我们的业务特点分解我们的业务模块。
概述
可参考我们的 《如何构建一个交易系统(三)》 的背景; 一个交易系统大概可分为, front-->middle-->back-end office, 性能要求几乎是呈梯减态势, 前端机直面前段流量的冲击, 所以必须高可用性、高响应,高吞吐量; 中端做进一步的校验,和信息的丰富; 后端系统, 做分析, 报表, 整理,风控,现金流等等。
Book
首先从最核心的 book 系统开始, 用户打的单; 流动性供应商报单, 这些所有单子, 最终汇聚到 book 系统。快速的下单、撤单管理订单是一个book 系统必需满足的条件。 book 系统把收到买单、卖单,按照价格的高低, 排好序, 触发匹配成交。 book domain 需要提供基本的对外服务包含:
- 下单(市价,委托,止损)
- 撤单
Book Domain 需要传递的信息(Event 方式)
- 单子状态(进入book列表,部分成交,成交, 拒绝,取消,取消失败,超时,出错等等)
- 当前 book 买卖价格(最高买, 最低卖),以多档形式(1档,5档)传递出去,报告,或者为pnl 所用等。
表面看来 book 业务非常简单明了, 从代码层面看, 就是两棵树(Tree数据结构), 每次打单往两个树上面添枝加叶, 如果最高的买价,高于最低的卖价格, 那么两棵树都会被削掉顶端,直到平衡。
Booker Aggregate Root 同时还有自身的状态需要维护, 比如临时关闭 booker,还有部分参考和调整参数: 比如报价停顿多长时间, 可以判断 book 有异常,进而停止接收单子; 没有足够流动性如何处理,是失效还是拒绝等, 需要设定price banding? 如果价格出现特殊波动。
要保证book 系统高并发、 高吞吐,一般做法把这些数据结构放置内存中做处理; 但是对于booker 系统有个无法避免的限制, 单个产品的 book 无法分布到两台机器上面; 所以最大限度是一个 book 占用一台机器,后续只能竖向扩展、优化单机的性能,或者 book 算法优化。
全部放置内存如何保障高可用性? 这里需要借助于 EventSource 里面的概念(下面有独立章节讲解), 每个进入book 的命令(下、取消单)都会触发一个book 的状态转变, 这些转换以 Event 方式落地, 而整个Aggregate Root 状态的变更, 又会定期以snapshot方式落地, 这样可以保证即使节点崩溃, 也可以以最快的速度恢复到最后一致状态, 参考log 或者git 工作的方式,每次修改都有相应的 check-in list, 可以按照需要恢复到任何一个历史版本, 这个也是Eventsource 带来的好处, 其实这也是现实世界非常自然的处理方式。
对于 book 的每次增删改查, 需要保证线程安全,具体如何保证, 下面的章节 Disruptor 的运用会详细描述。
需要说明book 外层有层薄的前置机(front)功能, 主要检查账号的可用性, 当然这里不能直接hit 数据库, 基本的信息都会保存在内存中,后续会有详细描述Apache Ignite 如何运用。
Portfolio
Portfolio 其实叫account 系统更通俗易懂点, 但是他又不是传统意义上的account 系统, 他不提供用户角色,权限,登录,名称修改等功能, 所以命名成portfolio 可能更合适, 这里只保存中性的信息:
- 可用现金
- 占用保证金
- 用户持仓
可能大家问,用户浮动盈亏呢? 这个我们单独撇出来, 因为这部分模块也是遵循 CQRS 设计模式, 每个账号对应一个Aggregate Root,对于高频的价格波动,由于每次涉及 Aggregate Root 状态变动, 和关联事件的落地,所以 CQRS 无法实现高性能运算,AxonFramework 试图借助AKKA persistence 功能优化Eventstore 部分性能瓶颈,本来打算参与他们的Bata测试, 但是事情不了了之, 价格的波动不属于domain 东西, 所以没有放在 portfolio domain 中, 有独立的query 端模块负责这部分工作。
一个Portfolio Domain 主要接收的command 有:
- 充钱 deposit
- 取款 withdraw
- 仓位变动
充钱 & 取款
这个涉及 aggregate root 可用现金的支配
仓位的变动
用户建仓, 减仓, 也就是 book 中的成交信息会传递到 Portfolio Domain 中,引起Portfolio 聚合根的状态变更, 这个是 Portfolio 接收最多的状态变化请求。 每个订单的匹配都会涉及两个账号的仓位信息的变更。订单的成交会导致:
- 仓位的增加或者减少
- 仓位平均价格变化
- PNL 的产生
仓位增加
同方向仓位变化, 比如同样做空、做多, 涉及:
- 仓位的累加
- 平均价格调整
- 保证金累加
仓位减少
反方向仓位变化, 比如当前多单, 做了个空单, 或者平仓:
- 仓位累减, 净仓可能变成反向或者继续保持原来方向
- 仓位价格, 保持原仓位方向, 平均价格不变;反方向,反方向价格
- 保证金减少
- PNL 生成
- 可用现金调整
Portfolio 状态的变更,同样以Event 方式传递出去; 以供下游系统使用可包括: Floating PNL 运算, margin call 触发等。
采用Eventsource 的设计方式,集群replication,partition 方式保证单点失败后能够以最快的速度恢复,保证状态的一致性。
Query
每个Aggregate Root Command 一般配合一个Query端使用, 外围状态查询不会直接抵达 command 端, 同时command 端的信息都是尽量无状态、normalization 化; 所以不适合直接供客户查询使用。 从aggregate root 中生成的 event 通过 ESB, 感兴趣的模块可以选择监听接受, 也就是我们的Query 端, query 端,可以进一步对数据进行丰富、demoralization、缓存、落地到查询数据库。
Order
Book 中订单状态信息, Order query, 负责这些数据的丰富, 落地, 供用户查询这些订单信息, 这个模块非常简单, 一般的order 都是朝生暮死, 对于快频的日内交易来说。
Account
这个才是我们真正用户接触最多的地方, 比起我们aggregate root 中的portfolio, query 中的account 信息多了浮动盈亏属性: 浮动盈亏顾名思义是根据现价不停调整, 在高杠杆的交易系统中, 浮动盈亏的运算涉及到margin call、隔夜费, 和其他风险的计算和处理, 所以尤为重要, 对于spot trade 这部分的困难度将不在一个数量级, 远远没有我们这里的交易系统复杂。
查询端的账号体系提供:
- 账户基本信息查询
- 可用资金
- 占用保证金
- 浮动盈亏
- 持仓比
- 持仓
- 持仓方向数量品种
- 浮动盈亏
- 结算
- 每单PNL
- 结算细节
浮动PNL
在开市时间, 用户有持仓, 就需要计算持仓浮动PNL和整个账号的浮动PNL. 如上文所述,在spot trade, 如我们买卖股票(无融资融券),浮动盈亏的计算, 没有特殊的时效需求。 但是对于高杠杆(50/100/200)的交易, 由于涉及margin call,浮动盈亏所以需要近实时的计算。 可以想象如果你有十万用户, 每人十个持仓, 总计100万持仓, 每个产品价格每秒变动3次,同时要在用户在不停调整自己的仓位的情况下聚合每个仓位的浮动盈亏到账号的总浮动盈亏上, 这里的运算和量将会非常的复杂和巨大。
我们采取的是将用户进行partition, 采用多台机分布式运算; 以满足高可扩展和高可用性需求, 浮动盈亏将影响用户进一步加减仓, 和后续的margin call触发, 所以此模块我们借助于 Apache ignite 内存数据网格, 实现在内存中大规模的运算。 ignite 很好的分布式支持, 和简单直观的落地解决方案, 可以很好的支撑我们的业务。 下面将有专门章节讲解Ignite的实践和运用。
MarginCall
MarginCall 一般存在于杠杆交易中(或者融资融券), 相当于你借了别人的钱在投资;如果成功大家皆大欢喜,但是如果投资失利,借贷方为了保证自己利益,会选择撤资。 margin call 在你的保证金,不足以抵消仓位损失的情况下, 将强平你的仓位,资不抵债,破产啦, 参考我前文举例。
浮动盈亏计算的效率, 其实将大大影响margin call 触发的概率,特别对于一些极值的点,没有计算及时将是比较大的风险,对于以此赚取利润这将白白让费好机会,对于以此规避风险可能导致更大的雪崩。
其实margin call 是控制风险的一种方式, 对绝大部分投资者是一种有效的保护, 杠杆交易可能导致你亏损超过本金,这部分亏空需要你来补偿的, 输掉一套裤子,总比输掉一条胳膊好, 输掉一条胳膊又比输掉一条命好!
避免margin call 最好的方法是控制仓位比, 勿贪婪。
这部分借助于ignite, 以reactive方法在浮动PNL 运算完, 达到一定benchmark 后, 触发margin call 处理。 由于现实中不同产品报价总是有先后,而价格又一直在波动, 所以很难界定一个合理的时间点。 应此margin call 触发后需要导致新一次重新计算。
隔夜费
在杠杆(融资融券)交易中, 隔夜费需要支付给平台和做市的人, 这个比较好理解, 100块本钱撬动200块的资产, 借的这100块需要付出利息的,不管你亏了还是赚了, 参考房贷等。
对于隔夜费不同人有不同的理解, 理论上一切合理, 但是细节不好推敲, 杠杆不是平台真的给你100块了, 账目上这部分钱甚至不存在, 房子是开发商名下的,开发商是借银行钱盖的, 贷款是你的,从银行贷的, 其实有没有凭空那100万钱?
隔夜费是个EOD job 挑战性不大, 如何做那个点账号snapshot 是有点细节考虑。 一个简单的ETL, batch job 可以完成这部分的逻辑。
下面的章节将再分具体的模块, 和具体的框架,分享技术上的难易和特点。
GoXTX 下一代交易平台技术供应商
GoXTX one-stop solution for neXT generation eXchange