基于另类数据对ETF进行回测

基本背景

彭博盘前简报:7月14日

每天都会看这个公众号,而且会在盘前发,发现内容中提到的中概股和中概ETF的涨跌具有一定的相关性,早就手痒痒了想做一下回测看是否真得如此,于是,花了一整天时间… 有了这篇文章。

爬取公众号文章

爬取过程中三个麻烦的点:
1)可能会输入验证码;
2)爬取几次后,cookies会失效;
3)到最后即使手动查询想要查询的公众号,也会报错,返回了None(不知道是不是爬取次数过多的原因),不过弄到的数据已经够我用了(再早,这个公众号还没有中概股这个版块);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import os,re

from selenium import webdriver #引入自动化登录模块
import time,datetime #时间模块
import json
import requests #抓取信息
import random
from selenium.webdriver.common.by import By #引入定位寻找模块
from selenium.webdriver.common.keys import Keys #引入键盘动作模块
from selenium.webdriver import ActionChains #引入鼠标行为模块


# 微信公众号账号
user=""
#公众号密码
password=""
#设置要爬取的公众号列表
gzlist=['彭博环球财经']
#保存的结果文件地址
fileName="articles.xls"

#登录微信公众号,获取登录之后的cookies信息,并保存到本地文本中
def weChat_login():
#定义一个空的字典,存放cookies内容
post={}

#用webdriver启动谷歌浏览器
print("启动浏览器,打开微信公众号登录界面")
#CHROME_DRIVER=r"F:\\chromedriver.exe"
op = webdriver.ChromeOptions()
# macos 使用的语句
driver = webdriver.Chrome(options=op)
#driver = webdriver.Chrome(executable_path=CHROME_DRIVER)
#driver.set_window_position(0,0)
#driver.maximize_window()
#打开微信公众号登录页面
driver.get('https://mp.weixin.qq.com/')
#等待5秒钟
time.sleep(10)
print("正在输入微信公众号登录账号和密码......")
driver.find_element_by_xpath("./*//a[@class='login__type__container__select-type']").click() #鼠标放到使用账号密码登录键上

#清空账号框中的内容
driver.find_element_by_xpath('//*[@id="header"]/div[2]/div/div/div[1]/form/div[1]/div[1]/div/span/input').clear()
#自动填入登录用户名
driver.find_element_by_xpath('//*[@id="header"]/div[2]/div/div/div[1]/form/div[1]/div[1]/div/span/input').send_keys(user)
#清空密码框中的内容
driver.find_element_by_xpath('//*[@id="header"]/div[2]/div/div/div[1]/form/div[1]/div[2]/div/span/input').clear()
#自动填入登录密码
driver.find_element_by_xpath('//*[@id="header"]/div[2]/div/div/div[1]/form/div[1]/div[2]/div/span/input').send_keys(password)


#自动点击登录按钮进行登录
driver.find_element_by_xpath('//*[@id="header"]/div[2]/div/div/div[1]/form/div[4]/a').click()
# 拿手机扫二维码!
print("请拿手机扫码二维码登录公众号")
time.sleep(20)
print("登录成功")
#重新载入公众号登录页,登录之后会显示公众号后台首页,从这个返回内容中获取cookies信息
driver.get('https://mp.weixin.qq.com/')
#获取cookies
cookie_items = driver.get_cookies()

#获取到的cookies是列表形式,将cookies转成json形式并存入本地名为cookie的文本中
for cookie_item in cookie_items:
post[cookie_item['name']] = cookie_item['value']
cookie_str = json.dumps(post)
with open('cookie.txt', 'w+', encoding='utf-8') as f:
f.write(cookie_str)
print("cookies信息已保存到本地")


def get_content(query):
#query为要爬取的公众号名称
#公众号主页
url = 'https://mp.weixin.qq.com/'
#设置headers
header = {
"HOST": "mp.weixin.qq.com",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0"
}

#读取上一步获取到的cookies
with open('cookie.txt', 'r', encoding='utf-8') as f:
cookie = f.read()
cookies = json.loads(cookie)

#登录之后的微信公众号首页url变化为:https://mp.weixin.qq.com/cgi-bin/home?t=home/index&lang=zh_CN&token=1849751598,从这里获取token信息
response = requests.get(url=url, cookies=cookies)
token = re.findall(r'token=(\d+)', str(response.url))[0]

#搜索微信公众号的接口地址
search_url = 'https://mp.weixin.qq.com/cgi-bin/searchbiz?'
#搜索微信公众号接口需要传入的参数,有三个变量:微信公众号token、随机数random、搜索的微信公众号名字
query_id = {
'action': 'search_biz',
'token' : token,
'lang': 'zh_CN',
'f': 'json',
'ajax': '1',
'random': random.random(),
'query': query,
'begin': '0',
'count': '5'
}
#打开搜索微信公众号接口地址,需要传入相关参数信息如:cookies、params、headers
search_response = requests.get(search_url, cookies=cookies, headers=header, params=query_id)
#print(search_response)
#取搜索结果中的第一个公众号
lists = search_response.json().get('list')[0]
print(lists)
#获取这个公众号的fakeid,后面爬取公众号文章需要此字段
fakeid = lists.get('fakeid')

