在量化投資領域中,回測是指利用歷史市場數據對一個投資策略進行模擬交易,以評估該策略在過去的市場環境下的表現。透過回測,投資者可以根據歷史市場數據得出策略的勝率、獲利和風險等關鍵指標,進而作出投資決策,判斷該策略是否有效以及了解策略在不同市場條件下的表現情況。
在本文中,我們將以 Node.js 為例,介紹如何使用富果提供的歷史行情 API 與回測工具,對過去的市場數據來模擬和驗證各種交易策略。
準備歷史數據
要進行回測,首先我們必須準備任意金融商品(如股票、期貨、外匯、加密貨幣等)的 OHLCV(開盤價、最高價、最低價、收盤價、成交量)歷史數據。為了使用實際的數據做範例,我們以台積電(2330)近三年(2020 至 2022 年)的歷史股價為例說明如何操作。
請確認已準備好 Node.js 開發環境,並已申請富果行情 API。接著,請安裝以下依賴套件:
$ npm install --save @fugle/realtime luxon
上述安裝的套件中,@fugle/realtime
是富果行情 API 客戶端函式庫;luxon
則是一個用於處理時間和日期的模組,可以方便地進行時間計算和日期格式化。
以下是一個取得特定股票近三年歷史股價的範例:
const { HttpClient } = require('@fugle/realtime');
const { DateTime } = require('luxon');
async function getData(options) {
const symbol = options?.symbol ?? '2330';
const lastYear = options?.lastYear ?? 2022;
const recentYears = options?.recentYears ?? 3;
const client = new HttpClient({ apiToken: 'YOUR_API_TOKEN' });
const data = [];
for (let i = 0, dt = DateTime.now().set({ year: lastYear }); i < recentYears; i++, dt = dt.minus({ year: 1 })) {
const historical = await client.historical.candles({
symbolId: symbol,
from: dt.startOf('year').toISODate(),
to: dt.endOf('year').toISODate(),
});
data.push(...historical.data);
}
return data;
}
- 第 1-2 行:使用
require
語句引入了兩個模組@fugle/realtime
和luxon
。@fugle/realtime
是富果行情 API 客戶端 SDK,而luxon
方便操作時間和日期的模組,協助我們處理時間和日期格式化。 - 第 4 行:定義了一個名為
getData
的 async function,用來取得指定股票的歷史數據。這個 function 接收一個名為options
的物件參數。 - 第 5-7 行:使用
?.
選擇運算子和??
null 合併運算子,對options
物件參數的symbol
、lastYear
和recentYears
屬性進行空值判斷,若這些屬性沒有被傳入,則使用預設值2330
、2022
和3
。 - 第 8 行:建立一個
HttpClient
物件,並將apiToken
設為 API 的存取權杖。需要注意的是,這裡的YOUR_API_TOKEN
必須替換成有效的 API 存取權杖。 - 第 11-18 行:使用 for 迴圈,從最近的年份開始,每次往前推一年,獲取指定金融商品的該年的 OHLCV 歷史數據。由於富果歷史行情API每次請求的最大時間範圍為一年,我們須逐次取得每年的歷史行情數據。
定義交易策略
定義交易策略是回測的核心,在實作交易策略之前,我們先安裝以下依賴套件:
$ npm install --save @fugle/backtest technicalindicators
上述安裝的套件中,@fugle/backtest
是富果提供的回測工具;technicalindicators
則是用於計算技術指標的模組,提供了許多常用的技術指標計算方法,例如 SMA、EMA、MACD、RSI、Bollinger Bands 等,可協助我們更快速地實現技術指標的計算和應用。
為了進行回測,我們需要自行定義的一個交易策略。實作交易策略需要繼承 @fugle/backtest
提供的 Strategy
類別。我們以移動平均雙線交叉策略為例,當短期均線向上穿越長期均線時,進行買進操作;當短期均線向下穿越長期均線時,進行賣出操作。以下是一個實現移動平均雙線交叉策略的範例:
const { Strategy } = require('@fugle/backtest');
const { SMA, CrossUp, CrossDown } = require('technicalindicators');
class SmaCross extends Strategy {
params = { n1: 20, n2: 60 };
init() {
const lineA = SMA.calculate({
period: this.params.n1,
values: this.data['close'].values,
});
this.addIndicator('lineA', lineA);
const lineB = SMA.calculate({
period: this.params.n2,
values: this.data['close'].values,
});
this.addIndicator('lineB', lineB);
const crossUp = CrossUp.calculate({
lineA: this.getIndicator('lineA'),
lineB: this.getIndicator('lineB'),
});
this.addSignal('crossUp', crossUp);
const crossDown = CrossDown.calculate({
lineA: this.getIndicator('lineA'),
lineB: this.getIndicator('lineB'),
});
this.addSignal('crossDown', crossDown);
}
next(ctx) {
const { index, signals } = ctx;
if (index < this.params.n1 || index < this.params.n2) return;
if (signals.get('crossUp')) this.buy({ size: 1000 });
if (signals.get('crossDown')) this.sell({ size: 1000 });
}
}
- 第 1-2 行:使用
require
語句引入了兩個模組@fugle/backtest
和technicalindicators
。@fugle/backtest
是富果提供的回測工具,而technicalindicators
是一個用於計算技術指標的模組。 - 第 4 行:定義一個名為
SmaCross
的類別,並繼承Strategy
。 - 第 5 行:定義
SmaCross
類別的屬性params
,表示該交易策略的參數,n1
表示短天期移動平均線的週期,n2
表示長天期移動平均線的週期。 - 第 7 行:定義
SmaCross
類別的方法init()
。該方法主要用來初始化技術指標和信號。 - 第 8-12 行:計算 SMA,定義
lineA
為短天期移動平均線,並透過addIndicator()
方法將短天期移動平均線加入指標清單中。 - 第 14-18 行:計算 SMA,定義
lineB
為長天期移動平均線,並透過addIndicator()
方法將長天期移動平均線加入指標清單中。 - 第 20-24 行:計算兩條 SMA 的交叉,定義
crossUp
為短天期移動平均線向上穿越長天期移動平均線,並透過addSignal()
方法將交叉點設為買進信號。 - 第 26-30 行:計算兩條 SMA 的交叉,定義
crossUp
為短天期移動平均線向下穿越長天期移動平均線,並透過addSignal()
方法將交叉點設為賣出信號。 - 第 33 行:定義
SmaCross
類別的方法next()
。可透過ctx
參數取得當前的索引、技術指標和信號。 - 第 34-37 行:在
next()
方法中,我們首先取出ctx
中的index
和signals
屬性,index
代表當前的時間點索引,signals
是一個 Map 物件,其中包含了所有已經計算出來的交易信號。如果發現crossUp
信號,就執行this.buy({ size: 1000 })
買進 1000 股,如果發現crossDown
信號,就執行this.sell({ size: 1000 })
賣出 1000 股。
如此,我們就完成一個交易策略。需要注意的是,這個策 略是非常簡單的,並且沒有考慮到許多重要的因素,例如交易成本、市場流動性等。因此,這個策略只是用來展示回測工具的基本使用方法,實際應用時需要仔細設計和調整。
運行回溯測試
準備好歷史數據並實作交易策略後,就可以運行回溯測試。使用 @fugle/backtest
提供的 Backtest
類別,即可根據輸入的歷史資料和定義的交易策略進行回測。此外 Backtest
類別也提供 options
選項設定,您可以根據需求自行調整:
cash
{number} 初始資金。預設值:10000
。commission
{number} 手續費率。預設值:0
。margin
{number} 槓桿帳戶所需的保證金比率。預設值:1
。tradeOnClose
{boolean} true 表示市場訂單會以當前K棒的收盤價為基準成交,而非下一根K棒的開盤價。預設值:false
。hedging
{boolean} true 表示允許同時在兩個方向上交易。如果設為 false,則相反方向的訂單首先以先進先出(FIFO)的方式關閉現有交易。預設值:false
。exclusiveOrders
{boolean} true 表示每個新訂單都會自動關閉之前的交易/持倉,以確保每次只會存在一個(多頭或空頭)交易。預設值:false
。
然後,調用 Backtest.run()
方法會執行回溯測試並回傳執行結果,其中包含我們策略的模擬結果和相關的統計數據、權益曲線變化以及所有交 易紀錄。
const { Backtest } = require('@fugle/backtest');
const backtest = new Backtest(data, SmaCross, {
cash: 1000000,
tradeOnClose: true,
});
backtest.run() // 執行回測
.then(results => {
results.print(); // 印出回測結果
results.plot(); // 繪製權益曲線並列出交易紀錄
});
回測結果:
╔════════════════════════╤═══════════════════════╗
║ Strategy │ SmaCross(n1=20,n2=60) ║
╟────────────────────────┼───────────────────────╢
║ Start │ 2020-01-02 ║
╟────────────────────────┼───────────────────────╢
║ End │ 2022-12-30 ║
╟────────────────────────┼───────────────────────╢
║ Duration │ 1093 ║
╟────────────────────────┼───────────────────────╢
║ Exposure Time [%] │ 55.102041 ║
╟────────────────────────┼───────────────────────╢
║ Equity Final [$] │ 1105000 ║
╟────────────────────────┼───────────────────────╢
║ Equity Peak [$] │ 1378000 ║
╟────────────────────────┼───────────────────────╢
║ Return [%] │ 10.5 ║
╟────────────────────────┼───────────────────────╢
║ Buy & Hold Return [%] │ 32.300885 ║
╟────────────────────────┼───────────────────────╢
║ Return (Ann.) [%] │ 3.482537 ║
╟────────────────────────┼───────────────────────╢
║ Volatility (Ann.) [%] │ 8.204114 ║
╟────────────────────────┼───────────────────────╢
║ Sharpe Ratio │ 0.424487 ║
╟────────────────────────┼───────────────────────╢
║ Sortino Ratio │ 0.660431 ║
╟────────────────────────┼───────────────────────╢
║ Calmar Ratio │ 0.175785 ║
╟────────────────────────┼───────────────────────╢
║ Max. Drawdown [%] │ -19.811321 ║
╟────────────────────────┼───────────────────────╢
║ Avg. Drawdown [%] │ -2.241326 ║
╟────────────────────────┼───────────────────────╢
║ Max. Drawdown Duration │ 708 ║
╟────────────────────────┼───────────────────────╢
║ Avg. Drawdown Duration │ 54 ║
╟────────────────────────┼───────────────────────╢
║ # Trades │ 6 ║
╟────────────────────────┼───────────────────────╢
║ Win Rate [%] │ 16.666667 ║
╟────────────────────────┼───────────────────────╢
║ Best Trade [%] │ 102.3729 ║
╟────────────────────────┼───────────────────────╢
║ Worst Trade [%] │ -10.4418 ║
╟────────────────────────┼───────────────────────╢
║ Avg. Trade [%] │ 5.718878 ║
╟────────────────────────┼───────────────────────╢
║ Max. Trade Duration │ 322 ║
╟────────────────────────┼───────────────────────╢
║ Avg. Trade Duration │ 100 ║
╟────────────────────────┼───────────────────────╢
║ Profit Factor │ 2.880822 ║
╟────────────────────────┼───────────────────────╢
║ Expectancy [%] │ 11.139483 ║
╟────────────────────────┼───────────────────────╢
║ SQN │ 0.305807 ║
╚════════════════════════╧═══════════════════════╝
以下是各項數據結果的意義:
- Start:回測的開始日期。
- End:回測的結束日期。
- Duration:回測的時間長度。
- Exposure Time [%]:持倉時間佔總時間的百分比。
- Equity Final [$]:回測結束時的帳戶戶權益值。
- Equity Peak [$]:回測期間帳戶權益的最高值。
- Return [%]:回測期間的總收益率。
- Buy & Hold Return [%]:買入持有策略在同樣時間內的總收益率。
- Return (Ann.) [%]:年化收益率。
- Volatility (Ann.) [%]:年化波動率。
- Sharpe Ratio:夏普比率,代表每承受一個單位的風險所獲得的超額報酬。
- Sortino Ratio:索提諾比率,是夏普比率的改進版,主要針對下跌風險進行衡量。
- Calmar Ratio:卡瑪比率,代表每承受一個單位的風險所獲得的超額報酬,同時考慮最大回撤。
- Max. Drawdown [%]:最大回撤率,指從峰值到谷底的最大跌幅。
- Avg. Drawdown [%]:平均回撤率,指回撤期間中,從峰值到谷底的平均跌幅。
- Max. Drawdown Duration:最大回撤期間,指從峰值到谷底的時間長度。
- Avg. Drawdown Duration:平均回撤期間,指回撤期間中,從峰值到谷底的平均時 間長度。
- Trades:回測期間的交易次數。
- Win Rate [%]:勝率,即獲利交易的比例。
- Best Trade [%]:最好的交易獲得的收益率。
- Worst Trade [%]:最壞的交易蒙受的虧損率。
- Avg. Trade [%]:平均交易獲得的收益率。
- Max. Trade Duration:最長持倉期間。
- Avg. Trade Duration:平均持倉期間。
- Profit Factor:獲利因子,是獲利交易與虧損交易的比例,數值越大代表交易績效越好。
- Expectancy [%]:期望值,是每筆交易所預期的平均收益率,數值越高代表交易績效越好。
- SQN:系統質量數值(System Quality Number),代表交易系統績效穩定性的指標,數值越高代表交易績效越好且穩定性越高。
繪製權益曲線:
列出交易紀錄:
最佳化參數
在上述策略中,我們提供了兩個可變參數 params.n1
與 params.n2
,它們代表兩條移動平均線的計算期間。透過調用 Backtest.optimize()
方法,我們可以尋找最佳參數組合。在此方法下,您可以設置 params
選項以改變 Strategy
提供的參數設定。Backtest.optimize()
將返回提供參數下的最佳組合。
以下程式範例以短期均線 5 日(週線)、10 日(雙週線)、20 日(月線)及長期移動平均線 60 日(季線)、120 日(半年線)、240 日(年線)為例,尋找最佳參數組合:
backtest.optimize({ // 執行最佳化參數的回測
params: {
n1: [5, 10, 20],
n2: [60, 120, 240],
},
})
.then(results => {
results.print(); // 印出最佳化參數的回測結果
results.plot(); // 繪製最佳化參數的權益曲線並列出交易紀錄
});
回測結果顯示,當短天期移動平均線參數設為 10 日,以及長天期移動平均線設為 60 日時,可獲得最佳結果:
╔════════════════════════╤═══════════════════════╗
║ Strategy │ SmaCross(n1=10,n2=60) ║
╟────────────────────────┼───────────────────────╢
║ Start │ 2020-01-02 ║
╟────────────────────────┼───────────────────────╢
║ End │ 2022-12-30 ║
╟────────────────────────┼───────────────────────╢
║ Duration │ 1093 ║
╟────────────────────────┼───────────────────────╢
║ Exposure Time [%] │ 55.646259 ║
╟────────────────────────┼───────────────────────╢
║ Equity Final [$] │ 1177500 ║
╟────────────────────────┼───────────────────────╢
║ Equity Peak [$] │ 1375500 ║
╟────────────────────────┼───────────────────────╢
║ Return [%] │ 17.75 ║
╟────────────────────────┼───────────────────────╢
║ Buy & Hold Return [%] │ 32.300885 ║
╟────────────────────────┼───────────────────────╢
║ Return (Ann.) [%] │ 5.761952 ║
╟────────────────────────┼───────────────────────╢
║ Volatility (Ann.) [%] │ 8.336371 ║
╟────────────────────────┼───────────────────────╢
║ Sharpe Ratio │ 0.691182 ║
╟────────────────────────┼───────────────────────╢
║ Sortino Ratio │ 1.125943 ║
╟────────────────────────┼───────────────────────╢
║ Calmar Ratio │ 0.400281 ║
╟────────────────────────┼───────────────────────╢
║ Max. Drawdown [%] │ -14.394766 ║
╟────────────────────────┼───────────────────────╢
║ Avg. Drawdown [%] │ -2.038896 ║
╟────────────────────────┼───────────────────────╢
║ Max. Drawdown Duration │ 708 ║
╟────────────────────────┼───────────────────────╢
║ Avg. Drawdown Duration │ 57 ║
╟────────────────────────┼───────────────────────╢
║ # Trades │ 8 ║
╟────────────────────────┼───────────────────────╢
║ Win Rate [%] │ 37.5 ║
╟────────────────────────┼───────────────────────╢
║ Best Trade [%] │ 98.3193 ║
╟────────────────────────┼───────────────────────╢
║ Worst Trade [%] │ -8.4189 ║
╟────────────────────────┼───────────────────────╢
║ Avg. Trade [%] │ 5.905751 ║
╟────────────────────────┼───────────────────────╢
║ Max. Trade Duration │ 322 ║
╟──────────────────── ────┼───────────────────────╢
║ Avg. Trade Duration │ 75 ║
╟────────────────────────┼───────────────────────╢
║ Profit Factor │ 4.284132 ║
╟────────────────────────┼───────────────────────╢
║ Expectancy [%] │ 9.567663 ║
╟────────────────────────┼───────────────────────╢
║ SQN │ 0.567317 ║
╚════════════════════════╧═══════════════════════╝
透過調用 Backtest.optimize()
方法,我們可以很容易地找出最佳的參數組合。但請注意,運用歷史資料進行最佳化,經常造成不切實際的預期,實際交易通常都無法複製這些最佳化的績效。就如同我們常看到的投資警語是「過去績效不代表未來績效」。我們運用回測的目的並不是在歷史資料測試過程中達成最高的報酬,而是找到某種適用於過去的健全概念,顯示這種概念也非常可能適用於未來。
程式範例
以上完整的程式碼,請參考 GitHub repo。
結語
在本文中,我們已經示範如何透過富果歷史行情 API 取得歷史股價,使用富果的回測工具定義交易策略,並進行回測。透過回測,您可以評估交易策略的表現是否符合預期,進一步優化和改善該策略。如果您有任何交易想法,回測是一個非常有用的工具!