本次是完成一个股票的计算项目,其主要功能如下:

(1)输入股票代码、起止日期后下载股票的日期、股票代码、名称、收盘价、最高价、最低价、开盘价等一系列基本股票数据。

(2)计算异同移动平均线指标(MACD)、布林线(BOLL)的上轨线指标和下轨线指标、平均趋向指标(ADX)、相对强弱指标(RSI)、顺势指标(CCI)、30日和60日的移动平均线指标(MA)等并进行展示。

(3)设计交互界面。

(原项目比这个麻烦一点,但是不符合实用性,并且由于连接了mysql数据库无法普遍使用,我对此做了一点小小更改)

写在前面:

        第一次写博客,做的不好多多包涵。本题目来源于个人的一个课程设计,进行了一定改编。

        其实这是一个很简单的小项目,特别是做完去回看自己写的代码,会觉得其实也并没有什么太难的地方,但是在自己刚开始写的时候,包括学习爬虫、csv文件的处理、mysql的报错、stockstats的函数等也花费了大量时间,仅以此文记录一下学习生涯。

一、获得股票数据

        首先关于股票数据,我选择某财经网站进行爬取,首先因为其有专门的历史数据页面,并且通过URL格式固定。

        其次,相比于东方财富有反爬虫部分,该网站并未对爬虫有较多的限制,更加方便爬取。(由于版权问题,代码中的URL已被删除,若需要请自行加入)

        关于爬虫部分,首先新建一个GetData模块来放置爬虫类,命名为Download_HistoryStock,在建立该类的self函数时,我们要传入code(股票代码),startDate(开始爬取的时间),endDate(截止时间)三个变量,而这三个变量我们会在交互页面通过用户输入传入,以此满足用户需求。而爬虫的headers只需要打开网页后按F12,选择网络,点击html的那个名称,并拉到最下面则可以查看自己的headers。

        第二个函数即Download(数据下载)函数,为了避免在当前文件夹下数据杂乱,我首先新建了一个文件夹“数据”用于存放下载的股票历史数据。在调试过程中我发现,如果想要读取当前文件夹下的其他文件夹中的文件无法直接打开,open函数只能读取当前文件夹或绝对路径,故我先设置了一个basepath来存放当前文件夹的路径(其中调用了os模块的path.dirname,主要功能是返回当前文件夹的路径)再加上“\\数据”对存放数据的文件夹进行文件读取。接下来使用os.path.exists来先判断一下“数据”文件夹是否存在,如果不存在则使用os.makedirs来创建文件夹。

        接下来就是对于网页的爬取部分,通过网页代码中找到网易财经历史数据的URL,通过观察发现网易财经的URL中包含了起止时间和股票代码一共三个会发生改变的变量,故我们将self的三个变量进行替换,即可完成对任意股票URL的爬取,随后我们使用request.get获得网页的内容,并通过循环将其读取到数据文件夹 的“code.csv”文件中。open函数可以在不存在该文件时创建文件,若存在则直接打开,并进行覆盖写入。

        源代码如下:

 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
import os

import requests

from fake_useragent import UserAgent

class Download_HistoryStock:

def __init__(self, code, startDate, endDate):

self.code = code

self.startDate = startDate

self.endDate = endDate

self.downloadURL =

print(self.downloadURL)

self.headers = {

"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 Edg/100.0.1185.44"

}

def Download(self):

path = "数据"

basepath = os.path.dirname(__file__)

if not os.path.exists(path):

os.makedirs(path)

DownloadURL =

data = requests.get(DownloadURL)

fileHistoryStock = open(basepath + "\\" + '数据' + '\\' + self.code + '.csv', 'wb')

for chunk in data.iter_content(chunk_size=10000):

if chunk:

fileHistoryStock.write(chunk)

print('股票', self.code, '历史数据爬取成功,已存到数据文件夹中的' + self.code + ".csv")

fileHistoryStock.close()

        来看一下执行的结果,以晨光股份(603899)为例:

        有一点美中不足的是在股票代码中,第一个字符会出现 ‘ ,应该是页面源代码的问题,在后续数据处理中可以用strip()函数将这个删除。

 二、数据处理并生成图像

        该部分在第一次看到我觉得非常难算,直到发现了一些金融类的python库也可帮忙解决,本次使用的是stockstats库,其安装方式很简单,只需要

