大家好,我是 Jack。
视频中,承诺的量化交易教程,它来了!
这期视频播放近 70 多万,后来经过 B 站编辑老师的建议,我对视频的部分内容进行了删减。
视频中,我提到,后续我会晒实仓情况,这个行为存在政策风险。
其实,很多好心读者也都提醒过我,这样不妥,很容易造成粉丝跟盘。
所以,后面我就不公布自己的实仓情况了,我们只探讨量化交易技术本身。
希望各位理解。
同时,我自己改进的量化交易算法,里面有一些激进的选股策略,会在我人为圈定的 top20 的股池中,投票选择得分高的几只股票进行买卖。
这个也存在一个问题:
假如这篇文章,一万人阅读,10% 的人,也就是 1000 人跑了这个算法,并真投了一万元。
这也会造成极端情况下,同一时刻,一起交易一千万的情况。这样也是不好的。
所以,今天要说的这个量化交易算法,是我之前测试过的一个基础版策略,也是别人开源过的。
原理都弄懂,你也可以自己改进策略。
这个量化交易策略,8 年回测,收益 715.44%,最大回撤 28%。
OK,进入我们今天的正题,量化交易。
聚宽
我目前使用的是聚宽平台,这里也就以它为例进行讲解。
PS:有聚宽工作的朋友吗?广告费记得结一下。
聚宽是一个量化交易平台,在这个平台有很多开源的量化交易策略,社区不错。
同时,使用这个平台,还可以回测我们实现的策略。
左边写好代码,选择时间和金额,就可以使用历史数据进行回测。
因为涉及到编写代码,所以你必须具备 Python 编程基础。
没有 Python 基础的小伙伴,先看我的 Python 入门视频吧:
https://www.bilibili.com/video/BV1Sh411a76E/
一定要先好好学 Python,无论你是不是程序员,都很有用。
属于,好学又实用的编程语言。
聚宽平台,有两个 api,可以使用。
一个是在聚宽平台使用的 api:
https://www.joinquant.com/help/api/help#api:API%E6%96%87%E6%A1%A3
如果你是在网页,进行回测,那就需要使用这个 api。
另一个,就是本地化数据 JQData:
https://www.joinquant.com/help/api/help#JQData:JQData
这个 api 是我平时使用的本地化服务接口,只需要 pip 安装一下,就可以本地环境调用接口,获取数据了。
如果你有 Python 基础,那我想这两份 api 使用起来,应该很简单。
ETF 动量轮动
今天要讲的这个量化交易策略,就是在聚宽社区,其他人开源的量化交易算法,起了个名字,叫 ETF 动量轮动。
其实,就是一种长期定投 ETF 的策略,定投大法好。
策略核心有两块,选哪个 ETF,以及何时买卖。
我将这个策略进行了重构,用本地化数据 JQData 的 api 进行了重写。
我对每一行代码,都进行了详细的注释,并罗列了每个知识点,可以参考的文章。
直接看代码吧!
#-*- codig:utf-8 -*- import jqdatasdk as jq from datetime import datetime, timedelta import time import numpy as np import math # https://www.joinquant.com/help/api/help#api:API%E6%96%87%E6%A1%A3 # https://www.joinquant.com/help/api/help#JQData:JQData # aa 为你自己的帐号, bb 为你自己的密码 jq.auth('aa','bb') # http://fund.eastmoney.com/ETFN_jzzzl.html stock_pool = [ '159915.XSHE', # 易方达创业板ETF '510300.XSHG', # 华泰柏瑞沪深300ETF '510500.XSHG', # 南方中证500ETF ] # 动量轮动参数 stock_num = 1 # 买入评分最高的前 stock_num 只股票 momentum_day = 29 # 最新动量参考最近 momentum_day 的 ref_stock = '000300.XSHG' #用 ref_stock 做择时计算的基础数据 N = 18 # 计算最新斜率 slope,拟合度 r2 参考最近 N 天 M = 600 # 计算最新标准分 zscore,rsrs_score 参考最近 M 天 score_threshold = 0.7 # rsrs 标准分指标阈值 # ma 择时参数 mean_day = 20 # 计算结束 ma 收盘价,参考最近 mean_day mean_diff_day = 3 # 计算初始 ma 收盘价,参考(mean_day + mean_diff_day)天前,窗口为 mean_diff_day 的一段时间 day = 1 # 1-1 选股模块-动量因子轮动 # 基于股票年化收益和判定系数打分,并按照分数从大到小排名 def get_rank(stock_pool): score_list = [] for stock in stock_pool: current_dt = time.strftime("%Y-%m-%d", time.localtime()) current_dt = datetime.strptime(current_dt, '%Y-%m-%d') previous_date = current_dt - timedelta(days = day) data = jq.get_price(stock, end_date = previous_date, count = momentum_day, frequency='daily', fields=['close']) # 收盘价 y = data['log'] = np.log(data.close) # 分析的数据个数(天) x = data['num'] = np.arange(data.log.size) # 拟合 1 次多项式 # y = kx + b, slope 为斜率 k,intercept 为截距 b slope, intercept = np.polyfit(x, y, 1) # (e ^ slope) ^ 250 - 1 annualized_returns = math.pow(math.exp(slope), 250) - 1 r_squared = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1))) score = annualized_returns * r_squared score_list.append(score) stock_dict = dict(zip(stock_pool, score_list)) sort_list = sorted(stock_dict.items(), key = lambda item:item[1], reverse = True) print("#" * 30 + "候选" + "#" * 30) for stock in sort_list: stock_code = stock[0] stock_score = stock[1] security_info = jq.get_security_info(stock_code) stock_name = security_info.display_name print('{}({}):{}'.format(stock_name, stock_code, stock_score)) print('#' * 64) code_list = [] for i in range((len(stock_pool))): code_list.append(sort_list[i][0]) rank_stock = code_list[0:stock_num] return rank_stock # 2-1 择时模块-计算线性回归统计值 # 对输入的自变量每日最低价 x(series) 和因变量每日最高价 y(series) 建立 OLS 回归模型,返回元组(截距,斜率,拟合度) # R2 统计学线性回归决定系数,也叫判定系数,拟合优度。 # R2 范围 0 ~ 1,拟合优度越大,自变量对因变量的解释程度越高,越接近 1 越好。 # 公式说明:https://blog.csdn.net/snowdroptulip/article/details/79022532 # https://www.cnblogs.com/aviator999/p/10049646.html def get_ols(x, y): slope, intercept = np.polyfit(x, y, 1) r2 = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1))) return (intercept, slope, r2) # 2-2 择时模块-设定初始斜率序列 # 通过前 M 日最高最低价的线性回归计算初始的斜率,返回斜率的列表 def initial_slope_series(): current_dt = time.strftime("%Y-%m-%d", time.localtime()) current_dt = datetime.strptime(current_dt, '%Y-%m-%d') previous_date = current_dt - timedelta(days = day) data = jq.get_price(ref_stock, end_date = previous_date, count = N + M, frequency='daily', fields=['high', 'low']) return [get_ols(data.low[i:i+N], data.high[i:i+N])[1] for i in range(M)] # 2-3 择时模块-计算标准分 # 通过斜率列表计算并返回截至回测结束日的最新标准分 def get_zscore(slope_series): mean = np.mean(slope_series) std = np.std(slope_series) return (slope_series[-1] - mean) / std # 2-4 择时模块-计算综合信号 # 1.获得 rsrs 与 MA 信号,rsrs 信号算法参考优化说明,MA 信号为一段时间两个端点的 MA 数值比较大小 # 2.信号同时为 True 时返回买入信号,同为 False 时返回卖出信号,其余情况返回持仓不变信号 # 解释: # MA 信号:MA 指标是英文(Moving average)的简写,叫移动平均线指标。 # RSRS 择时信号: # https://www.joinquant.com/view/community/detail/32b60d05f16c7d719d7fb836687504d6?type=1 def get_timing_signal(stock): # 计算 MA 信号 current_dt = time.strftime("%Y-%m-%d", time.localtime()) current_dt = datetime.strptime(current_dt, '%Y-%m-%d') previous_date = current_dt - timedelta(days = day) close_data = jq.get_price(ref_stock, end_date = previous_date, count = mean_day + mean_diff_day, frequency = 'daily', fields = ['close']) # 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1,23 天,要后 20 天 today_MA = close_data.close[mean_diff_day:].mean() # 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0,23 天,要前 20 天 before_MA = close_data.close[:-mean_diff_day].mean() # 计算 rsrs 信号 high_low_data = jq.get_price(ref_stock, end_date = previous_date, count = N, frequency='daily', fields = ['high', 'low']) intercept, slope, r2 = get_ols(high_low_data.low, high_low_data.high) slope_series.append(slope) rsrs_score = get_zscore(slope_series[-M:]) * r2 # 综合判断所有信号 if rsrs_score > score_threshold and today_MA > before_MA: return "BUY" elif rsrs_score < -score_threshold and today_MA < before_MA: return "SELL" else: return "KEEP" slope_series = initial_slope_series()[:-1] # 除去回测第一天的 slope ,避免运行时重复加入 def get_test(): for each_day in range(1, 100)[::-1]: current_dt = time.strftime("%Y-%m-%d", time.localtime()) current_dt = datetime.strptime(current_dt, '%Y-%m-%d') previous_date = current_dt - timedelta(days = each_day - 1) day = each_day print(each_day, previous_date) check_out_list = get_rank(stock_pool) for each_check_out in check_out_list: security_info = jq.get_security_info(each_check_out) stock_name = security_info.display_name stock_code = each_check_out print('今日自选股:{}({})'.format(stock_name, stock_code)) #获取综合择时信号 timing_signal = get_timing_signal(ref_stock) print('今日择时信号:{}'.format(timing_signal)) print('*' * 100) if __name__ == "__main__": check_out_list = get_rank(stock_pool) for each_check_out in check_out_list: security_info = jq.get_security_info(each_check_out) stock_name = security_info.display_name stock_code = each_check_out print('今日自选股:{}({})'.format(stock_name, stock_code)) #获取综合择时信号 timing_signal = get_timing_signal(ref_stock) print('今日择时信号:{}'.format(timing_signal)) print('*' * 100)
策略很短,不到 200 行。
需要注意的是,这个本地化的 api,需要通过官网申请后,才能使用。
申请地址:
https://www.joinquant.com/default/index/sdk
对应的,可以直接在聚宽平台运行的代码,在这里:
https://github.com/Jack-Cherish/quantitative/blob/main/lesson1/quantitive-etf-jq.py
输入代码,就可以直接运行,回测效果了。
时间有限,这里先写这么多。
这个策略,只用了宽基,轮动选择。
后续我会继续讲解,怎样将这个策略部署到我们的服务器上,并定时给我们的手机发送邮件,进行交易提醒。
股市有风险,入市需谨慎,请谨慎使用~
有什么问题,欢迎在评论区里留言。
我是 Jack,我们下期见。
来源:
https://cuijiahua.com/blog/2021/09/qt-2.html