2 minute read

Final Year Project要用QuantConnect做backtest。所以记录一下学习过程。

文档:https://www.quantconnect.com/docs/algorithm-reference/

目标程序:Short-Term Reversal

在最近的几次学习中,我总结了一个还不错的学习方式:找一个想看懂的程序,然后找文档,只看相关部分。目标就是把这个程序看懂,没别的。这可以有效避免一头扎进浩如烟海的文档中。

STR策略非常简单,只要买入超跌 卖出超涨就可以了。作为一个菜鸡,我认为超跌就是在我的stock universe中performance最差的一部分股票。超涨反之。

所以具体操作是:weekly rebalance。看最近一个月universe的performance,然后long 后10%,short前10%。

哦,universe是前100 large-cap US equity。主要流动性大手续费小。

代码如下:

from clr import AddReference
AddReference("System.Core")
AddReference("System.Collections")
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")
import statistics
from datetime import datetime
from System.Collections.Generic import List

class ShortTimeReversal(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2005, 1, 1)
        self.SetEndDate(2017, 5, 10)
        self.SetCash(1000000)
        
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction)
        self._numberOfSymbols = 100
        self._numberOfTradings = int(0.1 * self._numberOfSymbols)
        
        self._numOfWeeks = 0
        self._LastDay = -1
        self._ifWarmUp = False
        
        self._stocks = []
        self._values = {}

    def CoarseSelectionFunction(self, coarse):
        sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True)
        top100 = sortedByDollarVolume[:self._numberOfSymbols]
        return [i.Symbol for i in top100]

    def OnData(self, data):
        
        if not self._ifWarmUp:
            if self._LastDay == -1:
                self._LastDay = self.Time.date()
                self._stocks = []
                self.uni_symbol = None
                symbols = self.UniverseManager.Keys
                for i in symbols:
                    if str(i.Value) == "QC-UNIVERSE-COARSE-USA":
                        self.uni_symbol = i
                for i in self.UniverseManager[self.uni_symbol].Members:
                    self._stocks.append(i.Value.Symbol)
                    self._values[i.Value.Symbol] = [self.Securities[i.Value.Symbol].Price]
            else:
                delta = self.Time.date() - self._LastDay
                if delta.days >= 7:
                    self._LastDay = self.Time.date()
                    for stock in self._stocks:
                        self._values[stock].append(self.Securities[stock].Price)
            self._numOfWeeks += 1
            if self._numOfWeeks == 3:
                self._ifWarmUp = True
        else:
            delta = self.Time.date() - self._LastDay
            if delta.days >= 7:
                self._LastDay = self.Time.date()
                
                returns = {}
                for stock in self._stocks:
                    newPrice = self.Securities[stock].Price
                    oldPrice = self._values[stock].pop(0)
                    self._values[stock].append(newPrice)
                    try:
                        returns[stock] = newPrice/oldPrice
                    except:
                        returns[stock] = 0

                newArr = [(v,k) for k,v in returns.items()]
                newArr.sort()
                for ret, stock in newArr[self._numberOfTradings:-self._numberOfTradings]:
                    if self.Portfolio[stock].Invested:
                        self.Liquidate(stock)
                for ret, stock in newArr[0:self._numberOfTradings]:
                    self.SetHoldings(stock, 0.5/self._numberOfTradings)
                for ret, stock in newArr[-self._numberOfTradings:]:
                    self.SetHoldings(stock, -0.5/self._numberOfTradings)
                self._LastDay = self.Time.date()

Initialize

class ShortTimeReversal(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2005, 1, 1)
        self.SetEndDate(2017, 5, 10)
        self.SetCash(1000000)

        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction)
        self._numberOfSymbols = 100
        self._numberOfTradings = int(0.1 * self._numberOfSymbols)
        
        self._numOfWeeks = 0
        self._LastDay = -1
        self._ifWarmUp = False
        
        self._stocks = []
        self._values = {}

所有的strategy都要继承QCAlgorithm。这个抽象父类定义了一些基本的属性和方法。

然后set回测时间范围,开始的cash。

resolution是数据精度(e.g. 分钟,小时,天)

AddUniverse(CoarseSelectionFunction)的意思是从一堆股票里选出一些。Coarse…的意思是粗删。在这个函数里只能根据price,volume等大属性筛选。这里根据market cap筛选,Coarse够用了。

100支股票,trade10次(?),ifWarmUp表示有没有warmup过。

stocks是挑的100个stock的ticker,values是map,key是100支股票ticker,value是过去四周100支股票价格的list。

CoarseSelectFunction

def CoarseSelectionFunction(self, coarse):
    sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True)
    top100 = sortedByDollarVolume[:self._numberOfSymbols]
    return [i.Symbol for i in top100]

参数coarse应该就是一堆股票对象。根据DollarVolume切个100支出来就行。返回的是symbol的list。

OnData

OnData(self, data)

这里的参数data是一个Slice对象。Slice汇总了一个时间点的价格数据。

价格数据会根据你设定的时间频率以Slice的形式feed进来,然后自动触发OnData()事件(或者说调用该函数)。所以可以说是event-driven?

PS. 但是后面都没有直接用到data。所以似乎其他属性也会随着时间自动变化,比如Securities里面的price(下面有出现)。

Warm-up

先看一下比较简单的warm-up。

在backtest刚开始的时候没有历史数据可以使用,这时就要先收集一会数据。

if not self._ifWarmUp:
    if self._LastDay == -1:
        self._LastDay = self.Time.date()
        self._stocks = []
        self.uni_symbol = None
        symbols = self.UniverseManager.Keys
        for i in symbols:
            if str(i.Value) == "QC-UNIVERSE-COARSE-USA":
                self.uni_symbol = i
        for i in self.UniverseManager[self.uni_symbol].Members:
            self._stocks.append(i.Value.Symbol)
            self._values[i.Value.Symbol] = [self.Securities[i.Value.Symbol].Price]
    else:
        delta = self.Time.date() - self._LastDay
        if delta.days >= 7:
            self._LastDay = self.Time.date()
            for stock in self._stocks:
                self._values[stock].append(self.Securities[stock].Price)
    self._numOfWeeks += 1
    if self._numOfWeeks == 3:
        self._ifWarmUp = True

这段代码真的让我觉得python狗都不用,特别是doc也写得很烂的情况下。比如我完全搜不到UniverseManager的成员。

初始化(lastday == -1)

猜测一下,UniverseManager是一个map,它的Value是一堆universe里面的member。member就有一堆属性,比如symbol就是member.Value.Symbol。

(update:sample code貌似找不到这个QC什么什么的symbol了。smsb)

然后用这个symbol来定位某支股票,fill in stocks[]数组以及values{} map。这里有一个self.Securities,反正就是一堆securities,我也不知道是不是filter过的,反正是一个map, key是symbol,value是一个Security对象。

正式warm-up

就把每一天的price放到values{}里面,然后keep track of过去了多少周。

lastday表示上一周的最后一天。

主程序

delta = self.Time.date() - self._LastDay
if delta.days >= 7:
    self._LastDay = self.Time.date()

    returns = {}
    for stock in self._stocks:
        newPrice = self.Securities[stock].Price
        oldPrice = self._values[stock].pop(0)
        self._values[stock].append(newPrice)
        try:
            returns[stock] = newPrice/oldPrice
        except:
            returns[stock] = 0

    newArr = [(v,k) for k,v in returns.items()]
    newArr.sort()
    for ret, stock in newArr[self._numberOfTradings:-self._numberOfTradings]:
        if self.Portfolio[stock].Invested:
            self.Liquidate(stock)
    for ret, stock in newArr[0:self._numberOfTradings]:
        self.SetHoldings(stock, 0.5/self._numberOfTradings)
    for ret, stock in newArr[-self._numberOfTradings:]:
        self.SetHoldings(stock, -0.5/self._numberOfTradings)
    self._LastDay = self.Time.date()

如果delta.days() >= 7,说明又过去了一周,该rebalance了。对每一个stock:newprice就是今天的price。oldprice就是四周前的price。然后算下return。

sort一下return list,把第11~90名liquidate(平仓)了。

然后根据策略买卖就可以了。setHoldings()的第二个参数应该是 持仓占总资产的比例。

结果

res

Room for Improvement

一个是universe中最开始的100个large cap股票不一定后面也是。有很多股票不知道为什么non-tradable了。但是这和QC的universe的实现关系很大,不知道后面能不能重新选可以trade的那些股票。

另外就是时间区间可能可以变一下,拿最新数据整。