Nghiên cứu Backtesting môi trường trong Python với panda

Tác giả:Tốt, Tạo: 2019-03-16 11:58:20, Cập nhật:

Kiểm tra ngược là quá trình nghiên cứu áp dụng ý tưởng chiến lược giao dịch vào dữ liệu lịch sử để xác định hiệu suất trong quá khứ. Đặc biệt, một backtester không đảm bảo về hiệu suất trong tương lai của chiến lược. Tuy nhiên, chúng là một thành phần thiết yếu của quá trình nghiên cứu đường ống chiến lược, cho phép các chiến lược được lọc ra trước khi đưa vào sản xuất.

Trong bài viết này (và những bài viết tiếp theo) một hệ thống backtesting định hướng đối tượng cơ bản được viết bằng Python sẽ được phác thảo. Hệ thống đầu tiên này chủ yếu sẽ là một hỗ trợ giảng dạy, được sử dụng để chứng minh các thành phần khác nhau của hệ thống backtesting. Khi chúng ta tiến hành qua các bài viết, các chức năng phức tạp hơn sẽ được thêm vào.

Thông tin tổng quan về kiểm tra ngược

Quá trình thiết kế một hệ thống backtesting mạnh mẽ là cực kỳ khó khăn. Mô phỏng hiệu quả tất cả các thành phần ảnh hưởng đến hiệu suất của một hệ thống giao dịch thuật toán là một thách thức. Độ chi tiết dữ liệu kém, sự mờ nhạt của việc định tuyến lệnh tại một nhà môi giới, độ trễ lệnh và vô số các yếu tố khác âm mưu thay đổi hiệu suất thực sự của một chiến lược so với hiệu suất backtested.

Khi phát triển một hệ thống backtesting, bạn có xu hướng muốn liên tục "viết lại nó từ đầu" vì nhiều yếu tố được tìm thấy là rất quan trọng trong việc đánh giá hiệu suất.

Với những mối quan tâm này trong tâm trí backtester được trình bày ở đây sẽ được một chút đơn giản hóa. khi chúng ta khám phá các vấn đề khác (tối ưu hóa danh mục đầu tư, quản lý rủi ro, xử lý chi phí giao dịch) backtester sẽ trở nên mạnh mẽ hơn.

Các loại hệ thống kiểm tra ngược

Có hai loại hệ thống backtesting thường được quan tâm. Đầu tiên là dựa trên nghiên cứu, được sử dụng chủ yếu trong giai đoạn đầu, nơi nhiều chiến lược sẽ được thử nghiệm để chọn những người cho đánh giá nghiêm túc hơn.

Loại hệ thống backtesting thứ hai dựa trên sự kiện. nghĩa là, nó thực hiện quá trình backtesting trong một vòng lặp thực hiện tương tự (nếu không giống hệt) với hệ thống thực hiện giao dịch. Nó sẽ mô hình hóa thực tế dữ liệu thị trường và quá trình thực hiện lệnh để cung cấp đánh giá nghiêm ngặt hơn về một chiến lược.

Các hệ thống sau này thường được viết bằng ngôn ngữ hiệu suất cao như C ++ hoặc Java, nơi tốc độ thực thi là điều cần thiết.

Người kiểm tra hậu quả nghiên cứu định hướng đối tượng trong Python

Thiết kế và thực hiện một môi trường backtesting dựa trên nghiên cứu hướng đối tượng sẽ được thảo luận.

  • Các giao diện của mỗi thành phần có thể được xác định trước, trong khi nội bộ của mỗi thành phần có thể được sửa đổi (hoặc thay thế) khi dự án tiến triển
  • Bằng cách xác định các giao diện trước, có thể kiểm tra hiệu quả cách mỗi thành phần hoạt động (thông qua thử nghiệm đơn vị)
  • Khi mở rộng hệ thống, các thành phần mới có thể được xây dựng trên hoặc bổ sung cho các thành phần khác, bằng cách thừa kế hoặc thành phần

Trong giai đoạn này, backtester được thiết kế để dễ dàng thực hiện và có một mức độ linh hoạt hợp lý, với chi phí chính xác thị trường thực sự. Đặc biệt, backtester này sẽ chỉ có thể xử lý các chiến lược hoạt động trên một công cụ duy nhất. Sau đó, backtester sẽ được sửa đổi để xử lý các bộ công cụ. Đối với backtester ban đầu, các thành phần sau đây là cần thiết:

  • Chiến lược - Một lớp chiến lược nhận được một Pandas DataFrame của các thanh, tức là một danh sách các điểm dữ liệu mở-cao- thấp-tích thước (OHLCV) ở tần số cụ thể. Chiến lược sẽ tạo ra một danh sách các tín hiệu, bao gồm dấu thời gian và một phần tử từ tập hợp {1,0,−1} cho thấy tín hiệu dài, giữ hoặc ngắn tương ứng.
  • Portfolio - Phần lớn các công việc backtesting sẽ xảy ra trong lớp Portfolio. Nó sẽ nhận được một bộ tín hiệu (như được mô tả ở trên) và tạo ra một loạt các vị trí, được phân bổ so với một thành phần tiền mặt. Công việc của đối tượng Portfolio là tạo ra đường cong vốn chủ sở hữu, kết hợp chi phí giao dịch cơ bản và theo dõi các giao dịch.
  • Hiệu suất - Đối tượng Hiệu suất lấy một danh mục đầu tư và tạo ra một bộ thống kê về hiệu suất của nó. Đặc biệt, nó sẽ xuất các đặc điểm rủi ro / lợi nhuận (Sharpe, Sortino và Information Ratios), số liệu thương mại / lợi nhuận và thông tin rút vốn.

Có gì thiếu không?

Như có thể thấy, backtester này không bao gồm bất kỳ tham chiếu nào đến quản lý danh mục đầu tư / rủi ro, xử lý thực thi (tức là không có lệnh giới hạn) cũng không cung cấp mô hình hóa phức tạp về chi phí giao dịch. Đây không phải là vấn đề lớn ở giai đoạn này. Nó cho phép chúng ta làm quen với quá trình tạo backtester định hướng đối tượng và thư viện Pandas / NumPy. Theo thời gian nó sẽ được cải thiện.

Thực hiện

Bây giờ chúng ta sẽ tiếp tục phác thảo các thực hiện cho mỗi đối tượng.

Chiến lược

Đối tượng Chiến lược phải khá chung trong giai đoạn này, vì nó sẽ xử lý các chiến lược dự báo, đảo ngược trung bình, động lực và biến động. Các chiến lược được xem xét ở đây sẽ luôn dựa trên chuỗi thời gian, tức là giá trị. Một yêu cầu ban đầu cho backtester này là các lớp chiến lược phái sinh sẽ chấp nhận một danh sách các thanh (OHLCV) như đầu vào, thay vì các dấu chấm (giá trị theo giao dịch) hoặc dữ liệu sổ lệnh. Do đó, độ tỉ mỉ tốt nhất được xem xét ở đây sẽ là các thanh 1 giây.

Các lớp chiến lược cũng sẽ luôn tạo ra các khuyến nghị tín hiệu. Điều này có nghĩa là nó sẽ tư vấn cho một trường hợp danh mục đầu tư theo nghĩa là đi dài / ngắn hoặc giữ một vị trí. tính linh hoạt này sẽ cho phép chúng tôi tạo ra nhiều chiến lược advisors cung cấp một bộ tín hiệu, mà một lớp danh mục đầu tư tiên tiến hơn có thể chấp nhận để xác định các vị trí thực tế được nhập.

Giao diện của các lớp sẽ được thực thi bằng cách sử dụng phương pháp lớp cơ sở trừu tượng. Một lớp cơ sở trừu tượng là một đối tượng không thể được tạo ra và do đó chỉ có thể tạo ra các lớp phái sinh. Mã Python được cung cấp dưới đây trong một tệp có tênbacktest.py. Lớp Chiến lược yêu cầu bất kỳ lớp con nào thực hiện phương thức generate_signals.