1
pip install stockstats

        该库的具体介绍比较少,很多用法还需摸索,其源码可以在csdn上找到,共一千多行代码左右,感兴趣的可以看看(顺便学习一下金融知识)。

        首先该库可以直接读取csv文件,但是我导入时会报错如下

         utf-8编码类型不对,然后我去查看了那个stockstats的源码发现(对,就是那个一千多行,看到我头疼的源码),它对csv文件的处理非常死板,标题必须是英文,而我的标题是中文,其次,列名必须严格按照date,open,close,high,low,volume来编写,因此我们需要对csv文件做一个预处理。

        这部分主要就是删除多余的列,并将第一行替换为英文内容,多余的列可以直接用drop()来删除,但是根据网上删除行时一直发生错误,也无法直接在最前面加入标题行(注:网上很多对于csv文件的操作似乎都会产生一点问题,也没有太多资料进行查找)。于是,我选择新建一个csv文件,输入标题行后再循环输入该文件的除标题行以外的正文内容,最终完成对csv文件的预处理。

预处理源码如下

 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
import csv

import pandas as pd

import os

def CsvChangeToStockStats(code):

basepath = os.path.dirname(__file__)

data = pd.read_csv(basepath + '\\' + '数据' + '\\' + code + '.csv', encoding="gb2312")

data = data.sort_values(axis=0, by="日期", ascending=True)

data = data.drop(['股票代码', '名称', '前收盘', '涨跌额', '涨跌幅', '换手率', '成交金额', '总市值', '流通市值'], axis=1)

data['收盘价'], data['最高价'], data['最低价'], data['开盘价'] = data['开盘价'], data['收盘价'], data['最高价'], data['最低价']

data.to_csv(basepath + '\\' + '数据' + '\\' + code + "_backup.csv", index=False, encoding="gb2312")

num = 1

headline = ['date', 'open', 'close', 'high', 'low', 'volume']

with open(basepath + '\\' + '数据' + '\\' + code + '_ForStockStats.csv', 'w', newline='') as csvOutFile:

with open(basepath + '\\' + '数据' + '\\' + code + '_backup.csv', 'r', newline='') as csvInput:

reader = csv.reader(csvInput)

for a in reader:

if num == 1:

num = num + 1

csvWrite = csv.writer(csvOutFile)

csvWrite.writerow(headline)

else:

csvWrite = csv.writer(csvOutFile)

csvWrite.writerow(a)

csvInput.close()

csvOutFile.close()

os.remove(basepath + '\\' + '数据' + '\\' + code + "_backup.csv")

        处理完之后则可直接调用stockstats库进行计算相关指标,stockstats库功能非常强大,调用csv文件的代码如下:

1
stockStat = stockstats.StockDataFrame.retype(pd.read_csv('000001.csv'))

        调用计算代码如下:

1
stockStat[['macd']]

        但是如今网络上似乎找不到输出具体值的方法,如果直接print()由于输出无法通过索引获得,会导致无法将数据导出计算。于是,我又去读了一下那个长达一千两百行的源码(所以强烈建议大家多读读,能治疗头发过多),最终发现可以通过

1
stockStat[['macd']].get("macd")

        前后都需要说明是哪一个函数,第一次调用get时,又由于前面没有加上macd,导致报错。该函数后面可以加上索引进行导出。

        注:macd外面有两个[ ]。

        剩下的就是调用stockstats进行数据处理,并通过matplotlib.pyplot库来进行展示。代码如下:

 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
import matplotlib.pyplot as plt

import stockstats

def MACD(stockStat):

stockStat[['macd']].plot(subplots=True, figsize=(15, 10), grid=True)

plt.show()

def BOLL(stockStat):

stockStat[['boll', 'boll_ub', 'boll_lb']].plot(subplots=True, figsize=(15, 10), grid=True)

plt.show()

def ADX(stockStat):

stockStat[['adx']].plot(subplots=True, figsize=(15, 10), grid=True)

plt.show()

def RSI(stockStat):

stockStat[['rsi_6', 'rsi_12']].plot(subplots=True, figsize=(15, 10), grid=True)

plt.show()

def CCI(stockStat):

stockStat[['cci', 'cci_20']].plot(subplots=True, figsize=(15, 10), grid=True)

plt.show()

def MA(stockStat):

stockStat[['close_30_sma', 'close_60_sma']].plot(subplots=True, figsize=(15, 10), grid=True)

plt.show()

三、交互页面制作

        该部分主要是利用tkinter库进行页面的设置,并读取用户输入的股票代码和起止日期来传导给前面几个部分进行爬取和计算,主要是要进行差错检测,避免用户其不合规的操作导致程序产生错误。(后附页面截图)

  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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
import os

import CalculateStock

import tkinter.messagebox

import pandas as pd

from tkinter import *

import GetData

import CsvChangeForStockStats

class showStockStats:

def __init__(self):

self.win = Tk()

self.win.title("股票相应趋势图分析")

self.txtSeekStock = StringVar()

self.txtStartDate = StringVar()

self.txtEndDate = StringVar()

self.basepath = os.path.dirname(__file__)

def createLabel(self):

labelSeekStock = Label(self.win, text="请输入查询股票代码", font='楷体 -24 bold')

labelSeekStock.grid(row=0, column=0)

labelStartDate = Label(self.win, text="开始时间", font='楷体 -24 bold')

labelStartDate.grid(row=1, column=0)

labelEndDate = Label(self.win, text="结束时间", font='楷体 -24 bold')

labelEndDate.grid(row=1, column=2)

labelPs = Label(self.win, text='注1:时间格式如下 20220421 需严格按照本格式输入,且若是修改日期需要重新下载\n'

'注2:输入股票代码,起止时间后,需先进行数据下载,才能展现股票指标,\n '

'若使用结束后不需要该数据,可点击删除,\n否则将保留于当前文件夹下的数据文件夹中。', font='楷体 -24 bold')

labelPs.grid(row=5,columnspan=5)

def createTxt(self):

EntrySeekStock = Entry(self.win, textvariable=self.txtSeekStock, width=24, font='楷体 -24')

EntrySeekStock.grid(row=0, column=1)

EntryStartDate = Entry(self.win, textvariable=self.txtStartDate, width=24, font='楷体 -24')

EntryStartDate.grid(row=1, column=1)

EntryEndDate = Entry(self.win, textvariable=self.txtEndDate, width=24, font='楷体 -24')

EntryEndDate.grid(row=1, column=3)

def getCode(self):

return str(self.txtSeekStock.get())

def getStartDate(self):

return str(self.txtStartDate.get())

def getEndDate(self):

return str(self.txtEndDate.get())

def MACD_Show(self):

try:

code = self.txtSeekStock.get()

stockStat = CalculateStock.stockstats.StockDataFrame.retype(

pd.read_csv(self.basepath + '/' + '数据' + '/' + str(code) + '_ForStockStats.csv'))

CalculateStock.MACD(stockStat)

except:

tkinter.messagebox.showerror('错误', "数据未下载")

def BOLL_Show(self):

try:

code = self.txtSeekStock.get()

stockStat = CalculateStock.stockstats.StockDataFrame.retype(

pd.read_csv(self.basepath + '\\' + '数据' + '\\' + str(code) + '_ForStockStats.csv'))

CalculateStock.BOLL(stockStat)

except:

tkinter.messagebox.showerror('错误', "数据未下载")

def ADX_Show(self):

try:

code = self.txtSeekStock.get()

stockStat = CalculateStock.stockstats.StockDataFrame.retype(

pd.read_csv(self.basepath + '\\' + '数据' + '\\' + str(code) + '_ForStockStats.csv'))

CalculateStock.ADX(stockStat)

except:

tkinter.messagebox.showerror('错误', "数据未下载")

def RSI_Show(self):

try:

code = self.txtSeekStock.get()

stockStat = CalculateStock.stockstats.StockDataFrame.retype(

pd.read_csv(self.basepath + '\\' + '数据' + '\\' + str(code) + '_ForStockStats.csv'))

CalculateStock.RSI(stockStat)

except:

tkinter.messagebox.showerror('错误', "数据未下载")

def CCI_Show(self):

try:

code = self.txtSeekStock.get()

