2026年5月10日 星期日

用 Power Shell 更新 (安裝) VS Code

最近因為 Vibe coding 偶而要用到 VS Code, 但我的 LG 筆電在 2023 年安裝的 VS Code 版本太舊了 (v1.83), 這期間因為 AI 飛躍式發展, VS Code 已歷經數次重大改版, 必須升版才能使用許多 AI 相關的新功能 :




本來是到官網下載最新版 .exe 檔, 執行後會蓋掉舊版, 但 150MB 居然要花 2 小時, 詢問 Gemini 才知道用 PS Shell 可快速下載且自動安裝, 先用管理員身分開啟 PS Shell, 輸入下列指令 :

winget upgrade Microsoft.VisualStudioCode  

Windows PowerShell
著作權(C) Microsoft Corporation。保留擁有權利。

安裝最新的 PowerShell 以取得新功能和改進功能!https://aka.ms/PSWindows

PS C:\Users\tony1> winget upgrade Microsoft.VisualStudioCode  
`msstore` 來源要求您必須先檢視下列合約,再使用。
Terms of Transaction: https://aka.ms/microsoft-store-terms-of-transaction
來源需要將目前電腦的 2 個字母地理區域傳輸到後端服務,才能正確(例如"US")。

是否同意所有來源合約條款?
[Y] 是  [N] 否: y
找到 Microsoft Visual Studio Code [Microsoft.VisualStudioCode] 版本 1.119.0
此應用程式已由其擁有者授權給您。
Microsoft 不負任何責任,也不會授與協力廠商封裝的任何授權。
正在下載 https://vscode.download.prss.microsoft.com/dbazure/download/stable/8b640eef5a6c6089c029249d48efa5c99adf7d51/VSCodeUserSetup-x64-1.119.0.exe
  ██████████████████████████████   149 MB /  149 MB
已成功驗證安裝程式雜湊
正在啟動套件安裝...
已成功安裝
PS C:\Users\tony1>

不到三分鐘就搞定了 :




可見版本已提升至 v1.119 :



2026年5月9日 星期六

關於明台旅平險

這次去沖繩旅行前, 小舅問我有無投保旅平險, 他說即使是與舅媽騎機車上阿里山, 他都會叫婷婷表妹幫他買旅平險, 但我上網找了一輪, 發現幾乎所有保險公司對 80 歲以上長輩都拒保旅平險, 只好放棄幫爸買旅平險, 只能幫自己與菁菁買. 雖然水某用信用卡買機票已經有旅平險保障 (持卡人本人, 配偶, 及未滿 25 歲之未婚子女都在承保範圍, 注意, 在信用卡保險定義中, 父母並不屬於家屬的承保範圍), 但想說把這流程摸一遍做成 SOP, 下次旅行要買旅平險就不用摸索. 最後找到買車險的明台操作介面最熟悉, 且可自行決定保幾天 (其他家都固定三天? 奇怪) :


保額最高 900 萬元的四天旅平險保費 693 元, 保單內容如下 :




以後出國旅行, 只要在兩三天前上明台刷卡網路投保 (自己) 或傳真投保 (家人) 即可. 雖然線上系統會擋下 80 歲以上的保單, 但部分保險公司提供高齡專屬旅平險, 不過通常需要透過業務員或電話投保 (人工審核). 

2026年5月8日 星期五

2026 沖繩之旅 Day 4 (5/8)

由於波上宮行程移到昨日, 所以今日非常 relax, 都在國際通一帶購物逛街. 因為不用趕時間, 早上 08:10 才下去四樓吃早餐, 水某則與小姨子搭計程車又跑了波上宮一趟 (只為了幫小舅子買一個御守護). 吃完早餐回房間整理行李, 11 點拉行李到大廳辦退宿與寄放, 爸與岳父母因不想逛街在大廳休息, 叫我自己去逛逛. 

我沿國際通往西走 (下坡), 經過這家 USUMASA SUNNYDAY 咖啡飲料店時看見有賣宇治抹茶冰淇淋, 一時嘴饞便買了一個坐在門口椅子上吃, 正想拿手機來拍一下, 結果身子一斜, 那才舔了三口的冰淇淋卻掉了下來, 沾到左邊褲管與球鞋, 女店員見狀趕忙過來處理, 轉身又製做了一筒給我, 我說這是我的不小心造成, 我要付錢再買一個, 但她一直拒收我也只好接受她的好意. 




吃完冰淇淋繼續往下走, 看到右手邊有一個購物街, 走進去才發現這就是第一志牧市場, 在裡面買了兩件有沖繩風的花襯衫, 第一件 4950 日元, 第二件 2500 日元. 我出國旅行會買伴手禮給親友, 卻幾乎沒有買給自己, 結果每次他們說你買的哪個哪個歐蜜鴉給好好吃, 我心裡一陣蛤蛤蛤??? 我自己都沒吃到啊! 我哪裡知道? 所以從現在開始要對自己好一點了.  

