2026年5月3日 星期日

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)

結果如下 :




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

沒有留言 :