stockStat = CalculateStock.stockstats.StockDataFrame.retype(

pd.read_csv(self.basepath + '\\' + '数据' + '\\' + str(code) + '_ForStockStats.csv'))

CalculateStock.CCI(stockStat)

except:

tkinter.messagebox.showerror('错误', "数据未下载")

def MA_Show(self):

try:

code = self.txtSeekStock.get()

stockStat = CalculateStock.stockstats.StockDataFrame.retype(

pd.read_csv(self.basepath + '\\' + '数据' + '\\' + str(code) + '_ForStockStats.csv'))

CalculateStock.MA(stockStat)

except:

tkinter.messagebox.showerror('错误', "数据未下载")

def DownloadData(self):

try:

if str(self.getCode()) == "":

tkinter.messagebox.showerror('错误', '股票代码不能为空')

elif str(self.getStartDate()) == "":

tkinter.messagebox.showerror('错误', '开始时间不能为空')

elif str(self.getEndDate()) == '':

tkinter.messagebox.showerror('错误', '结束时间不能为空')

else:

Download = GetData.Download_HistoryStock(str(self.getCode()), self.getStartDate(), self.getEndDate())

Download.Download()

tkinter.messagebox.showinfo("成功", '下载完成,保存在当前文件夹中的数据文件夹中')

CsvChangeForStockStats.CsvChangeToStockStats(str(self.getCode()))

except:

tkinter.messagebox.showerror('错误', "股票代码输入错误,或未按要求输入时间(若确认正常,可能是由于网站维护造成爬取失败)")

def DeleteData(self):

try:

os.remove(self.basepath + "\\" + "数据" + "\\" + str(self.getCode()) + ".csv")

os.remove(self.basepath + "\\" + "数据" + "\\" + str(self.getCode()) + "_ForStockStats.csv")

tkinter.messagebox.showinfo("成功", '删除成功')

except:

tkinter.messagebox.showerror('错误', '文件已删除或不存在')

def createButtom(self):

btnMACD = Button(self.win, text='显示MACD线', command=self.MACD_Show, font='楷体 -24',

width=12)

btnMACD.grid(row=3, column=0, sticky=W)

btnBOLL = Button(self.win, text='显示BOLL线', command=self.BOLL_Show, font='楷体 -24',

width=12)

btnBOLL.grid(row=3, column=2, sticky=W)

btnADX = Button(self.win, text='显示ADX线', command=self.ADX_Show, font='楷体 -24',

width=12)

btnADX.grid(row=3, column=4, sticky=W)

btnRSI = Button(self.win, text='显示RSI线', command=self.RSI_Show, font='楷体 -24',

width=12)

btnRSI.grid(row=4, column=0, sticky=W)

btnCCI = Button(self.win, text='显示CCI线', command=self.CCI_Show, font='楷体 -24',

width=12)

btnCCI.grid(row=4, column=2, sticky=W)

btnMA = Button(self.win, text='显示MA线', command=self.MA_Show, font='楷体 -24',

width=12)

btnMA.grid(row=4, column=4, sticky=W)

btnClose = Button(self.win, text='关闭窗口', command=self.win.quit, font='楷体 -24', width=12)

btnClose.grid(row=2, column=4, sticky=W)

btnDownload = Button(self.win, text='下载数据', command=self.DownloadData, font='楷体 -24',

width=12)

btnDownload.grid(row=2, column=0, sticky=W)

btnDownload = Button(self.win, text='删除数据', command=self.DeleteData, font='楷体 -24',

width=12)

btnDownload.grid(row=2, column=2, sticky=W)

def run(self):

self.createLabel()

self.createTxt()

self.createButtom()

self.win.mainloop()

四、结果展示

1
2
3
4
5
6
7
import Show

if __name__ == '__main__':

show = Show.showStockStats()

show.run()

        本次以晨光股份(603899)为例

        交互页面如下:

输入603899,时间为2017年4月16日-2022年4月16日,首先点击下载数据

        显示各个函数线:

        MACD

        BOLL

        ADX

         RSI

        CCI

        MA

五、最后

        本文只是记录一下最近学python的过程,虽然最后呈现出来的比较简单,有很多写的不好的地方,欢迎各位大佬指点。