import vectorbt as vbt
import numpy as np
import pandas as pd
import matplotlib
from matplotlib import pyplot as plt
matplotlib.rcParams['figure.figsize'] = (12, 5)
數(shù)據(jù)
從 cryptocurrency exchange Poloniex 處獲取 OHLC 數(shù)據(jù)
# period = 300, 900, 1800, 7200, 14400, or 86400
ohlc_df = vbt.data.load_cryptopair('USDT_ETH', vbt.data.ago_dt(days=180), vbt.data.now_dt(), period=vbt.data.Period.D1)
ohlc_df.head()
done. 1.71s
# No future data
rate_sr = ohlc_df.O
# Fees and slippage
# 手續(xù)費(fèi)和滑點(diǎn)
investment = 1000
fees = 0.0025
slippage_factor = 0.25
slippage = (ohlc_df['H'] - ohlc_df['L']) * slippage_factor / rate_sr
vbt.graphics.plot_line(rate_sr)
輸出:
count mean std min 25% 50% 75% max O 180.0 713.444128 233.235322 369.0 529.683513 681.0821 858.69125 1382.0價(jià)格曲線
indicators 指標(biāo)
計(jì)算均線 EMA 指標(biāo)
fast_ma_sr = vbt.indicators.EMA(rate_sr, span=5)
slow_ma_sr = vbt.indicators.EMA(rate_sr, span=21)
signals 交易信號(hào)
根據(jù)條件計(jì)算交易信號(hào)
- 當(dāng)快速均線上穿慢速均線時(shí),做多,反之,做空
ma_entries = vbt.signals.DMAC_entries(fast_ma_sr, slow_ma_sr)
ma_exits = vbt.signals.DMAC_exits(fast_ma_sr, slow_ma_sr)
多空向量都是0/1序列,方便進(jìn)行快速向量運(yùn)算。
因?yàn)槲覀円揖€交叉點(diǎn),所以我們只需要這兩個(gè)向量0/1序列的第一個(gè)信號(hào)。
ma_entries = vbt.bitvector.first(ma_entries)
ma_exits = vbt.bitvector.first(ma_exits)
- 無論何時(shí),如果價(jià)格下跌10%就做空。
# trailstop_exits = vbt.signals.trailstop_exits(rate_sr, ma_entries, 0.1 * rate_sr)
結(jié)合均線退出策略和跟蹤止損策略(trailing stop)
# ma_exits = vbt.bitvector.OR(ma_exits, trailstop_exits)
# ma_exits = vbt.bitvector.first(ma_exits)
如果還要應(yīng)用其它的策略,生成你自己的 bit 向量然后使用 vector.AND/OR/XOR 進(jìn)行結(jié)合。
positions 頭寸
從多空策略向量生成頭寸向量 Generete positions out of both vectors (merge and reduce).
pos_sr = vbt.positions.from_signals(rate_sr, ma_entries, ma_exits)
pos_sr.head()
輸出:
O date 2018-01-05 1 2018-01-18 -1 2018-01-29 1 2018-02-02 -1 2018-02-18 1 dtype: int32
ff = pd.DataFrame(ma_entries).set_index(rate_sr.index)
ff[ff==1].dropna()
輸出:
O date 2018-01-05 1.0 2018-01-29 1.0 2018-02-18 1.0 2018-02-20 1.0 2018-04-14 1.0
ff = pd.DataFrame(ma_exits).set_index(rate_sr.index)
ff[ff==1].dropna()
輸出:
O date 2018-01-18 1.0 2018-02-02 1.0 2018-02-19 1.0 2018-02-21 1.0 2018-05-23 1.0
pos_sr
date 2018-01-05 1 2018-01-18 -1 2018-01-29 1 2018-02-02 -1 2018-02-18 1 2018-02-19 -1 2018-02-20 1 2018-02-21 -1 2018-04-14 1 2018-05-23 -1 dtype: int32
頭寸序列是一個(gè)二元序列,1表示買入,-1表示賣出。因?yàn)槲覀兿敫宄販y(cè)評(píng)一個(gè)策略(只考慮策略邏輯對(duì)凈值的影響,不考慮投資額度的大小和分配),所以頭寸序列中的一行只有多或空,沒有兩個(gè)同時(shí)的頭寸。
買賣點(diǎn)的可視化
vbt.positions.plot(rate_sr, pos_sr)
輸出:
count mean std min 25% 50% 75% \
0 5.0 -19.503679 137.281554 -205.209166 -61.0 -57.21 76.436671
max
0 149.464099

