本系列文章將會介紹如何使用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
...
不止損回測所有股票的盈虧走勢圖
情況二:止損回測。我們把預設閾值設爲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
...
止損回測所有股票的盈虧走勢圖
DolphinDB database 雖然是一個通用的分佈式時序數據庫,但因爲內置極其高效的多範式編程語言,開發效率非常高。如果回測不用考慮止損,僅用了3行代碼計算MA信號,3行代碼進行回測。DolphinDB的運行效率更是驚人,對美國股市18年的全部股票按日進行回測,不止損回測執行耗時僅4秒多,止損回測僅7秒多。
本文的目的是從技術上幫助金融工程師使用DolphinDB快速實現交易回測。文中採用的各種參數,譬如長短線時間,止損閾值,數據過濾的方法等等,只是起到演示的作用,並非實踐中的最佳參數。
歡迎訪問官網下載DolphinDB試用版