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)
数据
从 cryptocurrency exchange Poloniex 处获取 OHLC 数据
# 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
# 手续费和滑点
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
indicators 指标
计算均线 EMA 指标
fast_ma_sr = vbt.indicators.EMA(rate_sr, span=5)
slow_ma_sr = vbt.indicators.EMA(rate_sr, span=21)
signals 交易信号
根据条件计算交易信号
- 当快速均线上穿慢速均线时,做多,反之,做空
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序列,方便进行快速向量运算。
因为我们要找均线交叉点,所以我们只需要这两个向量0/1序列的第一个信号。
ma_entries = vbt.bitvector.first(ma_entries)
ma_exits = vbt.bitvector.first(ma_exits)
- 无论何时,如果价格下跌10%就做空。
# trailstop_exits = vbt.signals.trailstop_exits(rate_sr, ma_entries, 0.1 * rate_sr)
结合均线退出策略和跟踪止损策略(trailing stop)
# ma_exits = vbt.bitvector.OR(ma_exits, trailstop_exits)
# ma_exits = vbt.bitvector.first(ma_exits)
如果还要应用其它的策略,生成你自己的 bit 向量然后使用 vector.AND/OR/XOR
进行结合。
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
头寸序列是一个二元序列,1表示买入,-1表示卖出。因为我们想更清楚地测评一个策略(只考虑策略逻辑对净值的影响,不考虑投资额度的大小和分配),所以头寸序列中的一行只有多或空,没有两个同时的头寸。
买卖点的可视化
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 净值
基于交易手续费和滑点,从头寸生成净值。
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
在建立第一个头寸之前的净值都是 NaN。
基准和报价净值(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
注:红色为跑输股价,绿色为跑赢股价
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 回报率
生成回报率
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
回报率可视化
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 绩效
显示多个基于回报的KPI总结。
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 模型优化/参数搜索
传统的的优化方法就是网格搜索(或者说穷举)。这种方法穷举生成所有可能的参数组合。
这种方法的优点:
- 容易实现
- 二维组合可以通过热力图可视化
- 可以用于发现未知的组合模式
- 可以高度并行化处理
但是也有一些不足之处:
- 不够灵活,难以适应不断变化的金融市场
- 易于“过拟合”
- 没有中间回馈信号
网格搜索包括 3-4 阶段:
层级 | 目的 | 模块 | 结构 |
---|---|---|---|
1 | 计算头寸、净值、回报 | srmap |
{param: pd.Series} |
2 | 计算 KPIs | nummap |
pd.Series |
3 (可选) | 组合多个KPI指标为一个分值并比较 | scoremap |
pd.Series |
4 | 构造热力图以查看某些未知的模式 | matrix |
pd.DataFrame |
最后我们就可以比较不同交易策略的绩效水平了。
L1 第一阶段
srmap 模块
import vectorbt.optimizer.gridsearch as grids
对一系列均线组合计算回报。
1. 先计算所有的均线。
# 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 的意思是需计算次数和预计总的计算时间
而 done 后面是使用多核处理实际花费的时间,这里可以看到其加速了170倍!
下面计算头寸需要5050次计算,其加速超过700倍!!!
ma_cache Dict数据类型:
keys: 1..100
-
Values: key对应的日均线,Series类型数据,如 key=5:MA5
- 对每个均线组合,生成头寸向量。
# 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数据类型:
key: 均线组合 (fast, slow) tuple类型,如,(5, 21)为5日21日均线组合
-
value: 头寸向量,Series类型
- 对每个头寸向量,生成回报向量。
我们需要分开计算头寸和回报,因为我们需要每个均线组合的头寸向量长度来进行随机映射。
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数据类型:
- key: 均线组合 (fast, slow) tuple类型,如,(5, 21)为5日21日均线组合
- value: 回报向量,Series类型,
这个Series是每天的回报,如0.012表示回报1.2%
为每个均线组合生成同样长度的随机头寸向量和相应的回报向量。
# 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数据结构:
- keys tuple类型, (短线的天数,长线的天数,交易信号数量)
- value Series类型,每天的收益率
L2 第二阶段
nummap 模块
对每个回报向量,计算 KPI 指标。
注:此处对回报进行了累积得到了累积回报
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均线策略和随机策略的各个分位数分布情况。
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号标识了最好和最坏情况
绿色表示均线跑赢随机,红色相反
从这里看出,大约75%的情况均线策略是胜过随机策略的;随机策略可能出现比较极端的情况;最后,只做多头,在股价大幅下跌50%的情况下,两种策略都很少有取得正收益的参数配置 :-(
比较 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 多个指标的权重,生成一个1-100的分数。
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 模块
把二维参数网格映射到矩阵向量
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