精通-Python-金融第二版(四)

精通 Python 金融第二版(四)

原文:zh.annas-archive.org/md5/8b046e39ce2c1a10ac13fd89834aaadc

译者:飞龙

协议:CC BY-NC-SA 4.0

第八章:构建算法交易平台

算法交易自动化系统交易流程,根据定价、时机和成交量等多种因素以尽可能最佳价格执行订单。经纪公司可能会为希望部署自己交易算法的客户提供应用程序编程接口API)作为其服务提供的一部分。算法交易系统必须非常健壮,以处理订单执行过程中的任何故障点。网络配置、硬件、内存管理、速度和用户体验是设计执行订单系统时需要考虑的一些因素。设计更大的系统不可避免地会给框架增加更多复杂性。

一旦在市场上开立头寸,就会面临各种风险,如市场风险、利率风险和流动性风险。为了尽可能保护交易资本,将风险管理措施纳入交易系统非常重要。金融行业中最常用的风险度量可能是风险价值VaR)技术。我们将讨论 VaR 的优点和缺点,以及如何将其纳入我们将在本章开发的交易系统中。

在本章中,我们将涵盖以下主题:

  • 算法交易概述

  • 具有公共 API 的经纪人和系统供应商列表

  • 为交易系统选择编程语言

  • 设计算法交易平台

  • 在 Oanda v20 Python 模块上设置 API 访问

  • 实施均值回归算法交易策略

  • 实施趋势跟踪算法交易策略

  • 在我们的交易系统中引入 VaR 进行风险管理

  • 在 Python 上对 AAPL 进行 VaR 计算

介绍算法交易

上世纪 90 年代,交易所已经开始使用电子交易系统。到 1997 年,全球 44 个交易所使用自动化系统进行期货和期权交易,更多交易所正在开发自动化技术。芝加哥期货交易所(CBOT)和伦敦国际金融期货和期权交易所(LIFFE)等交易所将他们的电子交易系统用作传统的公开喊价交易场所之外的交易补充,从而使交易者可以全天候访问交易所的风险管理工具。随着技术的改进,基于技术的交易变得更加廉价,推动了更快更强大的交易平台的增长。订单执行的可靠性更高,消息传输错误率更低,这加深了金融机构对技术的依赖。大多数资产管理人、专有交易者和做市商已经从交易场所转移到了电子交易场所。

随着系统化或计算机化交易变得更加普遍,速度成为决定交易结果的最重要因素。通过利用复杂的基本模型,量化交易者能够动态重新计算交易产品的公平价值并执行交易决策,从而能够以牺牲使用传统工具的基本交易者的利润。这催生了高频交易HFT)这一术语,它依赖快速计算机在其他人之前执行交易决策。事实上,高频交易已经发展成为一个价值数十亿美元的行业。

算法交易是指对系统化交易流程的自动化,其中订单执行被大大优化以获得最佳价格。它不是投资组合配置过程的一部分。

银行、对冲基金、经纪公司、结算公司和交易公司通常会将他们的服务器放置在电子交易所旁边,以接收最新的市场价格,并在可能的情况下执行最快的订单。他们给交易所带来了巨大的交易量。任何希望参与低延迟、高交易量活动(如复杂事件处理或捕捉瞬息的价格差异)的人,可以通过获得交易所连接的方式来进行,可以选择共同定位的形式,他们的服务器硬件可以放置在交易所旁边的机架上,需要支付一定费用。

金融信息交换FIX)协议是与交易所进行电子通信的行业标准,从私人服务器实现直接市场访问DMA)到实时信息。C++是在 FIX 协议上进行交易的常见选择,尽管其他语言,如.NET Framework 公共语言和 Java 也可以使用。表述性状态转移REST)API 提供正在变得越来越普遍,供零售投资者使用。在创建算法交易平台之前,您需要评估各种因素,如学习的速度和便捷性,然后才能决定特定的语言用于此目的。

经纪公司将为他们的客户提供某种交易平台,以便他们可以在选定的交易所上执行订单,作为佣金费用的回报。一些经纪公司可能会提供 API 作为他们的服务提供的一部分,以满足技术倾向的客户希望运行自己的交易算法。在大多数情况下,客户也可以从第三方供应商提供的多个商业交易平台中进行选择。其中一些交易平台也可能提供 API 访问以将订单电子路由到交易所。在开发算法交易系统之前,重要的是事先阅读 API 文档,了解经纪人提供的技术能力,并制定开发算法交易系统的方法。

具有公共 API 的交易平台

以下表格列出了一些经纪人和交易平台供应商,他们的 API 文档是公开可用的:

经纪人/供应商 网址 支持的编程语言
CQG www.cqg.com REST, FIX, C#, C++, and VB/VBA
Cunningham Trading Systems www.ctsfutures.com Microsoft .Net Framework 4.0 and FIX
E*Trade developer.etrade.com/home Python, Java, and Node.js
Interactive Brokers www.interactivebrokers.com/en/index.php?f=5041 Java, C++, Python, C#, C++, and DDE
IG labs.ig.com/ REST, Java, JavaScript, .NET, Clojure, and Node.js
Tradier developer.tradier.com/ REST
Trading Technologies www.tradingtechnologies.com/trading/apis/ REST, .NET, and FIX
OANDA developer.oanda.com/ REST, Java, and FIX
FXCM www.fxcm.com/uk/algorithmic-trading/api-trading/ REST, Java, and FIX

选择编程语言

对于与经纪人或供应商进行接口的多种编程语言选择,对于刚开始进行算法交易平台开发的人来说,自然而然会产生一个问题:我应该使用哪种语言?

在回答这个问题之前,重要的是要弄清楚你的经纪人是否提供开发者工具。RESTful API 正变得越来越普遍,与 FIX 协议访问并列。少数经纪人支持 Java 和 C#。使用 RESTful API,几乎可以在支持超文本传输协议HTTP)的任何编程语言中搜索或编写其包装器。

请记住,每个工具选项都有其自身的限制。你的经纪人可能会对价格和事件更新进行速率限制。产品的开发方式、要遵循的性能指标、涉及的成本、延迟阈值、风险度量以及预期的用户界面都是需要考虑的因素。风险管理、执行引擎和投资组合优化器是会影响系统设计的一些主要组件。你现有的交易基础设施、操作系统的选择、编程语言编译器的能力以及可用的软件工具对系统设计、开发和部署提出了进一步的限制。

系统功能

定义交易系统的结果非常重要。结果可能是一个基于研究的系统,涉及从数据供应商获取高质量数据,执行计算或运行模型,并通过信号生成评估策略。研究组件的一部分可能包括数据清理模块或回测界面,以在历史数据上使用理论参数运行策略。在设计我们的系统时,CPU 速度、内存大小和带宽是需要考虑的因素。

另一个结果可能是一个更关注风险管理和订单处理功能以确保多个订单及时执行的执行型系统。系统必须非常健壮,以处理订单执行过程中的任何故障点。因此,在设计执行订单的系统时需要考虑网络配置、硬件、内存管理和速度以及用户体验等因素。

一个系统可能包含一个或多个这些功能。设计更大的系统不可避免地会给框架增加复杂性。建议选择一个或多个编程语言,可以解决和平衡交易系统的开发速度、开发便捷性、可扩展性和可靠性。

构建算法交易平台

在这一部分,我们将使用 Python 设计和构建一个实时算法交易系统。由于每个经纪人的开发工具和服务都不同,因此需要考虑与我们自己的交易系统集成所需的不同编程实现。通过良好的系统设计,我们可以构建一个通用服务,允许配置不同经纪人的插件,并与我们的交易系统良好地协同工作。

设计经纪人接口

在设计交易平台时,以下三个功能对于实现任何给定的交易计划都是非常理想的:

  • 获取价格:定价数据是交易所提供的最基本信息之一。它代表了市场为购买或出售交易产品所做的报价价格。经纪人可能会重新分发交易所的数据,以自己的格式传递给你。可用的价格数据的最基本形式是报价的日期和时间、交易产品的符号以及交易产品的报价买入和卖出价格。通常情况下,这些定价数据对基于交易的决策非常有用。

最佳的报价和询价价格被称为Level 1报价。在大多数情况下,可以向经纪人请求 Level 2、3 甚至更多的报价级别。

  • 向市场发送订单:当向市场发送订单时,它可能会被您的经纪人或交易所执行,也可能不会。如果订单得到执行,您将在交易产品中开立一个持仓,并使自己承担各种风险以及回报。最简单的订单形式指定了要交易的产品(通常用符号表示)、要交易的数量、您想要采取的持仓(即您是买入还是卖出),以及对于非市价订单,要交易的价格。根据您的需求,有许多不同类型的订单可用于帮助管理您的交易风险。

您的经纪人可能不支持所有订单类型。最好与您的经纪人核实可用的订单类型以及哪种订单类型可以最好地管理您的交易风险。市场参与者最常用的订单类型是市价订单、限价订单和长期有效订单。市价订单是立即在市场上买入或卖出产品的订单。由于它是根据当前市场价格执行的,因此不需要为此类型的订单指定执行价格。限价订单是以特定或更好的价格买入或卖出产品的订单。长期有效GTC)订单是一种保持在交易所队列中等待执行的订单,直到规定的到期时间。除非另有规定,大多数订单都是在交易日结束时到期的长期有效订单。您可以在www.investopedia.com/university/how-start-trading/how-start-trading-order-types.asp找到更多有关各种订单类型的信息。

  • 跟踪持仓:一旦您的订单执行,您将进入一个持仓。跟踪您的持仓将有助于确定您的交易策略表现如何(好坏皆有可能!),并管理和规划您的风险。您的持仓盈亏根据市场波动而变化,并被称为未实现盈利和亏损。关闭持仓后,您将获得实现盈利和亏损,这是您交易策略的最终结果。

有了这三个基本功能,我们可以设计一个通用的Broker类,实现这些功能,并可以轻松扩展到任何特定经纪人的配置。

Python 库要求

在本章中,我们将使用公开可用的 v20 模块,Oanda 作为我们的经纪人。本章中提到的所有方法实现都使用v20 Python 库作为示例。

安装 v20

OANDA v20 REST API 的官方存储库位于github.com/oanda/v20-python。使用终端命令使用 pip 进行安装。

pip install v20 

有关 OANDA v20 REST API 的详细文档可以在developer.oanda.com/rest-live-v20/introduction/找到。API 的使用因经纪人而异,因此在编写交易系统实现之前,请务必与您的经纪人咨询适当的文档。

编写基于事件驱动的经纪人类

无论是获取价格、发送订单还是跟踪持仓,基于事件驱动的系统设计将以多线程方式触发我们系统的关键部分,而不会阻塞主线程。

让我们开始编写我们的 PythonBroker类,如下所示:

from abc import abstractmethod

class Broker(object):
    def __init__(self, host, port):
        self.host = host
        self.port = port

        self.__price_event_handler = None
        self.__order_event_handler = None
        self.__position_event_handler = None

在构造函数中,我们可以为继承子类提供我们的经纪人的hostport公共连接配置。分别声明了三个变量,用于存储价格、订单和持仓更新的事件处理程序。在这里,我们设计了每个事件只有一个监听器。更复杂的交易系统可能支持同一事件处理程序上的多个监听器。

存储价格事件处理程序

Broker类内部,分别添加以下两个方法作为价格事件处理程序的 getter 和 setter:

@property
def on_price_event(self):
    """
    Listeners will receive: symbol, bid, ask
    """
    return self.__price_event_handler

@on_price_event.setter
def on_price_event(self, event_handler):
    self.__price_event_handler = event_handler

继承的子类将通过on_price_event方法调用通知监听器有关符号、买价和卖价的信息。稍后,我们将使用这些基本信息做出我们的交易决策。

存储订单事件处理程序

分别添加以下两个方法作为订单事件处理程序的获取器和设置器:

@property
def on_order_event(self):
    """
    Listeners will receive: transaction_id
    """
    return self.__order_event_handler

@on_order_event.setter
def on_order_event(self, event_handler):
    self.__order_event_handler = event_handler

在订单被路由到您的经纪人之后,继承的子类将通过on_order_event方法调用通知监听器,同时附带订单交易 ID。

存储持仓事件处理程序

添加以下两个方法作为持仓事件处理程序的获取器和设置器:

@property
def on_position_event(self):
    """
    Listeners will receive:
    symbol, is_long, units, unrealized_pnl, pnl
    """
    return self.__position_event_handler

@on_position_event.setter
def on_position_event(self, event_handler):
    self.__position_event_handler = event_handler

当从您的经纪人接收到持仓更新事件时,继承的子类将通过on_position_event方法通知监听器,其中包含符号信息、表示多头或空头持仓的标志、交易单位数、未实现的盈亏和已实现的盈亏。

声明一个用于获取价格的抽象方法

由于从数据源获取价格是任何交易系统的主要要求,创建一个名为get_prices()的抽象方法来执行这样的功能。它期望一个symbols参数,其中包含一个经纪人定义的符号列表,将用于从我们的经纪人查询数据。继承的子类应该实现这个方法,否则会抛出NotImplementedError异常:

@abstractmethod
def get_prices(self, symbols=[]):
    """
    Query market prices from a broker
    :param symbols: list of symbols recognized by your broker
    """
    raise NotImplementedError('Method is required!')

请注意,get_prices()方法预计执行一次获取当前市场价格的操作。这给我们提供了特定时间点的市场快照。对于一个持续运行的交易系统,我们将需要实时流式传输市场价格来满足我们的交易逻辑,接下来我们将定义这一点。

声明一个用于流式传输价格的抽象方法

添加一个stream_prices()抽象方法,使用以下代码接受一个符号列表来流式传输价格:

@abstractmethod
def stream_prices(self, symbols=[]):
    """"
    Continuously stream prices from a broker.
    :param symbols: list of symbols recognized by your broker
    """
    raise NotImplementedError('Method is required!')

继承的子类应该在从您的经纪人流式传输价格时实现这个方法,否则会抛出NotImplementedError异常消息。

声明一个用于发送订单的抽象方法

为继承的子类添加一个send_market_order()抽象方法,用于在向您的经纪人发送市价订单时实现:

@abstractmethod
def send_market_order(self, symbol, quantity, is_buy):
    raise NotImplementedError('Method is required!')

使用我们的Broker基类中编写的前述方法,我们现在可以在下一节中编写特定于经纪人的类。

实现经纪人类

在本节中,我们将实现特定于我们的经纪人 Oanda 的Broker类的抽象方法。这需要使用v20库。但是,您可以轻松地更改配置和任何特定于您选择的经纪人的实现方法。

初始化经纪人类

编写以下OandaBroker类,它是特定于我们经纪人的类,扩展了通用的Broker类:

import v20

class OandaBroker(Broker):
    PRACTICE_API_HOST = 'api-fxpractice.oanda.com'
    PRACTICE_STREAM_HOST = 'stream-fxpractice.oanda.com'

    LIVE_API_HOST = 'api-fxtrade.oanda.com'
    LIVE_STREAM_HOST = 'stream-fxtrade.oanda.com'

    PORT = '443'

    def __init__(self, accountid, token, is_live=False):
        if is_live:
            host = self.LIVE_API_HOST
            stream_host = self.LIVE_STREAM_HOST
        else:
            host = self.PRACTICE_API_HOST
            stream_host = self.PRACTICE_STREAM_HOST

        super(OandaBroker, self).__init__(host, self.PORT)

        self.accountid = accountid
        self.token = token

        self.api = v20.Context(host, self.port, token=token)
        self.stream_api = v20.Context(stream_host, self.port, token=token)

请注意,Oanda 使用两个不同的主机用于常规 API 端点和流式 API 端点。这些端点对于他们的模拟和实盘交易环境是不同的。所有端点都连接在标准的安全套接字层SSL)端口 440 上。在构造函数中,is_live布尔标志选择适合所选交易环境的适当端点,以保存在父类中。is_liveTrue值表示实盘交易环境。构造函数参数还保存了账户 ID 和令牌,这些信息是用于验证用于交易的账户的。这些信息可以从您的经纪人那里获取。

apistream_api变量保存了v20库的Context对象,通过调用方法向您的经纪人发送指令时使用。

实现获取价格的方法

以下代码实现了OandaBroker类中的父get_prices()方法,用于从您的经纪人获取价格:

def get_prices(self, symbols=[]):
    response = self.api.pricing.get(
        self.accountid,
        instruments=",".join(symbols),
        snapshot=True,
        includeUnitsAvailable=False
    )
    body = response.body
    prices = body.get('prices', [])
    for price in prices:
        self.process_price(price)

响应主体包含一个 prices 属性和一个对象列表。列表中的每个项目都由 process_price() 方法处理。让我们也在 OandaBroker 类中实现这个方法:

def process_price(self, price):
    symbol = price.instrument

    if not symbol:
        print('Price symbol is empty!')
        return

    bids = price.bids or []
    price_bucket_bid = bids[0] if bids and len(bids) > 0 else None
    bid = price_bucket_bid.price if price_bucket_bid else 0

    asks = price.asks or []
    price_bucket_ask = asks[0] if asks and len(asks) > 0 else None
    ask = price_bucket_ask.price if price_bucket_ask else 0

    self.on_price_event(symbol, bid, ask)

