B站热榜视频,炒股源码来了!
大家好,我是 Jack。
视频中,承诺的量化交易教程,它来了!
这期视频播放近 70 多万,后来经过 B 站编辑老师的建议,我对视频的部分内容进行了删减。
视频中,我提到,后续我会晒实仓情况,这个行为存在政策风险。
其实,很多好心读者也都提醒过我,这样不妥,很容易造成粉丝跟盘。
所以,后面我就不公布自己的实仓情况了,我们只探讨量化交易技术本身。
希望各位理解。
同时,我自己改进的量化交易算法,里面有一些激进的选股策略,会在我人为圈定的 top20 的股池中,投票选择得分高的几只股票进行买卖。
这个也存在一个问题:
假如这篇文章,一万人阅读,10% 的人,也就是 1000 人跑了这个算法,并真投了一万元。
这也会造成极端情况下,同一时刻,一起交易一千万的情况。这样也是不好的。
所以,今天要说的这个量化交易算法,是我之前测试过的一个基础版策略,也是别人开源过的。
原理都弄懂,你也可以自己改进策略。
这个量化交易策略,8 年回测,收益 715.44%,最大回撤 28%。
OK,进入我们今天的正题,量化交易。
聚宽
我目前使用的是聚宽平台,这里也就以它为例进行讲解。
https://www.joinquant.com/
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,我们下期见。