注:綠色三角為買,紅色倒三角為賣
equity 凈值
基于交易手續(xù)費(fèi)和滑點(diǎn),從頭寸生成凈值。
equity_sr = vbt.equity.from_positions(rate_sr, pos_sr, investment, fees, slippage)
equity_sr.head()
輸出:
date 2018-01-04 NaN 2018-01-05 973.100223 2018-01-06 999.309720 2018-01-07 1040.616509 2018-01-08 1155.616055 dtype: float64
在建立第一個(gè)頭寸之前的凈值都是 NaN。
基準(zhǔn)和報(bào)價(jià)凈值(base and quote equities)的可視化
vbt.equity.plot(rate_sr, equity_sr)
輸出:
base: equity - hold
count mean std min 25% 50% 75% \
0 179.0 44.435796 169.491093 -305.699824 -73.27329 110.092595 153.878813
max
0 323.737977

注:紅色為跑輸股價(jià),綠色為跑贏股價(jià)
quote: equity - hold
count mean std min 25% 50% 75% max
0 179.0 0.130039 0.277426 -0.33907 -0.077732 0.193859 0.267966 0.769767

returns 回報(bào)率
生成回報(bào)率
returns_sr = vbt.returns.from_equity(equity_sr)
returns_sr.head()
輸出:
date 2018-01-04 0.000000 2018-01-05 0.000000 2018-01-06 0.026934 2018-01-07 0.041335 2018-01-08 0.110511 dtype: float64
回報(bào)率可視化
vbt.returns.plot(vbt.returns.resample(returns_sr, 'W'))
輸出:
count mean std min 25% 50% 75% max
0 27.0 -0.003166 0.132375 -0.292969 0.0 0.0 0.0 0.374422

