Jacky Liu's Blog
Python + 股票: 个股资讯表
---- 以上是 Vim 在全屏模式下的截图。全屏模式下 Vim 没有了程序标签栏,并且覆盖了 OS 的系统任务栏。用来切换全屏模式的按键定义:
nnoremap <S-F10> :silent !wmctrl -r :ACTIVE: -b toggle,fullscreen<CR>
需要外部命令 wmctrl 可用。感谢 闲耘,是从他的 vimrc 里抄来的。
---- 基本搞定了个股资讯表。内容包括新闻动态、重大事项、公司公告。
---- 表的结构:<代码> <日期> <时间> <类型> <来源> <序号> <内容> <属性>。下面一条查询语句:
SELECT t1.代码, t1.日期, t1.时间, t1.类型, t1.来源, t1.序号, t1.内容, t1.属性 FROM 个股资讯 AS t1 JOIN ( SELECT 代码, 类型, MAX(日期) AS 最大日期 FROM 个股资讯 GROUP BY 类型, 代码 ) AS t2 ON t1.代码=t2.代码 AND t1.类型=t2.类型 AND t1.日期=t2.最大日期;
耗时 20 多秒,读出 12000 多条记录。不知道是否设计有问题,怎么会这么慢。眼下没时间深究,先这样吧。
---- Plotting 模块改用多进程也基本搞定,结果比预想的还要好。我的电脑是双核的,所以在主进程之外又建立两个工作进程来执行绘图任务。粗略统计,与单进程相比的速度提升在 120% ~ 130% 之间,现在绘制一幅图形大概只需要 3 到 4 秒。
---- 但是遇到一个神秘 bug,好久才搞定。好像必须给进程对象的建立留有足够的时间,否则进程跑起来会出问题。目前在程序里加了一个延时语句 sleep(0.3),看上去是好了。因为程序结构比较复杂,没心思去重现它了,能用就行,也先这样吧。
---- 接下来要把图形再优化一下,新的内容加进去。差不多了以后开始建日线统计表。
让 Python 在下载的时候使用 IPv4 域名解析
---- 这个话题由来于我想从深交所的网站扒些信息下来,发现用 Python 下载时,每次都要 30 秒才能连上,浏览器访问却正常。于是查找原因,最后用抓包分析,发现问题出在域名解析阶段。浏览器访问时,向 DNS 服务器发送的是 A 型查询,要求做 IPv4 地址解析; 而 Python 的 urllib.request 模块发送的却是 AAAA 型查询,要求做 IPv6 地址解析,对深交所网址进行 IPv6 解析居然要花 30 秒,不知道为什么。
---- 然后就是各种 Google + 查看代码,追根溯源。不细说了,只贴结果:
def Resolver_IPv4(hostname): return socket.gethostbyname(hostname) # 此函数返回 IPv4 地址 class HTTPConnection_IPv4(http.client.HTTPConnection): def connect(self): self.sock= socket.create_connection((Resolver_IPv4(self.host), self.port), self.timeout) class HTTPHandler_IPv4(urllib.request.HTTPHandler): def http_open(self, req): return self.do_open(HTTPConnection_IPv4, req) urlopen_IPv4= urllib.request.build_opener(HTTPHandler_IPv4).open
如此以后,用这个 urlopen_IPv4() 函数来取代平常用的 urllib.request.urlopen() 函数,就可以用 IPv4 地址解析的方式来下载:
webfile= urlopen_IPv4(fullurl='http://www.szse.cn/', timeout=3)
---- 最后说一下,抓包感觉就跟作弊一样,爽!
在 Vim 里控制外部程序,Vim 与 Emacs 与 Conque
---- 上一篇里说的想拿 Vim 当自编程序的 UI,基本已经做到了:
---- 底部的那个 buffer 属于 CSA 模块,这个新写的 Vim 插件就是负责充当外部程序在 Vim 里的界面,buffer 里显示的是外部程序的输出。关于用 Vim 来当 UI 的话题在 第一篇 里讲过。
---- 果然即使用 Python 接口建立了多个线程,也是没有办法让这些线程自动刷新 Vim buffer 里的显示的,况且还有上一篇里说的那个大 Bug。最后采用比较妥协的办法,负责监听的辅助线程还是建的,但是不产生任何输出,只修改后台数据。而主线程每次向外部程序发送输入之后,就 sleep 0.3 秒然后根据后台数据刷新显示,一般情况下都能即时显示外部程序的输出。另外再定义一个手动刷新的按键。
---- 要点记一下:
1. 上一篇里说的那个 bug,是 GUI 的问题,用终端版的 Vim 没事。GUI 版如果在后台建多个线程的话,只允许其中一个线程有可见的输出(也就是造成 Gvim 窗口内显示内容改变),其它线程只能修改后台数据。
2. 如上,即使多线程也不可能自动刷新 Vim buffer 的内容,但可以显示消息(不考虑 bug 的情况下)。自动刷新 Vim buffer 目前已知的只有一个办法:用 VimScript 的 feedkeys() 函数,但是有严重局限,相当于屏蔽了用户的其它输入。这个绝对不用。
3. 辅助线程监听外部程序的输出用的是 Python 的 select.select() 函数,可以监听一个 file descriptor 列表并返回其中有内容可读的 descriptor,但有一点要注意:在产生输出的外部程序已经终止以后,会出现 file descriptor 一直有效但读不到内容的情况,如果不注意这一点就有可能让监听过程陷入死循环,知道的话只要稍微处理一下就可以了。
---- 下一步打算升级程序的 Plotting 模块。现在这个模块还是测试性质的,上次试着画了 2000 多只股票自 2010 年以来的走势,全部单线程运行,花了两个多小时。现在打算改用 Python 的 multiprocessing 模块实现。据文档里说,这个模块与 threading 模块的接口十分类似,这就好了,以前那些关于 threading 的代码可以直接拿过来抄。以后批量执行绘图任务的时候,应该能省不少时间。
----------------------------------------------------- <以下补记> -------------------------------------------------------
---- 重大发现,感谢 依云 在回复里的提醒。以上要点第 2 条其实是不成立的。Vim 本身虽然是单线程的,但是通过 Python 完全能够实现多线程的特性。上面所说的辅助线程在监听到外部程序输出以后,就可以操作 vim.buffer 对象,这时 Vim buffer 实际上已经更新了,所缺的只是刷新显示的步骤而已。只要在刷新过程的最后添上一条 Vim 命令: "redraw!" 就可以了,出来的效果实在是 nice!
---- 这样一来,我完全想不到有什么是 Vim + Python 不能做的了。想起来在去年的时候,我想拿 Vim 当自编程序的界面,因为在 Vim 里找不到与外部进程交流的办法而被逼到去看 Emacs 。。。 如今看来,Vim + Python 真是个无敌组合,我忍不住要恶心一把。。。Emacs 你去 Shǐ 吧,哈哈,你的 Elisp 再强,能强得过 Python 吗?速度比不过 Vim,功能又比不过 Python。再想想 Elisp 里头那无数个括号,是要蛋疼到怎样啊?你那 N 个窗口,图片显示功能或许很酷,可是搁不住它慢啊!这事情一慢起来还有什么意思?好在 Emacs 早就顾及这一点,于是定义了更加蛋疼的操作方式,用 Emacs 的人,想快也快不起来,啊哈哈哈 ...
---- 以上讲笑,请浮云看待。如有用 Emacs 的看到,请先冷静 10 秒开骂 。。。
---- 既然这样,Conque 这个 Vim 插件就好拿出来再说一下了。确实像 依云 说的那样,它的实现可以有重大改进。Conque 是在 Vim 里模拟 Shell 终端的一个很流行的插件,它不同版本的实现曾经有过很大改变,但都使用了 Python 接口,插件负责底层交流的部分是 Python 写的。我在去年 7 月左右看过当时最新版的代码,学习了很多。首先,知道使用 Python 接口来编写 Vim 插件就是来自 Conque 的启发,这个还要感谢当时在 Vim-cn 群里跟我提起这个插件的网友 “Strange”。另外,Conque 使用了 Python 语言里负责底层系统操作的标准模块,比如 tty、select 这些,来与外部 Shell 进程交流,这些模块直接对应于底层的 Unix 系统调用,效率高而且可靠,我在自己写的程序里也用 select.select() 来监听外部进程,这也是从 Conque 里学来的。
---- 从当时的 Conque 的代码来看,它是单线程的,从键盘接收用户输入发送给 Shell 进程,而接收靠的是 Vim 函数 feedkeys(),以这个函数作为触发,不停地监听和提取 Shell 输出,经过格式、颜色处理以后发送到 Vim buffer 里。就像上面说的,用 feedkeys() 有严重局限,因为它完全模拟了用户的按键操作。这样一来,真正的用户就只能进行一个键的操作了。如果按键输入多于一个键,后面的键就会被 feedkeys() 冲掉。而 Conque 的解决办法,就是把键盘上几乎每一个键都做了映射,使得 Conque 能通过这些映射来区分是真正的用户输入,还是 feedkeys() 的输入,从而做不同处理。我觉得,这是个很无奈的做法。如果它对速度的影响还不那么糟糕的话,实际上它还屏蔽了 Vim 强大的编辑功能。如果只是把 Shell 原样不动地搬进 Vim 的窗口里是没多大意思的,能够在 Vim 里操作 Shell 的最大好处在于,在编辑那些火星文的 Shell 语句的时候,要能够毫无保留地使用 Vim 的强大编辑功能,这样才真正有意义。
---- 今天这个突破应该能够让以上成为现实,或许一半成为现实。。。因为还有 GUI 的那个大 Bug 在。是不是 Conque 的作者没有用多线程是因为,他当时就知道这个 bug 啊?为了顾及到插件的通用性,就只好放弃这个几近完美的解决方案了。因为这是 X 系统的 bug,我就叫你 X-bug 吧,万事具备,只欠踩死这只 bug!哈哈!
关于通过 vim 的 python 接口使用多线程特性的问题
---- Vim Bug 第三弹,以下代码:
python3 << EOF import threading import time print('xxx') def print_to_vim(): print('yyy') threading.Thread(name='test', target=print_to_vim).start() print('xxx') time.sleep(3) EOF
---- source 以后 gvim 窗口神奇蒸发,得到如下结果:
---- 结论: 通过 python 接口开了多个线程的话,只容许其中一个线程使用 print() 函数。不过这看起来像是 Gnome 或者 X 的问题,不是 Vim 的问题。
---- 我就是想拿 Vim 当我自编程序的用户界面而已,这至于吗。
---- 设计目标:
1. 从 Vim 窗口里通过自定义命令和按键控制外部程序,不需要进 Shell。
2. 程序输出通过 Vim 窗口来显示。利用 Python 的多线程特性,主线程处理日常功能,辅助线程专门监听程序的输出,一有输出就刷新 buffer 显示。
---- 以上。再配合用新写的 FSE 模块来浏览和管理程序文件,别的什么都不需要了。
---- 要是一切如常的话,搞定这些是完全没有问题的,就是怕这些层出不穷的 bug。我容易吗我。
FileSystemExplorer 写好了
---- 感谢 依云 解决了 Py3 接口内存泄漏的问题。已经下了 firefox 的源码来测试,一共 11 级目录 45000 多个节点,用扩展输出形式,一次打开花费 13 秒左右,反复关闭打开最多用 350 MB 内存,不会一直增加。
---- 左边是收藏夹功能,右边是文件结构浏览功能。可以建多个实例浏览不同路径,收藏夹只有一个。
---- 收藏夹界面里的操作:
'O': 添加同级标题 'o': 添加下级标题 '\a': 将当前主窗口内的路径加入收藏 '\s': 显示当前条目的信息 '\d': 删除当前光标下的标题(recursive)或节点 '\r': 修改当前标题
---- 浏览界面里的操作:
设置属性: :Set hidden 不显示隐藏文件(默认) :Set nohidden 显示隐藏文件 :Set byname 按名称排序(默认) :Set bysize 按大小排序 :Set bymtime 按修改时间排序 :Set byatime 按访问时间排序 :Set desc 逆序 :Set asc 正序(默认) :Set basic 显示基本信息(只有文件或目录名) :Set extend 显示扩展信息(默认,包括大小、修改时间、访问时间) :Set lock 锁定当前根节点,只能在其内部浏览 :Set unlock 解除根节点锁定(默认) 浏览: :CD {dir} 切换到指定节点 "Enter": 打开/关闭目录,打开文件。 "Shift-Enter": 递归式打开/关闭目录,打开文件。 "\r": 刷新显示。原来打开的节点仍打开,原来关闭的仍关闭 "\f": 显示当前节点的信息。 "\s": 把根节点切换到指定节点。 "\c": 关闭所有目录节点,除了当前节点和它的上级节点以外 "Alt-i, Alt-k" 定位到上一个/下一个节点。 1. 当前光标位于缩进部分的第 i 个 Tab 上时,定位到上一个/下一个 i 级节点。 2. 当光标位于其它位置时,定位到上一个/下一个同级节点。 文件操作: "o": 在指定目录下新建一个节点。 "O": 建立一个指定节点的同级节点。 "\d": 删除指定的节点。 "\t": 在指定目录或指定文件所在的目录下开启一个终端。
---- 源文件在 “Vim 插件” 页面里。但是自用插件写的时候完全没考虑通用性,所以换一台电脑八成是不能用的,只给感兴趣的人作参考。
Gvim7.3 + Python3 接口的内存使用完全失控
---- 为什么会有这种鸟事,为什么为什么。一度以为 Vim 已经很熟不需要再搞了,谁知道风平浪静的日子是没有的。快抓狂了。Vim 是这么成熟的编辑器,这种事难道该发生么。
---- 原本以为是 Python 代码写的有问题,对 Python 不熟,内存泄漏什么的。逐渐排除了以后开始锁定到 Python 接口上面,具体讲就是 vim.buffer 这个东西。这个是测试代码:
lcd %:h tabedit tmpbuffer setlocal buftype=nofile python3 << EOF for i in range(3): flines= ['x'*200] * 50000 vim.command("%s+\\_.*++g") for fl in flines: vim.current.buffer.append(fl) del flines[:] EOF
---- 而这个是结果:
在 gvim7.2 + Python2 接口下面:
第 1 次 source: 46.8 MB 内存,关闭 tmpbuffer 后不变
第 2 次 source: 59.2 MB 内存,关闭 tmpbuffer 后 58.1
第 3 次 source: 61.6 MB 内存,关闭 tmpbuffer 后 60.4
第 4 次 source: 63.9 MB 内存,关闭 tmpbuffer 后 63.6
第 5 次 source: 66.3 MB 内存,关闭 tmpbuffer 后 65.1
在 gvim7.3 + Python3 接口下面:
第 1 次 source: 142.6 MB 内存,关闭 tmpbuffer 后不变
第 2 次 source: 238.5 MB 内存,关闭 tmpbuffer 后不变
第 3 次 source: 334.5 MB 内存,关闭 tmpbuffer 后不变
---- buftype 选项其实对结果无影响。第一个看上去还好。第二个,好像 gvim7.3 的 Python3 接口是没有任何内存回收的,就这么任由它增长。另外初次运行的内存消耗也是 gvim7.2 的三倍。这没问题么? 问题大了。后来索性一直运行,这个是最后的结果:
---- 我表示这不是我的问题。考虑把这个贴到 vim-use 群里面。
---- 这个很打击我写插件的热情。不过还是要写,不写就亏大了,尼玛感觉就跟炒股被套一样。昨天新加的:
1. 丰富了颜色
2. 添加了功能: 除当前节点和它的上级节点之外,关闭其它所有打开的节点。
3. 添加了功能: 锁定根节点,只能在根节点内部浏览。
Vim 的 Python 接口的内存回收机制有问题 !!!!!
---- 续写 FileSystemExplorer 这个插件,现在写好的:
1. 能刷新
2. 能设置属性:
a. 是否显示隐藏文件
b. 显示基本输出(只有名称)还是扩展输出(包括大小,修改时间与访问时间)
c. 设置根据名称 / 大小 / 修改时间 / 访问时间排序,设置正序或逆序。
3. 能递归式打开节点(但是结果恐怖,后述。)
---- 几个要点记一下:
1. 数据结构内部不要形成引用回路(reference cycle)。让文件节点同时保持对上级和下级节点的引用可以方便操作,但是对上级的引用要用 weakref 实现。如果形成引用回路就会产生没法析构的对象,以及内存泄漏。
2. 怎样定义一个类似 list 的对象: 直接继承 list 类型不是个好主意,应该继承 Abstract Base Classes(ABC)里面的 MutableSequence,然后覆盖掉以下 “虚函数”: __init__(), __len__(), __getitem__(), __del__(), __setitem__(), insert()。
3. 类 list 对象的读取操作与 list 形式一样,可以用 slicing,也可以用 comprehension。但是赋值不一样,不能直接
self= 另一个sequence
,需要注意。
4. 可以使用 del[:] 清除一个 类 list 对象,但是操作之前要先清除成员之间的引用关系。否则即使没有引用回路存在,Python 也不知道先清除哪个成员,结果又是一堆没法析构的对象。(对这一点还不是十分确定,有可能是太过谨慎了,回头写个程序验证一下。)
5. 在函数的默认参数里不要使用可变值类型(mutable type)。比如:
def my_function(arg=[]):
pass
这样是不对的,第二次调用时那个值就会变掉。应该这样:
def my_function(arg=None):
if arg is None: arg= []
---- 测试: 使用基本输出形式,显示隐藏文件,用 recursive 方式打开我的根目录(但是产生输出内容用的是线性处理方式),将近 49000 个节点(垃圾文件触目惊心),时间大概 8 秒。记得以前用 NerdTree 递归式打开 firefox 源文件的目录,也是几万个节点,花了两分钟以上。用扩展输出形式,多用 10 秒。我的电脑是 07 年的双核笔记本。所以 Python 接口的速度还是不错的,跟 VimScript 相比。
---- 最后一个大要点必须单独写:
Vim 的 Python 接口的内存回收机制有问题 !!!!!
如上。虽然已经通过定义 __del__() 等方式确认所建立的 Python 数据对象都能被正确析构,但是内存占用还是一路彪升。用 recursive 方式打开一次根目录会增加几十 MB 内存,但是这些对象析构的时候内存却不减少。试着来回打开关闭了十几次,内存就到了 400 MB 以上,通过资源管理器来看,gvim 成了最耗内存的程序。
后来把 Python 代码搬出来,改成一般的 Python 测试程序,通过 Shell 运行,没出现这种情况。递归式建立 49000 个节点会耗用 160MB 内存,但是后面无论怎样销毁再建立,内存都不再增加,可见内存回收在起作用。所以不是我代码写的有问题,有可能是 Vim 与 Python 的 garbage collector 通气不畅所致。
[补记]:
---- 又想了个办法,在原来的 Vim 与 Python 混合代码里定义了一个测试命令,模拟其它所有内部操作但只是不往 Vim Buffer 做任何输出。通过此命令反复进行大量数据结构的建立与销毁操作,结果与上面的测试程序一样,内存占用是固定的,不会一直增加。所以问题出在 Python 接口上面。
---- 先不想这么多,内存问题绝对是我能力以外的事情。只要平时不会二到用 recursive 方式打开几万个节点的目录再关闭,再打开再关闭,再打开再关闭 ... 这个插件还是能用的。
---- 关于 Reference Cycle 的测试代码如下,如果两个对象互相硬指向对方的话,析构函数 __del__() 不会被调用,于是内存泄漏。
# -*- coding: utf-8 -*- import weakref class Child: def __init__(self, parent): # 这里切换使用软指向还是硬指向 # self._parent= parent # XXX: 硬指向上级对象 self._parent= weakref.ref(parent) # XXX: 软指向上级对象 print('Child.__init__() -- called !') def __del__(self): print('Child.__del__() -- called !') class Parent: def __init__(self): self._child= None print('Parent.__init__() -- called !') def add_child(self, child): self._child= child # XXX: 硬指向下级对象 def __del__(self): print('Parent.__del__() -- called !') parent= Parent() child= Child(parent=parent) parent.add_child(child=child) del parent del child
FileSystemExplorer 终于成形了
---- 外部的 Vim 插件我基本都是装上试试然后就卸掉,用到的基本都自己写。在文件操作方面,因为自带的 Netrw 实在不够用,一些 VimScript 写的外部插件(比如 NerdTree)又有慢的问题。自己写的 FileSystemExplorer 这个插件自从起个头以后名不副实了很久,现在终于成形了。
---- 主体还是 Python 写的(通过 Vim 的 Python 接口),纯 VimScript 写的太慢。但是目前还是很菜。以下是设想中,准备逐步添加的功能:
1. 刷新(也可以用于删除、新建操作之后)
2. 改变排序方式(也可以用于递归式打开目录之后)
3. 删除目录或文件
4. 批量删除
5. 转移目录或文件
6. 改名
7. 递归式打开目录
8. 新建目录或文件
9. 快速定位(到同级上一个/下一个节点,到上级节点)
10. 智能化开启文件(通过后台调用其它程序)
---- 基本上 Vim 和 Python 折腾到现在,不确定性已经越来越少。接下来的事可能跟工厂做工差不多。继续开工 。。。
用 Python / Matplotlib 画出来的股票 K线图 (二)
---- 最新的在这里: 用 Python / Matplotlib 画出来的股票 K线图 (四)
---- 下一篇在这里: 用 Python / Matplotlib 画出来的股票 K线图 (三)
---- 上一版的改进,双股同列 + 无数细小改进,如下图。dpi= 300。明的一条是个股走势,暗的是同期的指数走势。这大概是近期最强的一只。
---- 要想培养对走势的感觉,采用固定比例尺的图形是必须的。一般股票软件里的图形都为显示方便而做了变形处理,用处不大。
---- 图形感觉差不多了,告一段落。接下来的目标是 股本结构、历史分配、行业板块、股东研究 这些信息,还包括个股资讯。实时的数据仍然暂时不碰。
---- 源码贴出来。因为 Matplotlib 还不支持 Python3, 所以单写了一个 Python2 脚本。注意绘图数据是用 pickle file 传递的。
[补记:我决定放弃线性坐标了。这个脚本只支持对数坐标。]
# -*- coding: utf-8 -*- import os 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 matplotlib.font_manager as font_manager import numpy from matplotlib.ticker import FixedLocator, MultipleLocator, FuncFormatter, NullFormatter __font_properties__=font_manager.FontProperties(fname='/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc') __color_lightsalmon__= '#ffa07a' __color_pink__= '#ffc0cb' __color_navy__= '#000080' def Plot(pfile, figpath): ''' pfile 指明存放绘图数据的 pickle file,figpath 指定图片需存放的路径 ''' fileobj= open(name=pfile, mode='rb') pdata= pickle.load(fileobj) fileobj.close() os.remove(pfile) # 计算图片的尺寸(单位英寸) # 注意:Python2 里面, "1 / 10" 结果是 0, 必须写成 "1.0 / 10" 才会得到 0.1 #================================================================================================================================================== length= len(pdata[u'日期']) # 所有数据的长度,就是天数 open_price_pri= pdata[u'开盘'][0] # int 类型 open_price_sec= pdata[u'开盘二'][0] # 同上 highest_price_pri= max( [phigh for phigh in pdata[u'最高'] if phigh != None] ) # 第一个行情的最高价 highest_price_sec= max( [phigh for phigh in pdata[u'最高二'] if phigh != None] ) # 第二个行情的最高价 highest_price= max(highest_price_pri, highest_price_sec*open_price_pri/open_price_sec) # 以第一个行情为基准修正出的总最高价 lowest_price_pri= min( [plow for plow in pdata[u'最低'] if plow != None] ) # 最低价 lowest_price_sec= min( [plow for plow in pdata[u'最低二'] if plow != None] ) # 最低价 lowest_price= min(lowest_price_pri, lowest_price_sec*open_price_pri/open_price_sec) # 以第一个行情为基准修正出的总最低价 yhighlim_price= int(highest_price * 1.1) # K线子图 Y 轴最大坐标 ylowlim_price= int(lowest_price / 1.1) # K线子图 Y 轴最小坐标 xfactor= 10.0/230.0 # 一条 K 线的宽度在 X 轴上所占距离(英寸) yfactor= 0.3 # Y 轴上每一个距离单位的长度(英寸),这个单位距离是线性坐标和对数坐标通用的 expbase= 1.1 # 底数,取得小一点,比较接近 1。股价 3 元到 4 元之间有大约 3 个单位距离 # XXX: 价格在 Y 轴上的 “份数”。注意,虽然最高与最低价是以第一个行情为基准修正出来的,但其中包含的倍数因子对结果无影响,即: # log(base, num1) - log(base, num2) == # log(base, num1/num2) == # log(base, k*num1/k*num2) == # log(base, k*num1) - log(base, k*num2) # ,这是对数运算的性质。 ymulti_price= math.log(yhighlim_price, expbase) - math.log(ylowlim_price, expbase) ymulti_vol= 3.0 # 成交量部分在 Y 轴所占的 “份数” ymulti_top= 1.2 # 顶部空白区域在 Y 轴所占的 “份数” ymulti_bot= 1.2 # 底部空白区域在 Y 轴所占的 “份数” xmulti_left= 12.0 # 左侧空白区域所占的 “份数” xmulti_right= 12.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= __color_pink__ figedgecolor= __color_navy__ figdpi= 300 figlinewidth= 1.0 figobj= pyplot.figure(figsize=(xlen_fig, ylen_fig), dpi=figdpi, facecolor=figfacecolor, edgecolor=figedgecolor, linewidth=figlinewidth) # Figure 对象 # 整个 figure 的标题 title_pri= (pdata[u'代码'] + ' ' if u'代码' in pdata else '') + pdata[u'简称'] title_sec= (pdata[u'代码二'] + ' ' if u'代码二' in pdata else '') + pdata[u'简称二'] figobj.suptitle(title_pri + ' / ' + title_sec, fontsize=12, fontproperties=__font_properties__) #================================================================================================================================================== #================================================================================================================================================== #======= #======= XXX: 第一只:成交量部分 #======= #================================================================================================================================================== #================================================================================================================================================== # 第一只:添加 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] ) # 标示出该天股价日内走平的一个序列 if u'成交额' in pdata: volume= pdata[u'成交额'] else: 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], edgecolor='red', linewidth=3.0, label='_nolegend_') if True in down: axes_2.vlines(xindex[down], volzeros[down], rarray_vol[down], edgecolor='green', linewidth=3.0, label='_nolegend_') if True in side: axes_2.vlines(xindex[side], volzeros[side], rarray_vol[side], edgecolor='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= {} # value: 每周第一个交易日在所有日期列表中的 index; key: 当周的序号 week number(当周是第几周) for d in datelist: isoyear, weekno= d.isocalendar()[0:2] dmark= isoyear*100 + weekno if dmark not in wdindex: wdindex[dmark]= datelist.index(d) xMinorLocator= FixedLocator(numpy.array( sorted(wdindex.values()) )) # 第一只:确定 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(4) malabel.set_horizontalalignment('right') malabel.set_rotation('45') for milabel in axes_2.get_xticklabels(minor=True): milabel.set_fontsize(4) milabel.set_color('blue') milabel.set_horizontalalignment('right') milabel.set_rotation('45') # 第一只:设定成交量 Y 轴坐标的范围 #================================================================================================================================================== maxvol= max(volume) # 注意是 int 类型 axes_2.set_ylim(0, maxvol) # 第一只:设定成交量 Y 轴上的坐标 #================================================================================================================================================== vollen= len(str(maxvol)) volstep_pri= int(round(maxvol/10.0+5000, -4)) yMajorLocator_2= MultipleLocator(volstep_pri) # 第一只:确定 Y 轴的 MajorFormatter dimsuffix= u'元' if u'成交额' in pdata else u'股' def y_major_formatter_2(num, pos=None): if num >= 10**8: # 大于 1 亿 return (str(round(num/10.0**8, 2)) + u'亿' + dimsuffix) if num != 0 else '0' else: return (str(num/10.0**4) + u'万' + dimsuffix) 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_font_properties(__font_properties__) malab.set_fontsize(4.5) # 这个必须放在前一句后面,否则作用会被覆盖 # 第一只:成交量数值在图中间的显示 #================================================================================================================================================== for iy in range(volstep_pri, maxvol, volstep_pri): for ix in mdindex[1:-1:3]: newlab= axes_2.text(ix+8, iy, y_major_formatter_2(iy)) newlab.set_font_properties(__font_properties__) newlab.set_color('0.3') newlab.set_fontsize(3) newlab.set_zorder(0) # XXX: 放在底层 # newlab.set_verticalalignment('center') #================================================================================================================================================== #================================================================================================================================================== #======= #======= XXX: 第二条成交量图线 #======= #================================================================================================================================================== #================================================================================================================================================== # 添加 Axes 对象 #================================================================================================================================================== axes_2_sec= axes_2.twinx() # axes_2_sec.set_axisbelow(True) # 网格线放在底层 axes_2_sec.set_axisbelow(True) # 网格线放在底层 # 改变坐标线的颜色 #================================================================================================================================================== # for child in axes_2_sec.get_children(): # if isinstance(child, matplotlib.spines.Spine): # child.set_color('lightblue') # 得到 X 轴 和 Y 轴 的两个 Axis 对象 #================================================================================================================================================== xaxis_2_sec= axes_2_sec.get_xaxis() yaxis_2_sec= axes_2_sec.get_yaxis() # 设置两个坐标轴上的 grid #================================================================================================================================================== # xaxis_2_sec.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) # xaxis_2_sec.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1) # yaxis_2_sec.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2) # yaxis_2_sec.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1) #================================================================================================================================================== #======= 绘图 #================================================================================================================================================== if u'成交额二' in pdata: volume_sec= pdata[u'成交额二'] else: volume_sec= pdata[u'成交量二'] zipoc_sec= zip(pdata[u'开盘二'], pdata[u'收盘二']) up_sec= numpy.array( [ True if po < pc and po != None else False for po, pc in zipoc_sec] ) # 标示出该天股价日内上涨的一个序列 down_sec= numpy.array( [ True if po > pc and po != None else False for po, pc in zipoc_sec] ) # 标示出该天股价日内下跌的一个序列 side_sec= numpy.array( [ True if po == pc and po != None else False for po, pc in zipoc_sec] ) # 标示出该天股价日内走平的一个序列 rarray_vol_sec= numpy.array(volume_sec) volzeros_sec= numpy.zeros(length) # 辅助数据 # XXX: 如果 up_sec/down_sec/side_sec 各项全部为 False,那么 vlines() 会报错。 if True in up_sec: axes_2_sec.vlines(xindex[up_sec], volzeros_sec[up_sec], rarray_vol_sec[up_sec], edgecolor='pink', linewidth=1.0, label='_nolegend_', alpha=0.3) if True in down_sec: axes_2_sec.vlines(xindex[down_sec], volzeros_sec[down_sec], rarray_vol_sec[down_sec], edgecolor='lightgreen', linewidth=1.0, label='_nolegend_', alpha=0.3) if True in side_sec: axes_2_sec.vlines(xindex[side_sec], volzeros_sec[side_sec], rarray_vol_sec[side_sec], edgecolor='0.7', linewidth=1.0, label='_nolegend_', alpha=0.3) # 设定 X 轴坐标的范围 #================================================================================================================================================== # XXX: 不用了,与 axes_2 共用。 # 设定 Y 轴坐标的范围 #================================================================================================================================================== maxvol_sec= max(volume_sec) # 注意是 int 类型 axes_2_sec.set_ylim(0, maxvol_sec) # 设定 Y 轴上的坐标 #================================================================================================================================================== volstep_sec= volstep_pri*maxvol_sec/float(maxvol) yMajorLocator_2_sec= MultipleLocator(volstep_sec) # 确定 Y 轴的 MajorFormatter dimsuffix_sec= u'元' if u'成交额二' in pdata else u'股' def y_major_formatter_2_sec(num, pos=None): if num >= 10**8: # 大于 1 亿 print(('num= ' + str(num) + ', result= ' + str(round(num/10.0**8, 3)) + u'亿' + dimsuffix_sec).encode('utf8')) return (str(round(num/10.0**8, 3)) + u'亿' + dimsuffix_sec) if num != 0 else '0' else: return (str(round(num/10.0**4, 2)) + u'万' + dimsuffix_sec) if num != 0 else '0' # def y_major_formatter_2_sec(num, pos=None): # return int(num) yMajorFormatter_2_sec= FuncFormatter(y_major_formatter_2_sec) # 确定 Y 轴的 MinorFormatter # def y_minor_formatter_2(num, pos=None): # return int(num) # yMinorFormatter_2_sec= FuncFormatter(y_minor_formatter_2) yMinorFormatter_2_sec= NullFormatter() # 设定 X 轴的 Locator 和 Formatter yaxis_2_sec.set_major_locator(yMajorLocator_2_sec) yaxis_2_sec.set_major_formatter(yMajorFormatter_2_sec) # yaxis_2_sec.set_minor_locator(yMinorLocator_2_sec) yaxis_2_sec.set_minor_formatter(yMinorFormatter_2_sec) # 设定 Y 轴主要坐标点与辅助坐标点的样式 for malab in axes_2_sec.get_yticklabels(minor=False): malab.set_font_properties(__font_properties__) malab.set_fontsize(4.5) # 这个必须放在前一句后面,否则作用会被覆盖 #================================================================================================================================================== #================================================================================================================================================== #======= #======= XXX: K 线图部分 #======= #================================================================================================================================================== #================================================================================================================================================== # 添加 Axes 对象 #================================================================================================================================================== axes_1= figobj.add_axes(rect_1, axis_bgcolor='black', sharex=axes_2) axes_1.set_axisbelow(True) # 网格线放在底层 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 线部分 #================================================================================================================================================== # 对开收盘价进行视觉修正 for idx, poc in enumerate( zip(pdata[u'开盘'], pdata[u'收盘']) ): if poc[0] == poc[1] and None not in poc: variant= round((poc[1]+1000)/2000, 0) pdata[u'开盘'][idx]= poc[0] - variant # 稍微偏离一点,使得在图线上不致于完全看不到 pdata[u'收盘'][idx]= poc[1] + variant 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() 会报错。 # XXX: 可以使用 alpha 参数调节透明度 if True in up: axes_1.vlines(xindex[up], rarray_low[up], rarray_high[up], edgecolor='red', linewidth=0.6, label='_nolegend_') axes_1.vlines(xindex[up], rarray_open[up], rarray_close[up], edgecolor='red', linewidth=3.0, label='_nolegend_') if True in down: axes_1.vlines(xindex[down], rarray_low[down], rarray_high[down], edgecolor='green', linewidth=0.6, label='_nolegend_') axes_1.vlines(xindex[down], rarray_open[down], rarray_close[down], edgecolor='green', linewidth=3.0, label='_nolegend_') if True in side: axes_1.vlines(xindex[side], rarray_low[side], rarray_high[side], edgecolor='0.7', linewidth=0.6, label='_nolegend_') axes_1.vlines(xindex[side], rarray_open[side], rarray_close[side], edgecolor='0.7', linewidth=3.0, label='_nolegend_') # 绘制均线部分 #================================================================================================================================================== if u'5日均' in pdata: rarray_5dayave= numpy.array(pdata[u'5日均']) axes_1.plot(xindex, rarray_5dayave, 'o-', color='white', linewidth=0.1, label='ave_5', \ markersize=0.7, markeredgecolor='white', markeredgewidth=0.1) # 5日均线 if u'10日均' in pdata: rarray_10dayave= numpy.array(pdata[u'10日均']) axes_1.plot(xindex, rarray_10dayave, 'o-', color='yellow', linewidth=0.1, label='ave_10', \ markersize=0.7, markeredgecolor='yellow', markeredgewidth=0.1) # 10日均线 if u'30日均' in pdata: rarray_30dayave= numpy.array(pdata[u'30日均']) axes_1.plot(xindex, rarray_30dayave, 'o-', color='cyan', linewidth=0.1, label='ave_30', \ markersize=0.7, markeredgecolor='cyan', markeredgewidth=0.1) # 30日均线 # 绘制 复权提示 #================================================================================================================================================== if u'复权' in pdata: adjdict= dict(pdata[u'复权']) for idx, dstr in enumerate(pdata[u'日期']): if dstr in adjdict: axes_1.plot([idx, idx], [ylowlim_price, yhighlim_price], '-', color='purple', linewidth=0.3) # 设定 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 轴上的坐标 #================================================================================================================================================== # XXX: 不用 LogLocator 了,因为不能控制坐标点的位置。 # 主要坐标点 #---------------------------------------------------------------------------- yticks_major_pri= [] for i in range(1, 999): newloc= ylowlim_price * (expbase**i) if newloc <= yhighlim_price: yticks_major_pri.append(newloc) else: break yMajorLocator_1= FixedLocator(numpy.array(yticks_major_pri)) # 确定 Y 轴的 MajorFormatter def y_major_formatter_1(num, pos=None): return str(round(num/1000.0, 2)) yMajorFormatter_1= FuncFormatter(y_major_formatter_1) # 设定 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(6) # 辅助坐标点 #---------------------------------------------------------------------------- yticks_minor_pri= [] mtstart= ylowlim_price * (1.0+(expbase-1.0)/2) for i in range(999): newloc= mtstart * (expbase**i) if newloc <= yhighlim_price: yticks_minor_pri.append(newloc) else: break yMinorLocator_1= FixedLocator(numpy.array(yticks_minor_pri)) # XXX minor ticks 已经在上面一并设置,这里不需要了。 # 确定 Y 轴的 MinorFormatter def y_minor_formatter_1(num, pos=None): return str(round(num/1000.0, 2)) 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 mal in axes_1.get_yticklabels(minor=True): mal.set_fontsize(5) mal.set_color('blue') # 第一只:价格数值在图中间的显示 #================================================================================================================================================== for iy in yticks_major_pri: for ix in mdindex[1:-1:3]: newlab= axes_1.text(ix+8, iy*1.001, y_major_formatter_1(iy)) newlab.set_font_properties(__font_properties__) newlab.set_color('0.3') newlab.set_fontsize(3) newlab.set_zorder(0) # XXX: 放在底层 # newlab.set_verticalalignment('center') # 第一只:日期在图中间的显示 #================================================================================================================================================== for iy in yticks_minor_pri[1:-1:5]: for ix in mdindex: newlab= axes_1.text(ix-1, iy, pdata[u'日期'][ix]) newlab.set_font_properties(__font_properties__) newlab.set_color('0.3') newlab.set_fontsize(4) newlab.set_rotation('vertical') # newlab.set_horizontalalignment('left') # newlab.set_verticalalignment('bottom') newlab.set_zorder(0) # XXX: 放在底层 # newlab.set_verticalalignment('center') #================================================================================================================================================== #================================================================================================================================================== #======= #======= XXX: 第二条 K 线图 #======= #================================================================================================================================================== #================================================================================================================================================== # 添加 Axes 对象 #================================================================================================================================================== axes_1_sec= axes_1.twinx() # axes_1_sec.set_axisbelow(True) # 网格线放在底层 axes_1_sec.set_yscale('log', basey=expbase) # 使用对数坐标 # 得到 X 轴 和 Y 轴 的两个 Axis 对象 #================================================================================================================================================== xaxis_1_sec= axes_1_sec.get_xaxis() yaxis_1_sec= axes_1_sec.get_yaxis() #================================================================================================================================================== #======= 绘图 #================================================================================================================================================== # 绘制 K 线部分 #================================================================================================================================================== # 对开收盘价进行视觉修正 for idx, poc in enumerate( zipoc_sec ): if poc[0] == poc[1] and None not in poc: pdata[u'开盘二'][idx]= poc[0] - 5 # 稍微偏离一点,使得在图线上不致于完全看不到 pdata[u'收盘二'][idx]= poc[1] + 5 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_sec, down_sec, side_sec 里有一个全部为 False 组成,那么 vlines() 会报错。 # XXX: 可以使用 alpha 参数调节透明度 if True in up_sec: axes_1_sec.vlines(xindex[up_sec], rarray_low[up_sec], rarray_high[up_sec], edgecolor='red', linewidth=0.6, label='_nolegend_', alpha=0.3) axes_1_sec.vlines(xindex[up_sec], rarray_open[up_sec], rarray_close[up_sec], edgecolor='red', linewidth=3.0, label='_nolegend_', alpha=0.3) if True in down_sec: axes_1_sec.vlines(xindex[down_sec], rarray_low[down_sec], rarray_high[down_sec], edgecolor='green', linewidth=0.6, label='_nolegend_', alpha=0.3) axes_1_sec.vlines(xindex[down_sec], rarray_open[down_sec], rarray_close[down_sec], edgecolor='green', linewidth=3.0, label='_nolegend_', alpha=0.3) if True in side_sec: axes_1_sec.vlines(xindex[side_sec], rarray_low[side_sec], rarray_high[side_sec], edgecolor='0.7', linewidth=0.6, label='_nolegend_', alpha=0.3) axes_1_sec.vlines(xindex[side_sec], rarray_open[side_sec], rarray_close[side_sec], edgecolor='0.7', linewidth=3.0, label='_nolegend_', alpha=0.3) # 设定 X 轴坐标的范围 #================================================================================================================================================== axes_1_sec.set_xlim(-1, length) # 先设置 label 位置,再将 X 轴上的坐标设为不可见。因为与 成交量子图 共用 X 轴 #================================================================================================================================================== # 设定 X 轴的 Locator 和 Formatter xaxis_1_sec.set_major_locator(xMajorLocator) xaxis_1_sec.set_major_formatter(xMajorFormatter) xaxis_1_sec.set_minor_locator(xMinorLocator) xaxis_1_sec.set_minor_formatter(xMinorFormatter) # 将 X 轴上的坐标设为不可见。 for malab in axes_1_sec.get_xticklabels(minor=False): malab.set_visible(False) for milab in axes_1_sec.get_xticklabels(minor=True): milab.set_visible(False) # 设定 Y 轴坐标的范围 #================================================================================================================================================== axes_1_sec.set_ylim(ylowlim_price*open_price_sec/open_price_pri, yhighlim_price*open_price_sec/open_price_pri) # 设定 Y 轴上的坐标 #================================================================================================================================================== # 主要坐标点 #---------------------------------------------------------------------------- yticks_major_sec= [] ylowlim_price_sec= ylowlim_price*open_price_sec/open_price_pri yhighlim_price_sec= yhighlim_price*open_price_sec/open_price_pri for i in range(1, 999): newloc= ylowlim_price_sec * (expbase**i) if newloc <= yhighlim_price_sec: yticks_major_sec.append(newloc) else: break yMajorLocator_1_sec= FixedLocator(numpy.array(yticks_major_sec)) # 确定 Y 轴的 MajorFormatter def y_major_formatter_1_sec(num, pos=None): return str(round(num/1000.0, 2)) yMajorFormatter_1_sec= FuncFormatter(y_major_formatter_1_sec) # 设定 X 轴的 Locator 和 Formatter yaxis_1_sec.set_major_locator(yMajorLocator_1_sec) yaxis_1_sec.set_major_formatter(yMajorFormatter_1_sec) # 设定 Y 轴主要坐标点与辅助坐标点的样式 for mal in axes_1_sec.get_yticklabels(minor=False): mal.set_fontsize(6) # 辅助坐标点 #---------------------------------------------------------------------------- yticks_minor_sec= [] mtstart_sec= ylowlim_price_sec * (1.0+(expbase-1.0)/2) for i in range(999): newloc= mtstart_sec * (expbase**i) if newloc <= yhighlim_price_sec: yticks_minor_sec.append(newloc) else: break yMinorLocator_1_sec= FixedLocator(numpy.array(yticks_minor_sec)) # XXX minor ticks 已经在上面一并设置,这里不需要了。 # 确定 Y 轴的 MinorFormatter def y_minor_formatter_1_sec(num, pos=None): return str(round(num/1000.0, 2)) yMinorFormatter_1_sec= FuncFormatter(y_minor_formatter_1_sec) # 设定 X 轴的 Locator 和 Formatter yaxis_1_sec.set_minor_locator(yMinorLocator_1_sec) yaxis_1_sec.set_minor_formatter(yMinorFormatter_1_sec) # 设定 Y 轴主要坐标点与辅助坐标点的样式 for mal in axes_1_sec.get_yticklabels(minor=True): mal.set_fontsize(5) mal.set_color('blue') # 显示图片 #================================================================================================================================================== # pyplot.show() # 保存图片 #================================================================================================================================================== figobj.savefig(figpath, dpi=figdpi, facecolor=figfacecolor, edgecolor=figedgecolor, linewidth=figlinewidth) if __name__ == '__main__': Plot(pfile=sys.argv[1], figpath=sys.argv[2])
用来画股票 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)