使用 Python 进行事件驱动的回测-第二部分

使用 Python 进行事件驱动的回测-第二部分

我们讨论了如何构建策略类层次结构。策略,正如这里定义的,用于生成信号,投资组合对象使用这些信号来决定是否发送订单Portfolio。和以前一样,创建一个抽象基类 (ABC)是很自然的,所有后续子类都从其继承。

本文介绍了一个NaivePortfolio对象,它跟踪投资组合中的头寸并根据信号生成固定数量股票的订单。后续的投资组合对象将包括更复杂的风险管理工具,并将成为后续文章的主题。

仓位追踪和订单管理

投资组合订单管理系统可能是事件驱动回测器中最复杂的组件。其作用是跟踪所有当前市场头寸以及头寸的市场价值(称为“持仓”)。这只是头寸清算价值的估计值,部分来自回测器的数据处理功能。

除了头寸和持股管理之外,投资组合还必须了解风险因素和头寸调整技术,以优化发送给经纪公司或其他形式的市场准入的订单。

继续按照类Event层次结构,Portfolio对象必须能够处理SignalEvent对象、生成OrderEvent对象并解释对象以更新位置。因此,就代码行数 (LOC) 而言,对象通常是事件驱动系统中最大的组件,FillEvent这并不奇怪。Portfolio

执行

我们创建一个新文件portfolio.py并导入必要的库。这些与大多数其他抽象基类实现相同。我们需要从库floor中导入函数math以生成整数值的订单大小。我们还需要FillEventOrderEvent对象,因为Portfolio可以处理两者。

# portfolio.py

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

from abc import ABCMeta, abstractmethod
from math import floor

from event import FillEvent, OrderEvent

与之前一样,我们为 创建一个 ABC Portfolio,并有两个纯虚拟方法update_signalupdate_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类。它旨在处理头寸规模和当前持股,但将以“愚蠢”的方式执行交易订单,即直接将订单以预定的固定数量发送给经纪公司,而不考虑持有的现金。这些都是不切实际的假设,但它们有助于概述投资组合订单管理系统 (OMS) 如何以事件驱动的方式运作。

需要NaivePortfolio初始资本值,我已将其设置为默认值 100,000 美元。它还需要开始日期和时间。

投资组合包含all_positions和成员。前者存储在市场数据事件时间戳记录的所有先前头寸current_positions的列表。头寸只是资产的数量。负头寸表示资产已被卖空。后者成员存储一个字典,其中包含上次市场条更新的当前头寸。

除了头寸成员之外,投资组合还存储持仓,描述所持头寸的当前市场价值。“当前市场价值”在此实例中是指从当前市场栏获得的收盘价,这显然是一个近似值,但目前已经足够合理。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对象请求新的市场数据时,投资组合必须更新所有持有头寸的当前市场价值。在实时交易场景中,这些信息可以直接从经纪公司下载和解析,但对于回测实施,必须手动计算这些值。

不幸的是,由于买卖价差和流动性问题,并不存在“当前市场价值”这样的东西。因此,有必要通过将持有的资产数量乘以“价格”来估算它。我在这里采取的方法是使用收到的最后一根柱线的收盘价。对于日内策略来说,这是相对现实的。对于日内策略来说,这不太现实,因为开盘价可能与收盘价有很大差异。

该方法update_timeindex处理新持仓跟踪。它首先从市场数据处理程序获取最新价格,并通过将“新”持仓设置为“当前”持仓,创建一个新的符号词典来表示当前持仓。只有在FillEvent获得 时才会更改这些,稍后将在投资组合中处理。然后,该方法将这组当前持仓附加到列表中all_positions。接下来,以类似的方式更新持仓,不同之处在于通过将当前持仓数乘以最新条形图的收盘价(self.current_positions[s] * bars[s][0][5])来重新计算市场价值。最后将新持仓附加到all_holdings

# 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这里实现了 ABC中的纯虚方法Portfolio。它只是执行前面两个方法update_positions_from_fillupdate_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对象必须处理s,但它还必须在收到一个或多个s 后FillEvent负责生成s 。该方法只需接收信号来做多或做空资产,然后发送订单以执行 100 股此类资产的操作。显然 100 是一个任意值。在实际实施中,此值将由风险管理或头寸调整覆盖层确定。然而,这是一个,因此它“天真地”直接从信号发送所有订单,而没有风险系统。OrderEvent``SignalEvent``generate_naive_order``NaivePortfolio

该方法根据当前数量和特定符号处理头寸的买入、卖出和退出。OrderEvent然后生成相应的对象:

