成功的量化交易 PART 6——自动化交易(4)


成功的量化交易 PART 6——自动化交易(4)

策略


一个策略对象封装了对市场数据的所有计算,这些数据会向一个投资组合对象生成咨询信号。因此,所有的“策略逻辑”都驻留在这个类中。我选择为这个回测软件分离出策略和投资组合对象,因为我相信这更适合向一个更大的投资组合提供“想法”的多种策略的情况,然后它可以处理它自己的风险(例如部门分配、杠杆)。在高频率交易中,策略和投资组合的概念将是紧密耦合的,并且极其依赖于硬件。然而,这远远超出了本章的范围!


在事件驱动的回测软件开发的这个阶段,没有指标或过滤器的概念,就像在技术交易中发现的那样。它们也是创建类层次结构的良好候选对象,但超出了本章的范围。因此,这些机制将直接用于派生的策略对象。


策略层次结构相对简单,因为它由一个抽象基类和一个用于生成信号事件对象的纯虚方法组成。为了创建策略层次结构,需要导入NumPy、panda、队列对象(在Python 3中已经变成队列)、抽象基类工具和信号事件:

#!/usr/bin/python

# -*- coding: utf-8 -*-

# strategy.py from __future__ import print_function

from abc import ABCMeta, abstractmethod

import datetime

try:

import Queue as queue

except ImportError:

import queue

import numpy as np

import pandas as pd

from event import SignalEvent


策略抽象基类简单地定义了一个纯虚calculate_signals方法。在派生类中,它用于处理基于市场数据更新的信号事件对象的生成:

# strategy.py
class Strategy(object):
"""
Strategy是一个抽象基类,为所有后续(继承的)策略处理对象提供接口。
(派生的)策略对象的目标是基于数据处理器对象生成的bar (OHLCV)输入
为特定品种生成信号对象。
这样设计是为了同时处理历史数据和实时数据,


因为策略对象不知道数据来自何处,
由于它从队列对象获取bar元组。
"""
__metaclass__ = ABCMeta
@abstractmethod
def calculate_signals(self):
"""
提供计算信号列表的机制。
"""
raise NotImplementedError("Should implement calculate_signals()")


投资组合


本节描述一个投资组合对象,该对象跟踪投资组合中的头寸,并根据信号生成固定数量的股票订单。更复杂的投资组合对象可能包括风险管理和头寸估计工具(如Kelly标准)。事实上,在接下来的章节中,我们将把这些工具添加到我们的一些交易策略中,看看它们如何与更“天真”的投资组合方法相比较。


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


除了头寸和持仓管理之外,投资组合还必须了解风险因素和头寸规模估计技术,以便优化发送给经纪公司或其他市场渠道的订单。


不幸的是,投资组合和订单管理系统(OMS)可能变得相当复杂!因此,我在这里做了一个决定,保持投资组合对象相对简单,这样您就可以理解关键思想以及它们是如何实现的。面向对象设计的本质是允许以一种自然的方式扩展到更复杂的情况。


按照事件类层次结构的脉络,投资组合对象必须能够处理信号事件对象、生成订单事件对象和解释填充事件对象来更新位置。因此,就代码行数(LOC)而言,项目组合对象通常是事件驱动系统中最大的组件,这并不奇怪。


我们创建了一个新的文件portfolio.py并导入了必要的库。这些与大多数其他类实现是相同的,只是投资组合不是一个抽象基类。相反,它将是普通的基类。这意味着它可以被实例化,因此在测试新策略时,它可以作为“第一次尝试”的投资组合对象。其他的投资组合可以从它派生出来,并覆盖部分以增加更多的复杂性。


为了完整性,这里是performance.py文件:

#!/usr/bin/python
# -*- coding: utf-8 -*-
# performance.py
from __future__ import print_function
import numpy as np
import pandas as pd
def create_sharpe_ratio(returns, periods=252):
"""
基于零基准(即无无风险利率信息),
为该策略创建夏普比率。
参数:
returns -一个pandas序列,代表一个周期百分比回报。
periods - 每日(252)、每小时(252*6.5)、分钟(252*6.5*60)等。
"""
return np.sqrt(periods) * (np.mean(returns)) / np.std(returns)
def create_drawdowns(pnl):
"""
计算PnL曲线最大的峰谷落差,以及落差持续的时间。
要求pnl_returns是一个pandas序列。
参数:
pnl -pandas序列代表期间百分比回报。
返回:
drawdown, duration - 回撤,持续时间-最高峰值到低谷回撤和持续时间。
"""
# 计算累计收益曲线
# 并设置高水位标志
hwm = [0]
# 创建回撤和持续时间序列


