LOADING
1986 字
10 分钟
vectorbt学习_04StopSignals

学习笔记

OHLCSTX.run生成各类退出信号#

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

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

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简单分析下

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

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%内波动,则信号不会被触发。

继续分析如下代码块

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期末强制退出信号#

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

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#

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#

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',

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

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

可视化

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

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

各退出类型对应的收益率#

# 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,)

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

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

del01

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的总体统计,感觉说明不了什么。

各退出方式收益率分位图#

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

各退出方式胜率#

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

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

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#

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

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

交互式图表

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

dashboard.close()
vectorbt学习_04StopSignals
/posts/quant/ca006d43/
作者
思想的巨人
发布于
2023-10-15
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时