From bee61d79b0fb841df2c555767793528ed73b95b4 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Thu, 11 Apr 2019 14:01:29 +0800 Subject: [PATCH] [Add]CtaBackteserApp for GUI based backtesting --- tests/trader/run.py | 2 + vnpy/app/cta_backtester/__init__.py | 17 + vnpy/app/cta_backtester/engine.py | 199 +++++++++ vnpy/app/cta_backtester/ui/__init__.py | 1 + vnpy/app/cta_backtester/ui/backtester.ico | Bin 0 -> 64478 bytes vnpy/app/cta_backtester/ui/widget.py | 476 ++++++++++++++++++++++ vnpy/app/cta_strategy/__init__.py | 1 + vnpy/app/cta_strategy/backtesting.py | 5 +- 8 files changed, 699 insertions(+), 2 deletions(-) create mode 100644 vnpy/app/cta_backtester/__init__.py create mode 100644 vnpy/app/cta_backtester/engine.py create mode 100644 vnpy/app/cta_backtester/ui/__init__.py create mode 100644 vnpy/app/cta_backtester/ui/backtester.ico create mode 100644 vnpy/app/cta_backtester/ui/widget.py diff --git a/tests/trader/run.py b/tests/trader/run.py index 1ab073c5..2cda6ae2 100644 --- a/tests/trader/run.py +++ b/tests/trader/run.py @@ -16,6 +16,7 @@ from vnpy.gateway.huobi import HuobiGateway from vnpy.app.cta_strategy import CtaStrategyApp from vnpy.app.csv_loader import CsvLoaderApp from vnpy.app.algo_trading import AlgoTradingApp +from vnpy.app.cta_backtester import CtaBacktesterApp def main(): @@ -35,6 +36,7 @@ def main(): main_engine.add_gateway(HuobiGateway) main_engine.add_app(CtaStrategyApp) + main_engine.add_app(CtaBacktesterApp) main_engine.add_app(CsvLoaderApp) main_engine.add_app(AlgoTradingApp) diff --git a/vnpy/app/cta_backtester/__init__.py b/vnpy/app/cta_backtester/__init__.py new file mode 100644 index 00000000..c589e02f --- /dev/null +++ b/vnpy/app/cta_backtester/__init__.py @@ -0,0 +1,17 @@ +from pathlib import Path + +from vnpy.trader.app import BaseApp + +from .engine import BacktesterEngine, APP_NAME + + +class CtaBacktesterApp(BaseApp): + """""" + + app_name = APP_NAME + app_module = __module__ + app_path = Path(__file__).parent + display_name = "CTA回测" + engine_class = BacktesterEngine + widget_name = "BacktesterManager" + icon_name = "backtester.ico" diff --git a/vnpy/app/cta_backtester/engine.py b/vnpy/app/cta_backtester/engine.py new file mode 100644 index 00000000..621330d2 --- /dev/null +++ b/vnpy/app/cta_backtester/engine.py @@ -0,0 +1,199 @@ +import os +import importlib +from datetime import datetime +from threading import Thread +from pathlib import Path + +from vnpy.event import Event, EventEngine +from vnpy.trader.engine import BaseEngine, MainEngine +from vnpy.app.cta_strategy import ( + CtaTemplate, + BacktestingEngine, + OptimizationSetting +) + + +APP_NAME = "CtaBacktester" + +EVENT_BACKTESTER_LOG = "eBacktesterLog" +EVENT_BACKTESTER_BACKTESTING_FINISHED = "eBacktesterBacktestingFinished" +EVENT_BACKTESTER_OPTIMIZATION_FINISHED = "eBacktesterOptimizationFinished" + + +class BacktesterEngine(BaseEngine): + """ + For running CTA strategy backtesting. + """ + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__(main_engine, event_engine, APP_NAME) + + self.classes = {} + self.backtesting_engine = None + self.thread = None + + self.result_df = None + self.result_statistics = None + + self.load_strategy_class() + + def init_engine(self): + """""" + self.write_log("初始化CTA回测引擎") + + self.backtesting_engine = BacktestingEngine() + # Redirect log from backtesting engine outside. + self.backtesting_engine.output = self.write_log + + self.write_log("策略文件加载完成") + + def write_log(self, msg: str): + """""" + event = Event(EVENT_BACKTESTER_LOG) + event.data = msg + self.event_engine.put(event) + + def load_strategy_class(self): + """ + Load strategy class from source code. + """ + app_path = Path(__file__).parent.parent + path1 = app_path.joinpath("cta_strategy", "strategies") + self.load_strategy_class_from_folder( + path1, "vnpy.app.cta_strategy.strategies") + + path2 = Path.cwd().joinpath("strategies") + self.load_strategy_class_from_folder(path2, "strategies") + + def load_strategy_class_from_folder(self, path: Path, module_name: str = ""): + """ + Load strategy class from certain folder. + """ + for dirpath, dirnames, filenames in os.walk(path): + for filename in filenames: + if filename.endswith(".py"): + strategy_module_name = ".".join( + [module_name, filename.replace(".py", "")]) + self.load_strategy_class_from_module(strategy_module_name) + + def load_strategy_class_from_module(self, module_name: str): + """ + Load strategy class from module file. + """ + try: + module = importlib.import_module(module_name) + + for name in dir(module): + value = getattr(module, name) + if (isinstance(value, type) and issubclass(value, CtaTemplate) and value is not CtaTemplate): + self.classes[value.__name__] = value + except: # noqa + msg = f"策略文件{module_name}加载失败,触发异常:\n{traceback.format_exc()}" + self.write_log(msg) + + def get_strategy_class_names(self): + """""" + return list(self.classes.keys()) + + def run_backtesting( + self, + class_name: str, + vt_symbol: str, + interval: str, + start: datetime, + end: datetime, + rate: float, + slippage: float, + size: int, + pricetick: float, + capital: int, + setting: dict + ): + """""" + self.result_df = None + self.result_statistics = None + + engine = self.backtesting_engine + engine.clear_data() + + engine.set_parameters( + vt_symbol=vt_symbol, + interval=interval, + start=start, + end=end, + rate=rate, + slippage=slippage, + size=size, + pricetick=pricetick, + capital=capital + ) + + strategy_class = self.classes[class_name] + engine.add_strategy( + strategy_class, + setting + ) + + engine.load_data() + engine.run_backtesting() + self.result_df = engine.calculate_result() + self.result_statistics = engine.calculate_statistics(output=False) + + # Clear thread object handler. + self.thread = None + + # Put backtesting done event + event = Event(EVENT_BACKTESTER_BACKTESTING_FINISHED) + self.event_engine.put(event) + + def start_backtesting( + self, + class_name: str, + vt_symbol: str, + interval: str, + start: datetime, + end: datetime, + rate: float, + slippage: float, + size: int, + pricetick: float, + capital: int, + setting: dict + ): + if self.thread: + self.write_log("已有回测在运行中,请等待完成") + return False + + self.thread = Thread( + target=self.run_backtesting, + args=( + class_name, + vt_symbol, + interval, + start, + end, + rate, + slippage, + size, + pricetick, + capital, + setting + ) + ) + self.thread.start() + + return True + + def get_result_df(self): + """""" + return self.result_df + + def get_result_statistics(self): + """""" + return self.result_statistics + + def get_default_setting(self, class_name: str): + """""" + strategy_class = self.classes[class_name] + return strategy_class.get_class_parameters() diff --git a/vnpy/app/cta_backtester/ui/__init__.py b/vnpy/app/cta_backtester/ui/__init__.py new file mode 100644 index 00000000..a02dafb3 --- /dev/null +++ b/vnpy/app/cta_backtester/ui/__init__.py @@ -0,0 +1 @@ +from .widget import BacktesterManager diff --git a/vnpy/app/cta_backtester/ui/backtester.ico b/vnpy/app/cta_backtester/ui/backtester.ico new file mode 100644 index 0000000000000000000000000000000000000000..647b8ca91d4d0658d1ea00133f8f1c1e49bbd007 GIT binary patch literal 64478 zcmeI5d#oH)9mluah6`9IZ%cXHLcw6e8zU$}D~J`O;VBBH)L2Bs#%QX58s#>Cny5kf z!^B5Id?fh7$RGTHG4v)xgGNN9D1t%-wSuUCtu3^4{d{NV%$+@VchBrRc4vEkgP)!` zGjo3DobT_P-^`rdy*3(i_;2zP4gOu!cwl>@ad4y2I2I;hGw67uOb8&m-kRAq? zz{LewLHT{a*;Xq13!n`97h-3>*ruyP_78%KplJJLL$+iyZC`b&?xOakfwV~5S3S~9 z+Ld8nzKrZ=^#5G0Uj@w)cnKS)!fxR7ll@Df4EwV2CKv-}U;E3UEc>$M+gF`s*_Rg5 z#I;{qNi%6zMjflL6WQ;>|0_$Xdoy-U0pI@`*Jbog8lB{wD=#^&*FbrBR3!Y)Z%|wJB$Bejr?)t@)rN| zYbOmY{)hFm_#f8D%|}1A_#f8J;(u5pe_Xk|#sB=;NkfbOVf`%rhc$BZ(N8V@hqbf# zAJ)hpS1xbyKfiX;(BgksKa2lijof_nQ;Yv$?dll+%l!T4nZ%j&@AK^Uf9+$UZvNNr zCy#*};QE5BrTn3g{{6K1zux|LzjLlCHUHPt|DoTX7d=lL(3mp+oB!?i|I=fjHuwLc z^E*`D=6}b(7h(RNo(ul_V<>OyKl!zjwr%S_q4`_%yv=``|MvVhV+>gQ&mY4q{^!@u z;(s>nX3hG4(ffO-yxsq2#D2}eJn{bu%58-g3-S-jp9s<4{|ouA=y~d~^`DtBr(geT zEqx*A?_R#OAZJrvf6Fw}-yb#qN8?v>Wte~eQFBQC(mHBU)R*M5nWFv;m8UM7{{`lM zNZ-1jH~$yA|JU8Wq4t^o&Hs#nL4E$Wow`En$63zT=KU`;zv_N|fb;(}>Us(`733Mp zpAWV8``>lW9fIEd#zJ-2J4NN2>Hu5+%c*p%pVsFOfb&3~trS5vWGng(OU`CxE=E1G*8lfhytI*pUSh`j zRkbePtLjqD%|`!wKHrV}9&i}E1tNV86({UuWAf*tFO%Cv(5i0f6ih5c(?;L z!jrHG{vPiu|7AD|bZs;a%GACNwq}d}o&Vx=Z_?{PzTO7SF8q(<8XK3xUJ#50b?|9F zZP?ZQpY~b2uh{-2*b3eJ?jL`VYd;P9!Y(xiM#2AS&C~mx#(DaDhkecAtKk*!eH_Yb zyj%ctVVAiV)Ybn^V=vY7SaL7+&L#6QbzHuh2Mgd>P|W`)gnZrY{O>4x44D6WuXW^J z|99HPsr;>b^+GrrPKQ%K&t$KLwtF&p`P*H;gniA|&%@)O`~1t$&9_;O@1XoA;Z?9p zjDb4Mf2V1j>iz3=unsiuUIOXA9Tbb#!-qhAJ_3$|^I#Gl0(ZS){Z7c@-_SL(GX)2N z`M-M%=zIPhau#XN4Q4*)|ilg%JOx$dKE_whsgg*M|tz+{}%FD)~7?) zT!o$J`{h}+N$Gh6``?Cc`VafK?jg%T_ok7L0V)6MH`7mmem`A==O}+UWcI()kyJ7N zV=!zV_o`p<-TN1HD5h?|#FN3=IwBPpV-S94W zBa}V{(l+D$lS#h_--Idn4{U*-!FNEPb1j9WE!wZsmTKD|*qb+6GaBU*hSpFk~`jh9pKIi`d+ze~rBG7vA z+u&CC9y|(pYQ0w=4jUhZJ)m#hT^TQZ zJ9*hn*OO&`Gi6r6IP~2X=c_8xxILODXG7kYtaZ6x!h7M(un0Q&JL)^q;Pe;ym(0FBKHK=CG-hwLZqjQ8i0o(?y_ z-LM|?8K}lm);hag_c?56JSBC8v>C+tRC^Y&|6S$`+{4*4@Y2zo~U3LFRWhu+yV_V0n8!y`~e{PElOEOs>x_kv^$Xn&Ynsx6wo zhr$~8J^1bS^Xs|h5|D<;+%MV}Y&mT-Pxbup7x3#Yn%{~|^-<$N&%RaoyHgwgN?p2F zUI}`aaD6Q*TcPslcCfE|fnwliKyzUSD8`GI;C@&O$HEwt-iMUUx$Ntm{t4)$=cpdj zy-n|j?*hfn(&kUzfj}D)KeE3Y=w3JhOW;gUoRrTMBihC;`MlM3Ehn2XYRmC(A87oI zs?R&M`x*N4ThKVr`=$CC^}DMu6`yeRd#T!$jM4PDDw~t4M|B?ocYxlfI%zq|$8`VE zbEd|D=3QG{C9nRs`JhVOoo(uLbFeLS=LeB`Z8(tp&7kM{QTBJIw(I`-0%#n(6Z9UW zdxO?zP6FMN7sB44XQL!mus;Y_46P3JFNNad2VqdpY@K{nJ>1E@INPxkc=r8%PeXO+Gfwr!0CzYRLJ9Uh1GgVwi_d-DKX zF|;b^XEN5=-yN2N=83P-Y{+XIh_A!p5NR_s=M1o7*H==Ffs3HiJedt+!1eLNXfO`5 zaSXhces2S(MOCTp!B2wLM08))8d6nt^=nsaSj%8G`d_i{R~7g|&s*~ILofv&2d#nZ z3tAKT8fXn?13U#<+jDD*53>I>yaN6DGVk?zCOjAh7>S2lmHG;LA6N%@b#vvkuApbC zU%-7Jn`goP5Y4L&sn$vt!^NQIQLT^Z*;HHxcZ0^oAns3!OL`}mjWIy|mhYS|`%NFC z>}_x^=zY7Rzujo>cD}>$JvnwI{1`M2`fXRg*Q@=r(f@i5yaM#z+^=RTBOm!|ZiRHj zUV02^9cLB%2>u5BwlVAVibI9k?Z)`1q^`fDnzvtoe%=Rlt;!^GzU+QuJ3S6`uUZX1 zgnrtY)&K9nZnQ2~c0cNDk3Kt%wHERL&~sTxGhL%=Ri^SezK-dfu5sgLV48V#9!P!-=>4teGvZ^|I2i`k zmzs|Q#leMe21sL}eI1KpB3B) z8RG3iy5v<#&x%$*Kt8H=W1L^g0f`?sB9cwbc9rPBwu#8dv<1qvMAf02$g#y| zo#UF5?!eOMv>M;qCR%w{VeGi10WP0zfJ!L;9$ARTH~F!sP@Io?DH-B?OI$xcm7H%m ugI2`tX*mn4F0V>eYg<05B|6jk$Ir(uoxZ{iY= 0: + profit_pnl_height.append(pnl) + profit_pnl_x.append(count) + else: + loss_pnl_height.append(pnl) + loss_pnl_x.append(count) + + self.profit_pnl_bar.setOpts(x=profit_pnl_x, height=profit_pnl_height) + self.loss_pnl_bar.setOpts(x=loss_pnl_x, height=loss_pnl_height) + + # Set data for pnl distribution + hist, x = np.histogram(df["net_pnl"], bins="auto") + x = x[:-1] + self.distribution_curve.setData(x, hist) + + +class DateAxis(pg.AxisItem): + """Axis for showing date data""" + + def __init__(self, dates: dict, *args, **kwargs): + """""" + super().__init__(*args, **kwargs) + self.dates = dates + + def tickStrings(self, values, scale, spacing): + """""" + strings = [] + for v in values: + dt = self.dates.get(v, "") + strings.append(str(dt)) + return strings diff --git a/vnpy/app/cta_strategy/__init__.py b/vnpy/app/cta_strategy/__init__.py index 9d079f9b..e753746b 100644 --- a/vnpy/app/cta_strategy/__init__.py +++ b/vnpy/app/cta_strategy/__init__.py @@ -7,6 +7,7 @@ from vnpy.trader.utility import BarGenerator, ArrayManager from .base import APP_NAME, StopOrder from .engine import CtaEngine +from .backtesting import BacktestingEngine, OptimizationSetting from .template import CtaTemplate, CtaSignal, TargetPosTemplate diff --git a/vnpy/app/cta_strategy/backtesting.py b/vnpy/app/cta_strategy/backtesting.py index 39768844..5712f798 100644 --- a/vnpy/app/cta_strategy/backtesting.py +++ b/vnpy/app/cta_strategy/backtesting.py @@ -293,7 +293,7 @@ class BacktestingEngine: self.output("逐日盯市盈亏计算完成") return self.daily_df - def calculate_statistics(self, df: DataFrame = None, Output=True): + def calculate_statistics(self, df: DataFrame = None, output=True): """""" self.output("开始计算策略统计指标") @@ -377,7 +377,7 @@ class BacktestingEngine: return_drawdown_ratio = -total_return / max_ddpercent # Output - if Output: + if output: self.output("-" * 30) self.output(f"首个交易日:\t{start_date}") self.output(f"最后交易日:\t{end_date}") @@ -417,6 +417,7 @@ class BacktestingEngine: "total_days": total_days, "profit_days": profit_days, "loss_days": loss_days, + "capital": self.capital, "end_balance": end_balance, "max_drawdown": max_drawdown, "max_ddpercent": max_ddpercent,