You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何mplfinance悬停注释显示NaN而非Open/High等行情数值?

特斯拉周K线图悬停注释显示NaN问题解决

运行基于yfinance、mplfinance编写的Python代码绘制特斯拉周K线图时,图表的悬停注释无法正常显示Open、High、Low、Close及Volume的实际数值,反而出现NaN错误,具体情况如下图所示:

悬停注释显示NaN错误

原错误代码

import yfinance as yf
import mplfinance as mpf
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import pandas as pd
import numpy as np

# Dates to get stock data
start_date = "2020-01-01"
end_date = "2023-06-15"

# Fetch Tesla stock data
tesla_data = yf.download("TSLA", start=start_date, end=end_date)
tesla_weekly_data = tesla_data.resample("W").agg({"Open": "first", "High": "max", "Low": "min", "Close": "last", "Volume": "sum"}).dropna()

# Get the latest closing price
latest_price = tesla_weekly_data['Close'][-1]

# Create additional plot
close_price = tesla_weekly_data['Close']
apd = mpf.make_addplot(close_price, color='cyan', width=2)

# Plot the candlestick chart
fig, axes = mpf.plot(tesla_weekly_data,
                     type='candle',
                     addplot=apd,
                     style='yahoo',
                     title='Tesla Stock Prices',
                     ylabel='Price',
                     xlabel='Date',
                     volume=True,
                     ylabel_lower='Volume',
                     volume_panel=1,
                     figsize=(16, 8),
                     returnfig=True
                     )

# Move the y-axis labels to the left side
axes[0].yaxis.tick_left()
axes[1].yaxis.tick_left()

# Adjust the position of the y-axis label for price
axes[0].yaxis.set_label_coords(-0.08, 0.5)

# Adjust the position of the y-axis label for volume
axes[1].yaxis.set_label_coords(-0.08, 0.5)

# Set y-axis label for price and volume
axes[0].set_ylabel('Price', rotation=0, labelpad=20)
axes[1].set_ylabel('Volume', rotation=0, labelpad=20)

# Make the legend box
handles = axes[0].get_legend_handles_labels()[0]
red_patch = mpatches.Patch(color='red')
green_patch = mpatches.Patch(color='green')
cyan_patch = mpatches.Patch(color='cyan')
handles = handles[:2] + [red_patch, green_patch, cyan_patch]
labels = ["Price Up", "Price Down", "Closing Price"]
axes[0].legend(handles=handles, labels=labels)

# Add a box to display the current price
latest_price_text = f"Current Price: ${latest_price:.2f}"
box_props = dict(boxstyle='round', facecolor='white', edgecolor='black', alpha=0.8)
axes[0].text(0.02, 0.95, latest_price_text, transform=axes[0].transAxes, fontsize=12, verticalalignment='top', bbox=box_props)

# Define hover label format
hover_label_format = [
    ("Open: ", lambda x: f"${x:.2f}"),
    ("High: ", lambda x: f"${x:.2f}"),
    ("Low: ", lambda x: f"${x:.2f}"),
    ("Close: ", lambda x: f"${x:.2f}"),
    ("Volume: ", lambda x: f"{int(x):,}"),
]

# Function to create hover annotations
def hover_annotations(data):
    annot = pd.DataFrame(index=data.index, columns=data.columns)
    annot_visible = False

    texts = []

    def onmove(event):
        nonlocal annot_visible

        if event.inaxes == axes[0]:
            index = int(event.xdata)
            if index >= len(data.index):
                return

            values = data.iloc[index]
            for label, formatter in hover_label_format:
                value = values[label.rstrip(': ')]
                if np.isnan(value):
                    annot.iloc[index][label.rstrip(': ')] = ""
                else:
                    annot.iloc[index][label.rstrip(': ')] = f"{label}{formatter(value)}"

            annot_visible = True
        else:
            annot_visible = False

        for t, text, (x, y) in zip(texts, annot.values, zip([event.xdata] or [], [event.ydata] or [])):
            if isinstance(x, (list, np.ndarray)):
                x = x[0] if len(x) > 0 and not np.isnan(x[0]) else None
            if isinstance(y, (list, np.ndarray)):
                y = y[0] if len(y) > 0 and not np.isnan(y[0]) else None

            if x is not None and y is not None:
                t.set_position((x, y))
                t.set_text('\n'.join(map(str, text)))
                t.set_visible(annot_visible)

        fig.canvas.draw_idle()

    for _ in data.index:
        t = axes[0].text(0, 0, '', visible=False, ha='left', va='top')
        texts.append(t)

    fig.canvas.mpl_connect('motion_notify_event', onmove)

    return annot


# Attach hover annotations to the plot
annotations = hover_annotations(tesla_weekly_data)

# Display the chart
plt.show()

问题根源

  1. x轴索引匹配错误:原代码中index = int(event.xdata)直接将matplotlib的x轴像素坐标转换为整数索引,但mplfinance绘制的图表x轴实际是DatetimeIndex,并非连续整数,导致取到的索引与数据行不匹配,最终读取到NaN值。
  2. 注释文本逻辑混乱:为每个数据行创建注释文本对象,且遍历所有文本更新内容,逻辑冗余且错误,无法正确对应当前悬停的行。

修正方案

修改hover_annotations函数,简化注释逻辑并正确匹配x轴索引:

# Function to create hover annotations
def hover_annotations(data):
    # 仅创建一个注释文本对象,避免冗余
    annot = axes[0].text(0, 0, '', visible=False, ha='left', va='top', 
                         bbox=dict(boxstyle='round', facecolor='white', edgecolor='gray', alpha=0.9))
    annot_visible = False

    def onmove(event):
        nonlocal annot_visible

        if event.inaxes == axes[0]:
            x_val = event.xdata
            if np.isnan(x_val):
                return
            # 通过x轴坐标匹配对应的DatetimeIndex行
            idx = data.index.get_indexer([data.index[int(x_val)]], method='nearest')[0]
            if idx < 0 or idx >= len(data):
                return
            
            values = data.iloc[idx]
            # 构建当前行的注释文本
            annot_text = []
            for label, formatter in hover_label_format:
                col_name = label.rstrip(': ')
                value = values[col_name]
                if not np.isnan(value):
                    annot_text.append(f"{label}{formatter(value)}")
            
            # 更新注释的位置和内容
            annot.set_position((event.xdata, event.ydata))
            annot.set_text('\n'.join(annot_text))
            annot_visible = True
        else:
            annot_visible = False
        
        annot.set_visible(annot_visible)
        fig.canvas.draw_idle()

    fig.canvas.mpl_connect('motion_notify_event', onmove)
    return annot

修正后的完整代码

import yfinance as yf
import mplfinance as mpf
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import pandas as pd
import numpy as np

# Dates to get stock data
start_date = "2020-01-01"
end_date = "2023-06-15"

# Fetch Tesla stock data
tesla_data = yf.download("TSLA", start=start_date, end=end_date)
tesla_weekly_data = tesla_data.resample("W").agg({"Open": "first", "High": "max", "Low": "min", "Close": "last", "Volume": "sum"}).dropna()

# Get the latest closing price
latest_price = tesla_weekly_data['Close'][-1]

# Create additional plot
close_price = tesla_weekly_data['Close']
apd = mpf.make_addplot(close_price, color='cyan', width=2)

# Plot the candlestick chart
fig, axes = mpf.plot(tesla_weekly_data,
                     type='candle',
                     addplot=apd,
                     style='yahoo',
                     title='Tesla Stock Prices',
                     ylabel='Price',
                     xlabel='Date',
                     volume=True,
                     ylabel_lower='Volume',
                     volume_panel=1,
                     figsize=(16, 8),
                     returnfig=True
                     )

# Move the y-axis labels to the left side
axes[0].yaxis.tick_left()
axes[1].yaxis.tick_left()

# Adjust the position of the y-axis label for price
axes[0].yaxis.set_label_coords(-0.08, 0.5)

# Adjust the position of the y-axis label for volume
axes[1].yaxis.set_label_coords(-0.08, 0.5)

# Set y-axis label for price and volume
axes[0].set_ylabel('Price', rotation=0, labelpad=20)
axes[1].set_ylabel('Volume', rotation=0, labelpad=20)

# Make the legend box
handles = axes[0].get_legend_handles_labels()[0]
red_patch = mpatches.Patch(color='red')
green_patch = mpatches.Patch(color='green')
cyan_patch = mpatches.Patch(color='cyan')
handles = handles[:2] + [red_patch, green_patch, cyan_patch]
labels = ["Price Up", "Price Down", "Closing Price"]
axes[0].legend(handles=handles, labels=labels)

# Add a box to display the current price
latest_price_text = f"Current Price: ${latest_price:.2f}"
box_props = dict(boxstyle='round', facecolor='white', edgecolor='black', alpha=0.8)
axes[0].text(0.02, 0.95, latest_price_text, transform=axes[0].transAxes, fontsize=12, verticalalignment='top', bbox=box_props)

# Define hover label format
hover_label_format = [
    ("Open: ", lambda x: f"${x:.2f}"),
    ("High: ", lambda x: f"${x:.2f}"),
    ("Low: ", lambda x: f"${x:.2f}"),
    ("Close: ", lambda x: f"${x:.2f}"),
    ("Volume: ", lambda x: f"{int(x):,}"),
]

# Function to create hover annotations
def hover_annotations(data):
    # 仅创建一个注释文本对象,避免冗余
    annot = axes[0].text(0, 0, '', visible=False, ha='left', va='top', 
                         bbox=dict(boxstyle='round', facecolor='white', edgecolor='gray', alpha=0.9))
    annot_visible = False

    def onmove(event):
        nonlocal annot_visible

        if event.inaxes == axes[0]:
            x_val = event.xdata
            if np.isnan(x_val):
                return
            # 通过x轴坐标匹配对应的DatetimeIndex行
            idx = data.index.get_indexer([data.index[int(x_val)]], method='nearest')[0]
            if idx < 0 or idx >= len(data):
                return
            
            values = data.iloc[idx]
            # 构建当前行的注释文本
            annot_text = []
            for label, formatter in hover_label_format:
                col_name = label.rstrip(': ')
                value = values[col_name]
                if not np.isnan(value):
                    annot_text.append(f"{label}{formatter(value)}")
            
            # 更新注释的位置和内容
            annot.set_position((event.xdata, event.ydata))
            annot.set_text('\n'.join(annot_text))
            annot_visible = True
        else:
            annot_visible = False
        
        annot.set_visible(annot_visible)
        fig.canvas.draw_idle()

    fig.canvas.mpl_connect('motion_notify_event', onmove)
    return annot


# Attach hover annotations to the plot
annotations = hover_annotations(tesla_weekly_data)

# Display the chart
plt.show()

内容的提问来源于stack exchange,提问作者Damian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 03:52:03