0%

立方体参数优化

构建参数搜索空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Define hyper-parameter space
# 49 fast x 49 slow x 19 signal
fast_windows, slow_windows, signal_windows = vbt.utils.params.create_param_combs(
(product, (combinations, np.arange(2, 51, 1), 2), np.arange(2, 21, 1)))

pd.DataFrame({"fast_windows":fast_windows, "slow_windows":slow_windows, "signal_windows":signal_windows})
Out[6]:
fast_windows slow_windows signal_windows
0 2 3 2
1 2 3 3
2 2 3 4
3 2 3 5
4 2 3 6
... ... ... ...
22339 49 50 16
22340 49 50 17
22341 49 50 18
22342 49 50 19
22343 49 50 20

计算指标和信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Run MACD indicator
macd_ind = vbt.MACD.run(
price,
fast_window=fast_windows,
slow_window=slow_windows,
signal_window=signal_windows
)

# Long when MACD is above zero AND signal
entries = macd_ind.macd_above(0) & macd_ind.macd_above(macd_ind.signal)

# Short when MACD is below zero OR signal
exits = macd_ind.macd_below(0) | macd_ind.macd_below(macd_ind.signal)

exits
Out[16]:
macd_fast_window 2 ... 49
macd_slow_window 3 ... 50
macd_signal_window 2 3 ... 19 20
split_idx 0 1 2 0 ... 2 0 1 2
0 False False False False ... False False False False
1 False False False False ... False False False False
2 False False False False ... False False False False
3 True True True True ... False False False False
4 True False True True ... False False False False
5 True False True True ... False False False False
6 True True False True ... False False False False
7 False False False False ... False False False False
8 False True True False ... False False False False
9 True False True True ... False False False False

可见这里的信号为持续性信号,不同与常规的crossxx信号

组合收益评估

1
2
3
# Build portfolio
pf = vbt.Portfolio.from_signals(
price.vbt.tile(len(fast_windows)), entries, exits, fees=0.001, freq='1D')

原始的1列price,经过range_split后变为3列,但是依然和entries列数不同,所以需要tile对齐列数据。

1
2
3
4
5
6
price.shape
Out[26]: (16, 3)
len(fast_windows)
Out[27]: 22344
entries.shape
Out[28]: (16, 67032)

tile函数效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Out[21]:price.head() 
split_idx 0 1 2
0 0.374540 0.304242 0.065052
1 0.950714 0.524756 0.948886
2 0.731994 0.431945 0.965632
3 0.598658 0.291229 0.808397
4 0.156019 0.611853 0.304614

price.vbt.tile(2)
Out[22]:
split_idx 0 1 2 0 1 2
0 0.374540 0.304242 0.065052 0.374540 0.304242 0.065052
1 0.950714 0.524756 0.948886 0.950714 0.524756 0.948886
2 0.731994 0.431945 0.965632 0.731994 0.431945 0.965632
3 0.598658 0.291229 0.808397 0.598658 0.291229 0.808397
4 0.156019 0.611853 0.304614 0.156019 0.611853 0.304614
5 0.155995 0.139494 0.097672 0.155995 0.139494 0.097672

可视化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Draw all window combinations as a 3D volume
fig = pf.total_return().vbt.volume(
x_level='macd_fast_window',
y_level='macd_slow_window',
z_level='macd_signal_window',
slider_level='split_idx',
trace_kwargs=dict(
colorbar=dict(
title='Total return',
tickformat='%'
)
)
)
fig.show()

会导致电脑卡顿,效果上也没觉得多么直观。
del01

此demo主要是演示如何对数据进行按日切分的,如果进行日内策略可参考,类似,也可以实现数据按照自然月或季度回测的目标。

生成原始随机series序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Generate sample price
price_idx = pd.date_range('2018-01-01 12:00:00', periods=48, freq='H')
np.random.seed(42)
price = pd.Series(np.random.uniform(size=price_idx.shape), index=price_idx)
print(price)
print(price.shape)

2018-01-03 02:00:00 0.684233
2018-01-03 03:00:00 0.440152
2018-01-03 04:00:00 0.122038
2018-01-03 05:00:00 0.495177
2018-01-03 06:00:00 0.034389
2018-01-03 07:00:00 0.909320
2018-01-03 08:00:00 0.258780
2018-01-03 09:00:00 0.662522
2018-01-03 10:00:00 0.311711
2018-01-03 11:00:00 0.520068
Freq: H, dtype: float64
(48,)

数据补齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Sessions must be equal - fill missing dates
# Fill on first date before 12:00 and on last date after 11:00
first_date = price.index[0].date()
last_date = price.index[-1].date()+timedelta(days=1)
filled_idx = pd.date_range(first_date, last_date, freq='H')
filled_price = price.reindex(filled_idx) #等于在原数据基础上扩展新增了部分数据
print(filled_price)

2018-01-01 00:00:00 NaN
2018-01-01 01:00:00 NaN
2018-01-01 02:00:00 NaN
2018-01-01 03:00:00 NaN
2018-01-01 04:00:00 NaN
..
2018-01-03 20:00:00 NaN
2018-01-03 21:00:00 NaN
2018-01-03 22:00:00 NaN
2018-01-03 23:00:00 NaN
2018-01-04 00:00:00 NaN
Freq: H, Length: 73, dtype: float64 #数据长度从48变成73

筛选交易时间内行情

1
2
3
4
# Remove dates that are outside of trading sessions
session_price_idx = filled_price.between_time('9:00', '17:00', include_end=False).index
session_price = filled_price.loc[session_price_idx]
print(session_price)

筛选出start_idxs,end_idxs,基于此切分session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Select first and last ticks of each trading session and split price into ranges between those ticks
start_idxs = session_price.index[session_price.index.hour == 9]
end_idxs = session_price.index[session_price.index.hour == 16]
price_per_session, _ = session_price.vbt(freq='1H').range_split(start_idxs=start_idxs, end_idxs=end_idxs)
print(price_per_session)

split_idx 0 1 2
0 NaN 0.139494 0.662522
1 NaN 0.292145 0.311711
2 NaN 0.366362 0.520068
3 0.374540 0.456070 NaN
4 0.950714 0.785176 NaN
5 0.731994 0.199674 NaN
6 0.598658 0.514234 NaN
7 0.156019 0.592415 NaN

可视化效果

1
session_price.vbt(freq='1H').range_split(start_idxs=start_idxs, end_idxs=end_idxs,plot=True)

del01

用于运行策略

1
2
3
4
# Run your strategy (here using random signals)
entries, exits = pd.DataFrame.vbt.signals.generate_random_both(price_per_session.shape, n=2, seed=42)
pf = vbt.Portfolio.from_signals(price_per_session, entries, exits, freq='1H')
print(pf.total_return())

学习笔记

OHLCSTX.run生成各类退出信号

退出信号:
平仓(卖出)的方式,最简单的,相对买入价的固定比例止损,
止损:比如,相比买入价下跌10%就卖出。
跟踪止损:相比持有期间的最高价,下跌10%就卖出。
止盈:比如,相比买入价,上涨达到10%就卖出,落袋为安。
超时退出:买入最多持有10天,10天到期后强制卖出。
以上退出信号未必100%达成(触发),如果价格波动非常小,有可能一直不会被触发,退化为持续持有策略

稍微复杂的是OHLCSTX相关代码
先参考官方文档中关于:OHLCSTX的内容:https://vectorbt.dev/api/signals/generators/#vectorbt.signals.generators.OHLCSTX
函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

OHLCSTX.run(
entries,
open,
high,
low,
close,
sl_stop=Default(nan), #止损
sl_trail=Default(False),#跟踪止损
tp_stop=Default(nan),#止盈
reverse=Default(False),
stop_price=nan,#In-place output array.
stop_type=-1,#In-place output array.
short_name='ohlcstx',
hide_params=None,
hide_default=True,
**kwargs
)

参考官方demo简单分析下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

entries = pd.Series([True, False, False, False, False, False])
price = pd.DataFrame({
'open': [10, 11, 12, 11, 10, 9],
'high': [11, 12, 13, 12, 11, 10],
'low': [9, 10, 11, 10, 9, 8],
'close': [10, 11, 12, 11, 10, 9]
})
ohlcstx = vbt.OHLCSTX.run(
entries,
price['open'], price['high'], price['low'], price['close'],
sl_stop=[0.1, 0.1, np.nan],
sl_trail=[False, True, False],
tp_stop=[np.nan, np.nan, 0.1])

