weixin_39875028
2021-01-10 08:16 阅读 5

RQAlpha 2.0.0

在最开始,RQAlpha是作为股票策略的回测框架来使用的,因此最初设计认为 Postion 是仓位,而 Portoflio 作为仓位的组合,这样可以很好的解决计算对应股票策略的收益、风险度等非 position 指标数据的业务逻辑,并且在最初也确实可以比较充分的满足业务的需求。

但随着加入期货类型的回测,发现已有的 Portfolio 不能满足期货相关策略的需求,因此我们将 Portfolio 抽离成了一个 BasePortfolio ,而股票和期货分别继承自 BasePortfolio,形成自有的 StockPortfolioFuturePortfolio,他们不再完全一样,有自己内在的计算逻辑和事件响应逻辑。

这时候 Portfolio 不仅作为一个投资组合,其中还包含了诸如计算手续费, 计算滑点成交价,存储当日订单和成交等数据,我们认为其承担的职责过重,因此引入了一个新的概念 - Account 来承担 Portfolio 中事件响应的逻辑,滑点、交易成本计算的逻辑,persist的逻辑,以及订单、成交相关存储和处理的逻辑。我们将 Portfolio 抽离成了一个典型的 model,从而整体上以行为和模型分离了业务逻辑。

然而当我们继续深入下去,决定做混合策略的时候,发现独立的 StockAccountFutureAccount 同时存在,并且具有不同的业务逻辑,而又需要进行整合来作为整体,因为我们构建了 MixedAccount, 而 MixedAccount 下面存在构建的 MixedPortfolio, 而 MixedPortfolio 下面包含 MixedPositions,从而构建了一个看上去像是一个整体的模块,而实际上是一层层的代理。

这种方案下,可以把 MixedPortfolio 看作是一个黑盒的 portfolio, 我们实现了 portfolio 接口定义中所有的方法和属性。而且为了支持扩展性(即增加新的可交易品种,比如期权、比特币等),为了保证统一性(即使只交易股票,也存在 MixedPortfolio,并且需要对应的计算逻辑),无论是跑股票策略、期货策略还是混合策略,其实都使用相同的计算逻辑,而 MixedPortfolio 为了兼容多种模式,在性能上做出了妥协。

用户通过 context.portfolio.positions[some_order_book_id] 拿到的数据实际上是通过一层层的代理获取到的。因此如果用户在 handle_bar 中频繁调用上述语句来获取 position,会多次通过代理获取处理。

另外我们为了降低用户写策略时处理异常情况的复杂度,当不存在某个 order_book_id 对应的持仓时,仍然会自动初始化一个 position 返回给用户,而其中存在非常严重的性能问题,在 handle_bar 中查询过多的 position 时,会导致 python 的垃圾回收不及时,从而出现回测速度变得极慢的情况。

虽然目前为止,只有 Ricequant 一家做到了支持股票和全品种期货回测、模拟和实盘,但这不能成为速度变慢和扩展较复杂的借口,因此我们痛定思痛,决定重新梳理所有的内部计算逻辑,因此才有了这次的2.0.0版本。

1. Portfolio到底是什么? Account到底是什么? 和Portfolio到底应该是什么关系?

为了进行 API 的兼容,我们一直没有修改 Portfolio 的意义,以及其所承担的作用,但在多品种混合策略中,Portfolio 已经失去了原有的含义。我们认为每个策略实际上只应该有一个 Portfolio 来表示整体的投资组合,而其只应该包含账户汇总相关的信息,因此,全新的 Portfolio 包含如下内容:

  • start_date: 表示策略开始执行的日期
  • static_unit_net_value: 静态净值(当日看盘前,策略对应的净值)
  • units: 份额(随着出入金会变化)
  • accounts: 账户字典(其中包含了策略可以交易的所有账户,比如stock_account, future_account, option_account等)

