用来画股票 K线图 的 Python 脚本 - Jacky Liu's Blog
用来画股票 K线图 的 Python 脚本
---- <补记>:
最新的在这里: 用 Python / Matplotlib 画出来的股票 K线图 (四)
下一篇在这里: 用 Python / Matplotlib 画出来的股票 K线图 (三)
---- 花了 20 个小时左右的时间才从新浪下载完复权日线数据,把复权日线表建起来。这速度也太慢了,还有首次下载网页失败的比例居然这么高,一定有问题,印象中以前不是这么慢的,下载几千只股票的数据也只有几十个页面会首次下载失败吧。但昨天晚上更新最新数据的时候把下载任务之间的延迟扩大了一些,好像好一些,速度还可以,而且失败率不高。我开的是 5 个线程,下载页面之间的间隔是 0.2 ~ 0.3 秒。
---- 另外,把那个画 K 线图的脚本贴出来。这个脚本是通过研究 Matplotlib 官网里的示例并且借助 Google,用大概 1 周的时间改出来的。简单介绍一下:
1. 由两个子图(subplot)构成,上面一个显示价格(K 线),下面一个显示成交量。
2. K 线子图可以使用线性坐标或者对数坐标(由 Plot() 函数第三个参数控制)。使用线性坐标的时候,每个单位价格区间所占高度是固定的;使用对数坐标的时候,每个单位涨幅区间(比如 10%)所占高度是固定的。成交量子图的高度总是固定,不论成交量数值大小。
3. 对 X 轴来说,每根 K 线的宽度固定,整个图形的宽度决定于行情的天数。只要把行情数据文件作为参数传递过去就可以,图片尺寸由程序自主计算。
4. 另外,figdpi 这个变量控制图片的分辨率(解析度),可以随意调大调小。上一篇文章里贴的图使用的 dpi 值是 300。另外,X 轴和 Y 轴上的坐标点也是程序自主决定的。
---- 整个脚本还是一个 work-in-progress,目前的局限主要在于使用对数坐标时,Y 轴坐标点的确定。前一篇里所贴的那个图,可以看见价格上限在 20 块左右,如果换一只价格 90 块上下的股票,或者用来画几千点的指数行情,那 Y 轴的坐标点就会太密集。解决办法是根据取值区间来自主选择合适的 Y 轴坐标间距,但是这个目前还没有做。
---- 任何意见或建议都许多欢迎 !
---- <补记>:已经有了大幅改进的版本,在下一篇里。
# -*- coding: utf-8 -*- import sys import pickle import math import datetime import matplotlib matplotlib.use("WXAgg", warn=True) # 这个要紧跟在 import matplotlib 之后,而且必须安装了 wxpython 2.8 才行。 import matplotlib.pyplot as pyplot import numpy from matplotlib.ticker import FixedLocator, MultipleLocator, LogLocator, FuncFormatter, NullFormatter, LogFormatter def Plot(pfile, figpath, useexpo=True): ''' pfile 指明存放绘图数据的 pickle file,figpath 指定图片需存放的路径 ''' fileobj= open(name=pfile, mode='rb') pdata= pickle.load(fileobj) fileobj.close() # 计算图片的尺寸(单位英寸) # 注意:Python2 里面, "1 / 10" 结果是 0, 必须写成 "1.0 / 10" 才会得到 0.1 #================================================================================================================================================== length= len(pdata[u'日期']) # 所有数据的长度,就是天数 highest_price= max(pdata[u'最高']) # 最高价 lowest_price= min( [plow for plow in pdata[u'最低'] if plow != None] ) # 最低价 yhighlim_price= round(highest_price+50, -2) # K线子图 Y 轴最大坐标 ylowlim_price= round(lowest_price-50, -2) # K线子图 Y 轴最小坐标 xfactor= 10.0/230.0 # 一条 K 线的宽度在 X 轴上所占距离(英寸) yfactor= 0.3 # Y 轴上每一个距离单位的长度(英寸),这个单位距离是线性坐标和对数坐标通用的 if useexpo: # 要使用对数坐标 expbase= 1.1 # 底数,取得小一点,比较接近 1。股价 3 元到 4 元之间有大约 3 个单位距离 ymulti_price= math.log(yhighlim_price, expbase) - math.log(ylowlim_price, expbase) # 价格在 Y 轴上的 “份数” else: ymulti_price= (yhighlim_price - ylowlim_price) / 100 # 价格在 Y 轴上的 “份数” ymulti_vol= 3.0 # 成交量部分在 Y 轴所占的 “份数” ymulti_top= 0.2 # 顶部空白区域在 Y 轴所占的 “份数” ymulti_bot= 0.8 # 底部空白区域在 Y 轴所占的 “份数” xmulti_left= 10.0 # 左侧空白区域所占的 “份数” xmulti_right= 3.0 # 右侧空白区域所占的 “份数” xmulti_all= length + xmulti_left + xmulti_right xlen_fig= xmulti_all * xfactor # 整个 Figure 的宽度 ymulti_all= ymulti_price + ymulti_vol + ymulti_top + ymulti_bot ylen_fig= ymulti_all * yfactor # 整个 Figure 的高度 rect_1= (xmulti_left/xmulti_all, (ymulti_bot+ymulti_vol)/ymulti_all, length/xmulti_all, ymulti_price/ymulti_all) # K线图部分 rect_2= (xmulti_left/xmulti_all, ymulti_bot/ymulti_all, length/xmulti_all, ymulti_vol/ymulti_all) # 成交量部分 # 建立 Figure 对象 #================================================================================================================================================== figfacecolor= 'white' figedgecolor= 'black' figdpi= 600 figlinewidth= 1.0 figobj= pyplot.figure(figsize=(xlen_fig, ylen_fig), dpi=figdpi, facecolor=figfacecolor, edgecolor=figedgecolor, linewidth=figlinewidth) # Figure 对象 #================================================================================================================================================== #================================================================================================================================================== #======= 成交量部分 #================================================================================================================================================== #================================================================================================================================================== # 添加 Axes 对象 #================================================================================================================================================== axes_2= figobj.add_axes(rect_2, axis_bgcolor='black') axes_2.set_axisbelow(True) # 网格线放在底层 # 改变坐标线的颜色 #================================================================================================================================================== for child in axes_2.get_children(): if isinstance(child, matplotlib.spines.Spine): child.set_color('lightblue') # 得到 X 轴 和 Y 轴 的两个 Axis 对象 #================================================================================================================================================== xaxis_2= axes_2.get_xaxis() yaxis_2= axes_2.get_yaxis() # 设置两个坐标轴上的 grid #================================================================================================================================================== xaxis_2.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) xaxis_2.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1) yaxis_2.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) yaxis_2.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1) #================================================================================================================================================== #======= 绘图 #================================================================================================================================================== xindex= numpy.arange(length) # X 轴上的 index,一个辅助数据 zipoc= zip(pdata[u'开盘'], pdata[u'收盘']) up= numpy.array( [ True if po < pc and po != None else False for po, pc in zipoc] ) # 标示出该天股价日内上涨的一个序列 down= numpy.array( [ True if po > pc and po != None else False for po, pc in zipoc] ) # 标示出该天股价日内下跌的一个序列 side= numpy.array( [ True if po == pc and po != None else False for po, pc in zipoc] ) # 标示出该天股价日内走平的一个序列 volume= pdata[u'成交量'] rarray_vol= numpy.array(volume) volzeros= numpy.zeros(length) # 辅助数据 # XXX: 如果 up/down/side 各项全部为 False,那么 vlines() 会报错。 if True in up: axes_2.vlines(xindex[up], volzeros[up], rarray_vol[up], color='red', linewidth=3.0, label='_nolegend_') if True in down: axes_2.vlines(xindex[down], volzeros[down], rarray_vol[down], color='green', linewidth=3.0, label='_nolegend_') if True in side: axes_2.vlines(xindex[side], volzeros[side], rarray_vol[side], color='0.7', linewidth=3.0, label='_nolegend_') # 设定 X 轴坐标的范围 #================================================================================================================================================== axes_2.set_xlim(-1, length) # 设定 X 轴上的坐标 #================================================================================================================================================== datelist= [ datetime.date(int(ys), int(ms), int(ds)) for ys, ms, ds in [ dstr.split('-') for dstr in pdata[u'日期'] ] ] # 确定 X 轴的 MajorLocator mdindex= [] # 每个月第一个交易日在所有日期列表中的 index years= set([d.year for d in datelist]) # 所有的交易年份 for y in sorted(years): months= set([d.month for d in datelist if d.year == y]) # 当年所有的交易月份 for m in sorted(months): monthday= min([dt for dt in datelist if dt.year==y and dt.month==m]) # 当月的第一个交易日 mdindex.append(datelist.index(monthday)) xMajorLocator= FixedLocator(numpy.array(mdindex)) # 确定 X 轴的 MinorLocator wdindex= [] # 每周第一个交易日在所有日期列表中的 index for d in datelist: if d.weekday() == 0: wdindex.append(datelist.index(d)) xMinorLocator= FixedLocator(numpy.array(wdindex)) # 确定 X 轴的 MajorFormatter 和 MinorFormatter def x_major_formatter_2(idx, pos=None): return datelist[idx].strftime('%Y-%m-%d') def x_minor_formatter_2(idx, pos=None): return datelist[idx].strftime('%m-%d') xMajorFormatter= FuncFormatter(x_major_formatter_2) xMinorFormatter= FuncFormatter(x_minor_formatter_2) # 设定 X 轴的 Locator 和 Formatter xaxis_2.set_major_locator(xMajorLocator) xaxis_2.set_major_formatter(xMajorFormatter) xaxis_2.set_minor_locator(xMinorLocator) xaxis_2.set_minor_formatter(xMinorFormatter) # 设定 X 轴主要坐标点与辅助坐标点的样式 for malabel in axes_2.get_xticklabels(minor=False): malabel.set_fontsize(3) malabel.set_horizontalalignment('right') malabel.set_rotation('30') for milabel in axes_2.get_xticklabels(minor=True): milabel.set_fontsize(2) milabel.set_horizontalalignment('right') milabel.set_rotation('30') # 设定 Y 轴坐标的范围 #================================================================================================================================================== maxvol= max(volume) # 注意是 int 类型 axes_2.set_ylim(0, maxvol) # 设定 Y 轴上的坐标 #================================================================================================================================================== vollen= len(str(maxvol)) yMajorLocator_2= MultipleLocator(10**(vollen-1)) yMinorLocator_2= MultipleLocator((10**(vollen-2))*5) # 确定 Y 轴的 MajorFormatter # def y_major_formatter_2(num, pos=None): # numtable= {'1':u'一', '2':u'二', '3':u'三', '4':u'四', '5':u'五', '6':u'六', '7':u'七', '8':u'八', '9':u'九', } # dimtable= {3:u'百', 4:u'千', 5:u'万', 6:u'十万', 7:u'百万', 8:u'千万', 9:u'亿', 10:u'十亿', 11:u'百亿'} # return numtable[str(num)[0]] + dimtable[vollen] if num != 0 else '0' def y_major_formatter_2(num, pos=None): return int(num) yMajorFormatter_2= FuncFormatter(y_major_formatter_2) # 确定 Y 轴的 MinorFormatter # def y_minor_formatter_2(num, pos=None): # return int(num) # yMinorFormatter_2= FuncFormatter(y_minor_formatter_2) yMinorFormatter_2= NullFormatter() # 设定 X 轴的 Locator 和 Formatter yaxis_2.set_major_locator(yMajorLocator_2) yaxis_2.set_major_formatter(yMajorFormatter_2) yaxis_2.set_minor_locator(yMinorLocator_2) yaxis_2.set_minor_formatter(yMinorFormatter_2) # 设定 Y 轴主要坐标点与辅助坐标点的样式 for malab in axes_2.get_yticklabels(minor=False): malab.set_fontsize(3) for milab in axes_2.get_yticklabels(minor=True): milab.set_fontsize(2) #================================================================================================================================================== #================================================================================================================================================== #======= K 线图部分 #================================================================================================================================================== #================================================================================================================================================== # 添加 Axes 对象 #================================================================================================================================================== axes_1= figobj.add_axes(rect_1, axis_bgcolor='black', sharex=axes_2) axes_1.set_axisbelow(True) # 网格线放在底层 if useexpo: axes_1.set_yscale('log', basey=expbase) # 使用对数坐标 # 改变坐标线的颜色 #================================================================================================================================================== for child in axes_1.get_children(): if isinstance(child, matplotlib.spines.Spine): child.set_color('lightblue') # 得到 X 轴 和 Y 轴 的两个 Axis 对象 #================================================================================================================================================== xaxis_1= axes_1.get_xaxis() yaxis_1= axes_1.get_yaxis() # 设置两个坐标轴上的 grid #================================================================================================================================================== xaxis_1.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) xaxis_1.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1) yaxis_1.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) yaxis_1.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1) #================================================================================================================================================== #======= 绘图 #================================================================================================================================================== # 绘制 K 线部分 #================================================================================================================================================== rarray_open= numpy.array(pdata[u'开盘']) rarray_close= numpy.array(pdata[u'收盘']) rarray_high= numpy.array(pdata[u'最高']) rarray_low= numpy.array(pdata[u'最低']) # XXX: 如果 up, down, side 里有一个全部为 False 组成,那么 vlines() 会报错。 if True in up: axes_1.vlines(xindex[up], rarray_low[up], rarray_high[up], color='red', linewidth=0.6, label='_nolegend_') axes_1.vlines(xindex[up], rarray_open[up], rarray_close[up], color='red', linewidth=3.0, label='_nolegend_') if True in down: axes_1.vlines(xindex[down], rarray_low[down], rarray_high[down], color='green', linewidth=0.6, label='_nolegend_') axes_1.vlines(xindex[down], rarray_open[down], rarray_close[down], color='green', linewidth=3.0, label='_nolegend_') if True in side: axes_1.vlines(xindex[side], rarray_low[side], rarray_high[side], color='0.7', linewidth=0.6, label='_nolegend_') axes_1.vlines(xindex[side], rarray_open[side], rarray_close[side], color='0.7', linewidth=3.0, label='_nolegend_') # 绘制均线部分 #================================================================================================================================================== rarray_1dayave= numpy.array(pdata[u'1日权均']) rarray_5dayave= numpy.array(pdata[u'5日均']) rarray_30dayave= numpy.array(pdata[u'30日均']) axes_1.plot(xindex, rarray_1dayave, 'o-', color='white', linewidth=0.1, markersize=0.7, markeredgecolor='white', markeredgewidth=0.1) # 1日加权均线 axes_1.plot(xindex, rarray_5dayave, 'o-', color='yellow', linewidth=0.1, markersize=0.7, markeredgecolor='yellow', markeredgewidth=0.1) # 5日均线 axes_1.plot(xindex, rarray_30dayave, 'o-', color='green', linewidth=0.1, markersize=0.7, markeredgecolor='green', markeredgewidth=0.1) # 30日均线 # 设定 X 轴坐标的范围 #================================================================================================================================================== axes_1.set_xlim(-1, length) # 先设置 label 位置,再将 X 轴上的坐标设为不可见。因为与 成交量子图 共用 X 轴 #================================================================================================================================================== # 设定 X 轴的 Locator 和 Formatter xaxis_1.set_major_locator(xMajorLocator) xaxis_1.set_major_formatter(xMajorFormatter) xaxis_1.set_minor_locator(xMinorLocator) xaxis_1.set_minor_formatter(xMinorFormatter) # 将 X 轴上的坐标设为不可见。 for malab in axes_1.get_xticklabels(minor=False): malab.set_visible(False) for milab in axes_1.get_xticklabels(minor=True): milab.set_visible(False) # 用这一段效果也一样 # pyplot.setp(axes_1.get_xticklabels(minor=False), visible=False) # pyplot.setp(axes_1.get_xticklabels(minor=True), visible=False) # 设定 Y 轴坐标的范围 #================================================================================================================================================== axes_1.set_ylim(ylowlim_price, yhighlim_price) # 设定 Y 轴上的坐标 #================================================================================================================================================== if useexpo: # 主要坐标点 #----------------------------------------------------- yMajorLocator_1= LogLocator(base=expbase) yMajorFormatter_1= NullFormatter() # 设定 X 轴的 Locator 和 Formatter yaxis_1.set_major_locator(yMajorLocator_1) yaxis_1.set_major_formatter(yMajorFormatter_1) # 设定 Y 轴主要坐标点与辅助坐标点的样式 # for mal in axes_1.get_yticklabels(minor=False): # mal.set_fontsize(3) # 辅助坐标点 #----------------------------------------------------- minorticks= range(int(ylowlim_price), int(yhighlim_price)+1, 100) yMinorLocator_1= FixedLocator(numpy.array(minorticks)) # 确定 Y 轴的 MinorFormatter def y_minor_formatter_1(num, pos=None): return str(num/100.0) + '0' yMinorFormatter_1= FuncFormatter(y_minor_formatter_1) # 设定 X 轴的 Locator 和 Formatter yaxis_1.set_minor_locator(yMinorLocator_1) yaxis_1.set_minor_formatter(yMinorFormatter_1) # 设定 Y 轴主要坐标点与辅助坐标点的样式 for mil in axes_1.get_yticklabels(minor=True): mil.set_fontsize(3) else: # 如果使用线性坐标,那么只标主要坐标点 yMajorLocator_1= MultipleLocator(100) def y_major_formatter_1(num, pos=None): return str(num/100.0) + '0' yMajorFormatter_1= FuncFormatter(y_major_formatter_1) # 设定 Y 轴的 Locator 和 Formatter yaxis_1.set_major_locator(yMajorLocator_1) yaxis_1.set_major_formatter(yMajorFormatter_1) # 设定 Y 轴主要坐标点与辅助坐标点的样式 for mal in axes_1.get_yticklabels(minor=False): mal.set_fontsize(3) # 保存图片 #================================================================================================================================================== figobj.savefig(figpath, dpi=figdpi, facecolor=figfacecolor, edgecolor=figedgecolor, linewidth=figlinewidth) if __name__ == '__main__': Plot(pfile=sys.argv[1], figpath=sys.argv[2], useexpo=True)
2011年5月09日 11:58
能给我发一份单股的k线图脚本吗?万分感谢!
eiei1347@qq.com
2013年2月22日 09:23
博主,您好。能给我发份数据文件吗?
另外,你现在实时了实时数据的刷新显示么?另外能实现滚轴缩放吗?
2013年2月22日 09:24
我的邮箱:zhuandi.h@qq.com
2015年6月14日 06:45
>> 还有首次下载网页失败的比例居然这么高,一定有问题,印象中以前不是这么慢的,
我是用超时来做的,如果超时,则sleep一定时间来重试,基本上程序开始run以后就不用管了。睡上一觉,到天亮基本上数据都下载完了 :)
2015年6月14日 10:05
@Stephen Hu: 重试一定需要,不然下载的数据是残的,但这不算解决问题。我这边的问题后来查明了,根源在于 DNS。因为鸡国的 DNS 并不仅仅是 DNS 而已,还附带了域名审查、封锁,以及劫持、偷换 等等贴心服务,所以不仅慢,而且会拒绝操作。我早已换了正常的 DNS,所以没这些问题了。目前 DNS 劫持的情况已经越来越严重,即使访问墙内的大型网站也会被拒绝,页面被替换成 114 的广告。所以即使手动浏览也不能使用鸡国的 DNS。
2015年6月14日 15:24
@Jacky Liu: 多谢!那回头我也试试改变DNS。
BTW,我只是最近在抓股票在线数据,然后分析。今早无意看到你的博客,发现我走了一条几乎和你一模一样的路 :)
python->抓取历史数据->抓取复权历史数据(因为实在没有信息来自己计算复权。。。)->存入mysql->用matplotlib画图
不过我的一切都还很简陋,你的已经很完善了。