As the most common and basic CTA strategy, the double average strategy is often used to capture a general trend. Its idea is very simple. It consists of a short-term moving average and a long-term moving average. The short-term moving average represents the recent trend, while the long-term moving average is the trend for a long time:
- When the short-term moving average breaks through the long-term moving average from bottom to top, it means that there is an upward trend in the current time period. The breakthrough point, commonly known as the golden fork, means a long signal;
- When the short period moving average breaks through the short period signal from top to bottom, it means that there is a downward trend in the current time period. The breakthrough point, which is often called dead cross, means a short signal.
Source code analysis
Let's take VN Take the source code of double average strategy in py project as an example to analyze the strategy transaction logic and internal code implementation.
1. Create policy instance
The first thing to remember is that all VN The CTA policy classes (self-contained or user developed) in the. Py framework are subclasses implemented based on the CTA policy template class (CtaTemplate). The relationship between strategy class and template class is as abstract as concrete: according to the design drawings and technical specifications of cars, humans can build all kinds of cars.
Similarly, the CTA policy template defines a series of underlying transaction functions and logical paradigms of policies. According to this rule, we can quickly implement the policies we want.
class DoubleMaStrategy(CtaTemplate): author = "use Python Traders" fast_window = 10 slow_window = 20 fast_ma0 = 0.0 fast_ma1 = 0.0 slow_ma0 = 0.0 slow_ma1 = 0.0 parameters = ["fast_window", "slow_window"] variables = ["fast_ma0", "fast_ma1", "slow_ma0", "slow_ma1"] def __init__(self, cta_engine, strategy_name, vt_symbol, setting): """""" super(DoubleMaStrategy, self).__init__( cta_engine, strategy_name, vt_symbol, setting ) self.bg = BarGenerator(self.on_bar) self.am = ArrayManager()
First, we need to set the policy parameters and variables, both of which belong to the policy class. The difference is that the policy parameters are fixed (specified by the trader from the outside), while the policy variables change with the state of the policy in the process of trading, so the policy variables only need to be initialized to the corresponding basic type at the beginning, for example, the integer is set to 0, the floating point number is set to 0.0, The string is set to ''.
In the policy parameter list parameters, the parameter name string of the policy needs to be written. Based on the contents in the list, the policy engine will automatically read the policy configuration from the cached policy configuration json file, and the graphical interface will automatically provide a dialog box for users to configure policy parameters when creating a policy instance.
In the policy variables list variables, you need to write the variable name string of the policy. Based on the content, the graphical interface will automatically render and display (updated when calling the put_event function), and the policy engine will call sync when the user stops the policy and receives the transaction return_ Data function, write the variable data into the cache json file in the hard disk for the recovery of policy state after program restart.
Constructor of policy class__ init__， CTA needs to be passed_ engine,strategy_name,vt_ The four parameters of symbol and setting correspond to CTA engine object, policy name string, target code string and setting information dictionary respectively. Note that the CTA engine can be a real offer engine or a back test engine, so it is convenient to realize a set of code to run back test and real offer at the same time. The above parameters are automatically passed in by the policy engine when using the policy class to create a policy instance. Users need not care about them in essence.
In the constructor, we also create a BarGenerator instance and pass in on_bar's 1-minute K-line callback function is used to automatically synthesize tick data into minute level K-line data (BarData). In addition, the ArrayManager instance is used to cache the K-line data synthesized by BarGenerator, convert it into a time series data structure convenient for vectorization calculation, and internally support the use of talib to calculate indicators.
2. State variable initialization
Note that the policy state variable initialization here does not refer to the initialization function when creating the policy instance in the previous step__ init__ Logic in. When the user clicks the [add policy] button on the CTA policy module interface of VN Trader, and sets the policy instance name, contract code and policy parameters in the pop-up window, he actually completes the creation of the policy instance.
At this time, the variable status in the policy instance is still the original data such as 0 or "". The user needs to click the [initialize] button on the policy management interface to call on in the policy_ Init function, which completes the operation of playing back the loaded historical data to the policy initialization variable state.
def on_init(self): """ Callback when strategy is inited. """ self.write_log("Policy initialization") self.load_bar(10)
As you can see from the above code, the user is calling this on_ After the init function, the information is output in the log component of the CTA policy management interface, "policy initialization", and then the load_bar function provided by the parent class CtaTemplate is used to load the historical data. The CTA policy engine will be responsible for pushing the data to the initialization calculation of the variable state of the policy.
Notice here we load_bar, the input parameter is 10, which corresponds to loading 1-minute K-line data for 10 days. In the back test, 10 days refers to 10 trading days, while in the case of firm offer, 10 days refers to natural days. Therefore, it is better to load more days than too few. load_ The bar function is implemented as follows:
def load_bar( self, days: int, interval: Interval = Interval.MINUTE, callback: Callable = None, ): """ Load historical bar data for initializing strategy. """ if not callback: callback = self.on_bar #Set callback function self.cta_engine.load_bar(self.vt_symbol, days, interval, callback)
CtaTemplate calls the load of CtaEngine here_ Bar function to complete the loading and playback of historical data. Check for load in CtaEngine_ After the implementation of bar function, we can see two modes of historical data loading: first, try to pull from the remote server using RQData API, provided that the RQData account needs to be configured, and the market data of the contract can be found on RQData (mainly domestic futures), If the acquisition fails, it will try to find in the local database (the default is the sqlite database under the. vntrader folder).
def load_bar( self, vt_symbol: str, days: int, interval: Interval, callback: Callable[[BarData], None] ): """""" symbol, exchange = extract_vt_symbol(vt_symbol) end = datetime.now() start = end - timedelta(days) # Query bars from RQData by default, if not found, load from database. bars = self.query_bar_from_rq(symbol, exchange, interval, start, end) if not bars: bars = database_manager.load_bar_data( symbol=symbol, exchange=exchange, interval=interval, start=start, end=end, ) for bar in bars: callback(bar)
As can be seen from the above code, the current time is obtained through the datetime module as end, and then the time of 10 days is subtracted as start for query. Pass all bar data obtained through the first step load_ Callback function on set in bar_ Bar to push the loaded K-line data to the CTA strategy.
3. Start automatic transaction
After initializing the policy variables, you can start the automatic trading function of the policy. After clicking the [start strategy] button in the graphical interface, the CTA engine will automatically call on in the strategy_ Start function, and set the trading control variable of the policy to True, and the corresponding policy start log information will appear in the log component on the interface.
""" Callback when strategy is started. """ self.write_log("Policy start") self.put_event()
Note that put must be called here_ Event function to inform the graphical interface to refresh the display (variables) related to the policy status. If it is not called, the interface will not be updated.
4. Receive Tick push
After the automatic transaction is started, the CTP interface will push the Tick data once every 0.5 seconds, and then the event engine inside VN Trader will distribute and push it to our policy. The Tick data processing function in the policy is as follows:
def on_tick(self, tick: TickData): """ Callback of new tick data update. """ self.bg.update_tick(tick)
Because it is a relatively simple double average strategy, and the transaction logic is executed on the K-line time cycle, after receiving the tick data, call the update of the bg object (BarGenerator) to which the policy instance belongs_ Tick to realize the automatic synthesis of 1-minute K-line data by tick:
def update_tick(self, tick: TickData): """ Update new tick data into generator. """ new_minute = False # Filter tick data with 0 last price if not tick.last_price: return if not self.bar: new_minute = True elif self.bar.datetime.minute != tick.datetime.minute: self.bar.datetime = self.bar.datetime.replace( second=0, microsecond=0 ) self.on_bar(self.bar) new_minute = True if new_minute: self.bar = BarData( symbol=tick.symbol, exchange=tick.exchange, interval=Interval.MINUTE, datetime=tick.datetime, gateway_name=tick.gateway_name, open_price=tick.last_price, high_price=tick.last_price, low_price=tick.last_price, close_price=tick.last_price, open_interest=tick.open_interest ) else: self.bar.high_price = max(self.bar.high_price, tick.last_price) self.bar.low_price = min(self.bar.low_price, tick.last_price) self.bar.close_price = tick.last_price self.bar.open_interest = tick.open_interest self.bar.datetime = tick.datetime if self.last_tick: volume_change = tick.volume - self.last_tick.volume self.bar.volume += max(volume_change, 0) self.last_tick = tick
update_ The Tick function mainly checks whether the current Tick data and the previous Tick data belong to the same minute to judge whether a new 1-minute K-line is generated. If not, it will continue to accumulate and update the information of the current K-line.
This means that the bar data of T minutes will not be generated until the first Tick of T+1 minutes is received. When creating the bg object, we passed in on_bar is used as the callback function after K-line synthesis, so when a new 1-minute K-line is generated, it will pass on_ The bar function is pushed into the policy.
5. Core transaction logic
The most important part of each strategy is the core transaction logic of the strategy:
- If the policy logic is based on Tick data, on_ Implement relevant transaction logic in Tick function;
- If the strategy logic is based on the K-line, such as our double average strategy here, it is on_ The bar function implements the relevant transaction logic.
def on_bar(self, bar: BarData): """Callback of new bar data update.""" am = self.am am.update_bar(bar) if not am.inited: return fast_ma = am.sma(self.fast_window, array=True) self.fast_ma0 = fast_ma[-1] self.fast_ma1 = fast_ma[-2] slow_ma = am.sma(self.slow_window, array=True) self.slow_ma0 = slow_ma[-1] self.slow_ma1 = slow_ma[-2] cross_over = self.fast_ma0 > self.slow_ma0 and self.fast_ma1 < self.slow_ma1 cross_below = self.fast_ma0 < self.slow_ma0 and self.fast_ma1 > self.slow_ma1 if cross_over: if self.pos == 0: self.buy(bar.close_price, 1) elif self.pos < 0: self.cover(bar.close_price, 1) self.buy(bar.close_price, 1) elif cross_below: if self.pos == 0: self.short(bar.close_price, 1) elif self.pos > 0: self.sell(bar.close_price, 1) self.short(bar.close_price, 1) self.put_event()
After receiving the K-line data, that is, the push of the bar object, we need to put the bar data into the am (ArrayManager) time series container for updating. When there are at least 100 bar data, the am object is initialized (initialized becomes True).
It should be noted here that if there is not enough historical data to complete the initialization of am when initializing the policy state variable, at least 100 bar data need to be received to fill the AM container after the automatic transaction is started, and the following transaction logic code will not be executed until the initialization of AM is completed.
After that, we call the talib library encapsulated inside the ArrayManager to calculate the technical indicators in the latest window, which corresponds to the MA index of the MA and the 20 window of our double average strategy, that is, the 10 window.
Notice the am here SMA is actually a further encapsulation of the SMA function in talib. In essence, it is used to calculate the arithmetic mean of the closing price of bar data:
am = self.am am.update_bar(bar) if not am.inited: return fast_ma = am.sma(self.fast_window, array=True) self.fast_ma0 = fast_ma[-1] self.fast_ma1 = fast_ma[-2] slow_ma = am.sma(self.slow_window, array=True) self.slow_ma0 = slow_ma[-1] self.slow_ma1 = slow_ma[-2]
Then determine whether to trigger the transaction logic by judging whether there is a dead Cross:
cross_over = self.fast_ma0 > self.slow_ma0 and self.fast_ma1 < self.slow_ma1 cross_below = self.fast_ma0 < self.slow_ma0 and self.fast_ma1 > self.slow_ma1
- When there is a golden fork: if there is no position, it will directly buy and open the position; Or hold a short position, then close the short position first and then buy and open the position.
- When there is a dead Cross: if there is no position, it will be sold directly; Or if you hold a long position, you should level the long position first and then sell and open the position.
The specific delegation instructions have been encapsulated by the CTA policy template. on_bar function can be called directly:
- Buy: buy and open (Direction: many, Offset: open)
- Sell: sell out (Direction: empty, Offset: flat)
- Direction: short
- cover: buy and close (Direction: long, Offset: flat)
It should be noted here that domestic futures have the concept of opening and closing positions. For example, buying operations should be divided into buying opening and buying closing positions; However, for stocks, external futures and most digital currencies (except OKEX), it is a net position mode, and there is no concept of opening and closing positions, so you only need to use the two instructions of buy and sell.
if cross_over: if self.pos == 0: self.buy(bar.close_price, 1) elif self.pos < 0: self.cover(bar.close_price, 1) self.buy(bar.close_price, 1) elif cross_below: if self.pos == 0: self.short(bar.close_price, 1) elif self.pos > 0: self.sell(bar.close_price, 1) self.short(bar.close_price, 1) self.put_event()
6. Commission return
on_order is a delegate callback function. After we issue a transaction delegate, whenever the state of the delegate changes, we will receive the latest data push of the delegate, which is the delegate return.
Among them, the more important information is the status of entrustment (including order rejection, no transaction, partial transaction, complete transaction and cancelled order). We can realize more fine-grained transaction entrustment control (algorithmic transaction) based on the entrustment status.
Here, because the logic of our double average strategy is relatively simple, it is on_ There are no operations in order:
def on_order(self, order: OrderData): """ Callback of new order data update. """ pass
Also for on_trader (transaction return function) and on_stop_order (stop single return function) also has no operation.
7. Stop automatic trading
When the daily trading period ends (domestic futures generally close at 3 p.m.), you need to click the [stop] button in the CTA strategy interface to stop the automatic trading of the strategy.
At this point, the CTA policy engine will set the transaction state variable trading of the policy to False, cancel the delegate of all the active States issued before the strategy, and write the parameters in the list of policy variables to the cached json file, and finally call the on_ of the policy. The stop callback function executes user-defined logic:
def on_stop(self): """ Callback when strategy is stopped. """ self.write_log("Policy stop") self.put_event()
CTA transaction process sorting
Finally, using the mind map I made, take the double average strategy as an example to sort out VN Py for policy implementation and execution process:
The course "vn.py all practical advanced" is newly launched, with a total of 50 sections, covering the complete CTA quantitative business process from strategy design and development, parameter back testing and optimization to the final real offer automatic transaction. At present, it has been updated to episode 8. For details, please stamp the course online: "vn.py all practical advanced"! For more information, please pay attention to VN The official account of Py community.