这里的:
sl_stop=[0.1, 0.1, np.nan],
sl_trail=[False, True, False],
tp_stop=[np.nan, np.nan, 0.1]
对应了3种退出策略
sl_stop=[0.1,
sl_trail=[False,
tp_stop=[np.nan,
=》
止损幅度:0.1
止损:固定止损(非跟踪止损)
止盈幅度:0.1


sl_stop=[0.1,
sl_trail=[True,
tp_stop=[np.nan
=》
止损幅度:0.1
止损:跟踪止损
止盈幅度:无

sl_stop= np.nan],
sl_trail= False],
tp_stop= 0.1]
=》
止损幅度:无
止损:无
止盈幅度:0.1

参考上面含义解析,理解下面的信号结果output
直角方块:固定止损0.1,close=10,止损价9,所以最终退出价格为9,原因为止损退出
椭圆部分:跟踪止损0.1,high=13,止损价11.7,所以最终退出价格为11.7,原因为跟踪止损退出
圆角方框:无止损,止盈价10*1.1=11,所以最终退出价格为11,原因止盈退出

del01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
sl_exits = vbt.OHLCSTX.run(
entries,
ohlcv['Open'],
ohlcv['High'],
ohlcv['Low'],
ohlcv['Close'],
sl_stop=list(stops),
stop_type=None,
stop_price=None
).exits
ts_exits = vbt.OHLCSTX.run(
entries,
ohlcv['Open'],
ohlcv['High'],
ohlcv['Low'],
ohlcv['Close'],
sl_stop=list(stops),
sl_trail=True,
stop_type=None,
stop_price=None
).exits
tp_exits = vbt.OHLCSTX.run(
entries,
ohlcv['Open'],
ohlcv['High'],
ohlcv['Low'],
ohlcv['Close'],
tp_stop=list(stops),
stop_type=None,
stop_price=None
).exits

# 这3行代码原因参考下图数据的索引结构,目的是让多重索引保持对齐
sl_exits.vbt.rename_levels({'ohlcstx_sl_stop': 'stop_value'}, inplace=True)
ts_exits.vbt.rename_levels({'ohlcstx_sl_stop': 'stop_value'}, inplace=True)
tp_exits.vbt.rename_levels({'ohlcstx_tp_stop': 'stop_value'}, inplace=True)
ts_exits.vbt.drop_levels('ohlcstx_sl_trail', inplace=True)

del01

不同方式的退出信号达成率

信号达成率:退出是否被触发,比如止损5%,但是行情一直1%内波动,则信号不会被触发。

继续分析如下代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
print(pd.Series({
'SL': sl_exits.vbt.signals.total().mean(),
'TS': ts_exits.vbt.signals.total().mean(),
'TP': tp_exits.vbt.signals.total().mean()
}, name='avg_num_signals'))

SL 0.117000 #止损退出方式下,信号的平均达成率(成功固定止损)
TS 0.184667 #跟踪止损方式下,信号的平均达成率(成功跟踪止损)
TP 0.204750 #止盈方式下,信号的平均达成率(成功止盈)
Name: avg_num_signals, dtype: float64

pd.DataFrame({
'Stop Loss': sl_exits.vbt.signals.total().groupby('stop_value').mean(),
'Trailing Stop': ts_exits.vbt.signals.total().groupby('stop_value').mean(),
'Take Profit': tp_exits.vbt.signals.total().groupby('stop_value').mean()
}).vbt.plot(xaxis_title='Stop value', yaxis_title='Avg number of signals').show_svg()

del01

以 ‘Stop Loss’: sl_exits.vbt.signals.total().groupby(‘stop_value’).mean(),为例.
sl_exits退出方式下,stop_value从0.01->0.99不同取值下,对应的,达成率
所以:随着stop_value从小到大,分别意味着价格要下探到0.99,0.98 -> 0.01才能触发信号止损,故越靠右侧,曲线越接近与0
takeProfit线,就更明显了,止盈取值遇到,信号达成率越低。

merge期末强制退出信号

持有到期后(最后一天),生成卖出信号,强制卖出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sl_exits.iloc[-1, :] = True # 强制周期末尾退出信号为True,所以可能存在2个True情况
ts_exits.iloc[-1, :] = True
tp_exits.iloc[-1, :] = True

# Select one exit between two entries
sl_exits = sl_exits.vbt.signals.first(reset_by=entries, allow_gaps=True)# 2个True情况下,取得第一个True
ts_exits = ts_exits.vbt.signals.first(reset_by=entries, allow_gaps=True)
tp_exits = tp_exits.vbt.signals.first(reset_by=entries, allow_gaps=True)

print(pd.Series({
'SL': sl_exits.vbt.signals.total().mean(),# 由于每个标的的每个周期,都有且只有一个True信号,所以取值为1
'TS': ts_exits.vbt.signals.total().mean(),
'TP': tp_exits.vbt.signals.total().mean()
}, name='avg_num_signals'))

SL 1.0
TS 1.0
TP 1.0
Name: avg_num_signals, dtype: float64

持有到期hold_exits,随机退出rand_exits

1
2
3
4
5
6
7
8
hold_exits = pd.DataFrame.vbt.signals.empty_like(sl_exits)
hold_exits.iloc[-1, :] = True #买入并持有到期末的退出信号

print(hold_exits.shape)
rand_exits = hold_exits.vbt.shuffle(seed=seed)#随机卖出的退出信号

print(rand_exits.shape)

退出信号融合到columns.multiIndex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

exits = pd.DataFrame.vbt.concat(
sl_exits,
ts_exits,
tp_exits,
rand_exits,
hold_exits,
keys=pd.Index(exit_types, name='exit_type') #exit_types = ['SL', 'TS', 'TP', 'Random', 'Holding']
)

print(exits.shape)
(180, 60000)

print(exits.columns)
MultiIndex([( 'SL', 0.01, 0, '510050.XSHG'),
( 'SL', 0.01, 0, '510300.XSHG'),
( 'SL', 0.01, 0, '159901.XSHE'),
( 'SL', 0.01, 1, '510050.XSHG'),
( 'SL', 0.01, 1, '510300.XSHG'),
( 'SL', 0.01, 1, '159901.XSHE'),
( 'SL', 0.01, 2, '510050.XSHG'),
( 'SL', 0.01, 2, '510300.XSHG'),
( 'SL', 0.01, 2, '159901.XSHE'),
( 'SL', 0.01, 3, '510050.XSHG'),
...
('Holding', 1.0, 36, '159901.XSHE'),
('Holding', 1.0, 37, '510050.XSHG'),
('Holding', 1.0, 37, '510300.XSHG'),
('Holding', 1.0, 37, '159901.XSHE'),
('Holding', 1.0, 38, '510050.XSHG'),
('Holding', 1.0, 38, '510300.XSHG'),
('Holding', 1.0, 38, '159901.XSHE'),
('Holding', 1.0, 39, '510050.XSHG'),
('Holding', 1.0, 39, '510300.XSHG'),
('Holding', 1.0, 39, '159901.XSHE')],
names=['exit_type', 'stop_value', 'split_idx', 'symbol'], length=60000)

#可见vbt.concat实际效果是增加multiindex的维度,将各维度融合到一起
#新增了一个列的mulitindex,列明exit_type,取值
#sl_exits=》exit_types[0]='SL',
#ts_exits=》exit_types[1]='TS',
#tp_exits=》exit_types[2]='TP',

各退出方式,退出价对应持仓周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
avg_distance = entries.vbt.signals.between_ranges(other=exits)\ 
.duration.mean()\ #买入信号(为true)和卖出信号(为true)的距离的平均
.groupby(['exit_type', 'stop_value'])\ #根据退出类型和止损(退出)价格聚类
.mean()\ #聚类后平均
.unstack(level='exit_type')

print(avg_distance.mean())
exit_type
Holding 179.000000
Random 88.964167
SL 164.050500
TP 158.039583
TS 155.407917
dtype: float64

关于between_ranges,参考:https://vectorbt.dev/api/signals/accessors/#vectorbt.signals.accessors.SignalsAccessor.between_ranges
对于单列比对
del01

对于2列比对
del01

可视化

1
2
3
4
avg_distance[exit_types].vbt.plot(
xaxis_title='Stop value',
yaxis_title='Avg distance to entry'
).show_svg()

可见随机类型的平均持仓周期约为100,符合理论,随着Stop value的增大,持仓周期增大,意味着价格条件越苛刻,满足条件的标的越少,符合直观理解
del01

各退出类型对应的收益率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# del pf

from tqdm.auto import tqdm
import gc

total_returns = []

for i in tqdm(range(len(exit_types))):
chunk_mask = exits.columns.get_level_values('exit_type') == exit_types[i]
chunk_exits = exits.loc[:, chunk_mask]
chunk_pf = vbt.Portfolio.from_signals(ohlcv['Close'], entries, chunk_exits)
total_returns.append(chunk_pf.total_return())

del chunk_pf
gc.collect()

total_return = pd.concat(total_returns)
total_return

exit_type stop_value split_idx symbol
SL 0.01 0 510050.XSHG -0.048341
510300.XSHG -0.030499
159901.XSHE -0.050584
1 510050.XSHG -0.028360
510300.XSHG -0.035368
...
Holding 1.00 38 510300.XSHG 0.387240
159901.XSHE 0.475174
39 510050.XSHG 0.348895
510300.XSHG 0.409675
159901.XSHE 0.532666
Name: total_return, Length: 60000, dtype: float64

print(total_return.shape)
(60000,)

绘制特定类型的收益率分布

1
total_return_by_type = total_return.unstack(level='exit_type')[exit_types]

del01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print(total_return_by_type['Holding'].describe(percentiles=[]))

count 12000.000000
mean 0.105666
std 0.189775
min -0.331357
50% 0.120410
max 0.532666
Name: Holding, dtype: float64


total_return_by_type['SL'].vbt.histplot(
xaxis_title='Total return',
xaxis_tickformat='%',
yaxis_title='Count',
trace_kwargs=dict(marker_color=vbt.settings['plotting']['color_schema']['purple'])
).show_svg()

del01

由于这个数据是stopvalue止损价从0.01->0.99的总体统计,感觉说明不了什么。

各退出方式收益率分位图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print(pd.DataFrame({
'Mean': total_return_by_type.mean(),
'Median': total_return_by_type.median(),
'Std': total_return_by_type.std(),
}))
Mean Median Std
exit_type
SL 0.093459 0.103322 0.189059
TS 0.085152 0.094196 0.184434
TP 0.091800 0.104270 0.177477
Random 0.031920 0.013198 0.142426
Holding 0.105666 0.120410 0.189775

total_return_by_type.vbt.boxplot(
yaxis_title='Total return',
yaxis_tickformat='%'
).show_svg()

del01

各退出方式胜率

1
2
3
4
5
6
7
8
print((total_return_by_type > 0).mean().rename('win_rate'))
exit_type
SL 0.673667
TS 0.656500
TP 0.740917
Random 0.548500
Holding 0.733333
Name: win_rate, dtype: float64

不同止损方式在不同止损价位上的预期收益(期望收益)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
init_cash = vbt.settings.portfolio['init_cash']

def get_expectancy(total_return_by_type, level_name):
grouped = total_return_by_type.groupby(level_name, axis=0)
win_rate = grouped.apply(lambda x: (x > 0).mean())
avg_win = grouped.apply(lambda x: init_cash * x[x > 0].mean()).fillna(0)
avg_loss = grouped.apply(lambda x: init_cash * x[x < 0].mean()).fillna(0)
return win_rate * avg_win - (1 - win_rate) * np.abs(avg_loss)

expectancy_by_stop = get_expectancy(total_return_by_type, 'stop_value')

print(expectancy_by_stop.mean())
exit_type
SL 9.345944
TS 8.515217
TP 9.180045
Random 3.128960
Holding 10.566559
dtype: float64

expectancy_by_stop.vbt.plot(
xaxis_title='Stop value',
yaxis_title='Expectancy'
).show_svg()

这张图没太理解,从代码中的公式上看
图中y轴:胜率*平均收益-亏损概率*平均亏损 = 期望收益
但是随着stopvalue的上涨,期望收益不断靠近达到10? 这一点不是很理解

del01

看不懂todo

后面一部分看不懂了,暂时跳过吧,把图示截图出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
return_values = np.sort(total_return_by_type['Holding'].values)
idxs = np.ceil(np.linspace(0, len(return_values) - 1, 21)).astype(int)
bins = return_values[idxs][:-1]

def bin_return(total_return_by_type):
classes = pd.cut(total_return_by_type['Holding'], bins=bins, right=True)
new_level = pd.Index(np.array(classes.apply(lambda x: x.right)), name='bin_right')
return total_return_by_type.vbt.stack_index(new_level, axis=0)

binned_total_return_by_type = bin_return(total_return_by_type)

expectancy_by_bin = get_expectancy(binned_total_return_by_type, 'bin_right')

expectancy_by_bin.vbt.plot(
trace_kwargs=dict(mode='lines'),
xaxis_title='Total return of holding',
xaxis_tickformat='%',
yaxis_title='Expectancy'
).show_svg()

del01

交互式图表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
range_starts = pd.DatetimeIndex(list(map(lambda x: x[0], split_indexes)))
range_ends = pd.DatetimeIndex(list(map(lambda x: x[-1], split_indexes)))

symbol_lvl = total_return_by_type.index.get_level_values('symbol')
split_idx_lvl = total_return_by_type.index.get_level_values('split_idx')
range_start_lvl = range_starts[split_idx_lvl]
range_end_lvl = range_ends[split_idx_lvl]

asset_multi_select = ipywidgets.SelectMultiple(
options=symbols,
value=symbols,
rows=len(symbols),
description='Symbols'
)
dates = np.unique(yfdata.wrapper.index)
date_range_slider = ipywidgets.SelectionRangeSlider(
options=dates,
index=(0, len(dates)-1),
orientation='horizontal',
readout=False,
continuous_update=False
)
range_start_label = ipywidgets.Label()
range_end_label = ipywidgets.Label()
metric_dropdown = ipywidgets.Dropdown(
options=['Mean', 'Median', 'Win Rate', 'Expectancy'],
value='Expectancy'
)
stop_scatter = vbt.plotting.Scatter(
trace_names=exit_types,
x_labels=stops,
xaxis_title='Stop value',
yaxis_title='Expectancy'
)
stop_scatter_img = ipywidgets.Image(
format='png',
width=stop_scatter.fig.layout.width,
height=stop_scatter.fig.layout.height
)
bin_scatter = vbt.plotting.Scatter(
trace_names=exit_types,
x_labels=expectancy_by_bin.index,
trace_kwargs=dict(mode='lines'),
xaxis_title='Total return of holding',
xaxis_tickformat='%',
yaxis_title='Expectancy'
)
bin_scatter_img = ipywidgets.Image(
format='png',
width=bin_scatter.fig.layout.width,
height=bin_scatter.fig.layout.height
)

def update_scatter(*args, **kwargs):
_symbols = asset_multi_select.value
_from = date_range_slider.value[0]
_to = date_range_slider.value[1]
_metric_name = metric_dropdown.value

range_mask = (range_start_lvl >= _from) & (range_end_lvl <= _to)
asset_mask = symbol_lvl.isin(_symbols)
filtered = total_return_by_type[range_mask & asset_mask]

filtered_binned = bin_return(filtered)
if _metric_name == 'Mean':
filtered_metric = filtered.groupby('stop_value').mean()
filtered_bin_metric = filtered_binned.groupby('bin_right').mean()
elif _metric_name == 'Median':
filtered_metric = filtered.groupby('stop_value').median()
filtered_bin_metric = filtered_binned.groupby('bin_right').median()
elif _metric_name == 'Win Rate':
filtered_metric = (filtered > 0).groupby('stop_value').mean()
filtered_bin_metric = (filtered_binned > 0).groupby('bin_right').mean()
elif _metric_name == 'Expectancy':
filtered_metric = get_expectancy(filtered, 'stop_value')
filtered_bin_metric = get_expectancy(filtered_binned, 'bin_right')

stop_scatter.fig.update_layout(yaxis_title=_metric_name)
stop_scatter.update(filtered_metric)
stop_scatter_img.value = stop_scatter.fig.to_image(format="png")

bin_scatter.fig.update_layout(yaxis_title=_metric_name)
bin_scatter.update(filtered_bin_metric)
bin_scatter_img.value = bin_scatter.fig.to_image(format="png")

range_start_label.value = np.datetime_as_string(_from.to_datetime64(), unit='D')
range_end_label.value = np.datetime_as_string(_to.to_datetime64(), unit='D')

asset_multi_select.observe(update_scatter, names='value')
date_range_slider.observe(update_scatter, names='value')
metric_dropdown.observe(update_scatter, names='value')
update_scatter()


dashboard = ipywidgets.VBox([
asset_multi_select,
ipywidgets.HBox([
range_start_label,
date_range_slider,
range_end_label
]),
metric_dropdown,
stop_scatter_img,
bin_scatter_img
])
dashboard

del01

1
dashboard.close()

对应https://vectorbt.dev/getting-started/resources/的第一篇文章
Performance analysis of Moving Average Crossover,比特币,双均线,参数探测和可视化
需要对python工具包,pandas的series和dataframe有大致了解,否则代码的阅读会比较吃力。

文章概述

一共四部分
第一部分:数据查询和可视化
第二部分:Single window combination,单窗口组合
第三部分:Multiple window combinations,多参数组合测试
第四部分:Strategy comparison,策略比较

第一部分:数据查询和可视化

主要用来验证,数据查询没问题,需要关注复权情况,避免数据没做复权处理,避免分红,配股引入的回测偏差。

1
2
3
4
5
6
7
8
9
数据查询:
ohlcv_wbuf=dbtools.MySQLData.download('510050.XSHG').get() # 自带工具类查询

数据筛选和过滤
# Create a copy of data without time buffer
wobuf_mask = (ohlcv_wbuf.index >= start_date) & (ohlcv_wbuf.index <= end_date) # mask without buffer 计算指标时需要冗余数据
ohlcv = ohlcv_wbuf.loc[wobuf_mask, :]

绘制蜡烛图:ohlcv.vbt.ohlcv.plot().show_svg()

del01

第二部分:Single window combination,单窗口组合

观察指标的计算和信号的计算,触发等是否符合自己的设计思路,以及那些行情表现好,那些表现差,表现差的能否屏蔽或识别,过滤掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
确保无任何空值:
# there should be no nans after removing time buffer
assert (~fast_ma.ma.isnull().any())

单次金叉:fast_ma.ma_crossed_above(slow_ma)

绘制行情,指标,交易信号图:
fig = ohlcv['Open'].vbt.plot(trace_kwargs=dict(name='Price'))
fig = fast_ma.ma.vbt.plot(trace_kwargs=dict(name='Fast MA'), fig=fig)
fig = slow_ma.ma.vbt.plot(trace_kwargs=dict(name='Slow MA'), fig=fig)
fig = dmac_entries.vbt.signals.plot_as_entry_markers(ohlcv['Open'], fig=fig)
fig = dmac_exits.vbt.signals.plot_as_exit_markers(ohlcv['Open'], fig=fig)
fig.show_svg()

del01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
信号评估:dmac_entries.vbt.signals.stats(settings=dict(other=dmac_exits)) 

Start 2019-06-03 00:00:00+00:00
End 2020-06-01 00:00:00+00:00
Period 243 #开始-结束 交易日个数
Total 3 #交易次数(完整买卖,最后没卖出信号,自动卖出)
Rate [%] 1.234568 #todo
Total Overlapping 0 #重叠率,有重叠大概率说明买卖信号组合存在问题
Overlapping Rate [%] 0.0
First Index 2019-07-04 00:00:00+00:00 #推算应该是首次交易日
Last Index 2020-05-26 00:00:00+00:00
Norm Avg Index [-1, 1] 0.123967 #todo
Distance -> Other: Min 21.0 #最小持仓区间,下图A标记距离
Distance -> Other: Max 116.0 #最大持仓区间
Distance -> Other: Mean 68.5 #平均持仓区间
Distance -> Other: Std 67.175144
Total Partitions 3 #todo
Partition Rate [%] 100.0 #todo
Partition Length: Min 1.0
Partition Length: Max 1.0
Partition Length: Mean 1.0
Partition Length: Std 0.0
Partition Distance: Min 90.0 #2次买入信号最小间距,下图B标记距离
Partition Distance: Max 126.0 #2次买入信号最大间距
Partition Distance: Mean 108.0
Partition Distance: Std 25.455844
dtype: object

del01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
买卖信号图:(上图所示)
# Plot signals
fig = dmac_entries.vbt.signals.plot(trace_kwargs=dict(name='Entries'))
dmac_exits.vbt.signals.plot(trace_kwargs=dict(name='Exits'), fig=fig).show_svg()

交易结果分析:
# Build partfolio, which internally calculates the equity curve

# Volume is set to np.inf by default to buy/sell everything
# You don't have to pass freq here because our data is already perfectly time-indexed
dmac_pf = vbt.Portfolio.from_signals(ohlcv['Close'], dmac_entries, dmac_exits)

# Print stats
print(dmac_pf.stats())
Start 2019-06-03 00:00:00+00:00
End 2020-06-01 00:00:00+00:00
Period 243
Start Value 10000.0 #期初资金
End Value 9489.187544 #期末资金
Total Return [%] -5.108125 #总收益率
Benchmark Return [%] 6.669267 #基准回报率
Max Gross Exposure [%] 100.0 #最大总风险,todo
Total Fees Paid 121.927248 #总费用
Max Drawdown [%] 14.772497 #最大回撤
Max Drawdown Duration 138.0 #回撤持续区间
Total Trades 3 #总交易
Total Closed Trades 2 #todo
Total Open Trades 1 #todo
Open Trade PnL 168.683037 #todo
Win Rate [%] 50.0 #胜率
Best Trade [%] 0.77486 #0.77%收益率
Worst Trade [%] -7.528611 #-7.5%收益率
Avg Winning Trade [%] 0.77486 #盈利交易平均收益
Avg Losing Trade [%] -7.528611 #亏损交易平均收益
Avg Winning Trade Duration 116.0 #盈利交易持有平均周期
Avg Losing Trade Duration 21.0 #亏损交易持有平均周期
Profit Factor 0.102133 #todo
Expectancy -339.747747 #todo
dtype: object

交易历史明细单和可视化
# Plot trades
print(dmac_pf.trades.records)
dmac_pf.trades.plot().show_svg()

id col size entry_idx entry_price entry_fees exit_idx exit_price exit_fees pnl return direction status parent_id
0 0 0 3553.638170 22 2.807000 24.937656 138 2.842875 25.256373 77.292741 0.007749 0 1 0
1 1 0 3418.716194 148 2.940332 25.130406 169 2.733150 23.359660 -756.788234 -0.075286 0 1 1
2 2 0 3469.538407 238 2.679682 23.243153 242 2.735000 0.000000 168.683037 0.018143 0 0 2

del01

1
2
3
4
5
多组绩效同列比对  
# Equity
fig = dmac_pf.value().vbt.plot(trace_kwargs=dict(name='Value (DMAC)'))
hold_pf.value().vbt.plot(trace_kwargs=dict(name='Value (Hold)'), fig=fig).show_svg()

del01

1
2
3
4
5
6
7
8
9
10
可视化动态dashboard调参:
windows_slider.observe(on_value_change, names='value')
on_value_change({'new': windows_slider.value})

dashboard = widgets.VBox([
widgets.HBox([widgets.Label('Fast and slow window:'), windows_slider]),
dmac_img,
metrics_html
])
dashboard

del01

第三部分:Multiple window combinations,多参数组合测试

对策略涉及的参数进行提取,并测试这些参数组合,获得最佳的参数组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
组合测试:
# Pre-calculate running windows on data with time buffer
fast_ma, slow_ma = vbt.MA.run_combs(
ohlcv_wbuf['Open'], np.arange(min_window, max_window+1),
r=2, short_names=['fast_ma', 'slow_ma'])
print(fast_ma.ma.shape)
print(slow_ma.ma.shape)
print(fast_ma.ma.columns)
print(slow_ma.ma.columns)
(978, 4851)
(978, 4851)
Int64Index([ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
...
96, 96, 96, 96, 97, 97, 97, 98, 98, 99], dtype='int64', name='fast_ma_window', length=4851)
Int64Index([ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
...
97, 98, 99, 100, 98, 99, 100, 99, 100, 100], dtype='int64', name='slow_ma_window', length=4851)
这里需要注意的是4851怎么来的?
2:3->100(98)
3:4->100(97)
98:99->10(2)
99:100->100(1)
组合个数:(98+1)*98/2=4851
可以发现:原始的fast_ma.ma只有一个维度,长度978的float序列,现在多出一个维度,目前的ma多出的维度
fast_ma.ma.columns
Int64Index([ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
...
96, 96, 96, 96, 97, 97, 97, 98, 98, 99], dtype='int64', name='fast_ma_window', length=4851)

组合测试的信号生成:
表面和单指标相同
dmac_entries = fast_ma.ma_crossed_above(slow_ma)
print(dmac_entries.columns) # the same for dmac_exits
MultiIndex([( 2, 3),
( 2, 4),
( 2, 5),
( 2, 6),
( 2, 7),
( 2, 8),
( 2, 9),
( 2, 10),
( 2, 11),
( 2, 12),
...
(96, 97),
(96, 98),
(96, 99),
(96, 100),
(97, 98),
(97, 99),
(97, 100),
(98, 99),
(98, 100),
(99, 100)],
names=['fast_ma_window', 'slow_ma_window'], length=4851)
这里需要注意的fast_ma和slow_ma的columns本都是单个int取值,crossed后自动,由于columns不同组合,自动生成multiindex了。
组合测试回测评估
# Build portfolio
dmac_pf = vbt.Portfolio.from_signals(ohlcv['Close'], dmac_entries, dmac_exits)
dmac_perf = dmac_pf.deep_getattr(metric) #metric = 'total_return'

print(dmac_perf.shape)
print(dmac_perf.index)
(4851,)
MultiIndex([( 2, 3),
( 2, 4),
( 2, 5),
( 2, 6),
( 2, 7),
( 2, 8),
( 2, 9),
( 2, 10),
( 2, 11),
( 2, 12),
...
(96, 97),
(96, 98),
(96, 99),
(96, 100),
(97, 98),
(97, 99),
(97, 100),
(98, 99),
(98, 100),
(99, 100)],
names=['fast_ma_window', 'slow_ma_window'], length=4851)
可见:dmac_perf其实完成column转index,同时猜测如果metric含有多个取值,那么dmac_perf.columns也会增加。

最佳参数组:
# Calculate performance of each window combination
dmac_perf = dmac_pf.deep_getattr(metric) #metric = 'total_return'
dmac_perf.idxmax()
2维参数热力图可视化:
# Convert this array into a matrix of shape (99, 99): 99 fast windows x 99 slow windows
dmac_perf_matrix = dmac_perf.vbt.unstack_to_df(symmetric=True,
index_levels='fast_ma_window', column_levels='slow_ma_window')
dmac_perf_matrix.vbt.heatmap(
xaxis_title='Slow window',
yaxis_title='Fast window').show_svg()

del01

交互式图表,以及gif动图的生成,有点复杂了,感觉用处不大,不深究
del01

第四部分:Strategy comparison,策略比较

这一部分不是很懂干嘛用的,这个步骤的目标是什么,多个滚动时间窗口平均更能说明策略好坏
规避起始-结束时间区间,引入的回测误差,将策略运行周期也看做策略参数,比如,fast-slow-range,5-10-40,就是5日10日的双均线策略,在40日为一个单元情况下的收益分布。
但个人感觉类似40日这样可比性不强,由于波动性随着时间大概率有变化的,所以震荡市向单边市场靠近时,必然导致统计数据不准的情况。所以我也不是非常肯定,这种测试是用来说明什么的。
简单来说,这种策略测试,有意义,但意义不大,只能笼统看做是对策略开始看时间的敏感性测试。或是策略对单笔交易鲁棒性体现指标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
时间区间回测:
open_roll_wbuf, split_indexes = ohlcv_wbuf['Open'].vbt.range_split(
range_len=(ts_window + time_buffer).days, n=ts_window_n)

print(open_roll_wbuf.shape)
print(open_roll_wbuf.columns)
(465, 50)
Int64Index([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49], dtype='int64', name='split_idx')
比较容易理解,原始的1列数据,copy出50列,列索引从0-49。

# This will calculate moving averages for all date ranges and window combinations
fast_ma_roll, slow_ma_roll = vbt.MA.run_combs(
open_roll_wbuf, np.arange(min_window, max_window+1),
r=2, short_names=['fast_ma', 'slow_ma'])

print(fast_ma_roll.ma.shape)
print(fast_ma_roll.ma.columns)
(465, 242550) # 4851*50=242550
MultiIndex([( 2, 0),
( 2, 1),
( 2, 2),
( 2, 3),
( 2, 4),
( 2, 5),
( 2, 6),
( 2, 7),
( 2, 8),
( 2, 9),
...
(99, 40),
(99, 41),
(99, 42),
(99, 43),
(99, 44),
(99, 45),
(99, 46),
(99, 47),
(99, 48),
(99, 49)],
names=['fast_ma_window', 'split_idx'], length=242550)
从原始的常规columns数字索引,变成数字pair的二维multi索引。
# Generate crossover signals
dmac_entries_roll = fast_ma_roll.ma_crossed_above(slow_ma_roll)
print(dmac_entries_roll.columns)
MultiIndex([( 2, 3, 0),
( 2, 3, 1),
( 2, 3, 2),
( 2, 3, 3),
( 2, 3, 4),
( 2, 3, 5),
( 2, 3, 6),
( 2, 3, 7),
( 2, 3, 8),
( 2, 3, 9),
...
(99, 100, 40),
(99, 100, 41),
(99, 100, 42),
(99, 100, 43),
(99, 100, 44),
(99, 100, 45),
(99, 100, 46),
(99, 100, 47),
(99, 100, 48),
(99, 100, 49)],
names=['fast_ma_window', 'slow_ma_window', 'split_idx'], length=242550)
信号由原来的2维pair变成3维pair。

# Calculate the performance of the DMAC Strategy applied on rolled price
# We need to specify freq here since our dataframes are not more indexed by time
dmac_roll_pf = vbt.Portfolio.from_signals(close_roll, dmac_entries_roll, dmac_exits_roll, freq=freq)

dmac_roll_perf = dmac_roll_pf.deep_getattr(metric)

print(dmac_roll_perf.shape)
print(dmac_roll_perf.index)
(242550,)
MultiIndex([( 2, 3, 0),
( 2, 3, 1),
( 2, 3, 2),
( 2, 3, 3),
( 2, 3, 4),
( 2, 3, 5),
( 2, 3, 6),
( 2, 3, 7),
( 2, 3, 8),
( 2, 3, 9),
...
(99, 100, 40),
(99, 100, 41),
(99, 100, 42),
(99, 100, 43),
(99, 100, 44),
(99, 100, 45),
(99, 100, 46),
(99, 100, 47),
(99, 100, 48),
(99, 100, 49)],
names=['fast_ma_window', 'slow_ma_window', 'split_idx'], length=242550)
数据格式转换:
# Unstack this array into a cube
dmac_perf_cube = dmac_roll_perf.vbt.unstack_to_array(
levels=('fast_ma_window', 'slow_ma_window', 'split_idx'))

print(dmac_perf_cube.shape)
(98, 98, 50)
绘制fast-slow windows回测结果图
# For example, get mean performance for each window combination over all date ranges
heatmap_index = dmac_roll_perf.index.levels[0]
heatmap_columns = dmac_roll_perf.index.levels[1]
# np.nanmean取平均,所以最后是二维图而非立方体,https://www.python100.com/html/96013.html
heatmap_df = pd.DataFrame(np.nanmean(dmac_perf_cube, axis=2), index=heatmap_index, columns=heatmap_columns)
heatmap_df = heatmap_df.vbt.make_symmetric()

heatmap_df.vbt.heatmap(
xaxis_title='Slow window',
yaxis_title='Fast window',
trace_kwargs=dict(zmid=0, colorscale='RdBu')).show_svg()

del01

查看特定fast-slow windows参数组合的收益分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Or for example, compare a pair of window combinations using a histogram
window_comb1 = (10, 22)
window_comb2 = (73, 77)

# Get index of each window in strat_cube
fast1_idx = np.where(heatmap_df.index == window_comb1[0])[0][0]
slow1_idx = np.where(heatmap_df.columns == window_comb1[1])[0][0]
fast2_idx = np.where(heatmap_df.index == window_comb2[0])[0][0]
slow2_idx = np.where(heatmap_df.columns == window_comb2[1])[0][0]

print(fast1_idx, slow1_idx, fast2_idx, slow2_idx)

dmac_comb1_perf = dmac_perf_cube[fast1_idx, slow1_idx, :]
dmac_comb2_perf = dmac_perf_cube[fast2_idx, slow2_idx, :]

pd.DataFrame({str(window_comb1): dmac_comb1_perf, str(window_comb2): dmac_comb2_perf}).vbt.histplot().show_svg()

del01
由于每个参数对应50个不同的时间range,所以直方图列取值sum=50,可以近似看做特定参数组合的收益分布情况。

todo:补充,可以绘制各个参数的收益分布情况,可能更明显,选择高均值,低方差的参数组合,只是数据可能较多,100*100个组合。
可以笼统-》细化的思路处理,比如slow:1-》100,分成10个区间,1-》10,10-》20,fast也是类似的,这样可以找出平均收益最大的格子,锁定slow-fast区间,比如slow[10,20],fast:[20-30],之后再二次探测,类似迭代找局部最优解的思路。

用双均线策略和单纯的持有,以及随机买卖策略回测结果比对

1
2
3
4
5
6
7
8
pd.DataFrame({
'Random Strategy': rand_roll_perf,
'Hold Strategy': hold_roll_perf,
'DMAC Strategy': dmac_roll_perf,
}).vbt.histplot(
xaxis_title=metric,
yaxis_title='Cumulative # of tests',
trace_kwargs=dict(cumulative_enabled=True)).show_svg() # cumulative_enabled累加

del01

首先纵轴的250k是什么?

1
2
3
print(rand_roll_perf.shape)
(242550,)
就是之前的4851*50=242550

其次累积图,有点让人看不懂,不妨改为非累积

1
2
3
4
5
6
7
8
pd.DataFrame({
'Random Strategy': rand_roll_perf,
'Hold Strategy': hold_roll_perf,
'DMAC Strategy': dmac_roll_perf,
}).vbt.histplot(
xaxis_title=metric,
yaxis_title='Cumulative # of tests',
trace_kwargs=dict(cumulative_enabled=False)).show_svg()

del01
颜色上会有遮挡,hold策略收益分布较极端,dmac绿色部分,random对应绿色内部的深色部分。
这个能体现什么呢?也不是很懂,怎么评估优劣?,目前我也没看太懂。

时间维度绘制三种策略的收益变化图(平均收益)

1
2
3
4
5
6
7
pd.DataFrame({
'Random strategy': rand_roll_perf.groupby('split_idx').mean(),
'Hold strategy': hold_roll_perf.groupby('split_idx').mean(),
'DMAC strategy': dmac_roll_perf.groupby('split_idx').mean()
}).vbt.plot(
xaxis_title='Split index',
yaxis_title='Mean %s' % metric).show_svg()

del01
能体现什么信息呢?
大致体现随着时间窗口移动,策略整体有效性(由于上面用的mean平均收益,dmac_roll_perf.groupby(‘split_idx’).mean(),所以可以认为双均线策略的综合有效性)。
不过,由于不同参数的策略其实是完全不同的策略,所以感觉这组数据用来评估策略-时间关联性的说服力并不强。

下面是特定参数组合的例子。大致看出各参数组合策略收益稳定性。这个还是有一定说服力的。
del01

这个重点观察
先选定一组fast-slow windows参数
首先,思考下,本周一启动策略和下周一启动策略,那么策略执行结果相同么?肯定不同,如果本周触发交易信号,则由于交易序列不同,所以形成trads历史不同,最终收益自然也不同(策略对起始时间的敏感性,策略对单笔收益的鲁棒性,是否依靠某一笔收益取得正向结果)。由于我们不能乐观的估计,目前启动策略就一定位于高点上,所以需要采用窗口回测(windows=n)方法,得到一组收益数据。那么这组收益数据,就可以看做,是策略运行一个windows单位的最终收益分布。最优收益,最差收益,平均收益,以及收益稳定性。
所以重点关注这组fast-slow windows参数下:
01,理想的曲线时,都在0轴上方,越向上越好,均值大,波动小
02,是否稳定0轴上方, 如果0附近随机波动,说明类似掷筛子,如果有正均值还行,负均值就不理想了。
03,最高,最低点距离,希望波动小,波动大了,很可能今天进去,恰好赶上最差的周期,windows天后,悲提最差收益。
04,收益权限最高点,对应windows时间区间行情长相,说明策略对这一类行情有偏好。想办法筛选出。
同理,收益最低点,对应windows时间区间行情长相,说明策略对这一类行情有排斥。想办法过滤掉。

教程:https://vectorbt.dev/,started部分

Getting started

基础demo

1
2
3
4
5
6
7
8
9
10
btc_price = vbt.YFData.download('BTC-USD', start=start, end=end).get('Close')

fast_ma = vbt.MA.run(btc_price, 10, short_name='fast')
slow_ma = vbt.MA.run(btc_price, 20, short_name='slow')

entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)

pf = vbt.Portfolio.from_signals(btc_price, entries, exits)
pf.total_return()

多窗口+多标的

1
2
3
4
5
6
7
8
9
10
11
12
13
eth_price = vbt.YFData.download('ETH-USD', start=start, end=end).get('Close')
comb_price = btc_price.vbt.concat(eth_price,
keys=pd.Index(['BTC', 'ETH'], name='symbol'))
comb_price.vbt.drop_levels(-1, inplace=True)

fast_ma = vbt.MA.run(comb_price, [10, 20], short_name='fast')
slow_ma = vbt.MA.run(comb_price, [30, 30], short_name='slow')

entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)

pf = vbt.Portfolio.from_signals(comb_price, entries, exits)
pf.total_return()

del01

回测区间切分

1
2
3
4
5
6
7
8
9
10
mult_comb_price, _ = comb_price.vbt.range_split(n=2)
mult_comb_price
fast_ma = vbt.MA.run(mult_comb_price, [10, 20], short_name='fast')
slow_ma = vbt.MA.run(mult_comb_price, [30, 30], short_name='slow')

entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)

