數據庫交易回測系列二:多因子Alpha策略回測

本系列文章將會介紹如何使用DolphinDB優雅而高效的實現量化交易策略回測。本文將介紹在華爾街普遍應用的多因子Alpha策略的回測。多因子模型是量化交易選股中最重要的一類模型,基本思路是找到某些和回報率最相關的指標,並根據這些指標,構建股票投資組合(作多正相關的股票,作空負相關的股票)。多因子模型中,單獨一個因子的個股權重通常實現多空均衡(市場中性),沒有暴露市場風險的頭寸(beta爲0,因此稱之爲alpha策略),能實現絕對收益。多個因子之間相互正交,方便策略配置,實現回報和風險的最優控制。另外,相比於套利策略(一般能夠實現更高的sharpe ratio,可是scale很差),多因子alpha策略有很好的scale,能夠配置大量的資金。多因子Alpha策略在對衝基金中的使用很是廣泛。ios

1. 生成因子sql

本文的重點是實現多因子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函數說明:app

abs:取絕對值。框架

prev:把向量中的全部元素向右移動一個位置。編程語言

cumprod:計算累計乘積。分佈式

sqrt:計算平方根。ide

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)

661e44c41205e75a9db8a35b51bddbb3.png4個因子的累計盈虧走勢圖85f7a38c25c44c451793fc32e8e941cb.png動量因子的累計盈虧走勢圖

DolphinDB雖然是一個通用的分佈式時序數據庫,但由於內置極其高效的多範式編程語言,用於量化交易,開發效率很是高。上面的多因子回測框架,僅用了3個自定義函數,50餘行代碼。DolphinDB的運行效率更是驚人,對美國股市25年中市值較高的股票按日進行回測,最後產生的盈虧明細表包含1億餘條記錄。如此複雜的計算量,在單機(4核)上執行耗時僅50秒。


4. 討論

前面的回測框架,僅僅解決了多因子策略的一部分問題,也就是說單個因子中股票的配置。咱們還有兩個重要的問題須要解決:(1)多個因子之間,如何配置權重,平衡投資的回報和風險。(2)一個新的因子有沒有帶來額外的Alpha,換句話說,一個新的因子是否是能夠有已經存在的多個因子來表示,若是能夠,那麼這個新因子可能沒有存在的必要。下一篇文章,咱們會介紹如何使用DolphinDB來回答上面兩個問題。

相關文章
相關標籤/搜索