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
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ó 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.
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.
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:
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.
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.
Đố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à
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
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ể.
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
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.