pf = vbt.Portfolio.from_signals(mult_comb_price, entries, exits, freq='1D')
pf.total_return()

del01

del01

Features

Pandas:对pandas做了改写和加速

1
2
3
Pandas acceleration: Compiled versions of most popular pandas functions, such as mapping, reducing, rolling, grouping, and resamping. For best performance, most operations are done strictly using NumPy and Numba. Attaches a custom accessor on top of pandas to easily switch between pandas and vectorbt functionality.
Flexible broadcasting: Mechanism for broadcasting array-like objects of arbitrary shapes, including pandas objects with MultiIndex.
Pandas utilities: Grouping columns, wrapping NumPy arrays, transforming pandas objects and their indexes, and more.

Data

1
2
3
4
5
Data acquisition: Supports various data providers, such as Yahoo Finance, Binance, CCXT and Alpaca. Can merge multiple symbols with different index, as well as update them.
Data generation: Supports various (random) data generators, such as GBM.
Scheduled data updates: Can periodically update any previously downloaded data.
Data preparation: Transformation, rescaling, and normalization of data. Custom splitters for cross-validation. Supports Scikit-Learn splitters, such as for K-Folds cross-validation.
Labeling for ML: Discrete and continuous label generation for effective training of ML models.

Indicators

1
2
Technical indicators: Most popular technical indicators with full Numba support, including Moving Average, Bollinger Bands, RSI, Stochastic, MACD, and more. Out-of-the-box support for 99% indicators in Technical Analysis Library, Pandas TA, and TA-Lib thanks to built-in parsers. Each indicator is wrapped with the vectorbt's indicator engine and thus accepts arbitrary hyperparameter combinations - from arrays to Cartesian products.
Indicator factory: Sophisticated factory for building custom technical indicators of any complexity. Takes a function and does all the magic for you: generates an indicator skeleton that takes inputs and parameters of any shape and type, and runs the vectorbt's indicator engine. The easiest and most flexible way to create indicators you will find in open source.

