☰
用Python抓免费Level-2行情,实战绘制分时图与K线图
2026/10/15 16:39:03 网站建设 项目流程

简介:面向股票行情可视化场景的Python源码资源包,聚焦分时图、K线图绘制与免费Level2行情抓取,适合有基础Python知识、希望深入金融数据可视化与行情分析的开发者和量化爱好者。资源提供完整的PyCharm工程,包含分时图、K线图主程序以及公共函数、请求工具等脚本,覆盖数据获取、清洗、计算到图表呈现的完整链路;其中关键脚本演示了如何接入Tushare等免费接口拉取历史行情与Level2逐笔委托、成交数据,并利用Matplotlib、mplfinance完成专业图表输出,从数据请求到出图均有可运行示例。包内共20个文件,以.py源码为主,另有项目配置xml、编译缓存pyc及工程元数据,压缩包仅42KB,轻量便携、便于二次改造。已有13510人学习下载,适合作为股票行情可视化从入门到进阶的实战参考;主程序与辅助模块分离,结构清晰,可直接套用或按需扩展,便于自行排查问题与定制策略。

1. 把分时图、K线图和免费Level-2拼到一起:先看数据再看画图

一个盯盘场景足够说明问题:盘中某只票突然拉起,五档行情只显示买一卖一,挂单被吃掉又立刻补上,根本分不清是大单进场还是对倒诱多。这时候Level-2的十档盘口和逐笔成交才有发言权,但券商Level-2行情普遍要付费,个人开发者不可能为一个小工具掏这笔钱。于是就有了标题这条路线:用Python抓免费Level-2行情,顺手把分时图和K线图画出来。这篇文章要解决的核心问题是——免费行情源怎么找、分时图和K线图怎么从原始数据画出来、画完以后哪些坑会让你数据对不上、图会翻车。适合正在做个人盯盘工具、量化信号提醒,或者想省数据费的Python开发者。默认你装好了pandas、requests和matplotlib,绘图部分额外用到mplfinance。

2. Level-2行情的第一道门槛:数据结构、免费源和抓取脚本

2.1 十档盘口和逐笔成交:Level-2比Level-1多了什么

普通行情是五档,每三秒推送一次快照,中间撤单和挂单变化全部看不见。Level-2的增量主要集中在两块:十档盘口和逐笔成交。十档盘口能看到买卖两侧各10档的价格和委托量,逐笔成交则把每一笔成交的时间、价格、数量、主动买卖方向都暴露出来。这两块对Python程序来说意味着什么?意味着你可以用脚本去统计“大单净流入”而不是靠肉眼猜,可以检测“挂单撤单频率”来判断主力是否在对倒,可以在分时图上叠加逐笔成交标记,定位放量拉升的精确时间点。

逐笔成交的核心字段不多:成交时间精确到秒甚至毫秒、成交价、成交量、成交额、主动成交方向(买盘/卖盘/中性)。十档盘口就是买卖两侧各10档价格和委托量,一般按档位从近到远排列。注意,不同行情商对“Level-2免费版”的定义不一样,有的只给十档快照不给逐笔,有的给了逐笔但精度是秒级而不是毫秒级。动手之前先确认你拿到的免费权限覆盖哪几类字段,否则写代码到一半发现缺数据是常事。

拿逐笔成交能做什么实测?比如统计主动买量占全天成交量的比例,或者把一个时间窗口内的大额买单拆出来,看它是“连续吃单”还是“偶尔一笔”。Level-1的三秒快照做不了这件事,因为中间发生的几十笔成交被合并成了一条。这也是免费Level-2对个人开发者最大的实用价值:不需要自己抓高频数据,靠逐笔事件就能做很多盘口分析。

2.2 三个免费行情源怎么选:延迟、字段和稳定性权衡

