使用 Python 进行事件驱动的回测-第二部分
我们讨论了如何构建策略类层次结构。策略,正如这里定义的,用于生成信号,投资组合对象使用这些信号来决定是否发送订单Portfolio
。和以前一样,创建一个抽象基类 (ABC)是很自然的,所有后续子类都从其继承。
本文介绍了一个NaivePortfolio
对象,它跟踪投资组合中的头寸并根据信号生成固定数量股票的订单。后续的投资组合对象将包括更复杂的风险管理工具,并将成为后续文章的主题。
仓位追踪和订单管理
投资组合订单管理系统可能是事件驱动回测器中最复杂的组件。其作用是跟踪所有当前市场头寸以及头寸的市场价值(称为“持仓”)。这只是头寸清算价值的估计值,部分来自回测器的数据处理功能。
除了头寸和持股管理之外,投资组合还必须了解风险因素和头寸调整技术,以优化发送给经纪公司或其他形式的市场准入的订单。
继续按照类Event
层次结构,Portfolio
对象必须能够处理SignalEvent
对象、生成OrderEvent
对象并解释对象以更新位置。因此,就代码行数 (LOC) 而言,对象通常是事件驱动系统中最大的组件,FillEvent
这并不奇怪。Portfolio
执行
我们创建一个新文件portfolio.py
并导入必要的库。这些与大多数其他抽象基类实现相同。我们需要从库floor
中导入函数math
以生成整数值的订单大小。我们还需要FillEvent
和OrderEvent
对象,因为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_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
类。它旨在处理头寸规模和当前持股,但将以“愚蠢”的方式执行交易订单,即直接将订单以预定的固定数量发送给经纪公司,而不考虑持有的现金。这些都是不切实际的假设,但它们有助于概述投资组合订单管理系统 (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_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
对象必须处理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”)跟踪我们的头寸。