Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to use Python to complete the stock back test framework

2025-02-22 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article mainly shows you "how to use Python to complete the stock backtest framework", the content is easy to understand, clear, hope to help you solve your doubts, the following let Xiaobian lead you to study and learn "how to use Python to complete the stock backtest framework" this article.

What is the backtest framework?

Whether it is traditional stock trading or quantitative trading, one of the unavoidable problems is that we need to test whether our trading strategy is feasible, and the simplest way is to use historical data to test the trading strategy. The back test framework is to provide such a platform for the trading strategy to trade continuously in the historical data, and finally generate the final result, by viewing the strategic return of the result, the annualized return. The maximum return test is used to evaluate the feasibility of the trading strategy.

The code address is at the end.

This project is not a perfect project, it is still being improved.

Back test framework

The backtest framework should contain at least two parts, the retest class and the transaction class.

The backtest class provides a variety of hook functions to place its own trading logic, and the trading class is used to simulate the trading platform of the market. this class provides methods to buy and sell.

Code architecture

Take your own backtest framework as an example. It mainly contains the following two files

Backtest/ backtest.py broker.py

Backtest.py mainly provides BackTest as a class to provide a backtest framework that exposes the following hook functions.

Def initialize (self): initialization before the backtest starts "pass def before_on_tick (self, tick): pass def after_on_tick (self, tick): pass def before_trade (self) Order): this function is called before the transaction, where you can place the code for fund management and risk management. If True is returned, the transaction is allowed. Otherwise, abort the transaction "" return True def on_order_ok (self, order): "call" pass def on_order_timeout (self) when the order is executed successfully. " Order): "pass def finish (self):" call "pass @ abstractmethod def on_tick (self, bar):"the method that must be implemented for the backtest instance when the order times out. And write their own transaction logic "" pass "

Playing with quantitative platform backtesting framework or open source framework should be familiar with these hook functions, but the names are different, most of the functions are consistent, except for on_tick.

The reason for on_tick rather than on_bar is that I want the trading logic to be a point-in-time participation in trading, at which point I can get all the stock data at the current time and previous stock data to determine whether or not to trade, rather than an one-by-one participation logic at a point in time.

Broker.py mainly provides two methods of buy,sell for trading.

Def buy (self, code, price, shares, ttl=-1): "" submit a buy order at the limit price-Parameters: code:str stock symbol price:float or None highest possible purchase price If it is None, buy shares:int shares at market price. Maximum time allowed for ttl:int orders. Default is-1, never timeout-return: dict {"type": order type, "buy", "code": stock symbol, "date": submission date, "ttl": time to live, timeout when ttl equals 0 "shares" will not be executed in the future: target number of shares, "price": target price, "deal_lst": historical data of successful transactions. For example, [{"price": transaction price, "date": transaction time, "commission": transaction fee "shares": transaction share}] ""} "" if price is None: stock_info = self.ctx.tick_ data [code] price = stock_ info [self.deal _ price] order = {"type": "buy" "code": code, date: self.ctx.now, "ttl": ttl, "shares": shares, "price": price, "deal_lst": []} self.submit (order) return order def sell (self, code, price, shares Ttl=-1): limit price submit sell order-Parameters: code:str stock symbol price:float or None the lowest price that can be sold If None, sell shares:int shares at market price. Number of shares sold. Maximum time allowed for ttl:int orders. Default is-1, never timeout-return: dict {"type": order type, "sell", "code": stock symbol, "date": submission date, "ttl": time to live, timeout when ttl equals 0 "shares" will not be executed in the future: target number of shares, "price": target price, "deal_lst": historical data of successful transactions. For example, [{"open_price": opening price, "close_price": closing price, "close_date": closing time, "open_date": holding time, "commission": transaction fee. "shares": transaction share "profit": transaction earnings}] ""} "" if code not in self.position: return if price is None: stock_info = self.ctx.tick_ data [code] price = stock_ in [self.deal _ price] order = { "type": "sell" "code": code, "date": self.ctx.now, "ttl": ttl, "shares": shares, "price": price, "deal_lst": []} self.submit (order) return order

Because I hate abstracting too many classes, abstracting too many classes and methods, I am afraid I have forgotten, so the choice of objects is to use common data structures as much as possible, such as list, dict.

Here an dict is used to represent an order.

The above methods ensure the basic transaction logic of a backtest framework, and the operation of the backtest also requires a scheduler to constantly drive these methods, as shown here.

Class Scheduler (object): the scheduling center in the whole retest process drives the retest logic through a time scale (tick). All scheduled objects are bound to a Context object called ctx. Since all key data is shared throughout the retest process, available variables include: ctx.feed: {code1: pd.DataFrame Code2: pd.DataFrame} object ctx.now: time of the loop ctx.tick_data: all quoted stock quotes at the time of the loop ctx.trade_cal: trading calendar ctx.broker: Broker object ctx.bt/ctx.backtest: Backtest object available method: ctx.get_hist "" def _ _ init_ _ (self): "self.ctx = Context () self._pre_hook_lst = [] self._post_hook_lst = [] self._runner_lst = [] def run (self): # runner refers to the existence of a callable initialize The object runner_lst of finish, run (tick) runner_lst = list (chain (self._pre_hook_lst, self._runner_lst, self._post_hook_lst)) # before the loop starts, it is broker, backtest Instances such as hook bind ctx objects and call their initialize method for runner in runner_lst: runner.ctx = self.ctx runner.initialize () # to create a transaction calendar if "trade_cal" not in self.ctx: df = list (self.ctx.feed.values ()) [0] self.ctx ["trade_cal"] = df. Index # calls runner # in turn by traversing the time of the transaction calendar # first calls the run method # of all pre-hook and then calls broker Backtest's run method # finally calls post-hook 's run method for tick in self.ctx.trade_cal: self.ctx.set_currnet_time (tick) for runner in runner_lst: runner.run (tick) # after the loop ends, call the finish method for runner in runner_lst: runner.finish () of all runner objects