个人能接触到的免费Level-2行情来源,我实际用下来主要分三条路。第一条是某券商开户后赠送的Level-2权限,现在不少券商为了拉新会送一段时间甚至长期免费,但这个数据通常在自家客户端里,没有公开HTTP接口,要用就得配合客户端自动化,或者从本地数据文件里读。第二条是某财经门户网站提供的免费行情接口,直接requests就能拉,分时、日K、盘口字段都有,但限制请求频率,也没有正式服务协议。第三条是某行情客户端软件启动后缓存在本地目录的分钟线和日线数据,用Python直接读文件,胜在稳定不封IP,缺点是必须开着客户端,且拿到的是分钟级数据,做不了逐笔级分析。

把三条路的特性放到一起比较,按自己的目标选:

免费来源延迟字段完整度稳定性适合场景
券商赠送权限实时高(十档+逐笔)高但无公开API自用盯盘、策略信号
财经门户接口秒级中(快照为主)有封IP风险学习验证、原型开发
客户端本地文件分钟/日级中(分钟线完整)稳定离线回测、K线绘制

我一般建议分阶段选:先走财经门户接口把分时图和K线图跑通,因为requests拿JSON最方便,代码五分钟能起来;等确实需要逐笔级数据做盘口分析时,再研究券商客户端的数据文件。别一上来就啃客户端文件,那等于同时和编码规范、加密字段做斗争,容易把兴趣磨没。下面的抓取脚本以财经门户接口为例讲解,思路不变,换数据源只改解析函数。

2.3 用requests抓取行情快照的最小脚本与参数说明

一个行情快照接口的返回结构,常见格式有两种:一种是标准JSON,直接resp.json()解析;另一种是类字符串格式,形如v_sz000001="时间,价格,...",需要用字符串函数切。先写一个能跑的快照抓取函数:

import requests import json HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Referer": "https://example.com/" } def fetch_snapshot(symbol: str) -> dict: # symbol 传股票代码,如 "sh600000",sh/sz 前缀不能省 url = f"https://api.example.com/level2/snapshot?symbol={symbol}" try: resp = requests.get(url, headers=HEADERS, timeout=3) resp.raise_for_status() return resp.json() except requests.exceptions.Timeout: return {"error": "timeout"} except requests.exceptions.RequestException as exc: return {"error": str(exc)}

这里三个参数值得说明。User-Agent不能省,很多接口没有UA直接返回403;Referer是校验来源的,不带会被识别为爬虫;timeout=3是关键中的关键,行情接口偶尔卡住,没有超时的话requests会一直挂在那里,你的盘中刷新循环就被一条慢请求卡死。返回的JSON里,bids和asks一般是二维数组,bids[0]就是买一的价格和数量。如果拿到的是字符串格式,加一个解析函数:

def parse_snapshot_text(text: str) -> dict: # 形如 v_sz000001="2025011510:00:00,10.25,123456,..." key, _, payload = text.partition("=") fields = payload.strip('";').split(",") return { "time": fields[0], "price": float(fields[1]), "volume": int(fields[2]), # 后续字段按接口文档映射,没有文档就打印 fields 自己数位置 }

解析时最容易翻车的是字符串里的引号和分号,strip('";')一次性清理掉两端字符,比replace逐个删要稳。字段位置没有文档的话,先print完整fields列表,肉眼数一遍下标再写映射,别靠猜。免费接口没有书面文档可依,这是常态,我的习惯是写个小脚本拉一次数据全量打印,再对照行情软件里的数值猜字段,多试几次就能对齐。

3. 分时图从零绘制:价格线、均价线和成交量柱的细节

3.1 分时图的坐标系和均价线算法

分时图不是简单把每分钟价格连成线。A股交易时段是9:30到11:30、13:00到15:00,横轴是交易时间,纵轴是价格,盘中价格线是主视觉,黄色均价线是“当日持仓平均成本”,下方成交量柱反映每一分钟的活跃度。均价线算法的本质是累计成交额除以累计成交量,也就是从开盘到当前时刻,每一笔成交的加权平均价。

这意味着你的数据源里不能只有价格和成交量,还必须有成交额字段。很多免费接口的分时数据只给价格和成交量,不给成交额,那均价线就只能用“每分钟收盘价×成交量”近似,误差在波动大的票上会比较明显。我建议抓分时数据时主动多抓一个amount字段,如果接口实在太简陋,就自己用price * volume做估算值,并在图上标注这是“估算均价线”,避免误用。

