目前市面上的接口通常以事件回调的方式进行信息交互,导致业务层对当前状况的全景缺乏了解,不便于编写复杂业务逻辑。而 DIFF 协议将异步的事件回调转为同步的数据访问,使得业务层能简单同步的访问业务数据,简化了编码复杂度。
DIFF 协议分为两部分: 数据访问 和 数据传输
DIFF 协议要求服务端维护一个业务信息截面,例如:
{ "account_id": "41007684", # 账号 "static_balance": 9954306.319000003, # 静态权益 "balance": 9963216.550000003, # 账户资金 "available": 9480176.150000002, # 可用资金 "float_profit": 8910.231, # 浮动盈亏 "risk_ratio": 0.048482375, # 风险度 "using": 11232.23, # 占用资金 "position_volume": 12, # 持仓总手数 "ins_list": "SHFE.cu1609,...." # 行情订阅的合约列表 "quotes":{ # 所有订阅的实时行情 "SHFE.cu1612": { "instrument_id": "SHFE.cu1612", "datetime": "2016-12-30 13:21:32.500000", "ask_priceN": 36590.0, #卖N价 "ask_volumeN": 121, #卖N量 "bid_priceN": 36580.0, #买N价 "bid_volumeN": 3, #买N量 "last_price": 36580.0, # 最新价 "highest": 36580.0, # 最高价 "lowest": 36580.0, # 最低价 "amount": 213445312.5, # 成交额 "volume": 23344, # 成交量 "open_interest": 23344, # 持仓量 "pre_open_interest": 23344, # 昨持 "pre_close": 36170.0, # 昨收 "open": 36270.0, # 今开 "close" : "-", # 收盘 "lower_limit": 34160.0, #跌停 "upper_limit": 38530.0, #涨停 "average": 36270.1 #均价 "pre_settlement": 36270.0, # 昨结 "settlement": "-", # 结算价 }, ... } }
对应的客户端也维护了一个该截面的镜像,因此业务层可以简单同步的访问到全部业务数据。
- 业务截面的内容由各业务模块定义,例如 ref:quote ref:trade ref:mdhis
- 除非由业务模块的文档另行说明,否则业务截面中的数据应是自恰的。例如:任何时刻的业务截面都应包含 balance, static_balance 和 float_profit 字段,并且满足 balance = static_balance + float_profit
- 业务截面可能会包含协议中未写出的额外字段,使用方应忽略这些字段的信息,因此在扩展或新增业务模块时可以保持向后兼容性
- 部分字段可能会有多种数据类型,例如上述例子中的收盘和结算价在收盘前是字符串,在收盘后会更新为对应的数值
DIFF 协议使用 json 编码通过 websocket 传输,因此可以使用 ssl 实现传输层安全加密,permessage-deflate 实现数据压缩。协议通讯为全双工模式,任何一方都可以随时向对方发送数据包,也应随时准备接收对方发来的数据包,发出的包之间无需等待对方回应。每个数据包中均有一个 aid 字段,此字段值即为数据包类型。
- 业务信息截面更新:
DIFF 协议要求服务端将业务信息的变化以 JSON Merge Patch (https://tools.ietf.org/html/rfc7386) 的格式推送给客户端,例如:
{ "aid": "rtn_data", # 业务信息截面更新 "data": [ # 数据更新数组 { "balance": 10237421.1, # 账户资金 }, { "float_profit": 283114.780999997, # 浮动盈亏 }, { "quotes":{ "SHFE.cu1612": { "datetime": "2016-12-30 14:31:02.000000", "last_price": 36605.0, # 最新价 "volume": 25431, # 成交量 "pre_close": 36170.0, # 昨收 } } } ] }
- 以上数据包中的
"aid": "rtn_data"
表示该包的类型为业务信息截面更新包 - 整个 data 数组相当于一个事务,其中的每一个元素都是一个 JSON Merge Patch,处理完整个数组后业务截面即完成了从上一个时间截面推进到下一个时间截面。
- 处理过程中业务截面可能处于内部不一致的状态,例如上述例子中的 balance 更新后,float_profit 更新前,并不满足 balance = static_balance + float_profit,因此除非有特殊需求,否则业务层应等整个 data 数组都处理完成后再从业务截面中提取所需的数据
- 没有变化的字段服务端也可能发送,例如上述例子中的 pre_close。
- 服务端可以自行决定 data 数组的元素个数及每个元素中包含哪些更新,例如客户端不能假定更新行情最新价和成交量一定是在一个 JSON Merge Patch 中。
- 如果在处理完一个 JSON Merge Patch 后,某个 object 下的所有字段都被删除则也应将该 object 删除
- 以上数据包中的
- 业务信息截面更新请求:
DIFF 协议要求客户端发送 peek_message 数据包以获得业务信息截面更新
{ "aid": "peek_message" }
- 服务端在收到 peek_message 数据包后应检查是否有数据更新,如果有则应将更新内容立即发送给客户端,如果没有则应等到有更新发生时再回应客户端。
- 服务端发送 rtn_data 数据包后可以等收到下一个 peek_message 后再发送下一个 rtn_data 数据包。
- 一个简单的客户端实现可以在连接成功后及每收到一个 rtn_data 数据包后发送一个 peek_message 数据包,这样当客户端带宽不足时会自动降低业务信息截面的更新频率以适应低带宽
- 指令包:
当数据包中的 aid 字段不是 rtn_data 或 peek_message 则表示该包为一个指令包,具体指令由各业务模块定义,例如 subscribe_quote 表示订阅行情,insert_order 表示下单
- 由于客户端和服务端存在网络通讯延迟,客户端的指令需要过一段时间才会影响到截面中的业务数据,为了使客户端能分辨出服务端是否处理了该指令,通常服务端会将客户端的请求以某种方式体现在截面中(具体方式由各业务模块定义)。例如 subscribe_quote 订阅行情时服务端会将业务截面中的 ins_list 字段更新为客户端订阅的合约列表,这样当客户端检查业务截面时如果 ins_list 包含了客户端订阅的某个合约,但是 quotes 没有该合约则说明该合约不存在