# 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从中创建对象。

事件驱动回测实现的讨论之前已经考虑了事件循环事件类层次结构数据处理组件。在本文中,将概述策略类层次结构。Strategy对象将市场数据作为输入并产生交易信号事件作为输出。

对象Strategy封装了所有针对市场数据的计算,这些计算会向对象生成咨询信号Portfolio。在事件驱动回测器开发的这个阶段,没有指标过滤的概念,例如技术交易中的概念。这些也是创建类层次结构的良好候选者,但超出了本文的范围。

策略层次结构相对简单,因为它由一个抽象基类和一个用于生成SignalEvent对象的纯虚拟方法组成。为了创建Strategy层次结构,需要导入 NumPy、pandas、Queue 对象、抽象基类工具和SignalEvent

# strategy.py

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

from abc import ABCMeta, abstractmethod

from event import SignalEvent

抽象Strategy基类仅定义纯虚拟calculate_signals方法。在派生类中,该方法用于处理SignalEvent基于市场数据更新的对象生成:

# strategy.py

class Strategy(object):
    """
    Strategy is an abstract base class providing an interface for
    all subsequent (inherited) strategy handling objects.

    The goal of a (derived) Strategy object is to generate Signal
    objects for particular symbols based on the inputs of Bars 
    (OLHCVI) generated by a DataHandler object.

    This is designed to work both with historic and live data as
    the Strategy object is agnostic to the data source,
    since it obtains the bar tuples from a queue object.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def calculate_signals(self):
        """
        Provides the mechanisms to calculate the list of signals.
        """
        raise NotImplementedError("Should implement calculate_signals()")

ABC的定义Strategy很简单。我们对Strategy对象进行子类化的第一个示例利用买入并持有策略来创建BuyAndHoldStrategy类。这只是在某个日期买入并持有某只证券并将其保留在投资组合中。因此,每只证券只会生成一个信号。

构造函数(__init__)需要bars市场数据处理程序和events事件队列对象:

# strategy.py

class BuyAndHoldStrategy(Strategy):
    """
    This is an extremely simple strategy that goes LONG all of the 
    symbols as soon as a bar is received. It will never exit a position.

    It is primarily used as a testing mechanism for the Strategy class
    as well as a benchmark upon which to compare other strategies.
    """

    def __init__(self, bars, events):
        """
        Initialises the buy and hold strategy.

        Parameters:
        bars - The DataHandler object that provides bar information
        events - The Event Queue object.
        """
        self.bars = bars
        self.symbol_list = self.bars.symbol_list
        self.events = events

        # Once buy & hold signal is given, these are set to True
        self.bought = self._calculate_initial_bought()

在初始化BuyAndHoldStrategy字典bought成员时,每个符号都有一组键,这些键都设置为 False。一旦资产被“看涨”,则将其设置为 True。本质上,这允许策略知道它是否“在市场中”:

# strategy.py

    def _calculate_initial_bought(self):
        """
        Adds keys to the bought dictionary for all symbols
        and sets them to False.
        """
        bought = {
    
    }
        for s in self.symbol_list:
            bought[s] = False
        return bought

纯虚拟方法calculate_signals在此类中具体实现。该方法循环遍历符号列表中的所有符号并从bars数据处理程序中检索最新的条形图。然后检查该符号是否已被“购买”(即我们是否在该符号的市场中),如果没有,则创建一个SignalEvent对象。然后将其放在events队列中,并且bought字典正确地更新为该特定符号键的 True:

# strategy.py

    def calculate_signals(self, event):
        """
        For "Buy and Hold" we generate a single signal per symbol
        and then no additional signals. This means we are 
        constantly long the market from the date of strategy
        initialisation.

        Parameters
        event - A MarketEvent object. 
        """
        if event.type == 'MARKET':
            for s in self.symbol_list:
                bars = self.bars.get_latest_bars(s, N=1)
                if bars is not None and bars != []:
                    if self.bought[s] == False:
                        # (Symbol, Datetime, Type = LONG, SHORT or EXIT)
                        signal = SignalEvent(bars[0][0], bars[0][1], 'LONG')
                        self.events.put(signal)
                        self.bought[s] = True

这显然是一种简单的策略,但足以展示事件驱动策略层次结构的性质。在后续文章中,我们将考虑更复杂的策略,例如配对交易。在下一篇文章中,我们将考虑如何创建一个Portfolio层次结构,以盈亏(“PnL”)跟踪我们的头寸。

事件驱动回测实现的讨论之前已经考虑了事件循环事件类层次结构数据处理组件。在本文中,将概述策略类层次结构。Strategy对象将市场数据作为输入并产生交易信号事件作为输出。

对象Strategy封装了所有针对市场数据的计算,这些计算会向对象生成咨询信号Portfolio。在事件驱动回测器开发的这个阶段,没有指标过滤的概念,例如技术交易中的概念。这些也是创建类层次结构的良好候选者,但超出了本文的范围。

策略层次结构相对简单,因为它由一个抽象基类和一个用于生成SignalEvent对象的纯虚拟方法组成。为了创建Strategy层次结构,需要导入 NumPy、pandas、Queue 对象、抽象基类工具和SignalEvent

# strategy.py

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

from abc import ABCMeta, abstractmethod

from event import SignalEvent

抽象Strategy基类仅定义纯虚拟calculate_signals方法。在派生类中,该方法用于处理SignalEvent基于市场数据更新的对象生成:

# strategy.py

class Strategy(object):
    """
    Strategy is an abstract base class providing an interface for
    all subsequent (inherited) strategy handling objects.

    The goal of a (derived) Strategy object is to generate Signal
    objects for particular symbols based on the inputs of Bars 
    (OLHCVI) generated by a DataHandler object.

    This is designed to work both with historic and live data as
    the Strategy object is agnostic to the data source,
    since it obtains the bar tuples from a queue object.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def calculate_signals(self):
        """
        Provides the mechanisms to calculate the list of signals.
        """
        raise NotImplementedError("Should implement calculate_signals()")