昨收价是分时图的地基。正常的分时图会在昨收价位置画一条水平虚线,价格线在它上方为红、下方为绿,A股软件默认红涨绿跌。绘图前要从行情快照里取pre_close字段,如果取不到,就用昨天日K的收盘价。这里有个注意点:分时图纵轴范围要以昨收价为基准对称扩展,比如昨收10元,最高价10.5元,最低价9.8元,纵轴建议从9.7放到10.6,让价格线保持肉眼可见的波动幅度。

3.2 用matplotlib画分时图的最小实现

import pandas as pd import matplotlib.pyplot as plt import matplotlib.dates as mdates def plot_intraday(df: pd.DataFrame, pre_close: float) -> None: # df 必须包含 time、price、avg_price、volume 四列 fig, (ax_price, ax_vol) = plt.subplots( 2, 1, figsize=(12, 8), sharex=True, gridspec_kw={"height_ratios": [3, 1]} ) # 价格线和均价线 ax_price.plot(df["time"], df["price"], color="#1E90FF", lw=1.2, label="price") ax_price.plot(df["time"], df["avg_price"], color="#FFD700", lw=1.0, label="avg") ax_price.axhline(pre_close, color="#888888", lw=0.8, ls="--") ax_price.legend(loc="upper left") ax_price.set_ylabel("price") # 成交量柱,涨红跌绿 colors = ["#E74C3C" if p >= pre_close else "#2ECC71" for p in df["price"]] width = 60 / 1440 * 0.6 # 1分钟在日期轴上的宽度比例 ax_vol.bar(df["time"], df["volume"], width=width, color=colors) ax_vol.set_ylabel("volume") ax_price.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) plt.tight_layout() plt.show()

height_ratios=[3, 1]让价格区域占3份、成交量占1份,这是行情软件最常见的比例。sharex=True保证两个子图横轴对齐,缩放时不会错位。成交量柱的宽度我习惯用60 / 1440 * 0.6换算,因为在日期横轴上一天1440分钟,一分钟对应1/1440,乘0.6让柱子和柱子在视觉上留有间隔,不用写死一个魔法数。颜色判断用了p >= pre_close,涨了画红,跌了画绿,注意这里如果数据里混入了非交易时段就会把午休的平线也涂上颜色,所以绘图前清洗数据是硬前提。

另外,如果分时数据不是按分钟采样而是按每笔成交来推送,不要直接画。先重采样成1分钟再走这个函数,因为成交量柱的宽度假设是分钟单位,原始逐笔数据画出来会是一堆细碎尖刺,没法看。

3.3 把分时数据存成DataFrame:时间对齐与空档处理

免费接口的分时数据质量参差不齐,常见问题有时间戳重复、午休时段混入数据、下午开盘前缺几根K线。我的做法是先把原始数据统一走一遍清洗函数,再做后续计算:

def clean_intraday(df: pd.DataFrame) -> pd.DataFrame: # df 原始列:time(str/时间戳)、price、volume、amount df = df.copy() df["time"] = pd.to_datetime(df["time"]) df = df.set_index("time") # 去掉重复时间戳,保留最后一条(免费源经常推送重发) df = df[~df.index.duplicated(keep="last")].sort_index() # 只保留两个交易时段 morning = df.between_time("09:30", "11:30") afternoon = df.between_time("13:00", "15:00") return pd.concat([morning, afternoon])

duplicated(keep="last")是处理重复时间戳的标准姿势,有的接口在数据重连后会重推上一分钟的数据,保留最后一条比较贴近真实行情。between_time方法要求索引是DatetimeIndex,所以前面必须set_index。清洗完之后如果要做分钟级聚合,再补一步:

def resample_minute(df: pd.DataFrame) -> pd.DataFrame: out = df.resample("1min").agg({ "price": "last", "volume": "sum", "amount": "sum" }).dropna(subset=["price"]) out["avg_price"] = out["amount"].cumsum() / out["volume"].cumsum() return out