等我逛回來, 想說午餐就去嘉新對面的一蘭拉麵吃, 但小姨子說想去市場買魚請店家代煮, 哈哈, 我才剛從市場那邊回來哩, 只好又走回了市場內的魚市場, 怎麼挑魚我不會, 我只負責吃與付帳. 






吃完午餐走回嘉新已兩點半, 這時天已轉陰雨, 領出寄放的行李後請櫃檯幫忙叫了兩台計程車前往那霸機場, 我們這車的司機是個健談的阿嬤級, 知道我們從台灣來, 一路上參雜英日語跟我聊, 結果把前面那部菁菁她們坐的那台計程車跟丟了, 她這才顯得有點慌, 可能是擔心誤了班機, 我說時間還很充裕啦不急, 也只是晚了第一台車六七分鐘而已. 

今天班機也延誤 15 分鐘, 約 18:45 才起飛, 到小港時已近 17:30, 通關出來後還在出境大廳打開行李分歐蜜鴉給, 20:30 回到高雄家, 大致整理一下餵完阿咪與萬萬後載爸回鄉下, 到家已過了十點, 今天要早點睡了, 每次出國都睡眠不足啊! 

2026年5月7日 星期四

2026 沖繩之旅 Day 3 (5/7)

昨天回程時跟司機羽賀先生聊到 Day 4 只有波上宮一個行程, 因為在市內所以會搭計程車去, 他說氣象預報明日會出太陽, 建議回旅館討論一下, 可以考慮把波上宮挪到 Day 3, 趁有包車可以載我們過去. 晚上與菁菁討論後, 決定今日行程修改為 : 波上宮 -> 永旺來客夢 -> 美國村.

波上宮建在一個小坡上, 需要小爬一下, 徵得門口交管人員同意, 讓羽賀先生把車開上去, 免得三位長輩費力. 司機臨時幫我們腦補拜廟程序, 先到左側舀水洗手漱口, 然後到廟前將硬幣丟入賽錢箱, 拍手許願即可. 




然後拿出納經帖, 因為這是此行我唯一能蓋御朱印的地方 : 




離開波上宮前往沖繩中部的永旺永旺來客夢購物中心逛街購物及吃午餐. 




這裡有一家鰻魚飯, 我點了最小份量的居然還快吃不完, 因為他們的碗很深 : 





2026年5月6日 星期三

2026 沖繩之旅 Day 2 (5/6)

今天 07:40 帶爸與岳父母到嘉新 3 樓吃早餐, 感覺挺不錯的, 因為這兩年去日本都住民宿, 早餐自理, 上一次住旅館是十年前帶爸跟團去黑部立山. 不過我繞了兩圈沒找到納豆, 有點小失望 (2026-05-07 補充 : 有的, 在清粥附近架上).




吃過早餐 09:00 在飯店口坐上行腳沖繩的 9 人座包車前往早上景點, 位於沖繩北邊的谷宇利島, 車程大約 1.5 小時. 司機宇賀先生是來自瀋陽的移民, 原姓李, 在中國讀完大學 (機械系) 後, 因為對機械實在沒興趣, 來沖繩改讀觀光, 之後留在日本就業娶妻, 規化為日本籍, 宇賀桑非常健談, 一路上話題不斷, 也沿路介紹沖繩風土.  

不過今日沖繩天氣不佳, 一早就下毛毛雨, 整個天空灰濛濛一片. 過谷宇利大橋後前往古宇利海洋塔, 每人門票 1000 日元, 然後搭無人電動車上去海洋塔, 裡面有貝殼博物館 (第一次看到很特別的水字貝), 陳列各種貝殼, 然後登上樓上觀景台, 這如果是陽光普照的天氣絕對是拍照的絕佳景點. 







離開海洋塔後便前往一家百年歷史古屋改建的民宿餐廳 "お食事処 ちゃんや (Oshokuji Dokoro Chanya)" 品嘗道地沖繩飲食 : 







吃過午餐司機驅車前往美麗海水族館, 類似屏東海生館, 它的 "黑潮之海" 大水槽裡最吸引人的除了巨大的鯨鯊外, 便是首次近距離看到河豚的長相 :








另外我注意到這裡的魟魚似乎有兩種, 差別在於頭部結構, 一個是常見的單頭魟魚, 另一個看起來像是有兩個頭, 我把照片上傳 Gemini, 原來這其實是頭鰭, 是世界最大魟魚鬼蝠魟的特徵 :





2026年5月5日 星期二

2026 沖繩之旅 Day 1 (5/5)

今天早上 7 點載爸從鄉下出發, 先到楠梓接菁菁, 回到高雄約 8:30, 開始打包行李箱, 準備了小咪與阿萬的糧食, 12:30 搭預約的大發計程車前往小港 (車資 455 元). 與岳父母及小姨子會合辦理報到. 還好我昨天有檢視登機證, 發現了爸登機證英文名字拼寫與護照有一個字母不同, 馬上採取動作更正, 否則今日登機可能會有點麻煩. 




