本文摘自:证券金融 - 订单管理系统设计与实现


作者:zorkelvll
链接:https://www.zorkelvll.cn/blogs/zorkelvll/articles/2020/07/13/1594655044906
来源:Pipe

==============================================================================

众所周知,在证券金融行业交易系统设计中都不可避免地涉及到需要一套订单管理系统,以实现对买卖双方的交易订单进行交易管理,本文将基于 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

TODO

  • 符合 FIX5.0 协议的 下单、撤单、拒绝、报文 接口设计

  • 基于 h2database 关系型内存数据库的极简订单管理系统实现

  • 通用订单管理系统抽象