Để ngăn chặn lớp Strategy được thực hiện trực tiếp (vì nó là trừu tượng!) nó là cần thiết để sử dụng các đối tượng ABCMeta và abstractmethod từ mô-đun abc.Metaclassđể bằng với ABCMeta và sau đó trang trí phương thức generate_signals với trang trí abstractmethod.

# backtest.py

from abc import ABCMeta, abstractmethod

class Strategy(object):
    """Strategy is an abstract base class providing an interface for
    all subsequent (inherited) trading strategies.

    The goal of a (derived) Strategy object is to output a list of signals,
    which has the form of a time series indexed pandas DataFrame.

    In this instance only a single symbol/instrument is supported."""

    __metaclass__ = ABCMeta

    @abstractmethod
    def generate_signals(self):
        """An implementation is required to return the DataFrame of symbols 
        containing the signals to go long, short or hold (1, -1 or 0)."""
        raise NotImplementedError("Should implement generate_signals()!")

Trong khi giao diện trên là đơn giản, nó sẽ trở nên phức tạp hơn khi lớp này được thừa hưởng cho mỗi loại chiến lược cụ thể.

Cổ phiếu

Trong trường hợp này, các giao dịch sẽ được phân chia thành các phần khác nhau. Trong trường hợp này, các giao dịch sẽ được phân chia thành các phần khác nhau. Trong trường hợp này, các giao dịch sẽ được phân chia thành các phần khác nhau.

Các lớp học này sử dụng rất nhiều pandas và cung cấp một ví dụ tuyệt vời về nơi mà thư viện có thể tiết kiệm một lượng lớn thời gian, đặc biệt là liên quan đến boilerplate dữ liệu tranh cãi. Ngoài ra, thủ thuật chính với pandas và NumPy là tránh lặp lại trên bất kỳ tập dữ liệu nào bằng cách sử dụng cú pháp d in....

Mục tiêu của lớp Portfolio là cuối cùng tạo ra một chuỗi giao dịch và đường cong vốn chủ sở hữu, được phân tích bởi lớp Performance. Để đạt được điều này, nó phải được cung cấp một danh sách các khuyến nghị giao dịch từ một đối tượng Chiến lược. Sau đó, đây sẽ là một nhóm các đối tượng Chiến lược.

Lớp danh mục đầu tư sẽ cần phải được thông báo cách đầu tư được triển khai cho một bộ tín hiệu giao dịch cụ thể, cách xử lý chi phí giao dịch và các hình thức lệnh sẽ được sử dụng. Đối tượng Chiến lược hoạt động trên các thanh dữ liệu và do đó phải đưa ra giả định liên quan đến giá đạt được khi thực hiện lệnh. Vì giá cao / thấp của bất kỳ thanh nào là không rõ trước tiên, chỉ có thể sử dụng giá mở và đóng để giao dịch. Trong thực tế, không thể đảm bảo rằng một lệnh sẽ được hoàn thành ở một trong những giá cụ thể này khi sử dụng lệnh thị trường, vì vậy nó sẽ là, tốt nhất, một ước tính.

Ngoài các giả định về lệnh được thực hiện, backtester này sẽ bỏ qua tất cả các khái niệm về giới hạn ký quỹ / môi giới và sẽ giả định rằng có thể đi dài và ngắn trong bất kỳ công cụ nào một cách tự do mà không có bất kỳ hạn chế thanh khoản nào.

Danh sách sau tiếp tụcbacktest.py:

# backtest.py

class Portfolio(object):
    """An abstract base class representing a portfolio of 
    positions (including both instruments and cash), determined
    on the basis of a set of signals provided by a Strategy."""

    __metaclass__ = ABCMeta

    @abstractmethod
    def generate_positions(self):
        """Provides the logic to determine how the portfolio 
        positions are allocated on the basis of forecasting
        signals and available cash."""
        raise NotImplementedError("Should implement generate_positions()!")

    @abstractmethod
    def backtest_portfolio(self):
        """Provides the logic to generate the trading orders
        and subsequent equity curve (i.e. growth of total equity),
        as a sum of holdings and cash, and the bar-period returns
        associated with this curve based on the 'positions' DataFrame.

        Produces a portfolio object that can be examined by 
        other classes/functions."""
        raise NotImplementedError("Should implement backtest_portfolio()!")

