前言
本人大概是去年元旦左右开始接触量化交易的。从一开始的很多异想天开的想法,到逐渐定制交易策略,统计分析,到后面通过机器学习的多因子选股分析,经历了不少弯弯曲曲。希望和大家分享一下,也希望能找到更多的研究量化的朋友,共同学习和进步。
分析的基本对象是A股市场的历史数据。因为时间和资金量的关系,并没有去分析期货或者外汇市场。另外,考虑到A股的T+1交易规则以及手续费原因,也并没有爬取每天每隔几秒钟的实时数据,而是每天的价格信息和成交量等数据。这类数据基本上各类财经网站或者同花顺上也能获取。之所以通过爬虫爬到本地数据库进行分析,主要是考虑到工作和家庭(和视力)的原因,并没有太多的时间每时每刻盯着股票软件看,也没法同时分析3000+只股票。而是希望通过统计模型,来发现其中的规律。
这里面分析的方法都比较基本,也不涉及到诸如深度学习的东西(本人并不排斥深度学习,但从网上很多已有例子来看,在样本空间有限的情况下,过于复杂的深度学习结构容易导致模型的过拟合)。这里主要应用了一些基本的买卖策略和普通的机器学习策略。
采集的数据类型
根据需要,前期主要采集了写数值型的数据,包括:
- 股票每个财季的基本信息,这里主要是从每个财季的财报里面获取的,获取的网址是: [东方财富](http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?type=YJBB21_YJBB&token=70f12f2f4f091e459a279469fe49eca5&st=scode&sr=1&p=1&ps=50&js=var apple={pages:(tp),data: (x),font:(font)}&filter=(reportdate=2020-03-31))。 这里面获取了指定报告期的股票数据,包含同比增长,环比增长,每股收益率等等。
- 股票的每日数据,这里获得的是每只股票每天的开盘,收盘,最高,最低价,获取的网站是: 网易财经
- 股票每日资金流向数据,获取网站是 网易财经
- 后期分析的时候,额外加上了股东变化的数据,通过新浪财经获取:新浪财经
以上都是一些获取链接的例子,之后会详细描述具体爬取步骤。
数据库准备
先基本计算了下,A股所有股票每日的数据和每个财季的数据所占空间大概也就几个G的大小,所以也没必要使用复杂的分布式数据库,本地搭建个数据库也就够用了。
看了许多博客和专栏,很多人爬取股票的数据使用的是mysql database,个人觉得,对于这种结构化的行式数据,使用mysql 或者 cassandra 都是不错的选择。但本人当时恰好工作上使用 mongo 比较多,就顺手使用 mongodb 来做本地数据库了。没有太比较过 mysql 和 mongo 在读写性能的区别 (主要是读性能,因为写性能的瓶颈在于网络爬取速度),欢迎拍砖。
废话不多说了,直接上爬取流程把:
数据爬取和存储
股票的财季数据
在爬取之前,我们先分析一下上述网站能提供的资料信息,[东方财富](http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?type=YJBB21_YJBB&token=70f12f2f4f091e459a279469fe49eca5&st=scode&sr=1&p=1&ps=50&js=var apple={pages:(tp),data: (x),font:(font)}&filter=(reportdate=2020-03-31)) 链接提供的信息如图:
以上链接是给定报告期 (2020-03-31) 的股票基本信息,由于该报告期有3000+只股票,一页网页往往显示不下,所以,实际上是分页显示,这里一共有99页。上述贴图显示的是第一页信息,如需显示更多新页面,把 url 中的 p=1 改成 p=2,3,4,5… 就行了。第一页第一只股票是 平安银行 (000001)。第二页如下:
还是回到第一页显示的信息,如平安银行,有以下字段,分别解释如下(有些貌似没有统计意义的字段略过)
- scode
- sname
- securitytype
- trademarket
- latestnoticedate (这个是最后一次报告时间)
- reportdate (这个是报告期,这里是给定的 reportdate + HHMMSS, “020-03-31T00:00:00”)
- publishname (行业分类)
- firstnoticedate (这个是第一次报告时间,记住,这个是实际上的财报公告时间)
- basiceps (每股收益)
- cutbasiceps (扣除非经常性损益后的基本每股收益)
- totaloperatereve (总营收)
- ystz(营收同比增长率)
- yshz(营收环比增长率)
- parentnetprofit(净利润,个人理解为归属母公司的净利润)
- sjltz(净利润同比增长)
- sjlhz(净利润环比增长)
- roeweighted(净资产收益率)
- bps(每股净资产)
- mgjyxjje(每股现金流)
- xsmll(销售毛利率)
我们从截图看到,具体数字部分,比如 totaloperatereve 所对应的数字是编码的,可读性比较差。其对应的数字可以在每页最后部分找到:
我们在爬取数据后,需要对数字编码进行转换,并存取到数据库中。代码如下:
class NameScraper:
def __init__(self):
""" Initialization """
self.url = "http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?"
self.params = {
'type': 'YJBB21_YJBB',
'token': '70f12f2f4f091e459a279469fe49eca5',
'st': 'scode',
'sr': 1,
'p': 1,
'ps': 50,
'js': 'var apple={pages:(tp),data: (x),font:(font)}',
'filter': '(reportdate=^2020-03-31^)'
}
self.encoding = "utf_8_sig"
self.name_mgr = DBManager("name_all")
self.years = range(2015, 2020)
self.seasons = ["03-31", "06-30", "09-30", "12-31"]
self.network_max_try = 10
@staticmethod
def parse_font(fonts):
""" Since the numbers are encoded in original page, we need to parse the encoder to mapping file """
font_mapping = dict()
map_field_key = "FontMapping"
if map_field_key in fonts.keys():
for f in fonts[map_field_key]:
font_mapping[f["code"]] = f["value"]
else:
print("Error, font has not key " + map_field_key)
return font_mapping
@network_decorator
def get_response(self):
return requests.get(self.url, params=self.params).text
def get_table(self, page):
""" Get one page from site"""
self.params['p'] = page
response = self.get_response()
if response is None:
return None, None
pat = re.compile("var\sapple={pages:(\d+),data:\s+(\[.*\]),font:(.*)}")
page_all = re.search(pat, response)
if page_all is None:
print("Not found for wep page: " + str(page) + ", now exit!")
page_group = page_all.group(1)
data_group = page_all.group(2)
font_group = page_all.group(3)
debug_print("Page " + str(page) + " has " + str(page_group) + " entries!", 1)
font = json.loads(font_group)
font_dict = NameScraper.parse_font(font)
for key in fo