Signals

1
2
3
Signal analysis: Generation, mapping and reducing, ranking, and distribution analysis of entry and exit signals.
Signal generators: Random and stop loss (SL, TSL, TP, etc.) signal generators with full Numba support.
Signal factory: Signal factory based on indicator factory specialized for iterative signal generation.

Modeling

1
Portfolio modeling: The fastest backtesting engine in open source: fills 1,000,000 orders in 70-100ms on Apple M1. Flexible and powerful simulation functions for portfolio modeling, highly optimized for highest performance and lowest memory footprint. Supports two major simulation modes: 1) vectorized backtesting using user-provided arrays, such as orders, signals, and records, and 2) event-driven backtesting using user-defined callbacks. Supports shorting and individual as well as multi-asset mixed portfolios. Combines many features across vectorbt into a single behemoth class.

Analysis

1
2
3
4
5
Performance metrics: Numba-compiled versions of metrics from empyrical and their rolling versions. Adapter for QuantStats.
Stats builder: Class for building statistics out of custom metrics. Implements a preset of tailored statistics for many backtesting components, such as signals, returns, and portfolio.
Records and mapped arrays: In-house data structures for analyzing complex data, such as simulation logs. Fully compiled with Numba.
Trade analysis: Retrospective analysis of trades from various view points. Supports entry trades, exit trades, and positions.
Drawdown analysis: Drawdown statistics of any numeric time series.