Ở giai đoạn này, các lớp cơ sở trừu tượng Chiến lược và Cổ phiếu đã được giới thiệu.

Chúng ta sẽ bắt đầu bằng cách tạo ra một lớp con của Chiến lược gọi là RandomForecastStrategy, nhiệm vụ duy nhất của nó là tạo ra các tín hiệu dài / ngắn được chọn ngẫu nhiên! Mặc dù đây rõ ràng là một chiến lược giao dịch vô nghĩa, nó sẽ phục vụ nhu cầu của chúng ta bằng cách chứng minh khung kiểm tra ngược định hướng đối tượng. Do đó chúng ta sẽ bắt đầu một tệp mới có tên là random_forecast.py, với danh sách cho nhà dự báo ngẫu nhiên như sau:

# random_forecast.py

import numpy as np
import pandas as pd
import Quandl   # Necessary for obtaining financial data easily

from backtest import Strategy, Portfolio

class RandomForecastingStrategy(Strategy):
    """Derives from Strategy to produce a set of signals that
    are randomly generated long/shorts. Clearly a nonsensical
    strategy, but perfectly acceptable for demonstrating the
    backtesting infrastructure!"""    
    
    def __init__(self, symbol, bars):
    	"""Requires the symbol ticker and the pandas DataFrame of bars"""
        self.symbol = symbol
        self.bars = bars

    def generate_signals(self):
        """Creates a pandas DataFrame of random signals."""
        signals = pd.DataFrame(index=self.bars.index)
        signals['signal'] = np.sign(np.random.randn(len(signals)))

        # The first five elements are set to zero in order to minimise
        # upstream NaN errors in the forecaster.
        signals['signal'][0:5] = 0.0
        return signals

Bây giờ chúng ta có một hệ thống dự báo cụ thể, chúng ta phải tạo ra một thực hiện của một đối tượng danh mục đầu tư. Đối tượng này sẽ bao gồm phần lớn mã backtesting. Nó được thiết kế để tạo ra hai khung dữ liệu riêng biệt, đầu tiên là một khung vị trí, được sử dụng để lưu trữ số lượng của mỗi công cụ được nắm giữ tại bất kỳ thanh cụ cụ thể nào. Thứ hai, danh mục đầu tư, thực sự chứa giá thị trường của tất cả các cổ phần cho mỗi thanh, cũng như tổng số tiền mặt, giả sử vốn ban đầu. Điều này cuối cùng cung cấp một đường cong vốn chủ sở hữu để đánh giá hiệu suất chiến lược.

Đối tượng danh mục đầu tư, mặc dù cực kỳ linh hoạt trong giao diện của nó, đòi hỏi sự lựa chọn cụ thể khi xem xét cách xử lý chi phí giao dịch, lệnh thị trường vv. Trong ví dụ cơ bản này, tôi đã xem xét rằng sẽ có thể mua / mua ngắn một công cụ dễ dàng mà không có hạn chế hoặc biên, mua hoặc bán trực tiếp ở giá mở của thanh, không có chi phí giao dịch (bao gồm trượt, phí và tác động thị trường) và đã xác định số lượng cổ phiếu trực tiếp mua cho mỗi giao dịch.

Đây là sự tiếp tục của danh sách random_forecast.py:

# random_forecast.py