这个函数把秒级或逐笔数据聚合成分钟级,price取每分钟最后一笔作为该分钟收盘价,volume和amount求和。avg_price用累计额除以累计量算出来,注意分母volume如果为0,结果会是NaN,所以后续绘图前最好fillna(method="ffill")。如果数据本身就是分钟级,直接跳过resample。这一步做完,3.2的绘图函数拿到的就是一份干净、对齐、可复现的数据,后面K线聚合也依赖这份分钟级数据。

4. 把分钟切片聚合成K线:mplfinance画图的参数和复权坑

4.1 一分钟切片如何聚合成OHLC

K线不是另外一套数据,它就是分钟数据按时间窗口聚合结果。一根5分钟K线的开、高、低、收分别对应这个时间窗口内第一笔成交价、最高价、最低价、最后一笔成交价,成交量是该窗口的累计值。pandas的resample可以直接做这个事:

def minute_to_kline(minute_df: pd.DataFrame, rule: str = "5min") -> pd.DataFrame: # minute_df 必须包含 price、volume、amount 列 out = minute_df.resample(rule).agg( open=("price", "first"), high=("price", "max"), low=("price", "min"), close=("price", "last"), volume=("volume", "sum"), amount=("amount", "sum") ).dropna() return out

rule参数是聚合窗口,传"5min"得到5分钟K线,"15min"得到15分钟K线,"1D"得到日K。一个常见误区是直接用agg("ohlc"),pandas确实内置了这个快捷方式,但返回的列名是大小写混合的多级索引,mplfinance要求列名必须是大写开头的Open/High/Low/Close/Volume,所以像上面这样显式指定列名更省事。dropna()把没有成交的空窗口去掉,这一步同时把午休空档剔除了,比绘图时再过滤土办法强得多。

还有一点需要注意:resample的聚合是基于时间标签,A股午休时段自然没有数据,聚合结果不会出现11:30到13:00的K线,但如果原始数据里混入了收盘后的固定推送,就必须先执行3.3的clean_intraday再做聚合,否则收盘后那几分钟会被算成一根假K线。

4.2 用mplfinance画K线和均线的参数设置

import mplfinance as mpf def plot_kline(df: pd.DataFrame, mav=(5, 10, 20), title="KLine"): # df 列名必须为 Open/High/Low/Close/Volume,索引为 DatetimeIndex mpf.plot( df, type="candle", mav=mav, volume=True, title=title, ylabel="price", style="charles", warn_too_much_data=2000, )

mav=(5, 10, 20)对应5日、10日、20日均线,mplfinance会在图上自动叠加三条不同颜色的均线。style="charles"是A股习惯的配色,红涨绿跌,美股风格的默认样式反而容易让国内读者不适应。warn_too_much_data这个参数容易被忽略,默认值比较小,当你画几千根K线时mplfinance会弹警告打断输出,调到2000可以避免这个问题。

如果画的是日K线,Volume列必须存在,mplfinance的volume=True会把它画在下方副图。如果聚合出来的列名是小写,比如open,绘图前先执行一次df.rename(columns={"open": "Open", ...}),不然直接报错,这个报错信息不够直观,我第一次用时也卡了一会儿。

4.3 复权处理和除权除息的隐藏坑

免费接口默认返回的是未复权数据,未复权意味着除权除息当天K线会留下一个向下或向上的跳空缺口。比如某票10送10,除权日股价直接腰斩,K线图上看起来像暴跌,5日均线也在这个位置被拉出一个假突破信号。量化分析里这是致命的,因为策略会把这个缺口误判为行情反转。

处理方式首推接口自带的复权参数,很多财经接口在URL里加fq=qfq或fq=hfq,分别表示前复权和后复权。前复权保持最新价格不变,调整历史价格;后复权保持最老价格不变,调整后续价格。做K线分析我一般用前复权,因为最新价格和当前盘面一致,视觉上最直观。抓取函数里简单拼一个参数即可:

def fetch_kline(symbol: str, fq: str = "qfq") -> list: # fq 参数:qfq 前复权,hfq 后复权,空字符串则不复权 url = f"https://api.example.com/kline?symbol={symbol}&fq={fq}" data = requests.get(url, headers=HEADERS, timeout=5).json() # 返回按 [时间, 开, 高, 低, 收, 量] 排列的列表 return data