登機閘口與三月去大阪一樣, 都是最尾巴的 Gate 27, 要走好遠. 班機 15:55 起飛, 飛行約 1 小時 10 分鐘到達那霸機場, 順利通過海關與入境檢查後, 馬上就拿到行李, 走到出境大廳大約 18:30, 這時行腳沖繩的接機小巴士已拿著我的名牌等在那裏, 從機場到國際通嘉新酒店 (Hotel Collective) 車程約 15 分鐘 (車資 6000 日元), 外面下著毛毛雨, 看來今天一整天都是陰雨天. 

順利入住酒店後, 稍事整理約 20:10 步行前往島唄與地料理 Tubaraama 餐廳吃飯 (酒店出來右轉直走約 600 公尺), 到達時餐廳已有兩桌客人, 台上的兩位歌者已在準備 21:00 開始的表演. 今天由菁菁點菜, 據說都是琉球當地料理, 其中一道海葡萄形狀像縮小版的葡萄, 口感非常特殊, 嚐起來像明太子, 有點淡淡的鹹味 : 




2026年5月4日 星期一

菁菁 MAZDA MX5 車險續約

菁菁的小跑車 MX5 車險 5/8 日到期, 那天剛好從沖繩回來, 無法處理續約, 所以提前今天上網續保, 今年是第二年, 參考去年保險內容 :


今年強制+任意險比去年稍微便宜 :




機票英文名字拼錯問題

今天做行前檢查, 發現早上水某寄的登機證中, 爸的英文名字拼音與護照有一個字母不正確, 原因是水某訂機票時我傳遞的資料打錯了, 趕緊撥打華航客服 02-4129000, 告知有此情況, 客服先叫我到華航網站先取消爸的報到單, 然後寄一封確認信給我, 要求填寫錯誤拚寫與正確拼寫, 並附上護照內頁照片佐證, 叮囑我若一小時後仍未收到已更正回函, 要再次撥打客服催促, 因客服員有限, 務必在線等候直到有人接聽. 

# 華航網路報到網頁

到中午還沒收到完成回覆, 我再次打電話去詢問處理進度, 這回真的等了十分鐘才輪到, 客服表示已要求加速進行, 終於在下午三點收到更正回函, 上官網輸入機票號碼與正確姓名拼字, 果然就能進入機票與登機資訊網頁, 但無法線上寄發新的登機證, 說 "說請洽機場櫃檯領取登機證", 所以明天要早一點到櫃檯報到, 說明有更正姓名拼寫錯誤, 請其人工列印新登機證. 所以出國旅行前務必檢查旅行文件, 最好是列印出來做為備份, 也較能檢查出錯誤, 不要小看一個字母之差, 這可能會讓旅伴到機場才發現無法同行. 即使旅伴不是親人, 最好也能互相檢查一下較妥當. 

2026年5月3日 星期日

2026 年第 17 周記事

本周五勞動節連假只上四天班, 我週三沒有回鄉下, 因為周四下班就要回去了. 以往勞動節都去加班, 今年不加了, 即使可加也不想去了, 邁入耳順之年後更想要的是放假. 周六老同學大帥與仲仔, 風大師來訪, 因為之前我都下廚在加吃便飯, 坐下來聊天時間不甚多, 這回剛好峰大師四月底退休, 預定了菸樓坊一桌請大伙去吃飯, 但到了餐廳才發現遊覽車一車車進來, 不知要等多久才有空位, 大帥建議取消, 去人少的地方, 我想起鎮上的八分飽, 到餐廳時下車問還好有空桌, 於是便在此用餐. 我看以後還是自己下廚吧, 外面餐廳假日都是人. 

連假三天我都在家測試程式, 一口氣把 Plotly 與 Bokeh 畫 K 線圖的方法搞定. 雖然 AI 時代漸漸都不自己寫程式了, 小白或非科班就算呃, 但做為工程師還是得看得懂 AI 在寫甚麼吧? 

週五舅媽叫小舅帶來涼麵與木瓜粄, 加上峰大師周六帶來一隻烤雞, 所以連假三天都在消耗這些糧食, 甚至今天周日我也沒上市場買菜, 因為下周要去沖繩, 食材放太久也不行. 

下午要去菜園移植韭菜時, 經過車庫看見毛小妹與它的一隻小貓躺在一起睡覺, 看似小貓吃完奶就地睡著了, 那畫面看來好幸福, 趕緊進屋拿手機拍了下來 : 




但晚上爸在曬穀場騎腳踏車時, 一隻小貓跑到路對面去, 在橫越馬路跑回來時被一台機車撞到, 那人也沒停下來逕自騎走了, 爸把小貓報進來, 我看口鼻皆流血已無呼吸, 因它們四兄妹毛色很像, 不確定是否為下午那隻. 我家緊鄰馬路邊, 路雖不大但為交通要道, 來往車子都開太快, 打算買一個太陽能警示燈安裝在路旁電線桿, 希望提醒減速慢行. 

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 參數, 這樣三張圖就會疊在同一張畫布上了. 

結果如下 : 




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