#微信公众号文章接口地址
appmsg_url = 'https://mp.weixin.qq.com/cgi-bin/appmsg?'
#搜索文章需要传入几个参数:登录的公众号token、要爬取文章的公众号fakeid、随机数random
query_id_data = {
'token': token,
'lang': 'zh_CN',
'f': 'json',
'ajax': '1',
'random': random.random(),
'action': 'list_ex',
'begin': '0',#不同页,此参数变化,变化规则为每页加5
'count': '5',
'query': '',
'fakeid': fakeid,
'type': '9'
}
#打开搜索的微信公众号文章列表页
appmsg_response = requests.get(appmsg_url, cookies=cookies, headers=header, params=query_id_data)
print(appmsg_response.json())
#获取文章总数
max_num = appmsg_response.json().get('app_msg_cnt')
print(max_num)
#print(max_num)
#每页至少有5条,获取文章总的页数,爬取时需要分页爬
num = int(int(max_num) / 5)
#print(num)
#起始页begin参数,往后每页加5
begin = 0
while num + 1 > 0 :
query_id_data = {
'token': token,
'lang': 'zh_CN',
'f': 'json',
'ajax': '1',
'random': random.random(),
'action': 'list_ex',
'begin': '{}'.format(str(begin)),
'count': '5',
'query': '',
'fakeid': fakeid,
'type': '9'
}
print('正在翻页:--------------',begin)

#获取每一页文章的标题和链接地址,并写入本地文本中
query_fakeid_response = requests.get(appmsg_url, cookies=cookies, headers=header, params=query_id_data)
print(query_fakeid_response)
fakeid_list = query_fakeid_response.json().get('app_msg_list')
#print(fakeid_list)
for item in fakeid_list:
content_link=item.get('link') #链接
content_title=item.get('title') #题目
# 我只需要盘前简报
if re.search('彭博盘前简报', content_title):
print(content_title)
update_time=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(item.get('update_time'))) #发布时间
#print(content_title+"\t"+content_link+"\t""+update_time+"\n"))
with open(fileName,'a',encoding='utf-8') as fh:
fh.write(content_title+"\t"+content_link+"\t"+update_time+"\n")
num -= 1
begin = int(begin)
begin+=5
time.sleep(2)


if __name__=='__main__':
try:
#登录微信公众号,获取登录之后的cookies信息,并保存到本地文本中
weChat_login()
#登录之后,通过微信公众号后台提供的微信公众号文章接口爬取文章
for query in gzlist:
#爬取微信公众号文章,并存在本地文本中
print("开始爬取公众号:"+query)
get_content(query)
print("爬取完成")
except Exception as e:
print(str(e))

本部分参考链接

python爬取公众号历史文章_牛客博客
python爬取公众号阅读量_Python爬取微信公众号文章以及在看阅读数_weixin_39688870的博客-CSDN博客

处理数据

需要的文件:
1)上文爬到的结果文件;
2)中概ETF 513050的行情信息,由通达信导出;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import pandas as pd
from bs4 import BeautifulSoup
import numpy as np


dt = pd.read_table('articles.csv', header = None)

# 按需处理数据
dt = dt.drop_duplicates()
dt = dt[[ True if re.search('彭博盘前简报', x) else False for x in dt[0]]]

df = pd.DataFrame(columns = ['date', 'change'])
for index in range(0, dt.shape[0]):
#print(dt[1])
url = list(dt[1])[index]
date = list(dt[2])[index][:10]
res = requests.get(url)
soup = BeautifulSoup(res.content, "html.parser")
content = soup.get_text()
#match = re.search('(中概股:.*)美国债市', content)
# 这里我只使用了金龙中国指数作为测试
match1 = re.search('金龙中国指数上涨(\d\.\d?)%', content)
match2 = re.search('金龙中国指数下跌(\d\.\d?)%', content)
row = dict()
change = 0
if match1:
change = float(match1.group(1))

if match2:
change = 0 - float(match2.group(1))

row = {'date': date, 'change': change}
df = df.append(row, ignore_index=True)

df['date'] = pd.to_datetime(df['date'])
df = df.set_index('date')
df = df[df.change != 0]
df = df.drop_duplicates()

# etf 行情
price = pd.read_table('513050.xls')
new_column = [x.strip() for x in price.columns]
price.columns = new_column
price['date'] = pd.to_datetime(price['时间'], format='%Y/%m/%d')
# 计算每日开盘买入,收盘价卖出的话,收益率多少
price['etf_change'] = (price['收盘'] - price['开盘'])/price['开盘']*100
price = price[['date', '开盘', '收盘', 'etf_change']]
price = price.set_index('date')

# 合并数据,看下信合和场内的etf日涨跌幅是否有关系?
# 这边看效果不太好,可能和有缺失值相关
dm = pd.concat([price, df], join = 'inner', axis = 1)
fig = plt.figure(figsize = (20,8))
dm.change.plot(color = 'blue',kind = 'bar', alpha = 0.2)
dm.etf_change.plot(color = 'red', kind = 'bar', alpha = 0.2)

# 输出信号文件
# 信号日期往前推一天
dm['date'] = [x - np.timedelta64(1, 'D') for x in dm.index]
dm = dm.set_index('date')
dm['instrument'] = '510500.HOF'
dm.to_csv('signal.csv')

涨跌幅是否一致,画了个图,不是很一致…
不过,大过年的,来都来了… 就再回测一把吧….

回测平台回测

使用的平台是bigquant,交易逻辑很简单,还有优化的空间:
1)当信号大于1时,买入;
2)如果买入后第二天有信号,且信号小于0才卖出,否则不卖出; 如果没有 信号,则直接卖出;

回测有 4.25% 的收益率,跑赢基准 7个点 +.+

总结

其实我觉得公众号的能源和油气也可以试试,不过观察来看华宝油气一般开盘价就是最高点,不给买入的机会…. 后边再试试;
算是对自己观察所得另类数据的一个小尝试吧,有好多东西要学习啊啊啊啊啊啊啊…..

(✪ω✪)