共计 7958 个字符,预计需要花费 20 分钟才能阅读完成。
本系列文章将会介绍如何应用 DolphinDB 优雅而高效的实现量化交易策略回测。本文将介绍在华尔街广泛应用的多因子 Alpha 策略的回测。多因子模型是量化交易选股中最重要的一类模型,基本思路是找到某些和回报率最相干的指标,并依据这些指标,构建股票投资组合(做多正相干的股票,做空负相关的股票)。多因子模型中,独自一个因子的个股权重个别实现多空平衡(市场中性),没有裸露市场危险的头寸(beta 为 0,所以称之为 alpha 策略),能实现相对收益。多个因子之间互相正交,不便策略配置,实现回报和危险的最优控制。另外,相比于套利策略(通常能够实现更高的 sharpe ratio,然而 scale 不好),多因子 alpha 策略有很好的 scale,能够配置大量的资金。多因子 Alpha 策略在对冲基金中的应用十分广泛。
1. 生成因子
本文的重点是实现多因子 Alpha 策略的回测框架。因子不是重点,这部分通常由金融工程师或策略分析师来实现。为了不便大家了解,文章以动量因子、beta 因子、规模因子和稳定率因子 4 个罕用的危险因子为例,介绍如何在 DolphinDB database 中实现多因子回测。
输出数据表 inData 蕴含 6 个字段:sym(股票代码), date(日期), close(收盘价), RET(日回报), MV(市值), VOL(交易量)
def genSignals(inData){USstocks = select sym, date, close, RET, MV from inData where weekday(date) between 1:5, close>5, VOL>0, MV>100000 order by sym, date
update USstocks set prevMV=prev(MV), cumretIndex=cumprod(1+RET), signal_size=-sqrt(MV), signal_vol=-mstd(RET, 21)*sqrt(252) context by sym
update USstocks set mRet = wavg(RET, prevMV) context by date
update USstocks set signal_mom = move(cumretIndex,21)/move(cumretIndex,252)-1, signal_beta=mbeta(RET, mRet, 63) from USstocks context by sym
return select sym, date, close, RET as ret, signal_size, signal_beta, signal_vol, signal_mom from USstocks where date>=1991.01.01
}
DolphinDB 函数阐明:
_abs:_取绝对值。
_prev:_把向量中的所有元素向右挪动一个地位。
_cumprod:_计算累计乘积。
_sqrt:_计算平方根。
_mstd(X, k):_计算挪动标准差。
_wavg(X, k):_计算加权平均数。
_move(X, k):_如果 k 为负数,则把向量的所有元素向右挪动 k 个地位,如果 k 为正数,则把向量的所有元素向左挪动 k 个地位。
_mbeta(X, Y, k):_计算一般最小二乘回归的系数预计。
genSignals 函数阐明:
首先数据过滤,抉择市值较高的股票在交易日中的数据。接着应用过滤后的数据计算 4 个危险因子:
- 规模因子(signal_size):MV 的平方根的相反数
- 稳定率因子(signal_vol):过来一个月的股价稳定率的相反数
- 动量因子(signal_mom):过来 12 个月(去除最近一个月)的动量因子
- beta 因子(signal_beta):利用过来三个月的数据计算个股跟市场的 beta
2. 回测框架
多因子 Alpha 策略的回测框架蕴含 3 个局部。首先是在每个历史周期上,生成每个股票在每个策略上的权重。一个历史周期上的所有仓位能够成为一个 tranche。而后依据 tranche 的持有工夫,生成每一个股票在每一个 tranche 的每一个策略上每一天的仓位和盈亏。最初统计分析每个策略和所有策略的业绩。
2.1 计算历史周期的投资仓位
首先定义一个函数 formPeriodPort 计算一个周期(一天)的股票仓位。而后应用并行计算取得历史上每一个周期的投资仓位。
2.1.1 计算一天的股票投资组合
这一步的输出是每一个股票在不同因子上的值,输入是每一个股票在每一个因子上的投资权重。股票权重要满足两个条件:(1)一个因子中所有股票的权重和为零,也就是说多空平衡。(2)不同因子之间互相正交,也就是说第 i 个因子的权重 wi 和第 j 个因子的值 sj 的内积为 0(i<>j)。为了实现上述指标,咱们引入了因子矩阵(矩阵的每一列示意一个因子,每一行示意一个股票),并且将单位因子(所有元素均为 1)增加到因子矩阵中。
实际中,还须要思考的一个问题是,去除权重较小的股票。一个股票池有几千个股票,大部分的股票取得的权重很小,简直能够疏忽。咱们定义了一个嵌套函数 f 来调整单个因子中股票的权重。
函数 formPeriodPort 的输出参数有 3 个:
- signals 是由 genSignals 函数生成的数据表,蕴含 8 个字段:股票代码、日期、收盘价格、回报率和 4 个因子。
- signalNames 是所有因子的名称,用向量示意。
- stockPercentile 用于管制股票的数量。
函数的输入是一个数据表,存储一天的股票投资组合,包含 4 个字段:tranche, sym, signalIdx, exposure。
def formPeriodPort(signals, signalNames, stockPercentile){stockCount = signals.size()
signalCount = signalNames.size()
tranche = signals.date.first()
//demean all signals and add a unit column to the signal matrix
sigMat = matrix(take(1, stockCount), each(x->x - avg(x), signals[signalNames]))
//form weight matrix.
transSigMat = sigMat.transpose()
weightMat = transSigMat.dot(sigMat).inv().dot(transSigMat).transpose()[1:]
/* form exposures. allocate two dollars on each signal, one for long and one for short
trim small weights. In practice, we don't want to trade too many stocks */
f = def(sym, tranche, stockPercentile, signalVec, signalIdx){t = table(sym, signalVec as exposure, iif(signalVec > 0, 1, -1) as sign)
update t set exposure = exposure * (abs(exposure) < percentile(abs(exposure), stockPercentile)) context by sign
update t set exposure = exposure / sum(exposure).abs() context by sign
return select tranche as tranche, sym, signalIdx as signalIdx, exposure from t where exposure != 0
}
return loop(f{signals.sym, tranche, stockPercentile}, weightMat, 1..signalCount - 1).unionAll(false)
}
DolphinDB 函数阐明:
_size:_返回向量中元素的个数
_first:_返回第一个元素
_matrix:_构建矩阵
_transpose:_矩阵转置
dot:矩阵或向量内积
_inv:_矩阵求逆
_iif(condition, trueResult, falseResult):_如果满足条件 condition,则返回 trueResult,否则返回 falseResult。它相当于对每个元素别离运行 if…else 语句。
_loop(func,args):_高价模板函数,把函数 func 利用到参数 args 的每一个元素上,并将后果汇总到一个元组中。如果 args 蕴含三个 k 个参数,每个参数的长度是 n,那么 loop 将运行 n 次。
_unionAll:_合并多个表
2.1.2 计算过来每天的股票投资组合
回测时应用 的数据量十分宏大,因而咱们把数据放到内存的分区数据库中,而后应用并行计算。如果想要理解更多对于分区数据库的内容,能够参考 DolphinDB 分区数据库教程。咱们把 genSignals 函数生成的数据保留到分区表 partSignals 中,一个分区示意一天。接着,创立一个分区表 ports,用于保留计算出来的股票投资组合,一个分区示意一年。而后,应用 map-reduce 函数,把 formPeriodPort 函数利用到每一天,把每个后果合并到分区表 ports 中。
def formPortfolio(signals, signalNames, stockPercentile){dates = (select count(*) from signals group by date having count(*)>1000).date.sort()
db = database("", VALUE, dates)
partSignals = db.createPartitionedTable(signals, "signals", `date).append!(signals)
db = database("", RANGE, datetimeParse(string(year(dates.first()) .. (year(dates.last()) + 1)) +".01.01","yyyy.MM.dd"))
symType = (select top 10 sym from signals).sym.type()
ports = db.createPartitionedTable(table(1:0, `tranche`sym`signalIdx`exposure, [DATE,symType,INT,DOUBLE]), "", `tranche)
return mr(sqlDS(<select * from partSignals>), formPeriodPort{,signalNames,stockPercentile},,unionAll{,ports})
}
DolphinDB 函数阐明:
_sort:_把向量中的元素排序
_database(directory, [partitionType], [partitionScheme], [locations]):_创立数据库。如果 directory 为空,则创立内存数据库。
_createPartitionedTable(dbHandle, table, [tableName], partitionColumns):_在数据库中创立分区表。
_datatimeParse(X, format):_把字符串转换成 DolphinDB 中工夫类型数据。
_unionAll:_合并表
_type:_返回数据类型的 ID。
_mr(ds, mapFunc, [reduceFunc], [finalFunc], [parallel=true]):_map-reduce 函数。
2.2 计算仓位和盈亏
这一步的工作是依据持有的仓位以及持有工夫,生成每一个股票在每一个 tranche 的每一个因子上每一天的仓位和盈亏。首先定义一个嵌套函数来 f 来计算局部股票投资仓位的盈亏,接着把嵌套函数利用到所有股票投资组合(应用 mr 函数),计算所有股票投资组合的盈亏,并把后果保留到分区表 pnls 中。
函数 caclStockPnL 的输出参数包含:
- ports: 每一天的投资组合表,包含 4 个字段 tranche, sym, signalIdx, exposure
- dailyRtn:股票每天的回报表,包含 3 个字段 date, sym, ret
- holdingDays: 股票持有的天数
函数的输入是股票的盈亏明细表,包含字段 8 个字段 date, sym, signalIdx, tranche, age, ret, exposure, pnl
def calcStockPnL(ports, dailyRtn, holdingDays){ages = table(1..holdingDays as age)
dates = sort exec distinct(tranche) from ports
dictDateIndex = dict(dates, 1..dates.size())
dictIndexDate = dict(1..dates.size(), dates)
lastDaysTable = select max(date) as date from dailyRtn group by sym
lastDays = dict(lastDaysTable.sym, lastDaysTable.date)
// define a anonymous function to calculate the pnl for a part of the porfolios.
f = def(ports, dailyRtn, holdingDays, ages, dictDateIndex, dictIndexDate,lastDays){pos = select dictIndexDate[dictDateIndex[tranche]+age] as date, sym, signalIdx, tranche, age, take(0.0,size age) as ret, exposure, take(0.0,size age) as pnl from cj(ports,ages) where isValid(dictIndexDate[dictDateIndex[tranche]+age]), dictIndexDate[dictDateIndex[tranche]+age]<=lastDays[sym]
update pos set ret = dailyRtn.ret from ej(pos, dailyRtn,`date`sym)
update pos set exposure = exposure*cumprod(1+ret) from pos context by tranche, signalIdx, sym
update pos set pnl = exposure*ret/(1+ret)
return pos
}
// calculate pnls for all portfolios and save the result to a partitioned in-memory table pnls
db = database("", RANGE, datetimeParse(string(year(dates.first()) .. (year(dates.last()) + 1)) +".01.01","yyyy.MM.dd"))
symType = (select top 10 sym from ports).sym.type()
modelPnls = table(1:0, `date`sym`signalIdx`tranche`age`ret`exposure`pnl, [DATE,symType,INT,DATE,INT,DOUBLE,DOUBLE,DOUBLE])
pnls = db.createPartitionedTable(modelPnls, "", `tranche)
return mr(sqlDS(<select * from ports>), f{,dailyRtn,holdingDays,ages,dictDateIndex, dictIndexDate,lastDays},,unionAll{,pnls})
}
DolphinDB 函数阐明:
_dict(key, value):_创立字典。
_cj(leftTable, rightTable):_穿插连贯两个表。
_isValid:_查看元素是否为 NULL。如果不是 NULL,则返回 1,如果是 NULL,则返回 0。
_ej(leftTable, rightTable, matchingCols, [rightMatchingCols]):_等值连贯两个表。
3. 运行实例
咱们以美国股市为例,运行多因子 Alpha 策略回测。输出的股票日数据表 USPrices 蕴含 6 个字段:sym(股票代码), date(日期), close(收盘价), RET(日回报), MV(市值)和 VOL(交易量)。
// 加载数据
USPrices = ...
holdingDays = 5
stockPercentile = 20
signalNames = `signal_mom`signal_vol`signal_beta`signal_size
// 生成因子
signals=genSignals(USPrices)
// 计算每天的股票投资组合
ports = formPortfolio(signals, signalNames, stockPercentile)
// 计算盈亏
dailyRtn = select sym,date,ret from signals
pos = calcStockPnL(ports, dailyRtn, holdingDays)
// 绘制四个因子的累计盈亏走势图
pnls = select sum(pnl) as pnl from pos group by date, signalIdx
factorPnl = select pnl from pnls pivot by date, signalIdx
plot(each(cumsum,factorPnl[`C0`C1`C2`C3]).rename!(signalNames), factorPnl.date, "The Cumulative Pnl of All Four Signals")
// 绘制动量因子不同持仓日的累计盈亏走势图
pnls = select sum(pnl) as pnl from pos where signalIdx=0 group by date, age
momAgePnl = select pnl from pnls pivot by date, age
plot(each(cumsum,momAgePnl[`C1`C2`C3`C4`C5]).rename!(`C1`C2`C3`C4`C5), momAgePnl.date)
4 个因子的累计盈亏走势图
动量因子的累计盈亏走势图
DolphinDB 尽管是一个通用的分布式时序数据库,但因为内置极其高效的多范式编程语言,用于量化交易,开发效率十分高。下面的多因子回测框架,仅用了 3 个自定义函数,50 余行代码。DolphinDB 的运行效率更是惊人,对美国股市 25 年中市值较高的股票按日进行回测,最初产生的盈亏明细表蕴含 1 亿余条记录。如此简单的计算量,在单机(4 核)上执行耗时仅 50 秒。
4. 探讨
后面的回测框架,仅仅解决了多因子策略的一部分问题,也就是说单个因子中股票的配置。咱们还有两个重要的问题须要解决:(1)多个因子之间,如何配置权重,均衡投资的回报和危险。(2)一个新的因子有没有带来额定的 Alpha,换句话说,一个新的因子是不是能够有曾经存在的多个因子来示意,如果能够,那么这个新因子可能没有存在的必要。下一篇文章,咱们会介绍如何应用 DolphinDB 来答复下面两个问题。