欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

DolphinDB Database丨 最简最快的WorldQuant 101 Alpha因子实现

程序员文章站 2022-07-13 15:14:46
...

DolphinDB Database丨 最简最快的WorldQuant 101 Alpha因子实现

DolphinDB Database是一款高性能分布式时序数据库(time-series database),它特别适用于投资银行、对冲基金和交易所的定量查询和分析,可以用于构建基于历史数据的策略测试。下面我们将举例说明如何在DolphinDB中快速构建复杂的Alpha因子。

著名论文 101 Formulaic Alphas 给出了世界*量化对冲基金之一的WorldQuant所使用的101个Alpha因子公式。很多个人和机构尝试用不同的语言来实现这101个Alpha因子。本文中,我们例举了较为简单的Alpha #001和较为复杂的Alpha #098两个因子的实现,分别使用了2行和4行DolphinDB SQL代码,可谓史上最简。

 

因子介绍

Alpha#001公式:rank(Ts_ArgMax(SignedPower((returns<0?stddev(returns,20):close), 2), 5))-0.5

Alpha #001的详细解读可以参考【史上最详细】WorldQuant Alpha 101因子系列#001研究

Alpha#98公式:(rank(decay_linear(correlation(vwap, sum(adv5,26.4719), 4.58418), 7.18088))- rank(decay_linear(Ts_Rank(Ts_ArgMin(correlation(rank(open), rank(adv15), 20.8187), 8.62571), 6.95668) ,8.07206)))

这两个因子在计算时候既用到了cross sectional的信息,也用到了大量时间序列的计算。也即在计算某个股票某一天的因子时,既要用到该股票的历史数据,也要用到当天所有股票的信息,所以计算量很大。

 

需要的数据

输入数据为包含以下字段的table:

symbol:股票代码

date:日期

volume:成交量

vwap:成交量的加权平均价格

open:开盘价格

close:收盘价格

在计算Alpha #001因子时,只需要股票代码、日期和收盘价格三个字段。

 

代码实现

def alpha1(stock){
	t= select date,symbol ,mimax(pow(iif(ratios(close) < 1.0, mstd(ratios(close) - 1, 20),close), 2.0), 5) as maxIndex from stock context by symbol
	return select date,symbol, rank(maxIndex) - 0.5 as A1 from t context by date
}

def alpha98(stock){
	t = select symbol, date, vwap, open, mavg(volume, 5) as adv5, mavg(volume,15) as adv15 from stock context by symbol
	update t set rank_open = rank(open), rank_adv15 = rank(adv15) context by date
	update t set decay7 = mavg(mcorr(vwap, msum(adv5, 26), 5), 1..7), decay8 = mavg(mrank(9 - mimin(mcorr(rank_open, rank_adv15, 21), 9), true, 7), 1..8) context by symbol
	return select symbol, date, rank(decay7)-rank(decay8) as A98 from t context by date 
}

以上代码使用了DolphinDB的一些内置函数:

pow:计算指数幂

iif:条件运算函数

ratios:对向量X的每个元素,计算X(n)\X(n-1)

mstd:在滑动窗口中计算标准差

mavg:在滑动窗口中计算平均值

mcorr:在滑动窗口中计算相关性

msum:在滑动窗口中求和

mrank:返回元素在滑动窗口的按升序或降序排序后的位置

mimin:返回滑动窗口中最小值的索引位置

mimax:返回滑动窗口中最大值的索引位置。

所有的核心代码都用SQL实现,对用户非常方便,可读性也很好。SQL中最关键的功能是context by子句实现的分组计算功能。与group by每个组产生一行记录不同,context by会输出与输入相同行数的记录,方便的进行多个函数嵌套。cross sectional计算时,我们用date分组。时间序列计算时,我们用symbol(股票代码)分组。

 

性能分析

我们使用美国证券市场从2007年到2016年总共11,711只股票进行回测。每个股票每个交易日产生一个因子值, 总共产生1700万个因子值。测试机器配置如下:

CPU: Intel Core i7-9700 @ 3.0 GHz

内存: 32GB

操作系统: Ubuntu 18.04.4