Plotting

1
2
3
Data visualization: Numerous flexible data plotting functions distributed across vectorbt.
Figures and widgets: Custom interactive figures and widgets using Plotly, such as Heatmap and Volume. All custom widgets have dedicated methods for efficiently updating their state.
Plots builder: Class for building plots out of custom subplots. Implements a preset of tailored subplots for many backtesting components, such as signals, returns, and portfolio.

Extra

1
2
3
4
Notifications: Telegram bot based on Python Telegram Bot.
General utilities: Scheduling using schedule, templates, decorators, configs, and more.
Caching: Property and method decorators for caching most frequently used objects.
Persistance: Most Python objects including data and portfolio can be saved to a file and retrieved back using Dill.

Installation(略,已有)

代码和数据结构分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import numpy as np
import pandas as pd
from datetime import datetime

import vectorbt as vbt

# Prepare data
gbm_data = vbt.GBMData.download(
list(range(5)),
start='2023-01-01',
end='2023-06-01'
)
price=gbm_data.get()[0]
fast_ma = vbt.MA.run(price, 5)
slow_ma = vbt.MA.run(price, 10)
entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)

print(fast_ma.ma.head(40)[20:])
print(slow_ma.ma.head(40)[20:])
print(entries.head(40)[20:])
print(exits.head(40)[20:])

pf = vbt.Portfolio.from_signals(price, entries, exits, init_cash=100)
pf.total_profit()


fase_ma slow_ma entries exits
2023-01-20 16:00:00+00:00 84.647574 84.049794 True False
2023-01-21 16:00:00+00:00 83.575365 82.846123 False False
2023-01-22 16:00:00+00:00 82.321140 81.643256 False False
2023-01-23 16:00:00+00:00 82.111900 82.375203 False True
2023-01-24 16:00:00+00:00 83.728940 83.546189 True False
2023-01-25 16:00:00+00:00 86.367612 85.507593 False False
2023-01-26 16:00:00+00:00 89.732193 86.653779 False False
2023-01-27 16:00:00+00:00 92.474193 87.397667 False False
2023-01-28 16:00:00+00:00 93.575949 87.843924 False False
2023-01-29 16:00:00+00:00 93.334410 88.531675 False False
2023-01-30 16:00:00+00:00 92.383425 89.375519 False False
2023-01-31 16:00:00+00:00 93.128770 91.430482 False False
2023-02-01 16:00:00+00:00 95.725233 94.099713 False False
2023-02-02 16:00:00+00:00 99.180058 96.378003 False False
2023-02-03 16:00:00+00:00 103.323654 98.329032 False False
2023-02-04 16:00:00+00:00 106.262713 99.323069 False False
2023-02-05 16:00:00+00:00 108.586491 100.857631 False False
2023-02-06 16:00:00+00:00 111.566106 103.645670 False False
2023-02-07 16:00:00+00:00 114.347398 106.763728 False False
2023-02-08 16:00:00+00:00 114.953030 109.138342 False False

可见:
ma_crossed_above:金叉的第一个bar信号true,后续false
ma_crossed_below:死叉的第一个bar信号true,后续false

Usage(强悍的实例片段,略)

参考教程:
官方文档:https://vectorbt.dev/
vector-bt 例程介绍:https://www.jianshu.com/p/4fc4ce04b925

vectorbt中文资料有限,阅读官方英文材料效率较低,将学习过程梳理为笔记,方便大家学习。
由于水平所限,如有错误,欢迎指正。

docker失败

docker具体构建步骤参考

1
/home/john/docker/vectorbt/readme.txt

原始安装docker时这里会卡顿

1
2
3
4
5
6
7
8
Collecting plotly>=4.12.0
。。。

Collecting ccxt>=4.0.14
Downloading ccxt-4.1.2-py2.py3-none-any.whl (3.9 MB)

Collecting TA-Lib
Downloading TA-Lib-0.4.28.tar.gz (357 kB)

需要修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(base) john@john-HLYL-WXX9:~/docker/vectorbt/vectorbt$ git diff Dockerfile
diff --git a/Dockerfile b/Dockerfile
index 0200fa1..a9da8aa 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,6 +14,8 @@ RUN chmod -R +x scripts

ARG FULL="yes"

+RUN pip install plotly>=4.12.0 -i https://pypi.doubanio.com/simple ## 新增的行
+
RUN if [[ -n "${FULL}" ]] ; then \
scripts/install-talib.sh && pip install --no-cache-dir .[full] ; else \
pip install --no-cache-dir . ; fi
@@ -36,4 +38,4 @@ RUN if [[ -n "${TEST}" ]] ; then \

不可行,docker里安装talib失败。

conda+jupyter支持(依赖源码,成功)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
conda create -n vectorbt_env python=3.8
conda activate vectorbt_env

git clone git@github.com:polakowo/vectorbt.git vectorbt
pip install -e vectorbt

conda install pyyaml
pip install yfinance
pip install PyPortfolioOpt

pip install backtrader
pip install flask-SQLAlchemy
pip install pymysql
pip install -U kaleido

源码安装talib后pip安装(直接pip安装提示找不到头文件)
源码下载:https://ta-lib.org/install/
源码安装步骤:
$ untar and cd
$ ./configure --prefix=/usr
$ make
$ sudo make install

pip安装:pip install TA-Lib

pip install matplotlib==3.2.0 #cannot import name 'warnings' from 'matplotlib.dates'


增加jupyter支持 、

另外根据文档https://algotrading101.com/learn/vectorbt-guide/,可通过

1
pip install -U "vectorbt[full]"

安装完整版vectorbt(包含相关依赖项)

包升级

新增指标修改vectorbt源码和包升级

1
2
3
cd /home/john/git/repo_quant
conda activate vectorbt_env
pip install -e vectorbt

偶然刷到b站视频:
【灵魂拷问篇-02问】时间到底存不存在?从狭义相对论到圈量子引力,一个视频,为你重塑现代科学时空观:https://www.bilibili.com/video/BV1vm4y1e7Gv

里面对光速不变性的解释
突然想到这么个东西,弹簧圈:https://haokan.baidu.com/v?pd=wisenatural&vid=7959112340369363701
感觉光可能这个有点类似,是个弹性体,而非刚体,这样的话,即使绝对参考系里,也能推出光速不变性

一:基础假设:撒出去的光子,泼出去的水

刚体速度:对于刚体模型,一端发力会完整传导到另一端,所以刚体2端速度完全相同
光速(弹性体)问题:光本身电磁波,通过场感应产生的,所以光源移动会立马传导到几分钟前发出的光子上?当然不是。光子一旦发出,就速度恒定跑出去了。简单类比:破出去的水,不可能受到盆的影响吧?(盆子向后移动,然后泼出去的水也跟着向后跑?想想还怪吓人的)
当然这个纯属个人假设,没有理论基础,万一光子离开光源后,还实时受到光源影响呢?

二:绝对的上帝视角

引入绝对视角:上帝,他手中有个绝对原点和绝对尺子(就是绝对坐标系)
由于他是上帝么?他可以看到3重平行宇宙

其中
张三和李四,位于运动的板车上(就是运动着的坐标系,比如地球就是板车)
宇宙1:板车绝对静止,光1秒跑了c,板车长度也是c
宇宙2:板车朝着右侧运动速度0.5c
宇宙3:板车朝着右侧运动速度1c

此时巧了,各个宇宙的科学家张三,都开始计算光速了,开手电
那么就会发现这个现象,

宇宙3:1s后,第一缕手电光到达了哪里?显然是A线,基于假设:光子非刚体,一旦离开光源,不会受光源移动影响
首先,宇宙3中的光整体运行距离,当然是2c,但这个第一缕光达成的么?显然不是,第一缕光,在3个宇宙中都是同时到达A的。
宇宙3多出来的距离,纯属光源移动,导致的光柱的拉伸效果达到的。
那么宇宙3研究员计算的光速多少呢
从李四视角:他看到第一束光到达视网膜时间,以及最后一束光到达视网膜时间差多少?
第一束光会在B线处看到:时间0.5s
最后一束光等到李四移动到C线处:时间1.5s
得到光速:c/1s=c(为何这么算?计算光速的齿轮遮光法就是基于同样原理)
可见即使对于运动的宇宙3,其中研究员依然会计算出正确的光速c(也就是和静止宇宙1完全相同的速度)
对于宇宙2,我们也可以得到一样结论。
也就是说即使存在绝对坐标系,也能解释光速不变性,光在任何运动参考系中,测试的速度都是c(运动的参考系,不会影响真理的结论)

三:知乎更通俗的解释

这一种理解,知乎上还真有类似解释:而且解释的更通俗易懂
民科心中的物理(虽然这个名字看起来很不靠谱):https://www.zhihu.com/question/298096903/answer/2257455377
简单来说,对于3个宇宙,同样时间内发出光波个数核定的,假如为10hz,单个光波长度x=c/10,但是由于宇宙3速度快,所以光线扫过路程更长,光波长就是2c/10=2x。(简单来说,由于光源朝着远处运动,导致光波被拉长了。红移,就这么来的)。但是由于宇宙3的李四,也在朝着光波,以速度c运动,就会导致他眼中光波又被压缩了。刚才被拉长了1倍的,现在又原样压缩回去了。所以李四眼中光波长还是x,波长不变,个数不变,所以总距离也不变。

那么进一步扩展
对于向左运动的光源,上帝视角看:光线扫过面积如下图的第一个图:
但是里面科研人员测试的各个角度光速都是一样的(也就是不论整个参考系怎么运动,光速在各个方向上都是均匀的)
其根本原因就是:光源运动后,导致的光波被压缩或者拉伸效应,由于另一个统计人员也在同步运动,会把压缩的光波再拉伸回来。导致此人看到的光波长和完全静止的参考系真实波长完全相同。所以光速也完全相同。

四:速度和速度不同

光的速度和常规理解的速度不同, 常规意义上速度,由于物体刚体,所以计算一端移动速度就可以看做整体速度
而光可以看做弹性非常好的绳,一端拉动后,已经脱手后的另一端可以不受影响沿着之前速度跑,所以2端速度可以是不同的,而速度差,是由自身的拉长(或者缩短)弥补的。这里虽然只有一个东西(光柱),但2头速度是不同的,这里的速度和我们直观理解速度,显然已经不大一样了

五:另一个问题:静止的人看运动的光源,光速依然c(不会更快)

张三在地下,朝着右侧发出一束光,李四在天上高速朝右飞,手里手电朝右侧发射一束光,张三看李四手电筒光和李四看张三手电筒光速度多少?
根据:发出去的光子泼出去的水(不受光源影响),此时张三李四的光,运动状态应该完全一样,这两束光二者步调一致向前推进!

运动的李四看自己手电:根据之前结论,同一个参考系中看自己光源速度是c,不会受到参考系自身运动影响
再根据:自己手电光和静止的张三手电光由完全步调一致,
所有:运动的李四看静止的张三的手电光速度当然也是c。
同理:静止的张三看飞行李四的光依然是速度c。这种模型下,不能把张三和李四手里的光柱看做刚体模型(刚体下,一个端点运动会立刻传递给另一个端点),把光看做弹簧圈更合适!!

这里重点是“看”:这个看和常规意义“看“不同
比如:我们速度3km/h,一个小车速度5km/h,那么我们看小车速度:5km-3km=2km,那么我们和小车每小时距离变化为2km
光速的“看”不同,虽然李四看到的自己手电光速是c,但实际每秒钟,自己和第一缕光的距离增大c-v(v是自己移动速度),由于自己手电和自己运动方向一致,所以减法。如果v=0.5c,那么每秒二者距离增大0.5个c单位。
这个不对呀,不是说李四看自己手电光速是c么?这样c*1s=c对不上呀。
参考章节:三:知乎更通俗的解释,这里的“看”问题在于,**这里”看”意思是”测量”,”测量”特指:齿轮遮光法 测量(计算光速的方法)**,齿轮遮光法 对光速的计算其实是存在bug的。