而基于这些基础值,可以计算出来如下投资组合对应的属性:

  • unit_net_value: 实时净值
  • daily_pnl: 当日盈亏
  • daily_returns: 当日收益
  • total_value: 策略总权益
  • total_returns: 策略总收益
  • annualized_returns: 策略年化收益(基于start_date计算)
  • positions: 策略总持仓(positions 实际上属于 Account 下的内容,这里实际上还是做了一层汇总的代理,不过性能上进行了优化,和直接使用 account.positions 速度相差无几了。)
  • cash: 可用资金
  • market_value: 市值

Account 从字面意义理解,就是账户,就像股票账户、期货账户一样,账户下面会有持仓,会计算这个账户下的总权益、总市值、可用资金、交易成本等,Account 也应该负责监听事件来处理对应的事件逻辑。而 Portfolio 实际上是账户上层的一个逻辑概念,其包含了所有账户并进行汇总。

为了保证账户的通用性,无论是模拟交易还是实盘交易,无论是股票、期货还是期权,我们需要寻找他们共同的原子数据作为账户初始化的输入,经过讨论最终确定如下数据作为其共同的初始化内容:

  • total_cash: 总资金
  • positions: 仓位
  • backward_trade_set: 已经处理过的当日成交集合

只要获取到这三个数据,就可以完成 Account 的初始化,但实际交易过程中,可能还会存在,盘中重启,请求断连等需要同步仓位到最新状态的需求,因此我们定义了 fast_forward 函数,每一个账户都需要实现该函数,来保证账户可以进行 positions, frozen_cash, backward_trade_set 的同步。比如我们在 FutureAccount 中进行了如下实现:

python
class FutureAccount(BaseAccount):
    def fast_forward(self, orders=None, trades=list()):
        """
        同步账户信息至最新状态
        :param orders: 订单列表,主要用来计算frozen_cash,如果为None则不计算frozen_cash
        :param trades: 交易列表,基于Trades 将当前Positions ==> 最新Positions
        """
        # 计算 Positions
        for trade in trades:
            if trade.exec_id in self._backward_trade_set:
                continue
            self._apply_trade(trade)
        # 计算 Frozen Cash
        if orders is not None:
            self._frozen_cash = sum(self._frozen_cash_of_order(order) for order in orders if order._is_active())

2. 滑点、手续费等计算是否属于RQAlpha的核心逻辑?

RQAlpha 作为一个通用性的框架,不局限于回测或者模拟,因此计算滑点、手续费等内容实际上都并非是 RQAlpha 的核心组件,换言之应该是 Simulation Mod 的核心组件,因此我们将其进行了抽离,在 Simulation Mod 中提供了 commission_decider, slippage_decidertax_decider,并在 broker 中完成相应的计算逻辑。

我们还在梳理其他的内容,RQAlpha 只提供最核心、最标准、最通用的内容,其他的业务层内容全部放到Mod中实现。

3. 现有的Portfolio和Position计算的内容是否过于复杂,从而计算了很多用户根本不需要的东西?

我们梳理了 AccountPosition 的计算逻辑,进行了重写,新版本的计算逻辑摒弃了很多累加属性的计算,比如 total_orders, total_trades, buy_trade_value等,我们认为这些累加属性并不属于 RQAlpha 核心的一部分,也不属于基础需求,如果在某些场景下需要使用,我们可以通过 Mod 的方式扩展,但不应该纳入到核心模块中来。

同时我们取消了账户级别的盈亏、收益、净值、份额等概念,这些都不应该跟账户耦合,另外我们修改了 PositionAccount 几乎全部属性的计算方式,现在所有的属性都是由初始化时所传入的原子数据计算所得,而且几乎全部使用 lazy compute 来实现按需计算,减少回测和实盘中因为计算而产生的延时。

具体更改的内容非常多,如果对于细节感兴趣,可以通过对比代码的变化来进行了解。

4. 到底应该在哪里,哪个模块来处理诸如bar/tick这样的事件?或者说有必要每个bar来了计算market_value等内容吗?那又该是哪个模块来处理下单/拒单/成交相关的计算逻辑?

在不同的场景下,实际上 Account 需要监听的事件是不同的。

