2026年5月3日 星期日

Python 學習筆記 : 用 bokeh 繪製 K 線圖 (二)

Bokeh 的優點是高自由度與高效能, 但其 API 較低階, 故並未內建像 Plotly 的 CandleStick 那樣的類別, 本篇旨在為 Bokey 山寨一個 CandleStick 來簡化 K 線圖的繪製. 為了讓程式碼不至於過度複雜, 以下測試不處理非交易日 K 棒空缺問題. 

本系列全部測試文章索引參考 :



4. 用封裝的類別繪製 K 線圖 :   

下面範例改寫自前一篇測試的 bokeh_candlestick_3.py, 將繪製 K 線圖的邏輯封裝在自訂的 BokehChart 類別裡, 只要呼叫其建構式並傳入 title/width/height 參數建立物件, 然後在主程式中呼叫 add_candlestick(df) 即可繪製 K 線圖, 程式碼如下 : 

# bokeh_candlestick_class_1.py
import yfinance as yf
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span

class BokehChart:
    """封裝 Bokeh K線圖與技術分析工具的繪圖類別"""
    def __init__(self, title="K線圖", width=800, height=400):
        # 建立並初始化畫布
        self.fig=figure(
            x_axis_type='datetime', 
            title=title, 
            width=width,
            height=height,
            tools='xpan, xwheel_zoom, box_zoom, reset, save'
            )
        self.fig.grid.grid_line_alpha=0.3
        
        # 初始化虛線十字游標
        self._add_crosshair()

    def _add_crosshair(self):
        """內部方法:加入虛線十字游標"""
        w_span=Span(dimension="width", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        h_span=Span(dimension="height", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        self.fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

    def add_candlestick(self, df, date_col='Date', open_col='Open', high_col='High', low_col='Low', close_col='Close', vol_col='Volume'):
        """對外介面:加入 K 線圖與懸停工具"""
        # 複製資料以避免修改到原始 DataFrame
        data=df.copy()
        
        # 定義漲跌顏色 (紅漲綠跌)
        data['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(data[open_col], data[close_col])]
        
        # 建立資料來源
        source=ColumnDataSource(data)
        
        # 繪製上下影線
        self.fig.segment(date_col, high_col, date_col, low_col, color="black", source=source)
        
        # 繪製 K 棒 (12 小時寬度)
        width_ms=12 * 60 * 60 * 1000
        self.fig.vbar(date_col, width_ms, open_col, close_col, 
                      fill_color='color', line_color='black', source=source)
        
        # 加入互動式懸停工具 (HoverTool)
        hover=HoverTool(
            tooltips=[
                ("日期", f"@{date_col}{{%F}}"),
                ("開盤", f"@{open_col}{{0.00}}"),
                ("收盤", f"@{close_col}{{0.00}}"),
                ("最高", f"@{high_col}{{0.00}}"),
                ("最低", f"@{low_col}{{0.00}}"),
                ("成交量", f"@{vol_col}{{0,0}}")
                ],
            formatters={f'@{date_col}': 'datetime'}
            )
        self.fig.add_tools(hover)

    def show(self):
        """顯示圖表"""
        show(self.fig)

# ==========================================
# 主程式:使用類別繪圖 (如同 Plotly 般簡潔)
# ==========================================
if __name__ == "__main__":
    # 1. 下載真實 OHLCV 資料
    df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    df=df.reset_index()

    # 2. 實例化畫布並繪圖
    chart=BokehChart(title='0050.TW 台灣50 ETF K線圖 (2026-03 ~ 2026-04)', width=800, height=400)
    chart.add_candlestick(df)
    
    # 3. 顯示圖表
    chart.show()

結果如下 :




5. 用封裝的類別繪製 K 線圖 + 成交量副圖 :   

如果要同時繪製 K 線圖與成交量副圖, 則須匯入 bokeh.layouts.column() 函式進行垂直布局, 讓主圖 (fig_k) 與副圖能共用 X 軸並連動. 程式碼改寫如下 : 

# bokeh_candlestick_class_2.py
import yfinance as yf
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter
from bokeh.layouts import column  # 引入垂直佈局工具

class BokehChart:
    """封裝 Bokeh K線圖與成交量子圖的繪圖類別"""
    
    # 稍微調整初始化參數,允許分別設定主圖與副圖的高度
    def __init__(self, title="K線圖", width=800, height_k=400, height_v=150):
        # 1. 建立主圖 (K線圖)
        self.fig_k=figure(
            title=title, 
            width=width, height=height_k,
            tools='xpan, xwheel_zoom, box_zoom, reset, save'
            )
        self.fig_k.grid.grid_line_alpha=0.3
        self.fig_k.xaxis.visible=False  # 隱藏主圖的 X 軸,讓畫面更緊湊
        
        # 2. 建立副圖 (成交量圖)
        self.fig_v=figure(
            width=width, height=height_v,
            x_range=self.fig_k.x_range,  # 【關鍵連動】將副圖的 X 軸範圍綁定為主圖的 X 軸
            tools='xpan, xwheel_zoom, reset'
            )
        self.fig_v.grid.grid_line_alpha=0.3
        self.fig_v.y_range.start=0     # 確保成交量 Y 軸從 0 開始
        
        # 3. 組合為垂直佈局
        self.layout=column(self.fig_k, self.fig_v)
        
        # 4. 初始化虛線十字游標 (加在兩張圖上)
        self._add_crosshair(self.fig_k)
        self._add_crosshair(self.fig_v)

    def _add_crosshair(self, fig):
        """內部方法:為指定的畫布加入虛線十字游標"""
        w_span=Span(dimension="width", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        h_span=Span(dimension="height", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

    def add_candlestick(self, df, date_col='Date', open_col='Open', high_col='High', low_col='Low', close_col='Close', vol_col='Volume'):
        """對外介面:處理資料並繪製 K 線與成交量"""
        data=df.copy()
        
        # 建立連續整數序列 (消除假日空缺)
        data['seq']=range(len(data))
        
        # 定義漲跌顏色
        data['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(data[open_col], data[close_col])]
        
        # 用 Python 預先處理好 Bokeh 風格的日期字串
        date_map={}
        prev_year=None
        for i, d in enumerate(data[date_col]):
            label=f"{d.strftime('%b')} {d.day}"
            if i == 0 or d.year != prev_year:
                label += f"\n{d.year}"
            date_map[str(i)]=label
            prev_year=d.year
            
        source=ColumnDataSource(data)
        width_val=0.8  # 因為 X 軸變成了連續整數,寬度直接用比例 0.8
        
        # --- 繪製主圖 (K線) ---
        self.fig_k.segment('seq', high_col, 'seq', low_col, color="black", source=source)
        self.fig_k.vbar('seq', width_val, open_col, close_col, fill_color='color', line_color='black', source=source)
        
        # --- 繪製副圖 (成交量) ---
        # 使用 vbar,top 參數指定為成交量欄位
        self.fig_v.vbar('seq', width_val, top=vol_col, fill_color='color', line_color='black', source=source)
        
        # --- 設定副圖的 X 軸日期標籤 ---
        js_code="""
            var idx=Math.round(tick).toString();
            if (date_map[idx] !== undefined) {
                return date_map[idx];
            } else {
                return "";
            }
        """
        self.fig_v.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={'date_map': date_map})
        self.fig_v.xaxis.major_label_orientation=0 
        self.fig_v.xaxis.major_label_standoff=15
        
        # --- 加入互動式懸停工具 ---
        hover=HoverTool(
            tooltips=[
                ("日期", f"@{date_col}{{%F}}"),
                ("開盤", f"@{open_col}{{0.00}}"),
                ("收盤", f"@{close_col}{{0.00}}"),
                ("最高", f"@{high_col}{{0.00}}"),
                ("最低", f"@{low_col}{{0.00}}"),
                ("成交量", f"@{vol_col}{{0,0}}")
                ],
            formatters={f'@{date_col}': 'datetime'},
            mode='vline'  # 【推薦】改為 vline 模式,滑鼠對齊同一個垂直線就會觸發
            )
        self.fig_k.add_tools(hover)
        self.fig_v.add_tools(hover)

    def show(self):
        """顯示圖表佈局"""
        show(self.layout)  # 【修改】現在是顯示整個 layout 而不是單一 fig

# ==========================================
# 主程式
# ==========================================
if __name__ == "__main__":
    # 1. 下載實盤 OHLCV 資料
    df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    df=df.reset_index()

    # 2. 建立畫布並繪圖
    chart=BokehChart(title='0050.TW 台灣50 ETF (含同步成交量)')
    chart.add_candlestick(df)
    
    # 3. 顯示圖表
    chart.show()

注意, 此例之 BokehChart 類別還封裝了處理非交易日空缺的邏輯, 這讓整個畫布上的 K 棒看起來是連續無空缺的, 結果如下 : 




6. 用封裝的類別繪製 K 線圖 + 成交量 + 技術指標副圖 : 

上面範例中的 BokehChart 類別把主圖與副圖邏輯都寫在類別裡, 每次要添加技術指標副圖都要去改類別並不符合軟工原則, 若要讓程式碼具有擴充性, 必須對架構做一次重構, 要建立一個 動態串列 self.figures = [] 的來儲存所有畫布, 並增加通用副圖方法, 例如 add_bar_subplot() (畫成交量, MACD柱狀圖) 與 add_line_subplot() (畫 RSI, 均線).

程式碼如下 : 

# bokeh_candlestick_class_3.py
import yfinance as yf
import pandas as pd
import pandas_ta as ta  # 
from bokeh.plotting import figure
from bokeh.io import show as show_bokeh # 避免名稱衝突
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter
from bokeh.layouts import column

class BokehChart:
    """高擴充性 Bokeh 繪圖類別,支援動態疊加無限多個副圖"""
    
    def __init__(self, title="K線圖", width=800, main_height=400):
        self.width=width
        self.title=title
        self.main_height=main_height
        
        # 動態儲存所有畫布的清單 (主圖會在 index 0)
        self.figures=[]
        self.source=None
        self.date_map={}
        self.fig_k=None # 參照主圖,用來同步 x_range

    def _add_crosshair(self, fig):
        """內部方法:加入虛線十字游標"""
        w_span=Span(dimension="width", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        h_span=Span(dimension="height", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
        fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

    def add_candlestick(self, df, date_col='Date', open_col='Open', high_col='High', low_col='Low', close_col='Close'):
        """【步驟 1】:初始化主圖與共用資料源"""
        data=df.copy()
        
        # 建立連續整數序列 (消除假日空缺)
        data['seq']=range(len(data))
        data['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(data[open_col], data[close_col])]
        
        # 製作日期對照表
        self.date_map={}
        prev_year=None
        for i, d in enumerate(data[date_col]):
            label=f"{d.strftime('%b')} {d.day}"
            if i == 0 or d.year != prev_year:
                label += f"\n{d.year}"
            self.date_map[str(i)]=label
            prev_year=d.year
            
        # 建立全局共用的資料源 (此時 df 裡面已經包含外部算好的 RSI 等指標)
        self.source=ColumnDataSource(data)
        
        # 建立主畫布
        self.fig_k=figure(title=self.title, width=self.width, height=self.main_height, tools='xpan, xwheel_zoom, box_zoom, reset, save')
        self.fig_k.grid.grid_line_alpha=0.3
        
        # 繪製 K 線
        self.fig_k.segment('seq', high_col, 'seq', low_col, color="black", source=self.source)
        self.fig_k.vbar('seq', 0.8, open_col, close_col, fill_color='color', line_color='black', source=self.source)
        
        # 設定主圖 Hover
        hover=HoverTool(
            tooltips=[("日期", f"@{date_col}{{%F}}"), ("開盤", f"@{open_col}{{0.00}}"), ("收盤", f"@{close_col}{{0.00}}")],
            formatters={f'@{date_col}': 'datetime'}, mode='vline'
            )
        self.fig_k.add_tools(hover)
        self._add_crosshair(self.fig_k)
        
        # 加入畫布清單
        self.figures.append(self.fig_k)

    def add_bar_subplot(self, col_name, title="", color_col=None, color='blue', height=150):
        """通用介面:新增柱狀圖副圖 (例如:成交量、MACD柱)"""
        fig=figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools='xpan, xwheel_zoom, reset')
        fig.grid.grid_line_alpha=0.3
        fig.y_range.start=0
        
        # 若有指定顏色欄位 (如成交量的紅綠色),則使用該欄位;否則使用單一顏色
        fill_c=color_col if color_col else color
        fig.vbar('seq', 0.8, top=col_name, fill_color=fill_c, line_color='black', source=self.source)
        
        # 專屬 Hover
        hover=HoverTool(tooltips=[("日期", "@Date{%F}"), (title or col_name, f"@{col_name}{{0,0}}")], formatters={'@Date': 'datetime'}, mode='vline')
        fig.add_tools(hover)
        self._add_crosshair(fig)
        self.figures.append(fig)

    def add_line_subplot(self, col_name, title="", color='blue', height=150, hlines=None):
        """通用介面:新增折線圖副圖 (例如:RSI、均線、KD)"""
        fig=figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools='xpan, xwheel_zoom, reset')
        fig.grid.grid_line_alpha=0.3
        
        fig.line('seq', col_name, color=color, line_width=1.5, source=self.source)
        
        # 支援加入水平參考線 (例如 RSI 的 30, 70 超買超賣線)
        if hlines:
            for y_val in hlines:
                line=Span(location=y_val, dimension='width', line_color='gray', line_dash='dashed', line_alpha=0.5)
                fig.add_layout(line)

        # 專屬 Hover
        hover=HoverTool(tooltips=[("日期", "@Date{%F}"), (title or col_name, f"@{col_name}{{0.00}}")], formatters={'@Date': 'datetime'}, mode='vline')
        fig.add_tools(hover)
        self._add_crosshair(fig)
        self.figures.append(fig)

    def show(self):
        """渲染圖表:隱藏除最後一張圖外的 X 軸,並套用自訂日期標籤"""
        for i, fig in enumerate(self.figures):
            # 不是最後一張圖,隱藏 X 軸
            if i < len(self.figures) - 1:
                fig.xaxis.visible=False
            # 是最後一張圖,掛上 JavaScript 日期查表邏輯
            else:
                js_code="""
                    var idx=Math.round(tick).toString();
                    if (date_map[idx] !== undefined) {
                        return date_map[idx];
                        }
                    else {
                        return "";
                        }
                """
                fig.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={'date_map': self.date_map})
                fig.xaxis.major_label_orientation=0
                fig.xaxis.major_label_standoff=15

        # 垂直疊加所有被加入的圖表
        layout=column(*self.figures)
        show_bokeh(layout)


# ==========================================
# 主程式:展現架構擴充性的時刻
# ==========================================
if __name__ == "__main__":
    # 1. 獲取資料
    df=yf.download('0050.tw', start='2025-10-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    df=df.reset_index()

    # 2. 在外部計算所有技術指標 (類別完全不需要知道 RSI 怎麼算的)
    df['RSI']=ta.rsi(df['Close'], length=14)

    # 3. 開始繪圖 (如堆積木般直覺)
    chart=BokehChart(title='0050.TW 台灣50 ETF (整合 RSI 指標)')
    
    # 初始化主圖與資料源
    chart.add_candlestick(df)
    
    # 呼叫通用介面新增成交量 (使用 color 欄位決定紅綠)
    chart.add_bar_subplot('Volume', title='成交量', color_col='color', height=100)
    
    # 呼叫通用介面新增 RSI (自訂顏色,並加上 30/70 水平參考線)
    chart.add_line_subplot('RSI', title='RSI(14)', color='purple', height=150, hlines=[30, 70])
    
    # 顯示成果
    chart.show()

此例使用 pandas_ta 套件計算 RSI 指標值, 結果放進 df['RSI'] 欄位, 結果如下 : 




7. 獨立的 Bokeh K 線圖模組 : 

我們可以將上面範例的 BokehChart 類別寫在一個獨立的模組檔案例如 bokeh_chart.py 中, 再用 import 方式匯入 BokehChart 類別來繪製 K 線圖, 這樣主程式檔就會較簡潔了. 

模組檔案 bokeh_chart.py 內容如下 :

# bokeh_chart.py
import pandas as pd
from bokeh.plotting import figure
from bokeh.io import show as show_bokeh
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter
from bokeh.layouts import column

class BokehChart:
    """高擴充性 Bokeh 繪圖類別,支援動態疊加無限多個副圖"""
    
    def __init__(self, title='K線圖', width=800, main_height=400):
        self.width=width
        self.title=title
        self.main_height=main_height
        self.figures=[]
        self.source=None
        self.date_map={}
        self.fig_k=None 

    def _add_crosshair(self, fig):
        """加入虛線十字游標"""
        w_span=Span(dimension='width', line_dash='dashed', line_color="gray", line_alpha=0.6, line_width=1)
        h_span=Span(dimension='height', line_dash='dashed', line_color="gray", line_alpha=0.6, line_width=1)
        fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

    def add_candlestick(self, df, date_col='Date', open_col='Open', high_col='High', low_col='Low', close_col='Close'):
        """初始化主圖與共用資料源"""
        data=df.copy()
        # 建立連續整數序列與漲跌顏色
        data['seq']=range(len(data))
        data['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(data[open_col], data[close_col])]
        # 製作日期對照表
        self.date_map={}
        prev_year=None
        for i, d in enumerate(data[date_col]):
            label=f"{d.strftime('%b')} {d.day}"
            if i == 0 or d.year != prev_year:
                label += f"\n{d.year}"
            self.date_map[str(i)]=label
            prev_year=d.year
        self.source=ColumnDataSource(data)
        self.fig_k=figure(title=self.title, width=self.width, height=self.main_height, tools='xpan, xwheel_zoom, box_zoom, reset, save')
        self.fig_k.grid.grid_line_alpha=0.3
        self.fig_k.segment('seq', high_col, 'seq', low_col, color="black", source=self.source)
        self.fig_k.vbar('seq', 0.8, open_col, close_col, fill_color='color', line_color='black', source=self.source)
        hover=HoverTool(
            tooltips=[("日期", f"@{date_col}{{%F}}"), ("開盤", f"@{open_col}{{0.00}}"), ("收盤", f"@{close_col}{{0.00}}")],
            formatters={f'@{date_col}': 'datetime'}, mode='vline'
            )
        self.fig_k.add_tools(hover)
        self._add_crosshair(self.fig_k)
        self.figures.append(self.fig_k)

    def add_bar_subplot(self, col_name, title="", color_col=None, color='blue', height=150, on_fig=None, y_min_zero=False):
        """新增柱狀圖副圖 (支援傳入畫布疊加到現有畫布)"""
        # 如果有指定 on_fig,就用現成的畫布;否則建立新畫布
        if on_fig:
            fig=on_fig
        else:
            fig=figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools='xpan, xwheel_zoom, reset')
            fig.grid.grid_line_alpha=0.3
            self.figures.append(fig) # 只有新畫布才需要加入清單
            self._add_crosshair(fig) # 只有新畫布才需要加十字線
        # 只有成交量這類數值絕對為正的圖表,才需要強制 Y 軸從 0 開始
        if y_min_zero:
            fig.y_range.start=0
        fill_c=color_col if color_col else color
        fig.vbar('seq', 0.8, top=col_name, fill_color=fill_c, line_color='black', source=self.source)
        # 專屬 Hover
        hover=HoverTool(tooltips=[("日期", "@Date{%F}"), (title or col_name, f"@{col_name}{{0.00}}")], formatters={'@Date': 'datetime'}, mode='vline')
        fig.add_tools(hover)
        return fig # 回傳畫布讓後續的線可以畫在它上面

    def add_line_subplot(self, col_name, title="", color='blue', height=150, hlines=None, on_fig=None):
        """新增折線圖副圖 (支援疊加到現有畫布)"""
        # 如果有指定 on_fig,就用現成的畫布;否則建立新畫布
        if on_fig:
            fig = on_fig
        else:
            fig = figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools='xpan, xwheel_zoom, reset')
            fig.grid.grid_line_alpha = 0.3
            self.figures.append(fig)
            self._add_crosshair(fig)
        fig.line('seq', col_name, color=color, line_width=1.5, source=self.source)
        if hlines:
            for y_val in hlines:
                line = Span(location=y_val, dimension='width', line_color='gray', line_dash='dashed', line_alpha=0.5)
                fig.add_layout(line)
        hover = HoverTool(tooltips=[("日期", "@Date{%F}"), (title or col_name, f"@{col_name}{{0.00}}")], formatters={'@Date': 'datetime'}, mode='vline')
        fig.add_tools(hover)
        return fig # 回傳畫布讓後續的線可以畫在它上面

    def show(self):
        """渲染圖表"""
        for i, fig in enumerate(self.figures):
            if i < len(self.figures) - 1:
                fig.xaxis.visible=False
            else:
                js_code="""
                    var idx=Math.round(tick).toString();
                    if (date_map[idx] !== undefined) {
                        return date_map[idx];
                    } else {
                        return "";
                    }
                """
                fig.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={'date_map': self.date_map})
                fig.xaxis.major_label_orientation=0
                fig.xaxis.major_label_standoff=15
        layout=column(*self.figures)
        show_bokeh(layout)

欲繪製 K 線圖+成交量+技術指標時, 主程式檔只要先從 bokeh_chart.py 匯入 BokehChart 類別並建立 BokehChart 物件後, 呼叫 add_candlestick() 繪製主圖 (K 線圖), 呼叫 add_bar_subplot() 或 add_line_subplot() 添加副圖 (成交量 & 技術指標), 下面範例添加了 RSI 與 MACD 副圖 :

# bokeh_candlestick_class_4.py
import yfinance as yf
import pandas as pd
import pandas_ta as ta
from bokeh_chart import BokehChart

if __name__ == "__main__":
    # 1. 獲取資料
    df=yf.download('0050.tw', start='2025-10-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    df=df.reset_index()

    # 2. 計算技術指標
    # RSI
    df['RSI']=ta.rsi(df['Close'], length=14)
    # MACD (將計算結果合併回原本的 df)
    macd_df=ta.macd(df['Close'])
    df=pd.concat([df, macd_df], axis=1)
    # 計算 MACD 柱狀圖的顏色 (大於等於 0 紅色,小於 0 綠色)
    df['macd_color']=['#ff0000' if m >= 0 else '#00aa00' for m in df['MACDh_12_26_9']]

    # 3. 畫圖
    # 畫主圖 : K 線圖
    chart=BokehChart(title='0050.TW 台灣50 ETF (整合 MACD)')
    chart.add_candlestick(df)  
    # 畫成交量副圖 (加上 y_min_zero=True,避免長條圖懸空)
    chart.add_bar_subplot('Volume', title='成交量', color_col='color', height=100, y_min_zero=True)
    # ==========================================
    # 繪製 MACD 複合副圖
    # ==========================================
    # 第一步:先畫 MACD 柱狀圖,並用變數 macd_fig 把畫布接住
    macd_fig=chart.add_bar_subplot('MACDh_12_26_9', title='MACD柱', color_col='macd_color', height=150)
    # 第二步:把 MACD 線畫在 macd_fig 身上
    chart.add_line_subplot('MACD_12_26_9', title='MACD線', color='blue', on_fig=macd_fig)
    # 第三步:把 Signal 線也畫在 macd_fig 身上
    chart.add_line_subplot('MACDs_12_26_9', title='Signal線', color='orange', on_fig=macd_fig)
    # ==========================================

    chart.show()

注意, 此處 MACD 指標因為含有快線, 慢線, 與柱狀圖三個圖形, 通常都是疊在一個畫布上, 所以要分成三個步驟把三個圖層疊上去, 首先繪製柱狀圖, 然後將傳回的畫布物件 macd_fig 在後續繪製快線與慢線時傳給 on_fonfig 參數, 這樣三張圖就會疊在同一張畫布上了. 

結果如下 : 




可見將繪圖類別獨立出去後, 主程式結構就清爽易讀多了. 


Python 學習筆記 : 用 bokeh 繪製 K 線圖 (一)

Bokeh 不像 Plotly 那樣有內建專門繪製 K 線圖的 CandleStick 類別, 它需透過 bokeh.models 模組中的 Segment 物件 (用來繪製上下影線) 與 Vbar 物件 (用來繪製實體紅黑棒) 來達成, 方法雖然較低階, 但卻比 Plotly 擁有更高的自由度與效能, 尤其是要在瀏覽器中渲染數萬根 K 線時, Bokeh 的 WebGL 渲染模式通常比 Plotly 更為輕快, 且保持流暢的縮放和平移效果. 

本系列全部測試文章索引參考 :


首先匯入繪圖用的函式 figure() 與 show() : 

>>> from bokeh.plotting import figure, show     

然後從元件模組 bokeh.models 中匯入匯入用來繪製 K 棒的 ColumnDataSource 類別, 以及繪製互動效果的懸停工具類別 HoverTool : 
  
>>> from bokeh.models import ColumnDataSource, HoverTool  

接著匯入 Pandas 來處理 OHLC 資料 :

>>> import pandas as pd 
>>> data={
    'date': pd.to_datetime(['2026-05-01', '2026-05-02', '2026-05-03', '2026-05-04']),
    'open':  [100, 110, 105, 120],
    'high':  [115, 120, 110, 130],
    'low':   [95, 105, 100, 115],
    'close': [110, 105, 108, 125]
    }
>>> df=pd.DataFrame(data)   

在 df 中添加一個 color 欄位來記錄漲跌顏色 (紅漲綠跌) :

>>> df['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(df.open, df.close)]   

呼叫 ColumnDataSource 類別的建構式 ColumnDataSource() 並傳入 df 建立 ColumnDataSource 物件 : 

>>> source=ColumnDataSource(df)   
>>> type(source)   
<class 'bokeh.models.sources.ColumnDataSource'>  

呼叫 figure() 函式建立畫布物件 : 

>>> fig=figure(  
    x_axis_type='datetime', 
    title='Bokeh K線圖', 
    width=800,
    height=400
    )

呼叫 segment() 函式繪製上下影線 :

>>> fig.segment('date', 'high', 'date', 'low', color='black', source=source)    
GlyphRenderer(id='p1138', ...)   

呼叫 Figure 物件的 vbar() 方法繪製 K 棒 : 

>>> fig.vbar('date', pd.Timedelta(hours=12), 'open', 'close', 
       fill_color='color', line_color='black', source=source)  
GlyphRenderer(id='p1147', ...)

呼叫 HoverTool 類別的建構式建立懸停工具物件 : 

>>> hover=HoverTool(
    tooltips=[
        ("日期", "@date{%F}"),
        ("開盤", "@open"),
        ("收盤", "@close"),
        ("最高", "@high"),
        ("最低", "@low")
        ],
    formatters={'@date': 'datetime'}
    )

呼叫 Figure 物件的 add_tool() 方法將懸停工具物件加入畫布中 :

>>> fig.add_tools(hover)

這樣便可呼叫 show() 函式來顯示畫布了 :

>>> show(fig)   

Bokeh 會自動開啟瀏覽器顯示網頁來呈現畫布內容 :




完整程式碼如下 : 

# bokeh_candlestick_1.py
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool

# OHLC 資料 :
data={
    'date': pd.to_datetime(['2026-05-01', '2026-05-02', '2026-05-03', '2026-05-04']),
    'open':  [100, 110, 105, 120],
    'high':  [115, 120, 110, 130],
    'low':   [95, 105, 100, 115],
    'close': [110, 105, 108, 125]
    }
df=pd.DataFrame(data)

# 定義漲跌顏色
df['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(df.open, df.close)]
source=ColumnDataSource(df)

# 建立畫布
fig=figure(
    x_axis_type='datetime', 
    title='Bokeh K線圖', 
    width=800,
    height=400,
    #tools='pan, wheel_zoom, box_zoom, reset, save'
    )

# 繪製上下影線 (Segment 物件)
fig.segment('date', 'high', 'date', 'low', color="black", source=source)

# 繪製 K 棒 (Vbar 物件)
# width 設定為 12 小時 (以毫秒計算) 以確保條形之間有間隔
fig.vbar('date', pd.Timedelta(hours=12), 'open', 'close', 
       fill_color='color', line_color='black', source=source)

# 加入互動式懸停工具 (HoverTool)
hover=HoverTool(
    tooltips=[
        ("日期", "@date{%F}"),
        ("開盤", "@open"),
        ("收盤", "@close"),
        ("最高", "@high"),
        ("最低", "@low")
        ],
    formatters={'@date': 'datetime'}
    )
fig.add_tools(hover)

# 輸出與顯示
#output_file("candlestick.html")
show(fig)


1. 繪製從 yfinance 下載的實盤 K 線圖 : 

下面範例改從 yfinance 抓取實盤 OHLC 資料來繪製 K 線圖, 程式碼如下 :

# bokeh_candlestick_2.py
import yfinance as yf
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool

# 1. 下載真實 OHLCV 資料
df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
# 攤平 yfinance 最新版本回傳的 MultiIndex 欄位
df.columns=df.columns.map(lambda x: x[0])
# yfinance 的日期會被設為索引, 重設為一般欄位 以利 ColumnDataSource 讀取
df=df.reset_index()  

# 2. 定義漲跌顏色 (紅漲綠跌)
df['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(df['Open'], df['Close'])]

# 3. 建立資料來源
source=ColumnDataSource(df)

# 4. 建立畫布
fig=figure(
    x_axis_type='datetime', 
    title='0050.TW 台灣50 ETF K線圖 (2026-03 ~ 2026-04)', 
    width=800,
    height=400,
    # 加入十字游標 (crosshair) 與鎖定 X 軸縮放(xwheel_zoom) 以利看盤
    tools='xpan, xwheel_zoom, box_zoom, crosshair, reset, save'
    )
# 讓網格線淡一點,視覺更聚焦在 K 棒上
fig.grid.grid_line_alpha=0.3

# 5. 繪製上下影線
fig.segment('Date', 'High', 'Date', 'Low', color="black", source=source)

# 6. 繪製 K 棒
# Bokeh 處理時間軸時寬度用毫秒計算最為精準與穩定。12小時=12 * 60 * 60 * 1000 毫秒
width_ms=12 * 60 * 60 * 1000
fig.vbar('Date', width_ms, 'Open', 'Close', 
         fill_color='color', line_color='black', source=source)

# 7. 加入互動式懸停工具 (HoverTool)
# 加上了數值格式化 {0.00} 讓小數點對齊並順手加入成交量 (Volume)
hover=HoverTool(
    tooltips=[
        ("日期", "@Date{%F}"),
        ("開盤", "@Open{0.00}"),
        ("收盤", "@Close{0.00}"),
        ("最高", "@High{0.00}"),
        ("最低", "@Low{0.00}"),
        ("成交量", "@Volume{0,0}")
        ],
    formatters={'@Date': 'datetime'}
    )
fig.add_tools(hover)

# 8. 輸出與顯示
show(fig)

此例的關鍵之處是 yf 下載的 df 日期欄位會被設為索引, 所以必須呼叫 df.reset_index() 將日期欄位重設為一般欄位, 這樣 ColumnDataSource 物件才能讀得到, 另外, 懸停工具則添加了 yf 取得的成交量資訊, 結果如下 :




可見十字線會隨著滑鼠在畫布中移動, 當指向 K 棒時會出現懸停提示, 顯示 OHLCV 數據. 


2. 客製化的虛線十字輔助線 :   

預設的十字線是實線, 如果要改為虛線須取消 tools 參數中的 crosshair, 然後匯入 CrosshairTool, Span 這兩個類別 :

from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span 

其中 CrosshairTool 用來建立客製化十字輔助線, 而 Span 則用來建立具有 line_dash 屬性的 Span (跨距線) 物件, 然後把這些線覆蓋 (overlay) 到 CrosshairTool 物件上, 程式改寫如下 :

# bokeh_candlestick_3.py
import yfinance as yf
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span

# 1. 下載真實 OHLCV 資料
df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
# 攤平 yfinance 最新版本回傳的 MultiIndex 欄位
df.columns=df.columns.map(lambda x: x[0])
# yfinance 的日期會被設為索引, 重設為一般欄位 以利 ColumnDataSource 讀取
df=df.reset_index()

# 2. 定義漲跌顏色 (紅漲綠跌)
df['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(df['Open'], df['Close'])]

# 3. 建立資料來源
source=ColumnDataSource(df)

# 4. 建立畫布
fig=figure(
    x_axis_type='datetime', 
    title='0050.TW 台灣50 ETF K線圖 (2026-03 ~ 2026-04)', 
    width=800,
    height=400,
    # 鎖定 X 軸縮放(xwheel_zoom) 以利看盤 (取消 crosshair)
    tools='xpan, xwheel_zoom, box_zoom, , reset, save'
    )
# 讓網格線淡一點,視覺更聚焦在 K 棒上
fig.grid.grid_line_alpha=0.3

# 5. 繪製上下影線
fig.segment('Date', 'High', 'Date', 'Low', color="black", source=source)

# 6. 繪製 K 棒
# Bokeh 處理時間軸時寬度用毫秒計算最為精準與穩定。12小時=12 * 60 * 60 * 1000 毫秒
width_ms=12 * 60 * 60 * 1000
fig.vbar('Date', width_ms, 'Open', 'Close', 
         fill_color='color', line_color='black', source=source)

# 7. 加入互動式懸停工具 (HoverTool)
# 加上了數值格式化 {0.00} 讓小數點對齊並順手加入成交量 (Volume)
hover=HoverTool(
    tooltips=[
        ("日期", "@Date{%F}"),
        ("開盤", "@Open{0.00}"),
        ("收盤", "@Close{0.00}"),
        ("最高", "@High{0.00}"),
        ("最低", "@Low{0.00}"),
        ("成交量", "@Volume{0,0}")
        ],
    formatters={'@Date': 'datetime'}
    )
fig.add_tools(hover)

# 8. 加入虛線十字游標 (建立水平與垂直方向的 Span 物件, 設定為 dashed 虛線)
w_span=Span(dimension="width", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
h_span=Span(dimension="height", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

# 9. 輸出與顯示
show(fig)

結果如下 :




可見十字輔助線已經變成虛線了. 


3. 消除非交日空缺 :   

與 Plotly 一樣, Bokeh 在繪製 K 線圖時會自動把 yfinance 下載的 OHLCV 資料 X 軸的日期序列補齊, 使得 K 棒在非交易日無 K 棒出現不連續的空缺. Plotly 的 Figure 物件有內建的 rangebreaks 參數可用來隱藏這些空缺, 但 Bokeh 並沒有. 

在 Bokeh 中可以用 Index Mapping 來解決此問題. 概念是不要把 X 軸當成時間, 而是把它當成 連續的整數數列 (0, 1, 2, 3...), 然後透過一個標籤轉換器騙過使用者的眼睛, 讓 X 軸的數字顯示成對應的日期. 具體而言是透過 CustomJSTickFormatter 來實現, 但如果要維持原本 Bokeh 的 X 軸標籤顯示風格, 必須修改 Javascript 程式碼, 例如 :

# bokeh_candlestick_4.py
import yfinance as yf
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter

# 1. 下載真實 OHLCV 資料
df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
df.columns=df.columns.map(lambda x: x[0])
df=df.reset_index()

# 2. 建立連續整數序列
df['seq']=range(len(df))

# 用 Python 預先將日期轉為 Bokeh 風格的 "Apr 1" 格式
date_map={}
prev_year=None
for i, d in enumerate(df['Date']):
    # 取得月份縮寫與日期 (例如 "Apr 1")
    # strftime('%b') 會輸出英文月份縮寫,d.day 會輸出不補零的日期
    label=f"{d.strftime('%b')} {d.day}"
    # 如果是圖表的第一根 K 棒,或者是跨年的第一根 K 棒,在底下加上年份
    # 使用 \n 來讓 Bokeh 產生換行效果
    if i == 0 or d.year != prev_year:
        label += f"\n{d.year}"
    date_map[str(i)]=label
    prev_year=d.year

# 3. 定義漲跌顏色
df['color']=['#ff0000' if c >= o else '#00aa00' for o, c in zip(df['Open'], df['Close'])]

# 4. 建立資料來源
source=ColumnDataSource(df)

# 5. 建立畫布
fig=figure(
    title='0050.TW 台灣50 ETF K線圖 (完美日期標籤版)', 
    width=800,
    height=400,
    tools='xpan, xwheel_zoom, box_zoom, reset, save'
    )
fig.grid.grid_line_alpha=0.3

# 6. 使用 JavaScript 取出排版好的字串
js_code="""
    var idx=Math.round(tick).toString();
    if (date_map[idx] !== undefined) {
        return date_map[idx];
    } else {
        return "";
    }
"""
fig.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={'date_map': date_map})
# 把傾斜拿掉 (設為 0) 讓標籤恢復水平,這樣帶有 \n 年份的文字才會對齊
fig.xaxis.major_label_orientation=0
# 拉開標籤與 X 軸的間距 (預設通常是 5)
fig.xaxis.major_label_standoff=15

# 7. 繪製上下影線 
fig.segment('seq', 'High', 'seq', 'Low', color="black", source=source)

# 8. 繪製 K 棒
width_val=0.8
fig.vbar('seq', width_val, 'Open', 'Close', 
         fill_color='color', line_color='black', source=source)

# 9. 加入互動式懸停工具 
hover=HoverTool(
    tooltips=[
        ("日期", "@Date{%F}"),
        ("開盤", "@Open{0.00}"),
        ("收盤", "@Close{0.00}"),
        ("最高", "@High{0.00}"),
        ("最低", "@Low{0.00}"),
        ("成交量", "@Volume{0,0}")
        ],
    formatters={'@Date': 'datetime'}
    )
fig.add_tools(hover)

# 10. 加入虛線十字游標 
w_span=Span(dimension="width", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
h_span=Span(dimension="height", line_dash="dashed", line_color="gray", line_alpha=0.6, line_width=1)
fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))

# 11. 輸出與顯示
show(fig)

結果如下 :




不過這樣的程式碼看起來就有點複雜了. 

2026年5月2日 星期六

Python 學習筆記 : 利用語言模型計算技術指標 (二)

前一篇測試中, 我們透過串接 LLM 模型 (OpenAI & Gemini) 要求 AI 生成計算技術指標的程式碼在本機執行運算, 毋須使用例如 Ta-Lib 或 ta, pandas-ta 等技術指標函式庫. 本篇旨在前一篇的基礎上取得 AI 生成的程式碼後算出技術指標 (MACD) 數據, 並繪製 K 線圖. 

本系列全部測試文章索引參考 : 


關於 MACD 技術指標計算, 參考 : 



1. 用 pandas_ta 套件計算 MACD 指標 : 

MACD 由快線 (DIF), 慢線 (DEA) 和 柱狀圖 (Histogram) 三部分組成, 當快速線向上突破訊號線時為買進訊號; 快速線向下跌破訊號線時為賣出訊號. 柱狀圖用來觀察市場動能強弱, 柱狀圖由負轉正表示多方動能變強; 柱狀圖由正轉負表示空方動能變強. 訊號線則是 MACD 快速線的平滑化版本, 用來確認趨勢是否開始反轉. 

MACD 指標預設參數通常是快線 12 日, 慢線 26 日, 信號線 9 日, 以 pandas_ta 套件來說, 呼叫 df.ta.macd() 或 ta.macd() 即可計算這三組 MACD 指標數值. 注意, 在之前測試中呼叫 df.ta.macd() 時傳入 append=True, 這樣 pandas_ta 會將計算出來的 MACD 指標值自動插入 df 的新增欄位中, 預設欄位名稱依序是 MACD_12_26_9 (MACD 線), MACDh_12_26_9 (MACD 柱狀圖), 與 MACDs_12_26_9 (MACD 信號線), 為了後續繪製 K 線圖方便, 這次不使用 append=True 參數, 而是在 df 手動指定傳回的三個 Series 的欄位名稱 :

df[['MACD_line', 'MACD_histogram', 'MACD_signal']]=df.ta.macd(close='Close') 

程式碼如下 : 
 
# ai_stock_test_5.py
import yfinance as yf
import pandas as pd 
import pandas_ta as ta
from kbar import KBar
          
if __name__ == "__main__":
    df=yf.download('0050.tw', start='2024-11-06', end='2025-01-09', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    df[['MACD_line', 'MACD_histogram', 'MACD_signal']]=df.ta.macd(close='Close')
    print(df.tail())
    macd_line_name='MACD_line'       # MACD 線
    macd_signal_name='MACD_signal'   # MACD 訊號線
    macd_histogram_name='MACD_histogram'  # MACD 柱狀圖
    kb=KBar(df)
    histogram_colors=['g' if v >= 0 else 'r' for v in df[macd_histogram_name]]  
    macd_panel=2 
    kb.addplot(
        df[macd_histogram_name],
        panel=macd_panel,
        type='bar',
        color=histogram_colors,
        alpha=0.4,
        ylabel='MACD' # 設定此副圖的 Y 軸標籤
        )
    kb.addplot(
        df[macd_line_name],
        panel=macd_panel, # 必須使用與柱狀圖相同的 Panel
        color='fuchsia', # MACD 線顏色
        width=1.0
        )
    kb.addplot(
        df[macd_signal_name],
        panel=macd_panel, # 必須使用與柱狀圖相同的 Panel
        color='blue',    # 信號線顏色
        width=1.0
        )
    kb.plot(
        volume=True,  # 顯示成交量副圖 (Panel 1)
        mav=(5, 10),  # K 線圖上疊加 5 日和 10 日均線
        title='K 線圖與 MACD 指標'
        )
    
結果如下 :

>>> %Run ai_stock_test_5.py   
[*********************100%***********************]  1 of 1 completed
                Close       High  ...  MACD_histogram  MACD_signal
Date                              ...                             
2025-01-02  46.854988  47.241319  ...       -0.011482     0.202802
2025-01-03  47.325832  47.555217  ...       -0.023311     0.196974
2025-01-06  48.810799  48.847020  ...        0.063469     0.212842
2025-01-07  49.354084  49.740419  ...        0.147264     0.249658
2025-01-08  48.617638  49.100554  ...        0.142791     0.285355

[5 rows x 8 columns]
設定字型為: Microsoft JhengHei
使用指定字型: Microsoft JhengHei
字型候選清單: ['Microsoft JhengHei', 'DejaVu Sans', 'Arial']





2. 串接 OpenAI API 計算 SMA 指標 : 

下面範例是用提示詞要求 LLM 提供 MACD 指標計算函式, 讓本地程式呼叫 exce() 來執行 MACD 計算函式, 最後用 kbar 套件繪製 K 線圖, 提示詞要明確給 LLM 計算 MACD 所需參數, 即快慢線與信號線週期, 另外, 為了後面用 kbar 繪製 K 線圖時取值與運算方便, 還需明確指定 MACD 傳回計算結果時的欄位名稱 :

    計算 MACD 指標, 參數 : fast=12 日, slow=26 日, signal=9 日。
    計算結果的欄位名稱 : MACD_line, MACD_signal, MACD_histogram。
    請注意:務必在 ewm() 函式中設定 min_periods 參數(例如 slow 設為 min_periods=26),
    以確保前期資料不足天數時,算出的結果必須是 NaN。

程式碼如下 : 

# ai_stock_test_6.py
from  openai import OpenAI, APIError 
import yfinance as yf
import pandas as pd
import pandas_ta as ta
from dotenv import dotenv_values
from kbar import KBar

def ask_gpt(
    messages: list[dict[str, str]],
    model: str='gpt-3.5-turbo'
    ) -> str:
    try:
        reply=client.chat.completions.create(
            model=model, 
            messages=messages
            )
        return reply.choices[0].message.content or ''
    except APIError as e:
        return e.message

def ai_helper(df, user_msg):
    role=f'''
        作為一個專業的程式碼生成機器人,
        我需要您的協助來根據特定的用戶需求生成 Python 程式碼。
        為了進行下去,我將提供給您一個遵循格式 {list(df.columns)} 的 DataFrame(df)。
        您的任務是仔細分析用戶的需求並相應地生成 Python 程式碼。
        請注意,您的回應須僅包含代碼本身,並且不應包含任何額外的資訊。
        '''
    # 把 user_msg 加入到 task 的敘述中,讓 AI 知道要算什麼
    task=f'''
        您的任務是開發一個名為 'calculate(df)' 的 Python 函式。
        這個函式應接受一個 DataFrame 作為其參數。確保您僅使用資料集中存在的欄,
        特別是 {list(df.columns)}。        
        用戶的具體運算需求為:【 {user_msg} 】        
        處理後,該函式應返回處理過的 DataFrame。
        您的回應應嚴格包含 'calculate(df)' 函式的 Python 程式碼,
        並排除任何無關的內容。
        '''
    msg=[{"role": "system", "content": role},
         {"role": "user", "content": task}]
    reply_data=ask_gpt(msg)
    # 清理 markdown 語法
    cleaned_code=reply_data.replace("```", "")
    cleaned_code=cleaned_code.replace("python", "")      
    cleaned_code=cleaned_code.strip() # 建議加上 strip() 去除頭尾多餘的空白或換行
    # 傳回程式碼
    return cleaned_code
          
if __name__ == "__main__":
    config=dotenv_values('.env') 
    openai_api_key=config.get('OPENAI_API_KEY')
    client=OpenAI(api_key=openai_api_key)
    df=yf.download('0050.tw', start='2024-11-06', end='2025-01-09', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    prompt='''
    計算 MACD 指標, 參數 : fast=12 日, slow=26 日, signal=9 日。
    計算結果的欄位名稱 : MACD_line, MACD_signal, MACD_histogram。
    請注意:務必在 ewm() 函式中設定 min_periods 參數(例如 slow 設為 min_periods=26),
    以確保前期資料不足天數時,算出的結果必須是 NaN。
    '''
    code_str=ai_helper(df, prompt)
    print(code_str)
    exec(code_str)
    df=calculate(df)
    print(df.tail())
    macd_line_name='MACD_line'       # 快速線
    macd_signal_name='MACD_signal'   # 訊號線
    macd_histogram_name='MACD_histogram'  # 柱狀圖
    kb=KBar(df)
    histogram_colors=['g' if v >= 0 else 'r' for v in df[macd_histogram_name]]  
    macd_panel=2 
    kb.addplot(
        df[macd_histogram_name],
        panel=macd_panel,
        type='bar',
        color=histogram_colors,
        alpha=0.4,
        ylabel='MACD' # 設定此副圖的 Y 軸標籤
        )
    kb.addplot(
        df[macd_line_name],
        panel=macd_panel, # 必須使用與柱狀圖相同的 Panel
        color='fuchsia', # MACD 線顏色
        width=1.0
        )
    kb.addplot(
        df[macd_signal_name],
        panel=macd_panel, # 必須使用與柱狀圖相同的 Panel
        color='blue',    # 信號線顏色
        width=1.0
        )
    kb.plot(
        volume=True,  # 顯示成交量副圖 (Panel 1)
        mav=(5, 10),  # K 線圖上疊加 5 日和 10 日均線
        title='K 線圖與 MACD 指標'
        )

執行結果如下 :

>>> %Run ai_stock_test_6.py   
[*********************100%***********************]  1 of 1 completed
def calculate(df):
    fast = 12
    slow = 26
    signal = 9
    
    ema_fast = df['Close'].ewm(span=fast, min_periods=fast).mean()
    ema_slow = df['Close'].ewm(span=slow, min_periods=slow).mean()
    
    macd_line = ema_fast - ema_slow
    signal_line = macd_line.ewm(span=signal, min_periods=signal).mean()
    macd_histogram = macd_line - signal_line
    
    df['MACD_line'] = macd_line
    df['MACD_signal'] = signal_line
    df['MACD_histogram'] = macd_histogram
    
    return df
                Close       High  ...  MACD_signal  MACD_histogram
Date                              ...                             
2025-01-02  46.854992  47.241323  ...     0.237614       -0.050295
2025-01-03  47.325832  47.555217  ...     0.223323       -0.055554
2025-01-06  48.810799  48.847020  ...     0.231812        0.033190
2025-01-07  49.354080  49.740415  ...     0.261899        0.118182
2025-01-08  48.617638  49.100554  ...     0.291621        0.117175

[5 rows x 8 columns]
設定字型為: Microsoft JhengHei
使用指定字型: Microsoft JhengHei
字型候選清單: ['Microsoft JhengHei', 'DejaVu Sans', 'Arial']



但是, 這結果與上面用 pandas_ta 計算出來的有些出入, 圖形大致是一致, 但仔細看 MACD 數據有些誤差, 我詢問 AI 這個 LLM 生成的 calculate() 算法是否正確, 結論是 : 讓 LLM 生成以 Pandas 為基礎的函式, 會與專業金融套件例如 pandas_ta 計算的結果有些出入, 因為它們在底層實作指數移動平均 (EMA) 遞迴公式時, 對第一天的 EMA 該怎麼決定有所分歧, Pandas 會直接拿資料陣列中的第一筆收盤價當作 EMA 的初始種子, 然後從第二天開始套用 EMA 公式; 而 pandas_ta 在計算 12 日 EMA 時會先精準計算前 12 天的簡單移動平均 (SMA)作為第 12 天的 EMA 初始種子, 然後從第 13 天開始才套用 EMA 公式.

在前一篇 SMA 測試不會出現因計算細節分岐產生的錯誤 (因為 SMA 沒有遞迴依賴, 也不需要初始種子), 只要指標名稱裡帶有 "E" (Exponential 指數型, 例如 EMA, MACD) 或者是 "S" / "W" (Smoothed / Weighted 平滑加權型, 例如 RSI, WMA), 在跨套件核對時往往就需要特別去確認底層對第一筆初始值的實作邏輯. 

從以上測試得到一個教訓 : 金融計算應該用專業套件自行編碼, 不要靠提示詞叫 AI 生成, 如果直接執行這些未經審查的程式碼, 那麼計算出來的金融資料可能是錯誤的. 如果用 pandas_ta 一行指令就能搞定的事, 為何要用附帶許多注意事項的提示詞來要求 AI 生成可能計算出錯誤結果的程式碼? 本系列測試將到此為止, 量化投資程式在計算金融資訊部分還是要用專業套件去計算, 不要透過 AI. 

2026年5月1日 星期五

Python 學習筆記 : 用 plotly 繪製 K 線圖 (二)

在前一篇測試中已對 Plotly 的 K 線圖類別 plotly.graph_objects.CandleStick 的用法有了基本了解, 本篇旨在利用 Figure 物件的 add_trace() 方法在畫布上添加子圖 (例如成交量或 RSI 等技術指標), 同時透過 plotly.subplots.make_subplot() 函式來規劃畫布上的子圖佈局. 

本系列測試文章索引參考 :



4. 添加成交量子圖 : 

要在畫布上繪製子圖有兩種方式, 第一種是使用較低階的絕對領域 (Domain) 劃分法, 此方法須手動切割 Y 軸空間自行布局, 好處是可以客製化布局, 缺點是維護難度較高, 調整彈性較差, 在 "最強 AI 投資分析" 這本書的第四章就是採用此方法, 參考 :


第二種方式是使用 plotly.subplots.make_subplot() 函式, 這是採用網格系統來進行排版的高階 API, 只要指定子圖的列 (row) 與欄 (col), Plotly 會在底層自動計算所有圖表的高度, 對齊與間距, 是一種較簡易的低耦合設計. 

首先匯入子圖布局函式 make_subplots() :

from plotly.subplots import make_subplots 

make_subplots() 的參數說明如下表 :


 參數名稱  說明
 rows  子圖的列數(預設為 1)
 cols  子圖的欄數(預設為 1)
 shared_xaxes  是否共用 X 軸(True / False / 'all' / 'rows' / 'columns')
 shared_yaxes  是否共用 Y 軸(True / False / 'all' / 'rows' / 'columns')
 start_cell  子圖起始位置('top-left' 或 'bottom-left')
 subplot_titles  每個子圖的標題(list)
 specs  自訂每個子圖的型態與配置(例如 type='xy', 'domain', 'scene' 等)
 row_heights  各列高度比例(list,例如 [0.7, 0.3])
 column_widths  各欄寬度比例(list,例如 [0.6, 0.4])
 horizontal_spacing  子圖之間的水平間距(0~1)
 vertical_spacing  子圖之間的垂直間距(0~1)
 insets  設定內嵌子圖(inset charts)的位置與大小
 column_titles  每一欄的標題(list)
 row_titles  每一列的標題(list)
 x_title  整體 X 軸標題
 y_title  整體 Y 軸標題
 figure  將子圖加入既有的 Figure 物件


其中最常用的參數是 rows, cols, shared_xaxes, vertical_spacing, 與 row_heights 等. 

下列程式將畫布做 2 列 1 欄布局, 第一列高度占 70% 放 K 線圖 trace; 第二列高度占 30% 放成交量長條圖 (有做漲紅跌綠顏色設定) :

# plotly_candlestick_5.py
import yfinance as yf
import plotly.graph_objects as go
import pandas as pd
from plotly.subplots import make_subplots

if __name__ == "__main__":
    df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])

    # === 建立子圖 (2 rows: 上列放 K 線圖,下列放成交量) ===
    fig=make_subplots(
        rows=2,
        cols=1,
        shared_xaxes=True,
        vertical_spacing=0.03,
        row_heights=[0.7, 0.3]   # 列高度占比
        )

    # === K 線圖 ===
    price=go.Candlestick(
        x=df.index,
        open=df['Open'],
        high=df['High'],
        low=df['Low'],
        close=df['Close'],
        increasing_line_color='red',
        decreasing_line_color='green',
        name='Price'
        )
    fig.add_trace(price, row=1, col=1)   # K 線圖放 row 1

    # === 成交量顏色(漲紅跌綠) ===
    colors=['red' if c >= o else 'green' for c, o in zip(df['Close'], df['Open'])]
    volume=go.Bar(
        x=df.index,
        y=df['Volume'],
        marker_color=colors,
        name='Volume'
        )
    fig.add_trace(volume, row=2, col=1)  # 成交量放 row 2

    # === 移除非交易日 ===
    date_range=pd.date_range(start=df.index.min(), end=df.index.max())
    breaks=date_range[~date_range.isin(df.index)]
    breaks_list=breaks.tolist()
    fig.update_xaxes(
        rangebreaks=[{'values': breaks_list}],
        showspikes=True,
        spikethickness=1,
        spikecolor='blue',
        spikedash='dot',
        spikemode='across'
        )

    # === Y 軸 spike(兩個子圖都套用)===
    fig.update_yaxes(
        showspikes=True,
        spikethickness=1,
        spikecolor='blue',
        spikedash='dot',
        spikemode='across'
        )

    # === Layout ===
    fig.update_layout(
        title='台灣五十 (0050) K線 + 成交量',
        width=800,
        height=700,
        xaxis_rangeslider_visible=False,
        hovermode='x unified'   # 很重要:跨子圖同步 hover
        )

    fig.show()

注意, 之前沒有子圖時是呼叫 fig=go.Figure() 建立 Figure 物件, 但此處是用 make_subplots() 來建立. 其次, 有子圖的情況時, hovermode 參數務必要設為 'x unified', 否則會有多個 tooltip 分散, 以及子圖之間不同步問題 (無子圖時因所有 trace 都在同一個座標系故不會有這些問題). 

結果如下 :






5. 添加技術指標子圖 : 

接下來要在成交量下面添加 MACD 指標, 使用 pandas-ta 套件計算, 參考 :


這樣畫布上總共有三個子圖 : K 線圖, 成交量, 與 MACD. 

程式碼如下 :

# plotly_candlestick_6.py
import yfinance as yf
import plotly.graph_objects as go
import pandas as pd
import pandas_ta as ta
from plotly.subplots import make_subplots

if __name__ == "__main__":
    # === 下載資料 ===
    df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])

    # === 計算 MACD(pandas-ta)===
    macd=ta.macd(df['Close'], fast=12, slow=26, signal=9)

    # pandas-ta 會回傳三欄 :contentReference[oaicite:0]{index=0}
    df['MACD']=macd['MACD_12_26_9']
    df['MACDs']=macd['MACDs_12_26_9']   # signal
    df['MACDh']=macd['MACDh_12_26_9']   # histogram

    # === 建立子圖 ===
    fig=make_subplots(
        rows=3,
        cols=1,
        shared_xaxes=True,
        vertical_spacing=0.02,
        row_heights=[0.5, 0.2, 0.3]
        )

    # =====================
    # 1. K線圖
    # =====================
    fig.add_trace(
        go.Candlestick(
            x=df.index,
            open=df['Open'],
            high=df['High'],
            low=df['Low'],
            close=df['Close'],
            increasing_line_color='red',
            decreasing_line_color='green',
            name='Price'
            ),
        row=1, col=1
        )

    # =====================
    # 2. 成交量
    # =====================
    vol_colors=['red' if c >= o else 'green'
                  for c, o in zip(df['Close'], df['Open'])]
    fig.add_trace(
        go.Bar(
            x=df.index,
            y=df['Volume'],
            marker_color=vol_colors,
            name='Volume'
            ),
        row=2, col=1
        )

    # =====================
    # 3. MACD
    # =====================
    # Histogram(紅綠柱)
    macd_colors=['red' if v >= 0 else 'green' for v in df['MACDh']]
    fig.add_trace(
        go.Bar(
            x=df.index,
            y=df['MACDh'],
            marker_color=macd_colors,
            name='MACD Hist'
            ),
        row=3, col=1
        )

    # MACD 線
    fig.add_trace(
        go.Scatter(
            x=df.index,
            y=df['MACD'],
            line=dict(color='blue'),
            name='MACD'
            ),
        row=3, col=1
        )
    # Signal 線
    fig.add_trace(
        go.Scatter(
            x=df.index,
            y=df['MACDs'],
            line=dict(color='orange'),
            name='Signal'
            ),
        row=3, col=1
        )

    # =====================
    # 移除非交易日
    # =====================
    date_range=pd.date_range(start=df.index.min(), end=df.index.max())
    breaks=date_range[~date_range.isin(df.index)]
    fig.update_xaxes(
        rangebreaks=[{'values': breaks.tolist()}],
        showspikes=True,
        spikethickness=1,
        spikecolor='blue',
        spikedash='dot',
        spikemode='across'
        )
    fig.update_yaxes(
        showspikes=True,
        spikethickness=1,
        spikecolor='blue',
        spikedash='dot',
        spikemode='across'
        )

    # =====================
    # Layout
    # =====================
    fig.update_layout(
        title='0050 台灣五十 - K線 + 成交量 + MACD',
        width=900,
        height=800,
        xaxis_rangeslider_visible=False,
        hovermode='x unified'
        )

    fig.show()

注意, MACD 的紅綠柱, MACD 線與信號線都是要畫在 row=3 子圖疊在一起, 結果如下 :




Python 學習筆記 : 用 plotly 繪製 K 線圖 (一)

雖然 mplfinance 套件可以繪製精美的 K 線圖, 但那是沒有互動效果的靜態圖表, 也無法直接佈署在網頁應用中. 繪製互動式 K 線圖可以使用 plotly 套件的低階繪圖模組 graph_objects  (注意, plotly.express 模組沒有繪製 K 線圖功能, 它主要用來快速繪製常見統計圖表).

本系列測試文章索引參考 :


關於 plotly.graph_objects 用法參考 :



1. 新版 (v4.0+) Plotly 的輸出方式 : 

Plotly 的繪圖輸出方式已與以往不同, v4.0 之前的 Plotly 主要商業模式是線上繪圖, 預設會將圖表上傳到他們的雲端伺服器 (Chart Studio), 如果要離線繪圖必須明確呼叫 plotly.offline 模組; 然而 Plotly v4 對原本分散的繪圖方式 (離線 vs 雲端) 進行重整, 採取了離線優先原則, 繪製圖表時不需要再區分線上或離線 (原本的雲端功能被拆分出去變成一個獨立的套件 chart-studio), 改為直接呼叫 Figure 物件的方法來處理 :
  • fig.show() : 用於開發過程中的快速預覽 (依據環境自動選擇方式)
  • fig.write_html() : 用於產生最終的報告檔案 (於網頁中繪製圖表)
當呼叫 fig.show() 時, Plotly 會自動偵測程式是在 VS Code, Jupyter Notebook, Google Colab 還是純 Python 腳本中執行, 並選擇最適合的方式來顯示圖表. plotly.offline 的功能已經被 fig.write_html() 取代了, 但為了向下相容, Plotly 官方目前仍保留對 plotly.offline.plot() 的支援. 

注意, fig.write_html() 只是單純輸出 HTML 檔, 不會自動開啟瀏覽器顯示繪圖結果, 如果希望在存檔後順便開啟瀏覽器展示繪製結果, 可以傳入 auto_open=True 參數 (預設 False), 例如 :

fig.write_html("plot.html", auto_open=True)  

或者先呼叫 write_html() 再呼叫 show() :

fig.write_html("plot.html")
fig.show()

除此之外, Plotly v4.0 也把以前的獨立套件 plotly_express 納入主套件中成為 plotly.express 模組, 確立了 Plotly 未來高階 API 使用 plotly.express; 低階 API 使用 plotly. graph_objects 的雙軌應用模式. 


2. 建立 CandleStick 物件繪製 K 線圖 : 

首先匯入 plotly.graph_objects 模組, 通常取簡名為 go :

import plotly.graph_objects as go  

plotly.graph_objects 模組中有一個 Candlestick 用來繪製 K 線圖 : 

>>> import plotly.graph_objects as go   
>>> dir(go)   
['AngularAxis', 'Annotation', 'Annotations', 'Bar', 'Barpolar', 'Box', 'Candlestick', 'Carpet', 'Choropleth', 'Choroplethmap', 'Choroplethmapbox', 'ColorBar', 'Cone', 'Contour', 'Contourcarpet', 'Contours', 'Data', 'Densitymap', 'Densitymapbox', 'ErrorX', 'ErrorY', 'ErrorZ', 'Figure', 'FigureWidget', 'Font', 'Frame', 'Frames', 'Funnel', 'Funnelarea', 'Heatmap', 'Histogram', 'Histogram2d', 'Histogram2dContour', 'Histogram2dcontour', 'Icicle', 'Image', 'Indicator', 'Isosurface', 'Layout', 'Legend', 'Line', 'Margin', 'Marker', 'Mesh3d', 'Ohlc', 'Parcats', 'Parcoords', 'Pie', 'RadialAxis', 'Sankey', 'Scatter', 'Scatter3d', 'Scattercarpet', 'Scattergeo', 'Scattergl', 'Scattermap', 'Scattermapbox', 'Scatterpolar', 'Scatterpolargl', 'Scattersmith', 'Scatterternary', 'Scene', 'Splom', 'Stream', 'Streamtube', 'Sunburst', 'Surface', 'Table', 'Trace', 'Treemap', 'Violin', 'Volume', 'Waterfall', 'XAxis', 'XBins', 'YAxis', 'YBins', 'ZAxis', 'bar', 'barpolar', 'box', 'candlestick', 'carpet', 'choropleth', 'choroplethmap', 'choroplethmapbox', 'cone', 'contour', 'contourcarpet', 'densitymap', 'densitymapbox', 'funnel', 'funnelarea', 'heatmap', 'histogram', 'histogram2d', 'histogram2dcontour', 'icicle', 'image', 'indicator', 'isosurface', 'layout', 'mesh3d', 'ohlc', 'parcats', 'parcoords', 'pie', 'sankey', 'scatter', 'scatter3d', 'scattercarpet', 'scattergeo', 'scattergl', 'scattermap', 'scattermapbox', 'scatterpolar', 'scatterpolargl', 'scattersmith', 'scatterternary', 'splom', 'streamtube', 'sunburst', 'surface', 'table', 'treemap', 'violin', 'volume', 'waterfall']

呼叫 Candlestick 類別的建構式 Candlestick() 會建立一個 Candlestick 物件, 這在 Plotly 文件中被稱為 Trace 物件 (畫布上的數據內容, 例如一組 K 線). 一個完整的 Plotly 圖表 (Figure) 是由 data 和 layout 兩大部分組成的, 而 Candlestick 物件就是放在 data 清單中的一個成員. 

Candlestick() 建構式的參數如下表 : 


 參數名稱  說明
 x  時間序列數據(通常為日期字串或 datetime 物件)。
 open  開盤價序列 (Open prices)。
 high  最高價序列 (High prices)。
 low  最低價序列 (Low prices)。
 close  收盤價序列 (Close prices)。
 increasing_line_color  上漲時 K 線外框與影線的顏色(例:'red')。
 decreasing_line_color  下跌時 K 線外框與影線的顏色(例:'green')。
 name  此數據序列在圖例中顯示的名稱。
 text  滑鼠懸停時顯示的額外文字內容。
 hoverinfo  設定懸停顯示的資訊內容(如 'all', 'x+y' 等)。
 opacity  設定 K 線圖層的透明度(0.0 到 1.0)。
 whiskerwidth  設定上下影線相對於 K 線實體寬度的比例。


其中前 7 個參數 x, open, high, low, close, 以及 increasing_line_color 與 decreasing_line_color (漲跌顏色, 漲紅跌綠) 最常用, 例如 : 

>>> price=go.Candlestick( 
    x=['2026-05-01', '2026-05-02'],
    open=[100, 110],
    high=[115, 120],
    low=[95, 105],
    close=[110, 105],
    increasing_line_color='red',
    decreasing_line_color='green'    
    )
>>> type(price)  
<class 'plotly.graph_objs._candlestick.Candlestick'>   

這樣就建立了一個 CandleStick 物件了. 接下來只要將此 trace 放入串列, 傳給 go.Figure() 的 data 參數即可建立 Figure 畫布物件 : 

>>> fig=go.Figure(data=[price])   
>>> type(fig)   
<class 'plotly.graph_objs._figure.Figure'>  

最後呼叫 fig.show() 即可展示所繪製之圖表 :

>>> fig.show()   

fig.show() 會開啟一個 127.0.0.1:port 的網頁來繪製圖表 :




可見當滑鼠移到 K 棒上時就會動態地彈出其 OHLC 價格資訊. 注意, Plotly 預設會在 K 線圖底下開啟一個範圍滑桿 (RangeSlider 物件), 可透過滑動或縮放來觀察不同時段的價格細節. 若想取消此預設之範圍滑桿, 可呼叫 fig.update_layout() 並傳入 xaxis_rangeslider_visible=False 參數 :

>>> fig.update_layout(xaxis_rangeslider_visible=False)   
Figure({
    'data': [{'close': [110, 105],
              'decreasing': {'line': {'color': 'green'}},
              'high': [115, 120],
              'increasing': {'line': {'color': 'red'}},
              'low': [95, 105],
              'open': [100, 110],
              'type': 'candlestick',
              'x': [2026-05-01, 2026-05-02]}],
    'layout': {'template': '...', 'xaxis': {'rangeslider': {'visible': False}}}
})
>>> fig.show()  




fig.update_layout() 是 Plotly 中最核心的佈局控制方法, 它負責處理圖表內除了數據本身以外的所有視覺元素, 例如標題, 座標軸, 圖例及畫布大小等, 常用參數如下 : 


 參數名稱  說明
 title  圖表標題。可傳入字串或 dict(包含 text, font, x, y 等屬性)。預設為 None。
 width / height  設定畫布的寬度與高度(像素)。預設通常隨容器自動縮放。
 xaxis / yaxis  設定座標軸屬性(如標題、範圍、網格)。例如 xaxis_title='時間'。
 xaxis_rangeslider_visible  設定 X 軸下方的範圍滑桿是否顯示。預設值 True。
 showlegend  是否顯示圖例。預設值:當有多組數據序列時為 True。
 legend  設定圖例的位置與外觀(如 x, y 位置、orientation 'h' 或 'v')。
 margin  設定圖表與邊界的距離。傳入 dict(l, r, t, b) 分別代表左、右、上、下邊距。
 template  設定圖表主題(如 'plotly_dark', 'ggplot2', 'seaborn' 等)。預設為 'plotly'。
 hovermode  設定懸停資訊的觸發模式('x', 'y', 'closest', False)。預設為 'closest'。
 font  設定圖表全域字體(family, size, color)。預設字體依環境而定。
 paper_bgcolor  設定整張畫布(紙張)的背景顏色。預設為 '#fff'。
 plot_bgcolor  設定繪圖區域(座標軸內)的背景顏色。預設為 '#E5ECF6'。


這些參數中以 title, width, heigh, 與 showlegend 等最常用. 

下面範例從 yfinance 取得股價資料後用 plotly 繪製 K 線圖 :

# plotly_candlestick_1.py
import yfinance as yf
import plotly.graph_objects as go

if __name__ == "__main__":
    df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    price=go.Candlestick(
        x=df.index,
        open=df['Open'],
        high=df['High'],
        low=df['Low'],
        close=df['Close'],
        increasing_line_color='red',
        decreasing_line_color='green'    
        )
    fig=go.Figure(data=[price])
    fig.update_layout(
        title='台灣五十股價',
        width=800,
        height=600,
        xaxis_rangeslider_visible=False
        )
    fig.show() 

此處以 width 與 height 參數設定畫布尺寸, 以 title 設定圖表標題, 結果如下 :




3. 移除 K 線圖中的非交易日空格 : 

仔細看上面範例中的 K 線圖, 會發現例假日股市休息, 從 yfinance 取得的收盤資料都會跳過這些日期, 只傳回交易日之資料, 但 Plotly 在繪製 K 線圖時會自動把 X 軸的日期序列補齊, 但因為無 OHLC 欄位值, 所以 K 棒為空格, 看起來不連續, 這可以透過呼叫 fig.update_xaxes() 方法, 並傳入rangebreaks 參數來移除, 其值為一個非交易日的日期串列的字典 (鍵為 values), 例如 :

# plotly_candlestick_2.py
import yfinance as yf
import plotly.graph_objects as go
import pandas as pd  

if __name__ == "__main__":
    df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    price=go.Candlestick(
        x=df.index,
        open=df['Open'],
        high=df['High'],
        low=df['Low'],
        close=df['Close'],
        increasing_line_color='red',
        decreasing_line_color='green'    
        )
    fig=go.Figure(data=[price])
    # 移除 X 軸的非交易日
    date_range=pd.date_range(start=df.index.min(), end=df.index.max())
    breaks=date_range[~date_range.isin(df.index)]  # 取出 df 空缺之日期
    breaks_list=breaks.tolist()  # 轉成串列
    fig.update_xaxes(rangebreaks=[{'values': breaks_list}])  # 移除非交易日空缺
    fig.update_layout(
        title='台灣五十股價',
        width=800,
        height=600,        
        xaxis_rangeslider_visible=False
        )
    fig.show() 

此處使用 Pandas 的 date_range() 函式取得 df 起迄日期間的所有連續日期序列, 然後用串列運算與 df 比對抓出 df 中空缺之日期即為非交易日, 轉成串列後做為字典 values 鍵之值. 結果如下 :




可見非交易日的空 K 棒已消失, 整個 K 線圖 X 軸看起來是連續無空缺了. 

fig.update_xaxes() 常用參數如下表 :


 參數名稱  說明
 title  X 軸標題。可傳入字串或 dict(text='...', font=...)。
 type  座標軸類型:'linear' (數值), 'date' (時間), 'category' (類別), 'log' (對數)。
 range  設定顯示範圍 [min, max],對於時間軸 ['2026-01-01', '2026-05-01']。
 rangebreaks  隱藏特定時間區段(如週末或收盤時段)。常用於金融圖表。
 rangeslider_visible  是否顯示底部的時間範圍滑桿(True/False)。
 showgrid  是否顯示垂直網格線(True/False)。預設為 True。
 gridcolor / gridwidth  設定網格線的顏色與寬度。
 tickformat  設定刻度文字格式(如 '%Y-%m-%d' 顯示日期,'.2f' 顯示兩位小數)。
 tickangle  刻度文字的旋轉角度(例如 -45 代表順時針旋轉 45 度)。
 side  X 軸的位置。可設定為 'bottom' (預設) 或 'top'。
 nticks  設定刻度的最大數量(Plotly 會嘗試在此範圍內自動分配)。
 fixedrange  是否鎖定縮放(True 代表禁止使用者縮放該軸)。
 showspikes  是否顯示 Y 軸輔助線。預設 False (不顯示)。
 spikemode  X 軸輔助線模式 : 'toaxis' (預設指向坐標軸)、'across' (橫跨整個繪圖區)。
 spikecolor  X 軸輔助線顏色 : 顏色名稱或色碼 (預設 None 視資料點自動指配)。
 spikethickness  X 軸輔助線厚度 : 預設 3px。
 spikedash  X 軸輔助線樣式 : 'dash' (預設), 'dot', 'solid'。


除了有 X 軸設定方法 fig.update_xaxes() 外, 還有 Y 軸設定方法 fig.update_yaxes(), 參數結構類似 :


 參數名稱  說明
 title  Y 軸標題。可設定為字串或 dict(text='...', font=...)。
 type  座標軸類型:'linear' (預設)、'log' (對數軸)、'date'。
 range  設定 Y 軸顯示數值範圍。例如 [0, 1000]。
 autorange  是否自動縮放範圍:True, False 或 'reversed'(數值由大到小反轉)。
 showgrid  是否顯示水平網格線(True/False)。
 zeroline  是否加粗顯示數值為 0 的那條基準線。預設為 True。
 tickformat  刻度數值格式。例如 ',d' (千分位整數)、'.2f' (兩位小數)、'%' (百分比)。
 tickprefix / ticksuffix  在刻度數值前後加上符號(如 tickprefix='$' 或 ticksuffix='元')。
 side  Y 軸的位置。可設定為 'left' (預設) 或 'right' (常見於雙 Y 軸設計)。
 secondary_y  多軸圖表中指定此資料是否為第二個 Y 軸(配合 make_subplots 使用)。
 fixedrange  是否固定 Y 軸。設為 True 時,使用者無法透過滑鼠縮放 Y 軸方向。
 showspikes  是否顯示 Y 軸輔助線。預設 False (不顯示)。
 spikemode  Y 軸輔助線模式 : 'toaxis' (預設指向坐標軸)、'across' (橫跨整個繪圖區)。
 spikecolor  Y 軸輔助線顏色 : 顏色名稱或色碼 (預設 None 視資料點自動指配)。
 spikethickness  Y 軸輔助線厚度 : 預設 3px。
 spikedash  Y 軸輔助線樣式 : 'dash' (預設), 'dot', 'solid'。


4. 添加輔助十字線 : 

輔助十字線是當滑鼠在畫布上移動時會出現一個平行於 X 與 Y 軸十字線的互動效果, 開啟輔助十字線顯示需要將 fig.update_xaxes() 與 fig.update_yaxes() 的 showspike 參數設為 True. 例如 :

# plotly_candlestick_3.py
import yfinance as yf
import plotly.graph_objects as go
import pandas as pd

if __name__ == "__main__":
    df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    price=go.Candlestick(
        x=df.index,
        open=df['Open'],
        high=df['High'],
        low=df['Low'],
        close=df['Close'],
        increasing_line_color='red',
        decreasing_line_color='green'    
        )
    fig=go.Figure(data=[price])
    # 移除 X 軸的非交易日
    date_range=pd.date_range(start=df.index.min(), end=df.index.max())
    breaks=date_range[~date_range.isin(df.index)]  # 取出 df 空缺之日期
    breaks_list=breaks.tolist()  # 轉成串列
    fig.update_xaxes(rangebreaks=[{'values': breaks_list}],
                     showspikes=True)
    fig.update_yaxes(showspikes=True)    
    fig.update_layout(
        title='台灣五十股價',
        width=800,
        height=600,        
        xaxis_rangeslider_visible=False
        )
    fig.show() 

此處將 X/Y 軸設定參數 showspikes 設為 True 後, 當滑鼠移到 K 棒上面時就會顯示指向座標軸的 輔助線, 由於 spikemode 預設 'toaxis', 故輔助線是指向坐標軸, 而非貫穿繪圖區的十字線; 由於 spikecolor 預設為 None, 所以滑鼠移到紅 K 棒時輔助線為紅色, 移到綠 K 棒時輔助線為綠色 :





下面是傳入全部輔助線參數的範例 :

# plotly_candlestick_4.py
import yfinance as yf
import plotly.graph_objects as go
import pandas as pd

if __name__ == "__main__":
    df=yf.download('0050.tw', start='2026-03-01', end='2026-04-30', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    price=go.Candlestick(
        x=df.index,
        open=df['Open'],
        high=df['High'],
        low=df['Low'],
        close=df['Close'],
        increasing_line_color='red',
        decreasing_line_color='green'    
        )
    fig=go.Figure(data=[price])
    # 移除 X 軸的非交易日
    date_range=pd.date_range(start=df.index.min(), end=df.index.max())
    breaks=date_range[~date_range.isin(df.index)]  # 取出 df 空缺之日期
    breaks_list=breaks.tolist()  # 轉成串列
    fig.update_xaxes(rangebreaks=[{'values': breaks_list}],
                     showspikes=True,
                     spikethickness=1,
                     spikecolor='blue',
                     spikedash='dot',
                     spikemode='across')
    fig.update_yaxes(showspikes=True,
                     spikethickness=1,
                     spikecolor='blue',
                     spikedash='dot',
                     spikemode='across')  
    fig.update_layout(
        title='台灣五十股價',
        width=800,
        height=600,        
        xaxis_rangeslider_visible=False,
        #hovermode='x'
        )
    fig.show() 

此例傳入輔助線參數更改預設值, 結果如下 :




可見 spikethickness=1 讓輔助線變細了, spikecolor='blue' 則使其顏色不論紅 K 或綠 K 都顯示藍色; 而 spikemode='across' 則使輔助線貫穿整個畫布成為名符其實的十字線. 注意, 預設須當滑鼠移到 K 棒上時才會顯示輔助線, 如果要讓滑鼠在畫布任何位置都顯示輔助線, 則在呼叫 fig.update_layout() 要傳入 hovermode='x' 參數, 因為 K 線圖是按時間 (X 軸) 排列的, 每一格時間通常都有資料, 所以滑鼠左右移動時, 輔助線會非常流暢地跟隨.