數據庫交易回測系列一:技術信號回測

本系列文章將會介紹如何使用DolphinDB進行交易回測。本文以移動平均線指標爲例,介紹如何在DolphinDB中實現技術信號回測。移動平均線指標(Moving average,簡稱MA)屬於趨勢指標。在金融分析領域,移動平均線是不可缺少的指標工具。除了指示趨勢,均線指標還能避免由於股價下跌錯失清倉的機會,減少收益的損失,及時止損,也能避免股價上漲錯失買入的實際,從而獲得更高的收益。


回測過程中,我們考慮兩種情況:不止損回測和止損回測。

數據表需要包含以下字段:

股票代碼:sym

日期:date

收盤價格:close

1. 定義MA信號

當短期均線大於長期均線時,我們認爲這是一個MA交易信號。

def maSignal(x, shortHorizon, longHorizon){
	signal = mavg(x, shortHorizon) > mavg(x, longHorizon)
	signal[0:min(x.size(), longHorizon - 1)] = NULL
	return signal
}

2. 不止損回測

我們定義的交易算法如下:

假設前一天的MA信號爲prevSignal,當天的MA信號爲signal。

(1)如果prevSignal=false,signal=true,那麼買入多頭頭寸(long position)。

(2)如果prevSignal=true,signal=false,那麼賣出空頭頭寸(short position)。

(3)如果不符合以上兩種情況,則保持與前一天相同的頭寸。

def backtest(t){
	t2 = select sym,date,close,prev(close) as prevClose,signal, prev(signal) as prevSignal from t context by sym
	update t2 set position=iif(prevSignal==false and signal==true, 1 ,iif(prevSignal==true and signal==false, -1, int())).prev().ffill() context by sym
	return select sym,date,close,signal,position,position*(close - prevClose) as pnl from t2 where isValid(position)
}

DolphinDB函數說明:

iif(condition, trueResult, falseResult):如果滿足條件condition,則返回trueResult,否則返回falseResult。它相當於if...else語句,但是語法上更加簡潔。

int():返回int類型的NULL值。

prev(x):把向量中的所有元素向右移動一個位置。

ffill(x):使用NULL值前的非NULL元素填充向量中的NULL值。

isValid():檢查每個元素是否爲NULL。如果爲NULL,返回0,否則返回1。


backtest 函數說明:

回測時首先整理數據,使用prev()函數把前一天的收盤價格prevClose和前一天的MA信號prevSignal與當天的數據對齊,便於計算。

接着,按照我們定義的交易算法,計算每個股票的頭寸position。position=1表示買入,position=-1表示賣出,position=NULL表示保持不變。

最後,使用position*(close - prevClose)計算盈虧pnl。

3. 止損回測

3.1 判斷止損點

首先,定義函數stoploss判斷是否需要止損。該函數返回布爾類型的向量。

def stoploss(ret, threshold){
	cumret = cumprod(1+ret)
	drawDown = 1 - cumret / cumret.cummax()
	firstCutIndex = at(drawDown >= threshold).first() + 1
	indicator = take(false, ret.size())
	if(isValid(firstCutIndex) and firstCutIndex < ret.size())
		indicator[firstCutIndex:] = true
	return indicator
}

DolphinDB內置函數說明:

cumprod:計算累計乘積。

cummax:計算累計最大值。

at(x):x是布爾表達式,找出符合條件x的元素的位置。

first:返回第一個元素。

take(X, k):返回包含k個x的向量。


stoploss 函數說明:

首先計算累計回報率cumret,接着計算當前回報率和累計最大回報率的回撤drawdown,當回撤drawdown大於等於預設閾值threshold時,則認爲應當止損,並記錄止損的起始位置firstCutIndex(由於到股市收盤時才知道是否需要止損或止盈,所以firstCutIndex要加1)。止損信號indicator的所有元素一開始設定爲全是false。如果止損的起始位置firstCutIndex不爲NULL,且不超過當前的數據量,則把止損信號indicator中從firstCutIndex開始到最後的所有元素設爲true,表示從firstCutIndex開始,都應當止損。


3.2 止損回測

回測時,將止損前後的盈虧進行對比 。

def backtest_stoploss(t, thresholdDrawDown){
	t2 = select sym,date,close,prev(close) as prevClose,signal, prev(signal) as prevSignal from t context by sym
	update t2 set position=iif(prevSignal==false and signal==true, 1 ,iif(prevSignal==true and signal==false, -1, int())).prev().ffill() context by sym
	update t2 set pnl = position*(close - prevClose), ret = (close - prevClose)/prevClose
	update t2 set stoplossInd = segmentby(stoploss{,thresholdDrawDown}, ret, position) context by sym
	return select sym,date,close,signal,position,stoplossInd,pnl * stoplossInd as pnl, pnl as nostoplossPnl from t2 where isValid(position)
}

DolphinDB函數說明:

segmentby(func, funcArgs, segment):把funcArgs分成多個組,並把函數func應用到每個組中。segment是一個向量,可以把它看作是分組方案,連續相同的元素爲一組。通過下面的例子我們可以更好地理解segmentby:

x=1 2 3 0 3 2 1 4 5
y=1 1 1 -1 -1 -1 1 1 1
segmentby(cumsum,x,y)

1 3 6 0 3 5 1 5 10

上面的例子中,y定義了3個分組:1 1 1、-1 -1 -1 和1 1 1,第一個分組的index是0-2,第二個分組的index是3-5,第三個分組的index是6-9。按照這個規則把x分成3組:1 2 3、0 3 2、1 4 5,並在每個分組中計算累計和。

stoploss{, thresholdDrawDown}這種表達方式是定義一個部分應用,用於固定stoploss的第二個參數thresholdDrawDown。


backtest_stoploss 函數說明:

前三行代碼和1.2大致相同,除了計算盈虧pnl之外,還計算了回報率ret,因爲stoploss函數需要ret作爲輸入。接着把每個股票的回報率ret按階段分組(position中的元素連續多個1表示持續買入,連續多個-1表示持續賣出,連續多個NULL表示持續不變),在每個階段分組中判斷是否需要止損,爲每隻股票生成止損信號stoplossInd。最後計算止損前後的盈虧,止損前的盈虧爲nostoplossPnl,止損後的盈虧爲pnl。

4. 統計信息

通常情況下,我們還需要分析盈虧的統計信息。通過下面的自定義函數calcPerformance可以計算盈虧的統計信息,比如累計盈虧cumpnl、平均盈虧avgpnl、盈虧天數days、盈虧的標準差std、最大回撤maxDrawdown等。返回的數據類型是字典。

def calcPerformance(pnl){
	result = dict(STRING, DOUBLE)
	result[`cumpnl]= pnl.sum()
	result[`avgpnl]= pnl.avg()
	result[`days] = pnl.size()
	result[`std]= pnl.std()
	result[`maxDrawdown] = (pnl.cumsum().cummax() - pnl.cumsum()).max()
	return result
}

5. 運行實例

我們使用美國股市從1998年到2016年股票的每日交易信息作爲數據集來進行測試。數據集共包含3474萬條記錄。

//數據導入和數據處理,產生stock數據表,包含sym, date, close三個字段
...

//計算每個股票每天的MA信號
t = select sym,date,close,maSignal(close, 50, 100) as signal from stock context by sym

情況一:不止損回測

//不止損回測
positions = backtest(t)

//計算盈虧並繪製盈虧走勢圖
dailyPnl = select sum(pnl) as pnl from positions group by date order by date
calcPerformance(dailyPnl.pnl)
plot(dailyPnl.pnl.cumsum() as cumulativePnl, dailyPnl.date, "Cumulative Pnl of All Stocks without Stop Loss Control")

//分析每隻股票的盈虧信息
select calcPerformance(pnl) as `cumpnl`avgpnl`days`std`maxDrawdown from result group by sym

sym	cumpnl	avgpnl	days	std	maxDrawdown
A	48.75	0.0108	4,513.	1.5895	106.55
AA	7.9625	0.0017	4,624.	1.131	119.75
...

fa888b7cd9684b837471cb0492facf07.png不止損回測所有股票的盈虧走勢圖


情況二:止損回測。我們把預設閾值設爲2.5%。

 //止損回測
positions = backtst_stoploss(t,0.025)

//計算盈虧並繪製盈虧走勢圖
dailyPnl = select sum(pnl) as pnl from positions group by date order by date
calcPerformance(dailyPnl.pnl)
plot(dailyPnl.pnl.cumsum() as cumulativePnl, dailyPnl.date, "Cumulative Pnl of All Stocks with Stop Loss Control")

//分析每隻股票的盈虧信息
select calcPerformance(pnl) as `cumpnl`avgpnl`days`std`maxDrawdown from result group by sym

sym	cumpnl	avgpnl	days	std	maxDrawdown
A	58.2775	0.0129	4,513.	1.5731	102.125
AA	20.47	0.0044	4,624.	1.1126	110.8125
...

39b7f958ffe3e628350fedf183c60d76.png止損回測所有股票的盈虧走勢圖

DolphinDB database 雖然是一個通用的分佈式時序數據庫,但因爲內置極其高效的多範式編程語言,開發效率非常高。如果回測不用考慮止損,僅用了3行代碼計算MA信號3行代碼進行回測。DolphinDB的運行效率更是驚人,對美國股市18年的全部股票按日進行回測,不止損回測執行耗時僅4秒多,止損回測僅7秒多。


本文的目的是從技術上幫助金融工程師使用DolphinDB快速實現交易回測。文中採用的各種參數,譬如長短線時間,止損閾值,數據過濾的方法等等,只是起到演示的作用,並非實踐中的最佳參數。


歡迎訪問官網下載DolphinDB試用版


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章