比如新版本实现了 bar 数据和 tick 数据的缓存,因此 Account 不需要监听 bartick 事件来记录 last_price,只需要将 last_price做成 lazy property 即可,用到的时候从缓存的数据中取就可以了。

因为我们不再规定 Account 具体的事件注册内容,取而代之的是定义了 register_event 函数,每个 Account 只需要根据自己的需求来订阅事件即可。比如在 StockAccount 中注册了如下事件:

python
class StockAccount(BaseAccount):
    def register_event(self):
        """
        注册事件
        """
        event_bus = Environment.get_instance().event_bus
        event_bus.add_listener(EVENT.TRADE, self._on_trade)
        event_bus.add_listener(EVENT.ORDER_PENDING_NEW, self._on_order_pending_new)
        event_bus.add_listener(EVENT.ORDER_CREATION_REJECT, self._on_order_unsolicited_update)
        event_bus.add_listener(EVENT.ORDER_UNSOLICITED_UPDATE, self._on_order_unsolicited_update)
        event_bus.add_listener(EVENT.ORDER_CANCELLATION_PASS, self._on_order_unsolicited_update)
        event_bus.add_listener(EVENT.PRE_BEFORE_TRADING, self._before_trading)
        event_bus.add_listener(EVENT.PRE_AFTER_TRADING, self._after_trading)
        event_bus.add_listener(EVENT.SETTLEMENT, self._on_settlement)

目前 StockAccountFutureAccount 不再监听 BarTick 事件,也不会实时根据最新的价格更新对应的净值,而是采取 lazy_compute 的方式,只有使用到相应数据的时候再进行计算,从而极大降低了回测和实盘的计算逻辑延时。

5. 是否需要进行实时持久化?持久化真的有意义吗?

新版本的 RQAlpha 提供持久化的功能,但本身不依赖于实时持久化,Account 的恢复只需要同一个截面的 total_cash, positionsbackward_trade_set,当恢复以后直接通过读取当日 tradesopen_orders 通过 fast_forward 函数即可恢复到最新状态。直接连接实盘也可以瞬间完成准确的账户恢复。

6. 我们现有的持久化内容是否和对接实盘获取的数据一致,如果不一致就会导致产生完全不同的两套计算和恢复Account/Positions的逻辑,这些问题应该怎么标准化,怎么解决?

原先我们使用两套不同的逻辑来恢复策略在模拟交易和实盘交易中所拥有的账户及持仓信息,但这次重构以后,他们全部按照上面所述的进行恢复。当我们支持更多的实盘,或者更多的交易品种时,也同样以这样的方式进行恢复,未来对接新的实盘或者交易标的,都是一件非常轻松的事。

7. 如果我们增加净值和份额的话,那是否每个Account都有自己的净值和份额?如果有的话,产生出入金的时候MixedAccount的净值和份额又如何计算?

我们认真讨论过关于每个账户提供净值和份额是否可行的问题,最后的结论是不可行,因为 portfolio 层级的净值和份额无法再出入金后直接通过账户的净值份额来算出,而实际上账户级别应该看的是总资产的变化,讨论净值没有意义。因此我们只以 Portfolio 层级提供相应的净值和份额计算,出入金会更改 portfolio 的份额以及对应账户的cash。

8. 回测过程中,我们真的需要撮合引擎吗?其使用场景到底是怎样的?

很多人询问模拟(回测也算是一种模拟)过程中的撮合逻辑的问题,比如说什么是 CurrentBarClose 成交,什么是 NextBarOpen 成交,比如说我下单为什么还有部分订单被拒单,比如说 limit order 的下单成交是如何模拟的,比如有的用户其实就是把下单当做一个信号,并不想让其拒单。

这些问题或者需求都是现实存在的,而目前来说,基于日线/分钟线的撮合逻辑是存在误差且并不准确的。我们提供撮合逻辑的初衷是帮助初学者避免使用未来数据以及帮助不了解 Market Impact 可能会在实际交易中对于下单产生影响的同学进行模拟,但当前的撮合逻辑反而可能会影响到资深量化用户进行精准回测的需求。