使用单线程计算,Alpha #001因子耗时仅1.5秒,复杂的Alpha #098因子耗时仅4.3秒。使用pandas计算,Alpha #98耗时760.1秒,性能相差150倍以上。pandas实现代码见文末。DolphinDB的高性能得益于它的设计思路和技术架构,详情可以参考揭秘高性能DolphinDB

在计算Alpha因子时,除了要考虑性能问题,代码的简洁性和可读性也不容忽视。DolphinDB实现Alpha #001因子只需要2行核心代码,实现Alpha #098因子仅需要4行核心代码,而其他系统实现则需要大段代码,可以参考pandas实现或者其他系统计算Alpha #001因子。为什么DolphinDB的实现如此简洁?这得益于DolphinDB功能强大的脚本语言。在DolphinDB中,脚本语言与SQL是完全融合在一起的,SQL查询可以直接赋给一个变量或作为函数的参数。DolphinDB的SQL引擎除了支持标准的SQL以外,还为大数据分析特别是时间序列数据分析做了很多有用的扩展。比如上面使用到的context by是DolphinDB的特色之一,它相当于其他系统(SQL Server、PostgreSQL)的窗口函数,但是它比其他系统的窗口函数功能丰富得多,语法上也更简洁灵活,对面板数据非常友好。DolphinDB内置了许多与时序数据相关的函数,并进行了优化,大幅度提高了计算性能,比如上面使用到的mavg、mcorr、mrank、mimin等计算滑动窗口指标的函数复杂度仅为O(n)或O(nlogk), k为窗口大小。如果你想了解更多DolphinDB的脚本语言,可以参考DolphinDB脚本语言的混合范式编程

感兴趣的朋友可以到官网下载 DolphinDB database 尝试实现自己的Alpha因子和策略回测。

 

附件

pandas代码:

from time import time

import pandas as pd
import numpy as np
from scipy.stats import rankdata

def rank(df):
    return df.rank(pct=True)


def decay_linear(df, period=10):
    if df.isnull().values.any():
        df.fillna(method='ffill', inplace=True)
        df.fillna(method='bfill', inplace=True)
        df.fillna(value=0, inplace=True)
    na_lwma = np.zeros_like(df)
    na_lwma[:period, :] = df.iloc[:period, :]
    na_series = df.as_matrix()
    divisor = period * (period + 1) / 2
    y = (np.arange(period) + 1) * 1.0 / divisor
    # Estimate the actual lwma with the actual close.
    # The backtest engine should assure to be snooping bias free.
    for row in range(period - 1, df.shape[0]):
        x = na_series[row - period + 1: row + 1, :]
        na_lwma[row, :] = (np.dot(x.T, y))
    return pd.DataFrame(na_lwma, index=df.index, columns=['CLOSE'])


def rolling_rank(na):
    return rankdata(na)[-1]


def ts_rank(df, window=10):
    return df.rolling(window).apply(rolling_rank)


def ts_argmin(df, window=10):
    return df.rolling(window).apply(np.argmin) + 1


def correlation(x, y, window):
    return x.rolling(window).corr(y)


def decay7(df):
    return rank(decay_linear(correlation(df.vwap, df.adv5, 5).to_frame(), 7).CLOSE)


def decay8(df):
    return rank(decay_linear(ts_rank(ts_argmin(correlation(rank(df.open), rank(df.adv15), 21), 9), 7).to_frame(), 8).CLOSE)


def alpha098(df):
    return (decay7(df) - decay8(df)).to_frame()


path = 'your_path/USPrices.csv'
df = pd.read_csv(path, parse_dates=[1])
df = df[df.date.between('2007.01.01', '2016.12.31')]

print("loaded")

df["vwap"] = df["PRC"]
df["open"] = df["PRC"] + np.random.random(len(df))
df['adv5'] = df.groupby('PERMNO')['VOL'].transform(lambda x: x.rolling(5).mean())
df['adv15'] = df.groupby('PERMNO')['VOL'].transform(lambda x: x.rolling(15).mean())
df['rank_open'] = df.groupby('date')['open'].rank(method='min')
df['rank_adv15'] = df.groupby('date')['adv15'].rank(method='min')

print("start")
start = time()
df['A98'] = df.groupby('PERMNO').apply(alpha098)
end = time()

print(end - start)