从计算、建模到回测:因子挖掘的最佳实践

从计算、建模到回测:因子挖掘的最佳实践_第1张图片

量化投资与机器学习微信公众号,是业内垂直于量化投资、对冲基金、Fintech、人工智能、大数据等领域的主流自媒体。公众号拥有来自公募、私募、券商、期货、银行、保险、高校等行业30W+关注者,荣获2021年度AMMA优秀品牌力、优秀洞察力大奖,连续2年被腾讯云+社区评选为“年度最佳作者”。

前言

因子挖掘是量化交易的基础。除传统的基本面因子外,从中高频行情数据中挖掘有价值的因子,并进一步建模和回测以构建交易系统,是一个量化团队的必经之路。金融或者量化金融是一个高度市场化、多方机构高度博弈的领域。因子的有效时间会随着博弈程度的加剧而缩短,如何使用更加高效的工具和流程,更快的找到新的有效的因子,是每一个交易团队必须面对的问题。

交易团队用于因子挖掘的常见技术栈有几个大的类别:

  • 使用 python、matlab 等数据分析工具

  • 委托第三方开发有图形界面的因子挖掘工具

  • 使用 java、c++ 等编程语言自行开发挖掘工具

  • 在 DolphinDB 等专业工具上进行二次开发

之前,公众号跟大家分享过一个超高性能分布式时序数据库神器:DolphinDB

从计算、建模到回测:因子挖掘的最佳实践_第2张图片

DolphinDB:金融高频因子流批统一计算神器!


DolphinDB 作为分布式计算、实时流计算及分布式存储一体化的高性能时序数据库,在因子的存储、计算、建模、回测和实盘交易等场景中有着得天独厚的优势。

内置的多范式编程语言(函数式,命令式,向量式、SQL式),可以帮助研发人员高效开发不同风格的因子。

超过1400个性能高效的内置计算函数,尤其是优化过的窗口处理方面的内置算子,大大缩短了因子计算的延时。

同时,DolphinDB 自带的数据回放和流式增量计算引擎可以方便地解决因子挖掘中研发和生产一体化的问题。DolphinDB 的分布式存储和计算框架,天生便于解决工程中的可靠性、扩展性等问题。

通过部署 DolphinDB 单机或集群环境,用户可以快速地处理 GB 级别甚至 PB 级别的海量数据集,日级、分钟级、快照和逐笔委托数据均能高效响应。

下文中,将基于国内 A 股市场各个频率的数据,根据批量因子计算、实时因子计算、多因子建模、因子库存储规划、因子计算工程化等各个场景的实操演练,以及针对不同方案的对比分析,为大家介绍在 DolphinDB 中进行因子计算的最佳实践。

这里给大家推荐一下,5月19日(周四)晚7点,DolphinDB CEO 周小华博士将在线上开讲,通过日频因子、分钟频因子、快照因子、逐笔因子四个实际案例,为大家详细介绍因子挖掘的最佳实践。干货满满!快来报名吧~

长按下方海报识别二维码即可参与活动!

从计算、建模到回测:因子挖掘的最佳实践_第3张图片

1、测试数据集

本文的因子计算基于三类国内 A 股行情数据集:逐笔数据、快照数据和 K 线数据(分钟 K 线和日 K 线)。快照数据以两种形式存储:(1)各档数据分别存储为一列;(2)用 array vector (DolphinDB中一种特殊的向量,用于存储可变长度的二维数组)将所有档位的数据存储为一列。

从计算、建模到回测:因子挖掘的最佳实践_第4张图片


2、投研阶段的因子计算

在投研阶段,会通过历史数据批量计算生成因子。通常,推荐研究员将每一种因子的计算都封装成自定义函数。根据因子类型和使用者习惯的不同,DolphinDB 提供了面板和 SQL 两种计算方式。

2.1 面板数据模式

面板数据(panel data)是以时间为索引,标的为列,指标作为内容的一种数据载体,它非常适用于以标的集合为单位的指标计算,将数据以面板作为载体,可以大大简化脚本的复杂度,通常最后的计算表达式可以从原始的数学公式中一对一的翻译过来。除此之外,可以充分利用 DolphinDB 矩阵计算的高效能。

在因子计算中,面板数据通常可以通过 panel 函数,或者 exec 搭配 pivot by 得到,具体样例如下表:每一行是一个时间点,每一列是一个股票。

000001     000002  000003  000004  ...
                        --------- ---------- ------- ------ ---
