Kiểm tra ngược dựa trên sự kiện với Python - Phần V

Tác giả:Tốt, Tạo: 2019-03-25 15:54:16, Cập nhật:

Trong bài viết trước về kiểm tra ngược dựa trên sự kiện, chúng tôi đã xem xét cách xây dựng một hệ thống phân cấp lớp Chiến lược. Chiến lược, như được định nghĩa ở đây, được sử dụng để tạo ra các tín hiệu, được sử dụng bởi một đối tượng danh mục đầu tư để đưa ra quyết định về việc có nên gửi lệnh hay không. Như trước đây, nó là tự nhiên để tạo ra một lớp cơ sở trừu tượng danh mục đầu tư (ABC) mà tất cả các lớp con tiếp theo được thừa hưởng.

Bài viết này mô tả một đối tượng NaivePortfolio theo dõi các vị trí trong một danh mục đầu tư và tạo ra các đơn đặt hàng của một lượng cổ phiếu cố định dựa trên tín hiệu.

Theo dõi vị trí và quản lý đơn đặt hàng

Hệ thống quản lý đơn đặt hàng danh mục đầu tư có lẽ là thành phần phức tạp nhất của một backtester dựa trên sự kiện. Vai trò của nó là theo dõi tất cả các vị trí thị trường hiện tại cũng như giá trị thị trường của các vị trí (được gọi là holdings).

Ngoài việc quản lý các vị trí và cổ phần, danh mục đầu tư cũng phải nhận thức được các yếu tố rủi ro và các kỹ thuật kích thước vị trí để tối ưu hóa các lệnh được gửi đến môi giới hoặc các hình thức tiếp cận thị trường khác.

Tiếp tục theo hệ thống phân cấp lớp Event, một đối tượng Portfolio phải có khả năng xử lý các đối tượng SignalEvent, tạo các đối tượng OrderEvent và giải thích các đối tượng FillEvent để cập nhật vị trí.

Thực hiện

Chúng tôi tạo một tệp mớiportfolio.pyvà nhập các thư viện cần thiết. Chúng giống như hầu hết các thực hiện lớp cơ sở trừu tượng khác. Chúng tôi cần nhập hàm sàn từ thư viện toán học để tạo ra kích thước thứ tự có giá trị nguyên. Chúng tôi cũng cần các đối tượng FillEvent và OrderEvent vì Portfolio xử lý cả hai.

# portfolio.py

import datetime
import numpy as np
import pandas as pd
import Queue

từ abc nhập khẩu ABCMeta, abstractmethod từ sàn nhập toán

từ nhập sự kiện FillEvent, OrderEvent Như trước đây, chúng ta tạo ra một ABC cho Portfolio và có hai phương thức ảo tinh khiết update_signal và update_fill.

# portfolio.py

class Portfolio(object):
    """
    The Portfolio class handles the positions and market
    value of all instruments at a resolution of a "bar",
    i.e. secondly, minutely, 5-min, 30-min, 60 min or EOD.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        raise NotImplementedError("Should implement update_signal()")

    @abstractmethod
    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        raise NotImplementedError("Should implement update_fill()")

Chủ đề chính của bài viết này là lớp NaivePortfolio. Nó được thiết kế để xử lý kích thước vị trí và cổ phần hiện tại, nhưng sẽ thực hiện lệnh giao dịch theo cách "ngốc" bằng cách chỉ đơn giản gửi trực tiếp đến môi giới với kích thước số lượng cố định được xác định trước, bất kể số tiền mặt được nắm giữ.

NaivePortfolio đòi hỏi một giá trị vốn ban đầu, mà tôi đã thiết lập mặc định là 100.000 USD. Nó cũng đòi hỏi một thời gian bắt đầu.

Các danh mục đầu tư chứa các thành viên all_positions và current_positions. Thành viên đầu tiên lưu trữ danh sách tất cả các vị trí trước đó được ghi lại tại thời điểm đánh dấu thời gian của một sự kiện dữ liệu thị trường. Một vị trí chỉ đơn giản là số lượng của tài sản. Các vị trí âm có nghĩa là tài sản đã được bán ngắn. Thành viên sau lưu trữ một từ điển chứa các vị trí hiện tại cho bản cập nhật thanh thị trường cuối cùng.

Ngoài các thành viên vị trí, danh mục đầu tư lưu trữ cổ phần, mô tả giá trị thị trường hiện tại của các vị trí nắm giữ. Giá trị thị trường hiện tại trong trường hợp này có nghĩa là giá đóng được lấy từ thanh thị trường hiện tại, rõ ràng là một ước tính, nhưng đủ hợp lý trong thời gian này. all_holdings lưu trữ danh sách lịch sử của tất cả các cổ phần biểu tượng, trong khi current_holdings lưu trữ từ điển cập nhật nhất của tất cả các giá trị cổ phần biểu tượng.

# portfolio.py

class NaivePortfolio(Portfolio):
    """
    The NaivePortfolio object is designed to send orders to
    a brokerage object with a constant quantity size blindly,
    i.e. without any risk management or position sizing. It is
    used to test simpler strategies such as BuyAndHoldStrategy.
    """
    
    def __init__(self, bars, events, start_date, initial_capital=100000.0):
        """
        Initialises the portfolio with bars and an event queue. 
        Also includes a starting datetime index and initial capital 
        (USD unless otherwise stated).

        Parameters:
        bars - The DataHandler object with current market data.
        events - The Event Queue object.
        start_date - The start date (bar) of the portfolio.
        initial_capital - The starting capital in USD.
        """
        self.bars = bars
        self.events = events
        self.symbol_list = self.bars.symbol_list
        self.start_date = start_date
        self.initial_capital = initial_capital
        
        self.all_positions = self.construct_all_positions()
        self.current_positions = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )

        self.all_holdings = self.construct_all_holdings()
        self.current_holdings = self.construct_current_holdings()

Phương pháp sau, construct_all_positions, chỉ đơn giản tạo một từ điển cho mỗi ký hiệu, đặt giá trị thành 0 cho mỗi ký hiệu và sau đó thêm một khóa thời gian ngày, cuối cùng thêm nó vào danh sách.

# portfolio.py

    def construct_all_positions(self):
        """
        Constructs the positions list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        return [d]

Phương pháp construct_all_holdings tương tự như trên nhưng thêm các khóa bổ sung cho tiền mặt, hoa hồng và tổng số, tương ứng đại diện cho số tiền mặt dư thừa trong tài khoản sau khi mua hàng, hoa hồng tích lũy và tổng vốn tài khoản bao gồm tiền mặt và bất kỳ vị trí mở nào. Các vị trí ngắn được coi là âm. Tiền mặt bắt đầu và tổng vốn tài khoản đều được đặt theo vốn ban đầu:

