# Python筆記 : 股票策略回測 by Vectorbt
[Vectorbt官網](https://vectorbt.dev/)
## 大致介紹
vectorbt是一套拿來進行量化分析的套件,特別的點在於他有numpy的速度,以及pandas的方便性。
因此比起其他的回測套件,他擁有極佳的速度,可以在短時間之內分析大量的策略。其中,套到這個套件裡面的所有參數都可以進行向量化,允許我們同時對所有元素執行相同的操作。另外,也使用 Numba 解決了與向量化相關的路徑依賴問題。
這邊大部分使用的套件說明都可以在[這裡](https://vectorbt.pro/api/)看到詳細說明。
## 模組
```python=
import vectorbt as vbt
# import plotly 可不用,因vectorbt有自帶視覺化套件
import datetime
import pandas as pd
import numpy as np
```
## 時間設定以及視覺化分析資料的位置
```python=
filename='某個位置路徑'
end_date=datetime.datetime.now()
start_date=end_date - datetime.timedelta(days=3) #限制在3天之內
```
這邊設3天是因為等等要抓取的時間單位為一分鐘,故三天已經夠長了。
## Data Import
```python=
btc_price = vbt.YFData.download('BTC-USD',
#['BTC-USD','ETH-USD'], 可以用列表同時import多重的data
interval='1m', #改變時間的單位
start = start_date,
end=end_date,
missing_idnex='drop').get('Close')
```
vectorbt會自動抓取yfinace的api,只要yfinance有的,都可以抓的到。
標的可以分為:
| 台股 | 美股 | 加密貨幣 |
| -------- | -------- | -------- |
| Ex : 2330.TW | Ex : TSLA | Ex : BTC-USD |
## 交易策略
這裡使用的交易策略相當簡單,因為主要目的是為了熟悉vectorbt的使用。
| 情況 | 操作 |
| ------------------------------------ | ---- |
| rsi>設定的高標 | 賣出 |
| rsi<設定的低標 且 最近的收盤價低於ma | 買入 |
當rsi大於我們設定的高標,代表目前股市可能進入過熱的壯台,則我們可以選擇在這時候進行賣出的操作。當相反情況出現,rsi低於我們設定的低標的話,而且這時候的收盤價低於ma線,表示這時候或許是一個很好的入場點,則我們可以在這時候進行買入的操作。(這邊都是極度簡單的操作,要是這樣就可以穩穩贏過大盤就太感謝了...)
## 客製化訊號以及策略
這邊主要可以分成三個區塊,分別為:
1. Define一個函數,裡面主要用來計算出是否可以進入市場的訊號 -> 1及-1。
2. 算是一個用來做出策略的食譜,裡面放著等等產出的資料變數名稱以及一些默認變數。
3. 實際執行策略,裡面包含一些變數的範圍。
### 1. Define a function
```python=
def custom_indicator(close, #每當在這邊增加一個參數,就要在第二步驟的ind裡面的param_names裡面增加
rsi_window = 14,
ma_window = 50,
entry=30,
exits=70
):
close_5m = close.resample('5T').last() #將資料型態從1min變成5min
rsi = vbt.RSI.run(close_5m, window = rsi_window).rsi #rsi中的rsi值(因為在這裡的前面rsi出來的不會只有單純的rsi)
rsi, _ =rsi.align(close, #將5min的資料重新展開成1min
broadcast_axis=0,
method='ffill', #並將空值以第5分鐘的copy塞進去
join='right' #rsi是right table,close是left table
) #by doing so, rsi跟close有same shape,只是rsi是5min的資料,close是1分鐘的資料
close = close.to_numpy()
rsi = rsi.to_numpy()
ma = vbt.MA.run(close, window = ma_window).ma.to_numpy()
trend = np.where(rsi > exits, -1, 0) #要是rsi>70,則賣出(-1),否則甚麼都不做(0)
trend = np.where((rsi < entry)&(close < ma), 1, trend) #要是rsi<30而且收盤價<ma,則買入(1),如果沒有的話就按照原本的trend
return trend
```
### 2. 策略的食譜
```python=
ind = vbt.IndicatorFactory(
class_name = 'Combination',
short_name = 'comb',
input_names=['close'], #輸入的parameter的名字
#param_names=['window'], #hyper parameter的名字,也可用列表呈現
param_names=['rsi_window','ma_window','entry','exits'],
output_names=['value'] #output出來的名字
).from_apply_func( #提供一些默認值給recipe,就是上面define的function裡面的parameter
custom_indicator,
rsi_window=14,
ma_window=50,
entry=70,
exits=30,
keep_pd=True #保持資料型態為pandas,避免變成numpy arrays
)
```
### 3. 實際執行策略
```python=
res = ind.run( #run 一個策略
btc_price,
#rsi_window = [14,35,21], #給定特定值
rsi_window = np.arange(10,40,step=3,dtype=int), #給定一個範圍
#ma_window = [21,50,100],
ma_window = np.arange(20,200,step=15,dtype=int),
#entry=[30,40],
entry = np.arange(10,40,step=4,dtype=int),
#exits=[60,70],
exits = np.arange(60,85,step=4,dtype=int),
param_product=True) #如果沒有這個的話,只會按照list的順序,並不會兩兩配對
```
這邊有兩種險方式來定義變數,一個是給定特定值的list,Ex:[14,35,21],表示將rsi_window分別設定為14、35、21,代表只會嘗試這三個數值。另一種方式則是使用np.arange,將在這個range的數值全部帶進去進行計算,進而求出每一種狀況下的獲利。
### 執行結果
```python=
print(res.value)
```
output:
```
comb_rsi_window 10 ... 37
comb_ma_window 20 ... 185
comb_entry 10 ... 38
comb_exits 60 64 ... 80 84
symbol BTC-USD ETH-USD BTC-USD ... ETH-USD BTC-USD ETH-USD
Datetime ...
2022-08-11 03:07:00+00:00 0 0 0 ... 0 0 0
2022-08-11 03:09:00+00:00 0 0 0 ... 0 0 0
2022-08-11 03:11:00+00:00 0 0 0 ... 0 0 0
2022-08-11 03:13:00+00:00 0 0 0 ... 0 0 0
2022-08-11 03:15:00+00:00 0 0 0 ... 0 0 0
... ... ... ... ... ... ...
2022-08-14 03:00:00+00:00 0 0 0 ... 0 0 0
2022-08-14 03:01:00+00:00 0 0 0 ... 0 0 0
2022-08-14 03:02:00+00:00 0 0 0 ... 0 0 0
2022-08-14 03:03:00+00:00 0 0 0 ... 0 0 0
2022-08-14 03:04:00+00:00 0 0 0 ... 0 0 0
[3690 rows x 13440 columns]
```
如果將結果攤開來看,會呈現

以及

代表著我們進出場的訊號。
## 將訊號帶入策略執行
這時我們將剛剛得到的res進行處裡,其中1設定為entries,-1設定為exits。
```python=
entries = res.value == 1
exits = res.value == -1
```
實際執行
```python=
pf = vbt.Portfolio.from_signals(btc_price, entries, exits)
```
試著看看執行結果
```python=
print(pf.stats().to_string()) #to_string()可以將全部結果攤開
```
會得到:
```
Output from spyder call 'get_namespace_view':
Start 2022-08-11 03:07:00+00:00
End 2022-08-14 03:04:00+00:00
Period 3690
Start Value 100.0
End Value 100.179789
Total Return [%] 0.179789
Benchmark Return [%] 3.590516
Max Gross Exposure [%] 83.020833
Total Fees Paid 0.0
Max Drawdown [%] 2.078304
Max Drawdown Duration 2193.187668
Total Trades 4.26994
Total Closed Trades 3.966369
Total Open Trades 0.303571
Open Trade PnL 0.076599
Win Rate [%] 63.829362
Best Trade [%] 0.680632
Worst Trade [%] -0.39365
Avg Winning Trade [%] 0.607418
Avg Losing Trade [%] -0.6351
Avg Winning Trade Duration 260.345941
Avg Losing Trade Duration 445.916595
Profit Factor inf
Expectancy 0.202725
```
執行此段程式碼則可以獲得所有變數組合的報酬
```python=
print(pf.total_return().to_string())
```
只擷取其中一小段
```python
170 10 60 BTC-USD 0.000000
ETH-USD 0.000000
64 BTC-USD 0.000000
ETH-USD 0.000000
68 BTC-USD 0.000000
ETH-USD 0.000000
72 BTC-USD 0.000000
ETH-USD 0.000000
76 BTC-USD 0.000000
ETH-USD 0.000000
80 BTC-USD 0.000000
ETH-USD 0.000000
84 BTC-USD 0.000000
ETH-USD 0.000000
14 60 BTC-USD 0.000000
ETH-USD 0.000000
64 BTC-USD 0.000000
ETH-USD 0.000000
68 BTC-USD 0.000000
ETH-USD 0.000000
72 BTC-USD 0.000000
ETH-USD 0.000000
76 BTC-USD 0.000000
ETH-USD 0.000000
80 BTC-USD 0.000000
ETH-USD 0.000000
84 BTC-USD 0.000000
ETH-USD 0.000000
```
只抽取此策略的報酬率
```python=
returns = pf.total_return()
print(returns.max()) #最大的報酬率
print(returns.idxmax()) #最大的組合
print(returns.to_string()) #所有組合
```
可以得到
```python
0.06338762101981515
(37, 20, 30, 84, 'ETH-USD') #分別代表rsi_window, ma_window, entry, exits
```
## 資料視覺化
vectorbt套件中有附兩種資料視覺化的模式,分別為:
1. Heatmap
2. Volume
### Heatmap
```python=
fig = returns.vbt.heatmap(
x_level = 'comb_rsi_window',
#y_level = 'comb_ma_window',
y_level = 'comb_entry',
slider_level = 'symbol' #如果同時分析不同標的,則可以透過slider切換
)
fig.write_html(filename,auto_open=True)#圖片儲存並自動展開
```

### Volume
將資料以3d的樣式呈現,可同時比較多個變數。
```python=
fig = returns.vbt.volume(
x_level = 'comb_exits',
y_level = 'comb_ma_window',
z_level = 'comb_entry',
slider_level = 'symbol'
)
fig.write_html(filename,auto_open=True)#圖片儲存並自動展開
```
