vn.py quick start 6 - develop the first quantitative strategy

Posted by hairulazami on Tue, 08 Mar 2022 11:01:22 +0100

The quick start series has reached Chapter 6, and we are finally going to touch the content of programming practice. The content in this tutorial assumes that you have a certain basic grasp of Python language development:

 

  • Understand Python data results
  • Understand the concept of object-oriented programming
  • Can use control statements to express logic

 

 

If not, it doesn't matter. I recommend a quick start book, Learn Python the Hard Way, which is also the stepping stone for me to learn Python for the first time from a completely 0 basic Xiaobai many years ago. I knock more than 50 classes from beginning to end. As long as I don't lazy, I won't come to me.

   

Prepare IDE tools

 

The full name of this tool is "Integrated Development Environment", which is also called "Integrated Development Environment" in Chinese.

 

IDE provides a complete set of tools and environments necessary for writing programs, including code writing, debugging and running, version control, management interface, etc. in the next tutorial, we choose to use Visual Studio Code (VS Code for short), a special open source editor for programming launched by Microsoft (and then built into a super ide by the enthusiastic open source community and various plug-ins).

Go VSCode Homepage , click the Download green icon to Download and install all the way. After running, you will see the following interface:

 

 

 

Among the five buttons at the top of the left navigation bar, click the bottom button to enter the page of installing the extension, enter Python in the search box, and find the python plug-in officially launched by Microsoft:

 

 

Click the green Install button in the red box in the figure above to automatically download, install and start the plug-in in in the background. So far, our IDE is ready.

 

Although we recommend using VS Code here, if you already have common IDE tools, whether PyCharm, WingIDE, Vim or Visual Studio, you can use them to complete subsequent policy code development, so just use them directly. If you encounter some difficult problems in the process, you can switch back to VS Code. The operations in this series of tutorials are guaranteed to be fully available.  

 

Create policy file

 

The first thing to contact is the concept of a user directory, that is, the directory used by any operating system to store the cache files of the currently logged in user when running the program. Assuming that your login user name is client, then:

 

  • Under Windows: C:\Users\client\
  • Under Linux or Mac: / home/client/

The above is the most commonly used user directory path. Note that it is only common. If your system has made special configuration modifications, it may be different.

 

The default runtime directory of VN Trader is the operating system user directory, which will be created after startup vntrader folder is used to save configuration and temporary files (sometimes strange bug s can be solved by deleting the folder and restarting it).

 

At the same time, CtaStrategyApp will also scan the strategies folder under the VN Trader runtime directory to load user-defined policy files after startup. Take Windows as an example: C:\Users\client\strategies. Note that the strategies folder does not exist by default and needs to be created by the user.

 

After entering the strategies directory, create our first policy file: demo_strategy.py, and then open it with VS Code.  

 

Define policy classes

After the new policy file is opened, the interior is empty. At this time, we begin to add code to it, following the industry tradition. Here we also choose the fool's double average strategy as a demonstration:

 

from vnpy.app.cta_strategy import (
    CtaTemplate,
    StopOrder,
    TickData,
    BarData,
    TradeData,
    OrderData,
    BarGenerator,
    ArrayManager,
)


class DemoStrategy(CtaTemplate):
    """Simple double average for demonstration"""

    # Strategy author
    author = "Smart Trader"

    # Define parameters
    fast_window = 10
    slow_window = 20

    # Define variables
    fast_ma0 = 0.0
    fast_ma1 = 0.0
    slow_ma0 = 0.0
    slow_ma1 = 0.0

    # Add parameter and variable names to the corresponding list
    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().__init__(cta_engine, strategy_name, vt_symbol, setting)

        # K-line synthesizer: used to synthesize K-line from Tick
        self.bg = BarGenerator(self.on_bar)

        # Time series container: for calculating technical indicators
        self.am = ArrayManager()

    def on_init(self):
        """
        This function is called when the policy is initialized.
        """
        # Output log information, the same below
        self.write_log("Policy initialization")

        # Load 10 days of historical data to initialize playback
        self.load_bar(10)

    def on_start(self):
        """
        This function is called when the policy is started.
        """
        self.write_log("Policy start")

        # Notify graphical interface updates (policy update status)
        # If this function is not called, the interface will not change
        self.put_event()

    def on_stop(self):
        """
        This function is called when the policy is stopped.
        """
        self.write_log("Policy stop")

        self.put_event()

    def on_tick(self, tick: TickData):
        """
        Received through this function Tick Push.
        """
        self.bg.update_tick(tick)

    def on_bar(self, bar: BarData):
        """
        New 1 minute received through this function K Line push.
        """
        am = self.am

        # Update K-line to time series container
        am.update_bar(bar)

        # If the number of cached K-lines is not enough to calculate the technical indicators, it will be returned directly
        if not am.inited:
            return

        # Calculate fast moving average
        fast_ma = am.sma(self.fast_window, array=True)
        self.fast_ma0 = fast_ma[-1]     # Time T value
        self.fast_ma1 = fast_ma[-2]     # T-1 time value

        # Calculate slow moving average
        slow_ma = am.sma(self.slow_window, array=True)
        self.slow_ma0 = slow_ma[-1]
        self.slow_ma1 = slow_ma[-2]

        # Judge whether the gold fork
        cross_over = (self.fast_ma0 > self.slow_ma0 and
                      self.fast_ma1 < self.slow_ma1)

        # Judge whether there is a dead fork
        cross_below = (self.fast_ma0 < self.slow_ma0 and
                       self.fast_ma1 > self.slow_ma1)

        # If a golden fork happens
        if cross_over:
            # In order to ensure the transaction, add 5 to the closing price of the K line and issue a limit order
            price = bar.close_price + 5

            # If there is no position at present, open more directly
            if self.pos == 0:
                self.buy(price, 1)
            # If you currently hold a short position, you should first open it and then open more
            elif self.pos < 0:
                self.cover(price, 1)
                self.buy(price, 1)

        # If a dead cross occurs
        elif cross_below:
            price = bar.close_price - 5

            # If there is no position at present, open it directly
            if self.pos == 0:
                self.short(price, 1)
            # If you currently hold a short position, you should first level it and then open it
            elif self.pos > 0:
                self.sell(price, 1)
                self.short(price, 1)

        self.put_event()

    def on_order(self, order: OrderData):
        """
        Receive the push of delegate status update through this function.
        """
        pass

    def on_trade(self, trade: TradeData):
        """
        Receive transaction push through this function.
        """
        # If the strategic logical position changes after the transaction, the notification interface needs to be updated.
        self.put_event()

    def on_stop_order(self, stop_order: StopOrder):
        """
        Local stop single push is received through this function.
        """
        pass

 