# portfolio.py

    def construct_all_holdings(self):
        """
        Constructs the holdings list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        d['cash'] = self.initial_capital
        d['commission'] = 0.0
        d['total'] = self.initial_capital
        return [d]

Phương pháp sau, construct_current_holdings gần như giống với phương pháp trên ngoại trừ việc nó không gói từ điển trong một danh sách:

# portfolio.py

    def construct_current_holdings(self):
        """
        This constructs the dictionary which will hold the instantaneous
        value of the portfolio across all symbols.
        """
        d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
        d['cash'] = self.initial_capital
        d['commission'] = 0.0
        d['total'] = self.initial_capital
        return d

Trong một kịch bản giao dịch trực tiếp, thông tin này có thể được tải xuống và phân tích trực tiếp từ môi giới, nhưng để thực hiện backtesting, cần phải tính toán các giá trị này theo cách thủ công.

Thật không may, không có thứ như giá trị thị trường hiện tại do chênh lệch giá bán / bán và các vấn đề thanh khoản. Do đó, cần phải ước tính nó bằng cách nhân số lượng tài sản được nắm giữ bằng giá. Cách tiếp cận mà tôi đã thực hiện ở đây là sử dụng giá đóng của thanh cuối cùng được nhận. Đối với chiến lược trong ngày, điều này tương đối thực tế. Đối với chiến lược hàng ngày, điều này ít thực tế hơn vì giá mở có thể khác biệt đáng kể với giá đóng.

Phương pháp update_timeindex xử lý việc theo dõi các cổ phiếu mới. Đầu tiên nó lấy giá mới nhất từ trình xử lý dữ liệu thị trường và tạo ra một từ điển biểu tượng mới để đại diện cho các vị trí hiện tại, bằng cách đặt các vị trí new bằng với các vị trí current. Những vị trí này chỉ được thay đổi khi có FillEvent, được xử lý sau đó trong danh mục đầu tư. Sau đó, phương pháp thêm tập hợp các vị trí hiện tại này vào danh sách all_positions. Tiếp theo, cổ phiếu được cập nhật theo cách tương tự, ngoại trừ việc giá trị thị trường được tính lại bằng cách nhân các vị trí hiện tại với giá đóng của thanh mới nhất (self.current_positions[s] * bars[s][0][5]). Cuối cùng, các cổ phiếu mới được thêm vào tất cả các cổ phiếu:

# portfolio.py

    def update_timeindex(self, event):
        """
        Adds a new record to the positions matrix for the current 
        market data bar. This reflects the PREVIOUS bar, i.e. all
        current market data at this stage is known (OLHCVI).

        Makes use of a MarketEvent from the events queue.
        """
        bars = {}
        for sym in self.symbol_list:
            bars[sym] = self.bars.get_latest_bars(sym, N=1)

        # Update positions
        dp = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dp['datetime'] = bars[self.symbol_list[0]][0][1]

        for s in self.symbol_list:
            dp[s] = self.current_positions[s]

        # Append the current positions
        self.all_positions.append(dp)

        # Update holdings
        dh = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dh['datetime'] = bars[self.symbol_list[0]][0][1]
        dh['cash'] = self.current_holdings['cash']
        dh['commission'] = self.current_holdings['commission']
        dh['total'] = self.current_holdings['cash']

        for s in self.symbol_list:
            # Approximation to the real value
            market_value = self.current_positions[s] * bars[s][0][5]
            dh[s] = market_value
            dh['total'] += market_value

        # Append the current holdings
        self.all_holdings.append(dh)

Phương pháp update_positions_from_fill xác định xem FillEvent là mua hay bán và sau đó cập nhật từ điển current_positions phù hợp bằng cách thêm/từ trừ số lượng cổ phần chính xác:

# portfolio.py

    def update_positions_from_fill(self, fill):
        """
        Takes a FilltEvent object and updates the position matrix
        to reflect the new position.

        Parameters:
        fill - The FillEvent object to update the positions with.
        """
        # Check whether the fill is a buy or sell
        fill_dir = 0
        if fill.direction == 'BUY':
            fill_dir = 1
        if fill.direction == 'SELL':
            fill_dir = -1

        # Update positions list with new quantities
        self.current_positions[fill.symbol] += fill_dir*fill.quantity

update_holdings_from_fill tương tự như phương pháp trên nhưng thay vào đó cập nhật giá trị nắm giữ. Để mô phỏng chi phí của một FillEvent, phương pháp sau không sử dụng chi phí liên quan từ FillEvent. Tại sao vậy? Nói đơn giản, trong môi trường backtesting chi phí chứa thực sự không được biết và do đó phải được ước tính. Do đó chi phí chứa được đặt thành giá thị trường hiện tại (giá đóng thanh cuối cùng).

Một khi chi phí lấp đầy được biết, các cổ phần hiện tại, tiền mặt và tổng giá trị đều có thể được cập nhật.

# portfolio.py

    def update_holdings_from_fill(self, fill):
        """
        Takes a FillEvent object and updates the holdings matrix
        to reflect the holdings value.

        Parameters:
        fill - The FillEvent object to update the holdings with.
        """
        # Check whether the fill is a buy or sell
        fill_dir = 0
        if fill.direction == 'BUY':
            fill_dir = 1
        if fill.direction == 'SELL':
            fill_dir = -1

        # Update holdings list with new quantities
        fill_cost = self.bars.get_latest_bars(fill.symbol)[0][5]  # Close price
        cost = fill_dir * fill_cost * fill.quantity
        self.current_holdings[fill.symbol] += cost
        self.current_holdings['commission'] += fill.commission
        self.current_holdings['cash'] -= (cost + fill.commission)
        self.current_holdings['total'] -= (cost + fill.commission)

Phương pháp update_fill ảo thuần túy từ Portfolio ABC được thực hiện ở đây. Nó chỉ đơn giản là thực hiện hai phương thức trước đó, update_positions_from_fill và update_holdings_from_fill, đã được thảo luận ở trên:

# portfolio.py

    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        if event.type == 'FILL':
            self.update_positions_from_fill(event)
            self.update_holdings_from_fill(event)

Trong khi đối tượng Portfolio phải xử lý FillEvents, nó cũng phải chăm sóc việc tạo OrderEvents khi nhận được một hoặc nhiều SignalEvents. Phương pháp generate_naive_order chỉ đơn giản là lấy một tín hiệu để mua hoặc bán một tài sản và sau đó gửi một lệnh để làm như vậy cho 100 cổ phiếu của tài sản đó. Rõ ràng 100 là một giá trị tùy ý. Trong một thực hiện thực tế, giá trị này sẽ được xác định bởi quản lý rủi ro hoặc lớp phủ kích thước vị trí. Tuy nhiên, đây là một NaivePortfolio và vì vậy nó nghiêm ngây gửi tất cả các lệnh trực tiếp từ các tín hiệu, mà không có hệ thống rủi ro.

Phương pháp xử lý mong muốn, mua ngắn và thoát khỏi một vị trí, dựa trên số lượng hiện tại và biểu tượng cụ thể.

# portfolio.py

    def generate_naive_order(self, signal):
        """
        Simply transacts an OrderEvent object as a constant quantity
        sizing of the signal object, without risk management or
        position sizing considerations.

        Parameters:
        signal - The SignalEvent signal information.
        """
        order = None

        symbol = signal.symbol
        direction = signal.signal_type
        strength = signal.strength

        mkt_quantity = floor(100 * strength)
        cur_quantity = self.current_positions[symbol]
        order_type = 'MKT'

        if direction == 'LONG' and cur_quantity == 0:
            order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
        if direction == 'SHORT' and cur_quantity == 0:
            order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')   
    
        if direction == 'EXIT' and cur_quantity > 0:
            order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
        if direction == 'EXIT' and cur_quantity < 0:
            order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
        return order

Phương thức update_signal chỉ đơn giản gọi phương thức trên và thêm thứ tự được tạo vào hàng đợi sự kiện:

# portfolio.py

    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        if event.type == 'SIGNAL':
            order_event = self.generate_naive_order(event)
            self.events.put(order_event)

Phương pháp cuối cùng trong NaivePortfolio là tạo ra đường cong vốn chủ sở hữu. Điều này chỉ đơn giản tạo ra một dòng lợi nhuận, hữu ích cho tính toán hiệu suất và sau đó bình thường hóa đường cong vốn chủ sở hữu để dựa trên tỷ lệ phần trăm. Do đó, kích thước ban đầu của tài khoản bằng 1.0:

# portfolio.py

    def create_equity_curve_dataframe(self):
        """
        Creates a pandas DataFrame from the all_holdings
        list of dictionaries.
        """
        curve = pd.DataFrame(self.all_holdings)
        curve.set_index('datetime', inplace=True)
        curve['returns'] = curve['total'].pct_change()
        curve['equity_curve'] = (1.0+curve['returns']).cumprod()
        self.equity_curve = curve

Đối tượng danh mục đầu tư là khía cạnh phức tạp nhất của toàn bộ hệ thống backtest dựa trên sự kiện. Việc thực hiện ở đây, mặc dù phức tạp, tương đối cơ bản trong việc xử lý các vị trí. Các phiên bản sau sẽ xem xét quản lý rủi ro và kích thước vị trí, điều này sẽ dẫn đến một ý tưởng thực tế hơn về hiệu suất chiến lược.

Trong bài viết tiếp theo chúng ta sẽ xem xét phần cuối cùng của backtester điều khiển bởi sự kiện, cụ thể là đối tượng ExecutionHandler, được sử dụng để lấy các đối tượng OrderEvent và tạo các đối tượng FillEvent từ chúng.


Thêm nữa