price 对象包含一个字符串对象的 instrument 属性,以及 bidsasks 属性中的 list 对象。通常,Level 1 报价是可用的,所以我们读取每个列表的第一项。列表中的每个项目都是一个 price_bucket 对象,我们从中提取买价和卖价。

有了这些提取的信息,我们将其传递给 on_price_event() 事件处理程序方法。请注意,在这个例子中,我们只传递了三个值。在更复杂的交易系统中,您可能希望考虑提取更详细的信息,比如成交量、最后成交价格或多级报价,并将其传递给价格事件监听器。

实现流动价格的方法

OandaBroker 类中添加以下 stream_prices() 方法,以从经纪人那里开始流动价格:

def stream_prices(self, symbols=[]):
    response = self.stream_api.pricing.stream(
        self.accountid,
        instruments=",".join(symbols),
        snapshot=True
    )

    for msg_type, msg in response.parts():
        if msg_type == "pricing.Heartbeat":
            continue
        elif msg_type == "pricing.ClientPrice":
            self.process_price(msg)

由于主机连接期望连续流,response 对象有一个 parts() 方法来监听传入的数据。msg 对象本质上是一个 price 对象,我们可以重复使用它来通知监听器有一个传入的价格事件。

实现发送市价订单的方法

OandaBroker 类中添加以下 send_market_order() 方法,它将向您的经纪人发送一个市价订单:

def send_market_order(self, symbol, quantity, is_buy):
    response = self.api.order.market(
        self.accountid,
        units=abs(quantity) * (1 if is_buy else -1),
        instrument=symbol,
        type='MARKET',
    )
    if response.status != 201:
        self.on_order_event(symbol, quantity, is_buy, None, 'NOT_FILLED')
        return

    body = response.body
    if 'orderCancelTransaction' in body:
        self.on_order_event(symbol, quantity, is_buy, None, 'NOT_FILLED')
        return transaction_id = body.get('lastTransactionID', None) 
    self.on_order_event(symbol, quantity, is_buy, transaction_id, 'FILLED')

当调用 v20 order 库的 market() 方法时,预期响应的状态为 201,表示成功连接到经纪人。建议进一步检查响应主体,以查看我们订单执行中的错误迹象。在成功执行的情况下,交易 ID 和订单的详细信息通过调用 on_order_event() 事件处理程序传递给监听器。否则,订单事件将以空的交易 ID 触发,并带有 NOT_FILLED 状态,表示订单不完整。

实现获取头寸的方法

OandaBroker 类中添加以下 get_positions() 方法,它将为给定账户获取所有可用的头寸信息:

def get_positions(self):
    response = self.api.position.list(self.accountid)
    body = response.body
    positions = body.get('positions', [])
    for position in positions:
        symbol = position.instrument
        unrealized_pnl = position.unrealizedPL
        pnl = position.pl
        long = position.long
        short = position.short

        if short.units:
            self.on_position_event(
                symbol, False, short.units, unrealized_pnl, pnl)
        elif long.units:
            self.on_position_event(
                symbol, True, long.units, unrealized_pnl, pnl)
        else:
            self.on_position_event(
                symbol, None, 0, unrealized_pnl, pnl)

在响应主体中,position 属性包含一个 position 对象列表,每个对象都有合同符号、未实现和已实现的盈亏、多头和空头头寸的数量属性。这些信息通过 on_position_event() 事件处理程序传递给监听器。

获取价格

现在定义了来自我们经纪人的价格事件监听器的方法,我们可以通过阅读当前市场价格来测试与我们经纪人之间建立的连接。可以使用以下 Python 代码实例化 Broker 类:

# Replace these 2 values with your own!
ACCOUNT_ID = '101-001-1374173-001'
API_TOKEN = '6ecf6b053262c590b78bb8199b85aa2f-d99c54aecb2d5b4583a9f707636e8009'

broker = OandaBroker(ACCOUNT_ID, API_TOKEN)

用您的经纪人提供的自己的凭据替换两个常量变量 ACCOUNT_IDAPI_TOKEN,这些凭据标识了您自己的交易账户。broker 变量是 OandaBroker 的一个实例,我们可以使用它来执行各种特定于经纪人的调用。

假设我们有兴趣了解 EUR/USD 货币对的当前市场价格。让我们定义一个常量变量来保存这个工具的符号,这个符号被我们的经纪人所认可:

SYMBOL = 'EUR_USD'

接下来,使用以下代码定义来自我们经纪人的价格事件监听器:

import datetime as dt

def on_price_event(symbol, bid, ask):
   print(
        dt.datetime.now(), '[PRICE]',
        symbol, 'bid:', bid, 'ask:', ask
    )

broker.on_price_event = on_price_event

on_price_event() 函数被定义为监听器,用于接收价格信息,并分配给 broker.on_price_event 事件处理程序。我们期望从定价事件中获得三个值 - 合同符号、买价和卖价 - 我们只是简单地将它们打印到控制台。

调用 get_prices() 方法来从我们的经纪人那里获取当前市场价格:

broker.get_prices(symbols=[SYMBOL])

我们应该在控制台上得到类似的输出:

2018-11-19 21:29:13.214893 [PRICE] EUR_USD bid: 1.14361 ask: 1.14374

输出是一行,显示 EUR/USD 货币对的买价和卖价分别为 1.143611.14374

发送一个简单的市价订单

与获取价格时一样,我们可以重用broker变量向我们的经纪人发送市价订单。

现在假设我们有兴趣购买一单位相同的 EUR/USD 货币对;以下代码执行此操作:

def on_order_event(symbol, quantity, is_buy, transaction_id, status):
    print(
        dt.datetime.now(), '[ORDER]',
        'transaction_id:', transaction_id,
        'status:', status,
        'symbol:', symbol,
        'quantity:', quantity,
        'is_buy:', is_buy,
    )

broker.on_order_event = on_order_event
broker.send_market_order(SYMBOL, 1, True)

on_order_event()函数被定义为监听来自我们经纪人的订单更新的函数,并分配给broker.on_order_event事件处理程序。例如,执行的限价订单或取消的订单将通过此方法调用。最后,send_market_order()方法表示我们有兴趣购买一单位 EUR/USD 货币对。

如果在运行上述代码时货币市场开放,您应该会得到以下结果,交易 ID 不同:

2018-11-19 21:29:13.484685 [ORDER] transaction_id: 754 status: FILLED symbol: EUR_USD quantity: 1 is_buy: True

输出显示订单成功填写,购买一单位 EUR/USD 货币对,交易 ID 为754

获取持仓更新

通过发送市价订单进行开仓,我们应该能够查看当前的 EUR/USD 头寸。我们可以在broker对象上使用以下代码来实现:

def on_position_event(symbol, is_long, units, upnl, pnl):
    print(
        dt.datetime.now(), '[POSITION]',
        'symbol:', symbol,
        'is_long:', is_long,
        'units:', units,
        'upnl:', upnl,
        'pnl:', pnl
    )

broker.on_position_event = on_position_event
broker.get_positions()

on_position_event()函数被定义为监听来自我们经纪人的持仓更新的函数,并分配给broker.on_position_event事件处理程序。当调用get_positions()方法时,经纪人返回持仓信息并触发以下输出:

2018-11-19 21:29:13.752886 [POSITION] symbol: EUR_USD is_long: True units: 1.0 upnl: -0.0001 pnl: 0.0

我们的头寸报告目前是 EUR/USD 货币对的一个多头单位,未实现损失为$0.0001。由于这是我们的第一笔交易,我们还没有实现任何利润或损失。

构建均值回归算法交易系统

现在我们的经纪人接受订单并响应我们的请求,我们可以开始设计一个完全自动化的交易系统。在本节中,我们将探讨如何设计和实施一个均值回归算法交易系统。

设计均值回归算法

假设我们相信在正常的市场条件下,价格会波动,但往往会回归到某个短期水平,例如最近价格的平均值。在这个例子中,我们假设 EUR/USD 货币对在近期短期内表现出均值回归特性。首先,我们将原始的 tick 级数据重新采样为标准时间序列间隔,例如一分钟间隔。然后,取最近几个周期来计算短期平均价格(例如,使用五个周期),我们认为 EUR/USD 价格将向前五分钟的价格平均值回归。

一旦 EUR/USD 货币对的出价价格超过短期平均价格,以五分钟为例,我们的交易系统将生成一个卖出信号,我们可以选择通过卖出市价订单进入空头头寸。同样,当 EUR/USD 的询价价格低于平均价格时,将生成买入信号,我们可以选择通过买入市价订单进入多头头寸。

一旦开仓,我们可以使用相同的信号来平仓。当开多头头寸时,我们在卖出信号时通过输入市价卖出订单来平仓。同样,当开空头头寸时,我们在买入信号时通过输入市价买入订单来平仓。

您可能会观察到我们交易策略中存在许多缺陷。平仓并不保证盈利。我们对市场的看法可能是错误的;在不利的市场条件下,信号可能会在一个方向上持续一段时间,并且有很高的可能性以巨大的损失平仓!作为交易员,您应该找出适合自己信念和风险偏好的个人交易策略。

实施均值回归交易员类

我们交易系统需要的两个重要参数是重新取样间隔和计算周期数。首先,创建一个名为MeanReversionTrader的类,我们可以实例化并作为我们的交易系统运行:

import time
import datetime as dt
import pandas as pd

class MeanReversionTrader(object):
    def __init__(
        self, broker, symbol=None, units=1,
        resample_interval='60s', mean_periods=5
    ):
        """
        A trading platform that trades on one side
            based on a mean-reverting algorithm.

        :param broker: Broker object
        :param symbol: A str object recognized by the broker for trading
        :param units: Number of units to trade
        :param resample_interval: 
            Frequency for resampling price time series
        :param mean_periods: Number of resampled intervals
            for calculating the average price
        """
        self.broker = self.setup_broker(broker)

        self.resample_interval = resample_interval
        self.mean_periods = mean_periods
        self.symbol = symbol
        self.units = units

        self.df_prices = pd.DataFrame(columns=[symbol])
        self.pnl, self.upnl = 0, 0

        self.mean = 0
        self.bid_price, self.ask_price = 0, 0
        self.position = 0
        self.is_order_pending = False
        self.is_next_signal_cycle = True

构造函数中的五个参数初始化了我们交易系统的状态 - 使用的经纪人、要交易的标的、要交易的单位数、我们价格数据的重新取样间隔,以及我们均值计算的周期数。这些值只是存储为类变量。

setup_broker()方法调用设置我们的类来处理我们即将定义的broker对象的事件。当我们接收到价格数据时,这些数据存储在一个pandas DataFrame 变量df_prices中。最新的买入和卖出价格存储在bid_priceask_price变量中,用于计算信号。mean变量将存储先前mean_period价格的计算均值。position变量将存储我们当前持仓的单位数。负值表示空头持仓,正值表示多头持仓。

is_order_pending布尔标志指示是否有订单正在等待经纪人执行,is_next_signal_cycle布尔标志指示当前交易状态周期是否开放。请注意,我们的系统状态可以如下:

  1. 等待买入或卖出信号。

  2. 在买入或卖出信号上下订单。

  3. 当持仓被打开时,等待卖出或买入信号。

  4. 在卖出或买入信号上下订单。

  5. 当持仓被平仓时,转到步骤 1。

在步骤 1 到 5 的每个周期中,我们只交易一个单位。这些布尔标志作为锁,防止多个订单同时进入系统。

添加事件监听器

让我们在我们的MeanReversionTrader类中连接价格、订单和持仓事件。

setup_broker()方法添加到这个类中,如下所示:

def setup_broker(self, broker):
    broker.on_price_event = self.on_price_event
    broker.on_order_event = self.on_order_event
    broker.on_position_event = self.on_position_event
    return broker

我们只是将三个类方法分配为经纪人生成的任何事件的监听器,以监听价格、订单和持仓更新。

on_price_event()方法添加到这个类中,如下所示:

def on_price_event(self, symbol, bid, ask):
    print(dt.datetime.now(), '[PRICE]', symbol, 'bid:', bid, 'ask:', ask)

    self.bid_price = bid
    self.ask_price = ask
    self.df_prices.loc[pd.Timestamp.now(), symbol] = (bid + ask) / 2.

    self.get_positions()
    self.generate_signals_and_think()

    self.print_state()

当收到价格事件时,我们将它们存储在我们的bid_priceask_pricedf_prices类变量中。随着价格的变化,我们的持仓和信号值也会发生变化。get_position()方法调用将检索我们持仓的最新信息,generate_signals_and_think()调用将重新计算我们的信号并决定是否进行交易。使用print_state()命令将系统的当前状态打印到控制台。

编写get_position()方法来从我们的经纪人中检索持仓信息,如下所示:

def get_positions(self):
    try:
        self.broker.get_positions()
    except Exception as ex:
        print('get_positions error:', ex)

on_order_event()方法添加到我们的类中,如下所示:

def on_order_event(self, symbol, quantity, is_buy, transaction_id, status):
    print(
        dt.datetime.now(), '[ORDER]',
        'transaction_id:', transaction_id,
        'status:', status,
        'symbol:', symbol,
        'quantity:', quantity,
        'is_buy:', is_buy,
    )
    if status == 'FILLED':
        self.is_order_pending = False
        self.is_next_signal_cycle = False

        self.get_positions()  # Update positions before thinking
        self.generate_signals_and_think()

当接收到订单事件时,我们将它们打印到控制台上。在我们的经纪人的on_order_event实现中,成功执行的订单将传递status值为FILLEDUNFILLED。只有在成功的订单中,我们才能关闭我们的布尔锁,检索我们的最新持仓,并进行决策以平仓我们的持仓。

on_position_event()方法添加到我们的类中,如下所示:

def on_position_event(self, symbol, is_long, units, upnl, pnl):
    if symbol == self.symbol:
        self.position = abs(units) * (1 if is_long else -1)
        self.pnl = pnl
        self.upnl = upnl
        self.print_state()

当接收到我们预期交易标的的持仓更新事件时,我们存储我们的持仓信息、已实现收益和未实现收益。使用print_state()命令将系统的当前状态打印到控制台。

print_state()方法添加到我们的类中,如下所示:

def print_state(self):
    print(
        dt.datetime.now(), self.symbol, self.position_state, 
        abs(self.position), 'upnl:', self.upnl, 'pnl:', self.pnl
    )

一旦我们的订单、持仓或市场价格有任何更新,我们就会将系统的最新状态打印到控制台。

编写均值回归信号生成器

我们希望我们的决策算法在每次价格或订单更新时重新计算交易信号。让我们在MeanReversionTrader类中创建一个generate_signals_and_think()方法来做到这一点:

def generate_signals_and_think(self):
    df_resampled = self.df_prices\
        .resample(self.resample_interval)\
        .ffill()\
        .dropna()
    resampled_len = len(df_resampled.index)

    if resampled_len < self.mean_periods:
        print(
            'Insufficient data size to calculate logic. Need',
            self.mean_periods - resampled_len, 'more.'
        )
        return

    mean = df_resampled.tail(self.mean_periods).mean()[self.symbol]

    # Signal flag calculation
    is_signal_buy = mean > self.ask_price
    is_signal_sell = mean < self.bid_price

    print(
        'is_signal_buy:', is_signal_buy,
        'is_signal_sell:', is_signal_sell,
        'average_price: %.5f' % mean,
        'bid:', self.bid_price,
        'ask:', self.ask_price
    )

    self.think(is_signal_buy, is_signal_sell)

由于价格数据存储在df_prices变量中作为 pandas DataFrame,我们可以按照构造函数中给定的resample_interval变量的定义,定期对其进行重新采样。ffill()方法向前填充任何缺失的数据,dropna()命令在重新采样后移除第一个缺失值。必须有足够的数据可用于计算均值,否则此方法将简单退出。mean_periods变量表示必须可用的重新采样数据的最小长度。

tail(self.mean_periods)方法获取最近的重新采样间隔并使用mean()方法计算平均值,从而得到另一个 pandas DataFrame。平均水平通过引用 DataFrame 的列来获取,该列简单地是工具符号。

使用均值回归算法可用的平均价格,我们可以生成买入和卖出信号。在这里,当平均价格超过市场要价时,会生成买入信号,当平均价格超过市场竞价时,会生成卖出信号。我们的短期信念是市场价格将回归到平均价格。

在将这些计算出的值打印到控制台以便更好地调试后,我们现在可以利用买入和卖出信号来执行实际交易,这在同一类中的名为think()的方法中完成:

def think(self, is_signal_buy, is_signal_sell):
    if self.is_order_pending:
        return

    if self.position == 0:
        self.think_when_flat_position(is_signal_buy, is_signal_sell)
    elif self.position > 0:
        self.think_when_position_long(is_signal_sell)
    elif self.position < 0: 
        self.think_when_position_short(is_signal_buy)       

如果订单仍处于待处理状态,我们只需不做任何操作并退出该方法。由于市场条件可能随时发生变化,您可能希望添加自己的逻辑来处理待处理状态已经过长时间的订单,并尝试另一种策略。

这三个 if-else 语句分别处理了当我们的仓位是平的、多头的或空头的交易逻辑。当我们的仓位是平的时,将调用think_when_position_flat()方法,写成如下:

def think_when_position_flat(self, is_signal_buy, is_signal_sell):
    if is_signal_buy and self.is_next_signal_cycle:
        print('Opening position, BUY', 
              self.symbol, self.units, 'units')
        self.is_order_pending = True
        self.send_market_order(self.symbol, self.units, True)
        return

    if is_signal_sell and self.is_next_signal_cycle:
        print('Opening position, SELL', 
              self.symbol, self.units, 'units')
        self.is_order_pending = True
        self.send_market_order(self.symbol, self.units, False)
        return

    if not is_signal_buy and not is_signal_sell:
        self.is_next_signal_cycle = True

第一个if语句处理的是,在买入信号时,当前交易周期处于开放状态时,我们通过发送市价订单来买入并将该订单标记为待处理的条件。相反,第二个if语句处理的是在卖出信号时进入空头仓位的条件。否则,由于仓位是平的,既没有买入信号也没有卖出信号,我们只需将is_next_signal_cycle设置为True,直到有信号可用为止。

当我们处于多头仓位时,将调用think_when_position_long()方法,写成如下:

def think_when_position_long(self, is_signal_sell):
    if is_signal_sell:
        print('Closing position, SELL', 
              self.symbol, self.units, 'units')
        self.is_order_pending = True
        self.send_market_order(self.symbol, self.units, False)

在卖出信号时,我们将订单标记为待处理,并立即通过发送市价订单来卖出来平仓我们的多头仓位。

同样,当我们处于空头仓位时,将调用think_when_position_short()方法,写成如下:

def think_when_position_short(self, is_signal_buy):
    if is_signal_buy:
        print('Closing position, BUY', 
              self.symbol, self.units, 'units')
        self.is_order_pending = True
        self.send_market_order(self.symbol, self.units, True)

在买入信号时,我们将订单标记为待处理,并立即通过发送市价订单来买入来平仓我们的空头仓位。

为了执行订单路由功能,将以下send_market_order()类方法添加到我们的MeanReversionTrader类中:

def send_market_order(self, symbol, quantity, is_buy):
    self.broker.send_market_order(symbol, quantity, is_buy)

订单信息简单地转发给我们的Broker类进行执行。

运行我们的交易系统

最后,为了开始运行我们的交易系统,我们需要一个入口点。将以下run()类方法添加到MeanReversionTrader类中:

def run(self):
    self.get_positions()
    self.broker.stream_prices(symbols=[self.symbol])

在我们的交易系统的第一次运行期间,我们读取我们当前的仓位并使用该信息来初始化所有与仓位相关的信息。然后,我们请求我们的经纪人开始为给定的符号流式传输价格,并保持连接直到程序终止。

有了入场点的定义,我们只需要初始化我们的MeanReversionTrader类,并使用以下代码调用run()命令:

trader = MeanReversionTrader(
    broker, 
    symbol='EUR_USD', 
    units=1
    resample_interval='60s', 
    mean_periods=5,
)
trader.run()

请记住,broker变量包含了前面获取价格部分定义的OandaBroker类的实例,我们可以重复使用它。我们的交易系统将使用这个经纪人对象来执行与经纪人相关的调用。我们对 EUR/USD 货币对感兴趣,每次交易一单位。resample_interval变量的值为60s表示我们的存储价格将以一分钟的间隔重新采样。mean_periods变量的值为5表示我们将取最近五个间隔的平均值,或者过去五分钟的平均价格。

要启动我们的交易系统,请调用run();定价更新将开始涓涓流入,使我们的系统能够自行交易。您应该在控制台上看到类似以下的输出:

...
2018-11-21 15:19:34.487216 [PRICE] EUR_USD bid: 1.1393 ask: 1.13943
2018-11-21 15:19:35.686323 EUR_USD FLAT 0 upnl: 0.0 pnl: 0.0
Insufficient data size to calculate logic. Need 5 more.
2018-11-21 15:19:35.694619 EUR_USD FLAT 0 upnl: 0.0 pnl: 0.0
...

从输出中看,我们的头寸目前是平的,并且没有足够的定价数据来计算我们的交易信号。

五分钟后,当有足够的数据进行交易信号计算时,我们应该能够观察到以下结果:

...
2018-11-21 15:25:07.075883 EUR_USD FLAT 0 upnl: 0.0 pnl: -0.3246
is_signal_buy: False is_signal_sell: True average_price: 1.13934 bid: 1.13936 ask: 1.13949
Opening position, SELL EUR_USD 1 units
2018-11-21 15:25:07.356520 [ORDER] transaction_id: 2848 status: FILLED symbol: EUR_USD quantity: 1 is_buy: False
2018-11-21 15:25:07.688082 EUR_USD SHORT 1.0 upnl: -0.0001 pnl: 0.0
is_signal_buy: False is_signal_sell: True average_price: 1.13934 bid: 1.13936 ask: 1.13949
2018-11-21 15:25:07.692292 EUR_USD SHORT 1.0 upnl: -0.0001 pnl: 0.0

...

过去五分钟的平均价格为1.13934。由于 EUR/USD 的当前市场竞价价格为1.13936,高于平均价格,生成了一个卖出信号。生成一个卖出市价订单,开设 EUR/USD 的一个单位的空头头寸。这导致了 0.0001 美元的未实现损失。

让系统自行运行一段时间,它应该能够自行平仓。要停止交易,请使用Ctrl + Z或类似的方法终止运行的进程。请记住,一旦程序停止运行,手动平仓任何剩余的交易头寸。现在您拥有一个完全功能的自动交易系统了!

这里的系统设计和交易参数仅作为示例,并不一定会产生积极的结果!您应该尝试不同的交易参数,并改进事件处理,以找出您交易计划的最佳策略。

构建趋势跟踪交易平台

在前一节中,我们按照构建均值回归交易平台的步骤进行了操作。相同的功能可以很容易地扩展到包括任何其他交易策略。在本节中,我们将看看如何重用MeanReversionTrader类来实现一个趋势跟踪交易系统。

设计趋势跟踪算法

假设这一次,我们相信当前的市场条件呈现出趋势跟踪的模式,可能是由于季节性变化、经济预测或政府政策。随着价格的波动,短期平均价格水平穿过平均长期价格水平的某个阈值,我们生成买入或卖出信号。

首先,我们将原始的 tick 级数据重新采样为标准的时间序列间隔,例如,每分钟一次。其次,我们取最近的若干个周期,例如,五个周期,计算过去五分钟的短期平均价格。最后,取最近的较大数量的周期,例如,十个周期,计算过去十分钟的长期平均价格。

在没有市场波动的市场中,短期平均价格应该与长期平均价格相同,比率为一 - 这个比率也被称为贝塔。当短期平均价格增加超过长期平均价格时,贝塔大于一,市场可以被视为处于上升趋势。当短期价格下降超过长期平均价格时,贝塔小于一,市场可以被视为处于下降趋势。

在上升趋势中,一旦 beta 穿过某个价格阈值水平,我们的交易系统将生成买入信号,我们可以选择以买入市价单进入多头头寸。同样,在下降趋势中,当 beta 跌破某个价格阈值水平时,将生成卖出信号,我们可以选择以卖出市价单进入空头头寸。

一旦开仓,相同的信号可以用来平仓。当开多头头寸时,我们在卖出信号时平仓,通过以市价卖出单进入卖出订单。同样,当开空头头寸时,我们在买入信号时平仓,通过以市价买入单进入买入订单。

上述机制与均值回归交易系统设计非常相似。请记住,该算法不能保证任何利润,只是对市场的简单看法。您应该有一个与此不同(更好)的观点。

编写趋势跟踪交易员类

让我们为我们的趋势跟踪交易系统编写一个新的名为TrendFollowingTreader的类,它简单地扩展了MeanReversionTrader类,使用以下 Python 代码:

class TrendFollowingTrader(MeanReversionTrader):
    def __init__(
        self, *args, long_mean_periods=10,
        buy_threshold=1.0, sell_threshold=1.0, **kwargs
    ):
        super(TrendFollowingTrader, self).__init__(*args, **kwargs)

        self.long_mean_periods = long_mean_periods
        self.buy_threshold = buy_threshold
        self.sell_threshold = sell_threshold

在我们的构造函数中,我们定义了三个额外的关键字参数,long_mean_periodsbuy_thresholdsell_threshold,保存为类变量。long_mean_periods变量定义了我们的时间序列价格的重新采样间隔数量,用于计算长期平均价格。请注意,父构造函数中现有的mean_periods变量用于计算短期平均价格。buy_thresholdsell_threshold变量包含确定生成买入或卖出信号的 beta 边界值。

编写趋势跟踪信号生成器

因为只有决策逻辑需要从我们的父类MeanReversionTrader类中进行修改,而其他所有内容,包括订单、下单和流动价格,都保持不变,我们只需覆盖generate_signals_and_think()方法,并使用以下代码实现我们的新趋势跟踪信号生成器:

def generate_signals_and_think(self):
    df_resampled = self.df_prices\
        .resample(self.resample_interval)\
        .ffill().dropna()
    resampled_len = len(df_resampled.index)

    if resampled_len < self.long_mean_periods:
        print(
            'Insufficient data size to calculate logic. Need',
            self.mean_periods - resampled_len, 'more.'
        )
        return

    mean_short = df_resampled\
        .tail(self.mean_periods).mean()[self.symbol]
    mean_long = df_resampled\
        .tail(self.long_mean_periods).mean()[self.symbol]
    beta = mean_short / mean_long

    # Signal flag calculation
    is_signal_buy = beta > self.buy_threshold
    is_signal_sell = beta < self.sell_threshold

    print(
        'is_signal_buy:', is_signal_buy,
        'is_signal_sell:', is_signal_sell,
        'beta:', beta,
        'bid:', self.bid_price,
        'ask:', self.ask_price
    )

    self.think(is_signal_buy, is_signal_sell)

与以前一样,在每次调用generate_signals_and_think()方法时,我们以resample_interval定义的固定间隔重新采样价格。现在,用于计算信号的最小间隔由long_mean_periods而不是mean_periods定义。mean_short变量指的是短期平均重新采样价格,mean_long变量指的是长期平均重新采样价格。

beta变量是短期平均价格与长期平均价格的比率。当 beta 上升到buy_threshold值以上时,将生成买入信号,并且is_signal_buy变量为True。同样,当 beta 跌破sell_threshold值时,将生成卖出信号,并且is_signal_sell变量为True

交易参数被打印到控制台以进行调试,并且对父类think()类方法的调用会触发使用市价订单进行买入和卖出的通常逻辑。

运行趋势跟踪交易系统

通过实例化TrendFollowingTrader类并使用以下代码运行我们的趋势跟踪交易系统:

trader = TrendFollowingTrader(
    broker,
    resample_interval='60s',
    symbol='EUR_USD',
    units=1,
    mean_periods=5,
    long_mean_periods=10,
    buy_threshold=1.000010,
    sell_threshold=0.99990,
)
trader.run()

第一个参数broker与上一节中为我们的经纪人创建的对象相同。同样,我们以一分钟间隔重新取样我们的时间序列价格,并且我们对交易 EUR/USD 货币对感兴趣,在任何给定时间最多进入一单位的头寸。使用mean_periods值为5,我们对最近的五个重新取样间隔感兴趣,以计算过去五分钟的平均价格作为我们的短期平均价格。使用long_mean_period值为10,我们对最近的 10 个重新取样间隔感兴趣,以计算过去 10 分钟的平均价格作为我们的长期平均价格。

短期平均价格与长期平均价格的比率被视为贝塔。当贝塔上升到超过buy_threshold定义的值时,将生成买入信号。当贝塔下降到低于sell_threshold定义的值时,将生成卖出信号。

设置好交易参数后,调用run()方法启动交易系统。我们应该在控制台上看到类似以下的输出:

...
2018-11-23 08:51:12.438684 [PRICE] EUR_USD bid: 1.14018 ask: 1.14033
2018-11-23 08:51:13.520880 EUR_USD FLAT 0 upnl: 0.0 pnl: 0.0
Insufficient data size to calculate logic. Need 10 more.
2018-11-23 08:51:13.529919 EUR_USD FLAT 0 upnl: 0.0 pnl: 0.0
... 

在交易开始时,我们获得了当前市场价格,保持平仓状态,既没有盈利也没有损失。没有足够的数据可用于做出任何交易决策,我们将不得不等待 10 分钟,然后才能看到计算参数生效。

如果您的交易系统依赖于更长时间的过去数据,并且不希望等待所有这些数据被收集,考虑使用历史数据对您的交易系统进行引导。

过一段时间后,您应该会看到类似以下的输出:

...
is_signal_buy: True is_signal_sell: False beta: 1.0000333228980047 bid: 1.14041 ask: 1.14058
Opening position, BUY EUR_USD 1 units
2018-11-23 09:01:01.579208 [ORDER] transaction_id: 2905 status: FILLED symbol: EUR_USD quantity: 1 is_buy: True
2018-11-23 09:01:01.844743 EUR_USD LONG 1.0 upnl: -0.0002 pnl: 0.0
...

让系统自行运行一段时间,它应该能够自行平仓。要停止交易,请使用Ctrl + Z或类似的方法终止运行进程。记得在程序停止运行后手动平仓任何剩余的交易头寸。采取措施改变您的交易参数和决策逻辑,使您的交易系统成为盈利性的!

请注意,作者对您的交易系统的任何结果概不负责!在实时交易环境中,需要更多的控制参数、订单管理和头寸跟踪来有效管理风险。

在接下来的部分,我们将讨论一个可以应用于我们交易计划的风险管理策略。

VaR 用于风险管理

一旦我们在市场上开仓,就会面临各种风险,如波动风险和信用风险。为了尽可能保护我们的交易资本,将风险管理措施纳入我们的交易系统是非常重要的。

也许金融行业中最常用的风险度量是 VaR 技术。它旨在简单回答以下问题:在特定概率水平(例如 95%)和一定时间段内,预期的最坏损失金额是多少? VaR 的美妙之处在于它可以应用于多个层次,从特定头寸的微观层面到基于组合的宏观层面。例如,对于 1 天的时间范围,95%的置信水平下的 100 万美元 VaR 表明,平均而言,你只有 20 天中的 1 天可能会因市场波动而损失超过 100 万美元。

以下图表说明了一个均值为 0%的正态分布组合收益率,VaR 是分布中第 95 百分位数对应的损失:

假设我们在一家声称具有与标普 500 指数基金相同风险的基金中管理了 1 亿美元,预期收益率为 9%,标准偏差为 20%。使用方差-协方差方法计算 5%风险水平或 95%置信水平下的每日 VaR,我们将使用以下公式:

在这里,P是投资组合的价值,N^(−1)(α,u,σ)是具有风险水平α、平均值u和标准差σ的逆正态概率分布。每年的交易日数假定为 252 天。结果表明,5%水平的每日 VaR 为$2,036,606.50。

然而,VaR 的使用并非没有缺陷。它没有考虑正态分布曲线尾端发生极端事件的损失概率。超过一定 VaR 水平的损失规模也很难估计。我们调查的 VaR 使用历史数据和假定的恒定波动率水平 - 这些指标并不代表我们未来的表现。

让我们采取一种实际的方法来计算股票价格的每日 VaR;我们将通过从数据源下载 AAPL 股票价格来调查:

"""
Download the all-time AAPL dataset
"""
from alpha_vantage.timeseries import TimeSeries

# Update your Alpha Vantage API key here...
ALPHA_VANTAGE_API_KEY = 'PZ2ISG9CYY379KLI'

ts = TimeSeries(key=ALPHA_VANTAGE_API_KEY, output_format='pandas')
df, meta_data = ts.get_daily_adjusted(symbol='AAPL', outputsize='full')

数据集将作为 pandas DataFrame 下载到df变量中:

df.info()

这给我们以下输出:

<class 'pandas.core.frame.DataFrame'>
Index: 5259 entries, 1998-01-02 to 2018-11-23
Data columns (total 8 columns):
1\. open                 5259 non-null float64
2\. high                 5259 non-null float64
3\. low                  5259 non-null float64
4\. close                5259 non-null float64
5\. adjusted close       5259 non-null float64
6\. volume               5259 non-null float64
7\. dividend amount      5259 non-null float64
8\. split coefficient    5259 non-null float64
dtypes: float64(8)
memory usage: 349.2+ KB

我们的 DataFrame 包含八列,价格从 1998 年开始到现在的交易日。感兴趣的列是调整后的收盘价。假设我们有兴趣计算 2017 年的每日 VaR;让我们使用以下代码获取这个数据集:

import datetime as dt
import pandas as pd

# Define the date range
start = dt.datetime(2017, 1, 1)
end = dt.datetime(2017, 12, 31)

# Cast indexes as DateTimeIndex objects
df.index = pd.to_datetime(df.index)
closing_prices = df['5\. adjusted close']
prices = closing_prices.loc[start:end]

prices变量包含了我们 2017 年的 AAPL 数据集。

使用前面讨论的公式,您可以使用以下代码实现calculate_daily_var()函数:

from scipy.stats import norm

def calculate_daily_var(
    portfolio, prob, mean, 
    stdev, days_per_year=252.
):
    alpha = 1-prob
    u = mean/days_per_year
    sigma = stdev/np.sqrt(days_per_year)
    norminv = norm.ppf(alpha, u, sigma)
    return portfolio - portfolio*(norminv+1)

假设我们持有$100 百万的 AAPL 股票,并且有兴趣找到 95%置信水平下的每日 VaR。我们可以使用以下代码定义 VaR 参数:

import numpy as np

portfolio = 100000000.00
confidence = 0.95

daily_returns = prices.pct_change().dropna()
mu = np.mean(daily_returns)
sigma = np.std(daily_returns)

musigma变量分别代表每日平均百分比收益和每日收益的标准差。

我们可以通过调用calculate_daily_var()函数获得 VaR,如下所示:

VaR = calculate_daily_var(
    portfolio, confidence, mu, sigma, days_per_year=252.)
print('Value-at-Risk: %.2f' % VaR)

我们将得到以下输出:

Value-at-Risk: 114248.72

假设每年有 252 个交易日,2017 年 AAPL 股票的每日 VaR 在 95%的置信水平下为$114,248.72。

摘要

在本章中,我们介绍了交易从交易场到电子交易平台的演变,并了解了算法交易的产生过程。我们看了一些经纪人提供 API 访问其交易服务。为了帮助我们开始开发算法交易系统,我们使用 Oanda v20库来实现一个均值回归交易系统。

在设计一个事件驱动的经纪人接口类时,我们为监听订单、价格和持仓更新定义了事件处理程序。继承Broker类的子类只需用经纪人特定的函数扩展这个接口类,同时保持底层交易函数与我们的交易系统兼容。我们通过获取市场价格、发送市价订单和接收持仓更新成功测试了与我们经纪人的连接。

我们讨论了一个简单的均值回归交易系统的设计,该系统根据历史平均价格的波动以及开仓和平仓市价订单来生成买入或卖出信号。由于这个交易系统只使用了一个交易逻辑来源,因此需要更多的工作来构建一个健壮、可靠和盈利的交易系统。

我们还讨论了一个趋势跟随交易系统的设计,该系统根据短期平均价格与长期平均价格的波动来生成买入或卖出信号。通过一个设计良好的系统,我们看到了通过简单地扩展均值回归父类并覆盖决策方法来修改现有交易逻辑是多么容易。

交易的一个关键方面是有效地管理风险。在金融行业,VaR 是用来衡量风险的最常见的技术。使用 Python,我们采取了一种实际的方法来计算 AAPL 过去数据集的每日 VaR。

一旦我们建立了一个有效的算法交易系统,我们可以探索其他衡量交易策略表现的方式。其中之一是回测;我们将在下一章讨论这个话题。

第九章:实施回测系统

回测是对模型驱动的投资策略对历史数据的响应进行模拟。在设计和开发回测时,以创建视频游戏的概念思考会很有帮助。

在这一章中,我们将使用面向对象的方法设计和实现一个事件驱动的回测系统。我们交易模型的结果利润和损失可以绘制成图表,以帮助可视化我们交易策略的表现。然而,这足以确定它是否是一个好模型吗?

在回测中有许多问题需要解决,例如交易成本的影响、订单执行的延迟、获取详细交易信息的途径以及历史数据的质量。尽管存在这些因素,创建回测系统的主要目标是尽可能准确地测试模型。

回测涉及大量值得研究的内容,这些内容值得有专门的文献。我们将简要讨论一些在实施回测时可能要考虑的想法。通常,回测中会使用多种算法。我们将简要讨论其中一些:k 均值聚类、k 最近邻、分类和回归树、2k 因子设计和遗传算法。

在这一章中,我们将涵盖以下主题:

  • 介绍回测

  • 回测中的关注点

  • 事件驱动回测系统的概念

  • 设计和实施回测系统

  • 编写类来存储 tick 数据和市场数据

  • 编写订单和持仓类

  • 编写一个均值回归策略

  • 运行回测引擎单次和多次

  • 回测模型的十个考虑因素

  • 回测中的算法讨论

介绍回测

回测是对模型驱动的投资策略对历史数据的响应进行模拟。进行回测实验的目的是发现有关过程或系统的发现。通过使用历史数据,您可以节省测试投资策略的时间。它帮助您测试基于被测试期间的运动的投资理论。它也用于评估和校准投资模型。创建模型只是第一步。投资策略通常会使用该模型来帮助您进行模拟交易决策并计算与风险或回报相关的各种因素。这些因素通常一起使用,以找到一个能够预测回报的组合。

回测中的关注点

然而,在回测中有许多问题需要解决:

  • 回测永远无法完全复制投资策略在实际交易环境中的表现。

  • 历史数据的质量是有问题的,因为它受第三方数据供应商的异常值影响。

  • 前瞻性偏差有很多形式。例如,上市公司可能会分拆、合并或退市,导致其股价发生重大变化。

  • 对于基于订单簿信息的策略,市场微观结构极其难以真实模拟,因为它代表了连续时间内的集体可见供需。这种供需反过来受到世界各地新闻事件的影响。

  • 冰山和挂单是市场的一些隐藏元素,一旦激活就可能影响结构

  • 其他需要考虑的因素包括交易成本、订单执行的延迟以及从回测中获取详细交易信息的途径。

尽管存在这些因素,创建回测系统的主要目标是尽可能准确地测试模型。

前瞻性偏差是在分析期间使用可用的未来数据,导致模拟或研究结果不准确。在金融领域,冰山订单是将大订单分成几个小订单。订单的一小部分对公众可见,就像冰山的一角一样,而实际订单的大部分是隐藏的。挂单是一个价格远离市场并等待执行的订单。

事件驱动回测系统的概念

在设计和开发回测时,以创建视频游戏的概念来思考会很有帮助。毕竟,我们正在尝试创建一个模拟的市场定价和订单环境,非常类似于创建一个虚拟的游戏世界。交易也可以被视为一个买低卖高的刺激游戏。

在虚拟交易环境中,需要组件来模拟价格数据源、订单匹配引擎、订单簿管理,以及账户和持仓更新功能。为了实现这些功能,我们可以探索事件驱动回测系统的概念。

让我们首先了解贯穿游戏开发过程的事件驱动编程范式的概念。系统通常将事件作为其输入接收。它可能是用户输入的按键或鼠标移动。其他事件可能是由另一个系统、进程或传感器生成的消息,用于通知主机系统有一个传入事件。

以下图表说明了游戏引擎系统涉及的阶段:

让我们看一下主游戏引擎循环的伪代码实现:

while is_main_loop:  # Main game engine loop
     handle_input_events()
     update_AI()
     update_physics()
     update_game_objects()
     render_screen()
     sleep(1/60)  # Assuming a 60 frames-per-second video game rate

主游戏引擎循环中的核心功能可能会处理生成的系统事件,就像handle_input_events()函数处理键盘事件一样:

def handle_input_events()
    event = get_latest_event()
    if event.type == 'UP_KEY_PRESS':
        move_player_up()
    elif event.type == 'DOWN_KEY_PRESS':
        move_player_down()

使用事件驱动系统,例如前面的例子,可以通过能够交换和使用来自不同系统组件的类似事件来实现代码模块化和可重用性。面向对象编程的使用进一步得到加强,其中类定义了游戏中的对象。这些特性在设计交易平台时特别有用,可以与不同的市场数据源、多个交易算法和运行时环境进行接口。模拟交易环境接近真实环境,有助于防止前瞻性偏差。

设计和实施回测系统

现在我们已经有了一个设计视频游戏来创建回测交易系统的想法,我们可以通过首先定义交易系统中各个组件所需的类来开始我们的面向对象方法。

我们有兴趣实施一个简单的回测系统来测试一个均值回归策略。使用数据源提供商的每日历史价格,我们将取每天的收盘价来计算特定工具价格回报的波动率,以 AAPL 股价为例。我们想要测试一个理论,即如果过去一定数量的日子的回报标准差远离零的均值达到特定阈值,就会生成买入或卖出信号。当确实生成这样的信号时,市场订单将被发送到交易所,以在下一个交易日的开盘价执行。

一旦我们开仓,我们希望追踪到目前为止的未实现利润和已实现利润。我们的持仓可以在生成相反信号时关闭。在完成回测后,我们将绘制利润和损失,以查看我们的策略表现如何。

我们的理论听起来像是一个可行的交易策略吗?让我们来看看!以下部分解释了实施回测系统所需的类。

编写一个类来存储 tick 数据

编写一个名为TickData的类,表示从市场数据源接收的单个数据单元的 Python 代码:

class TickData(object):
    """ Stores a single unit of data """

    def __init__(self, timestamp='', symbol='', 
                 open_price=0, close_price=0, total_volume=0):
        self.symbol = symbol
        self.timestamp = timestamp
        self.open_price = open_price
        self.close_price = close_price
        self.total_volume = total_volume

在这个例子中,我们对存储时间戳、工具的符号、开盘价和收盘价以及总成交量感兴趣。随着系统的发展,可以添加单个 tick 数据的详细描述,比如最高价或最后成交量。

编写一个类来存储市场数据

MarketData类的一个实例在整个系统中用于存储和检索由各个组件引用的价格。它本质上是一个用于存储最后可用 tick 数据的容器。还包括额外的get辅助函数,以提供对所需信息的便捷引用:

class MarketData(object):
    """ Stores the most recent tick data for all symbols """

    def __init__(self):
        self.recent_ticks = dict()  # indexed by symbol

    def add_tick_data(self, tick_data):
        self.recent_ticks[tick_data.symbol] = tick_data

    def get_open_price(self, symbol):
        return self.get_tick_data(symbol).open_price

    def get_close_price(self, symbol):
        return self.get_tick_data(symbol).close_price

    def get_tick_data(self, symbol):
        return self.recent_ticks.get(symbol, TickData())

    def get_timestamp(self, symbol):
        return self.recent_ticks[symbol].timestamp

编写一个类来生成市场数据的来源

编写一个名为MarketDataSource的类,以帮助我们从外部数据提供商获取历史数据。在本例中,我们将使用Quandl作为我们的数据提供商。该类的构造函数定义如下:

class MarketDataSource(object):
    def __init__(self, symbol, tick_event_handler=None, start='', end=''):
        self.market_data = MarketData()

        self.symbol = symbol
        self.tick_event_handler = tick_event_handler
        self.start, self.end = start, end
        self.df = None

在构造函数中,symbol参数包含了我们的数据提供商识别的值,用于下载我们需要的数据集。实例化了一个MarketData对象来存储最新的市场数据。tick_event_handler参数存储了方法处理程序,当我们迭代数据源时使用。startend参数指的是我们希望保留在pandas DataFrame 变量df中的数据集的开始和结束日期。

MarketDataSource方法中添加fetch_historical_prices()方法,其中包含从数据提供商下载并返回所需的pandas DataFrame 对象的具体指令,该对象保存我们的每日市场价格,如下所示:

def fetch_historical_prices(self):
   import quandl

   # Update your Quandl API key here...
  QUANDL_API_KEY = 'BCzkk3NDWt7H9yjzx-DY'
  quandl.ApiConfig.api_key = QUANDL_API_KEY
   df = quandl.get(self.symbol, start_date=self.start, end_date=self.end)
   return df

由于此方法特定于 Quandl 的 API,您可以根据自己的数据提供商重新编写此方法。

此外,在MarketDataSource类中添加run()方法来模拟在回测期间从数据提供商获取流式价格:

def run(self):
    if self.df is None:
        self.df = self.fetch_historical_prices()

    total_ticks = len(self.df)
    print('Processing total_ticks:', total_ticks)

    for timestamp, row in self.df.iterrows():
        open_price = row['Open']
        close_price = row['Close']
        volume = row['Volume']

        print(timestamp.date(), 'TICK', self.symbol,
              'open:', open_price,
              'close:', close_price)
        tick_data = TickData(timestamp, self.symbol, open_price,
                            close_price, volume)
        self.market_data.add_tick_data(tick_data)

        if self.tick_event_handler:
            self.tick_event_handler(self.market_data)

请注意,第一个if语句在执行从数据提供商下载之前对现有市场数据的存在进行检查。这使我们能够在回测中运行多个模拟,使用缓存数据,避免不必要的下载开销,并使我们的回测运行更快。

for循环在我们的df市场数据变量上用于模拟流式价格。每个 tick 数据被转换和格式化为TickData的一个实例,并添加到market_data对象中作为特定符号的最新可用 tick 数据。然后将此对象传递给任何监听 tick 事件的 tick 数据事件处理程序。

编写订单类

以下代码中的Order类表示策略发送到服务器的单个订单。每个订单包含时间戳、符号、数量和指示买入或卖出订单的标志。在以下示例中,我们将仅使用市价订单,并且预计is_market_orderTrue。如果需要,可以实现其他订单类型,如限价和止损订单。一旦订单被执行,订单将进一步更新为填充价格、时间和数量。按照以下代码给出的方式编写此类:

class Order(object):
    def __init__(self, timestamp, symbol, 
        qty, is_buy, is_market_order, 
        price=0
    ):
        self.timestamp = timestamp
        self.symbol = symbol
        self.qty = qty
        self.price = price
        self.is_buy = is_buy
        self.is_market_order = is_market_order
        self.is_filled = False
        self.filled_price = 0
        self.filled_time = None
        self.filled_qty = 0

编写一个类来跟踪持仓。

Position类帮助我们跟踪我们对交易工具的当前市场位置和账户余额,并且定义如下:

class Position(object):
    def __init__(self, symbol=''):
        self.symbol = symbol
        self.buys = self.sells = self.net = 0
        self.rpnl = 0
        self.position_value = 0

已声明买入、卖出和净值的单位数量分别为buyssellsnet变量。rpnl变量存储了该符号的最近实现利润和损失。请注意,position_value变量的初始值为零。当购买证券时,证券的价值从此账户中借记。当出售证券时,证券的价值记入此账户。

当订单被填充时,账户的持仓会发生变化。在Position类中编写一个名为on_position_event()的方法来处理这些持仓事件:

def on_position_event(self, is_buy, qty, price):
    if is_buy:
        self.buys += qty
    else:
        self.sells += qty

    self.net = self.buys - self.sells
    changed_value = qty * price * (-1 if is_buy else 1)
    self.position_value += changed_value

    if self.net == 0:
        self.rpnl = self.position_value
        self.position_value = 0

在我们的持仓发生变化时,我们更新并跟踪买入和卖出的证券数量,以及证券的当前价值。当净头寸为零时,持仓被平仓,我们获得当前的实现利润和损失。

每当持仓开启时,我们的证券价值会受到市场波动的影响。有一个未实现的利润和损失的度量有助于跟踪每次 tick 移动中市场价值的变化。在Position类中添加以下calculate_unrealized_pnl()方法:

def calculate_unrealized_pnl(self, price):
    if self.net == 0:
        return 0

    market_value = self.net * price
    upnl = self.position_value + market_value
    return upnl

使用当前市场价格调用calculate_unrealized_pnl()方法可以得到特定证券当前市场价值。

编写一个抽象策略类

以下代码中给出的Strategy类是所有其他策略实现的基类,并且被写成:

from abc import abstractmethod

class Strategy:
    def __init__(self, send_order_event_handler):
        self.send_order_event_handler = send_order_event_handler

    @abstractmethod
    def on_tick_event(self, market_data):
        raise NotImplementedError('Method is required!')

    @abstractmethod
    def on_position_event(self, positions):
        raise NotImplementedError('Method is required!')

    def send_market_order(self, symbol, qty, is_buy, timestamp):
        if self.send_order_event_handler:
            order = Order(
                timestamp,
                symbol,
                qty,
                is_buy,
                is_market_order=True,
                price=0,
            )
            self.send_order_event_handler(order)

当新的市场 tick 数据到达时,将调用on_tick_event()抽象方法。子类必须实现这个抽象方法来对传入的市场价格进行操作。每当我们的持仓有更新时,将调用on_position_event()抽象方法。子类必须实现这个抽象方法来对传入的持仓更新进行操作。

send_market_order()方法由子策略类调用,将市价订单路由到经纪人。这样的事件处理程序存储在构造函数中,实际的实现由本类的所有者在下一节中完成,并直接与经纪人 API 进行接口。

编写一个均值回归策略类

在这个例子中,我们正在实现一个关于 AAPL 股票价格的均值回归交易策略。编写一个继承上一节中Strategy类的MeanRevertingStrategy子类:

import pandas as pd

class MeanRevertingStrategy(Strategy):
    def __init__(self, symbol, trade_qty,
        send_order_event_handler=None, lookback_intervals=20,
        buy_threshold=-1.5, sell_threshold=1.5
    ):
        super(MeanRevertingStrategy, self).__init__(
            send_order_event_handler)

        self.symbol = symbol
        self.trade_qty = trade_qty
        self.lookback_intervals = lookback_intervals
        self.buy_threshold = buy_threshold
        self.sell_threshold = sell_threshold

        self.prices = pd.DataFrame()
        self.is_long = self.is_short = False

在构造函数中,我们接受参数值,告诉我们的策略要交易的证券符号和每笔交易的单位数。send_order_event_handler函数变量被传递给父类进行存储。lookback_intervalsbuy_thresholdsell_threshold变量是与使用均值回归计算生成交易信号相关的参数。

pandas DataFrame prices变量将用于存储传入的价格,is_longis_short布尔变量存储此策略的当前持仓,任何时候只有一个可以为True。这些变量在MeanRevertingStrategy类中的on_position_event()方法中分配:

def on_position_event(self, positions):
    position = positions.get(self.symbol)

    self.is_long = position and position.net > 0
    self.is_short = position and position.net < 0

on_position_event()方法实现了父抽象方法,并在我们的持仓更新时被调用。

此外,在MeanRevertingStrategy类中实现on_tick_event()抽象方法:

def on_tick_event(self, market_data):
    self.store_prices(market_data)

    if len(self.prices) < self.lookback_intervals:
        return

    self.generate_signals_and_send_order(market_data)

在每个 tick-data 事件中,市场价格存储在当前策略类中,用于计算交易信号,前提是有足够的数据。在这个例子中,我们使用 20 天的日历史价格回溯期。换句话说,我们将使用过去 20 天价格的平均值来确定均值回归。在没有足够数据的情况下,我们只是跳过这一步。

MeanRevertingStrategy类中添加store_prices()方法:

def store_prices(self, market_data):
    timestamp = market_data.get_timestamp(self.symbol)
    close_price = market_data.get_close_price(self.symbol)
    self.prices.loc[timestamp, 'close'] = close_price

在每个 tick 事件上,prices DataFrame 存储每日收盘价,由时间戳索引。

生成交易信号的逻辑在MeanRevertingStrategy类中的generate_signals_and_send_order()方法中给出:

def generate_signals_and_send_order(self, market_data):
    signal_value = self.calculate_z_score()
    timestamp = market_data.get_timestamp(self.symbol)

    if self.buy_threshold > signal_value and not self.is_long:
        print(timestamp.date(), 'BUY signal')
        self.send_market_order(
            self.symbol, self.trade_qty, True, timestamp)
    elif self.sell_threshold < signal_value and not self.is_short:
        print(timestamp.date(), 'SELL signal')
        self.send_market_order(
            self.symbol, self.trade_qty, False, timestamp)

在每个 tick 事件上,计算当前时期的z-score,我们将很快介绍。一旦 z-score 超过我们的买入阈值值,就会生成买入信号。我们可以通过向经纪人发送买入市价订单来关闭空头头寸或进入多头头寸。相反,当 z-score 超过我们的卖出阈值值时,就会生成卖出信号。我们可以通过向经纪人发送卖出市价订单来关闭多头头寸或进入空头头寸。在我们的回测系统中,订单将在第二天开盘时执行。

MeanRevertingStrategy类中添加calculate_z_score()方法,用于在每个 tick 事件上计算 z-score:

def calculate_z_score(self):
    self.prices = self.prices[-self.lookback_intervals:]
    returns = self.prices['close'].pct_change().dropna()
    z_score = ((returns - returns.mean()) / returns.std())[-1]
    return z_score

使用以下公式对收盘价的每日百分比收益进行 z-score 标准化:

在这里,x是最近的收益,μ是收益的平均值,σ是收益的标准差。 z-score 值为 0 表示该分数与平均值相同。例如,买入阈值值为-1.5。当 z-score 低于-1.5 时,这表示强烈的买入信号,因为预计随后的时期的 z-score 将恢复到零的平均值。同样,卖出阈值值为 1.5 可能表示强烈的卖出信号,预计 z-score 将恢复到平均值。

因此,这个回测系统的目标是找到最优的阈值,以最大化我们的利润。

将我们的模块与回测引擎绑定

在定义了所有核心模块化组件之后,我们现在准备实现回测引擎,作为BacktestEngine类,使用以下代码:

class BacktestEngine:
    def __init__(self, symbol, trade_qty, start='', end=''):
        self.symbol = symbol
        self.trade_qty = trade_qty
        self.market_data_source = MarketDataSource(
            symbol,
            tick_event_handler=self.on_tick_event,
            start=start, end=end
        )

        self.strategy = None
        self.unfilled_orders = []
        self.positions = dict()
        self.df_rpnl = None

在回测引擎中,我们存储标的物和交易单位数量。使用标的物创建一个MarketDataSource实例,同时定义数据集的开始和结束日期。发出的 tick 事件将由我们的本地on_tick_event()方法处理,我们将很快实现。strategy变量用于存储我们均值回归策略类的一个实例。unfilled_orders变量充当我们的订单簿,将存储下一个交易日执行的市场订单。positions变量用于存储Position对象的实例,由标的物索引。df_rpnl变量用于存储我们在回测期间的实现利润和损失,我们可以在回测结束时使用它来绘图。

运行回测引擎的入口点是Backtester类中给出的start()方法。

def start(self, **kwargs):
    print('Backtest started...')

    self.unfilled_orders = []
    self.positions = dict()
    self.df_rpnl = pd.DataFrame()

    self.strategy = MeanRevertingStrategy(
        self.symbol,
        self.trade_qty,
        send_order_event_handler=self.on_order_received,
        **kwargs
    )
    self.market_data_source.run()

    print('Backtest completed.')

通过调用start()方法可以多次运行单个Backtester实例。在每次运行开始时,我们初始化unfilled_orderspositionsdf_rpl变量。使用策略类的一个新实例化,传入标的物和交易单位数量,以及一个名为on_order_received()的方法,用于接收来自策略的订单触发,以及策略需要的任何关键字kwargs参数。

BacktestEngine类中实现on_order_received()方法:

def on_order_received(self, order):
    """ Adds an order to the order book """
    print(
        order.timestamp.date(),
        'ORDER',
        'BUY' if order.is_buy else 'SELL',
        order.symbol,
        order.qty
    )
    self.unfilled_orders.append(order)

当订单生成并添加到订单簿时,我们会在控制台上收到通知。

BacktestEngine类中实现on_tick_event()方法,用于处理市场数据源发出的 tick 事件:

def on_tick_event(self, market_data):
    self.match_order_book(market_data)
    self.strategy.on_tick_event(market_data)
    self.print_position_status(market_data)

在这个例子中,市场数据源预计是每日的历史价格。接收到的 tick 事件代表一个新的交易日。在交易日开始时,我们通过调用match_order_book()方法来检查我们的订单簿,并匹配开盘时的任何未成交订单。之后,我们将最新的市场数据market_data变量传递给策略的 tick 事件处理程序,执行交易功能。在交易日结束时,我们将我们的持仓信息打印到控制台上。

BacktestEngine类中实现match_order_book()match_unfilled_orders()方法:

def match_order_book(self, market_data):
    if len(self.unfilled_orders) > 0:
        self.unfilled_orders = [
            order for order in self.unfilled_orders
            if self.match_unfilled_orders(order, market_data)
        ]

def match_unfilled_orders(self, order, market_data):
    symbol = order.symbol
    timestamp = market_data.get_timestamp(symbol)

    """ Order is matched and filled """
    if order.is_market_order and timestamp > order.timestamp:
        open_price = market_data.get_open_price(symbol)

        order.is_filled = True
        order.filled_timestamp = timestamp
        order.filled_price = open_price

        self.on_order_filled(
            symbol, order.qty, order.is_buy,
            open_price, timestamp
        )
        return False

    return True

在每次调用match_order_book()命令时,都会检查存储在unfilled_orders变量中的待处理订单列表,以便在市场中执行,并在此操作成功时从列表中移除。match_unfilled_orders()方法中的if语句验证订单是否处于正确状态,并立即以当前市场开盘价标记订单为已填充。这将触发on_order_filled()方法上的一系列事件。在BacktestEngine类中实现这个方法:

def on_order_filled(self, symbol, qty, is_buy, filled_price, timestamp):
    position = self.get_position(symbol)
    position.on_position_event(is_buy, qty, filled_price)
    self.df_rpnl.loc[timestamp, "rpnl"] = position.rpnl

    self.strategy.on_position_event(self.positions)

    print(
        timestamp.date(),
        'FILLED', "BUY" if is_buy else "SELL",
        qty, symbol, 'at', filled_price
    )

一旦订单被执行,就需要更新交易符号的相应头寸。position变量包含检索到的Position实例,并且调用其on_position_event()命令会更新其状态。实现的利润和损失会被计算并保存到pandas DataFrame df_rpnl中,并附上时间戳。通过调用on_position_event()命令,策略也会被通知头寸的变化。当这样的事件发生时,我们会在控制台上收到通知。

BacktestEngine类中添加以下get_position()方法:

 def get_position(self, symbol):
    if symbol not in self.positions:
        self.positions[symbol] = Position(symbol)

    return self.positions[symbol]

get_position()方法是一个辅助方法,简单地获取一个交易符号的当前Position对象。如果找不到实例,则创建一个。

on_tick_event()最后一次调用的命令是print_position_status()。在BacktestEngine类中实现这个方法:

def print_position_status(self, market_data):
    for symbol, position in self.positions.items():
        close_price = market_data.get_close_price(symbol)
        timestamp = market_data.get_timestamp(symbol)

        upnl = position.calculate_unrealized_pnl(close_price)

        print(
            timestamp.date(),
            'POSITION',
            'value:%.3f' % position.position_value,
            'upnl:%.3f' % upnl,
            'rpnl:%.3f' % position.rpnl
        )

在每次 tick 事件中,我们打印当前市场价值、实现和未实现利润和损失的任何可用头寸信息到控制台。

运行我们的回测引擎

BacktestEngine类中定义了所有必需的方法后,我们现在可以使用以下代码创建这个类的一个实例:

engine = BacktestEngine(
    'WIKI/AAPL', 1,
    start='2015-01-01',
    end='2017-12-31'
)

在这个例子中,我们对每次交易感兴趣,使用 2015 年到 2017 年三年的每日历史数据进行回测。

发出start()命令来运行回测引擎:

engine.start(
    lookback_intervals=20,
    buy_threshold=-1.5,
    sell_threshold=1.5
)

lookback_interval参数参数值为 20 告诉我们的策略在计算 z 分数时使用最近 20 天的历史每日价格。buy_thresholdsell_threshold参数参数定义了生成买入或卖出信号的边界限制。在这个例子中,-1.5 的买入阈值值表示当 z 分数低于-1.5 时希望持有多头头寸。同样,1.5 的卖出阈值值表示当 z 分数上升到 1.5 以上时希望持有空头头寸。

当引擎运行时,您将看到以下输出:

Backtest started...
Processing total_ticks: 753
2015-01-02 TICK WIKI/AAPL open: 111.39 close: 109.33
...
2015-02-25 TICK WIKI/AAPL open: 131.56 close: 128.79
2015-02-25 BUY signal
2015-02-25 ORDER BUY WIKI/AAPL 1
2015-02-26 TICK WIKI/AAPL open: 128.785 close: 130.415
2015-02-26 FILLED BUY 1 WIKI/AAPL at 128.785
2015-02-26 POSITION value:-128.785 upnl:1.630 rpnl:0.000
2015-02-27 TICK WIKI/AAPL open: 130.0 close: 128.46

从输出日志中,我们可以看到在 2015 年 2 月 25 日生成了一个买入信号,并且在下一个交易日 2 月 26 日开盘时以 128.785 美元的价格向订单簿中添加了一个市价订单以执行。到交易日结束时,我们的多头头寸将有 1.63 美元的未实现利润:

...
2015-03-30 TICK WIKI/AAPL open: 124.05 close: 126.37
2015-03-30 SELL signal
2015-03-30 ORDER SELL WIKI/AAPL 1
2015-03-30 POSITION value:-128.785 upnl:-2.415 rpnl:0.000
2015-03-31 TICK WIKI/AAPL open: 126.09 close: 124.43
2015-03-31 FILLED SELL 1 WIKI/AAPL at 126.09
2015-03-31 POSITION value:0.000 upnl:0.000 rpnl:-2.695
...

继续向下滚动日志,您会看到在 2015 年 3 月 30 日生成了一个卖出信号,并且在下一天 3 月 31 日以 126.09 美元的价格执行了一个卖出市价订单。这关闭了我们的多头头寸,并使我们遭受了 2.695 美元的实现损失。

当回测引擎完成时,我们可以使用以下 Python 代码将我们的策略实现的利润和损失绘制到图表上,以可视化这个交易策略:

%matplotlib inline
import matplotlib.pyplot as plt

engine.df_rpnl.plot(figsize=(12, 8));

这给我们以下输出:

请注意,回测结束时,实现的利润和损失并不完整。我们可能仍然持有未实现的利润或损失的多头或空头头寸。在评估策略时,请确保考虑到这个剩余价值。

回测引擎的多次运行

使用固定的策略参数,我们能够让回测引擎运行一次并可视化其性能。由于回测的目标是找到适用于交易系统考虑的最佳策略参数,我们希望我们的回测引擎在不同的策略参数上多次运行。

例如,定义我们想要在名为THRESHOLDS的常量变量中测试的阈值列表:

THRESHOLDS = [
    (-0.5, 0.5),
    (-1.5, 1.5),
    (-2.5, 2.0),
    (-1.5, 2.5),
]

列表中的每个项目都是买入和卖出阈值值的元组。我们可以使用for循环迭代这些值,调用engine.start()命令,并在每次迭代时绘制图表,使用以下代码:

%matplotlib inline import matplotlib.pyplot as plt

fig, axes = plt.subplots(nrows=len(THRESHOLDS)//2, 
    ncols=2, figsize=(12, 8))
fig.subplots_adjust(hspace=0.4)
for i, (buy_threshold, sell_threshold) in enumerate(THRESHOLDS):
     engine.start(
         lookback_intervals=20,
         buy_threshold=buy_threshold,
         sell_threshold=sell_threshold
     )
     df_rpnls = engine.df_rpnl
     ax = axes[i // 2, i % 2]
     ax.set_title(
         'B/S thresholds:(%s,%s)' % 
         (buy_threshold, sell_threshold)
     )
     df_rpnls.plot(ax=ax)

我们得到以下输出:

四个图显示了在我们的策略中使用各种阈值时的结果。通过改变策略参数,我们得到了不同的风险和回报概况。也许您可以找到更好的策略参数,以获得比这更好的结果!

改进您的回测系统

在本章中,我们基于每日收盘价创建了一个简单的回测系统,用于均值回归策略。有几个方面需要考虑,以使这样一个回测模型更加现实。历史每日价格足以测试我们的模型吗?应该使用日内限价单吗?我们的账户价值从零开始;如何能够准确反映我们的资本需求?我们能够借股做空吗?

由于我们在创建回测系统时采用了面向对象的方法,将来集成其他组件会有多容易?交易系统可以接受多个市场数据源。我们还可以创建组件,使我们能够将系统部署到生产环境中。

上述提到的关注点列表并不详尽。为了指导我们实施健壮的回测模型,下一节详细阐述了设计这样一个系统的十个考虑因素。

回测模型的十个考虑因素

在上一节中,我们进行了一次回测的复制。我们的结果看起来相当乐观。然而,这足以推断这是一个好模型吗?事实是,回测涉及大量研究,值得有自己的文献。以下列表简要涵盖了在实施回测时您可能想要考虑的一些想法。

限制模型的资源

可用于您的回测系统的资源限制了您可以实施回测的程度。只使用最后收盘价生成信号的金融模型需要一组收盘价的历史数据。需要从订单簿中读取的交易系统需要在每个 tick 上都有订单簿数据的所有级别。这增加了存储复杂性。其他资源,如交易所数据、估计技术和计算机资源,对可以使用的模型的性质施加了限制。

模型评估标准

我们如何得出模型好坏的结论?一些要考虑的因素包括夏普比率、命中率、平均收益率、VaR 统计数据,以及遇到的最小和最大回撤。这些因素的组合如何平衡,使模型可用?在实现高夏普比率时,最大回撤能够容忍多少?

估计回测参数的质量

在模型上使用各种参数通常会给我们带来不同的结果。从多个模型中,我们可以获得每个模型的额外数据集。最佳表现模型的参数可信吗?使用模型平均等方法可以帮助我们纠正乐观的估计。

模型平均技术是对多个模型的平均拟合,而不是使用单个最佳模型。

做好面对模型风险的准备

也许经过广泛的回测,你可能会发现自己拥有一个高质量的模型。它会保持多久?在模型风险中,市场结构或模型参数可能会随时间改变,或者制度变革可能会导致你的模型的功能形式突然改变。到那时,你甚至可能不确定你的模型是否正确。解决模型风险的方法是模型平均。

使用样本内数据进行回测

回测帮助我们进行广泛的参数搜索,优化模型的结果。这利用了样本数据的真实和特异方面。此外,历史数据永远无法模仿整个数据来自实时市场的方式。这些优化的结果将始终产生对模型和使用的策略的乐观评估。

解决回测中的常见陷阱

回测中最常见的错误是前瞻性偏差,它有许多形式。例如,参数估计可能来自样本数据的整个时期,这构成了使用未来信息。这些统计估计和模型选择应该按顺序估计,这实际上可能很难做到。

数据错误以各种形式出现,从硬件、软件和人为错误,可能在数据分发供应商路由时发生。上市公司可能会分拆、合并或退市,导致其股价发生重大变化。这些行动可能导致我们的模型中出现生存偏差。未能正确清理数据将给予数据的特异方面不当的影响,从而影响模型参数。

生存偏差是一种逻辑错误,它集中于经历了某种过去选择过程的结果。例如,股市指数可能会报告在不好的时候也有强劲的表现,因为表现不佳的股票被从其组成权重中剔除,导致对过去收益的高估。

未使用收缩估计量或模型平均可能会报告包含极端值的结果,使比较和评估变得困难。

在统计学中,收缩估计量被用作普通最小二乘估计量的替代,以产生最小均方误差。它们可以用来将模型输出的原始估计值收缩到零或另一个固定的常数值。

对模型有一个常识性的想法

在我们的模型中常常缺乏常识。我们可能会尝试用趋势变量解释无趋势变量,或者从相关性推断因果关系。当上下文需要或不需要时,可以使用对数值吗?让我们在接下来的部分看看。

了解模型的背景

对模型有一个常识性的想法几乎是不够的。一个好的模型考虑了历史、参与人员、运营约束、常见的特殊情况,以及对模型的理性理解。商品价格是否遵循季节性变动?数据是如何收集的?用于计算变量的公式可靠吗?这些问题可以帮助我们确定原因,如果出现问题。

确保你有正确的数据

我们中的许多人都无法访问 tick 级别的数据。低分辨率的 tick 数据可能会错过详细信息。即使是 tick 级别的数据也可能充满错误。使用摘要统计数据,如均值、标准误差、最大值、最小值和相关性,告诉我们很多关于数据的性质,无论我们是否真的可以使用它,或者推断回测参数估计。

当进行数据清理时,我们可能会问这些问题:需要注意什么?数值是否现实和合理?缺失数据是如何编码的?

制定一套报告数据和结果的系统。使用图表有助于人眼可视化可能出乎意料的模式。直方图可能显示出意想不到的分布,或者残差图可能显示出意想不到的预测误差模式。残差化数据的散点图可能显示出额外的建模机会。

残差化数据是观察值与模型值之间的差异或残差

挖掘你的结果

通过对多次回测进行迭代,结果代表了关于模型的信息来源。在实时条件下运行模型会产生另一个结果来源。通过数据挖掘所有这些丰富的信息,我们可以获得一个避免将模型规格定制到样本数据的数据驱动结果。建议在报告结果时使用收缩估计或模型平均。

回测中的算法讨论

在考虑设计回测模型时,可以使用一个或多个算法来持续改进模型。本节简要介绍了在回测领域使用的一些算法技术,如数据挖掘和机器学习。

K 均值聚类

k 均值聚类算法是数据挖掘中的一种聚类分析方法。从n次观察的回测结果中,k 均值算法旨在根据它们相对距离将数据分类为k个簇。计算每个簇的中心点。目标是找到给出模型平均点的簇内平方和。模型平均点表示模型的可能平均性能,可用于与其他模型的性能进行进一步比较。

K 最近邻机器学习算法

k 最近邻KNN)是一种懒惰学习技术,不构建任何模型。