performance 績(jī)效
顯示多個(gè)基于回報(bào)的KPI總結(jié)。
vbt.performance.summary(returns_sr)
輸出:
distribution count 180.000000
mean -0.000616
std 0.038778
min -0.197584
25% 0.000000
50% 0.000000
75% 0.000000
max 0.133985
performance profit -0.219902
avggain 0.049820
avgloss 0.053396
winrate 0.500000
expectancy -0.003547
maxdd 0.577767
risk/return profile sharpe -0.015882
sortino -0.014195
dtype: float64
optimizer.gridsearch 模型優(yōu)化/參數(shù)搜索
傳統(tǒng)的的優(yōu)化方法就是網(wǎng)格搜索(或者說窮舉)。這種方法窮舉生成所有可能的參數(shù)組合。
這種方法的優(yōu)點(diǎn):
- 容易實(shí)現(xiàn)
- 二維組合可以通過熱力圖可視化
- 可以用于發(fā)現(xiàn)未知的組合模式
- 可以高度并行化處理
但是也有一些不足之處:
- 不夠靈活,難以適應(yīng)不斷變化的金融市場(chǎng)
- 易于“過擬合”
- 沒有中間回饋信號(hào)
網(wǎng)格搜索包括 3-4 階段:
| 層級(jí) | 目的 | 模塊 | 結(jié)構(gòu) |
|---|---|---|---|
| 1 | 計(jì)算頭寸、凈值、回報(bào) | srmap |
{param: pd.Series} |
| 2 | 計(jì)算 KPIs | nummap |
pd.Series |
| 3 (可選) | 組合多個(gè)KPI指標(biāo)為一個(gè)分值并比較 | scoremap |
pd.Series |
| 4 | 構(gòu)造熱力圖以查看某些未知的模式 | matrix |
pd.DataFrame |
最后我們就可以比較不同交易策略的績(jī)效水平了。
L1 第一階段
srmap 模塊
import vectorbt.optimizer.gridsearch as grids
對(duì)一系列均線組合計(jì)算回報(bào)。
1. 先計(jì)算所有的均線。
# Init
ma_func = lambda span: vbt.indicators.EMA(rate_sr, span=span)
min_ma, max_ma, step = 1, 100, 1
fees = 0.0025
# Cache moving averages
param_range = grids.params.range_params(min_ma, max_ma, step)
ma_cache = dict(grids.srmap.from_func(ma_func, param_range))
cores: 8
processes: 1
starmap: False
calcs: 100 (~5.26s) ..
done. 0.03s
注:上面輸出中的 calcs 的意思是需計(jì)算次數(shù)和預(yù)計(jì)總的計(jì)算時(shí)間
而 done 后面是使用多核處理實(shí)際花費(fèi)的時(shí)間,這里可以看到其加速了170倍!
下面計(jì)算頭寸需要5050次計(jì)算,其加速超過700倍?。?!
ma_cache Dict數(shù)據(jù)類型:
keys: 1..100
-
Values: key對(duì)應(yīng)的日均線,Series類型數(shù)據(jù),如 key=5:MA5
- 對(duì)每個(gè)均線組合,生成頭寸向量。
# Params
ma_space = grids.params.combine_rep_params(min_ma, max_ma, step, 2)
# Func
def ma_positions_func(fast_ma, slow_ma):
# Cache
fast_ma_sr = ma_cache[fast_ma]
slow_ma_sr = ma_cache[slow_ma]
# Signals
entries = vbt.signals.DMAC_entries(fast_ma_sr, slow_ma_sr)
entries = vbt.bitvector.first(entries)
exits = vbt.signals.DMAC_exits(fast_ma_sr, slow_ma_sr)
exits = vbt.bitvector.first(exits)
# Positions
pos_sr = vbt.positions.from_signals(rate_sr, entries, exits)
return pos_sr
ma_positions_srmap = grids.srmap.from_func(ma_positions_func, ma_space)
cores: 8
processes: 1
starmap: True
calcs: 5050 (~1453.19s) ..
done. 2.07s
ma_positions_srmap 為Dict數(shù)據(jù)類型:
key: 均線組合 (fast, slow) tuple類型,如,(5, 21)為5日21日均線組合
-
value: 頭寸向量,Series類型
- 對(duì)每個(gè)頭寸向量,生成回報(bào)向量。
我們需要分開計(jì)算頭寸和回報(bào),因?yàn)槲覀冃枰總€(gè)均線組合的頭寸向量長(zhǎng)度來進(jìn)行隨機(jī)映射。
def ma_returns_func(fast_ma, slow_ma):
# Equity
pos_sr = ma_positions_srmap[(fast_ma, slow_ma)]
equity_sr = vbt.equity.from_positions(rate_sr, pos_sr, investment, fees, slippage)
# Returns
returns_sr = vbt.returns.from_equity(equity_sr)
return returns_sr
ma_returns_srmap = grids.srmap.from_func(ma_returns_func, ma_space)
cores: 8
processes: 1
starmap: True
calcs: 5050 (~1451.34s) ..
done. 5.68s
ma_returns_srmap 為Dict數(shù)據(jù)類型:
- key: 均線組合 (fast, slow) tuple類型,如,(5, 21)為5日21日均線組合
- value: 回報(bào)向量,Series類型,
這個(gè)Series是每天的回報(bào),如0.012表示回報(bào)1.2%
為每個(gè)均線組合生成同樣長(zhǎng)度的隨機(jī)頭寸向量和相應(yīng)的回報(bào)向量。
# Params
random_space = [(fma, sma, len(np.flatnonzero(ma_positions_srmap[(fma, sma)].values))) for fma, sma in ma_space]
# Func
def random_returns_func(slow_ma, fast_ma, n):
# Positions
pos_sr = vbt.positions.random(rate_sr, n)
# Equity
equity_sr = vbt.equity.from_positions(rate_sr, pos_sr, investment, fees, slippage)
# Returns
returns_sr = vbt.returns.from_equity(equity_sr)
return returns_sr
random_returns_srmap = grids.srmap.from_func(random_returns_func, random_space)
cores: 8
processes: 1
starmap: True
calcs: 5050 (~1445.95s) ..
done. 7.49s
random_returns_srmap 為Dict數(shù)據(jù)結(jié)構(gòu):
- keys tuple類型, (短線的天數(shù),長(zhǎng)線的天數(shù),交易信號(hào)數(shù)量)
- value Series類型,每天的收益率
L2 第二階段
nummap 模塊
對(duì)每個(gè)回報(bào)向量,計(jì)算 KPI 指標(biāo)。
注:此處對(duì)回報(bào)進(jìn)行了累積得到了累積回報(bào)
if_i_hold = vbt.performance.profit(rate_sr.pct_change())
profit = lambda r: vbt.performance.profit(r) - if_i_hold
ma_profit_nummap = grids.nummap.from_srmap(ma_returns_srmap, vbt.performance.profit)
cores: 8
processes: 1
starmap: False
calcs: 5050 (~4.29s) ..
done. 2.42s
min (1, 7): -0.7055079244549305
max (11, 13): 0.033964270132262
sharpe = lambda r: vbt.performance.sharpe(r, nperiods=252)
ma_sharpe_nummap = grids.nummap.from_srmap(ma_returns_srmap, sharpe)
cores: 8
processes: 1
starmap: False
calcs: 5050 (~1411.08s) ..
done. 0.97s
min (1, 7): -1.909136002820758
max (11, 13): 0.3736374454176382
random_profit_nummap = grids.nummap.from_srmap(random_returns_srmap, vbt.performance.profit)
cores: 8
processes: 1
starmap: False
calcs: 5050 (~4.86s) ..
done. 2.38s
min (2, 4, 31): -0.8685124600763209
max (75, 85, 4): 1.6696239049805262
比較EMA均線策略和隨機(jī)策略的各個(gè)分位數(shù)分布情況。
grids.nummap.compare_quantiles(ma_profit_nummap, random_profit_nummap)
count mean std min 25% 50% 75% \ nummap 5050.0 -0.204439 0.132031 -0.705508 -0.306606 -0.216076 -0.116936 benchmark 5050.0 -0.295012 0.305769 -0.868512 -0.521892 -0.350998 -0.122543 max nummap 0.033964 benchmark 1.669624

注:X號(hào)標(biāo)識(shí)了最好和最壞情況綠色表示均線跑贏隨機(jī),紅色相反從這里看出,大約75%的情況均線策略是勝過隨機(jī)策略的;隨機(jī)策略可能出現(xiàn)比較極端的情況;最后,只做多頭,在股價(jià)大幅下跌50%的情況下,兩種策略都很少有取得正收益的參數(shù)配置 :-(
比較 KPI 分布情況。
cmap = plt.cm.rainbow_r
norm = plt.Normalize()
grids.nummap.compare_hists(ma_profit_nummap, random_profit_nummap, 50, cmap, norm)
count mean std min 25% 50% 75% \ nummap 5050.0 -0.204439 0.132031 -0.705508 -0.306606 -0.216076 -0.116936 benchmark 5050.0 -0.295012 0.305769 -0.868512 -0.521892 -0.350998 -0.122543 max nummap 0.033964 benchmark 1.669624


L3 第三階段
scoremap 模塊
考慮 KPI 多個(gè)指標(biāo)的權(quán)重,生成一個(gè)1-100的分?jǐn)?shù)。
ma_scoremap = grids.scoremap.from_nummaps([ma_profit_nummap, ma_sharpe_nummap], [2/3, 1/3], [False, False])
done. 0.01s
min (1, 7): 1.0
max (11, 13): 99.99999999999999
L4 第四階段
matrix 模塊
把二維參數(shù)網(wǎng)格映射到矩陣向量
ma_matrix = grids.matrix.from_nummap(ma_profit_nummap, symmetric=True).fillna(0)
done. 1.94s
顯示為熱力圖
cmap = plt.cm.rainbow_r
norm = plt.Normalize()
matplotlib.rcParams['figure.figsize'] = (12, 9)
grids.matrix.plot(ma_matrix, cmap, norm)
matplotlib.rcParams['figure.figsize'] = (12, 5)
count mean std min 25% 50% 75% \ 0 10000.0 -0.206483 0.131083 -0.705508 -0.306749 -0.223663 -0.116936 max 0 0.033964


