在金融市场中,大额订单的一次性交易可能会对市场价格产生较大冲击,导致交易成本增加。例如,大额买入订单可能会迅速推高股价,使后续买入成本上升;大额卖出订单则可能打压股价,造成资产贱卖。拆单算法通过将大额订单拆分成多个小额订单,在指定时间段的不同时间点进行小额订单交易,避免了一次性大额交易对市场价格的过度影响。
本教程使用了 DolphinDB 的 CEP (复杂事件处理引擎用)产品,系统的讲解了基于 CEP 引擎搭建拆单调度系统的全流程,包括 TWAP 和 VWAP 两种拆单算法。读者将学习以下内容:
- CEP 的基础介绍。
- 如何基于 CEP 引擎实现 TWAP 算法拆单。
- 如何基于 CEP 引擎实现 VWAP 算法拆单
1. CEP 引擎介绍
复杂事件处理(Complex Event Processing,简称 CEP )引擎是一款用于实时处理和分析事件流中复杂事件的引擎。其主要功能包括接收实时数据流,定义事件并从事件流中检测特定事件,进而对满足指定规则的事件执行预设的操作。详细功能介绍如下:
- 事件捕捉和过滤: 从大量实时数据流中找到特定事件。
- 事件模式:识别指定的事件模式,这些模式可以涉及多个事件的组合,形成具有特定含义的事件序列。
- 复杂事件处理:执行复杂的事件处理逻辑,包括筛选、聚合、转换等操作,以识别关键信息或发现特定的业务模式。

图 1-1 CEP 引擎架构图
由图 1-1 可以看出,一个完整的 CEP 应用包括以下几个部分:事件流序列化器、事件流反序列化器、事件分发器、CEP 子引擎。事件是贯穿这些部分的基本元素,详细介绍请查阅官网。
2. TWAP 拆单算法
本章将介绍如何使用 CEP 引擎实现 TWAP 拆单算法。
2.1 算法思想
TWAP(Time Weighted Average Price),时间加权平均价格算法,是一种最简单的拆单算法交易策略,主要适用于流动性较好的市场和订单规模较小的交易。该模型将交易时间进行均匀分割,并在每个分割节点上等量拆分订单进行提交。例如,可以将某个交易日的交易时间平均分为 n 段,TWAP 算法会将该交易日需要执行的订单均匀分配在这 n 个时间段上去执行,从而使得交易均价跟踪 TWAP 。TWAP 模型设计的目的是使交易对市场影响减小的同时提供一个较低的平均成交价格,从而达到减小交易成本的目的。TWAP 算法的公式如下:

其中,n 为时间段的个数,pricei 为分割节点上拆分订单的价格。但使用 TWAP 算法进行拆单存在一些问题:
- 在订单规模很大的情况下,均匀分配到每个节点上的下单量仍然较大,当市场流动性不足时仍可能对市场造成一定的冲击。
- 传统的 TWAP 算法对交易时间和订单规模进行均匀拆分,这种有规律的交易模式容易被市场其他参与者察觉和预测。一旦其他交易者发现了这种模式,就可以根据其规律提前布局,增加交易成本。
本例基于上述问题,对传统 TWAP 算法做了如下改进:
- 子单规模、下单时间间隔在用户指定的范围内随机,使得交易模式更加隐蔽,避免被其他市场参与者察觉和针对。
- 对大额母单的拆单状态进行实时管理,包括暂停下单、恢复下单和终止下单。用户可以根据实时市场形势,对母单状态进行管理,增加风险容错。
2.2 功能模块
TWAP 算法逻辑实现主要依赖 DolphinDB 如下产品和功能:
- CEP 引擎:核心组件,把所有流式数据包括行情和订单都看作是事件流,自定义事件流的处理规则。
- 数据回放功能:模拟实时快照行情。
- 流表的发布订阅功能:解耦用户母单发布和拆单 CEP 引擎,促进系统内模块间的通信。
TWAP 的算法逻辑实现流程图如下所示:

图 2-1 TWAP 算法流程图
本例中 CEP 引擎的策略启动事件是母单事件 ParentOrder
,母单事件进入引擎后,将启动对母单状态管理事件 OrderAlterAction
的监听。行情快照数据通过回放功能进入键值内存表后,供拆单核心函数 PlaceOrder
读取,以确定子单下单的价格。下面将分模块介绍它们的主要功能。
1)数据回放
数据回放 Replay
是 DolphinDB 中常用于高频策略回测场景的方法,它可以根据指定的回放模式,按时间顺序将一个或多个数据表的数据回放到某个数据表或引擎,模拟实时数据的写入。
由于行情快照由交易所按固定时间间隔推送, Replay
可以很好地将这种数据按时间戳排序后输出到流数据表。而行情快照数据中包含了交易所中多种基金的大量的历史行情,因此直接提供给 CEP 引擎会导致慢查询。在现实市场中,确定子单价格只需要该基金最新的快照盘口数据,因此一支基金只需要保留最新的行情快照记录,DolphinDB 提供的键值内存表可以实现上述思路。有关键值内存表的说明见官方文档 。
本例中键值内存表以基金 ID 为主键,快照行情数据(包含买卖盘口十档价格)为非主键列,就可以向 CEP 引擎提供多支基金的实时行情快照数据,以模拟实时行情快照写入。
2)流表发布订阅
DolphinDB 的流表订阅采用经典的发布-订阅(Pub-Sub)通信模型,通过消息队列实现流数据的发布与订阅,从而将流数据生产者(发布者)与消费者(订阅者)解耦。异构流数据表接收流数据的输入,当新的流数据注入到该流数据表时,发布者会将这些数据发送到所有订阅者的消费队列,并通知所有订阅方,从消费队列中获取数据进行增量处理。
内存表可以通过 subscribeTable
函数订阅流数据表,上文中的键值内存表订阅流数据表,需要自定义回调函数,并在回调函数中将接收到的增量数据插入到键值内存表中。最终键值内存表中记录了所有基金的最新行情快照数据。如下图所示。