如果接口不支持复权参数,才考虑本地近似复权:拿到除权因子后,把除权日之前的所有价格除以因子。但真实复权还要处理现金分红,本地做容易出错,我的建议是优先依赖接口复权,毕竟免费接口的复权数据是现成的,自己实现的性价比不高。

提示:复权和未复权数据画的均线完全不一样,同一个票在同一个日期,复权后的5日均线可能向上,未复权的可能向下。回测之前先确认你拉的数据是复权还是未复权,并在文件命名里写清楚,别混用。

5. 五大常见坑:免费Level-2抓取和绘图的踩坑记录

5.1 时间轴断层:午休被画成一条直线

现象:分时图上11:30到13:00之间出现一条横线,从上午收盘价直连下午开盘价,成交量区域也是空白或一条平线。第一次看到还以为是数据源的问题,后来发现是自己画的。

原因:免费接口在午休时段停止推送,但某些来源会用上午最后一笔价格填充这段时间的数据,DataFrame里多出了整整90分钟无成交记录。绘图函数不管这些,价格线直接把它们连起来。

解决:绘图前必须做3.3的clean_intraday清洗,只保留9:30到11:30、13:00到15:00两个交易时段。我习惯把清洗逻辑放在抓取函数返回之后、任何绘图之前,这样分时图和K线图共用一份干净数据。如果你拿到的数据是分钟级但接口本身不带时间标记,那基本只能找别的数据源了。

5.2 逐笔成交和盘口快照时间戳对不齐

现象:同一个时刻,逐笔成交显示10:00:03.456有一笔成交,盘口快照却标的时间是10:00:05,两条数据混在一起画图时价格线和成交量柱错位,看起来像“成交价和成交量对不上”。

原因:Level-2里逐笔和快照是两个独立推送通道,逐笔是事件驱动,成交发生就有记录;快照是周期生成,通常三秒或一秒一张。免费源的两个字段甚至可能来自上游不同服务器,时间基准本身就有偏差。

解决:绘图分析时统一以快照时间为准,逐笔成交只作为事件标记叠加在分时图上,比如在对应快照时间段内标注“这里有连续大单”,而不是把逐笔的时间戳直接当作横轴坐标。如果一定要精确对齐,用本地接收时间给每条数据重新打时间戳,至少保证系统内是一致的。

5.3 免费源的最新价滞后,K线收盘价对不上

现象:收盘后看当日K线,发现收盘价和行情软件里的“今收”差几分钱,且这个偏差不是随机的,在成交不活跃的票上特别明显,低流动性时段偏差可以到几毛钱。

原因:免费接口的last字段更新周期比盘口快照更慢,收盘集合竞价后也没有额外推送最终结算价,抓到的最后一笔快照价不等于收盘价。盘后所有数据源都会用官方结算价校准,但你的本地数据如果停在了最后一笔快照上,就永久对不上了。

解决:在每天15:00收盘后单独用日K接口拉一次当日数据,用日K的Close字段覆盖本地K线的收盘价。实时盘中展示时,不要把last当作最终收盘价写进分析结果。我自己跑回测时,每天收盘后有一个固定的“校准任务”,把当天所有K线的close统一替换成官方日K收盘价,之后再跑策略。

5.4 除权除息导致K线跳空,均线失真

现象:某天K线突然低开8%,但当天大盘和板块都没跌,查公告才发现是高送转或大额分红除权日。更麻烦的是,5日、10日均线在除权日前后出现明显拐头,看起来像是趋势破坏,实际是数据没复权。

原因:未复权数据保留了除权除息的原始价格跳变,这个跳变是会计调整,不是真实交易行为。均线这类统计指标对这个假跳变非常敏感,一根假阴线就能把短期均线砸弯。

解决:抓取K线时统一带fq=qfq前复权参数,见4.3的抓取函数。如果用的是本地文件数据,下载时选择“前复权”版本再入库。这里有个坑:前复权数据的最新价格是当前真实价,但历史价格随着每次除权会整体变化,所以你本地存K线时最好每天重新拉一次,别让旧文件里的历史前复权价格和新文件算出来的不一致。