初始的回测模型参数集是随机选择或最佳猜测。

在分析模型结果之后,将使用与原始集最接近的k个参数集进行下一步计算。然后模型将选择给出最佳结果的参数集。

该过程持续进行,直到达到终止条件,从而始终提供可用的最佳模型参数集。

分类和回归树分析

分类和回归树CART)分析包含两个用于数据挖掘的决策树。分类树使用分类规则通过决策树中的节点和分支对模型的结果进行分类。回归树试图为分类结果分配一个实际值。得到的值被平均以提供决策质量的度量。

2k 阶乘设计

在设计回测实验时,可以考虑使用2k 阶乘设计。假设有两个因素 A 和 B。每个因素都是布尔值,取值为+1 或-1。+1 表示定量高值,而-1 表示低值。这给我们提供了 2²=4 种结果的组合。对于 3 因素模型,这给我们提供了 2³=8 种结果的组合。以下表格说明了具有 W、X、Y 和 Z 结果的两个因素的示例:

A B 复制 I
+1 +1 W
+1 -1 X
-1 +1 Y
-1 -1 Z

请注意,我们正在生成一个回测的复制,以产生一组结果。进行额外的复制可以为我们提供更多信息。从这些数据中,我们可以进行回归分析和分析其方差。这些测试的目标是确定哪些因素 A 或 B 对另一个更有影响,并选择哪些值,使结果要么接近某个期望值,能够实现低方差,或者最小化不可控变量的影响。

遗传算法

遗传算法(GA)是一种技术,其中每个个体通过自然选择的过程进化,以优化问题。在优化问题中,候选解的种群经历选择的迭代过程,成为父代,经历突变和交叉以产生下一代后代。经过连续世代的循环,种群朝着最优解进化。

遗传算法的应用可以应用于各种优化问题,包括回测,特别适用于解决标准优化、不连续或非可微问题或非线性结果。

总结

回测是模型驱动的投资策略对历史数据的响应的模拟。进行回测实验的目的是发现有关过程或系统的信息,并计算与风险或回报相关的各种因素。这些因素通常一起使用,以找到预测回报的组合。

在设计和开发回测时,以创建视频游戏的概念思考将会很有帮助。在虚拟交易环境中,需要组件来模拟价格流、订单匹配引擎、订单簿管理,以及账户和持仓更新的功能。为了实现这些功能,我们可以探索事件驱动的回测系统的概念。

在本章中,我们设计并实现了一个回测系统,与处理 tick 数据的各种组件进行交互,从数据提供商获取历史价格,处理订单和持仓更新,并模拟触发我们策略执行均值回归计算的流动价格。每个周期的 z 分数被评估为交易信号,这导致生成市场订单,以在下一个交易日开盘时执行。我们进行了单次回测运行以及多次运行,参数不同的策略,绘制了结果的利润和损失,以帮助我们可视化我们交易策略的表现。

回测涉及大量研究,值得有专门的文献。在本章中,我们探讨了设计回测模型的十个考虑因素。为了持续改进我们的模型,可以在回测中使用许多算法。我们简要讨论了其中一些:k 均值聚类,k 最近邻,分类和回归树,2k 因子设计和遗传算法。

在下一章中,我们将学习使用机器学习进行预测。

第十章:金融机器学习

机器学习正在迅速被金融服务行业广泛采用。金融服务业对机器学习的采用受到供应因素的推动,如数据存储、算法和计算基础设施的技术进步,以及需求因素的推动,如盈利需求、与其他公司的竞争,以及监管和监督要求。金融中的机器学习包括算法交易、投资组合管理、保险承保和欺诈检测等多个领域。

有几种类型的机器学习算法,但在机器学习文献中你通常会遇到的两种主要算法是监督学习和无监督学习。我们本章的讨论重点在监督学习上。监督学习涉及提供输入和输出数据来帮助机器预测新的输入数据。监督学习可以是基于回归或基于分类的。基于回归的机器学习算法预测连续值,而基于分类的机器学习算法预测类别或标签。

在本章中,我们将介绍机器学习,研究其在金融领域的概念和应用,并查看一些应用机器学习来辅助交易决策的实际例子。我们将涵盖以下主题:

  • 探索金融中的机器学习应用

  • 监督学习和无监督学习

  • 基于分类和基于回归的机器学习

  • 使用 scikit-learn 实现机器学习算法

  • 应用单资产回归机器学习来预测价格

  • 了解风险度量标准以衡量回归模型

  • 应用多资产回归机器学习来预测回报

  • 应用基于分类的机器学习来预测趋势

  • 了解风险度量标准以衡量分类模型

机器学习简介

在机器学习算法成熟之前,许多软件应用决策都是基于规则的,由一堆ifelse语句组成,以生成适当的响应以交换一些输入数据。一个常见的例子是电子邮箱收件箱中的垃圾邮件过滤器功能。邮箱可能包含由邮件服务器管理员或所有者定义的黑名单词。传入的电子邮件内容会被扫描以检查是否包含黑名单词,如果黑名单条件成立,邮件将被标记为垃圾邮件并发送到“垃圾邮件”文件夹。随着不受欢迎的电子邮件的性质不断演变以避免被检测,垃圾邮件过滤机制也必须不断更新自己以做得更好。然而,通过机器学习,垃圾邮件过滤器可以自动从过去的电子邮件数据中学习,并在收到新的电子邮件时计算分类新邮件是否为垃圾邮件的可能性。

面部识别和图像检测背后的算法基本上是相同的。存储在位和字节中的数字图像被收集、分析和分类,根据所有者提供的预期响应。这个过程被称为“训练”,使用“监督学习”方法。经过训练的数据随后可以用于预测下一组输入数据作为某种输出响应,并带有一定的置信水平。另一方面,当训练数据不包含预期的响应时,机器学习算法被期望从训练数据中学习,这个过程被称为“无监督学习”。

金融中的机器学习应用

机器学习在金融领域的许多领域中越来越多地发挥作用,如数据安全、客户服务、预测和金融服务。许多使用案例也利用了大数据和人工智能,它们并不仅限于机器学习。在本节中,我们将探讨机器学习如何改变金融行业的一些方式。

算法交易

机器学习算法研究高度相关资产价格的统计特性,在回测期间测量它们对历史数据的预测能力,并预测价格在一定精度范围内。机器学习交易算法可能涉及对订单簿、市场深度和成交量、新闻发布、盈利电话或财务报表的分析,分析结果转化为价格变动可能性,并纳入生成交易信号的考虑。

投资组合管理

近年来,“机器顾问”这一概念越来越受欢迎,作为自动化对冲基金经理。它们帮助进行投资组合构建、优化、配置和再平衡,甚至根据客户的风险承受能力和首选投资工具建议客户投资的工具。这些咨询服务作为与数字财务规划师互动的平台,提供财务建议和投资组合管理。

监管和监管职能

金融机构和监管机构正在采用人工智能和机器学习来分析、识别和标记需要进一步调查的可疑交易。像证券交易委员会这样的监管机构采取数据驱动的方法,利用人工智能、机器学习和自然语言处理来识别需要执法的行为。全球范围内,中央机构正在开发监管职能的机器学习能力。

保险和贷款承销

保险公司积极利用人工智能和机器学习来增强一些保险行业的功能,改善保险产品的定价和营销,减少理赔处理时间和运营成本。在贷款承销方面,单个消费者的许多数据点,如年龄、收入和信用评分,与候选人数据库进行比较,以建立信用风险概况,确定信用评分,并计算贷款违约的可能性。这些数据依赖于金融机构的交易和支付历史。然而,放贷人越来越多地转向社交媒体活动、手机使用和消息活动,以捕捉对信用价值的更全面的观点,加快放贷决策,限制增量风险,并提高贷款的评级准确性。

新闻情绪分析

自然语言处理,作为机器学习的一个子集,可以用于分析替代数据、财务报表、新闻公告,甚至是 Twitter 动态,以创建由对冲基金、高频交易公司、社交交易和投资平台使用的投资情绪指标,用于实时分析市场。政治家的演讲,或者重要的新发布,比如中央银行发布的,也正在实时分析,每个字都在被审查和计算,以预测资产价格可能会如何变动以及变动的幅度。机器学习不仅能理解股价和交易的波动,还能理解社交媒体动态、新闻趋势和其他数据来源。

金融之外的机器学习

机器学习越来越多地应用于面部识别、语音识别、生物识别、贸易结算、聊天机器人、销售推荐、内容创作等领域。随着机器学习算法的改进和采用速度的加快,使用案例的列表变得更加长。

让我们通过了解一些术语来开始我们的机器学习之旅,这些术语在机器学习文献中经常出现。

监督和无监督学习

有许多类型的机器学习算法,但你通常会遇到的两种主要类型是监督和无监督机器学习。

监督学习

监督学习从给定的输入中预测特定的输出。这些输入到输出数据的配对被称为训练数据。预测的质量完全取决于训练数据;不正确的训练数据会降低机器学习模型的有效性。例如,一个带有标签的交易数据集,标识哪些是欺诈交易,哪些不是。然后可以构建模型来预测新交易是否是欺诈交易。

监督学习中一些常见的算法包括逻辑回归、支持向量机和随机森林。

无监督学习

无监督学习是基于给定的不包含标签的输入数据构建模型,而是要求检测数据中的模式。这可能涉及识别具有相似基本特征的观察值的聚类。无监督学习旨在对新的、以前未见过的数据进行准确预测。

例如,无监督学习模型可以通过寻找具有相似特征的证券群来定价不流动的证券。常见的无监督学习算法包括 k 均值聚类、主成分分析和自动编码器。

监督机器学习中的分类和回归

有两种主要类型的监督机器学习算法,主要是分类和回归。分类机器学习模型试图从预定义的可能性列表中预测和分类响应。这些预定义的可能性可能是二元分类(例如对问题的“是这封电子邮件是垃圾吗?”的回答)或多类分类。

回归机器学习模型试图预测连续的输出值。例如,预测房价或温度都期望连续范围的输出值。常见的回归形式有普通最小二乘(OLS)回归、LASSO 回归、岭回归和弹性网络正则化。

过度拟合和欠拟合模型

机器学习模型的性能不佳可能是由于过度拟合或欠拟合造成的。过度拟合的机器学习模型是指在训练数据上训练得太好,导致在新数据上表现不佳。这是因为训练数据适应了每一个微小的变化,包括噪音和随机波动。无监督学习算法非常容易过度拟合,因为模型从每个数据中学习,包括好的和坏的。

欠拟合的机器学习模型预测准确性差。这可能是由于可用于构建准确模型的训练数据太少,或者数据不适合提取其潜在趋势。欠拟合模型很容易检测,因为它们始终表现不佳。要改进这样的模型,可以提供更多的训练数据或使用另一个机器学习算法。

特征工程

特征是定义数据特征的属性。通过使用数据的领域知识,可以创建特征来帮助机器学习算法提高其预测性能。这可以是简单的将现有数据的相关部分分组或分桶以形成定义特征。甚至删除不需要的特征也是特征工程。

例如,假设我们有以下时间序列价格数据:

编号 日期和时间 价格 价格行动
1 2019-01-02 09:00:01 55.00 上涨
2 2019-01-02 10:03:42 45.00 下跌
3 2019-01-02 10:31:23 48.00 上涨
4 2019-01-02 11:14:02 33.00 DOWN

通过一天中的小时将时间序列分组,并在每个时间段内采取最后的价格行动,我们得到了这样一个特征:

No. Hour of Day Last Price Action
1 9 UP
2 10 UP
3 11 DOWN

特征工程的过程包括以下四个步骤:

  1. 构思要包括在训练模型中的特征

  2. 创建这些特征

  3. 检查特征如何与模型配合

  4. 从步骤 1 重复,直到特征完美工作

在构建特征方面,没有绝对的硬性规则。特征工程被认为更像是一门艺术而不是科学。

用于机器学习的 Scikit-learn

Scikit-learn 是一个专为科学计算设计的 Python 库,包含一些最先进的机器学习算法,用于分类、回归、聚类、降维、模型选择和预处理。其名称源自 SciPy 工具包,这是 SciPy 模块的扩展。有关 scikit-learn 的详细文档可以在scikit-learn.org找到。

SciPy 是用于科学计算的 Python 模块集合,包含一些核心包,如 NumPy、Matplotlib、IPython 等。

在本章中,我们将使用 scikit-learn 的机器学习算法来预测证券的走势。Scikit-learn 需要安装 NumPy 和 SciPy。使用以下命令通过pip包管理器安装 scikit-learn:

 pip install scikit-learn

使用单一资产回归模型预测价格

配对交易是一种常见的统计套利交易策略,交易者使用一对协整和高度正相关的资产,尽管也可以考虑负相关的配对。

在本节中,我们将使用机器学习来训练基于回归的模型,使用一对可能用于配对交易的证券的历史价格。给定某一天某一证券的当前价格,我们每天预测另一证券的价格。以下示例使用了纽约证券交易所NYSE)上交易的高盛GS)和摩根大通JPM)的历史每日价格。我们将预测 2018 年 JPM 股票价格。

通过 OLS 进行线性回归

让我们从一个简单的线性回归模型开始我们的基于回归的机器学习调查。一条直线的形式如下:

这尝试通过 OLS 拟合数据:

  • a是斜率或系数

  • cy截距的值

  • x是输入数据集

  • 是直线的预测值

系数和截距由最小化成本函数确定:

y是用于执行直线拟合的观察实际值的数据集。换句话说,我们正在执行最小化平方误差和,以找到系数ac,从中我们可以预测当前时期。

在开发模型之前,让我们下载并准备所需的数据集。

准备自变量和目标变量

让我们使用以下代码从 Alpha Vantage 获取 GS 和 JPM 的价格数据集:

In [ ]:
    from alpha_vantage.timeseries import TimeSeries

    # Update your Alpha Vantage API key here...
    ALPHA_VANTAGE_API_KEY = 'PZ2ISG9CYY379KLI'

    ts = TimeSeries(key=ALPHA_VANTAGE_API_KEY, output_format='pandas')
    df_jpm, meta_data = ts.get_daily_adjusted(
        symbol='JPM', outputsize='full')
    df_gs, meta_data = ts.get_daily_adjusted(
        symbol='GS', outputsize='full')

pandas DataFrame 对象df_jpmdf_gs包含了 JPM 和 GS 的下载价格。我们将从每个数据集的第五列中提取调整后的收盘价。

让我们使用以下代码准备我们的自变量:

In [ ]:
    import pandas as pd

    df_x = pd.DataFrame({'GS': df_gs['5\. adjusted close']})

从 GS 的调整收盘价中提取到一个新的 DataFrame 对象df_x。接下来,使用以下代码获取我们的目标变量:

In [ ]: 
    jpm_prices = df_jpm['5\. adjusted close']

JPM 的调整收盘价被提取到jpm_prices变量中,作为一个pandas Series 对象。准备好我们的数据集以用于建模后,让我们继续开发线性回归模型。

编写线性回归模型

我们将创建一个类,用于使用线性回归模型拟合和预测值。这个类还用作在本章中实现其他模型的基类。以下步骤说明了这个过程。

  1. 声明一个名为LinearRegressionModel的类,如下所示:
