События, обусловленные обратным тестированием с Python - Часть V

Автор:Доброта, Создано: 2019-03-25 15:54:16, Обновлено:

В предыдущей статье о обратном тестировании, основанном на событиях, мы рассмотрели, как построить иерархию классов стратегии. Стратегии, как определено здесь, используются для генерации сигналов, которые используются объектом портфеля для принятия решений о том, отправлять ли заказы. Как и ранее, естественно создать абстрактный базовый класс портфеля (ABC), от которого наследуют все последующие подклассы.

В этой статье описывается объект NaivePortfolio, который отслеживает позиции в портфеле и генерирует заказы на фиксированное количество акций на основе сигналов.

Отслеживание позиций и управление заказами

Система управления портфельными ордерами, возможно, является самым сложным компонентом бактэстера, основанного на событиях. Ее роль заключается в отслеживании всех текущих рыночных позиций, а также рыночной стоимости позиций (известной как держания). Это просто оценка ликвидационной стоимости позиции и частично получена из устройства обработки данных бактэстера.

В дополнение к управлению позициями и пакетом активов портфель должен также знать о факторах риска и методах размещения позиций, чтобы оптимизировать заказы, которые отправляются в брокерскую или другую форму доступа к рынку.

Продолжая в иерархии класса Event, объект Portfolio должен иметь возможность обрабатывать объекты SignalEvent, генерировать объекты OrderEvent и интерпретировать объекты FillEvent для обновления позиций.

Использование

Мы создаем новый файлportfolio.pyи импортировать необходимые библиотеки. Это то же самое, что и большинство других абстрактных реализаций базовых классов. Нам нужно импортировать полную функцию из математической библиотеки, чтобы генерировать цельнозначные размеры порядка. Нам также нужны объекты FillEvent и OrderEvent, поскольку Portfolio обрабатывает оба.

# portfolio.py

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

из abc import ABCMeta, абстрактный метод из класса математического импорта

из импорта события FillEvent, OrderEvent Как и ранее, мы создаем ABC для Portfolio и имеем два чистых виртуальных метода update_signal и 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()")

Основным предметом этой статьи является класс NaivePortfolio. Он предназначен для обработки размеров позиций и текущих активов, но будет выполнять торговые заказы "глупым" способом, просто отправляя их непосредственно в брокерскую компанию с заранее определенным фиксированным количеством, независимо от наличности.

NaivePortfolio требует начальной стоимости капитала, которую я установил на 100 000 долларов США.

Портфель содержит все_позиции и текущие_позиции. Первый хранит список всех предыдущих позиций, зарегистрированных на момент события с данными рынка. Позиция - это просто количество актива. Отрицательные позиции означают, что актив был сокращен. Последний хранит словарь, содержащий текущие позиции для последнего обновления рыночной строки.

В дополнение к позициям участников портфель сохраняет акции, которые описывают текущую рыночную стоимость имеющихся позиций. Текущая рыночная стоимость в данном случае означает цену закрытия, полученную из текущей рыночной стойки, которая явно является приближением, но в настоящее время является достаточно разумной. all_holdings сохраняет исторический список всех символьных акций, в то время как current_holdings сохраняет самый современный словарь всех ценностей символьных акций.

# 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()

Следующий метод, construct_all_positions, просто создает словарь для каждого символа, устанавливает значение на нуль для каждого и затем добавляет ключ даты и времени, наконец, добавляя его в список.

# 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]

Метод construct_all_holdings аналогичен вышеизложенному, но добавляет дополнительные ключи для наличности, комиссии и суммы, которые соответственно представляют собой свободные денежные средства на счету после любых покупок, накопленную комиссию и общий капитал счета, включая наличные и любые открытые позиции. Краткие позиции рассматриваются как отрицательные. Начальные денежные средства и общий капитал счета устанавливаются на начальный капитал:

# 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]

Следующий метод, construct_current_holdings почти идентичен методу выше, за исключением того, что он не обертывает словарь в список:

# 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

При каждом сердцебиении , то есть каждый раз, когда новые рыночные данные запрашиваются от объекта DataHandler, портфель должен обновлять текущую рыночную стоимость всех позиций.

К сожалению, не существует такого понятия, как текущая рыночная стоимость из-за спредов bid/ask и проблем с ликвидностью. Таким образом, необходимо оценить его, умножив количество актива, удерживаемого на цену. Подход, который я использовал здесь, заключается в использовании цены закрытия последнего полученного бара. Для внутридневной стратегии это относительно реалистично. Для ежедневной стратегии это менее реалистично, поскольку цена открытия может существенно отличаться от цены закрытия.

Метод update_timeindex обрабатывает отслеживание новых позиций. Во-первых, он получает последние цены из обработчика данных рынка и создает новый словарь символов для представления текущих позиций, установив new позиции, равные current позициям. Они изменяются только при получении FillEvent, который обрабатывается позже в портфеле. Затем метод добавляет этот набор текущих позиций в список all_positions. Далее акции обновляются аналогичным образом, за исключением того, что рыночная стоимость пересчитывается путем умножения текущих позиций с ценой закрытия последней строки (self.current_positions[s] * bars[s][0][5]).

# 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)

Метод update_positions_from_fill определяет, является ли FillEvent покупкой или продажей, а затем соответствующим образом обновляет словарь current_positions, добавляя/отнимая правильное количество акций:

# 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 похож на вышеуказанный метод, но вместо этого обновляет значения холдингов. Чтобы смоделировать стоимость заполнения, следующий метод не использует стоимость, связанную с FillEvent. Почему это так? Проще говоря, в среде обратного тестирования стоимость заполнения на самом деле неизвестна, и, следовательно, должна быть оценена. Таким образом, стоимость заполнения устанавливается на текущую рыночную цену (цену закрытия последней строки).

После того, как стоимость заполнения будет известна, текущие запасы, денежные средства и общая стоимость могут быть обновлены.

# 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)

Здесь реализован чистый виртуальный метод update_fill из Portfolio ABC. Он просто выполняет два предыдущих метода, update_positions_from_fill и update_holdings_from_fill, которые уже обсуждались выше:

# 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)

В то время как объект Portfolio должен обрабатывать FillEvents, он также должен заботиться о генерации OrderEvents при получении одного или нескольких SignalEvents. Метод generate_naive_order просто берет сигнал для длинного или короткого актива, а затем отправляет ордер на 100 акций такого актива. Очевидно, что 100 является произвольным значением. В реалистичной реализации это значение будет определено управлением рисками или размером позиции. Однако это NaivePortfolio, и поэтому он наивно отправляет все ордера непосредственно из сигналов, без системы риска.

Метод обрабатывает длительное, короткое и выход из позиции, на основе текущего количества и конкретного символа.

# 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

Метод update_signal просто вызывает вышеуказанный метод и добавляет генерируемый порядок в очередь событий:

# 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)

Последним методом в NaivePortfolio является генерация кривой доли. Это просто создает поток доходов, полезный для расчетов производительности, а затем нормализует кривую доли на основе процента. Таким образом, начальный размер счета равен 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

Объект Portfolio является самым сложным аспектом всей системы обратного теста, основанной на событиях. Реализация здесь, хотя и сложна, относительно элементарна в обращении с позициями.

В следующей статье мы рассмотрим заключительную часть движимого событиями бэкстестера, а именно объект ExecutionHandler, который используется для получения объектов OrderEvent и создания из них объектов FillEvent.


Больше