2020.01.02T09:29:00.000|3066.336 3212.982  257.523 2400.042 ...
2020.01.02T09:30:00.000|3070.247  3217.087 258.696 2402.221 ...
2020.01.02T09:31:00.000|3070.381 3217.170  259.066 2402.029 ...

在面板数据上,由于是以时间为索引,标的为列,因子可以方便地在截面上做各类运算。DolphinDB 包含 row 系列函数以及各类滑动窗口函数,在下面两个因子计算例子中,原本复杂的计算逻辑,在面板数据中,可以用一行代码轻松实现。

  • Alpha 1 因子计算中,下例使用了 rowRank 函数,可以在面板数据中的每一个时间截面对各标的进行排名;iif 条件运算,可以在标的向量层面直接筛选及计算;mimax 及 mstd 等滑动窗口函数也是在标的层面垂直计算的。因此,在面板计算中合理应用 DolphinDB 的内置函数,可以从不同维度智慧计算。

//alpha 1 
//Alpha#001公式:rank(Ts_ArgMax(SignedPower((returns<0?stddev(returns,20):close), 2), 5))-0.5


@state
def alpha1TS(close){
  return mimax(pow(iif(ratios(close) - 1 < 0, mstd(ratios(close) - 1, 20),close), 2.0), 5)
}


def alpha1Panel(close){
  return rowRank(X=alpha1TS(close), percent=true) - 0.5
}


input = exec close from loadTable("dfs://k_minute","k_minute") where date(tradetime) between 2020.01.01 : 2020.01.31 pivot by tradetime, securityid
res = alpha1Panel(input)
  • Alpha 98 因子计算中,同时使用了三个面板数据,分别是 vwap, open 和 vol。不仅各矩阵内部运用了 rowRank 函数横向截面运算以及m系列垂直滑动窗口计算,矩阵之间也进行了二元运算。用一行代码解决了多维度的复杂的嵌套计算逻辑。

//alpha 98
//Alpha #98计算公式:
(rank(decay_linear(correlation(vwap, sum(adv5, 26.4719), 4.58418), 7.18088)) -
rank(decay_linear(Ts_Rank(Ts_ArgMin(correlation(rank(open), rank(adv15), 20.8187), 8.62571), 6.95668), 8.07206)))