In the file header, the most important of our series of imports is CtaTemplate, which is the base class of policy template used by us to develop CTA policy. The policy template provides a series of on_ The callback function at the beginning is used to accept event push, and other active functions are used to perform operations (delegation, cancellation, logging, etc.).

 

All developed policy classes must inherit the CtaTemplate base class, and then implement the policy logic in the required callback function, that is, the corresponding operation we need to perform when something happens: for example, when we receive a 1-minute K-line push, we need to calculate the moving average index, and then judge whether to execute the transaction.

   

Setting parameter variables

All quantitative trading strategies will inevitably involve two concepts related to values: parameters and variables.

 

Parameters are some values used to control the output of results in the internal logic algorithm of the policy. The default values of these parameters need to be defined in the policy class:

 

# Define parameters
fast_window = 10
slow_window = 20

 

After definition, you need to add the name (string) of the parameter to the parameters list:

 

parameters = ["fast_window", "slow_window"]

This step is to let the policy engine in the system know the parameters of the policy, and pop up the corresponding dialog box when initializing the policy for the user to fill in, or directly assign the value of the corresponding key in the configuration dictionary to the policy variable in the command line mode.

 

Variables are the internal logic algorithm of the policy, which is used to cache some values of the intermediate state during execution. The default values of these variables also need to be defined in the policy class:

 

# Define variables
fast_ma0 = 0.0
fast_ma1 = 0.0
slow_ma0 = 0.0
slow_ma1 = 0.0

 

After the definition, you need to add the name (string) of the variable to the variables list:

 

variables = ["fast_ma0", "fast_ma1", "slow_ma0", "slow_ma1"]

 

Similar to parameters, this step is to let the policy engine in the system know which variables the policy has, display the latest values of these variables when updating the policy status on the GUI Graphical interface, and write these variables when saving the policy running status to the cache file (the policy will be automatically cached when the policy is closed every day in the real disk).

 