ABC的定义Strategy很简单。我们对Strategy对象进行子类化的第一个示例利用买入并持有策略来创建BuyAndHoldStrategy类。这只是在某个日期买入并持有某只证券并将其保留在投资组合中。因此,每只证券只会生成一个信号。

构造函数(__init__)需要bars市场数据处理程序和events事件队列对象:

# strategy.py

class BuyAndHoldStrategy(Strategy):
    """
    This is an extremely simple strategy that goes LONG all of the 
    symbols as soon as a bar is received. It will never exit a position.

    It is primarily used as a testing mechanism for the Strategy class
    as well as a benchmark upon which to compare other strategies.
    """

    def __init__(self, bars, events):
        """
        Initialises the buy and hold strategy.

        Parameters:
        bars - The DataHandler object that provides bar information
        events - The Event Queue object.
        """
        self.bars = bars
        self.symbol_list = self.bars.symbol_list
        self.events = events

        # Once buy & hold signal is given, these are set to True
        self.bought = self._calculate_initial_bought()

在初始化BuyAndHoldStrategy字典bought成员时,每个符号都有一组键,这些键都设置为 False。一旦资产被“看涨”,则将其设置为 True。本质上,这允许策略知道它是否“在市场中”:

# strategy.py

    def _calculate_initial_bought(self):
        """
        Adds keys to the bought dictionary for all symbols
        and sets them to False.
        """
        bought = {
    
    }
        for s in self.symbol_list:
            bought[s] = False
        return bought

纯虚拟方法calculate_signals在此类中具体实现。该方法循环遍历符号列表中的所有符号并从bars数据处理程序中检索最新的条形图。然后检查该符号是否已被“购买”(即我们是否在该符号的市场中),如果没有,则创建一个SignalEvent对象。然后将其放在events队列中,并且bought字典正确地更新为该特定符号键的 True:

# strategy.py

    def calculate_signals(self, event):
        """
        For "Buy and Hold" we generate a single signal per symbol
        and then no additional signals. This means we are 
        constantly long the market from the date of strategy
        initialisation.

        Parameters
        event - A MarketEvent object. 
        """
        if event.type == 'MARKET':
            for s in self.symbol_list:
                bars = self.bars.get_latest_bars(s, N=1)
                if bars is not None and bars != []:
                    if self.bought[s] == False:
                        # (Symbol, Datetime, Type = LONG, SHORT or EXIT)
                        signal = SignalEvent(bars[0][0], bars[0][1], 'LONG')
                        self.events.put(signal)
                        self.bought[s] = True

这显然是一种简单的策略,但足以展示事件驱动策略层次结构的性质。在后续文章中,我们将考虑更复杂的策略,例如配对交易。在下一篇文章中,我们将考虑如何创建一个Portfolio层次结构,以盈亏(“PnL”)跟踪我们的头寸。

猜你喜欢

转载自blog.csdn.net/m0_74840398/article/details/143040085