Skip to main content

Node.js 量化投資實戰:使用富果歷史行情 API 與回測工具評估交易策略

· 18 min read
Kevin Wang

在量化投資領域中,回測是指利用歷史市場數據對一個投資策略進行模擬交易,以評估該策略在過去的市場環境下的表現。透過回測,投資者可以根據歷史市場數據得出策略的勝率、獲利和風險等關鍵指標,進而作出投資決策,判斷該策略是否有效以及了解策略在不同市場條件下的表現情況。

在本文中,我們將以 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/realtimeluxon@fugle/realtime 是富果行情 API 客戶端 SDK,而 luxon 方便操作時間和日期的模組,協助我們處理時間和日期格式化。
  • 第 4 行:定義了一個名為 getData 的 async function,用來取得指定股票的歷史數據。這個 function 接收一個名為 options 的物件參數。
  • 第 5-7 行:使用 ?. 選擇運算子和 ?? null 合併運算子,對 options 物件參數的 symbollastYearrecentYears 屬性進行空值判斷,若這些屬性沒有被傳入,則使用預設值 233020223
  • 第 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/backtesttechnicalindicators@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 中的 indexsignals 屬性,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),代表交易系統績效穩定性的指標,數值越高代表交易績效越好且穩定性越高。

繪製權益曲線:

equity-curve.png

列出交易紀錄:

list-of-trades.png

最佳化參數

在上述策略中,我們提供了兩個可變參數 params.n1params.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 取得歷史股價,使用富果的回測工具定義交易策略,並進行回測。透過回測,您可以評估交易策略的表現是否符合預期,進一步優化和改善該策略。如果您有任何交易想法,回測是一個非常有用的工具!