其实还有另一种更容易理解方式:
如果这里的“看”,和我们常规理解“看”一个意思:
那么:张三距离第一缕速度c,李四距离第一缕速度c,所以张三和第一缕光的距离=李四和第一缕光的距离,而两人同时开手电时,第一缕光同时出现,反推出张三和李四位于同一位置,但显然不可能啊,同一个参考系,一个静止,一个固定方向运动,不可能处于同一位置。
B站上很多对光速的理解,也是真么理解,但这肯定不对的。

类比假设:重物比轻物的更快落地的是对的
那么构造场景:10kg大球绑1kg小球,
得出结论:
角度01结论:整体是11kg的东西,下落速度大于10kg,
角度02结论:1kg东西拖慢了10kg速度,所以下落速度小于10kg,故假设必然不成立。

避免物理实验,通过理论上的自我矛盾,直达真理,是不是很优雅!!

六:迈克尔逊-莫雷实验

简单来说,可以证明:以太不存在(更严格来说,和地球有相对运动的以太不存在),由于地球在宇宙中有运动,自转也算。所以地球上,光顺着以太和逆着以太时,速度应该不同。本身没问题,也很严谨,证明了以太不存在。但是如果以太存在,但是受到地球引力束缚,和地球一起运动,那么这个实验就啥也说明不了,最简单的,拿声波水波,复现这个实验,都可以得到声波水波不依赖某种介质传播的结论,显然,这俩肯定是依赖介质的
实验本身并没问题,目前也没证据显示以太有存在了,真空也能传递光线,可以认为电磁波可以不依赖目前已知东西传播。
关于这一点,b站这里有解释
相对论4︱迈克尔逊-莫雷实验为什么证明不了光速恒定?:https://www.bilibili.com/video/BV1eo4y1Q7oi

七:齿轮遮挡法的bug

先说理想的光速测量:速度测量距离/时间,如果是光速,那么理想测试方法就是
2个点:点A,点B
二者距离:x米
A点开灯,同时B点开始计时,A点第一缕光线到达B点,此时距离/时间差就是光速。
这种方法绝对精准,这应该没啥疑问吧。
问题:同时,现在是做不到的,因为光已经是现实中已知的信息传播的最快途径了,假设有了什么量子纠缠技术。真正达到同时,那么就没问题。

再说现实中光速测量,关于齿轮遮挡法,可以参考科普教程
光速是如何被测量出来的?巧妙的“齿轮遮挡法”:https://www.bilibili.com/video/BV1rg4y157Fi
但是,齿轮遮光发测出的光速,由于2面反射镜都是和光源位于同一个参考系且相对静止状态,所以根据前文解释(三:知乎更通俗的解释),光源相对绝对时空运动导致的光波“拉长”,“压缩”,由于观察者(这里就是镜子,接受光子的人)的同步运动,把“拉长”or“压缩”的光波再次“压缩”or”拉长”回来,导致测量的速度等于光线本身速度,也即是c。

如果采用齿轮遮光法测量水波,声波,也会得到同样结论:首先会精准的得到二者速度数据,同时也会得到结论,不论波源如何移动,测量的波速都是相同的。但是实际,如果波源朝某一个方向运动,那么它靠近这个方向的波边缘更近。所以这个放在光波边缘一样合理。

简单来说,按照绝对参考系对光速的理解方式,目前已知的实验其实都是正常成立。光波的特性类比到水波和声波一样成立,目前的是实验,不论是迈克尔逊-莫雷实验,还是 齿轮遮挡法测光速,类比水波声波也一样成立。唯一需要注意的是,B站大量的错误解读,导致的误解,比如:张三以0.5c的速度,朝着光线运动,那么他眼中光速是多少,c没错,但是,他距离光线边缘的距离变化呢?0.5c,而非c。本质就是因为这里的“看”,和我们直观理解的“看”,并非同一个意思。

八:总结

简单来说,电磁场无法传导力,所以不论光源怎么移动,光子只会以一个速度MOVE,光源左右移动,只会导致发出的光波长拉伸or压缩。无法传导到已经发送出去的光子上。
另一种理解就是把电磁波发生器看做打印机,打印机左右移动,会导致新打印出的字体被拉伸或者压缩,但不会对已经打印出的字带来任何影响(也无法对字体施加速度)。 当然,和电磁波相比,打印出的字无法自动扩散传播的。那就类比成在水面上打字吧,打印机自个移动不会加速水波的传递速度的
这一点声波也是一样的,相同状态的空气介质,声波不会因为声源移动而获得加速。

参考文章:常用技术指标之一文读懂RSI指标:https://blog.csdn.net/richardzhutalk/article/details/125348446

公式代码(略)

信号策略研发

趋势(区间突破)

实例


交易思路:
RSI指标大于70,认为牛市信号,买入
RSI指标小于60,认为牛市结束,平多
RSI指标小于30,认为熊市,做空
RSI指标大于40,认为熊市结束,平空

抽象化和信号表达式

信号表达式:

1
2
3
open_signal= if(rsi> 70,1,if(ris<30,-1,0))  
exit_signal_long=rsi-60
exit_signal_short=rsi-40

bt代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

class RSI_101(bt.Indicator):
lines = ('signal',)#k-d
params = dict(period=10)
plotinfo = dict(subplot=True)

def __init__(self):
rsi = bt.talib.RSI(self.datas[0].close, timeperiod=self.p.period)
self.lines.signal = bt.If(rsi>70,1,bt.If(rsi<30,-1,0))

class RSI_60(bt.Indicator):
lines = ('signal',)
params = dict(period=10)
# plotinfo = dict(subplot=False)

def __init__(self):
rsi = bt.talib.RSI(self.datas[0].close, timeperiod=self.p.period)
self.lines.signal = rsi-60
class RSI_40(bt.Indicator):
lines = ('signal',)
params = dict(period=10)
# plotinfo = dict(subplot=False)

def __init__(self):
rsi = bt.talib.RSI(self.datas[0].close, timeperiod=self.p.period)
self.lines.signal = rsi - 40

可视化和正确性验证

1
python ./signal_template.py --plot --market_type longshort --open_signal RSI_101 --exit_signal_long RSI_60 --exit_signal_short RSI_40 --fromdate 2022-01-10 --todate 2023-01-01

del02
验证:
开仓点:开多时rsi>70,开空时rsi<30
平多:rsi死叉60
平空:rsi金叉40
成立,可见思路无问题

波动(区间震荡)

实例

交易思路:
RSI指标小于70(死叉70水平线),认为即将回调,开空
RSI指标小于60,认为回调结束,平空
RSI指标大于30(金叉30水平线),认为即将反转上涨,做多
RSI指标大于40,认为反转结束,平多

抽象化和信号表达式

信号表达式:

1
2
3
4
5
open_signal_tmp01= bt.min(crossover(rsi,70),0)# 将-1,0,1转为-1,0.也就是只保留死叉那部分信号(丢弃金叉信号)
open_signal_tmp02= bt.max(crossover(rsi,30),0)# 将-1,0,1转为1,0.也就是只保留金叉那部分信号(丢弃死叉信号)
open_signal=open_signal_tmp01+open_signal_tmp02
exit_signal_long=rsi-60
exit_signal_short=rsi-40

bt代码

1
2
3
4
5
6
7
8
9
10
11
12
class RSI_inner101(bt.Indicator):
lines = ('signal',)
params = dict(period=10)
plotinfo = dict(subplot=True)

def __init__(self):
rsi = bt.talib.RSI(self.datas[0].close, timeperiod=self.p.period)
open_signal_tmp01 = bt.Min(bt.ind.CrossOver(rsi, 70), 0) # 将-1,0,1转为-1,0.也就是只保留死叉那部分信号(丢弃金叉信号)
open_signal_tmp02 = bt.Max(bt.ind.CrossOver(rsi, 30), 0) # 将-1,0,1转为1,0.也就是只保留金叉那部分信号(丢弃死叉信号)
open_signal = open_signal_tmp01 + open_signal_tmp02
self.lines.signal = open_signal

可视化和正确性验证

1
python ./signal_template.py --plot --market_type longshort --open_signal RSI_inner101 --exit_signal_long RSI_60 --exit_signal_short RSI_40 --fromdate 2022-01-10 --todate 2023-01-01  --stock_id 300760.XSHE

del02

回测

趋势(区间突破)

多头模式

多空模式

波动(区间震荡)

多头模式

多空模式

总结

整体效果并不好,主要是rsi指标变化较快,容易急速突破导致假信号

公式代码

1
2
3
BOLL:MA(CLOSE,N);
UB:BOLL+2*STD(CLOSE,N);
LB:BOLL-2*STD(CLOSE,N);

BOLL线:短线上下布林中,开口突破就大变
 经过长时间的总结发现,布林线中长期看来是一种优秀的趋势指标,当布林线由收口转至开口时,表示股价结束盘整,即将产生剧烈波动,而股价的突破方向,标志着未来趋势的运动方向。也即,股价向上突破阻力线,则是一轮上升趋势,反之,将是下跌趋势。同时。平均线与阻力线(或支撑线)构成的上行(楚游)通道对于把握股价的中长期走势 有着强烈的指示作用。

信号策略研发

趋势(区间突破)

实例

del02
交易思路:
点1:突破下轨,表示行情走差,做空。
点2:向上突破中轨,说明走差行情结束,平空。
点3:向上突破上轨,说明行情走强,做多。
点4:向下突破中轨,说明行情走弱,平多。

抽象化和信号表达式

del01
上图中,中线是mid中间线,曲线是close收盘价。
图中红色为多(1),蓝色为空(-1),采用蓝色代替绿色,考虑到部分人红绿色盲。

信号表达式:

1
2
3
open_signal= if(close> top,1,if(close<bot,-1,0))  
exit_signal_long=close>mid
exit_signal_short=close>mid

这个案例中为何是亏损的:高买低卖,因为这个case图示是震荡市。

bt代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# boll带,突破boll上界1,突破下界-1,否则0
class BollBands_broketopbot101(bt.Indicator):
lines = ('signal','top','bot','show') # 声明 signal 线,交易信号放在 signal line 上
params = dict(period=20)
plotinfo = dict(subplot=False)