from sklearn.linear_model import LinearRegression

class LinearRegressionModel(object):
    def __init__(self):
        self.df_result = pd.DataFrame(columns=['Actual', 'Predicted'])

    def get_model(self):
        return LinearRegression(fit_intercept=False)

    def get_prices_since(self, df, date_since, lookback):
        index = df.index.get_loc(date_since)
        return df.iloc[index-lookback:index]        

在我们新类的构造函数中,我们声明了一个名为df_result的 pandas DataFrame,用于存储之后绘制图表时的实际值和预测值。get_model()方法返回sklearn.linear_model模块中LinearRegression类的一个实例,用于拟合和预测数据。set_intercept参数设置为True,因为数据没有居中(即在xy轴上都围绕 0)。

有关 scikit-learn 的LinearRegression的更多信息可以在scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html找到。

get_prices_since()方法使用iloc命令从给定的日期索引date_since开始,获取由lookback值定义的较早期间的子集。

  1. LinearRegressionModel类中添加一个名为learn()的方法,如下所示:
def learn(self, df, ys, start_date, end_date, lookback_period=20):
     model = self.get_model()

     for date in df[start_date:end_date].index:
         # Fit the model
         x = self.get_prices_since(df, date, lookback_period)
         y = self.get_prices_since(ys, date, lookback_period)
         model.fit(x, y.ravel())

         # Predict the current period
         x_current = df.loc[date].values
         [y_pred] = model.predict([x_current])

         # Store predictions
         new_index = pd.to_datetime(date, format='%Y-%m-%d')
         y_actual = ys.loc[date]
         self.df_result.loc[new_index] = [y_actual, y_pred]

learn()方法作为运行模型的入口点。它接受dfys参数作为我们的自变量和目标变量,start_dateend_date作为对应于我们将要预测的数据集索引的字符串,以及lookback_period参数作为用于拟合当前期间模型的历史数据点的数量。

for循环模拟了每日的回测。调用get_prices_since()获取数据集的子集,用于在xy轴上拟合模型。ravel()命令将给定的pandas Series 对象转换为用于拟合模型的目标值的扁平列表。

x_current变量表示指定日期的自变量值,输入到predict()方法中。预测的输出是一个list对象,我们从中提取第一个值。实际值和预测值都保存到df_result DataFrame 中,由当前日期作为pandas对象的索引。

  1. 让我们实例化这个类,并通过以下命令运行我们的机器学习模型:
In [ ]:
    linear_reg_model = LinearRegressionModel()
    linear_reg_model.learn(df_x, jpm_prices, start_date='2018', 
                           end_date='2019', lookback_period=20)

learn()命令中,我们提供了我们准备好的数据集df_xjpm_prices,并指定了 2018 年的预测。在这个例子中,我们假设一个月有 20 个交易日。使用lookback_period值为20,我们使用过去一个月的价格来拟合我们的模型以进行每日预测。

  1. 让我们从模型中检索结果的df_result DataFrame,并绘制实际值和预测值:
In [ ]:
    %matplotlib inline

    linear_reg_model.df_result.plot(
        title='JPM prediction by OLS', 
        style=['-', '--'], figsize=(12,8));

style参数中,我们指定实际值绘制为实线,预测值绘制为虚线。这给我们以下图表:

图表显示我们的预测结果在一定程度上紧随实际值。我们的模型实际上表现如何?在下一节中,我们将讨论用于衡量基于回归的模型的几种常见风险指标。

用于衡量预测性能的风险指标

sklearn.metrics模块实现了几种用于衡量预测性能的回归指标。我们将在接下来的部分讨论平均绝对误差、均方误差、解释方差得分和 R²得分。

作为风险指标的平均绝对误差

平均绝对误差MAE)是一种风险度量,衡量了平均绝对预测误差,可以表示如下:

这里,y分别是实际值和预测值的列表,长度相同,为ny[i]分别是索引i处的预测值和实际值。取绝对值意味着我们的输出结果为正小数。希望 MAE 的值尽可能低。完美的分数为 0 表示我们的预测能力与实际值完全一致,因为两者之间没有差异。

使用sklearn.metrics模块的mean_abolute_error函数获得我们预测的 MAE 值,以下是代码:

In [ ]:
    from sklearn.metrics import mean_absolute_error

    actual = linear_reg_model.df_result['Actual']
    predicted = linear_reg_model.df_result['Predicted']

    mae = mean_absolute_error(actual, predicted)
    print('mean absolute error:', mae)
Out[ ]:
    mean absolute error: 2.4581692107823367

我们的线性回归模型的 MAE 为 2.458。

均方误差作为风险度量

与 MAE 类似,均方误差MSE)也是一种风险度量,衡量了预测误差的平方的平均值,可以表示如下:

平方误差意味着 MSE 的值始终为正,并且希望 MSE 的值尽可能低。完美的 MSE 分数为 0 表示我们的预测能力与实际值完全一致,这些差异的平方可以忽略不计。虽然 MSE 和 MAE 的应用有助于确定模型预测能力的强度,但 MSE 通过对偏离均值较远的错误进行惩罚而胜过 MAE。平方误差对风险度量施加了更重的偏见。

使用以下代码通过sklearn.metrics模块的mean_squared_error函数获得我们预测的 MSE 值:

In [ ]:
    from sklearn.metrics import mean_squared_error
    mse = mean_squared_error(actual, predicted)
    print('mean squared error:', mse)
Out[ ]:
    mean squared error: 12.156835196436589

我们的线性回归模型的 MSE 为 12.156。

解释方差分数作为风险度量

解释方差分数解释了给定数据集的误差分散,公式如下:

这里,Var(y)分别是预测误差和实际值的方差。接近 1.0 的分数是非常理想的,表示误差标准差的平方更好。

使用sklearn.metrics模块的explained_variance_score函数获得我们预测的解释方差分数的值,以下是代码:

In [ ]:
    from sklearn.metrics import explained_variance_score
    eva = explained_variance_score(actual, predicted)
    print('explained variance score:', eva)
Out[ ]:
    explained variance score: 0.5332235487812286

我们线性回归模型的解释方差分数为 0.533。

R²作为风险度量

R²分数也被称为确定系数,它衡量了模型对未来样本的预测能力。它的表示如下:

这里,是实际值的均值,可以表示如下:

R²分数的范围从负值到 1.0。R²分数为 1 表示回归分析没有误差,而分数为 0 表示模型总是预测目标值的平均值。负的 R²分数表示预测表现低于平均水平。

使用sklearn.metrics模块的r2_score函数获得我们预测的 R²分数,以下是代码:

In [ ]:
    from sklearn.metrics import r2_score
    r2 = r2_score(actual, predicted) 
    print('r2 score:', r2)
Out[ ]:
    r2 score: 0.41668246393290576

我们的线性回归模型的 R²为 0.4167。这意味着 41.67%的目标变量的可变性已经被解释。

岭回归

岭回归,或 L2 正则化,通过惩罚模型系数的平方和来解决 OLS 回归的一些问题。岭回归的代价函数可以表示如下:

在这里,α参数预期是一个控制收缩量的正值。较大的 alpha 值会产生更大的收缩,使得系数对共线性更加稳健。

sklearn.linear_model模块的Ridge类实现了岭回归。要实现这个模型,创建一个名为RidgeRegressionModel的类,扩展LinearRegressionModel类,并运行以下代码:

In [ ]:
    from sklearn.linear_model import Ridge

    class RidgeRegressionModel(LinearRegressionModel): 
        def get_model(self):
            return Ridge(alpha=.5)

    ridge_reg_model = RidgeRegressionModel()
    ridge_reg_model.learn(df_x, jpm_prices, start_date='2018', 
                          end_date='2019', lookback_period=20)

在新类中,重写get_model()方法以返回 scikit-learn 的岭回归模型,同时重用父类中的其他方法。将alpha值设为 0.5,其余模型参数保持默认。ridge_reg_model变量表示我们的岭回归模型的一个实例,并使用通常的参数值运行learn()命令。

创建一个名为print_regression_metrics()的函数,以打印之前介绍的各种风险指标:

In [ ]:
    from sklearn.metrics import (
        accuracy_score, mean_absolute_error, 
        explained_variance_score, r2_score
    )
    def print_regression_metrics(df_result):
        actual = list(df_result['Actual'])
        predicted = list(df_result['Predicted'])
        print('mean_absolute_error:', 
            mean_absolute_error(actual, predicted))
        print('mean_squared_error:', mean_squared_error(actual, predicted))
        print('explained_variance_score:', 
            explained_variance_score(actual, predicted))
        print('r2_score:', r2_score(actual, predicted)) 

df_result变量传递给此函数,并在控制台显示风险指标:

In [ ]:
    print_regression_metrics(ridge_reg_model.df_result)
Out[ ]:
    mean_absolute_error: 1.5894879428144535
    mean_squared_error: 4.519795633665941
    explained_variance_score: 0.7954229624785825
    r2_score: 0.7831280913202121

岭回归模型的平均误差得分都低于线性回归模型,并且更接近于零。解释方差得分和 R²得分都高于线性回归模型,并且更接近于 1。这表明我们的岭回归模型在预测方面比线性回归模型做得更好。除了性能更好外,岭回归计算成本也比原始线性回归模型低。

其他回归模型

sklearn.linear_model模块包含了我们可以考虑在模型中实现的各种回归模型。其余部分简要描述了它们。线性模型的完整列表可在scikit-learn.org/stable/modules/classes.html#module-sklearn.linear_model找到。

Lasso 回归

与岭回归类似,最小绝对值收缩和选择算子LASSO)回归也是正则化的另一种形式,涉及对回归系数的绝对值之和进行惩罚。它使用 L1 正则化技术。LASSO 回归的成本函数可以写成如下形式:

与岭回归类似,alpha 参数α控制惩罚的强度。然而,由于几何原因,LASSO 回归产生的结果与岭回归不同,因为它强制大多数系数被设为零。它更适合估计稀疏系数和具有较少参数值的模型。

sklearn.linear_modelLasso类实现了 LASSO 回归。

弹性网络

弹性网络是另一种正则化回归方法,结合了 LASSO 和岭回归方法的 L1 和 L2 惩罚。弹性网络的成本函数可以写成如下形式:

这里解释了 alpha 值:

在这里,alphal1_ratioElasticNet函数的参数。当alpha为零时,成本函数等同于 OLS。当l1_ratio为零时,惩罚是岭或 L2 惩罚。当l1_ratio为 1 时,惩罚是 LASSO 或 L1 惩罚。当l1_ratio在 0 和 1 之间时,惩罚是 L1 和 L2 的组合。

sklearn.linear_modelElasticNet类实现了弹性网络回归。

结论

我们使用了单资产的趋势跟随动量策略通过回归来预测使用 GS 的 JPM 价格,假设这对是协整的并且高度相关。我们也可以考虑跨资产动量来从多样化中获得更好的结果。下一节将探讨用于预测证券回报的多资产回归。

使用跨资产动量模型预测回报

在本节中,我们将通过拥有四种多样化资产的价格来预测 2018 年 JPM 的每日回报,创建一个跨资产动量模型。我们将使用 S&P 500 股票指数、10 年期国库券指数、美元指数和黄金价格的先前 1 个月、3 个月、6 个月和 1 年的滞后回报来拟合我们的模型。这给我们总共 16 个特征。让我们开始准备我们的数据集来开发我们的模型。

准备独立变量

我们将再次使用 Alpha Vantage 作为我们的数据提供者。由于这项免费服务没有提供我们调查所需的所有数据集,我们将考虑其他相关资产作为代理。标准普尔 500 股票指数的股票代码是 SPX。我们将使用 SPDR Gold Trust(股票代码:GLD)来表示黄金价格的代理。Invesco DB 美元指数看涨基金(股票代码:UUP)将代表美元指数。iShares 7-10 年期国库券 ETF(股票代码:IEF)将代表 10 年期国库券指数。运行以下代码来下载我们的数据集:

In [ ]:
    df_spx, meta_data = ts.get_daily_adjusted(
        symbol='SPX', outputsize='full')
    df_gld, meta_data = ts.get_daily_adjusted(
        symbol='GLD', outputsize='full')
    df_dxy, dxy_meta_data = ts.get_daily_adjusted(
        symbol='UUP', outputsize='full')
    df_ief, meta_data = ts.get_daily_adjusted(
        symbol='IEF', outputsize='full')

ts变量是在上一节中创建的 Alpha Vantage 的TimeSeries对象。使用以下代码将调整后的收盘价合并到一个名为df_assetspandas DataFrame 中,并使用dropna()命令删除空值:

In [ ]:
    import pandas as pd

    df_assets = pd.DataFrame({
        'SPX': df_spx['5\. adjusted close'],
        'GLD': df_gld['5\. adjusted close'],
        'UUP': df_dxy['5\. adjusted close'],
        'IEF': df_ief['5\. adjusted close'],
    }).dropna()

使用以下代码计算我们的df_assets数据集的滞后百分比回报:

IN [ ]:
    df_assets_1m = df_assets.pct_change(periods=20)
    df_assets_1m.columns = ['%s_1m'%col for col in df_assets.columns]

    df_assets_3m = df_assets.pct_change(periods=60)
    df_assets_3m.columns = ['%s_3m'%col for col in df_assets.columns]

    df_assets_6m = df_assets.pct_change(periods=120)
    df_assets_6m.columns = ['%s_6m'%col for col in df_assets.columns]

    df_assets_12m = df_assets.pct_change(periods=240)
    df_assets_12m.columns = ['%s_12m'%col for col in df_assets.columns]

pct_change()命令中,periods参数指定要移动的周期数。在计算滞后回报时,我们假设一个月有 20 个交易日。使用join()命令将四个pandas DataFrame 对象合并成一个 DataFrame:

In [ ]:
    df_lagged = df_assets_1m.join(df_assets_3m)\
        .join(df_assets_6m)\
        .join(df_assets_12m)\
        .dropna()

使用info()命令查看其属性:

In [ ]:
    df_lagged.info()
Out[ ]:
    <class 'pandas.core.frame.DataFrame'>
    Index: 2791 entries, 2008-02-12 to 2019-03-14
    Data columns (total 16 columns):
    ...

输出被截断,但您可以看到 16 个特征作为我们的独立变量,跨越 2008 年至 2019 年。让我们继续获取我们目标变量的数据集。

准备目标变量

JPM 的收盘价早些时候已经下载到pandas Series 对象jpm_prices中,只需使用以下代码计算实际百分比收益:

In [ ]:
    y = jpm_prices.pct_change().dropna()

我们获得一个pandas Series 对象作为我们的目标变量y

多资产线性回归模型

在上一节中,我们使用了单一资产 GS 的价格来拟合我们的线性回归模型。这个相同的模型LinearRegressionModel可以容纳多个资产。运行以下命令来创建这个模型的实例并使用我们的新数据集:

In [ ]:
    multi_linear_model = LinearRegressionModel()
    multi_linear_model.learn(df_lagged, y, start_date='2018', 
                             end_date='2019', lookback_period=10)

在线性回归模型实例multi_linear_model中,learn()命令使用具有 16 个特征的df_lagged数据集和y作为 JPM 的百分比变化。考虑到有限的滞后回报数据,lookback_period值被减少。让我们绘制 JPM 的实际与预测百分比变化:

In [ ]:
    multi_linear_model.df_result.plot(
        title='JPM actual versus predicted percentage returns',
        style=['-', '--'], figsize=(12,8));

这将给我们以下图表,其中实线显示了 JPM 的实际百分比回报,而虚线显示了预测的百分比回报:

我们的模型表现如何?让我们在前一节中定义的print_regression_metrics()函数中运行相同的性能指标:

In [ ]:
    print_regression_metrics(multi_linear_model.df_result)
Out[ ]:
    mean_absolute_error: 0.01952328066607389
    mean_squared_error: 0.0007225502867195044
    explained_variance_score: -2.729798588246765
    r2_score: -2.738404583097052

解释的方差分数和 R²分数都在负数范围内,表明模型表现低于平均水平。我们能做得更好吗?让我们探索更复杂的用于回归的树模型。

决策树的集成

决策树是用于分类和回归任务的广泛使用的模型,就像二叉树一样,其中每个节点代表一个问题,导致对相应左右节点的是或否答案。目标是通过尽可能少的问题得到正确答案。

可以在arxiv.org/pdf/1806.06988.pdf找到描述深度神经决策树的论文。

深入决策树很快会导致给定数据的过拟合,而不是从中抽取的分布的整体特性。为了解决这个过拟合问题,数据可以分成子集,并在不同的树上进行训练,每个子集上进行训练。这样,我们最终得到了不同决策树模型的集成。当用替换抽取预测的随机样本子集时,这种方法称为装袋自举聚合。我们可能会或可能不会在这些模型中获得一致的结果,但通过对自举模型进行平均得到的最终模型比使用单个决策树产生更好的结果。使用随机化决策树的集成被称为随机森林

让我们在 scikit-learn 中访问一些决策树模型,我们可能考虑在我们的多资产回归模型中实施。

装袋回归器

sklearn.ensembleBaggingRegressor类实现了装袋回归器。我们可以看看装袋回归器如何对 JPM 的百分比收益进行多资产预测。以下代码说明了这一点:

In [ ]:
    from sklearn.ensemble import BaggingRegressor

    class BaggingRegressorModel(LinearRegressionModel):
        def get_model(self):
            return BaggingRegressor(n_estimators=20, random_state=0) 
