vector-bt 例程介绍

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 交易信号

根据条件计算交易信号

  1. 当快速均线上穿慢速均线时,做多,反之,做空
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)
  1. 无论何时,如果价格下跌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

    1. 对每个均线组合,生成头寸向量。
# 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类型

    1. 对每个头寸向量,生成回报向量。

我们需要分开计算头寸和回报,因为我们需要每个均线组合的头寸向量长度来进行随机映射。

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  
均线策略KPI分布
随机策略KPI分布

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  
各参数组合热力图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343