用来画股票 K线图 的 Python 脚本 - Jacky Liu's Blog

用来画股票 K线图 的 Python 脚本

Jacky Liu posted @ 2011年3月02日 18:40 in Python with tags python 股票 Matplotlib candlestick K线 蜡烛 stock , 25942 阅读

    ---- <补记>:

        最新的在这里: 用 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)










 

 

Avatar_small
eiei1347 说:
2011年5月09日 11:58

能给我发一份单股的k线图脚本吗?万分感谢!

eiei1347@qq.com

Avatar_small
fish1207 说:
2013年2月22日 09:23

博主,您好。能给我发份数据文件吗?
另外,你现在实时了实时数据的刷新显示么?另外能实现滚轴缩放吗?

Avatar_small
fish1207 说:
2013年2月22日 09:24

我的邮箱:zhuandi.h@qq.com

Avatar_small
Stephen Hu 说:
2015年6月14日 06:45

>> 还有首次下载网页失败的比例居然这么高,一定有问题,印象中以前不是这么慢的,

我是用超时来做的,如果超时,则sleep一定时间来重试,基本上程序开始run以后就不用管了。睡上一觉,到天亮基本上数据都下载完了 :)

Avatar_small
Jacky Liu 说:
2015年6月14日 10:05

@Stephen Hu: 重试一定需要,不然下载的数据是残的,但这不算解决问题。我这边的问题后来查明了,根源在于 DNS。因为鸡国的 DNS 并不仅仅是 DNS 而已,还附带了域名审查、封锁,以及劫持、偷换 等等贴心服务,所以不仅慢,而且会拒绝操作。我早已换了正常的 DNS,所以没这些问题了。目前 DNS 劫持的情况已经越来越严重,即使访问墙内的大型网站也会被拒绝,页面被替换成 114 的广告。所以即使手动浏览也不能使用鸡国的 DNS。

Avatar_small
Stephen Hu 说:
2015年6月14日 15:24

@Jacky Liu: 多谢!那回头我也试试改变DNS。

BTW,我只是最近在抓股票在线数据,然后分析。今早无意看到你的博客,发现我走了一条几乎和你一模一样的路 :)

python->抓取历史数据->抓取复权历史数据(因为实在没有信息来自己计算复权。。。)->存入mysql->用matplotlib画图

不过我的一切都还很简陋,你的已经很完善了。

Avatar_small
Assam Board Model Pa 说:
2022年9月10日 16:35

For the SCERT and NCERT Syllabus of Assamese Medium, Bengali Medium, English Medium, and Urdu Medium Students Studying in Government & Private Schools of the State & Central Board, Assam State School Education Board, Assam Board Model Paper Itanagar, and others have designed and suggested Assam Board 2nd Class Model Paper 2023 with Mock Test and Practice Questions for Term 1 & Term 2 Exams along with Annual Final exams of the Course.

Avatar_small
anonymous 说:
2023年8月22日 00:20

For many people this is the best solution here      see how to do it. Kissimmee Wildlife Removal

Avatar_small
anonymous 说:
2023年8月28日 15:51

These things are very important, good think so - I think so too...  Orlando Wildlife Removal

Avatar_small
anonymous 说:
2023年8月28日 15:53

You bear through a awesome vacancy. I sanity definitely quarry it moreover personally suggest to my buddys. I am self-possessed they determination be benefited from this scene. Wildlife Removal Oviedo


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter
Host by is-Programmer.com | Power by Chito 1.3.3 beta | © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee