本次是完成一个股票的计算项目,其主要功能如下:
(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函数可以在不存在该文件时创建文件,若存在则直接打开,并进行覆盖写入。
源代码如下:
#By ZUEL_Ronin
# 该模块用于爬取网站
import os
import requests
from fake_useragent import UserAgent
class Download_HistoryStock: # 爬取股票历史数据
def __init__(self, code, startDate, endDate): # URL 和 headers设置
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库,其安装方式很简单,只需要
pip install stockstats
该库的具体介绍比较少,很多用法还需摸索,其源码可以在csdn上找到,共一千多行代码左右,感兴趣的可以看看(顺便学习一下金融知识)。
首先该库可以直接读取csv文件,但是我导入时会报错如下
utf-8编码类型不对,然后我去查看了那个stockstats的源码发现(对,就是那个一千多行,看到我头疼的源码),它对csv文件的处理非常死板,标题必须是英文,而我的标题是中文,其次,列名必须严格按照date,open,close,high,low,volume来编写,因此我们需要对csv文件做一个预处理。
这部分主要就是删除多余的列,并将第一行替换为英文内容,多余的列可以直接用drop()来删除,但是根据网上删除行时一直发生错误,也无法直接在最前面加入标题行(注:网上很多对于csv文件的操作似乎都会产生一点问题,也没有太多资料进行查找)。于是,我选择新建一个csv文件,输入标题行后再循环输入该文件的除标题行以外的正文内容,最终完成对csv文件的预处理。
预处理源码如下
#By ZUEL_Ronin
# 该模块用于对爬取的数据进行预处理
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:
# 将修改后的文件保存在 code_ForStockStats.csv中
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文件的代码如下:
stockStat = stockstats.StockDataFrame.retype(pd.read_csv('000001.csv'))
调用计算代码如下:
stockStat[['macd']]
但是如今网络上似乎找不到输出具体值的方法,如果直接print()由于输出无法通过索引获得,会导致无法将数据导出计算。于是,我又去读了一下那个长达一千两百行的源码(所以强烈建议大家多读读,能治疗头发过多),最终发现可以通过
stockStat[['macd']].get("macd")
前后都需要说明是哪一个函数,第一次调用get时,又由于前面没有加上macd,导致报错。该函数后面可以加上索引进行导出。
注:macd外面有两个[ ]。
剩下的就是调用stockstats进行数据处理,并通过matplotlib.pyplot库来进行展示。代码如下:
# By ZUEL_Ronin
# 该模块用于计算和展示相关股票指数
import matplotlib.pyplot as plt
import stockstats # 该库在show模块用到,因主要影响该模块,故放在该模块中
def MACD(stockStat): #计算MACD并展示
stockStat[['macd']].plot(subplots=True, figsize=(15, 10), grid=True)
plt.show()
def BOLL(stockStat): #计算BOLL以及其上轨线指标和下轨线指标并展示
stockStat[['boll', 'boll_ub', 'boll_lb']].plot(subplots=True, figsize=(15, 10), grid=True)
plt.show()
def ADX(stockStat): #计算ADX并展示
stockStat[['adx']].plot(subplots=True, figsize=(15, 10), grid=True)
plt.show()
def RSI(stockStat): #计算RSI并展示
stockStat[['rsi_6', 'rsi_12']].plot(subplots=True, figsize=(15, 10), grid=True)
plt.show()
def CCI(stockStat): #计算CCI并展示
stockStat[['cci', 'cci_20']].plot(subplots=True, figsize=(15, 10), grid=True)
plt.show()
def MA(stockStat): #计算MA30和MA60并展示
stockStat[['close_30_sma', 'close_60_sma']].plot(subplots=True, figsize=(15, 10), grid=True)
plt.show()
三、交互页面制作
该部分主要是利用tkinter库进行页面的设置,并读取用户输入的股票代码和起止日期来传导给前面几个部分进行爬取和计算,主要是要进行差错检测,避免用户其不合规的操作导致程序产生错误。(后附页面截图)
# by ZUEL_Ronin
# 该模块用于展示界面
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') # 定义窗口label组件,并显示相关文字
labelSeekStock.grid(row=0, column=0) # 按相关顺序显示label
labelStartDate = Label(self.win, text="开始时间", font='楷体 -24 bold') # 定义窗口label组件,并显示相关文字
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()
四、结果展示
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的过程,虽然最后呈现出来的比较简单,有很多写的不好的地方,欢迎各位大佬指点。