5.5 盘中刷新卡死:matplotlib内存泄漏

现象:用循环方式刷新分时图,运行几分钟后界面越来越卡,内存占用从200MB一路涨到1GB以上,最后直接无响应。

原因:每次刷新都调用plt.subplots创建新的figure和axes对象,但旧对象没有关闭,matplotlib不会自动回收。加上循环里每次用pd.concat复制全部历史数据,数据量随时间线性增长,内存就爆了。

解决:复用同一个figure对象,只更新线条数据,不要重建;或者用第6章的FuncAnimation方案。如果一定要用循环,每次循环末尾加plt.close(fig)手动释放。另外一个隐藏问题是免费接口的轮询频率,我早期每200毫秒拉一次,三分钟就被限流了,后来把间隔放到1.5秒以上,才稳定跑完整个交易日。

6. 进阶:用FuncAnimation做每秒刷新不卡顿的分时图

盘中实时刷新分时图,核心只用matplotlib的FuncAnimation就能解决,不必上pyqtgraph那种重型框架。FuncAnimation的优势在于只维护一个figure对象,每一帧更新线条数据而不是重建整个图表,内存稳定在固定水平。看一个能直接跑的最小模板:

import pandas as pd import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation # 全局缓存,保存当天所有分钟数据 cache = pd.DataFrame(columns=["price", "volume", "amount"]) fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True) line_price, = ax1.plot([], [], color="#1E90FF", lw=1.2) line_avg, = ax1.plot([], [], color="#FFD700", lw=1.0) ax1.axhline(10.0, color="#888888", ls="--") # pre_close 占位 def update(frame): global cache snap = fetch_snapshot("sh600000") # 复用第2章的抓取函数 new_row = pd.DataFrame([{ "price": float(snap["price"]), "volume": int(snap["volume"]), "amount": float(snap["amount"]), }], index=pd.DatetimeIndex([pd.Timestamp.now()])) # 同一分钟内的数据用最新值覆盖,避免重复追加 if not cache.empty and cache.index[-1].floor("min") == new_row.index[-1].floor("min"): cache.iloc[-1] = new_row.iloc[0] else: cache = pd.concat([cache, new_row]) # 更新价格线 line_price.set_data(cache.index, cache["price"]) avg_price = cache["amount"].cumsum() / cache["volume"].cumsum() line_avg.set_data(cache.index, avg_price) ax2.clear() ax2.bar(cache.index, cache["volume"], width=60/1440*0.6) ax1.relim() ax1.autoscale_view() return line_price, line_avg ani = FuncAnimation(fig, update, interval=1000, blit=False) plt.show()

interval=1000表示每秒触发一次update,这个频率刚好匹配免费接口的秒级推送。如果你拿到的是三秒一推的数据,改成3000更合理。blit=False是保守选择,用它虽然稍慢但是不容易出残影问题,等图画稳定了再改blit=True拿性能提升。注意ax2.clear()会重建成交量子图,数据量小的时候无所谓,等分钟数据超过几百根后如果想进一步优化,可以把成交量柱也改成复用对象,但那是锦上添花的事。

这个实时方案和数据源的交互方式值得多说一句:fetch_snapshot每次返回的volume是当日累计成交量,不是这一秒的新增量,所以分钟聚合时要用iloc[-1]覆盖当前分钟的数据,而不是累加。我曾在这个地方翻过车,把累计量当增量累加,结果成交量柱一路飙升到天文数字,图完全没法看。判断“当前分钟是否已存在”我用floor("min")比较分钟标签,这样即使快照时间带秒也能对齐到分钟级别。

做这个进阶方案前,先把前面几章的脏数据清洗、复权处理和刷新频率控制都准备好,实时分时图才能真正跑一个交易日。我自己踩过最深的坑就是没做频率控制,头天晚上写好脚本,第二天开盘五分钟就被限流,然后整个下午都在调试重连逻辑。后来把抓取模块加了指数退避重试,连续失败三次自动暂停,才做到盘中稳定不中断。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询