class MarketOnOpenPortfolio(Portfolio):
    """Inherits Portfolio to create a system that purchases 100 units of 
    a particular symbol upon a long/short signal, assuming the market 
    open price of a bar.

    In addition, there are zero transaction costs and cash can be immediately 
    borrowed for shorting (no margin posting or interest requirements). 

    Requires:
    symbol - A stock symbol which forms the basis of the portfolio.
    bars - A DataFrame of bars for a symbol set.
    signals - A pandas DataFrame of signals (1, 0, -1) for each symbol.
    initial_capital - The amount in cash at the start of the portfolio."""

    def __init__(self, symbol, bars, signals, initial_capital=100000.0):
        self.symbol = symbol        
        self.bars = bars
        self.signals = signals
        self.initial_capital = float(initial_capital)
        self.positions = self.generate_positions()
        
    def generate_positions(self):
    	"""Creates a 'positions' DataFrame that simply longs or shorts
    	100 of the particular symbol based on the forecast signals of
    	{1, 0, -1} from the signals DataFrame."""
        positions = pd.DataFrame(index=signals.index).fillna(0.0)
        positions[self.symbol] = 100*signals['signal']
        return positions
                    
    def backtest_portfolio(self):
    	"""Constructs a portfolio from the positions DataFrame by 
    	assuming the ability to trade at the precise market open price
    	of each bar (an unrealistic assumption!). 

    	Calculates the total of cash and the holdings (market price of
    	each position per bar), in order to generate an equity curve
    	('total') and a set of bar-based returns ('returns').

    	Returns the portfolio object to be used elsewhere."""

    	# Construct the portfolio DataFrame to use the same index
    	# as 'positions' and with a set of 'trading orders' in the
    	# 'pos_diff' object, assuming market open prices.
        portfolio = self.positions*self.bars['Open']
        pos_diff = self.positions.diff()

        # Create the 'holdings' and 'cash' series by running through
        # the trades and adding/subtracting the relevant quantity from
        # each column
        portfolio['holdings'] = (self.positions*self.bars['Open']).sum(axis=1)
        portfolio['cash'] = self.initial_capital - (pos_diff*self.bars['Open']).sum(axis=1).cumsum()

        # Finalise the total and bar-based returns based on the 'cash'
        # and 'holdings' figures for the portfolio
        portfolio['total'] = portfolio['cash'] + portfolio['holdings']
        portfolio['returns'] = portfolio['total'].pct_change()
        return portfolio

Điều này cung cấp cho chúng ta tất cả mọi thứ chúng ta cần để tạo ra một đường cong vốn chủ sở hữu dựa trên một hệ thống như vậy.chínhchức năng:

if __name__ == "__main__":
    # Obtain daily bars of SPY (ETF that generally 
    # follows the S&P500) from Quandl (requires 'pip install Quandl'
    # on the command line)
    symbol = 'SPY'
    bars = Quandl.get("GOOG/NYSE_%s" % symbol, collapse="daily")

    # Create a set of random forecasting signals for SPY
    rfs = RandomForecastingStrategy(symbol, bars)
    signals = rfs.generate_signals()

    # Create a portfolio of SPY
    portfolio = MarketOnOpenPortfolio(symbol, bars, signals, initial_capital=100000.0)
    returns = portfolio.backtest_portfolio()

    print returns.tail(10)

Kết quả của chương trình là như sau. của bạn sẽ khác với kết quả dưới đây tùy thuộc vào phạm vi ngày bạn chọn và hạt giống ngẫu nhiên được sử dụng:

          SPY  holdings    cash  total   returns

Ngày
2014-01-02 -18398 -18398 111486 93088 0.000097 2014-01-03 18321 18321 74844 93165 0.000827 2014-01-06 18347 18347 74844 93191 0.000279 2014-01-07 18309 18309 74844 93153 -0.000408 2014-01-08 -18345 -18345 111534 93189 0.000386 2014-01-09 -18410 -18410 111534 93124 -0.000698 2014-01-10 -18395 -18395 111534 93139 0.000161 2014-01-13 -18371 -18371 111534 93163 0.000258 2014-01-14 -18228 -18228 111534 93306 0.001535 2014-01-15 18410 18410 74714 93124 -0.001951

Trong trường hợp này, chiến lược đã mất tiền, điều này không đáng ngạc nhiên do bản chất ngẫu nhiên của nhà dự báo!

Chúng ta cũng có thể cải thiện đối tượng Portfolio để xử lý chi phí giao dịch một cách thực tế hơn (như hoa hồng và trượt). Chúng ta cũng có thể trực tiếp đưa một công cụ dự báo vào đối tượng Chiến lược, điều này sẽ (hy vọng) mang lại kết quả tốt hơn.


Thêm nữa