分享

wxpyton加回测引擎主流程跑通,使用pandas_bokeh作回测结果可视化(代码下载)

 AI量化实验室 2023-10-12 发布于北京

原创文章第284篇,专注“个人成长与财富自由、世界运作的逻辑与投资"。

今天继续,把回测引擎整合到gui中。

这里最大的难点在于,由于回测过程需要的时间比较耗时,不能直接跑在gui的主线程中,需要起子线程。不过子线程的状态,要在gui里显示出来,直接跨线程操作界面会出问题。

我们使用wxpython内置的startWorker启动一个后台工作线程。

job_id = 100
startWorker(self._end_backtest, self._start_backtest, jobID=job_id)

def _append_logs(self, message):
self.panel_backtest.text_logs.AppendText(message + '\r\n')

def _start_backtest(self):
self.panel_backtest.btn_backtest.Enable(False)
try:
name = self.combo_proj.GetValue()
if name not in self.loader.proj_dict.keys():
wx.MessageBox("策略不存在")
return
self.do_backtest(name)
except:
self._append_logs('执行回测出错')

def _end_backtest(self, something):
self._append_logs('回测成功完成!')
self.panel_backtest.btn_backtest.Enable(True)
self.panel_backtest.gauge_backtest.SetValue(0)

我们看一下一个典型的策略配置文件,toml格式:

name = '静待花开的聚宝盘'

[data]
start_date = '20100101'
end_date = ''
symbols = [
'511220.SH', #城投债
'512010.SH', # 医药
'518880.SH', #黄金
'163415.SZ', #兴全商业
'159928.SZ', # 消费
'161903.SZ', # 万家行业优选
'513100.SH' # 纳指
]
fields = ['roc(close,20)']
names = ['roc_20']

benchmarks=['000300.SH']

[[algos]]
name = 'RunDays'# 运行周期与再平衡
days=5

[[algos]]
name = 'SelectBySignal'
buy_rules=['ind(roc_20)>0.02']
sell_rules=['ind(roc_20)<-0.02']


[[algos]]
name = 'WeightEqually'

toml格式与dict字典是等价的。

全局的消息转发:

class GlobalHandler:
def __init__(self):
self.observers_fns = []

def add_observer_fn(self, fn):
self.observers_fns.append(fn)

def notify(self, data: dict):
for o in self.observers_fns:
o(data)


g = GlobalHandler()


def my_logger_notify(data):
g.notify({'msg_type': 'LOGGER', 'data': data})


from loguru import logger

logger.add(my_logger_notify)

注意最后这两句,loguru可以转logger的日志传给GlobalHandler。

而界面“观察”全局处理器,进而把logger显示在gui上。

进度条更新:

同样的使用event_handler,影响on_bar事件即可,这样代码的耦合最低。

def _event_handler(self, data: dict):
# print('logger......')
if 'msg_type' in data.keys() and data['msg_type'] == 'LOGGER':
self._append_logs(data['data'])
if data['msg_type'] == 'ON_BAR':
self.panel_backtest.gauge_backtest.SetValue(data['step'])

我们使用bokeh来显现回测结果可视化。

要特别注意一点是,在计算线程里,如果调用browser的SetPage是无效的。这里“浪费”了我1个小时,应该是在call_back里调用,然后更新回测结果:

_end_backtest

from bokeh.plotting import figure, show
from bokeh.layouts import row
from bokeh.models import HoverTool
from bokeh.models import ColumnDataSource
import numpy as np
import pandas as pd
from datetime import datetime
from engine.datafeed.dataloader import Hdf5Dataloader

import pandas_bokeh

from bokeh.models.widgets import DataTable, TableColumn
from bokeh.models import ColumnDataSource

np.random.seed(55)


class BokehMgr:
def plot_line(self, df, y_col, **kwargs):
df.plot_bokeh(kind="line", y=[y_col, 'open'], **kwargs)

def _calc_metrics(self, portfolio_df):
returns = portfolio_df['market_value'].pct_change()
loader = Hdf5Dataloader(['399006.SZ'], start_date="20100101")
bench_df = loader.load()
returns.name = '策略'
# if self.benchmarks_df is not None and len(self.benchmarks_df):

benchmark_returns = bench_df.pivot_table(index='date', columns='symbol', values='return_0')
returns = pd.concat([returns, benchmark_returns], axis=1)

from empyrical import max_drawdown, sharpe_ratio, annual_return

returns.dropna(inplace=True)

print(annual_return(returns))
print(max_drawdown(returns))

from engine.performance import PerformanceUtils
df_ratio, df_corr = PerformanceUtils().calc_rates(returns)
print(df_ratio)
df_ratio['名称'] = df_ratio.index
return df_ratio, df_corr

def _show_table(self, df):
data_table = DataTable(
columns=[TableColumn(field=Ci, title=Ci) for Ci in df.columns],
source=ColumnDataSource(df),
height=300,
)
return data_table

def show(self, backtest_h5, plot=False, return_html=True):
with pd.HDFStore(backtest_h5) as s:
portfolio_df = s['portfolio_df']
orders_df = s['orders_df']
orders_df['date'] = orders_df['date'].apply(lambda x: x.strftime('%Y-%m-%d'))

portfolio_df['equity'] = (portfolio_df['market_value'].pct_change() + 1).cumprod()

df_metrics, df_corr = self._calc_metrics(portfolio_df=portfolio_df)
table_metrics = self._show_table(df_metrics)
table_orders = self._show_table(orders_df)
table_corr = self._show_table(df_corr)

# 创建散点图:
p_equity = portfolio_df.plot_bokeh.line(
y="equity",
# category="species",
title="投资万元波动图",
show_figure=False,
rangetool=False,
)

p_market_value = portfolio_df.plot_bokeh.line(
y="market_value",
title="投资组合市值",
show_figure=False,
# rangetool=True,
)

# Combine Table and Scatterplot via grid layout:
html = pandas_bokeh.plot_grid([[p_equity, table_metrics], [table_orders, table_corr]], show_plot=plot,
return_html=return_html)
return html


if __name__ == '__main__':
from engine.datafeed.dataloader import Hdf5Dataloader
from engine.config import etfs_indexes

symbols = etfs_indexes.values()
loader = Hdf5Dataloader(['000300.SH'], start_date="20100101")
fields = ["roc(close,20)", "shift(close, -5)/shift(open, -1) - 1",
"qcut(label_c, 10)"
]
names = ["roc_20", "label_c",
'label'
]

# df = loader.load(fields=fields, names=names)
# df.dropna(inplace=True)
# df.index = pd.to_datetime(df.index)
# df['equity'] = (1 + df['return_0']).cumprod()

from engine.config import DATA_RESULTS

html = BokehMgr().show(backtest_h5=DATA_RESULTS.joinpath('等权策略.h5').resolve(), plot=True, return_html=False)
# print(html)

通过gui回测,主流程已经跑通了,后续要加上规则/模型的配置界面即可。

代码已经上传至星球,请大家请往下载。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多