def prepareDataForDDBPanel(raw_data, start_time, end_time){
  t = select tradetime,securityid, vwap,vol,open from raw_data where date(tradetime) between start_time : end_time
  return dict(`vwap`open`vol, panel(t.tradetime, t.securityid, [t.vwap, t.open, t.vol]))
}


@state
def alpha98Panel(vwap, open, vol){
  return rowRank(X = mavg(mcorr(vwap, msum(mavg(vol, 5), 26), 5), 1..7),percent=true) - rowRank(X=mavg(mrank(9 - mimin(mcorr(rowRank(X=open,percent=true), rowRank(X=mavg(vol, 15),percent=true), 21), 9), true, 7), 1..8),percent=true)
}


raw_data = loadTable("dfs://k_minute","k_day")
start_time = 2020.01.01
end_time = 2020.12.31
input = prepareDataForDDBPanel(raw_data, start_time, end_time)
timer alpha98DDBPanel = alpha98Panel(input.vwap, input.open, input.vol)

2.2 SQL 模式

DolphinDB 在存储和计算框架上都是基于列式结构,表中的一个列可以直接作为一个向量化函数的输入参数。因此如果一个因子的计算逻辑只涉及股票自身的时间序列数据,不涉及多个股票横截面上的信息,可以直接在 SQL 中按股票分组,然后在 select 中调用因子函数计算每个股票在一段时间内的因子值。如果数据在数据库中本身是按股票分区存储的,那么可以非常高效地实现数据库内并行计算。

def sum_diff(x, y){
    return (x-y)\(x+y)
}


@state
def factorDoubleEMA(price){
    ema_2 = ema(price, 2)
    ema_4 = ema(price, 4)
    sum_diff_1000 = 1000 * sum_diff(ema_2, ema_4)
    return ema(sum_diff_1000, 2) - ema(sum_diff_1000, 3)
}


res = select tradetime, securityid, `doubleEMA as factorname, factorDoubleEMA(close) as val from loadTable("dfs://k_minute","k_minute") where  tradetime between 2020.01.01 : 2020.01.31 context by securityid

上面的例子中定义了一个因子函数 factorDoubleEMA,只需要用到股票的价格序列信息。在 SQL 中通过 context by 子句按股票代码分组,然后调用factorDoubleEMA函数,计算每个股票的因子序列。值得注意的是,context by 是 DolphinDB SQL 对 group by 的扩展,是 DolphinDB 特有的 SQL 语句。group by 只适用于聚合计算,也就是说输入长度为n,输出长度是1;context by 适用于向量计算,输入长度是n,输出长度也是n。另外因子函数 factorDOubleEMA 除了可以接受一个向量作为输入,也可以接受一个面板数据作为输入。这也是DolphinDB所强调的,因子函数的粒度尽可能细,这样可以应用于很多场景。

3、不同频率的因子开发举例

不同频率数据的因子,有着不同的特点。本章节将分别举例分钟频、日频、快照、逐笔数据的特点因子,阐述不同频率数据计算因子的最佳实践。

3.1 分钟级和日级数据

日级数据的计算,通常是涉及多个截面的复杂计算,在上面的章节中已展现。对于稍简单的计算,则与分钟级数据的因子相似。

针对分钟级数据,下面的例子是日内收益率偏度的因子计算,对于这类只涉及表内字段的计算,通常使用 SQL 模式,配合 group by 语句将计算分组:

defg dayReturnSkew(close){
  return skew(ratios(close))  
}


minReturn = select `dayReturnSkew as factorname, dayReturnSkew(close) as val from loadTable("dfs://k_minute_level", "k_minute") where date(tradetime) between 2020.01.02 : 2020.01.31 group by date(tradetime) as tradetime, securityid


#output
tradetime  securityid factorname    val               
---------- ---------- ------------- -------
2020.01.02 000019     dayReturnSkew 11.8328
2020.01.02 000048     dayReturnSkew 11.0544
2020.01.02 000050     dayReturnSkew 10.6186

3.2 基于快照数据的有状态因子计算

有状态的因子,意为因子的计算需要基于之前的计算结果,如一般的滑动窗口计算,聚合计算等,都是有状态的因子计算。

下例 flow 这个自定义函数中,参数为四个列字段,运用 mavg 滑动平均函数以及 iif 条件运算函数,可以直接在SQL中得到因子结果:

@state
def flow(buy_vol, sell_vol, askPrice1, bidPrice1){
        buy_vol_ma = round(mavg(buy_vol, 5*60), 5)
        sell_vol_ma = round(mavg(sell_vol, 5*60), 5)
        buy_prop = iif(abs(buy_vol_ma+sell_vol_ma) < 0, 0.5 , buy_vol_ma/ (buy_vol_ma+sell_vol_ma))
        spd = askPrice1 - bidPrice1
        spd = iif(spd < 0, 0, spd)
        spd_ma = round(mavg(spd, 5*60), 5)
        return iif(spd_ma == 0, 0, buy_prop / spd_ma)
}


res_flow = select TradeTime, SecurityID, `flow as factorname, flow(BidOrderQty[1],OfferOrderQty[1], OfferPrice[1], BidPrice[1]) as val from loadTable("dfs://LEVEL2_Snapshot_ArrayVector","Snap") where date(TradeTime) <= 2020.01.30 and date(TradeTime) >= 2020.01.01 context by SecurityID


# output sample
TradeTime               SecurityID factorname val              
----------------------- ---------- ---------- -----------------
2020.01.22T14:46:27.000 110065     flow       3.7587
2020.01.22T14:46:30.000 110065     flow       3.7515
2020.01.22T14:46:33.000 110065     flow       3.7443
...

3.3 快照数据的多档赋权无状态因子计算

计算 Level 2的多档快照数据,传统的方式是将多档量价数据存储成为多个列, 再将多档挂单或者报价用 matrix 转换与权重做计算。更推荐的做法是,将多档数据存储为 array vector,仍旧可以用原来的自定义函数,但是资源消耗包括效率都有提升。下面的例子是计算多档报价的权重偏度因子,使用 array vector 后计算时间从 4 秒缩短到 2 秒。

def mathWghtCovar(x, y, w){
  v = (x - rowWavg(x, w)) * (y - rowWavg(y, w))
  return rowWavg(v, w)
}


@state
def mathWghtSkew(x, w){
  x_var = mathWghtCovar(x, x, w)
  x_std = sqrt(x_var)
  x_1 = x - rowWavg(x, w)
  x_2 = x_1*x_1
  len = size(w)
  adj = sqrt((len - 1) * len) \ (len - 2)
  skew = rowWsum(x_2, x_1) \ (x_var * x_std) * adj \ len
  return iif(x_std==0, 0, skew)
}


//weights:
w = 10 9 8 7 6 5 4 3 2 1


//权重偏度因子:
resWeight =  select TradeTime, SecurityID, `mathWghtSkew as factorname, mathWghtSkew(BidPrice, w)  as val from loadTable("dfs://LEVEL2_Snapshot_ArrayVector","Snap")  where date(TradeTime) = 2020.01.02 map
resWeight1 =  select TradeTime, SecurityID, `mathWghtSkew as factorname, mathWghtSkew(matrix(BidPrice0,BidPrice1,BidPrice2,BidPrice3,BidPrice4,BidPrice5,BidPrice6,BidPrice7,BidPrice8,BidPrice9), w)  as val from loadTable("dfs://snapshot_SH_L2_TSDB", "snapshot_SH_L2_TSDB")  where date(TradeTime) = 2020.01.02 map


#output
TradeTime               SecurityID factorname val               
----------------------- ---------- ---------- ------
...
2020.01.02T09:30:09.000 113537     array_1    -0.8828 
2020.01.02T09:30:12.000 113537     array_1    0.7371 
2020.01.02T09:30:15.000 113537     array_1    0.6041 
...

3.4 基于快照数据的分钟聚合

投研中经常需要基于快照数据聚合分钟线的 OHLC ,下例就是这一场景中的通用做法:

//基于快照因子的分钟聚合OHLC,vwap
tick_aggr = select first(LastPx) as open, max(LastPx) as high, min(LastPx) as low, last(LastPx) as close, sum(totalvolumetrade) as vol,sum(lastpx*totalvolumetrade) as val,wavg(lastpx, totalvolumetrade) as vwap from loadTable("dfs://LEVEL2_Snapshot_ArrayVector","Snap") where date(TradeTime) <= 2020.01.30 and date(TradeTime) >= 2020.01.01 group by SecurityID, bar(TradeTime,1m)

3.5 逐笔数据

逐笔数据量较大,一般会针对成交量等字段进行计算,下面的例子计算了每天主买成交量占全部成交量的比例,同样使用 SQL 模式,发挥库内并行计算的优势,并使用 csort 语句用来对组内数据按照时间顺序排序:

 
   
@state
def buyTradeRatio(buyNo, sellNo, tradeQty){
    return cumsum(iif(buyNo>sellNo, tradeQty, 0))\cumsum(tradeQty)
}


factor = select TradeTime, SecurityID, `buyTradeRatio as factorname, buyTradeRatio(BuyNo, SellNo, TradeQty) as val from loadTable("dfs://tick_SH_L2_TSDB","tick_SH_L2_TSDB") where date(TradeTime)<2020.01.31 and time(TradeTime)>=09:30:00.000 context by SecurityID, date(TradeTime) csort TradeTime


#output
TradeTime           SecurityID factorname       val              
------------------- ---------- ---------------- ------
2020.01.08T09:30:07 511850     buyTradeRatio    0.0086
2020.01.08T09:30:31 511850     buyTradeRatio    0.0574
2020.01.08T09:30:36 511850     buyTradeRatio    0.0569
...

4、生产环境的流式因子计算

在生产环境中,DolphinDB 提供了实时流计算框架。在流计算框架下,用户在投研阶段封装好的基于批量数据开发的因子函数,可以无缝投入交易和投资方面的生产程序中,这就是通常所说的批流一体。

使用流批一体可以加速用户的开发和部署。同时流计算框架还在算法的路径上,做了极致的优化,在具有高效开发的优势的同时,又兼顾了计算的高效性能。在这一章中,将会基于实际的状态因子案例,展示实时流计算的使用方法。

DolphinDB 流计算解决方案的核心部件是流计算引擎和流数据表。流计算引擎用于时间序列处理、横截面处理、窗口处理、表关联、异常检测等操作。流数据表可以看作是一个简化版的消息中间件,或者说是消息中间件中的一个主题(topic),可以往其发布(publish)数据,也可以从其订阅(subscribe)数据。

流计算引擎和流数据表均继承于 DolphinDB 的数据表(table),因此都可以通过 append! 函数往其注入数据。流计算引擎的输出也是数据表的形式,因此多个计算引擎可以跟搭积木一样自由组合,形成流式处理的流水线。

从计算、建模到回测:因子挖掘的最佳实践_第5张图片

4.1 流式增量计算

金融方面的原始数据和计算指标,在时间上通常有延续性的关系。以最简单的五周期移动均线 mavg(close,5) 为例,当新一个周期的数据传入模型时,可以将之前最远的第五周期值从 sum 中减出,再把最新一个周期的值加入 sum ,这样就不必每个周期只更新一个值时都重算一遍 sum 。这种增量计算是流计算的核心,可以大大降低实时计算的延时。DolphinDB内置了大量量化金融中需要用到的基本算子,并为这些算子实现了高效的增量算法。不仅如此,DolphinDB还支持自定义函数的增量实现。在前一章节中,部分自定义的因子函数加了修饰符 @state,表示该函数支持增量计算。

4.1.1主买成交量占比因子的流式处理

第三章的逐笔数据因子的例子展示了主买成交量占比因子(buyTradeRatio)的批量实现方式。此处演示如何使用响应式状态引擎(reactive state engine)来实现该因子的流式增量计算。

@state
def buyTradeRatio(buyNo, sellNo, tradeQty){
    return cumsum(iif(buyNo>sellNo, tradeQty, 0))\cumsum(tradeQty)
}


tickStream = table(1:0, `SecurityID`TradeTime`TradePrice`TradeQty`TradeAmount`BuyNo`SellNo, [SYMBOL,DATETIME,DOUBLE,INT,DOUBLE,LONG,LONG])
result = table(1:0, `SecurityID`TradeTime`Factor, [SYMBOL,DATETIME,DOUBLE])
factors = <[TradeTime, buyTradeRatio(BuyNo, SellNo, TradeQty)]>
demoEngine = createReactiveStateEngine(name="demo", metrics=factors, dummyTable=tickStream, outputTable=result, keyColumn="SecurityID")

上述代码创建了一个名为 demo 的响应式状态引擎,SecurityID 作为分组键,输入的消息格式同内存表 tickStrea。需要计算的指标定义在 factors 中,其中1个是输入表中的原始字段 TradeTime,另一个是需要计算的因子的函数表示。输出到内存表 result,除了在 factors 中定义的指标外,输出结果还会添加分组键。请注意,自定义的因子函数跟批计算中的完全一致!创建完引擎之后,即可往引擎中插入几条数据,并观察计算结果。

4.1.2 大小单的流式处理

资金流分析是逐笔委托数据的一个重要应用场景。在实时处理逐笔数据时,大小单的统计是资金流分析的一个具体应用。大小单在一定程度上能反映主力、散户的动向。

但在实时场景中,大小单的生成有很多难点:

(1)大小单的计算涉及历史状态,如若不能实现增量计算,当计算下午的数据时,可能需要回溯有关这笔订单上午的数据,效率会非常低下。

(2)计算涉及至少两个阶段。在第一阶段需要根据订单分组,根据订单的累计成交量判断大小单,在第二阶段要根据股票来分组,统计每个股票的大小单数量及金额。

大小单是一个动态的概念,一个小单在成交量增加后可能变成一个大单。DolphinDB 的两个内置函数:dynamicGroupCumsum 和 dynamicGroupCumcount ,可以用于对动态组的增量计算。

 
   
@state
def factorSmallOrderNetAmountRatio(tradeAmount, sellCumAmount, sellOrderFlag, prevSellCumAmount, prevSellOrderFlag, buyCumAmount, buyOrderFlag, prevBuyCumAmount, prevBuyOrderFlag){
  cumsumTradeAmount = cumsum(tradeAmount)
  smallSellCumAmount, bigSellCumAmount = dynamicGroupCumsum(sellCumAmount, prevSellCumAmount, sellOrderFlag, prevSellOrderFlag, 2)
  smallBuyCumAmount, bigBuyCumAmount = dynamicGroupCumsum(buyCumAmount, prevBuyCumAmount, buyOrderFlag, prevBuyOrderFlag, 2) 
  f = (smallBuyCumAmount - smallSellCumAmount) \ cumsumTradeAmount
  return smallBuyCumAmount, smallSellCumAmount, cumsumTradeAmount, f
}


def createStreamEngine(result){
  tradeSchema = createTradeSchema()
  result1Schema = createResult1Schema()
  result2Schema = createResult2Schema()
  engineNames = ["rse1", "rse2", "res3"]
  cleanStreamEngines(engineNames)
  
  metrics3 = <[TradeTime, factorSmallOrderNetAmountRatio(tradeAmount, sellCumAmount, sellOrderFlag, prevSellCumAmount, prevSellOrderFlag, buyCumAmount, buyOrderFlag, prevBuyCumAmount, prevBuyOrderFlag)]>
  rse3 = createReactiveStateEngine(name=engineNames[2], metrics=metrics3, dummyTable=result2Schema, outputTable=result, keyColumn="SecurityID")
  
  metrics2 = <[BuyNo, SecurityID, TradeTime, TradeAmount, BuyCumAmount, PrevBuyCumAmount, BuyOrderFlag, PrevBuyOrderFlag, factorOrderCumAmount(TradeAmount)]>
  rse2 = createReactiveStateEngine(name=engineNames[1], metrics=metrics2, dummyTable=result1Schema, outputTable=rse3, keyColumn="SellNo")
  
  metrics1 = <[SecurityID, SellNo, TradeTime, TradeAmount, factorOrderCumAmount(TradeAmount)]>
  return createReactiveStateEngine(name=engineNames[0], metrics=metrics1, dummyTable=tradeSchema, outputTable=rse2, keyColumn="BuyNo")
}

自定义函数 factorSmallOrderNetAmountRatio 是一个状态因子函数,用于计算小单的净流入资金占总的交易资金的比例。createStreamEngine 创建流式计算引擎。DolphinDB 一共创建了3个级联的响应式状态引擎,后一个作为前一个的输出,因此从最后一个引擎开始创建。前两个计算引擎 rse1 和 rse2 分别以买方订单号(BuyNo)和卖方订单号(SellNo)作为分组键,计算每个订单的累计交易量,并以此区分是大单或小单。第三个引擎rse3把股票代码(SecurityID)作为分组键,统计每个股票的小单净流入资金占总交易资金的比例。下面输入一些样本数据来观察流计算引擎的运行。

 
   
result = createResultTable()
rse = createStreamEngine(result)
insert into rse values(`000155, 1000, 1001, 2020.01.01T09:30:00, 20000)
insert into rse values(`000155, 1000, 1002, 2020.01.01T09:30:01, 40000)
insert into rse values(`000155, 1000, 1003, 2020.01.01T09:30:02, 60000)
insert into rse values(`000155, 1004, 1003, 2020.01.01T09:30:03, 30000)


select * from result


SecurityID TradeTime           smallBuyOrderAmount smallSellOrderAmount totalOrderAmount factor
---------- ------------------- ------------------- -------------------- ---------------- ------
000155     2020.01.01T09:30:00 20000               20000                20000            0
000155     2020.01.01T09:30:01 60000               60000                60000            0
000155     2020.01.01T09:30:02 0                   120000               120000           -1
000155     2020.01.01T09:30:03 30000               150000               150000           -0.8

4.1.3 复杂因子Alpha #1流式计算的快捷实现

从前一个大小单的例子可以看到,有些因子的流式实现比较复杂,需要创建多个引擎进行流水线处理来完成。完全用手工的方式来创建多个引擎其实是一件耗时的工作。如果输入的指标计算只涉及一个分组键,DolphinDB提供了一个解析引擎 streamEngineParser 来解决此问题。下面以第三章3.1面板数据模式的alpha #1因子为例,展示 streamEngineParser 的使用方法。以下为核心代码。

@state
def alpha1TS(close){
  return mimax(pow(iif(ratios(close) - 1 < 0, mstd(ratios(close) - 1, 20),close), 2.0), 5)
}


def alpha1Panel(close){
  return rowRank(X=alpha1TS(close), percent=true) - 0.5
}


inputSchema = table(1:0, ["SecurityID","TradeTime","close"], [SYMBOL,TIMESTAMP,DOUBLE])
result = table(10000:0, ["TradeTime","SecurityID", "factor"], [TIMESTAMP,SYMBOL,DOUBLE])
metrics = <[SecurityID, alpha1Panel(close)]>
streamEngine = streamEngineParser(name="alpha1Parser", metrics=metrics, dummyTable=inputSchema, outputTable=result, keyColumn="SecurityID", timeColumn=`tradetime, triggeringPattern='keyCount', triggeringInterval=4000)

因子alpha1实际上包含了时间序列处理和横截面处理,需要响应式状态引擎和横截面引擎串联来处理才能完成。但这儿仅仅使用了 streamEngineParser 就创建了全部引擎,大大简化了创建过程。

前面三个例子展示了 DolphinDB 如何通过流计算引擎实现因子在生产环境中的增量计算。在传统的研究框架下,用户往往需要对同一个因子计算逻辑写两套代码,一套用于在历史数据上建模、回测,另外一套专门处理盘中传入的实时数据。值得注意的是,DolphinDB 的流式计算时直接使用了投研阶段生成的核心因子代码,这很好的解决了传统金融分析面临的批流一体问题。

除了三个例子中用到的响应式状态引擎(reactive state engine)和横截面引擎(cross sectional engine),DolphinDB 还提供了多种流数据处理引擎包括做流表连接的 asof join engine,equal join engine,lookup join engine,window join engine ,时间序列聚合引擎(time series engine),异常检测引擎(anomaly detection engine),会话窗口引擎(session window engine)等。

4.2 数据回放

前一节介绍了因子计算的批流一体实现方案,简单地说,就是一套代码(自定义的因子函数),两种引擎(批计算引擎和流计算引擎)。事实上,DolphinDB提供一种更为简洁的批流一体实现方案,那就是在历史数据建模时,通过数据回放,也用流引擎来实现计算。

在第三章中介绍了用 SQL 语句方式批处理计算 factorDoubleEMA 因子的例子,这里介绍如何使用流计算的方式回放数据,计算 factorDoubleEMA 的因子值。

//创建流引擎,并传入因子算法factorDoubleEMA
factors = <[TradeTime, factorDoubleEMA(close)]>
demoEngine = createReactiveStateEngine(name=engineName, metrics=factors, dummyTable=inputDummyTable, outputTable=resultTable, keyColumn="SecurityID")
  
//demo_engine订阅snapshotStreamTable流表
subscribeTable(tableName=snapshotSharedTableName, actionName=actionName, handler=append!{demoEngine}, msgAsTable=true)


//创建播放数据源供replay函数历史回放;盘中的时候,改为行情数据直接写入snapshotStreamTable流表
inputDS = replayDS(, `securityid, HASH,10)


def factorDoubleEMAMap(table){
return select tradetime, securityid, `doubleEMA as factorname, factorDoubleEMA(close) as val from table context by securityid map
}


res = mr(ds,factorDoubleEMAMap,,unionAll)

7.3.3 通过submitJob提交任务

之前的两种并行计算都是在前台执行的,并行度是由参数 localExecutors 设置。而有些作业可能很大,或者用户不想影响前台使用,此时可以通过 submitJob 提交任务。submitJob 的并行度由 maxBatchJobWorker 参数设置。由于后台作业之间是独立的,通常不需要返回到前端的任务都推荐用后台提交 submitJob 的形式。

仍旧以 dayReturnSkew 因子为例。通常需要将因子写入因子库表,此时可以将整一个过程提交几个后台作业去执行,而在客户端中,同时可以继续做其他计算。由于此例存入的因子库的分区是按月和因子名 VALUE 分区,故此时应按照月份去提交作业。这样既可以并行写入不会冲突,又可以将作业提交到后台,不影响前台提交其他任务。

 
   
def writeDayReturnSkew(dBegin,dEnd){
  dReturn = select `dayReturnSkew as factorname, dayReturnSkew(close) as val from loadTable("dfs://k_minute_level", "k_minute") where date(tradetime) between dBegin : dEnd group by date(tradetime) as tradetime, securityid
//写入因子库
loadTable("dfs://K_FACTOR_VERTICAL","factor_k").append!(dReturn)
  }


for (i in 0..11){
  dBegin = monthBegin(temporalAdd(2020.01.01,i,"M"))
  dEnd = monthEnd(temporalAdd(2020.01.01,i,"M"))
  submitJob("writeDayReturnSkew","writeDayReturnSkew_"+dBegin+"_"+dEnd, writeDayReturnSkew,dBegin,dEnd)
}

7.4 内存管理

在配置 DolphinDB 后端环境时,计算和事务的内存占用可在单节点的 ”dolphindb.cfg” 或集群的 cluster.cfg(以下简称“节点配置文件”)中,通过参数”maxMemSize“配置单节点最大占用内存。

在调试大任务量的计算完成后,可通过 undef 函数将变量赋值为 NULL,或者关闭 session 来及时释放变量的内存。

章节3.2中的例子,对半年的快照数据做操作,批处理方式的中间变量占用内存达到21GB,如果设置的内存小于21GB,则报Out of Memory错误。这种情况下可以将作业拆分后再提交写入。

更多有关内存管理的详细内容,请参阅DolphinDB内存管理教程。

7.5 权限管理

因子数据是非常重要的数据,并不是每一个用户都可以随意访问所有因子的,所以要对因子数据做好权限管理。DolphinDB database 提供了强大、灵活、安全的权限控制系统,可以满足因子库表级,函数视图级的管理。控制节点(controller)作为权限管理中心,使用RSA加密方式对用户关键信息进行加密。DolphinDB的权限管理主要有以下功能,可以方便研究员和运维人员安全有效的管理权限:

  • 提供用户和组角色,方便权限控制

  • 提供9种权限控制类别,适应各种场景

  • 丰富的权限控制函数

  • 函数视图兼顾保护数据隐私与提供分析结果

  • 对任务调度和流数据任务动态鉴权,保证系统安全

  • 使用RSA对用户关键信息加密

  • 支持SSO,简化登录,方便系统扩展

更多有关权限管理的详细内容,请参阅DolphinDB权限管理和安全教程。

7.6 任务管理

因子计算的任务通常分为全量计算所有因子任务、交互式单因子重算任务、所有因子增量计算任务这三种,本章会对每一种因子计算任务进行详细介绍。

因子任务可以通过以下三种方式执行:

  • 通过交互的方式执行。

  •  通过 submitJob   提交一个Job来执行。

  • 通过 scheduleJob   提交一个定时任务来进行周期性的执行。

7.6.1 全量计算

因子的全量跑批任务,通常是系统初始化因子数据时的一次性任务,或者较长周期进行一次的任务,这类任务可以通过单次触发或者定时任务(scheduleJob)的方式进行管理。

  • 单次触发的任务:建议通过submitJob 函数提交任务。通过submitJob 提交的任务,会提交到服务器的Job 队列中执行,不再受客户端影响。可以通过 getRecentJobs 观察任务是否完成。

 
   
// 通过summitjob进行提交
submitJob("batchTask","batchTask", bacthExeCute)
  • 周期性任务:如果计算的因子频率较低需要每天盘后或者其他周期定期全部重算一次,可以使用定时任务(ScheduleJob)的方式进行管理。

 
   
//设置一段时间每天执行
scheduleJob(jobId=`daily, jobDesc="Daily Job 1", jobFunc=bacthExeCute, scheduleTime=17:23m, startDate=2018.01.01, endDate=2018.12.31, frequency='D')

7.6.2 因子运维管理

在因子研发过程中,当碰到因子算法、参数调整的情况,需要对该因子进行重新计算,同时需要将计算的新的因子数据更新到数据库中,对于不同更新的频率的因子,通常有以下两种方式:

  • 因子的数据频率较高,因子的总数据量较大

因子的数据频率较高,数据量很大时,推荐在因子数据分区时拉长时间维度,以因子名进行VALUE分区。这样可以使每个因子的数据独立的保存在一个分区中,控制分区大小在一个合适的范围。当用户碰到因子重算的情况,便可以用dropPartition函数先删除这个因子所对应的分区数据,然后直接重算这个因子并保存到数据表中。

  • 因子的数据频率较低,因子的总数据量较小

当因子的数据频率较低,因子的总数据量较小时,如若将每个因子划分为独立的分区会使得每个分区特别小,而过小的分区可能会影响写入速度。这种情况下,可以按照因子 HASH 分区。使用 update! 来进行因子数据更新操作,或使用 upsert 来进行插入更新操作。此外,对于 TSDB 引擎,可以设置参数 keepDuplicates=LAST , 此时可以直接使用append!或者 tableInsert 插入数据,从而达到效率更高的更新数据的效果。

update! , upsert 以及 TSDB 引擎特殊设置下的直接 append! 覆盖数据,这三种更新操作都建议在数据量较小,且更新不频繁的情况下使用。对于需要大量因子重算的数据更新的场景,DolphinDB 推荐使用单因子独立分区的方式。当因子重算时先用dropPartition函数删除因子所在分区,再重算写入新因子入库。

总 结

用DolphinDB来进行因子的计算时,可选择面板和SQL两种方式来封装因子的核心逻辑。面板方式使用矩阵来计算因子,实现思路非常简练;而SQL方式要求投研人员使用向量化的思路进行因子开发。无论哪种方式,DolphinDB均支持批流一体的实现。DolphinDB内置了相关性和回归分析等计算工具,可分析因子的有效性,可对多因子建模。

在因子库的规划上,如果追求灵活性,建议采用单值纵表模型。如果追求效率和性能,推荐使用TSDB引擎,启用多值宽表模式,标的(股票代码)作为表的列。

最后,基于大部分团队的IT和投研相对独立的事实,给出了在代码管理上的工程化方案,投研团队通过模块和自定义函数封装核心因子业务逻辑,IT团队则维护框架代码。同时利用权限模块有效隔离各团队之间的数据访问权限。

从计算、建模到回测:因子挖掘的最佳实践_第17张图片

长按上方海报识别二维码即可参与活动

你可能感兴趣的:(大数据,数据库,python,java,人工智能)