图2-2 键值对内存表功能示意图
CEP引擎可以通过 subscribeTable
函数订阅流数据表,需要指定 handler 为 CEP 引擎的句柄,通过 getStreamEngine
获得对应 CEP 引擎的句柄。CEP 引擎订阅异构流数据表,当流表中出现增量 ParentOrder
和 OrderAlterAction
事件时,将事件注入 CEP 引擎,CEP 引擎添加对应的监视器和回调函数进行事件处理,完成拆单下单和母单状态管理的过程。
3)CEP 引擎
CEP 引擎模块是拆单系统中最重要,也是最复杂的部分。在本案例中,采用了动态启动策略的方式:当引擎内的事件监听器(Event Listener)捕获策略启动事件 ParentOrder
时,才会设置对 OrderAlterAction
的监听。行情快照通过数据回放进入流数据表,流数据表将数据发布到键值内存表中,供 CEP 引擎查询基金最新的盘口价格。
- 拆单下单:
ParentOrder
注入 CEP 引擎后,调用核心函数PlaceOrder
进行拆单下单。PlaceOrder
首先判断母单状态,若处于初始化/下单中,则进行拆单。PlaceOrder
根据用户指定的子单股数范围,使用 DolphinDB 的rand
函数随机选取子单股数。从键值内存表中读取当前基金的最新盘口价格,确定子单价格。确定子单参数后,将子单输出到子单流表中即下单。下单后判断是否下单完毕,若下单完毕则注销监视器;否则使用rand
函数随机选取间隔时间后,定时启动下一次拆单下单。 - 母单状态修改:策略启动后,CEP 引擎设置对
OrderAlterAction
事件的监听,当OrderAlterAction
注入 CEP 引擎,监视器根据母单当前状态和目标状态对母单进行操作。例如将处于下单状态的母单设置为暂停下单,此时 CEP 引擎会暂停拆单下单的过程,监听下一个OrderAlterAction
事件注入引擎,从而恢复下单。OrderAlterAction
事件输出到状态修改流表中。 - 可视化:Dashboard 是 DolphinDB 提供的一个强大的数据可视化和分析工具,旨在帮助用户更好地理解和利用数据。将子单流表和状态修改流表中的数据输出到 Dashboard ,可以实时观察母单的拆单下单过程,以及母单的状态修改情况。
2.3 代码实现
在本节中我们将具体介绍 TWAP 拆单系统代码的实现,包括定义事件类、键值内存表订阅流表、回放行情数据、定义监视器、创建 CEP 引擎、CEP 引擎订阅流表、启动策略实例等功能模块,完整代码见文末附录。
step1-定义事件类
DolphinDB 将事件定义为类,首先使用 DolphinDB 脚本语言将母单信息、母单状态变更信息定义为类。完整的事件类代码见附件。
- 母单类
ParentOrder
:除了母单的基本信息,例如母单 ID、批次、基金代码、证券代码、执行人、母单股数、买卖方向等,需要定义为母单类的成员变量外,还需要将拆单的核心参数定义为母单类的成员变量:
arduino
fundCode :: STRING // 基金ID
tradeQuantity :: LONG //母单股数
tradeDirection :: STRING // 买卖方向,"B"对应买,"S"对应卖
//拆单参数
splitMethod :: STRING // 拆单算法
priceOption :: INT // 买一还是卖一价格
startTime :: TIMESTAMP // 拆单开始时间
endTime :: TIMESTAMP // 拆单结束时间
lowChildOrderQty :: INT // 子单最小股数
highChildOrderQty :: INT // 子单最大股数
lowSplitInterval :: INT // 最小拆单间隔(秒)
highSplitInterval :: INT // 最大拆单间隔(秒)
orderStatus :: STRING // 拆单状态
splitMethod
指定算法类型,这里为 TWAP;priceOption
指定子单价格使用行情快照中的买一价格还是卖一价格;startTime
、endTime
指定拆单时间范围;lowChildOrderQty
、highChildOrderQty
指定子单下单股数范围;lowSplitInterval
、highSplitInterval
指定子单拆单时间间隔范围; orderStatus
记录母单状态,例如初始化、下单中、暂停等。
- 母单状态变更类
OrderAlterAction
:将对母单状态变更操作的信息定义为母单状态变更类,包含以下成员变量。
arduino
splitOrderId :: STRING //操作的母单号
eventType :: STRING //事件类型
operation :: STRING //操作类型
batchId :: STRING //批次ID(母单的唯一ID)
handlerEmpid :: STRING //执行人
handlerName :: STRING // 执行人
eventTime :: TIMESTAMP // 下达变更单时间
核心成员变量是 operation
指定此次状态变更的操作类型,例如暂停、恢复、终止下单等。CEP 引擎的监视器根据操作类型对母单的 orderStatus
进行变更。
step2-创建母单、子单记录内存表
创建母单记录内存表 parentOrderManage
,修改订单记录内存表 alterOrderManage
,子单接收流表 subOrderStream
以及 CEP 引擎订阅的异构流数据表 orderBlobStream
。
sql
//创建母单记录内存表
colNames=["splitOrderId","eventType","batchId","tagBatchId","sortNum","fundCode","fundName","assetId","assetName","combinationNo","combinationName","stockCode","stockName","tradeDate","tradeQuantity","tradeDirection","market","handlerEmpid","handlerName","splitMethod","orderType","price","startTime","endTime","splitInterval","orderStatus","splitOrderAmount","eventTime","lastUpdateTime"]
colTypes=[STRING,SYMBOL,STRING,STRING,INT,SYMBOL,SYMBOL,STRING,STRING,STRING,STRING,SYMBOL,SYMBOL,STRING,LONG,SYMBOL,SYMBOL,STRING,STRING,SYMBOL,SYMBOL,DOUBLE,TIMESTAMP,TIMESTAMP,INT,SYMBOL,INT,TIMESTAMP,TIMESTAMP]
share table(1:0,colNames,colTypes) as parentOrderManage
//创建修改订单记录内存表
colNames=`splitOrderId`eventType`operation`batchId`handlerEmpid`handlerName`eventTime
colTypes=[STRING,STRING,STRING,STRING,STRING,STRING,TIMESTAMP]
share table(1:0, colNames, colTypes) as alterOrderManage
// 创建子单接收流数据表
colNames=["splitOrderId","batchId","tagBatchId","sortNum","fundCode","fundName","assetId","assetName","combinationNo","combinationName","stockCode","stockName","tradeDate","tradeQuantity","tradeDirection","market","handlerEmpid","handlerName","orderType","price","lastUpdateTime"]
colTypes=[STRING,STRING,STRING,INT,SYMBOL,SYMBOL,STRING,STRING,STRING,STRING,SYMBOL,SYMBOL,STRING,LONG,SYMBOL,SYMBOL,STRING,STRING,SYMBOL,DOUBLE,TIMESTAMP]
share streamTable(1:0, colNames, colTypes) as subOrderStream
// 创建异构流数据表,被CEP引擎订阅
share(streamTable(100:0,`timestamp`eventType`blob`splitOrderId, [TIMESTAMP, STRING,BLOB,STRING]),"orderBlobStream")
step3-键值内存表订阅流表
使用键值内存表 snapshotOutputKeyedTb
订阅行情快照流表 snapshotStream
,首先定义两张表。
- 表结构定义: 定义行情快照流表
snapshotStream
,用于接收回放的行情快照数据;定义键值内存表snapshotOutputKeyedTb
,用于存储每支基金的最新行情快照。
scss
// 定义行情快照流表,这个流表用于接收回放的行情快照数据
colNames = `Market`TradeDate`TradeTime`SecurityID`OfferPrice`BidPrice`OfferOrderQty`BidOrderQty
coltypes = [SYMBOL,DATE,TIME,SYMBOL,DOUBLE[],DOUBLE[],INT[],INT[]]
share streamTable(1:0,colNames,coltypes) as snapshotStream
// 创建键值对表,该表订阅snapshotStream,存储每个基金的买盘和卖盘,每个基金的买盘和卖盘只有一条记录
snapshotOutputKeyedTbTmp = keyedTable(`SecurityID,1:0,colNames,coltypes)
share snapshotOutputKeyedTbTmp as snapshotOutputKeyedTb
snapshotStream
中,SecurityID
为基金唯一 ID ,OfferPrice
为卖盘十档价格,OfferOrderQty
为卖盘对应的十档委托量;BidPrice
为买盘十档价格,BidOrderQty
为买盘对应的十档委托量。
snapshotOutputKeyedTb
中,主键为 SecurityID
,字段和 snapshotStream
一致。最终每支基金只会保存一条最新的行情快照记录。
- 订阅:
snapshotOutputKeyedTb
订阅snapshotStream
中的增量行情快照数据。
ini
// 订阅snapshotStream回调函数
def handleSnapshot(msg) {
// 拿到所有数据
data = exec * from msg
// 向snapshotOutputKeyedTb表写入
insert into snapshotOutputKeyedTb values(data[`Market],data[`TradeDate],data[`TradeTime],data[`SecurityID],
data[`OfferPrice],data[`BidPrice],data[`OfferOrderQty],data[`BidOrderQty])
}
// 订阅
subscribeTable(tableName = `snapshotStream,actionName=`handleSnapshot,handler = handleSnapshot,msgAsTable=true,batchSize = 1)
使用 subscribeTable
函数订阅 snapshotStream
,回调函数 handleSnapshot
接收到的增量行情快照数据插入到 snapshotOutputKeyedTb
。
step4-定义监视器
CEP 引擎内部监视器的配置是拆单系统实现中最关键的步骤。监视器内封装了整个拆单策略,其结构大致如下。
javascript
class SplitMonitor:CEPMonitor{
def SplitMonitor() {
//本例中,初始monitor 不需要传值, 在克隆复制任务monitor 时进行设置。
}
//初始记录母单记录信息
def initPOrderManageInfo(pOrder){...}
//更新母单记录信息
def updatePOrderManageInfo(pOrder,opTime){...}
}
//TWAP 算法下单监听 monitor,继承关系
class TWAPSplitMonitor:SplitMonitor {
// 记录子单总下股数的变量
subOrderQtys :: INT
// 当前母单
parentOrder :: ParentOrder
def TWAPSplitMonitor() {
//本例中,初始monitor 不需要传值, 在克隆复制任务monitor 时进行设置。
}
// 在范围内选取随机数,被时间浮动和单量浮动调用
def randNum(lowNum, highNum){...}
//TWAP 下单方法
def placeOrder(){...}
//母单拆单状态变更操作
def orderAlter(oaAction){...}
//初始化parentOrder,进行拆单下单,设置OrderAlterAction事件监听
def startPlaceOrder(pOrder){...}
//创建母单下单monitor实例
def forkParentOrderMonitor(pOrder){...}
//初始任务
def onload(){
addEventListener(forkParentOrderMonitor, "ParentOrder", ,"all")
}
}
成员变量 subOrderQtys
:由于子单是给定股数范围内随机股数下单,使用 subOrderQtys
记录当前已下子单的股数和,避免超出母单股数。
成员变量 parentOrder
:记录当前母单的参数,包括基本信息,拆单参数,和拆单状态。
下面将按照 CEP 引擎运作的逻辑顺序,依次介绍监视器中各个成员方法的具体内容。
- onload 初始任务
创建引擎并实例化监视器后,将首先调用其内部的 onload
函数。回顾上文, CEP 引擎工作流的源头是监听策略启动事件 ParentOrder
,一旦监听到 ParentOrder
注入才进行下一步的操作。因此,在 onload
函数中,只需考虑设置相关的事件监听器以便启动策略即可。使用 addEventListener
函数监听 ParentOrder
事件注入,指定回调函数为 forkParentOrderMonitor
,事件类型是 ParentOrder
,设置监听次数为持续监听。
csharp
//初始任务
def onload(){
addEventListener(forkParentOrderMonitor, "ParentOrder", ,"all")
}
onload
方法设置了一个事件监听器,监听所有的母单事件 ParentOrder
。当监听到该类型事件时,下一步将启动整个拆单下单过程。为了控制母单拆单下单的线程安全,为每个母单创建不同的 Monitor 实例,因此在对应的回调函数 forkParentOrderMonitor
中需要包含对 Monitor 实例的创建和母单参数传递等步骤。从 onload
方法开始,函数调用的流程与实现的功能可以分为四个模块,如下图所示。