def __init__(self):
self.lines.top = bt.indicators.BollingerBands(self.datas[0], period=self.p.period).top
self.lines.bot = bt.indicators.BollingerBands(self.datas[0], period=self.p.period).bot
self.lines.signal=bt.If(self.datas[0].close>self.lines.top,1,bt.If(self.datas[0].close<self.lines.bot,-1,0))
self.lines.show = self.l.signal*0.1*self.datas[0].close[0]+self.datas[0].close[0]

# boll带,中线上部:>0,中线下部:<0
class BollBands_mid11(bt.Indicator):
lines = ('signal','mid','show') # 声明 signal 线,交易信号放在 signal line 上
params = dict(period=20)
plotinfo = dict(subplot=False)

def __init__(self):
self.lines.mid = bt.indicators.BollingerBands(self.datas[0], period=self.p.period).mid
self.lines.signal =self.data-self.lines.mid
# self.lines.date0=self.data
self.lines.show= bt.If(self.l.signal>0,1, -1)*0.1*self.datas[0].close[0]+self.datas[0].close[0]

可视化和正确性验证

测试命令:

1
2
3
开仓信号测试:python ./signal_template.py --plot --market_type longshort --open_signal BollBands_broketopbot101  --fromdate 2022-01-10 --todate 2023-01-01
平仓信号测试:python ./signal_template.py --plot --market_type longshort --open_signal BollBands_mid11 --fromdate 2022-01-10 --todate 2023-01-01
开平仓信号测试:python ./signal_template.py --plot --market_type longshort --open_signal BollBands_broketopbot101 --exit_signal_long BollBands_mid11 --exit_signal_short BollBands_mid11 --fromdate 2022-01-10 --todate 2023-01-01

开仓信号测试
del01
可见:
当close高于boll.top时,信号signal为1。
当close低于boll.bot时,信号signal为-1。
符合本意。

平仓信号测试:
del01
可见:close位于均线上时,为做多,反之,做空。符合本意。

开平仓信号测试
del02
开仓信号:主图
平仓信号:下部辅图

波动(区间震荡)

实例

del02
交易思路:
点1:向下突破上轨,表示上涨行情即将回归,做空。
点2:向下突破中轨,说明行情回归结束,平空。
点3:向上突破下轨,说明下跌行情即将回归,做多。
点4:向下突破中轨,说明行情回归结束,平多。

抽象化和信号表达式

del01
上图中,横向是mid中线,上下是close收盘价

信号表达式:

1
2
3
4
5
open_signal_tmp01= bt.min(crossover(close,top),0)# 将-1,0,1转为-1,0.也就是只保留死叉那部分信号(丢弃金叉信号)
open_signal_tmp02= bt.max(crossover(close,bot),0)# 将-1,0,1转为-1,0.也就是只保留金叉那部分信号(丢弃死叉信号)
open_signal=open_signal_tmp01+open_signal_tmp02
exit_signal_long=mid-close
exit_signal_short=mid-close

bt代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# boll带,均值回归策略,死叉top,-1,金叉bot,1
class BollBands_crosstopbot101(bt.Indicator):
lines = ('signal','top','bot','show') # 声明 signal 线,交易信号放在 signal line 上
params = dict(period=20)
plotinfo = dict(subplot=True)

def __init__(self):
self.lines.top = bt.indicators.BollingerBands(self.datas[0], period=self.p.period).top
self.lines.bot = bt.indicators.BollingerBands(self.datas[0], period=self.p.period).bot
open_signal_tmp01 = bt.Min(bt.ind.CrossOver(self.datas[0].close, self.lines.top), 0)
open_signal_tmp02 = bt.Max(bt.ind.CrossOver(self.datas[0].close, self.lines.bot), 0)
open_signal = open_signal_tmp01 + open_signal_tmp02
self.lines.signal =open_signal
# self.lines.date0=self.data
self.lines.show= self.l.signal*0.1*self.datas[0].close[0]+self.datas[0].close[0]

class BollBands_blowmid11(bt.Indicator):
lines = ('signal','mid','show') # 声明 signal 线,交易信号放在 signal line 上
params = dict(period=20)
plotinfo = dict(subplot=False)

def __init__(self):
self.lines.mid = bt.indicators.BollingerBands(self.datas[0], period=self.p.period).mid
self.lines.signal =self.lines.mid-self.datas[0].close
self.lines.show= bt.If(self.l.signal>0,1,-1)*0.1*self.datas[0].close[0]+self.datas[0].close[0]

可视化和正确性验证

命令

1
2
3
4
5
开仓信号测试:python ./signal_template.py --plot --market_type longshort --open_signal BollBands_crosstopbot101 --fromdate 2022-01-10 --todate 2023-01-01
平仓信号测试:python ./signal_template.py --plot --market_type longshort --open_signal BollBands_blowmid11 --fromdate 2022-01-10 --todate 2023-01-01

开仓平仓结合测试:
python ./signal_template.py --plot --market_type longshort --open_signal BollBands_crosstopbot101 --exit_signal_long BollBands_blowmid11 --exit_signal_short BollBands_blowmid11 --fromdate 2022-01-10 --todate 2023-01-01

结果
开仓信号测试:
del01

可见:
a,信号正确,close靠近top时,频繁发出-1信号,说明频繁死叉top。close靠近bot时,频繁发出1信号,说明频繁金叉bot。符合逻辑
b,信号对应仓位正确,信号-1是开空仓,信号1时多仓,符合逻辑。

平仓信号测试:
del02

可见:当close位于mid上方时,信号提示空仓(用来平多),下方提示多仓(用来平空仓),符合预期。

开仓平仓结合测试:
del02
开仓信号:辅图
平仓信号:主图

回测

趋势(区间突破)

多头模式

标的:002466天齐锂业
时间:20220110-20230101
信号参数:boll周期10
回测命令:

1
python ./signal_template.py --plot --market_type longonly --open_signal BollBands_broketopbot101 --exit_signal_long  BollBands_mid11 --fromdate 2022-01-10 --todate 2023-01-01 --stock_id 002466.XSHE

回测结果:5w ->6.8W,收益率36%
del02

多空模式

标的:002466天齐锂业
时间:20220110-20230101
信号参数:boll周期15
回测命令:

1
python ./signal_template.py --plot --market_type longshort --open_signal BollBands_broketopbot101 --exit_signal_long  BollBands_mid11  --exit_signal_short  BollBands_mid11 --fromdate 2022-01-10 --todate 2023-01-01 --stock_id 002466.XSHE

del02
回测结果:5w ->6.5W,收益率30%

波动(区间震荡)

多头模式

标的:300760迈瑞医疗
时间:20220110-20230101
信号参数:2信号boll周期均为10

1
python ./signal_template.py --plot --market_type longonly --open_signal BollBands_crosstopbot101 --exit_signal_long BollBands_blowmid11 --fromdate 2022-01-10 --todate 2023-01-01  --stock_id 300760.XSHE

结果:5w->6w,收益率20%
del02

多空模式

标的:300760迈瑞医疗
时间:20220110-20230101
信号参数:2信号boll周期均为10

1
python ./signal_template.py --plot --market_type longshort --open_signal BollBands_crosstopbot101 --exit_signal_long BollBands_blowmid11 --exit_signal_short BollBands_blowmid11 --fromdate 2022-01-10 --todate 2023-01-01  --stock_id 300760.XSHE

结果:5w->7w,收益率40%
del02

基于信号思路实现常见策略系列。
参考前文的backtrader信号交易机制,各个技术必须转化为[1,0,-1]的表达方式后(这里用1代表正数,-1代表负数),才能利用backtrader信号交易机制。

公式代码

由于其产生的信号,本身就是非正即负(或0),所以不需要特别处理。

信号策略研发

实例,抽象化,交易信号,bt代码

实例

del01
交易思路:
点1:快均线由下至上穿过慢均线,说明行情由跌转涨,应该平空仓,开多仓
点2:快均线由上至下穿过慢均线,说明行情由涨转跌,应该平多仓,开空仓
双均线金叉买入(快线从下至上穿过慢线),死叉卖出(快线从上至下穿过慢线)。

抽象化和信号表达式


图中红色为多(1),蓝色为空(-1),采用蓝色代替绿色,考虑到部分人红绿色盲。

交易信号表达式

1
open_singal=fast_ma-close_ma  

bt代码

1
2
3
4
5
6
7
8
9
class DoubleMA(bt.Indicator):
lines = ('signal',) # 声明 signal 线,交易信号放在 signal line 上
params = dict( short_period=5, long_period=20)

def __init__(self):
self.s_ma = bt.ind.SMA(period=self.p.short_period)
self.l_ma = bt.ind.SMA(period=self.p.long_period)
# 短期均线上穿长期均线,取值为1;反之,短期均线下穿长期均线,取值为-1
self.lines.signal = self.s_ma-self.l_ma

可视化和正确性验证

Figure_0
主图蓝线就是signal线(做了平移和缩放,否则主图上显示不明显(由于是diff,取值较小))
可见和2均线的交叉点对应,所以信号计算没问题。

回测

多头模式(longonly)

股票模式,只允许做多,不允许做空

1
2
3
4
5
6
7
python ./signal_template.py --plot --main_signal_type longonly --main_signal DoubleMA  --fromdate 2022-01-10 --todate 2023-01-01 
v3:python ./signal_template.py --plot --market_type longonly --open_signal DoubleMA_11 --fromdate 2022-01-10 --todate 2023-01-01

标的:002466天齐锂业
时间:20220110-20230101
参数:快均线5,慢均线10
回测命令:python ./signal_template.py --plot --market_type longonly --open_signal DoubleMA_11 --fromdate 2022-01-10 --todate 2023-01-01 --stock_id 002466.XSHE

回测结果:从5w到7.5w,收益率约50%
del01

多空模式(longshort)

期货模式,既允许做多,也允许做空

1
2
3
4
5
6
7
python ./signal_template.py --plot --main_signal_type longonly --main_signal DoubleMA  --fromdate 2022-01-10 --todate 2023-01-01 
v3:python ./signal_template.py --plot --market_type longshort --open_signal DoubleMA_11 --fromdate 2022-01-10 --todate 2023-01-01

标的:002466天齐锂业
时间:20220110-20230101
参数:快均线5,慢均线15
可做空(融券)模式下

回测结果:初始资金5w,期末资金9w,收益率约80%
del02

总结

整个区间,趋势性较强,没有反复震荡导致频繁的亏损清仓+反向加仓等操作。