因此我们决定提供全新的一个回测模式,即信号模式。用户下单只是信号,不会因为当前 Bar 成交量不足而被拒单,RQAlpha 会根据用户的下单指令立即成交并返回成交信息,更新账户状态(但是验证可用资金是否足够,平仓验证可平仓为是否足够,验证价格是否超过涨跌停等等的风控逻辑还是存在的)。在这种模式下,用户可以做到精准回测,不需要考虑订单被撤销、追单等逻辑,回测速度回更快,但需要自行处理未来数据等问题。

9. 为什么Release的新版本会是2.0.0?具体的发布时间?

我们主要是宣传目前版本 0.3.14而在知乎上发布了 米筐开源量化交易框架 RQAlpha 2.0

因为相比于半年前的 0.0.72 版本,几乎是完全不同的框架了,因此以 2.0 vs 1.0 的方式来介绍全新的RQAlpha。

但有不少同学,询问具体的版本为什么两边是不同步的,因此觉得从这个角度来看,确实现在的状态不太好。

所以正好趁着这次有别于与 0.3.14 的大版本迭代,升级项目至 2.0.0 版本,而且这么大改动,确实对得起这个版本号>_<

2.0.0 版本会在 2017/3/27 发布beta版本,并同时发布 rqlpha-mod-vnpy 的beta版本。

未来会有更多的交易标的(比如期权、比特币,国外市场)的计算逻辑的支持以及对应的实盘接口和实时数据的支持,也希望有兴趣的同学加入到RQAlpha的开发和测试中来。

该提问来源于开源项目:ricequant/rqalpha

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享

11条回答 默认 最新

  • weixin_39661405 weixin_39661405 2021-01-10 08:16

    吓到了-。- ,老司机写了那么长的总结。。。

    点赞 评论 复制链接分享
  • weixin_39555320 weixin_39555320 2021-01-10 08:16

    辛苦艾老师,正在拜读。

    点赞 评论 复制链接分享
  • weixin_39779739 weixin_39779739 2021-01-10 08:16

    辛苦老司机了。这次重构完毕之后,业务逻辑更加清晰。account管钱,portfolio管组合方面的风险收益计算,strategy是策略本身的逻辑实现。

    点赞 评论 复制链接分享
  • weixin_39875028 weixin_39875028 2021-01-10 08:16

    部分是总结...部分这次的重构的核心内容

    点赞 评论 复制链接分享
  • weixin_39785814 weixin_39785814 2021-01-10 08:16

    升级到2.0时会自动卸载原有的numpy+MKL...导致我一票依赖mkl的包scipy numba全废了- -

    点赞 评论 复制链接分享
  • weixin_39875028 weixin_39875028 2021-01-10 08:16

    ?? RQAlpha 2.0.0还没发布啊 你是怎么升级到2.0的...?

    点赞 评论 复制链接分享
  • weixin_39875028 weixin_39875028 2021-01-10 08:16

    另外 Python的包依赖本来就很烂,所以才有的 pyenv 或者 conda env,你不能因为一个库扰乱了另一个项目的环境而怪在这个库的头上... 正确的做法是通过env 来进行分离他们之间的依赖关系。

    点赞 评论 复制链接分享
  • weixin_39785814 weixin_39785814 2021-01-10 08:16

    受教了,刚刚看到upgrade并非是2.0,感谢!

    点赞 评论 复制链接分享
  • weixin_39633452 weixin_39633452 2021-01-10 08:16

    good job

    点赞 评论 复制链接分享
  • weixin_39860108 weixin_39860108 2021-01-10 08:16

    期待啊,2.0什么时候发布

    点赞 评论 复制链接分享
  • weixin_39875028 weixin_39875028 2021-01-10 08:16

    RQAlpha 刚刚发布了 2.0.0b0 测试版本。

    您可以通过 pip install -U rqalpha==2.0.0b0 来升级试用

    点赞 评论 复制链接分享

相关推荐