图2-3 函数调用模块图
其中, startPlaceOrder
函数是后三个模块的启动函数,启动顺序如上图所示。模块 3 、模块 4 中,函数调用的流程与实现的功能如下图所示。

图 2-4 模块 3 、模块 4 流程图
接下来从策略启动事件对应的回调函数 forkParentOrderMonitor
开始来介绍具体的代码实现。
- forkParentOrderMonitor 生成监视器实例
在现实的交易市场中,系统会同时接到多个需要拆分的大额委托单。若 CEP 引擎内只使用一个 Monitor 实例来操作当前委托单的拆单下单,由于一个 Monitor 实例只有一个 parentOrder
成员变量,则会使得成员变量 parentOrder
的属性值一直被新注入的母单事件修改,引起线程安全问题。
为了解决上面的问题,CEP 引擎内的初始 Monitor 只负责监控策略启动事件注入,参考上文的 onload
函数。每当 onload
函数监测到新的 ParentOrder
事件注入,调用 forkParentOrderMonitor
生成一个新的 sub-Monitor 实例,进行当前委托单的拆单下单操作。
scss
//生成一个母单下单monitor实例
def forkParentOrderMonitor(pOrder){
name = "母单下单["+pOrder.splitOrderId +"]"
spawnMonitor(name,startPlaceOrder, pOrder)
}
- startPlaceOrder 启动核心模块
startPlaceOrder
函数中,包含模块 2 、模块 3 和模块 4 的启动步骤,函数定义如下。
scss
//模块启动函数
def startPlaceOrder(pOrder){
// 设置当前子任务 monitor 对象的内部母单变量
parentOrder = pOrder
// 对子单量总股数初始化为0
subOrderQtys = 0
//TWAP 拆单初始化
parentOrder.setAttr(`orderStatus,'初始化')
parentOrder.setAttr(`sortNum,0) //拆单顺序号
//记录母单状态到内存表
initPOrderManageInfo(parentOrder)
//计算母单开始拆单下单时间
if(parentOrder.startTime == null|| now()>=parentOrder.startTime){//初始下单时间为空,或者初始下单时间早于现在,则立即开始下单
placeOrder()
}else {//下单等待时间,按指定startTime 时间开始下单
//当前时间到开始下单时间间隔,计算出来的是毫秒数,转成秒
period_wait = round((parentOrder.startTime - now())\1000 ,0)
//指定在 period_wait 秒后开始启动一次下单
addEventListener(placeOrder,,,1,,duration(period_wait+"s"))
}
//创建母单变更操作监听器,持续监听
addEventListener(orderAlter, "OrderAlterAction", <OrderAlterAction.splitOrderId = pOrder.splitOrderId>,"all")
}
函数逻辑如下:
- 先进行当前 Monitor 母单成员变量
parentOrder
、子单总股数成员变量subOrderQtys
的初始化。然后调用initPOrderManageInfo
函数,将当前母单事件记录到内存表parentOrderManage
中,对应模块 2 。 - 初始化完成,检查当前时间是否到达开始拆单时间。若已经超过开始下单时间,则调用
placeOrder
函数对当前委托单进行拆单下单;否则等待到达开始时间后,调用placeOrder
函数。对应模块 3 中的拆单开始时间判断。 - 拆单启动完成,使用
addEventListener
函数启动对OrderAlterAction
事件的监听。对应模块 4 中的初始监听。
- initPOrderManageInfo 记录母单信息
initPOrderManageInfo
函数的定义十分简单,将监听到的母单事件记录到对应内存表 parentOrderManage
中。
scss
//初始记录母单记录信息
def initPOrderManageInfo(pOrder){
parentOrderManage=objByName('parentOrderManage')
insert into parentOrderManage values(
pOrder.splitOrderId,pOrder.eventType,pOrder.batchId,pOrder.tagBatchId,pOrder.sortNum,
pOrder.fundCode,pOrder.fundName,pOrder.assetId,pOrder.assetName,pOrder.combinationNo,
pOrder.combinationName,pOrder.stockCode,pOrder.stockName,pOrder.tradeDate,pOrder.tradeQuantity,
pOrder.tradeDirection,pOrder.market,pOrder.handlerEmpid,pOrder.handlerName,
pOrder.splitMethod,pOrder.orderType,pOrder.price,pOrder.startTime,pOrder.endTime,
0,pOrder.orderStatus,0,pOrder.eventTime,now())
}
- randNum 生成范围内随机数
randNum
函数用于在给定范围内生成随机整数并返回。上文提到,用户可以在母单事件中指定子单股数范围为 lowChildOrderQty
~ highChildOrderQty
,拆单时间间隔范围为 lowSplitInterval
~ highSplitInterval
。randNum
函数用于在给定范围内随机生成子单股数和时间间隔。
scss
// 在范围内选取随机数,被时间浮动和单量浮动调用
def randNum(lowNum, highNum){
if(lowNum == highNum){
return lowNum
}
// 向量保存可以选择的浮动值
nums = array(INT, 0).append!(lowNum..highNum)
// 随机下标,范围是0~highnum-lownum,这里返回的是数组,因此要带上索引
indexNum = highNum-lowNum
index = rand(indexNum, 1)[0];
// 取出浮动值
return nums[index];
}
randNum
函数先生成一个数组保存给定范围内的所有整数,然后生成随机下标,根据随机下标访问并返回。
- updatePOrderManageInfo 更新母单最后修改时间
updatePOrderManageInfo
函数用于修改母单的最后修改时间。
csharp
//更新母单记录信息
def updatePOrderManageInfo(pOrder,opTime){
parentOrderManage=objByName('parentOrderManage')
update parentOrderManage set sortNum = pOrder.sortNum,orderStatus=pOrder.orderStatus, lastUpdateTime = opTime where splitOrderId = pOrder.splitOrderId
}
- placeOrder 拆单核心函数
placeOrder
函数是拆单下单的核心函数,对应模块 3 中的后半部分。
scss
//TWAP 下单方法
def placeOrder(){
//判断是否超过下单时限
if(now()>= parentOrder.endTime){ //当前时间大于下单结束时间,则不再下单
parentOrder.setAttr(`orderStatus,'时限中止')
updatePOrderManageInfo(parentOrder,now())
return
}
//判断当前母单状态是否是可以下单状态,不是则退出
if(!(parentOrder.orderStatus in ['初始化','下单中'])){
return
}
// 计算子单已经下过的单数
totalQty = subOrderQtys
//计算剩余待下单股数 = 母单股数 - 所有子单数
remainQty = parentOrder.tradeQuantity - totalQty
// 计算当前应该下的子单数
//若剩余股数大于等于最小子单股数, 下随机子单股数
if(remainQty >= parentOrder.lowChildOrderQty){
//下单股数,分两个区间,如果剩余股数在low-high之间,则下的股数在low-remain之间随机,否则在low-high之间随机
if(remainQty < parentOrder.highChildOrderQty){
subOrderQty = randNum(parentOrder.lowChildOrderQty, remainQty)
}else{
subOrderQty = randNum(parentOrder.lowChildOrderQty, parentOrder.highChildOrderQty)
}
}else{//否则下单剩余股数
subOrderQty = remainQty
}
// 更新子单股数
subOrderQtys = subOrderQtys+subOrderQty
// 更新剩余单数
remainQty = remainQty-subOrderQty
// 拿到母单的基金代码
v_securityId = parentOrder.fundCode
// 直接从分布式表中进行查询,定义一个函数
if(parentOrder.priceOption == 0){//从买一价格获取,从BidPrice[0]获取价格
// 从键值对表中获取
BidPrice = exec BidPrice from snapshotOutputKeyedTb where SecurityID = v_securityId
// 买一价格
subOrderPrice = BidPrice[0]
}else{//从卖一价格获取,从OfferPrice[0]获取价格
// 从键值对表中获取
OfferPrice = exec OfferPrice from snapshotOutputKeyedTb where SecurityID = v_securityId
// 卖一价格
subOrderPrice = OfferPrice[0]
}
//构建子单
//创建子单时间
subOrderPlaceTime = now()
//构建下达子单到流表
subOrderStream = objByName('subOrderStream')
// 插入子单流数据表
insert into subOrderStream values(parentOrder.splitOrderId,parentOrder.batchId,
parentOrder.splitOrderId+'_'+(parentOrder.sortNum+1),parentOrder.sortNum+1,
parentOrder.fundCode,parentOrder.fundName,parentOrder.assetId,parentOrder.assetName,
parentOrder.combinationNo,parentOrder.combinationName,parentOrder.stockCode,parentOrder.stockName,
parentOrder.tradeDate,subOrderQty,parentOrder.tradeDirection,parentOrder.market,parentOrder.handlerEmpid,
parentOrder.handlerName,parentOrder.orderType,subOrderPrice,subOrderPlaceTime);
//设置下单次数
parentOrder.setAttr(`sortNum,parentOrder.sortNum+1)
//判断是否还需继续下单
if(remainQty>0){
parentOrder.setAttr(`orderStatus,'下单中')
//保存母单信息
updatePOrderManageInfo(parentOrder,subOrderPlaceTime)
//设置下次下单的监听,等待时间为拆单策略参数中设定
// addEventListener(handler=placeOrder, wait=duration(parentOrder.splitInterval+"s"), times=1)
// 等待时间进行浮动
realTime = randNum(parentOrder.lowSplitInterval, parentOrder.highSplitInterval)
addEventListener(placeOrder,,,1,,duration(realTime+"s"))
}else{//最后一次下单,销毁下单监听
parentOrder.setAttr(`orderStatus,'下单完毕')
//保存母单信息
updatePOrderManageInfo(parentOrder,now())
//下单完结,销毁监视器
destroyMonitor()
}
}
placeOrder
函数逻辑如下:
- 判断当前时间是否超过拆单结束时间,若超过结束时间,则设置母单状态为
'时限中止'
,并调用updatePOrderManageInfo
函数更新母单最后修改时间。 - 判断当前母单状态是否处于
'初始化'
或'下单中'
,如果不满足,则停止拆单。 - 根据成员变量
subOrderQtys
和母单属性tradeQuantity
计算剩余待下单股数remainQty
,调用randNum
函数确定子单的股数subOrderQty
,并对subOrderQtys
和remainQty
进行更新。 - 根据母单属性
priceOption
确定子单价格为买一还是卖一价格,并从键值内存表snapshotOutputKeyedTb
中查询。 - 构建子单,并插入子单流表
subOrderStream
。 - 根据剩余股数
remainQty
判断是否还需要下单。若还需要下单,则保存母单状态和修改时间,调用randNum
函数确定下单间隔,并定时再次调用placeOrder
函数重复上述步;若已经没有剩余股数,则保存母单状态和修改时间,最后摧毁监视器。
- orderAlter 母单状态管理
startPlaceOrder
函数中设置了对 OrderAlterAction
事件的监听。当 OrderAlterAction
事件注入,调用 orderAlter
函数对母单状态进行修改。
scss
//母单拆单变更操作
def orderAlter(oaAction){
alterOrderManage=objByName('alterOrderManage')
insert into alterOrderManage values (oaAction.splitOrderId,oaAction.eventType,oaAction.operation,oaAction.batchId,oaAction.handlerEmpid,oaAction.handlerName,now())
if(oaAction.operation=='暂停'){
parentOrder.setAttr(`orderStatus,'暂停')
updatePOrderManageInfo(parentOrder,now())
}else if(oaAction.operation=='恢复'&& parentOrder.orderStatus=='暂停'){
parentOrder.setAttr(`orderStatus,'下单中')
updatePOrderManageInfo(parentOrder,now())
//重新开始下单
placeOrder()
}else if(oaAction.operation=='终止母单'){
parentOrder.setAttr(`orderStatus,'终止母单')
//保存母单信息
updatePOrderManageInfo(parentOrder,now())
//下单完结,销毁监视器
destroyMonitor()
}
}
函数逻辑如下:
- 将状态修改记录保存到状态修改流表
alterOrderManage
中。 - 根据
OrderAlterAction
事件的operation
属性对母单做对应的操作。若operation
为'暂停'
,则将母单状态设置为暂停,并更新最后修改时间,此时placeOrder
中的状态校验不通过,不会进行拆单操作;若operation
为'恢复'
并且母单状态为'暂停'
,则将母单状态重置为下单中,并更新最后修改时间,此时placeOrder
中的状态校验通过,将继续拆单下单过程;若operation
为'终止母单'
,则将母单状态设置为终止母单,更新最后修改时间,并摧毁监视器,结束拆单下单过程。
step5.创建 CEP 引擎并订阅异构流表
使用 createCEPEngine
函数创建 CEP 引擎,并使用 subscribeTable
函数使 CEP 引擎订阅异构流表 orderBlobStream
,orderBlobStream
中接收 ParentOrder
和 OrderAlterAction
事件流。
less
//创建下单任务引擎,表示TwapSplitMonitor引擎订阅的流数据表类型(母单类型和变更操作类型,多余的字段压缩到BLOB中)
dummy = table(1:0, `timestamp`eventType`blobs`splitOrderId, `TIMESTAMP`STRING`BLOB`STRING)
//创建cep 监听引擎
engine = createCEPEngine(name='TwapSplitMonitor', monitors=<TWAPSplitMonitor()>, dummyTable=dummy, eventSchema=[ParentOrder,OrderAlterAction],timeColumn=`timestamp)
// 订阅异构流表
subscribeTable(tableName="orderBlobStream", actionName="orderBlobStream",handler=getStreamEngine("TwapSplitMonitor"),msgAsTable=true)
step6-回放行情数据
使用 replay
函数,向行情快照流表 snapshotStream
中回放分布式表中的历史行情快照数据,模拟实时行情写入。
csharp
// 往行情流表里回放行情数据
snapshotTb = loadTable("dfs://l2TLDB_TWAP","snapshot")
// 回放前十支股票就行,选择需要的列
replayData = select Market,TradeDate,TradeTime,SecurityID,OfferPrice,BidPrice,OfferOrderQty,BidOrderQty
from snapshotTb where SecurityID in (select distinct SecurityID from snapshotTb limit 10) and TradeTime>=09:30m
// 每秒20条回放
submitJob("replay_snapshot", "snapshot", replay{replayData, snapshotStream, `TradeDate, `TradeTime, 20, true})
本例中历史行情快照数据保存在分布式表 snapshot
中,由于基金行情快照数据太多,这里仅回放 10 条基金的行情数据。snapshotStream
接收到回放数据后,自动将增量数据发布到 snapshotOutputKeyedTb
。最终 snapshotOutputKeyedTb
中,保存 10 支基金的最新行情快照数据。
step7-启动策略实例
使用 DolphinDB 提供的 JAVA API ,将 ParentOrder
事件放入 orderBlobStream
,启动拆单策略;随后将 OrderAlterAction
事件放入 orderBlobStream
,观察 CEP 引擎对母单状态的管理。核心函数 putOrder
如下。完整的启动策略项目文件见附录。
less
public static void putOrder() throws IOException, InterruptedException {
// 连接dolphindb数据库
DBConnection conn = DBUtil.getConnection();
// 封装母单订阅流表
EventSender sender1 = EventSenderHelperTWAP.createEventSender(conn);
// 拿到拆单参数map
HashMap<String, Object> map = getMap();
// 定义母单
DolphinDbParentSplitParamsVo dolphinDbParentVo1 = new DolphinDbParentSplitParamsVo(
"SP_001_2025030500001", // splitOrderId: 母单拆单操作单元唯一ID
"ParentOrder", // eventType: 事件类型
"2025030500001", // batchId: 母单唯一ID
"", // tagBatchId: 子单唯一ID
0, // sortNum: 拆单顺序号(从1开始)
"501019", // fundCode: 基金代码
"蓝筹精选混合", // fundName: 基金名称
"A123456789", // assetId: 资产单元ID
"量化投资组合", // assetName: 资产单元名称
"C789", // combinationNo: 组合编号
"全天候策略", // combinationName: 组合名称
"600000", // stockCode: 证券代码
"浦发银行", // stockName: 证券名称
"20231010", // tradeDate: 交易日期(yyyyMMdd)
48000L, // tradeQuantity: 交易量(注意L后缀)
"B", // tradeDirection: 交易方向(1-买入)
"SSE", // market: 交易市场
"E1001", // handlerEmpid: 执行人工号
"王强", // handlerName: 执行人姓名
(String) map.get("splitMethod"), // splitMethod: 拆单算法
(String) map.get("orderType"), // orderType: 订单类型
(Double) map.get("price"), // 子单下单价格
(Integer) map.get("priceOption"), //选择卖一价格下子单
(LocalDateTime) map.get("startTime"), // startTime: 拆单开始时间
(LocalDateTime) map.get("endTime"), // endTime: 拆单结束时间
(Integer) map.get("lowChildOrderQty"), //子单数量范围
(Integer) map.get("highChildOrderQty"),
(Integer) map.get("lowSplitInterval"), // splitInterval: 拆单间隔范围(秒)
(Integer) map.get("highSplitInterval"),
"", // orderStatus: 拆单状态
LocalDateTime.now() // eventTime: 事件下达时间
);
// 定义暂停操作
DolphinDbOrderActionVo orderAlterAction = new DolphinDbOrderActionVo(
"SP_001_2025030500001", // splitOrderId: 母单拆单操作单元唯一ID
"subOrder", // eventType
"暂停", // operation
"2025030500001", // batchId
"OOOO1", // handlerEmpid
"王强", // handlerName
LocalDateTime.now() // eventTime
);
// 定义恢复操作
DolphinDbOrderActionVo orderAlterAction1 = new DolphinDbOrderActionVo(
"SP_001_2025030500001", // splitOrderId: 母单拆单操作单元唯一ID
"subOrder", // eventType
"恢复", // operation
"2025030500001", // batchId
"OOOO1", // handlerEmpid
"王强", // handlerName
LocalDateTime.now() // eventTime
);
//发送母单,将母单放入订阅流表,供CEP引擎消费
sender1.sendEvent(dolphinDbParentVo1.getEventType(), dolphinDbParentVo1.toEntities());
System.out.println("母单放入母单订阅流表");
Thread.sleep(5000);
//下达暂停,将母单状态暂停单放入订阅流表,供CEP引擎消费
sender1.sendEvent(orderAlterAction.getEventType(), orderAlterAction.toEntities());
System.out.println("暂停单放入订阅流表");
Thread.sleep(5000);
//下达恢复,将母单状态恢复单放入订阅流表,供CEP引擎消费
sender1.sendEvent(orderAlterAction1.getEventType(), orderAlterAction1.toEntities());
System.out.println("恢复单放入订阅流表");
}
母单中的拆单参数,通过 HashMap
传入,模拟现实系统中的用户自定义拆单参数传递。
2.4 结果检视
本小节通过查看输出事件展示拆单系统运行的结果。DolphinDB Web 端提供了强大的数据可视化和分析工具------数据面板(Dashboard),旨在帮助用户更好地理解和利用数据。在本例中,母单事件、母单状态修改事件注入 CEP 引擎都分别记录到对应的内存表,拆分子单进行下单记录到子单接收流表,如此在 Dashboard 中便可以选取需要的数据进行可视化。
step1-JAVA 环境准备
配置系统的 maven 、jdk 环境,本例中的 jdk 和 maven 版本如下:
arduino
jdk - java version "1.8.0_441"
maven - Apache Maven 3.8.6
step2-数据准备
下载附录中的 TWAP 算法代码并解压。将 data/snapshot_twap.csv
放到 dolphindb/server
目录下。运行导入脚本 data/data_input.dos
进行建库建表,并将测试数据导入建好的分布式表中。将 data/dashboard.TWAP 拆单监控.json
导入到 Dashboard。
step3-系统环境准备
运行脚本 01 clearEnv.dos
、 02 Event.dos
、 03 createTable.dos
、 04 subscribeSnapshot.dos
。01 clearEnv.dos
脚本将系统中已存在的内存共享表、订阅信息、流式引擎等进行清除,确保不会重复定义; 02 Event.dos
、 03 createTable.dos
、 04 subscribeSnapshot.dos
脚本分别对应上文介绍的定义事件类、创建母单子单记录内存表、键值内存表订阅流表的功能。
step4-CEP引擎创建
运行脚本 05 Monitor.dos
、06 createCEPEngine.dos
,分别对应上文中定义监视器、创建CEP引擎订阅异构流表的功能。
step5-回放行情快照数据
运行脚本 07 replaySnapshot.dos
,将快照数据回放到快照流表 snapshotStream
中。由于键值内存表 snapshotOutputKeyedTb
订阅了 snapshotStream
,数据会被自动发布到 snapshotOutputKeyedTb
中。回放后使用如下语句查询 snapshotOutputKeyedTb
中的数据:
csharp
select * from snapshotOutputKeyedTb
查询到十支基金的最新的行情数据如下:

图2-5 键值内存表数据示意图
其中前四列分别表示基金所属市场、交易日期、交易时间和基金唯一代码; OfferPrice
和 BidPrice
表示市场中卖盘和买盘十档价格;OfferOrderQty
和 BidOrderQty
表示市场中对应的卖盘和买盘十档委托量。
step6-启动策略
下载附录中的 JAVA API 策略启动代码并解压。修改 common/DBUtil.java
中的数据库配置为用户自己的环境。运行 startTWAP.java
,将母单事件、母单状态修改事件放入异构流表中,观察 Dashboard 中,母单监控、子单监控、变更单监控中的对应输出如下:

图2-6 Dashboard输出结果图
本例中,startTWAP.java
中指定:母单总股数为 48000 ,子单的下单数量在 10000 ~ 12000 间浮动,下单间隔在 7s ~ 9s 间浮动,母单的基金代码是 501019
, priceOption
指定为 0 即选择子单价格为买一价格。
可以观察到,子单的下单数量在给定范围内随机;从子单的下单时间也能推理出,下单间隔也在给定范围内随机;子单的下单价格为 snapshotOutputKeyedTb
中基金代码为 501019
的买一价格。
2.5 总结
本章通过循序渐进的方式,介绍了如何使用 DolphinDB 的 CEP引擎实现 TWAP 拆单算法。首先说明了算法思想;然后模块化介绍了系统的功能;其次详细介绍了系统的实现流程和代码,其中最复杂的是监视器定义的部分,详细阐述了各个函数之间的调用关系;最后将系统的拆单过程,在结果检视部分使用 Dashboard 进行展示。
3. VWAP 拆单算法
本章将介绍如何使用 CEP 引擎实现 VWAP 拆单算法。
3.1 算法思想
VWAP(Volume Weighted Average Price,成交量加权平均价格)算法是一种广泛使用的拆单交易策略,主要适用于大额订单执行。该模型通过分析历史成交量分布模式,将大订单按成交量比例拆分到各个时间段执行,使成交均价尽可能接近市场 VWAP 基准。VWAP 的计算公式如下:

其中,pricei 为分割节点上拆分订单的价格,volumei 为分割节点上拆分订单的股数。
与 TWAP 均匀分配不同,VWAP 会根据市场典型的成交量分布特征动态调整下单量,在成交量大的时段分配更多订单,在成交量小的时段分配较少订单。这种设计既考虑了时间因素,又充分尊重市场的流动性分布规律。
3.2.功能模块
VWAP 算法的功能模块与上文中的 TWAP 相似。算法逻辑通过 CEP 引擎实现;通过数据回放功能模拟实时行情快照写入;使用流表订阅功能解耦用户母单发布和拆单 CEP 引擎。大致流程如下图所示。

图3-1 VWAP算法流程图
与 TWAP 算法不同的是:
- VWAP 拆单的子单股数不再是给定范围内随机,而是根据前一天的逐笔交易数据,先计算出每个时段的交易量占全天总交易量的比例,子单股数为前一天当前时段的交易量比例与母单股数的乘积。本例中时段长度为一分钟。
- VWAP 拆单的时间间隔不再是给定范围内随机,而是固定的时段间隔,即一分钟。
3.3 代码实现
在本节中我们介绍 VWAP 算法的代码实现,也主要围绕与上文 TWAP 算法的区别来阐述。
step1-定义事件类
定义事件类代码与 TWAP 算法 step1-定义事件类 类似,不同的是,母单类中不再有子单股数范围上下限、拆单间隔范围上下限四个参数:
arduino
//拆单参数开始
splitMethod :: STRING // 拆单算法
orderType :: STRING // 订单类型(限价/市价)
price :: DOUBLE // 限定价格
priceOption :: INT // 买一还是卖一价格
startTime :: TIMESTAMP // 拆单开始时间
endTime :: TIMESTAMP // 拆单结束时间
orderStatus :: STRING // 拆单状态
//拆单参数结束
eventTime :: TIMESTAMP // 事件下达时间
step2-创建内存表
- 创建母单记录内存表
parentOrderManage
,修改订单记录内存表alterOrderManage
,子单接收流表subOrderStream
以及 CEP 引擎订阅的异构流数据表orderBlobStream
。代码同 TWAP 算法。 - 创建一个内存表,用于存储基金一天每分钟的交易量。代码如下:
csharp
// 创建内存表,用于存储每分钟的交易量
trade = loadTable("dfs://l2TLDB","trade")
tradeData = select * from trade where SecurityID = "300041" and TradeDate = 2023.02.01
QtyTB =select * from (select sum(TradeQty) as TradeQty from tradeData group by minute(TradeTime) as TradeTimeInterval order by TradeTimeInterval) where TradeTimeInterval > 09:29m
share QtyTB as QtyTb
最终 QtyTb
表中记录了基金一天每分钟的交易量,如图所示。

图3-2 历史交易量表
step3-键值内存表订阅流表
键内存表订阅流表代码同 TWAP 算法 step3-键值内存表订阅流表 。
step4-定义监视器
VWAP 拆单算法中,子单股数和拆单时间间隔不需要在给定范围内随机,因此监视器类中不需要 rand
函数。监视器类的结构如下:
csharp
class SplitMonitor:CEPMonitor{
def SplitMonitor() {
//本例中,初始monitor 不需要传值, 在克隆复制任务monitor 时进行设置。
}
//初始记录母单记录信息
def initPOrderManageInfo(pOrder){...}
//更新母单记录信息
def updatePOrderManageInfo(pOrder,opTime){...}
}
//vwap 算法下单监听 monitor,继承关系
class VWAPSplitMonitor:SplitMonitor {
// 记录子单总下股数的变量
subOrderQtys :: INT
// 母单
parentOrder :: ParentOrder
// 开始拆单与历史交易表对应的时间(分钟)
splitStartTime :: MINUTE
def VWAPSplitMonitor() {
//本例中,初始monitor 不需要传值, 在克隆复制任务monitor 时进行设置。
}
//VWAP 下单方法
def placeOrder(){...}
//初始化parentOrder,进行拆单下单,设置OrderAlterAction事件监听
def startPlaceOrder(pOrder){...}
//创建母单下单monitor实例
def forkParentOrderMonitor(pOrder){}
//初始任务
def onload(){
addEventListener(forkParentOrderMonitor, "ParentOrder", ,"all")
}
}
成员变量 subOrderQtys
:使用 subOrderQtys
记录当前已下子单的股数和,避免超出母单股数。
成员变量 parentOrder
:记录当前母单的参数,包括基本信息,拆单参数,和拆单状态。
成员变量 splitStartTime
:记录当前子单的时间,与历史交易表时段对应。
下面将按照 CEP 引擎运作的逻辑顺序,依次介绍监视器中各个成员方法的具体内容。
- onload 初始任务
创建引擎并实例化监视器后,将首先调用其内部的 onload
函数。在 onload
函数中,使用 addEventListener
函数监听 ParentOrder
事件注入,指定回调函数为 forkParentOrderMonitor
,事件类型是 ParentOrder
,设置监听次数为持续监听。
csharp
//初始任务
def onload(){
addEventListener(forkParentOrderMonitor, "ParentOrder", ,"all")
}
onload
方法设置了一个事件监听器,监听所有的母单事件 ParentOrder
。当监听到该类型事件时,下一步将启动整个拆单下单过程。为了控制母单拆单下单的线程安全,为每个母单创建不同的 Monitor 实例,因此在对应的回调函数 forkParentOrderMonitor
中需要包含对 Monitor 实例的创建和母单参数传递等步骤。从 onload
方法开始,函数调用的流程与实现的功能可以分为三个模块,如下图所示。

图3-3 函数调用模块图
其中, startPlaceOrder
函数是两个模块的启动函数,启动顺序如上图所示。模块 3 中,函数调用的流程与实现的功能如下图所示。

图3-4 模块三函数调用流程图
接下来从策略启动事件对应的回调函数 forkParentOrderMonitor
开始来介绍具体的代码实现。
- forkParentOrderMonitor 生成监视器实例
forkParentOrderMonitor
函数代码实现同 TWAP 算法 step5.定义监视器 。
- startPlaceOrder 启动核心模块
startPlaceOrder
函数代码实现大致同 TWAP 算法 step5.定义监视器 。但需要对拆单时间进行初始化。
// 交易开始时间初始化
splitStartTime =
minute(09:30m)
即从上午 9:30 开始拆单。
- initPOrderManageInfo 记录母单信息
initPOrderManageInfo
函数代码实现同 TWAP 算法 step5.定义监视器。
- updatePOrderManageInfo 更新母单最后修改时间
updatePOrderManageInfo
函数代码实现同 TWAP 算法 step5.定义监视器。
- placeOrder 拆单核心函数
placeOrder
函数是拆单下单的核心函数,对应模块 3 中的后半部分。
scss
//VWAP 下单方法
def placeOrder(){
writeLog("============开始下单==========")
//判断是否超过下单时限
if(now()>= parentOrder.endTime){ //当前时间大于下单结束时间,则不再下单
parentOrder.setAttr(`orderStatus,'时限中止')
updatePOrderManageInfo(parentOrder,now())
// 摧毁监视器
destroyMonitor()
return
}
//判断当前母单状态是否是可以下单状态,不是则退出
if(!(parentOrder.orderStatus in ['初始化','下单中'])){
return
}
//计算剩余待下单股数 = 母单股数 - 所有子单数
remainQty = parentOrder.tradeQuantity - subOrderQtys
// 计算当前应该下的子单数
// 查找历史交易表,计算总交易量
totalQty = exec sum(TradeQty) from QtyTb
// 跳过中间的空白区域
if(splitStartTime==11:31m){
splitStartTime = 13:00m
}
// 将当前拆单时间转为分钟
nowTime = minute(splitStartTime)
// 查找历史交易表,当前时段(一分钟),是否有交易量
nowQtyVector = exec TradeQty from QtyTb where TradeTimeInterval=nowTime
// 确定子单股数
if(nowQtyVector.size()==0){//当前时段没有交易记录,不下单,直接设置下一次下单的监听
parentOrder.setAttr(`orderStatus,'下单中')
//保存母单信息
updatePOrderManageInfo(parentOrder,now())
// 将拆单时间更新
splitStartTime = temporalAdd(splitStartTime,1,"m")
//设置下次下单的监听,等待时间为10s
addEventListener(placeOrder,,,1,,duration(10+"s"))
return
}else{
// 拿到当前时段总交易量
nowQty = nowQtyVector[0]
totalNum = parentOrder.tradeQuantity
// 根据比例计算,这里类型转换是为了计算小数比例,否则为0,最后向上取整
subOrderQty = ceil((double(nowQty)/totalQty)*totalNum)
// subOrderQty可能会超过剩余单数
subOrderQty = (subOrderQty>remainQty) ? remainQty : subOrderQty
}
// 将拆单时间更新
splitStartTime = temporalAdd(splitStartTime,1,"m")
// 更新子单股数
subOrderQtys = subOrderQtys+subOrderQty
// 更新剩余单数
remainQty = remainQty-subOrderQty
// 拿到母单的基金代码
v_securityId = parentOrder.fundCode
// 直接从分布式表中进行查询,定义一个函数
if(parentOrder.priceOption == 0){//从买一价格获取,从BidPrice[0]获取价格
// 从键值对表中获取
BidPrice = exec BidPrice from snapshotOutputKeyedTb where SecurityID = v_securityId
// 买一价格
subOrderPrice = BidPrice[0]
}else{//从卖一价格获取,从OfferPrice[0]获取价格
// 从键值对表中获取
OfferPrice = exec OfferPrice from snapshotOutputKeyedTb where SecurityID = v_securityId
// 卖一价格
subOrderPrice = OfferPrice[0]
}
//构建子单
//创建子单时间
subOrderPlaceTime = now()
//构建下达子单到流表
subOrderStream = objByName('subOrderStream')
// 插入子单流数据表
insert into subOrderStream values(parentOrder.splitOrderId,parentOrder.batchId,
parentOrder.splitOrderId+'_'+(parentOrder.sortNum+1),parentOrder.sortNum+1,
parentOrder.fundCode,parentOrder.fundName,parentOrder.assetId,parentOrder.assetName,
parentOrder.combinationNo,parentOrder.combinationName,parentOrder.stockCode,parentOrder.stockName,
parentOrder.tradeDate,subOrderQty,parentOrder.tradeDirection,parentOrder.market,
parentOrder.handlerEmpid,parentOrder.handlerName,parentOrder.orderType,subOrderPrice,subOrderPlaceTime);
//设置下单次数
parentOrder.setAttr(`sortNum,parentOrder.sortNum+1)
//更新剩余股数
updateRemainQty()
//判断是否还需继续下单
if(remainQty>0){
parentOrder.setAttr(`orderStatus,'下单中')
//保存母单信息
updatePOrderManageInfo(parentOrder,subOrderPlaceTime)
//设置下次下单的监听,等待时间为10s
addEventListener(placeOrder,,,1,,duration(10+"s"))
}else{//最后一次下单,销毁下单监听
parentOrder.setAttr(`orderStatus,'下单完毕')
//保存母单信息
updatePOrderManageInfo(parentOrder,now())
//下单完结,销毁监视器
destroyMonitor()
}
}
placeOrder
函数逻辑如下:
- 判断当前时间是否超过拆单结束时间,若超过结束时间,则设置母单状态为
'时限中止'
,并调用updatePOrderManageInfo
函数更新母单最后修改时间。 - 判断当前母单状态是否处于
'初始化'
或'下单中'
,如果不满足,则停止拆单。 - 根据成员变量
subOrderQtys
和母单属性tradeQuantity
计算剩余待下单股数remainQty
,查询历史交易表QtyTb
,根据当前时段的历史交易比例和母单股数,计算确定子单的股数subOrderQty
,并对subOrderQtys
和remainQty
进行更新。 - 根据母单属性
priceOption
确定子单价格为买一还是卖一价格,并从键值内存表snapshotOutputKeyedTb
中查询。 - 构建子单,并插入子单流表
subOrderStream
。 - 根据剩余股数
remainQty
判断是否还需要下单。若还需要下单,则保存母单状态和修改时间,并定时再次调用placeOrder
函数重复上述步;若已经没有剩余股数,则保存母单状态和修改时间,最后摧毁监视器。
step5.创建 CEP 引擎订阅异构流表
创建 CEP 引擎订阅异构流表代码同 TWAP 算法 step6.创建 CEP 引擎并订阅异构流表 。
step6-回放行情数据
回放行情数据代码同 TWAP 算法 step4-回放行情数据 。
step7.启动策略实例
启动策略实例代码同 TWAP 算法 step7.启动策略实例 。
3.4 结果检视
本小节通过查看 Dashboard 中输出事件展示拆单系统运行的结果。在本例中,母单事件、母单状态修改事件注入 CEP 引擎都分别记录到对应的内存表,拆分子单进行下单记录到子单接收流表,如此在 Dashboard 中便可以选取需要的数据进行可视化。
step1-JAVA环境准备
配置系统的 maven 、jdk 环境,本例中的 jdk 和 maven 版本如下:
arduino
jdk - java version "1.8.0_441"
maven - Apache Maven 3.8.6
step2-数据准备
下载附录中的 VWAP 算法代码并解压。将 data/snapshot_vwap.csv
、data/trade.csv
放到 dolphindb/server
目录下。运行导入脚本 data/data_input_snapshot.dos
进行建库建表,并将测试数据导入建好的分布式表中。运行导入脚本 data/data_input_trade.dos
进行建库建表,并将测试数据导入建好的分布式表中。将 data/dashboard.VWAP 拆单监控.json
导入到 Dashboard 。
step3-系统环境准备
运行脚本 01 clearEnv.dos
、 02 Event.dos
、 03 createTable.dos
、 04 subscribeSnapshot.dos
。01 clearEnv.dos
脚本将系统中已存在的内存共享表、订阅信息、流式引擎等进行清除,确保不会重复定义; 02 Event.dos
、 03 createTable.dos
、 04 subscribeSnapshot.dos
脚本分别对应上文介绍的定义事件类、创建内存表、键值内存表订阅流表的功能。
step4-CEP 引擎创建
运行脚本 05 Monitor.dos
、06 createCEPEngine.dos
,分别对应上文中定义监视器、创建CEP引擎订阅异构流表的功能。
step5-回放行情快照数据
运行脚本 07 replaySnapshot.dos
,将快照数据回放到快照流表 snapshotStream
中。由于键值内存表 snapshotOutputKeyedTb
订阅了 snapshotStream
,数据会被自动发布到 snapshotOutputKeyedTb
中。
step6-启动策略
下载附录中的 JAVA API 策略启动代码并解压。修改 common/DBUtil.java
中的数据库配置为用户自己的环境。运行 startVWAP.java
,将母单事件放入异构流表中,观察 Dashboard 中的输出。
- 子单监控中的对应输出如下:

图3-5 子单监控输出
本例中,startVWAP.java
中指定:母单的基金代码是 300041
,总股数为48000, priceOption
指定为 0 即选择子单价格为买一价格。
可以观察到,子单的下单数量根据当前时段的历史交易比例计算得到,下单时间间隔为 10s;子单的下单价格为 snapshotOutputKeyedTb
中基金代码为 300041
的买一价格。
3.5 总结
本章通过循序渐进的方式,介绍了如何使用 DolphinDB 的 CEP引擎实现 VWAP 拆单算法。首先说明了算法思想;然后模块化介绍了系统的功能;其次详细介绍了系统的实现流程和代码,其中最复杂的是监视器定义的部分,详细阐述了各个函数之间的调用关系;最后将系统的拆单过程,在结果检视部分使用 Dashboard 进行展示。
4. 总结
本文基于 CEP 引擎构建了一套完整的算法拆单调度框架。通过事件驱动架构和动态事件监听器来实现订单下达、拆单的监听与处理。同时利用键值内存表缓存实时行情快照,为子单价格计算提供毫秒级响应支持。在功能实现上,文中提供了了 TWAP 和 VWAP 拆单算法实现 Demo (随机浮动参数避免市场预测) ,并通过可视化监控面板进行了实时展示订单状态等信息。