In [ ]:
 bagging = BaggingRegressorModel()
    bagging.learn(df_lagged, y, start_date='2018', 
                  end_date='2019', lookback_period=10) 

我们创建了一个名为BaggingRegressorModel的类,它扩展了LinearRegressionModel,并且get_model()方法被重写以返回装袋回归器。n_estimators参数指定了集成中的20个基本估计器或决策树,random_state参数作为随机数生成器使用的种子为0。其余参数为默认值。我们使用相同的数据集运行这个模型。

运行相同的性能指标,看看我们的模型表现如何:

In [ ]:
    print_regression_metrics(bagging.df_result)
Out[ ]:
    mean_absolute_error: 0.0114699264723
    mean_squared_error: 0.000246352185742
    explained_variance_score: -0.272260304849
    r2_score: -0.274602137956

MAE 和 MSE 值表明,决策树的集成产生的预测误差比简单线性回归模型少。此外,尽管解释方差分数和 R²分数为负值,但它表明数据的方差向均值的方向比简单线性回归模型提供的更好。

梯度树提升回归模型

梯度树提升,或简单地说梯度提升,是一种利用梯度下降过程来最小化损失函数,从而改善或提升弱学习器性能的技术。树模型,通常是决策树,一次添加一个,并以分阶段的方式构建模型,同时保持模型中现有的树不变。由于梯度提升是一种贪婪算法,它可以很快地过拟合训练数据集。然而,它可以受益于对各个部分进行惩罚的正则化方法,并减少过拟合以改善其性能。

sklearn.ensemble模块提供了一个梯度提升回归器,称为GradientBoostingRegressor

随机森林回归

随机森林由多个决策树组成,每个决策树都基于训练数据的随机子样本,并使用平均化来提高预测准确性和控制过拟合。随机选择无意中引入了某种形式的偏差。然而,由于平均化,它的方差也减小,有助于补偿偏差的增加,并被认为产生了一个整体更好的模型。

sklearn.ensemble模块提供了一个随机森林回归器,称为RandomForestRegressor

更多集成模型

sklearn.ensemble模块包含各种其他集成回归器,以及分类器模型。更多信息可以在scikit-learn.org/stable/modules/classes.html#module-sklearn.ensemble找到。

使用基于分类的机器学习预测趋势

基于分类的机器学习是一种监督机器学习方法,模型从给定的输入数据中学习,并根据新的观察结果进行分类。分类可以是双类别的,比如识别一个期权是否应该行使,也可以是多类别的,比如价格变化的方向,可以是上升、下降或不变。

在这一部分,我们将再次看一下通过四种多元资产的价格来预测 2018 年 JPM 每日趋势的动量模型。我们将使用 S&P 500 股票指数、10 年期国债指数、美元指数和黄金价格的前 1 个月和 3 个月滞后收益来拟合预测模型。我们的目标变量包括布尔指示器,其中True值表示与前一个交易日收盘价相比的增加或不变,而False值表示减少。

让我们开始准备我们模型的数据集。

准备目标变量

我们已经在之前的部分将 JPM 数据集下载到了pandas DataFrame df_jpm中,而y变量包含了 JPM 的每日百分比变化。使用以下代码将这些值转换为标签:

In [ ]:
    import numpy as np
    y_direction = y >= 0
    y_direction.head(3)
Out[ ]:
    date
    1998-01-05     True
    1998-01-06    False
    1998-01-07     True
    Name: 5\. adjusted close, dtype: bool

使用head()命令,我们可以看到y_direction变量成为了一个pandas Series 对象,其中包含布尔值。百分比变化为零或更多的值被分类为True标签,否则为False。让我们使用unique()命令提取唯一值作为以后使用的列名:

In [ ]:
    flags = list(y_direction.unique())
    flags.sort()
    print(flags)
Out[ ]:    
    [False, True]

列名被提取到一个名为flags的变量中。有了我们的目标变量,让我们继续获取我们的独立多资产变量。

准备多个资产作为输入变量的数据集

我们将重用前一部分中包含四种资产的滞后 1 个月和 3 个月百分比收益的pandas DataFrame 变量df_assets_1mdf_assets_3m,并将它们合并为一个名为df_input的单个变量,使用以下代码:

In [ ]:    
    df_input = df_assets_1m.join(df_assets_3m).dropna()

使用info()命令查看其属性:

In [ ]:
    df_input.info()
Out[ ]:
    <class 'pandas.core.frame.DataFrame'>
    Index: 2971 entries, 2007-05-25 to 2019-03-14
    Data columns (total 8 columns):
    ...

输出被截断了,但是你可以看到我们有八个特征作为我们的独立变量,跨越了 2007 年到 2019 年。有了我们的输入和目标变量,让我们探索 scikit-learn 中可用的各种分类器模型。

逻辑回归

尽管其名称是逻辑回归,但实际上它是用于分类的线性模型。它使用逻辑函数,也称为S 形函数,来模拟描述单次试验可能结果的概率。逻辑函数有助于将任何实值映射到 0 和 1 之间的值。标准的逻辑函数如下所示:

e是自然对数的底,x是 S 形函数中点的X值。是预测的实际值,介于 0 和 1 之间,可以通过四舍五入或截断值转换为 0 或 1 的二进制等价值。

sklean.linear_model模块的LogisticRegression类实现了逻辑回归。让我们通过编写一个名为LogisticRegressionModel的新类来实现这个分类器模型,该类扩展了LinearRegressionModel,使用以下代码:

In [ ]:
    from sklearn.linear_model import LogisticRegression

    class LogisticRegressionModel(LinearRegressionModel):
        def get_model(self):
            return LogisticRegression(solver='lbfgs')

我们的新分类器模型使用了相同的基本线性回归逻辑。get_model()方法被重写以返回一个使用 LBFGS 求解器算法的LogisticRegression分类器模型的实例。

有关用于机器学习的有限内存 BroydenFletcherGoldfarbShanno (LBFGS)算法的论文可以在arxiv.org/pdf/1802.05374.pdf上阅读。

创建这个模型的实例并提供我们的数据:

In [ ]:
    logistic_reg_model = LogisticRegressionModel()
    logistic_reg_model.learn(df_input, y_direction, start_date='2018', 
                             end_date='2019', lookback_period=100)

再次,参数值表明我们有兴趣对 2018 年进行预测,并且在拟合模型时将使用lookback_period值为100作为每日历史数据点的数量。让我们使用head()命令检查存储在df_result中的结果:

In [ ]:
    logistic_reg_model.df_result.head()

这产生了以下表格:

日期 实际 预测
2018-01-02 True True
2018-01-03 True True
2018-01-04 True True
2018-01-05 False True
2018-01-08 True True

由于我们的目标变量是布尔值,模型输出也预测布尔值。但我们的模型表现如何?在接下来的部分中,我们将探讨用于测量我们预测的风险指标。这些指标与前面部分用于基于回归的预测的指标不同。基于分类的机器学习采用另一种方法来测量输出标签。

用于测量基于分类的预测的风险指标

在本节中,我们将探讨用于测量基于分类的机器学习预测的常见风险指标,即混淆矩阵、准确度分数、精度分数、召回率分数和 F1 分数。

混淆矩阵

混淆矩阵,或错误矩阵,是一个帮助可视化和描述分类模型性能的方阵,其中真实值是已知的。sklearn.metrics模块的confusion_matrix函数帮助我们计算这个矩阵,如下面的代码所示:

In [ ]:
    from sklearn.metrics import confusion_matrix

    df_result = logistic_reg_model.df_result 
    actual = list(df_result['Actual'])
    predicted = list(df_result['Predicted'])

    matrix = confusion_matrix(actual, predicted)
In [ ]:
    print(matrix)
Out[ ]:
    [[60 66]
     [55 70]]

我们将实际值和预测值作为单独的列表获取。由于我们有两种类标签,我们获得一个二乘二的矩阵。seaborn库的heatmap模块帮助我们理解这个矩阵。

Seaborn 是一个基于 Matplotlib 的数据可视化库。它提供了一个高级接口,用于绘制引人注目和信息丰富的统计图形,是数据科学家的流行工具。如果您没有安装 Seaborn,只需运行命令:pip install seaborn

运行以下 Python 代码生成混淆矩阵:

In [ ]:
    %matplotlib inline
    import seaborn as sns
    import matplotlib.pyplot as plt

    plt.subplots(figsize=(12,8))
    sns.heatmap(matrix.T, square=True, annot=True, fmt='d', cbar=False, 
                xticklabels=flags, yticklabels=flags)
    plt.xlabel('Actual')
    plt.ylabel('Predicted')
    plt.title('JPM percentage returns 2018');

这将产生以下输出:

不要让混淆矩阵让你困惑。让我们以一种逻辑的方式分解数字,看看混淆矩阵是如何工作的。从左列开始,我们有 126 个样本被分类为 False,分类器正确预测了 60 次,这些被称为真负例(TNs)。然而,分类器错误预测了 66 次,这些被称为假负例(FNs)。在右列,我们有 125 个属于 True 类的样本。分类器错误预测了 55 次,这些被称为假正例(FPs)。分类器虽然有 70 次预测正确,这些被称为真正例(TPs)。这些计算出的比率在其他风险指标中使用,我们将在接下来的部分中发现。

准确度分数

准确度分数是正确预测的观测值与总观测值的比率。默认情况下,它表示为 0 到 1 之间的分数。当准确度分数为 1.0 时,意味着样本中的整个预测标签集与真实标签集匹配。准确度分数可以写成如下形式:

这里,I(x)是指示函数,对于正确的预测返回 1,否则返回 0。sklearn.metrics模块的accuracy_score函数使用以下代码为我们计算这个分数:

In [ ]:
    from sklearn.metrics import accuracy_score
    print('accuracy_score:', accuracy_score(actual, predicted))
Out[ ]:
    accuracy_score: 0.5179282868525896

准确度分数表明我们的模型有 52%的正确率。准确度分数非常适合测量对称数据集的性能,其中假正例和假负例的值几乎相同。为了充分评估我们模型的性能,我们需要查看其他风险指标。

精度分数

精度分数是正确预测的正例观测值与总预测的正例观测值的比率,可以写成如下形式:

这给出了一个介于 0 和 1 之间的精度分数,1 表示模型一直正确分类。sklearn.metrics模块的precision_score函数使用以下代码为我们计算这个分数:

In [ ]:
    from sklearn.metrics import precision_score
    print('precision_score:', precision_score(actual, predicted))
Out[ ]:
    precision_score: 0.5147058823529411

精度分数表明我们的模型能够正确预测分类的 52%的时间。

召回分数

召回分数是正确预测的正样本占实际类别中所有样本的比率,可以写成如下形式:

这给出了一个介于 0 和 1 之间的召回分数,1 是最佳值。sklearn.metrics模块的recall_score函数使用以下代码为我们计算这个分数:

In [ ]:
    from sklearn.metrics import recall_score
    print('recall_score:', recall_score(actual, predicted))
Out[ ]:
    recall_score: 0.56

召回分数表明我们的逻辑回归模型正确识别正样本的时间为 56%。

F1 分数

F1 分数,或 F-度量,是精度分数和召回分数的加权平均值,可以写成如下形式:

这给出了介于 0 和 1 之间的 F1 分数。当精度分数或召回分数为 0 时,F1 分数将为 0。但是,当精度分数和召回分数都为正时,F1 分数对两个度量给予相等的权重。最大化 F1 分数可以创建一个具有最佳召回和精度平衡的平衡分类模型。

sklearn.metrics模块的f1_score函数使用以下代码为我们计算这个分数:

In [ ]:
    from sklearn.metrics import f1_score
    print('f1_score:', f1_score(actual, predicted))
Out[ ]:
    f1_score: 0.5363984674329502

我们的逻辑回归模型的 F1 分数为 0.536。

支持向量分类器

支持向量分类器SVC)是使用支持向量对数据集进行分类的支持向量机SVM)的概念。

有关 SVM 的更多信息可以在www.statsoft.com/textbook/support-vector-machines找到。

SVC类的sklean.svm模块实现了 SVM 分类器。编写一个名为SVCModel的类,并使用以下代码扩展LogisticRegressionModel

In [ ]:
    from sklearn.svm import SVC

    class SVCModel(LogisticRegressionModel):
        def get_model(self):
            return SVC(C=1000, gamma='auto')
In [ ]:
    svc_model = SVCModel()
    svc_model.learn(df_input, y_direction, start_date='2018', 
                    end_date='2019', lookback_period=100)

在这里,我们重写get_model()方法以返回 scikit-learn 的SVC类。指定了高惩罚C值为1000gamma参数是具有默认值auto的核系数。使用我们通常的模型参数执行learn()命令。有了这些,让我们在这个模型上运行风险指标:

In [ ]:
    df_result = svc_model.df_result
    actual = list(df_result['Actual'])
    predicted = list(df_result['Predicted'])
In [ ]:
    print('accuracy_score:', accuracy_score(actual, predicted))
    print('precision_score:', precision_score(actual, predicted))
    print('recall_score:', recall_score(actual, predicted))
    print('f1_score:', f1_score(actual, predicted)) 
Out[ ]:
    accuracy_score: 0.5577689243027888
    precision_score: 0.5538461538461539
    recall_score: 0.576
    f1_score: 0.5647058823529412

我们获得的分数比逻辑回归分类器模型更好。默认情况下,线性 SVM 的C值为 1.0,这在实践中通常会给我们与逻辑回归模型相当的性能。选择C值没有固定的规则,因为它完全取决于训练数据集。可以通过向SVC()模型提供kernel参数来考虑非线性 SVM 核。有关 SVM 核的更多信息,请访问scikit-learn.org/stable/modules/svm.html#svm-kernels

其他类型的分类器

除了逻辑回归和 SVC,scikit-learn 还包含许多其他类型的分类器用于机器学习。以下部分讨论了一些我们也可以考虑在我们的基于分类的模型中实现的分类器。

随机梯度下降

随机梯度下降SGD)是一种使用迭代过程来估计梯度以最小化目标损失函数的梯度下降形式,例如线性支持向量机或逻辑回归。随机项是因为样本是随机选择的。当使用较少的迭代时,会采取更大的步骤来达到解决方案,模型被认为具有高学习率。同样,使用更多的迭代,会采取更小的步骤,导致具有小学习率的模型。SGD 是从业者中机器学习算法的流行选择,因为它已经在大规模文本分类和自然语言处理模型中得到有效使用。

sklearn.linear_model模块的SGDClassifier类实现了 SGD 分类器。

线性判别分析

线性判别分析LDA)是一种经典的分类器,它使用线性决策面,其中估计了数据每个类的均值和方差。它假设数据是高斯分布的,每个属性具有相同的方差,并且每个变量的值都在均值附近。LDA 通过使用贝叶斯定理为每个观测计算判别分数,以确定它属于哪个类。

sklearn.discriminant_analysis模块的LinearDiscriminantAnalysis类实现了 LDA 分类器。

二次判别分析

二次判别分析QDA)与 LDA 非常相似,但使用二次决策边界,每个类使用自己的方差估计。运行风险指标显示,QDA 模型不一定比 LDA 模型表现更好。必须考虑所需模型的决策边界类型。QDA 更适用于具有较低偏差和较高方差的大型数据集。另一方面,LDA 适用于具有较低偏差和较高方差的较小数据集。

sklearn.discriminant_analysis模块的QuadraticDiscriminantAnalysis类实现了 QDA 模型。

KNN 分类器

k-最近邻k-NN)分类器是一种简单的算法,它对每个点的最近邻进行简单的多数投票,并将该点分配给在该点的最近邻中具有最多代表的类。虽然不需要为泛化训练模型,但预测阶段在时间和内存方面较慢且成本较高。

sklearn.neighbors模块的KNeighborsClassifier类实现了 KNN 分类器。

对机器学习算法的使用结论

您可能已经注意到,我们模型的预测值与实际值相差甚远。本章旨在展示 scikit-learn 提供的机器学习功能的最佳特性,这可能用于预测时间序列数据。迄今为止,没有研究表明机器学习算法可以预测价格接近 100%的时间。构建和有效运行机器学习系统需要付出更多的努力。

总结

在本章中,我们介绍了金融领域的机器学习。我们讨论了人工智能和机器学习如何改变金融行业。机器学习可以是监督的或无监督的,监督算法可以是基于回归或基于分类的。Python 的 scikit-learn 库提供了各种机器学习算法和风险指标。

我们讨论了基于回归的机器学习模型的使用,如 OLS 回归、岭回归、LASSO 回归和弹性网络正则化,用于预测证券价格等连续值。还讨论了决策树的集成,如装袋回归器、梯度树提升和随机森林。为了衡量回归模型的性能,我们讨论了均方误差、平均绝对误差、解释方差分数和 R²分数。

基于分类的机器学习将输入值分类为类别或标签。这些类别可以是二元或多元的。我们讨论了逻辑回归、支持向量机、LDA 和 QDA 以及 k-NN 分类器用于预测价格趋势。为了衡量分类模型的性能,我们讨论了混淆矩阵、准确率、精确度和召回率,以及 F1 分数。

在下一章中,我们将探讨在金融领域中使用深度学习。

posted @ 2024-04-18 12:00  绝不原创的飞龙  阅读(20)  评论(0编辑  收藏  举报