分类 标签 存档 社区 博客 友链 GitHub 订阅 搜索

证券金融 - 订单管理系统设计与实现

92 浏览

众所周知,在证券金融行业交易系统设计中都不可避免地涉及到需要一套订单管理系统,以实现对买卖双方的交易订单进行交易管理,本文将基于 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 关系型内存数据库的极简订单管理系统实现
  • 通用订单管理系统抽象
评论  
留下你的脚步
推荐阅读