使用Python进行量化投资的学习报告 Python量化投资学习报告
CatsJuice
编辑于 2019-4-26
上一次更新: 2019-05-21 16:04
CONTENTS:
- quantitative-investment-learning
- 1. 数据抓取
- 2. 热点获取
- 3. 数据分析
Tushare是一个免费、开源的python财经数据接口包。主要实现对股票等金融数据从数据采集、清洗加工 到数据存储的过程,能够为金融分析人员提供快速、整洁、和多样的便于分析的数据,为他们在数据获取方面极大地减轻工作量,使他们更加专注于策略和模型的研究与实现上。
接口文档地址:http://tushare.org/
ToShare Pro: https://tushare.pro/
使用前提:
- 安装Python
- 安装Pandas, lxml
下载安装:
- 方式1:
pip install tushare
- 方式2:访问https://pypi.python.org/pypi/Tushare/下载安装
使用:
以历史行情接口为例, 如下调用:
import tushare as ts
ts.get_hist_data('600848') #一次性获取全部日k线数据
结果显示:
Tushare
返回的绝大部分的数据格式都是pandas DataFrame
类型,非常便于用pandas
/NumPy
/Matplotlib
进行数据分析和可视化
官网: https://www.wind.com.cn/Default.html
中国市场的精准金融数据服务供应商,为量化投资与各类金融业务系统提供准确、及时、完整的落地数据,内容涵盖 股票、债券、基金、衍生品、指数、宏观行业等各类金融市场数据,助您运筹帷幄,决胜千里
官网: https://uqer.io/
提供各类资产的财务、因子、主题、宏观行业特色大数据,以及量化场景下的PIT数据,保障量化过程不引入未来数据。 股票、期货、指数、场内外基金等多资产多策略回测。丰富的衍生工具,保证多因子策略、事件驱动等快速实现。
手动写爬虫进行抓取, 首先需要确定数据来源, 较为主流的财经数据平台有新浪财经, 东方财富, 网易财经等。 接下来针对若干平台, 分析抓取的过程以及可能遇到的问题;
新浪财经的数据地址为http://vip.stock.finance.sina.com.cn/q/go.php/vIR_CustomSearch/index.phtml, 通过切换页面可以发现, 地址栏的url的参数发生了变化,变化的规律为:p=n
, n
为当前页码, 而股票代码和股票名称, 通过F12
打开开发者工具, 表单中的值均可定位到, 应该可以通过爬虫抓取所有股票的基本信息, 通过新浪财经抓取股票的代码我这里没有写, 暂先略过;
在新浪财经数据中心, 我并没有找到交易数据的表格, 要查看某只股票的日线 / 分线只能通过点击某只股票进到详情页(而url中的股票代码对应着不同的股票), 而新浪财经的数据并非以表格的形式展示, 而是通过图表展示, 鼠标移动时通过JavaScript
更新日期及当前股票的数据信息;那么直接抓取网页是无法获取到数据的, 现在的思路是分析js文件, 找到鼠标移动时监听到的事件, 查明js是如何更新数据的, js的数据从哪里提取,以此来抓取信息;
首先, 通过页面元素审查可以发现, 分时线是通过HTML5
的canvas
绘制的, 在Sources找相关的js文件, 可以找到paintSth.js
文件, 由于在鼠标移动时会更新页面元素, 所以可以直接在文件中查找mousemove
, 找到了相关代码如下:
C = this.interactCanvas,
...
...
...
C.addEventListener("mousemove", o),
可以看到Canvas加了一个mousemove的监听器, 执行o
, 再查找o()
, 可以找到如下代码:
function o() {
if (!c) {
var t = document.createElement("canvas");
c = t.getContext("2d")
}
return c
}
这里c
又是一个未知量, 所以应该继续检索c
的信息, 由于关联的js文件较多, 这种做法过于费时费力, 爬取新浪财经的交易数据应该不是明智的选择;
换种思路, 既然可以浏览, 那么使用 selenium
就有可能, selenium
可以进行自动化测试, 让鼠标在固定位置移动, 同时抓取更新的信息, 这种做法是可行的, 我也尝试着做了, 由于这一方法过于不实用, 源码略;
这种做法局限性太大, 首先, 效率过低, 这是很致命的一个缺陷, 除此之外, 由于移动导致的像素不同, 可能会出现数据遗漏或重复;最后, 新浪财经默认只显示一定日期的交易数据, 要查看更早的需要手动拖动进度条, 这就使得selenium
的操作误差更大;
综上所述, 在新浪抓取交易数据是挺不容易的事情;
新浪的财务数据在http://vip.stock.finance.sina.com.cn/q/go.php/vFinanceAnalyze/kind/profit/index.phtml?p=1, 这应该是爬虫抓取中喜闻乐见的格式了,换页不需要通过ajax, 所以抓取的时候只需要设定好抓取的总页数, 循环抓取页面再解析即可, 接下来即可直接进行代码的编写(未亲自验证)
网易财经的所有沪深A股数据位于http://quotes.money.163.com/old/#query=EQA&DataType=HS_RANK&sort=PERCENT&order=desc&count=24&page=0, 从url
来看, 换页通过url传参即可改变了, 但是实际操作可以发现, 点击换页时url
中的page
并不会改变, 但改变url
中的page
参数, 当前页面序号会改变;但这不意味着可以像1.2.1. 新浪财经中爬取新浪财经一样, 枚举url的page参数来爬取所有信息;因为股票数据是异步加载的, 直接抓取无法获取到值;
对于网易财经,由于点击换页时页面的url没有更新,所以应该是使用了Ajax
或Js
来更新数据, 通过F12
调起开发者工具, 在Network
选型卡中, 筛选XHR
, 每当点击换页时, 就会有新的XHR
, 分析这些XHR
的url可以发现,只有page
值在改变:
直接复制Request URL
并使用浏览器访问, 可以得到json
格式的数据, 但是可以看到中文通过Unicode
编码了, 在获取后, 可以通过s.decode('unicode_escape')
来解码;接下来就是对json
解析并提取需要的信息了, json格式如下:
在list
中有[0]
到[23]
共24条数据, 对应请求中的参数count=24
, 关于字段名的解释, 以下为我的分析:
No. | key_name | meaning |
---|---|---|
1 | ANNOUNMT | 公告信息,并非必须,对应在页面中有公告标签的股票才有这个字段 |
2 | CODE | 股票代码 |
3 | FIVE_MINUTE | 5分钟涨跌额 |
4 | HIGH | 最高 |
5 | HS | 换手率(不带%) |
6 | LB | 量比 |
7 | LOW | 最低 |
8 | MCAP | 流通市值 |
9 | MFRATIO | list, 包含2个值MFRATIO2 :净利润, MFRATIO10 : 主营收 |
10 | MFSUM | 每股收益 |
11 | NAME | 名称 |
12 | NO | 网易财经中的编号 |
13 | OPEN | 今日开盘 |
14 | PE | 市盈率 |
15 | PERCENT | 涨跌幅 |
16 | PRICE | 价格 |
17 | TCAP | 总市值 |
18 | TURNOVER | 成交额 |
19 | VOLUME | 成交量 |
20 | WB | 委比 |
所以可以直接抓取这个url来获取相关的数据, 更有趣的是, 请求参数中有个count
参数, 决定了数据的数量, 所以我尝试将count
设置成全部数量, 查看网易财经沪深A股, 网易的编号最后一只为3607
, 所以如下请求:
'http://quotes.money.163.com/hs/service/diyrank.php?host=http%3A%2F%2Fquotes.money.163.com%2Fhs%2Fservice%2Fdiyrank.php&page=0&query=STYPE%3AEQA&fields=NO%2CSYMBOL%2CNAME%2CPRICE%2CPERCENT%2CUPDOWN%2CFIVE_MINUTE%2COPEN%2CYESTCLOSE%2CHIGH%2CLOW%2CVOLUME%2CTURNOVER%2CHS%2CLB%2CWB%2CZF%2CPE%2CMCAP%2CTCAP%2CMFSUM%2CMFRATIO.MFRATIO2%2CMFRATIO.MFRATIO10%2CSNAME%2CCODE%2CANNOUNMT%2CUVSNEWS&sort=PERCENT&order=desc&count=3607&type=query'
即可返回所有json
格式的数据, 然后再进行解析, 并写入文件, 完整代码如下:
import urllib
import json
import csv
from tqdm import tqdm
# 下载器
class Downloader(object):
def __init__(self, url):
self.url = url
def download(self):
html_content = urllib.request.urlopen(self.url).read()
html_content = html_content.decode("utf-8")
return html_content
# 调度器
class Controller(object):
def __init__(self):
self.downloader = None
self.parser = None
self.saver = None
def get_data(self):
url = "http://quotes.money.163.com/hs/service/diyrank.php?host=http%3A%2F%2Fquotes.money.163.com%2Fhs%2Fservice%2Fdiyrank.php&page=0&query=STYPE%3AEQA&fields=NO%2CSYMBOL%2CNAME%2CPRICE%2CPERCENT%2CUPDOWN%2CFIVE_MINUTE%2COPEN%2CYESTCLOSE%2CHIGH%2CLOW%2CVOLUME%2CTURNOVER%2CHS%2CLB%2CWB%2CZF%2CPE%2CMCAP%2CTCAP%2CMFSUM%2CMFRATIO.MFRATIO2%2CMFRATIO.MFRATIO10%2CSNAME%2CCODE%2CANNOUNMT%2CUVSNEWS&sort=PERCENT&order=desc&count=3607&type=query"
html_content = urllib.request.urlopen(url).read()
# 这时候解码可能导致json解析错误!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# html_content = html_content.decode("unicode_escape")
# 当数量大于2995会报错, json解析失败, 原因是编号为2996的股票, 在公告中嵌套了引号导致json解析失败
data = json.loads(html_content)
self.saver = Saver(data=data)
self.saver.save()
class Saver(object):
def __init__(self, data):
self.data = data
self.prifix = 'F:\\files\\sharesDatas\\code_list\\' # 存放目录
def save(self):
# 新建文件, 写入文件头
file_header = ["代码", "名称", '流通市值', '每股收益', '总市值']
csv_file = open(self.prifix+'沪深A股.csv', 'w', newline='')
writer = csv.writer(csv_file)
writer.writerow(file_header)
list = self.data['list']
sum = len(list)
for i in tqdm(range(0, sum)):
item = list[i]
# 处理股票代码, 去掉网易财经的0/1前缀, 并使其在Excel中显示正常(加`)
code = str(item['CODE'])
code = code[1:7] if len(code) == 7 else code[:]
code = "`" + code
row = [code, item['NAME'], item['MCAP'], item['MFSUM'], item['TCAP']]
csv_file = open(self.prifix + '沪深A股.csv', 'a', newline='') # 追加
writer = csv.writer(csv_file)
writer.writerow(row)
if __name__ == '__main__':
controller = Controller()
controller.get_data()
网易财经的日线交易数据可在http://quotes.money.163.com/trade/lsjysj_601318.html#06f01查看, 需要将url
中的601318
替换成相应的股票代码, 在这个页面没有换页按钮, 仅显示若干条数据, 但是在数据表的右上角有个下载数据
的链接, 点击后, 需要勾选需要下载的字段, 点击下载后会下载一个code.csv
文件, 所以要做的就是抓取下载的真实url
, 按F12
打开开发者工具, 点击下载按钮后, 在控制台看到如下提示:
"http://quotes.money.163.com/service/chddata.html?code=0601318&start=20070301&end=20190426&fields=TCLOSE;HIGH;LOW;TOPEN;LCLOSE;CHG;PCHG;TURNOVER;VOTURNOVER;VATURNOVER;TCAP;MCAP"
而想要搞清楚url的参数, 可以转到submit
的文件, 这里即b.667271.min.js:1
, 这是一个压缩的js文件, 格式化后, 在最后可以找到如下关键代码:
submit: function() {
var e = n.value;
if (e) e = e.replace(/-/g, "");
var a = i.value;
if (a) a = a.replace(/-/g, "");
var o = t.elem.getElementsByTagName("input"),
r = [];
for (var d = 0; d < o.length; d++) {
if (o[d].type == "checkbox" && o[d].checked) {
r.push(o[d].value)
}
}
var c = "/service/chddata.html?code=" + window["STOCKCODE"];
e && /^\d{8}$/.test(e) && (c += "&start=" + e);
a && /^\d{8}$/.test(a) && (c += "&end=" + a);
r.length && (c += "&fields=" + r.join(";"));
location.href = c
}
能够清楚地看到url的拼接过程, 具体参数如下:
编号 | 参数 | 解释 | 规范 |
---|---|---|---|
1 | start | 起始日期 | 年月日直接拼接,如20190427 ,不足2位补0 |
2 | end | 截止日期 | 年月日直接拼接,如20190427 ,不足2位补0 |
3 | fields | 即需要下载的字段 | 使用; 分割,如:TCLOSE;HIGH;LOW;TOPEN;LCLOSE;CHG;PCHG;TURNOVER;VOTURNOVER;VATURNOVER;TCAP;MCAP 具体意义不作详述 |
4 | code | 股票代码 | 关于股票代码, 网易财经的股票代码在传参时, 如果是以6 开头的股票, 需要在前面加0 , 而以0 和3 开头的股票需要在前面加1 ,如:1000333 , 1300001 , 0601318 |
接下来是做抓取, 抓取就是根据已经获取的股票代码, 枚举股票代码并下载对应的日线数据, 仅需注意每次循环最好使用time.sleep(random.random()*2)
, 否则可能因操作频繁被拒绝访问;下面是部分源代码(注: 这里源代码中的股票代码是实时获取的, 抓取的是股城网, 因为只需要抓取股票代码即可):
import random
import time
from stock.classes.Downloader import Downloader
from stock.classes.Parser import Parser
from stock.classes.Saver import Saver
# 控制器
class Controller(object):
# 构造函数
def __init__(self, url, kline_filepath, codelist_filepath, date):
'''
:param url: 股票基本信息url( 股城网>行情>沪深A股 )
:param kline_filepath: 日线数据文件保存路径
:param codelist_filepath: 股票代码文件保存路径
:param date: 查询截止日期
'''
self.url = url
self.kline_filepath = kline_filepath
self.codelist_filepath = codelist_filepath
self.date = date
self.downloader = None # 下载器实例
self.parser = None # 解析器实例
self.saver = Saver() # 存储器实例
# 执行函数
def start(self):
page = 1
all_code = []
while page <= 181:
print("当前为第%s页" % page)
time.sleep(random.random()*2)
self.downloader = Downloader(self.url % page)
page += 1
html_content = self.downloader.download()
self.parser = Parser(html_content)
codeList = self.parser.parseSharesCode()
for code in codeList:
all_code.append(code)
# print(codeList)
self.saver.saveKlineToCSV(codeList=codeList, filepath=self.kline_filepath, date=self.date)
self.save(codelist=all_code)
def save(self,codelist):
self.saver.saveCodeListToCSV(codeList=codelist, filepath=self.codelist_filepath) # 保存代码到csv
# self.saver.saveKlineToMySQL(filepath=self.kline_filepath) # 保存所有股票的信息到数据库
# 程序入口
if __name__ == '__main__':
url = 'https://hq.gucheng.com/HSinfo.html?en_hq_type_code=&sort_field_name=px_change_rate&sort_type=desc&page=%s' # 股城网>行情>沪深A股
kline_filepath = 'F:\\files\\sharesDatas\\dayline\\' # 定义数据文件保存路径
codelist_filepath = 'F:\\files\\sharesDatas\\code_list\\' # 定义数据文件保存路径
controller = Controller(url=url, kline_filepath=kline_filepath, codelist_filepath=codelist_filepath, date='20190428')
controller.start()
项目地址: https://github.com/CatsJuice/netease-stock-day-line)
或者:
git clone https://github.com/CatsJuice/netease-stock-day-line.git
网页url为http://quotes.money.163.com/f10/zycwzb_601318.html#01c01, 可以看到同交易日线数据一样, 这里有一个下载数据的按钮, 对应盈利能力
, 偿还能力
等, 而这里直接通过开发者工具审查元素, 即可看到超链接的href
:/service/zycwzb_601318.html?type=report&part=ylnl
, 很快, 就能拿到完整的url
, 然后操作同网易日线数据, 项目地址同样位于https://github.com/CatsJuice/netease-stock-day-line)
因为没找到合适的页面, 实时资金流向排行http://data.eastmoney.com/zjlx/detail.html爬取所有沪深A股代码, 这里不作详述;
东方财富的交易数据/财务数据我也尝试过使用爬虫爬取, 但是还是要走不少弯路的, 而且可能最后还没成功, 首先, 东方财富的数据页面和其他平台一样, 股票代码在url中, 如http://data.eastmoney.com/bbsj/yjbb/600175.html, 直接爬取, 或者使用开发者工具定位页面元素会发现:
对应的数据是乱码的, 而如果继续挖掘其js文件, 是可以找到有加密的函数的,如下图所示:
在这个名字特别明显直白的js
文件load_table_data_pc.js?201606021831
中, 可以看到加密, 解密的方法, 这也使得爬取成为可能, 但是太大费周章暂不考虑。
这里我抓取的是东方财富的国内经济要闻, 页面地址位于http://finance.eastmoney.com/news/cgnjj.html
这一页面的新闻列表不是通过Ajax异步加载的, 可以直接抓取, 而页码由url中cgnjj_x
(x为页码)决定,抓取过程较为简单;这里统计的思路是, 将所有的要闻简述拼接成字符串(以;
分隔并写入txt作备份), 然后利用jieba
库分词并统计词频, 项目地址:https://github.com/CatsJuice/eastmoney-yaowen-keyword
也可以直接clone
至本地
git clone https://github.com/CatsJuice/eastmoney-yaowen-keyword.git
在证券投资中, 有很多技术层面的投资策略, 如各种公式, 通过策略可以筛选出股票, 但并不意味着一定能盈利, 而量化投资可以验证这一策略的可靠性, 接下来就是我对若干策略的验证;
分析连续处于低换手率的股票, 脱离低换手率后, 出现连续处于高换手率, 判断2个时期的收盘价均价, 分析满足这一特征的股票的价格是否会上涨;
程序设计的思路如下
- 迭代日线数据文件
- 判断是否是连续高换手率
- 判断是否在连续高换手率后出现连续低换手率
- 结果展示
注: 因为数据文件是按日期的倒序排序的, 所以分析迭代时, 先判断是否出现连续高换手率
核心代码如下:
high = []
low = []
for row in arr:
rate = row[10]
if rate == "None":
high = []
low = []
continue
if rate == 0:
if len(high) >= self.min_days and len(low) >= self.min_days:
dic = {'high': high, 'low': low}
self.res.append(dic)
high = []
low = []
continue
rate = float(rate)
# 4. 判断是否是 高 换手率
if rate >= self.border_rate:
# 4.3 判断是否是: 连续高 ->连续低 -> 结束连续低
if len(low) >= self.min_days:
# 符合条件, 写入
dic = {'high': high, 'low': low}
self.res.append(dic)
high = []
low = []
elif len(low) > 0: # 连续低中有值
# 不满足连续 低, 重置
high = []
low = []
high.append(row)
elif len(high) < self.min_days: # 是低换手率, 判断是否前面是连续高换手率
# 4.1 前面不是连续的高换手率, 重置高换手率数组
high = []
else:
# 4.2 前面是连续高换手率, 地换手率写入
low.append(row)
# 判断日期是否已达到最后
if row[0] <= self.end_date:
# 判断是否满足条件
if len(high) >= self.min_days and len(low) >= self.min_days:
dic = {'high': high, 'low': low}
self.res.append(dic)
high = []
low = []
break
简易流程图如下:
程序可调整参数如下:
No | param | type | meaning | demo |
---|---|---|---|---|
1 | file_path_prefix |
str |
日线数据目录前缀 | 'F:\\files\\sharesDatas\\kline\\' |
2 | min_days |
int |
最小连续天数 | 10 |
3 | border_rate |
float |
换手率高低边界 | 2 |
4 | end_date |
str |
统计的最早日期 | '2018-12-28' |
程序运行结果截图如下:201905012052.png
完整项目地址:https://github.com/CatsJuice/low-switch-hand-rate
或者:
git clone https://github.com/CatsJuice/low-switch-hand-rate.git
顺势指标又叫CCI指标,CCI指标是美国股市技术分析 家唐纳德·蓝伯特(Donald Lambert)于20世纪80年代提出的,专门测量股价、外汇或者贵金属交易是否已超出常态分布范围。属于超买超卖类指标中较特殊的一种。波动于正无穷大和负无穷大之间。但是,又不需要以0为中轴线,这一点也和波动于正无穷大和负无穷大的指标不同。
1
. 当CCI
指标曲线在+100
线~-100
线的常态区间里运行时,CCI
指标参考意义不大,可以用KDJ等其它技术指标进行研判。
2
. 当CCI
指标曲线从上向下突破+100
线而重新进入常态区间时,表明市场价格的上涨阶段可能结束,将进入一个比较长时间的震荡整理阶段,应及时平多做空。
3
. 当CCI
指标曲线从上向下突破-100
线而进入另一个非常态区间(超卖区)时,表明市场价格的弱势状态已经形成,将进入一个比较长的寻底过程,可以持有空单等待更高利润。如果CCI
指标曲线在超卖区运行了相当长的一段时间后开始掉头向上,表明价格的短期底部初步探明,可以少量建仓。CCI
指标曲线在超卖区运行的时间越长,确认短期的底部的准确度越高。
4
.CCI
指标曲线从下向上突破-100
线而重新进入常态区间时,表明市场价格的探底阶段可能结束,有可能进入一个盘整阶段,可以逢低少量做多。
5
.CCI
指标曲线从下向上突破+100
线而进入非常态区间(超买区)时,表明市场价格已经脱离常态而进入强势状态,如果伴随较大的市场交投,应及时介入成功率将很大。
6
.CCI
指标曲线从下向上突破+100
线而进入非常态区间(超买区)后,只要CCI
指标曲线一直朝上运行,表明价格依然保持强势可以继续持有待涨。但是,如果在远离+100
线的地方开始掉头向下时,则表明市场价格的强势状态将可能难以维持,涨势可能转弱,应考虑卖出。如果前期的短期涨幅过高同时价格回落时交投活跃,则应该果断逢高卖出或做空。
关于公式有两种:
TYP:=(HIGH+LOW+CLOSE)/3;
CCI:(TYP-MA(TYP,N))/(0.015*AVEDEV(TYP,N));
该公式来自通达信
, 含义如下:
TYP赋值:(最高价+最低价+收盘价)/3
输出CCI:(TYP-TYP的N日简单移动平均)/(0.015*TYP的N日平均绝对偏差)
该公式摘录于https://www.joinquant.com/view/community/detail/219
程序中我采用的是公式二, 生成的 CCI
指标追加到日线文件中, 在相应日期后增加 CCI
的值, 在 Python
中对 csv
文件进行增加列的方法, 在网上可找到的方法较少, 以下为一个提供了多种解决方案的, 较为完善的链接: https://stackoverflow.com/questions/11070527/how-to-add-a-new-column-to-a-csv-file, 我使用的是 pandas
库, 关键代码如下:
df = pd.read_csv(filename, encoding='gbk')
df['cci'] = ''
for index, row in pd.iterrows():
df.loc[index, 'cci'] = cci
总共设计了4个类如下:
CCIAnalyze(object)
// CCI分析类analyze_all(self)
// 分析所有股票analyze_one(self)
// 分析一只股票test_buy(self)
// 测试购买(策略一:在下文说明)test_buy_2(self)
// 测试购买(策略二:在下文说明)
CCICalculate(object)
// 根据3.2.3. 公式中的公式二计算CCICCICalculate_2(object)
// 根据3.2.3. 公式中的公式一计算CCIBuyInfo(object)
// 购买辅助类
id | param | type | default | mean | demo | necessary |
---|---|---|---|---|---|---|
1 | file_path_prefix |
str |
None |
日线数据文件目录前缀 | 'F:\\files\\sharesDatas\\kline\\' |
True |
2 | cci_path_prefix |
str |
None |
CCI数据将要保存的目录前缀 | 'F:\\files\\sharesDatas\\cci\\' |
True |
3 | code |
str / int |
None |
股票代码 | 000001 |
False |
4 | end_date |
str |
'0000-00-00' |
最早的日期(截止日期) | '2009-01-01' |
False |
或者clone
:
git clone https://github.com/CatsJuice/stock-cci
在进行测试购买时, 采用的策略有2种,如下:
策略一:
对应CCIAnalyze(object)
中的test_buy(self)
方法, 当CCI
向下突破-100
时, 后一交易日买入, 等到CCI
向上突破100
时, 后一交易日卖出(由于计算出当日的CCI, 当日已不可买入/卖出,所以计算后一交易日买入/卖出)
策略二:
对应CCIAnalyze(object)
中的test_buy_2(self)
方法, 当CCI
向下突破-100
时, 继续观察, 等到向下达到第一个峰值时, 后一交易日买入,等到CCI
向上突破100
时, 继续观察, 等到向上达到第一个峰值时, 后一交易日卖出(和策略一一样, 计算出CCI
后只能后一日买入, 而策略二不同的是, 要判断达到第一个峰值, 必须确定峰值后一日CCI降低, 所以测试时, 购买的是峰值的后第2个
交易日)
经过若干次测试,分别使用的数据是2019-01-01 ~ 2019-04-26
, 2018-01-01 ~ 2019-04-26
, 2017-01-01 ~ 2019-04-26
, 测试的结果, 购买策略一的收益率大概为56%
, 可见这个指标有一定依据, 但是盈利的几率不够高; 而策略二的收益率大概为57%
, 较策略一高一点, 但是依旧无法将其作为购买的唯一指标, 以下是某次运行结果的部分截图:
胡立阳根据股票的价量关系
对股票进行打分(第21招), 而其打分的依据如下:
当日个股表现:
(1)价涨量增
+2
分(2)价涨量缩
+1
分(3)价跌量增
-2
分(4)价跌量缩
-1
分每日累计评分。你只要连续计算一个星期,以最高分或者是评分稳定增加的作为你投资的第一选择,因为那只个股具备了“价量配合”的上涨条件
根据以上打分标准, 不难计算每只股票某日的得分, 然后将所有得分排序即可, 这样能够得到某一天的所有股票打分, 而需要验证每只股票是否盈利, 这里我简单地判断后一天收盘价是否比当天高,所以能够得出每只股票是否盈利, 计算出当天得分前n
只股票的盈利与否, 再用 盈利股票的只数 / n 即当天的盈利率;
以此类推m
天可得出m天这一策略的盈利几率
程序流程图如下:
参数详情如下表
id | param | type | mean | demo |
---|---|---|---|---|
1 | prefix |
str | 网易财经日线数据文件前缀 | 'F:\\files\\sharesDatas\\kline\\' |
2 | date_now |
str | 最新数据的第一个日期, 对应爬取的最新数据表第一行的日期 | '2019-04-26' ; Format( yyyy-mm-dd ) |
3 | days |
int | 加分的天数 | 5 |
4 | calculate_days |
int | 要统计的天数 | 20 |
5 | best_num |
int | 选出的最佳的天数 | 10 |
键值说明:
date
: 统计的日期
rate_low
: 打分最低的盈利几率
rate_high
: 打分最高的盈利几率
在分别统计了每个分数的股票的盈利率, 和排名靠前的股票的盈利率来看, 胡立阳的打分欠缺科学性, 盈利率不稳定, 不建议做参考;
项目地址: https://github.com/CatsJuice/stock-price-num-score
直接clone
:
git clone https://github.com/CatsJuice/stock-price-num-score.git
移动平均线,
Moving Average
,简称MA
,MA
是用统计分析的方法,将一定时期内的证券价格(指数)加以平均,并把不同时间的平均值连接起来,形成一根MA
,用以观察证券价格变动趋势的一种技术指标。
常见的均线有5日均线(MA5), 十日均线(MA10), 二十日均线(MA20), 三十日均线(MA30), 六十日均线(MA60)
在这里, 我的计算方法是简单地计算 n 日内的 收盘价 的算数平均值
基本思路如下:
- 定义要计算的几日(假设为n)均线数组, 进行遍历计算
- 迭代文件列表
- 打开某个文件, 选取 n 行, 计算这n行的
收盘价
总和 - 迭代
csv
文件的每一行, 每次迭代, 将上述收盘价的总和减去首行, 追加新的一行 - 计算
ma
并写入原文件
和之前计算cci不同, 这里不再做多余的迭代, 以此能大大提高效率
id | param | type | mean | demo | necessary |
---|---|---|---|---|---|
1 | file_path_prefix |
str |
日线数据文件目录前缀 | 'H:\\sharesDatas\\kline\\' |
true |
2 | code |
int / str |
股票代码 | '000001' |
false |
3 | end_date |
str |
最早的日期(截止日期) | '2019-01-01' |
false |
4 | all_n |
Array |
要计算的几日ma 数组 | [5,10] |
false |
购买的策略较为简单, 收盘价低于均线即买入, 将收盘价作为买入价格(假设第二个交易日开盘即买入), 收盘价高于均线即卖出, 同样将收盘价作为卖出价格
'黄金交叉' 和 '死亡交叉' 的概念来自 日均线 的百度百科(https://baike.baidu.com/item/%E6%97%A5%E5%9D%87%E7%BA%BF/8784586?fr=aladdin)
1、”黄金交叉” 当10日均线由下往上穿越30日均线,形成10日均线在上,30日均线在下时,其交叉点就是黄金交叉,黄金交叉是多头的表现,出现黄金交叉后,后市会有一定的涨幅空间,这是进场的最佳时机。
2、”死亡交叉” 当10日均线由上往下穿越30日均线,形成30日均线在上,10日均线在下时,其交点称之为”死亡交叉”,”死亡交叉”预示空头市场来临,股市将下跌此时是出场的最佳时机。
对于'老太太选股法', 我分别测试了5日均线和10日均线(由于20日数据较大, 电脑处理太慢, 暂时不考虑), 最终得到的结果为:
ma5
的盈利几率为63.3132%
; 而 ma10
的盈利几率为67.0026%
以下是某次运行的截图:
运行结果中, 可以发现, 无论是盈利或是亏损, 金额基本都在1元以内, 可见这一策略 首先具有一定的科学性, 同时风险也不是很大, 但盈利金额较高的可能性很小;
对此, 我增加了结果判断, 统计亏损 / 盈利 超过 1 元的比例, 以下是结果:
对于 ma5
:
对于 ma10
:
对于 ma20
:
对于 ma30
:
对于 ma60
:
从更详细的结果可以看到, 无论盈利或亏损, 超过 1 元的概率都不大, 但是亏损的时候超过 1 元的概率比盈利大; 更有趣的是, 当 ma的计算天数越多, 盈利的几率越大, 由于一般看盘软件中仅提供了上述这些均线(MA5
, MA10
, MA20
, MA30
, MA60
), 所以这里不再多更高的天数进行测试; 虽然天数越大时, 盈利几率越高, 但是从更细节数据可以看到, 盈利的情况大于1元的概率始终在 10% ~ 15%
, 而亏损时超过 1 元的概率却表现出和天数正相关的趋势, 并且从 24%
跳跃到高达 50%
运行结果如图:
对于这个结果的确是非常出乎意料, 这个盈利率低得感觉好像可以作为一个反向指标使用;
对于这个结果, 首先应该怀疑是自己的代码逻辑出现了问题, 于是我检查了关键代码如下:
# 前一天 ma30 > ma10 , 今天ma10 >= ma30
if prev_row is not None and prev_row['ma30'] > prev_row['ma10'] and row['ma10'] >= row['ma30']:
# 买入
...
...
# 前一天 ma30 < ma10 , 今天ma10 <= ma30
if prev_row['ma30'] < prev_row['ma10'] and row['ma10'] <= row['ma30']:
# 卖出
关键代码部分好像并没有什么逻辑问题, 但是不排除其他部分出现问题, 由于这个指标的预期过于不符, 我试着尝试了交换 '黄金交叉'和'死亡交叉', 修改上述关键代码如下(其实就是把所有大小判断符号反置):
# 前一天 ma30 < ma10 , 今天ma10 <= ma30
if prev_row is not None and prev_row['ma30'] < prev_row['ma10'] and row['ma10'] <= row['ma30']:
# 买入
...
...
# 前一天 ma30 > ma10 , 今天ma10 >= ma30
if prev_row['ma30'] > prev_row['ma10'] and row['ma10'] >= row['ma30']:
# 卖出
以下为运行结果:
这时候盈利率可以说是马马虎虎还算过得去了, 但是盈利率却还是不如 ‘老太太选股法’
项目地址:https://github.com/CatsJuice/line-of-ma
直接 clone
:
git clone https://github.com/CatsJuice/line-of-ma.git
成交手指的是每笔买卖流动的股票数量,现行股票交易所用
手
方便表示成交数量,一手相当于股票一百股,各类炒股软件中的成交量一般用手
表示
当某日开盘初出现集中的大量 买入/卖出 , 即成交手出现连续峰值的情况(大概如下图所示情形), 分析该股在当日的趋势
设计的关键, 是为了找出某日开盘时的集中峰值, 在这里我首先设定分界线, 默认 10
点前为开盘初, 统计全天的换手量平均值,以及开盘初的换手量平均值, 当开盘初的换手量大于全天的某倍数(默认为2)时, 即认定当日开盘初的换手量为集中峰值;
但将今日的换手量平均值作为比较, 在实际运用中可能无法实施, 所以另一策略是统计 前一日 的平均换手量作为参考(在此尚未实现)
no. | param | type | mean | format | default | necessary | demo |
---|---|---|---|---|---|---|---|
1 | tick_file_prefix |
str |
分时数据文件目录前缀 | / | none |
true |
"F:\\files\\sharesDatas\\tushare_tick_data\\" |
2 | end_date |
str |
终止日期 | 'yyyymmdd' | '00000000' |
false |
'20190426' |
3 | end_time |
str |
开盘初的终止时间 | 'hh:mm:ss' |
'10:00:00' |
false |
'11:00:00' |
4 | multiple |
int /float |
设定倍数 | / | 2 |
false |
2.5 |
以下为不区分买盘或卖盘, 倍数为 2 时, 当天前后均价变化是否上涨的结果:
而只考虑买盘时, 这个几率为34%, 只考虑卖盘时几率为47%, 可见开盘初集中的成交量不一定会导致今日价格走上升的趋势,但在这个部分的程序设计中, 分析得不是很到位, 首先实际中没法考虑当天后面的成交量情况, 所以更稳妥的方法应该是计算 前一交易日的平均成交量 , 而在判断这这股票是否处于上涨的趋势时, 简单判断今日的前后均价也不是最不合理的。
项目地址为:https://github.com/CatsJuice/stock-volume-analyze
直接 clone
:
git clone https://github.com/CatsJuice/stock-volume-analyze.git
百度百科的解释:
MACD
称为异同移动平均线,是从双指数移动平均线发展而来的,由快的指数移动平均线(EMA12
)减去慢的指数移动平均线(EMA26
)得到快线DIF
,再用2 ×(快线DIF-DIF的9日加权移动均线DEA)
得到 MACD柱
MACD
指标是股票技术分析中一个重要的技术指标,由两条曲线和一组红绿柱线组成。两条曲线中波动变化大的是DIF
线,通常为白线或红线,相对平稳的是DEA
线(MACD线),通常为黄线。当DIF
线上穿DEA
线时,这种技术形态叫做MACD金叉
,通常为 买入信号
DIF
由上向下突破DEA
,为卖出信号
【MACD公式】
以下为通达信的公式:
DIF:EMA(CLOSE,SHORT)-EMA(CLOSE,LONG);
DEA:EMA(DIF,MID);
MACD:(DIF-DEA)*2,COLORSTICK;
需要的变量参数为 SHORT
, LONG
, MID
, 在通达信中默认分别为 12
, 26
, 9
;
要计算 MACD
,关键在于求 EMA
(指数移动平均值), 其公式如下:
其中, α
为平滑指数, 一般取 2 / ( N + 1 )
该公式是依赖于前一日的递归的计算,最大的问题便是第一天的 EMA[yesterday]
无法求得, 在程序设计时, 我将它设置为当日收盘价
,
再求 DIF
, 第一天的 DIF
就变成了 0
(当日收盘价 - 当日收盘价), 而求 DEA
(即 DIF 的 EMA) 时,第一天所需的 DEA[yesterday]
同样不知道, 将其设置为 DIF
( 即 0 )。
【性能优化之多线程】
由于数据量较大, 在这里我尝试使用 Python 的多线程进行分析, 添加了一个 calculate_all_by_thread
的方法, 接收 1
个参数(thread_num)即需要的线程数, 然后根据线程数动态创建线程, 将文件划分为等分的块分别计算, 创建线程部分的代码如下:
def calculate_all_by_thread(self, thread_num):
file_list = os.listdir(self.file_prefix)
file_count = len(file_list)
offset = file_count / thread_num
offset = math.ceil(offset)
threads = []
for i in range(thread_num):
start = i * offset
end = (i+1) * offset if (i+1) * offset < file_count else -1
thread = threading.Thread(target=self.calculate_block, args=(start, end))
threads.append(thread)
for t in threads:
t.setDaemon(True)
t.start()
t.join()
这里我使用的 2 个线程进行计算, 实际测试速度虽然不是单线程的 2 倍,但是计算的时间上是有所优化的。
【数据存储】
在了解公式后, 便可迭代文件进行带入计算, 计算结果这里我采用的是将其写入原文件, 在列末尾追加新的 5 列:
EMA26
(26根据传参而定)EMA12
(12根据传参而定)DIF
DEA
MACD
【数据验证】
完成计算后, 有必要对计算结果进行验证, 这里, 我简单写了一个输出来进行简单验证, 该部分代码如下:
def verify_calculate(self, code):
try:
df = pd.read_csv(self.file_prefix + str(code) + '.csv', encoding='gbk')
except:
print('文件%s.csv打开失败' % code)
return
df = df[df.日期 > self.end_date]
df = df[::-1]
mid = []
dates = []
difs = []
deas = []
macds = []
color_macd = []
for index, row in df.iterrows():
mid.append(0)
dates.append(row['日期'])
difs.append(row['DIF'])
deas.append(row['DEA'])
macds.append(row['MACD'])
if row['MACD'] > 0:
color_macd.append('red')
else:
color_macd.append('green')
# 绘制图表
fig = plt.figure(dpi=128, figsize=(100, 6))
plt.plot(dates, mid, c='blue')
plt.plot(dates, difs, c='yellow')
plt.plot(dates, deas, c='black')
plt.bar(dates, macds, color=color_macd)
plt.xticks(fontsize=5)
plt.xlabel('', fontsize=5)
fig.autofmt_xdate()
plt.show()
在对股票 '000002'
进行验证, 验证结果如下(上图为通达信MACD截图, 下图为程序输出的图片, 时间为2018-01-01
~ 2019-04-26
):
可以看到图形基本一致, 但是同样对股票 000001
进行验证, 结果如下:
此时, 两图形基本没有重合, 原因是因为计算 MACD
时, 第一天的 EMA
并不是前一天计算的,同样 DEA
也是, 而这里数据只计算了 2018-01-01 ~ 2010-04-26
, 所以前面基本一致的时候, 是因为第一日刚好相近导致后面计算偏差不大, 对此, 应该不设置截止日期以保证数据的可靠性, 我将时间设置到了 2014-01-01
, 重新验证 000001
得到如下:
然后再随机抽取一直股票进行验证, 如下为 600702
的结果
这里主要是对 MACD
的分析, 所以以上计算的数据实际上只要用到 MACD
, 对 MACD
柱满足以下形态的股票进行分析:
该形态需满足以下特点:
- 出现连续的红色块(MACD > 0)
- 紧接着出现连续蓝色块(MACD < 0)
- 蓝色块小于第一个红色块
- 蓝色块后面跟着一个红色块, 且后一红色块大于前一红色块
在程序设计时, 使用的是迭代数据行, 通过 if
判断, 来定位上述的 3 个块, 在第三块大于第一块时即为符合条件的形态;大致思路是, 定义 3 个变量来标记 3 块的合计值:
- red_1
- green_1
- red_2
判断三个块的条件分别为:
- 块 1 :
green_1 == 0 and red_2 == 0
- 块 2 :
red_1 != 0 and red_2 == 0
- 块 3 :
red_1 != 0 and red_2 != 0 and green_1 != 0
no. | param | type | mean | format | default | necessary | demo |
---|---|---|---|---|---|---|---|
1 | file_prefix |
str |
日线文件前缀 | / | None |
True |
'F:\\files\\sharesDatas\\kline\\' |
2 | end_date |
str |
截止日期 | yyyy-mm-dd |
'0000-00-00' | False |
'2019-01-01' |
3 | short |
int |
短期的天数 | / | 12 |
False |
12 |
4 | long |
int |
长期的天数 | / | 26 |
False |
26 |
5 | mid |
int |
计算DEA 时, DIF 的EMA 天数 |
/ | 9 |
False |
9 |
6 | count_max |
int |
判断后面的多少天涨跌情况 | / | 5 |
False |
10 |
7 | count_border |
int |
设置最大天数内至少多少天涨才符合 | / | 3 |
False |
6 |
calculate_noe
- 计算单只股票的
MACD
、DIF
、EMA
、DEA
- 需要参数:
code
: 股票的代码
- 返回值:
None
- 计算单只股票的
calculate_all_by_thread
- 通过多线程计算所有股票
- 需要参数:
thread_num
: 线程数量
- 返回值:
None
calculate_block
:- 计算指定代码块的股票
- 需要参数:
start
:块下标开始end
: 块下标结束
- 返回值:
None
verify_calculate
- 验证计算的结果
- 需要参数:
code
: 股票的代码
- 返回值:
None
analyze_macd_one
:- 分析单只股票 的
macd
- 需要参数:
code
: 股票的代码
- 返回值:
None
- 分析单只股票 的
analyze_macd_by_thread
:- 通过多线程分析所有股票
- 需要参数:
thread_num
: 线程数量
- 返回值:
None
analyze_block
:- 分析指定代码块的股票
- 需要参数:
start
:块下标开始end
: 块下标结束
- 返回值:
None
show_res
:- 输出分析结果并写入相关文件
- 需要参数:
None
- 返回值:
None
【某次结果截图】
计算 MACD 时候的传参均为默认, 即通达信公式中的默认值, 对上述情况进行分析得到的结果是, 后 10 天内至少 6 天比符合条件时涨了的比例约为 55% , 而如果将参数调整为 5 天内至少 3 天, 则比例降低到约47%, 而如果不是 MACD 的值计算有误的话, 可见这个 MACD
的指标并不是很可靠, 但可以调整计算 MACD
的参数的值
项目地址: https://github.com/CatsJuice/MACD-Analyze
直接 clone:
git clone https://github.com/CatsJuice/MACD-Analyze.git