It should be noted that:

 

  1. Both variables and parameters must be defined in the policy class, not in the policy class__ init__ In function;
  2. Parameters and variables only support four basic data types in Python: str, int, float and bool. Using other types will lead to various errors (especially don't use containers such as list and dict);
  3. If you really need to use containers such as list and dict for data caching in policy logic, please__ init__ Function to create these containers.

 

 

Transaction logic implementation

 

As mentioned above, in VN In the CTA policy module of Py, all policy logic is driven by events. For events, for example:

 

  • The policy will be initialized and the operation will be executed_ Call the init function. At this time, you can load the historical data to initialize the technical indicators
  • When the new 1-minute K-line is completed, you will receive on_ Call of bar function, and the parameter is BarData of this K-line object
  • When the status of the delegation sent by the policy changes, it will receive on_ Call of the order function. The parameter is the latest status OrderData of the delegate

 

For DemoStrategy, the simplest double average strategy, we don't need to pay attention to the details such as the change of entrustment status and transaction push. We just need to execute the logical judgment related to the transaction when we receive the K-line push (in the on_bar function).

 

Every time a new K-line is completed, the strategy will pass on_ The bar function receives the data push of the K-line. Note that the data received at this time is only the K-line, but most of the technical indicators need the historical data of the past N cycles.

 

Therefore, in order to calculate the technical index of moving average, we need to use an object called time series container ArrayManager to realize the caching of K-line history and the calculation of technical index. The creation of this object is in the process of policy__ init__ In function:

 

# Time series container: for calculating technical indicators
self.am = ArrayManager()

 

On_ In the logic of the bar function, the first step is to push the K-line object into the time series container:

 

# Just for the sake of follow-up, you can write less self
am = self.am

# Update K-line to time series container
am.update_bar(bar)

# If the number of cached K-lines is not enough to calculate the technical indicators, it will be returned directly
if not am.inited:
    return

 

In order to meet the requirements of technical index calculation, we usually need a cache of at least N K lines (n is 100 by default). Before the data pushed into the ArrayManager object is less than N, the required technical index cannot be calculated. Judge whether the cached data is sufficient through am The inited variable can be easily judged. Before inited becomes True, it should only cache data without any other operations.

 

When the amount of cached data meets the requirements, we can easily use am SMA function to calculate the value of EMA index:

 

# Calculate fast moving average
fast_ma = am.sma(self.fast_window, array=True)
self.fast_ma0 = fast_ma[-1]     # Time T value
self.fast_ma1 = fast_ma[-2]     # T-1 time value

# Calculate slow moving average
slow_ma = am.sma(self.slow_window, array=True)
self.slow_ma0 = slow_ma[-1]
self.slow_ma1 = slow_ma[-2]

 

Note that here we passed in the optional parameter array=True, so the returned fast_ma is the array of the latest moving average, in which the ma value of the moving average of the latest cycle (time T) can be obtained through the - 1 subscript, and the ma value of the previous cycle (time T-1) can be obtained through the - 2 subscript.

 

With the values of the two moving averages at time t and time T-1, we can judge the core logic of the double moving average strategy, that is, whether there is a golden fork or dead fork in the moving average:

 

# Judge whether the gold fork
cross_over = (self.fast_ma0 > self.slow_ma0 and
              self.fast_ma1 < self.slow_ma1)

# Judge whether there is a dead fork
cross_below = (self.fast_ma0 < self.slow_ma0 and
               self.fast_ma1 > self.slow_ma1)

The so-called moving average golden fork refers to the fast moving average at T-1 time_ MA1 is lower than slow moving average_ MA1, while the fast moving average at time t is fast_ma0 is greater than or equal to slow moving average slow_ma10, which realizes the behavior of putting on (i.e. golden fork). Moving average deadlock is the opposite.

 

When the golden cross or dead cross occurs, you need to perform the corresponding transaction operations:

 

# If a golden fork happens
if cross_over:
    # In order to ensure the transaction, add 5 to the closing price of the K line and issue a limit order
    price = bar.close_price + 5

    # If there is no position at present, open more directly
    if self.pos == 0:
        self.buy(price, 1)
    # If you currently hold a short position, you should first open it and then open more
    elif self.pos < 0:
        self.cover(price, 1)
        self.buy(price, 1)

# If a dead cross occurs
elif cross_below:
    price = bar.close_price - 5

    # If there is no position at present, open it directly
    if self.pos == 0:
        self.short(price, 1)
    # If you currently hold a short position, you should first level it and then open it
    elif self.pos > 0:
        self.sell(price, 1)
        self.short(price, 1)

For the simple double average strategy, it is used in the state of holding a position. It takes a long position after the golden fork and a short position after the dead fork.

 

So when the golden fork occurs, we need to check the current position. If there is no position (self.pos == 0), it indicates that the strategy has just started trading at this time, and the long position opening operation (buy) should be directly executed. If you already hold a short position (self.pos, 0), you should first perform a short cover operation, and then immediately perform a long open operation (buy) at the same time. In order to ensure the transaction (simplify the strategy), we chose to increase the price when placing an order (bull + 5, short - 5).  

Note that although we choose to use the double average strategy for demonstration here, the effect of the simple average strategy is often very poor in practical experience. Do not use it to run the real market, and it is not recommended to expand the development on this basis. After all, what is built on the tofu residue project is tofu residue

 

 

Solid K-line synthesis

 

The double average trading logic of DemoStrategy can ensure the transaction through over price trading, so as to ignore more detailed event driven logic such as order cancellation, entrustment update and transaction push.  

However, in the case of solid trading, any trading system (whether futures CTP, or digital currency BITMEX, etc.) will only push the latest Tick update data, and there will be no complete K-line push. Therefore, users need to complete the synthesis logic from Tick to K-line locally.  

This VN The user only needs to provide a perfect synthetic line in the K PY generator__ init__ Create instance in function:

 

# K-line synthesizer: used to synthesize K-line from Tick
self.bg = BarGenerator(self.on_bar)

When the BarGenerator object is created, the parameter passed in (self.on_bar) is the callback function triggered when the 1-minute K-line is completed.

 

When the firm offer policy receives the latest Tick push, we only need to update the Tick data to the BarGenerator:

 

def on_tick(self, tick: TickData):
    """
    Received through this function Tick Push.
    """
    self.bg.update_tick(tick)

When the BarGenerator finds that a K-line has been completed, it will push the 1-minute K-line synthesized by the Tick data in the past 1 minute to the policy, and automatically call the on of the policy_ The bar function executes the transaction logic explained in the previous section.

For more information, please pay attention to VN The official account of Py community.

Topics: Python def Visual Studio Code webp