idx = pnl.index
drawdown = pd.Series(index = idx)
duration = pd.Series(index = idx)
# 遍历索引范围
for t in range(1, len(idx)):
hwm.append(max(hwm[t-1], pnl[t]))
drawdown[t]= (hwm[t]-pnl[t])
duration[t]= (0 if drawdown[t] == 0 else duration[t-1]+1)
return drawdown, drawdown.max(), duration.max()


下面是Portfolio.py文件的导入清单。我们需要从数学库中导入floor函数来生成整数值的订单大小。我们还需要填充事件和订单事件对象,因为投资组合同时处理这两个对象。还要注意,我们添加了两个额外的函数,create_sharpe_ratio和create_drawdowns,它们都来自上面描述的performance.py文件。

#!/usr/bin/python
# -*- coding: utf-8 -*-
# portfolio.py
from __future__ import print_function
import datetime
from math import floor
try:
import Queue as queue
except ImportError:
import queue
import numpy as np
import pandas as pd
from event import FillEvent, OrderEvent
from performance import create_sharpe_ratio, create_drawdowns


投资组合对象的初始化需要访问bars 数据处理器、事件的事件队列、起始日期时间戳和初始资本值(默认为100,000美元)。


该投资组合的设计目的是处理头寸规模和当前持有的头寸,但它将以一种“愚蠢”的方式执行交易指令,即直接将这些指令发送给预先设定了固定数量规模的券商,而不管持有多少现金。这些都是不现实的假设,但它们有助于帮助理解项目组合订单管理系统(OMS)如何以事件驱动的方式工作。


投资组合包含all_positions和current_positions成员。前者存储在市场数据事件的时间戳中记录的所有先前头寸的列表。头寸就是所持有资产的数量。空头头寸意味着该资产已被做空。后一个current_positions字典存储区包含针对每个品种的最新市场bar更新的当前位置。


除了头寸数据外,投资组合还存储描述所持头寸当前市场价值的持有量。本例中的“当前市值”是指从当前市场条形图中获得的收盘价,这显然是一个近似值,但就目前而言是足够合理的。all_holdings存储所有品种持有的历史列表,而current_holdings存储所有品种持有值的最新字典:


# portfolio.py
class Portfolio(object):
"""
投资组合类以“条”的分辨率处理所有工具的头寸和市场价值,
即,秒、分钟、5分钟、30分钟、60分钟或EOD。
位置DataFrame存储所持有位置数量的时间索引。
持有量DataFrame存储特定时间指数的每个品种的现金和总市场持有量值,
以及跨bar的投资组合总变化百分比。
"""
def __init__(self, bars, events, start_date, initial_capital=100000.0):
"""
用bars和事件队列初始化投资组合。
还包括起始日期时间指数和初始资本(美元,除非另有说明)。
参数:
bars - 带当前市场数据的数据处理器对象。
events - 事件队列对象。
start_date - 投资组合的开始日期(bar)。
initial_capital - 初始资本(美元)。
"""
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只是为每个品种创建一个字典,将每个品种的值设置为0,然后添加一个datetime键,最后将其添加到一个列表中。它使用字典理解,这在精神上类似于列表理解:

# portfolio.py
def construct_all_positions(self):
"""
使用start_date构造位置列表,
以确定何时开始执行时间索引。
"""
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方法与上述方法类似,但为现金、佣金和总计添加了额外的密钥,分别表示在任何购买之后帐户中的闲置现金、累计佣金应计值和包括现金和任何未平仓的帐户总股本。空头头寸被视为空头。初始现金和账户总股本都设定为初始资本。


以这种方式,每个品种都有单独的“账户”、“手头现金”、支付的“佣金”(交互经纪人费用)和“总”投资组合价值。显然,这没有考虑保证金要求或做空限制,但足以让你大致了解这样一个OMS是如何创建的:


# portfolio.py
def construct_all_holdings(self):
"""
使用start_date构造holdings列表
以确定何时开始执行时间索引。
"""
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):
"""
这样就构建了一个字典,
它将包含所有品种组合的瞬时值。
"""
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


分享到:


相關文章: