众所周知,在证券金融行业交易系统设计中都不可避免地涉及到需要一套订单管理系统,以实现对买卖双方的交易订单进行交易管理,本文将基于 FIX5.0 协议讨论如何优雅地设计这样一套便于扩展的订单管理系统数据模型!
一、订单下单数据模型
数据结构设计
字段 | 类型 | 必填 | 描述 | FIX5.0 |
---|---|---|---|---|
orderID | String | Y | 当前节点系统生成该订单的唯一主键,可携带当前节点信息,简单化解决分布式系统 ID 问题 | 37 |
userID | String | Y | 该订单所属用户 | |
tradingAccountID | String | Y | 该订单所属用户的交易账户 | |
clOrdID | String | Y | 上游客户下单请求中的唯一请求号 | 11 |
transactTime | datetime | Y | 下单请求中的客户委托时间 | 60 |
securityID | String | Y | 证券代码 | 48 |
securityIDSource | String | Y | 证券代码来源代码:4-ISIN,101-上交所,102-深交所...... | 22 |
symbol | String | Y | 证券名称 | 55 |
securityExchange | String | Y | 交易所代码 | 207 |
side | String | Y | 买卖方向:BUY-1-看涨,SELL-2-看跌 | 54 |
orderQty | double | Y | 委托数量 | 38 |
price | double | Y | 委托价格 | 44 |
tradeDate | String | Y | 交易日期 | 75 |
cumQty | double | Y | 累计成交数量 | 14 |
avgPx | double | Y | 平均成交价格 | 6 |
grossTradeAmt | double | Y | 累计成交金额 | 381 |
cxlQty | double | Y | 撤成数量 | 84 |
leavesQty | double | Y | 在途数量 | 151 |
ordStatus | String | Y | 订单状态:A-PendingNew、0-New、1-PartiallyFilled、2-Filled;8-Rejected;6-PendingCancel;4-Cancelled | 39 |
ordRejReason | String | N | 订单拒绝原因:102-证券停牌...... | 103 |
text | String | N | 文本 | 58 |
exchangeOrdID | String | N | 下游交易所的订单编号 ID | |
currentNodeID | String | Y | 该记录所属的交易系统节点 ID 标识 for 分布式 | |
createdAt | datetime | Y | 创建时间 | |
refStrTag1 | String | N | 扩展 Str 字段 1 | |
refStrTag2 | String | N | 扩展 Str 字段 2 | |
refStrTag3 | String | N | 扩展 Str 字段 3 | |
refDoubleTag1 | double | N | 扩展 Double 字段 1 | |
refDoubleTag2 | double | N | 扩展 Double 字段 2 |
唯一主键:orderID
唯一性索引:tradingAccountID + clOrdID
领域内关键性行为
boolean checkStatus(Action action) {
switch (action.getExecType()) {
case PendingNew:
return getOrdStatus() == OrdStatus.PendingNew;
case New:
return getOrdStatus() == OrdStatus.PendingNew || getOrdStatus() == OrdStatus.PendingCancel;
case Trade:
case PendingCancel:
case Cancelled:
return getOrdStatus() == OrdStatus.PendingNew || getOrdStatus() == OrdStatus.New || getOrdStatus() == OrdStatus.PartiallyFilled || getOrdStatus() == OrdStatus.PendingCancel;
case Rejected:
return getOrdStatus() == OrdStatus.PendingNew || getOrdStatus() == OrdStatus.New || getOrdStatus() == OrdStatus.PendingCancel;
case CancelRejected:
return getOrdStatus() == OrdStatus.PendingCancel;
default:
LOGGER.warn("no switch case to check this action = {}", action);
return false;
}
}
void apply(Action action) {
switch (action.getExecType()) {
case PendingNew:
setOrdStatus(OrdStatus.PendingNew);
break;
case New:
setOrdStatus(OrdStatus.New);
setLeavesQty(getOrderQty());
break;
case Trade:
setCumQty(getCumQty() + action.getLastQty());
setLeavesQty(getOrderQty() - getCumQty() - getCxlQty());
setCumAmount(getCumAmount() + action.getLastQty() * action.getLastPx());
computeAvgPx();
if (DecimalUtil.isZero(getOrderQty() - getCumQty())) {
setOrdStatus(OrdStatus.Filled);
} else if (DecimalUtil.isZero(getLeavesQty())) {
setOrdStatus(OrdStatus.Cancelled);
} else {
if (OrdStatus.PendingCancel != getOrdStatus()) {
setOrdStatus(OrdStatus.PartiallyFilled);
}
}
break;
case Rejected:
setOrdStatus(OrdStatus.Rejected);
setLeavesQty(0.0d);
setRejectedReason(action.getRejectedReason());
break;
case PendingCancel:
setOrdStatus(OrdStatus.PendingCancel);
break;
case Cancelled:
setCxlQty(getCxlQty() + action.getCxlQty());
setLeavesQty(getOrderQty() - getCumQty() - getCxlQty());
if (DecimalUtil.isZero(getLeavesQty())) {
setOrdStatus(OrdStatus.Cancelled);
}
break;
case CancelRejected:
if (getCumQty() < ConstDefine.TradeManage.XConst.nearlyZero) {
setOrdStatus(OrdStatus.New);
} else {
if (DecimalUtil.isZero(getOrderQty() - getCumQty())) {
setOrdStatus(OrdStatus.Filled);
} else {
setOrdStatus(OrdStatus.PartiallyFilled);
}
}
break;
default:
LOGGER.warn("no switch case to apply this action = {}", action);
break;
}
LOGGER.info("applied by actionType={} after result ordStatus={}, orderID={}", action.getExecType(), getOrdStatus(), getOrderID());
}
boolean complete() {
return (DecimalUtil.isZero(getOrderQty() - getCumQty() - getCxlQty())) || getOrdStatus().equals(OrdStatus.Rejected);
}
二、订单撤单数据模型
数据结构设计
字段 | 类型 | 必填 | 描述 | FIX5.0 |
---|---|---|---|---|
cxlOrderID | String | Y | 当前节点系统生成该撤单记录的唯一主键 | |
userID | String | Y | 该撤单所属用户 冗余 from order | |
tradingAccountID | String | Y | 该撤单所属用户的交易账户 冗余 from order | |
clOrdID | String | Y | 撤单请求中的唯一请求号 | 11 |
origClOrdID | String | Y | 撤单请求对应的原订单的唯一请求号 | 41 |
orderID | String | Y | 撤单请求对应的原订单的订单 ID | 37 |
tradeDate | String | Y | 交易日期 | 75 |
exchangeOrdID | String | N | 撤单请求对应的原订单的下游交易所的订单编号 ID 冗余 from order | |
cxlRejResponseTo | String | N | 撤单拒绝回应类型 | 434 |
cxlRejReason | String | N | 撤单拒绝原因 | 102 |
currentNodeID | String | Y | 该记录所属的交易系统节点 ID 标识 for 分布式 | |
createdAt | datetime | Y | 创建时间 | |
refStrTag1 | String | N | 扩展 Str 字段 | |
refStrTag2 | String | N | 扩展 Str 字段 2 |
唯一主键:cxlOrderID
唯一性索引:tradingAccountID + clOrdID
三、订单报文数据模型
数据结构设计
字段 | 类型 | 必填 | 描述 | FIX5.0 |
---|---|---|---|---|
orderID | String | Y | 订单 ID | 37 |
execType | String | Y | 报文执行类型:A-PendingNew,0-New,F-Trade;6-PendingCancel,4-Canceled;8-Rejected;501-CancelRejected | 150 |
execID | String | Y | 执行回报唯一编号 | 17 |
sequence | double | Y | 当日该节点上系统生成的当前报文的序号,严格从 0 开始递增 | |
userID | String | Y | 所属用户 冗余 from order | |
tradingAccountID | String | Y | 所属用户的交易账户 冗余 from order | |
clOrdID | String | Y | 下单或撤单请求中的 11 | 11 |
exchangeOrdID | String | N | 该报文所属订单的下游交易所的订单编号 ID 冗余 from order | |
lastQty | double | Y | 当次成交数量 | 32 |
lastPx | double | Y | 当次成交价格 | 31 |
cumQty | double | Y | 累计成交数量 | 14 |
cxlQty | double | Y | 撤成数量 | 84 |
leavesQty | double | Y | 在途数量 | 151 |
transactTime | datetime | Y | 当次报文达成时间 | 60 |
ordStatus | String | Y | 订单状态:A-PendingNew、0-New、1-PartiallyFilled、2-Filled;8-Rejected;6-PendingCancel;4-Cancelled | 39 |
securityID | String | Y | 证券代码 | 48 |
securityExchange | String | Y | 交易所代码 | 207 |
side | String | Y | 买卖方向 | 54 |
orderQty | double | Y | 委托数量 | 38 |
price | double | Y | 委托价格 | 44 |
tradeDate | String | Y | 交易日期 | 75 |
ordRejReason | String | N | 订单拒绝原因类型:102-证券停牌...... | 103 |
ordRejReasonDesc | String | N | 订单拒绝原因 | 58 |
cxlRejResponseTo | String | N | 撤单拒绝回应类型 | 434 |
cxlRejReason | String | N | 撤单拒绝原因 | 102 |
currentNodeID | String | Y | 该记录所属的交易系统节点 ID 标识 for 分布式 | |
createdAt | datetime | Y | 创建时间 |
唯一主键:orderID + execType + execID
四、客户资金数据模型
数据结构设计
字段 | 类型 | 必填 | 描述 | FIX5.0 |
---|---|---|---|---|
tradingAccountID | String | Y | 该资金账户 ID | |
currency | String | Y | 该资金账户币种 | 15 |
initAmount | double | Y | 期初金额:日初始化时 = 前一交易日期末金额,盘中不变 | |
holdingAmount | double | Y | 当前金额:成交时发生变动,holdingAmount= holdingAmount+ amount(有正负) - abs(amount) * (commission +stamp) | |
tradableAmount | double | Y | 可用金额(若存在 在途占用金额,则该字段对外验资和显示应为:holdingAmount -(在途买金额 + 在途买佣金 + 在途买印花税) - 累计冻结金额 + 累计解冻金额) =》在该公式中影响因素发送变动时均需要通过该公式计算该值,且对外显示不是直接 get 该值而是通过当时查询时刻该计算公式计算进行展示!!!已报时该字段值保持该笔单子之前的值不变,但是前台显示的值和下一次验资的值是减去了占用的金额的;废单时对外显示的值与数据库的值一直且均是该笔单子之前的值;部成则对外显示与数据库中的值均是扣除全部占用后的值;全成或撤成,则对外显示与数据库中的值均是扣除实际成交那部分占用的值,未参与实际成交的那部分的占用被回退成功 | |
endAmount | double | Y | 期末金额 == 当前金额 | |
intradayBoughtAmount | double | Y | 当日买入成交金额 | |
intradayEffectiveEntrustBuyAmount | double | Y | 当日买入有效(终态时通过该字段回退占用金额)委托金额:待报则加,废单则减,撤成则减,全成则减(因为委托价大于等于成交价) | |
intradaySoldAmount | double | Y | 当日卖出成交金额 | |
intradayEffectiveEntrustSellAmount | double | Y | 当日卖出有效(终态时通过该字段回退占用金额)委托金额:待报则加,废单则减,撤成则减,全成则减(因为委托价小于等于成交价) | |
intradayBoughtCommission | double | Y | 当日买入成交佣金 | |
intradayEffectiveEntrustBuyCommission | double | Y | 类似于 intradayEffectiveEntrustBuyAmount) | |
intradaySoldCommission | double | Y | 当日卖出成交佣金 | |
intradayEffectiveEntrustSellCommission | double | Y | 类似于 intradayEffectiveEntrustSellAmount | |
intradayBoughtStamp | double | Y | 当日买入成交印花税 | |
intradayEffectiveEntrustBuyStamp | double | Y | 类似于 intradayEffectiveEntrustBuyAmount | |
intradaySoldStamp | double | Y | 当日卖出成交印花税 | |
intradayEffectiveEntrustSellStamp | double | Y | 类似于 intradayEffectiveEntrustSellAmount | |
freezedAmount | double | Y | 累计冻结金额:根据冻结流水对该值进行增减 | |
unfreezeAmount | double | Y | 累计解冻金额:根据冻结流水对该值进行增减 | |
createdAt | datetime | Y | 创建时间 |
唯一主键:tradingAccountID+ currency
领域内关键性行为
double showTradableAmount() {
return getHoldingAmount() + getUnfreezeAmount() - getFreezedAmount()
- (getIntradayEntrustBuyAmount() - getIntradayBoughtAmount())
- (getIntradayEntrustBuyComission() - getIntradayBoughtCommission())
- (getIntradayEntrustBuyStamp() - getIntradayBoughtStamp());
}
void updateTradableAmount() {
setTradableAmount(showTradableAmount());
}
boolean check(double amount, double commission, double stamp) {
double holdingAmountTemp = getHoldingAmount() + amount - commission - stamp;
double tradableAmountTemp = holdingAmountTemp + getUnfreezeAmount() - getFreezedAmount()
- (getIntradayEntrustBuyAmount() - getIntradayBoughtAmount())
- (getIntradayEntrustBuyComission() - getIntradayBoughtCommission())
- (getIntradayEntrustBuyStamp() - getIntradayBoughtStamp()));
return holdingAmountTemp >= 0 && tradableAmountTemp >= 0;
}
void triggerByFlows(FlowsBizType type, double amount, double commission, double stamp) {
switch (type) {
case FreezeAmount:
setFreezedAmount(getFreezedAmount() + amount);
updateIntradayAmount();
break;
case UnFreezeAmount:
setUnFreezedAmount(getUnFreezedAmount() + amount);
updateIntradayAmount();
break;
case Sell:
case AmountIncrease:
case Buy:
case AmountDecrease:
setHoldingAmount(getHoldingAmount() + amount - stamp- commission);
setEndAmount(getHoldingAmount());
if (type.equals(WarrantBuy)) {
setIntradayBoughtAmount(getIntradayBoughtAmount() - amount);
setIntradayBoughtCommission(getIntradayBoughtCommission() + commission);
setIntradayBoughtStamp(getIntradayBoughtStamp() + stamp);
} else if (type.equals(WarrantSell)) {
setIntradaySoldAmount(getIntradaySoldAmount() + amount);
setIntradaySoldFeeCommission(getIntradaySoldCommission() + commission);
setIntradaySoldFeeStamp(getIntradaySoldStamp() + stamp);
}
updateIntradayAmount();
break;
default:
break;
}
}
五、客户持仓数据模型
数据结构设计
字段 | 类型 | 必填 | 描述 | FIX5.0 |
---|---|---|---|---|
tradingAccountID | String | Y | 该资金账户 ID | |
securityID | String | Y | 证券代码 | 48 |
securityExchange | String | Y | 交易所代码 | 207 |
initQty | double | Y | 期初持仓:日初始化时 = 前一交易日期末持仓,盘中不变 | |
holdingQty | double | Y | 当前持仓:成交时发生变动,holdingQty= holdingQty + quantity(/卖方向则-quantity) | |
tradableQty | double | Y | 可用持仓(若存在 在途占用持仓,则该字段对外验券和显示应为:holdingQty - 在途卖数量 - 累计冻结数量 + 累计解冻数量) =》在该公式中影响因素发送变动时均需要通过该公式计算该值,且对外显示不是直接 get 该值而是通过当时查询时刻该计算公式计算进行展示!!! 类似于可用金额 | |
endQty | double | Y | 期末持仓 == 当前持仓 | |
intradayBoughtQty | double | Y | 当日买入成交数量 | |
intradayEffectiveEntrustBuyQty | double | Y | 当日买入有效委托数量:待报则加,废单则减,撤成则减,全成则减(其实是减 0 因为 -orderQty+cumQty = 0) | |
intradaySoldQty | double | Y | 当日卖出成交数量 | |
intradayEffectiveEntrustSellQty | double | Y | 当日卖出有效委托数量:待报则加,废单则减,撤成则减,全成则减(其实是减 0 因为 -orderQty+cumQty = 0) | |
freezedQty | double | Y | 累计冻结数量:根据冻结流水对该值进行增减 | |
unfreezeQty | double | Y | 累计解冻数量:根据冻结流水对该值进行增减 | |
createdAt | datetime | Y | 创建时间 |
唯一主键:tradingAccountID+ securityID + securityExchange
领域内关键性行为
double showTradableQty() {
return getHoldingQty() + getUnfreezeQty() - getFreezedQty()
- (getIntradayEntrustSellQty() - getIntradaySoldQty());
}
void updateTradableQty() {
setTradableQty(showTradableQty());
}
boolean check(double quantity) {
double holdingQtyTemp = getHoldingQty() + quantity(根据买卖加减);
double tradableQtyTemp = holdingQtyTemp + getUnfreezeQty() - getFreezedQty()
- (getIntradayEntrustSellQty() - getIntradaySoldQty());
return holdingQtyTemp >= 0 && tradableQtyTemp >= 0;
}
void triggerByFlows(FlowsBizType type, double quantity) {
switch (type) {
case Buy:
case QtyIncrease:
setHoldingQty(getHoldingQty() + quantity);
setEndQty(getHoldingQty());
if (type.equals(FlowsBizType.WarrantBuy)) {
setIntradayBoughtQty(getIntradayBoughtQty() + quantity);
}
updateIntradayQty();
break;
case Sell:
case QtyDecrease:
setHoldingQty(getHoldingQty() - quantity);
setEndQty(getHoldingQty());
if (type.equals(FlowsBizType.WarrantSell)) {
setIntradaySoldQty(getIntradaySoldQty() + quantity);
}
updateIntradayQty();
break;
case FreezeQty:
setFreezedQty(getFreezedQty() + quantity);
updateTradableQty();
break;
case UnFreezeQty:
setUnFreezedQty(getUnFreezedQty() + quantity);
updateTradableQty();
break;
default:
break;
}
}
六、资金持仓变动流水数据模型
数据结构设计
字段 | 类型 | 必填 | 描述 | FIX5.0 |
---|---|---|---|---|
tradingAccountID | String | Y | 该资金账户 ID | |
flowID | String | Y | 流水 ID | |
tradeDate | String | Y | 流水触发的日期 | 75 |
bizType | String | Y | 流水类型:买入;卖出;资金增加;资金减少;持仓增加;持仓减少;冻结资金;解冻资金;冻结持仓;解冻持仓 | |
securityID | String | Y | 证券代码 | 48 |
securityExchange | String | Y | 交易所代码 | 207 |
variableValue | double | Y | 变化值,取绝对值,大于等于 0:买卖时为当次成交数量;资金增减及冻结解冻时为当次变化金额;持仓增减及冻结解冻时为当次变化数量 | |
orderID | String | N | 买卖时 订单 ID | 37 |
execID | String | N | 买卖时 执行回报唯一编号 | 17 |
lastPx | double | N | 买卖时 当次成交价格 | 31 |
orderQty | double | N | 买卖时 委托数量 | 38 |
price | double | N | 买卖时 委托价格 | 44 |
commission | double | N | 买卖时 佣金 | |
stamp | double | N | 买卖时 印花税 | |
currentNodeID | String | Y | 该记录所属的交易系统节点 ID 标识 for 分布式 | |
createdAt | datetime | Y | 创建时间 |
References
- http://www.quickfixengine.org/FIX50.html
- https://www.onixs.biz/fix-dictionary/5.0.SP2/index.html
- 《证券交易数据交换协议》- STEP 20050325
TODO
- 符合 FIX5.0 协议的 下单、撤单、拒绝、报文 接口设计
- 基于 h2database 关系型内存数据库的极简订单管理系统实现
- 通用订单管理系统抽象
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于