A scheduler object is automatically created when the Backtest class is instantiated, and then the scheduler can be started through the start method of the Backtest instance, and the scheduler constantly drives the Backtest based on a timestamp of the historical data, and the Broker instance is called.

In order to handle the data access isolation between different instances, a Context object is bound to a Backtest, Broker instance, and the shared data is accessed through self.ctx. The shared data mainly includes feed objects, that is, historical data, and a dictionary object with the following data structure.

{code1: pd.DataFrame, code2: pd.DataFrame}

And this Context object is also bound to an instance of Broker and Backtest, which can unify the data access interface, but it may lead to data access confusion, which depends on the use of the strategist. One advantage is that it reduces a bunch of proxy methods, and it is really not troublesome for those people to add methods to access other objects.

The binding and Context object codes are as follows:

Class Context (UserDict): def _ getattr__ (self, key): # Let you call this to return self [key] def set_currnet_time (self, tick): self ["now"] = tick tick_data = {} # get all the current stock quotes with quotes Hist in self ["feed"] .items (): df = hist [hist.index = = tick] if len (df) = 1: tick_ data [code] = df.iloc [- 1] self ["tick_data"] = tick_data def get_hist (self, code=None): "" if code is not specified Get the historical data of all stocks as of the current time "" if code is None: hist = {} for code Hist in self ["feed"] .items (): hist [code] = hist [hist.index = high_val: if high_val = = low_val or high_index > = low_index: high_val = low_val = val high_index = low_index = idx continue dropdown = (high_ Val-low_val) / high_val dropdown_lst.append (dropdown) dropdown_index_lst.append ((high_index Low_index) high_val = low_val = val high_index = low_index = idx if low_val is None: low_val = val low_index = idx if val

< low_val: low_val = val low_index = idx if low_index >

High_index: dropdown = (high_val-low_val) / high_val dropdown_lst.append (dropdown) dropdown_index_lst.append ((high_index, low_index)) return dropdown_lst, dropdown_index_lst @ property def max_dropdown (self): "" maximum return rate "dropdown_lst" Dropdown_index_lst = self.get_dropdown () if len (dropdown_lst) > 0: return max (dropdown_lst) else: return 0 @ property def annual_return (self): "annualized yield y = (vUnip c) ^ (Dhand T)-1 v: final value c: Initial value D: effective investment time (365) Note: although investing in stocks is only 250 days But after holding the stock, there is no way to invest elsewhere on the non-trading day. So here I take 365reference: https://wiki.mbalib.com/zh-tw/%E5%B9%B4%E5%8C%96%E6%94%B6%E7%9B%8A%E7%8E%87 "" D = 365c = self._ast_val_hist [0] v = self._ast_val_hist [- 1] days = (self._date_hist [ -1]-self._date_hist [0]) .days ret = (v / c) * * (D / days)-1 return ret

At this point, a back-test framework that the author needs has been formed.

Transaction history data

I do not integrate various methods of obtaining data in the back-testing framework, because this is not the part that the back-testing framework must integrate. It is OK to specify the data structure. The data can be obtained by viewing the data pieces.

Back test report

I also put the backtest report outside the retest framework, where a Plottter object is written to draw some retest indicators, and so on. The results are as follows:

Back test example

The following is an example of a back test.

Import jsonfrom backtest import BackTestfrom reporter import Plotterclass MyBackTest (BackTest): def initialize (self): self.info ("initialize") def finish (self): self.info ("finish") def on_tick (self, tick): tick_data = self.ctx ["tick_data"] for code Hist in tick_data.items (): if hist ["ma10"] > 1.05 * hist ["ma20"]: self.ctx.broker.buy (code, hist.close, 500,500, ttl=5) if hist ["ma10"] < hist ["ma20"] and code in self.ctx.broker.position: self.ctx.broker.sell (code, hist.close, 200) Ttl=1) if _ _ name__ = ='_ _ main__': from utils import load_hist feed = {} for code Hist in load_hist ("000002.SZ"): # hist = hist.iloc [: 100] hist ["ma10"] = hist.close.rolling (10). Mean () hist ["ma20"] = hist.close.rolling (20). Mean () feed [code] = hist mytest = MyBackTest (feed) mytest.start () order_lst = mytest.ctx.broker.order_hist_lst with open ("report/order_hist.json") "w") as wf: json.dump (order_lst, wf, indent=4 Default=str) stats = mytest.stat stats.data.to_csv ("report/stat.csv") print ("strategic gain: {: .3f}%" .format (stats.total_returns * 100)) print ("maximum recall: {: .3f}%" .format (stats.max_dropdown * 100)) print ("annualized return: {: .3f}%" .format (stats.annual_return * 100)) Print ("Sharp ratio: {: .3f}" .format (stats.sharpe)) plotter = Plotter (feed) Stats, order_lst) plotter.report ("report/report.png") these are all the contents of the article "how to use Python to complete the stock backtest framework" Thank you for reading! I believe we all have a certain understanding, hope to share the content to help you, if you want to learn more knowledge, welcome to follow the industry information channel!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report