学习笔记
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,原因止盈退出

sl_exits = vbt.OHLCSTX.run( entries, ohlcv['Open'], ohlcv['High'], ohlcv['Low'], ohlcv['Close'], sl_stop=list(stops), stop_type=None, stop_price=None).exitsts_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).exitstp_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)
不同方式的退出信号达成率
信号达成率<退出是否被触发>退出是否被触发>,比如止损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()
以 ‘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, :] = Truetp_exits.iloc[-1, :] = True
# Select one exit between two entriessl_exits = sl_exits.vbt.signals.first(reset_by=entries, allow_gaps=True)# 2个True情况下,取得第一个Truets_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.0TS 1.0TP 1.0Name: 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_typeHolding 179.000000Random 88.964167SL 164.050500TP 158.039583TS 155.407917dtype: float64关于between_ranges,参考:https://vectorbt.dev/api/signals/accessors/#vectorbt.signals.accessors.SignalsAccessor.between_ranges
对于单列比对

对于2列比对

可视化
avg_distance[exit_types].vbt.plot( xaxis_title='Stop value', yaxis_title='Avg distance to entry').show_svg()可见随机类型的平均持仓周期约为100,符合理论,随着Stop value的增大,持仓周期增大,意味着价格条件越苛刻,满足条件的标的越少,符合直观理解。

各退出类型对应的收益率
# del pf
from tqdm.auto import tqdmimport 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 symbolSL 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.532666Name: total_return, Length: 60000, dtype: float64
print(total_return.shape)(60000,)绘制特定类型的收益率分布
total_return_by_type = total_return.unstack(level='exit_type')[exit_types]
print(total_return_by_type['Holding'].describe(percentiles=[]))
count 12000.000000mean 0.105666std 0.189775min -0.33135750% 0.120410max 0.532666Name: 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()
由于这个数据是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 Stdexit_typeSL 0.093459 0.103322 0.189059TS 0.085152 0.094196 0.184434TP 0.091800 0.104270 0.177477Random 0.031920 0.013198 0.142426Holding 0.105666 0.120410 0.189775
total_return_by_type.vbt.boxplot( yaxis_title='Total return', yaxis_tickformat='%').show_svg()
各退出方式胜率
print((total_return_by_type > 0).mean().rename('win_rate'))exit_typeSL 0.673667TS 0.656500TP 0.740917Random 0.548500Holding 0.733333Name: 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_typeSL 9.345944TS 8.515217TP 9.180045Random 3.128960Holding 10.566559dtype: float64
expectancy_by_stop.vbt.plot( xaxis_title='Stop value', yaxis_title='Expectancy').show_svg()这张图没太理解,从代码中的公式上看
图中y轴:胜率*平均收益-亏损概率*平均亏损 = 期望收益
但是随着stopvalue的上涨,期望收益不断靠近达到10? 这一点不是很理解

看不懂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()
交互式图表
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
dashboard.close()部分信息可能已经过时