顯示具有 量化投資 標籤的文章。 顯示所有文章
顯示具有 量化投資 標籤的文章。 顯示所有文章

2026年10月21日 星期三

購買 MaterTalk 線上課程 : 用 AI 打造美股投資工具 (補記)

我在 10/13 購買了 MasterTalk 的美股投資課程卻忘了記下來, 今天整理信箱發現平台確認信才補記, 此課程預計 11/24 (二) 上線, 已設定鬧鐘提醒. 





去年這個時候明賢表弟線上 Meeting 向我說明如何使用 Claude Code 時, 便以他用 AI 開發美股 + 選擇權投資輔助軟體為例, 那時就有想要在 First Trade 開戶, 但要做的事情實在太多太雜了沒時間. 我明年應該會開始投入美股量化交易, 所以先上個課暖身. 

2026年9月24日 星期四

三民買簡體書 1 本 : 打開量化投資的黑箱(原書第3版)

今天找到一本很棒的量化投資書籍 :


這是華爾街資深量化對沖基金經理人 Rishi K. Narang 所寫的經典書籍 "Inside the Black Box: A Simple Guide to Quantitative and High-Frequency Trading" 的簡體中文版, 原著第一版於 2009 年由 Willy 出版, 後來因應 2010 年美股閃電崩盤以及演算法技術升級, 作者又進行了大量重寫於 2013 年出版第二版, 大幅擴充高頻交易與反思系統性風險等內容. 

作者曾創辦並主持量化投資顧問公司 Telesis Capital LLC, 專注於量化交易系統評估與量化基金投資組合配置. 在 Narang 的這本經典之作中並無任何數學公式或程式碼, 他是從頂層哲學視角將量化系統拆為 Alpha 模型, 風險模型, 交易成本模型與部位構建. 讀懂這本書才會明白每一行量化程式碼背後是在解決哪個環節的問題.

此書中文版僅有中國機械工業出版社的簡體版而已, 博客來與天瓏都只寫第二版, 三民的第三版可能是出版社重新引進, 換封面或重新排版再刷的中文譯本版本號, 因為原版最新也只有第二版. 由於三民打 79 折, 所以就在三民買 :




7-11 至興店取貨 469 元. 

2026年9月23日 星期三

好書 : 活用數學交易選擇權

昨天整理臥房書櫃, 找到這本 2009 年買的書 (天啊! 17 年前), 拍拍灰塵後翻閱, 發現當時只看了前兩章就擱下了 (滿滿的數學呵呵). 此書為寰宇出版, 藍子軒翻譯自 C. B. Reel 於 2005 年出版的 "The Mathematics od Options Trading", 書附光碟一片, 內有兩個 Windows 程式 Expectation.exe 與 DailyCheck.exe, 這種 32 位元的舊軟體在目前 PC 上可能已無法執行. 

雖然是年代久遠的老書了, 但其中第 2~5 章介紹的是不隨時間改變的選擇權底層數學原理部分 (機率論, 部位管理與敏感度模型等), 在過去的幾十年並未變動值得一讀 :
  • 第二章 : Statistics, Probability, and Other Wonders (統計, 機率與其他好用的數學工具)
    建立選擇權交易必備的統計學基礎, 包含常態分佈, 標準差與報酬機率分配, 偏態與峰態 (解釋為何市場實際波動往往有肥尾 (Fat tails) 而非完美的常態分佈. 學習目標是在撰寫 Python 模型或分析回測時能深刻理解底層分佈假設與真實市場的落差. 
  • 第三章 : Visualizing Option Structures and Strategies (將選擇權的架構與策略圖形化)
    用圖形化與代數化方式來解析選擇權基本組合. 此章重點內容為探討買賣雙方在不同履約價下的損益平衡點 (Breakeven) 與風險邊界, 並透過代數公式表示各類價差單 (Vertical Spreads 等) 的線性/非線性回報輪廓.
  • 第四章 : Mathematical Expectation and Optimal Position Size (數學期望值與最佳部位規模)
    這是本書含金量最高的一章, 聚焦於解決 "該不該進場" 與 "該壓多少資金" 的問題. 學習重點有二 : 
    • 數學期望值 (Mathematical Expectation / EV) :
      如何將各價格區間的發生機率與損益加權, 計算一筆策略的長期期望回報.
    • 部位規模控制 (Position Sizing) :
      借鑑凱利準則 (Kelly Criterion) 與資金管理邏輯, 說明即使期望值為正, 重倉或滿倉依然可能因為連續回撤而爆倉 (破產風險 Risk of Ruin). 
  • 第五章 : Volatility and Sensitivity to Change (波動率與敏感度分析)
    拆解選擇權定價靈魂-波動率與希臘字母, 這是所有衍生品定價模型的基石, 不論是用 Black-Scholes 還是進階的二項樹模型, 其原理都一樣, 主要介紹下列內容 :
    • 歷史波動率 (HV) 與隱含波動率 (IV) 的計算差異.
    • Delta, Gamma, Theta, Vega 的數學定義與動態連動.
    • 波動率隨時間流逝對價內/價外選擇權的時間價值衰減影響.
其餘探討市場結構與實務工具的章節則已過時. 我叫 Gemini 列舉此書過時與未討論的課題摘要如下 : 
  • 目前市場交易量極大比例集中在 0DTE (零日到期期權) 與 Weekly Options (每週選擇權), 它們的 Gamma 暴衝與日內波動特性, 在傳統按月結算的架構下並未涉及. 
  • 現代造市商與交易員對厚尾風險 (Tail Risk), 跳躍擴散模型 (Jump-Diffusion), 局部/隨機波動率模型 (如 Heston Model) 的依賴度極高, 而這本書則假設波動率對稱或簡化處理. 
  • 2005 年的書通常使用靜態試算表 Excel/VBA 或手動計算, 現代量化交易與分析幾乎全面採用 Python 的 scipy, numpy 或 pyql / QuantLib 等函式庫. 
實作方面, 歐萊禮 Yves Hilpisch 寫的 Python for Finance 會更實用. 至於這本老書, 我希望能在明年春節前看完 2~5 章. 

2026年7月17日 星期五

購買 MasterTalk 課程 (補記)

今天查看 LINE 紀錄才想起上周三 (7/8) 請假陪水某與菁菁去大甲鎮瀾宮拜拜, 在自強號上滑手機時看到 MasterTalk 平台的新課程通知 :





有點數折換打 9 折, 實付 3492 元. 

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 軸) 排列的, 每一格時間通常都有資料, 所以滑鼠左右移動時, 輔助線會非常流暢地跟隨. 

2026年4月29日 星期三

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

最近重讀旗標出版的 "最強 AI 投資分析" 這本書, 此書於 2023 年底買來看了前幾章便擱下, 也沒時間做測試, 今天重讀第四章後, 決定動手來測試看看, 因為去年 10/7 儲值 5 美元的 OpenAI API Key 目前只用了 0.01 美元, 只剩半年就要被清零了, 得在這之前趕快用掉 (在 Vibe coding 時代親自寫程式已淪落為純興趣了). 





書中範例程式碼下載網址 :



1. 利用 pandas_ta 計算 SMA 指標 : 

首先用 pandas_ta 來計算移動平均指標 SMA8 與 SMA13 暖暖身, 畢竟已有近半年沒接觸了, 關於  pandas_ta 套件用法參考 :


下列程式使用 yfinance 取得收盤資料, 然後用 pandas_ta 套件的擴展屬性用法呼叫 df.ta.ma() 計算 SMA 指標, 結果會自動放入 df 的指定欄位, 最後用 kbar 套件繪製 K 線圖, 關於 kbar 套件用法參考 :


# ai_stock_test_1.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-07-01', end='2024-08-21', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    df['SMA_8']=df.ta.sma(length=8)
    df['SMA_13']=df.ta.sma(length=13)
    print(df.tail())
    kb=KBar(df)
    kb.addplot(df['SMA_8'], panel=2, ylabel='SMA_8')
    kb.addplot(df['SMA_13'], panel=2, ylabel='SMA_13')
    kb.plot(volume=True, mav=[8, 13])
    
此處除了在 panel 2 上繪製 SMA8 與 SMA13 指標外, 同時也在 plot() 方法中指定 mav=[8, 13] 繪製 K 線圖之疊圖 (預設 panel=0), 結果如下 : 

>>> %Run ai_stock_test_1.py   
[*********************100%***********************]  1 of 1 completed
                Close       High        Low  ...    Volume      SMA_8     SMA_13
Date                                         ...                                
2024-08-14  43.643597  43.909202  43.450429  ...  74857276  41.775311  42.438161
2024-08-15  43.305553  43.703958  43.233115  ...  45926588  42.397066  42.414943
2024-08-16  44.283455  44.343819  44.029927  ...  52823660  42.876964  42.466949
2024-08-19  44.343822  44.597354  44.223093  ...  37122372  43.163695  42.518955
2024-08-20  44.367966  44.718080  44.355892  ...  43139504  43.562101  42.514312

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




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

接下來要串接 OpenAI API, 讓 LLM 模型來生成計算技術指標的程式碼後, 用 exec() 執行該程式碼計算技術指標, 好處是毋須去熟悉例如 pandas_ta, ta, 或 Ta-Lib 套件之函式呼叫介面, 直接用自然語言來指揮 LLM 傳回技術指標計算式, 做法參考書中 ˋ4-1 的範例 : 


原程式碼的提示詞使用英文, 作者說經測試使用英文較能得到穩定之回應, 但現在 LLM 日新月異, 對中文的理解能力已非常精準, 因此我將其改寫為中文提示詞, 程式碼如下 :

# ai_stock_test_2.py
from  openai import OpenAI, APIError 
import yfinance as yf
import pandas as pd 
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-07-01', end='2024-08-21', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    code_str=ai_helper(df, "計算 8 日 MA (欄名 SMA_8) 與 13 日 MA (欄名 SMA_13)")
    print(code_str)
    exec(code_str)
    new_df=calculate(df)
    print(new_df.tail())
    kb=KBar(new_df)
    kb.addplot(new_df['SMA_8'], panel=2, ylabel='SMA_8')
    kb.addplot(new_df['SMA_13'], panel=2, ylabel='SMA_13')
    kb.plot(volume=True, mav=[8, 13]) 

此程式的 ask_gpt() 函式負責向 GPT 提問並取得回應, 注意, ask_gpt() 的傳入參數都使用了類型提示語法以增加程式碼可讀性. 例如 ask_gpt() 中的 messages: list[dict[str, str]] 意思是 :
  • messages 是一個串列, 裡面的每個元素都是字典.
  • 字典的鍵與值都是字串, 例如 {"role": "user", "content": "hello"}
參考 :


而 ai_helper() 函式則負責組裝提示詞 (字典串列) 並呼叫 ask_gpt(), 取得回應的指標計算程式碼後進行清理, 傳回純淨之 Python 程式碼給主函式以 exec() 執行, 結果如下 : 

>>> %Run ai_stock_test_2.py  
[*********************100%***********************]  1 of 1 completed
def calculate(df):
    df['SMA_8'] = df['Close'].rolling(window=8).mean()
    df['SMA_13'] = df['Close'].rolling(window=13).mean()
    return df
                Close       High        Low  ...    Volume      SMA_8     SMA_13
Date                                         ...                                
2024-08-14  43.643597  43.909202  43.450429  ...  74857276  41.775311  42.438161
2024-08-15  43.305553  43.703958  43.233115  ...  45926588  42.397066  42.414943
2024-08-16  44.283455  44.343819  44.029927  ...  52823660  42.876964  42.466949
2024-08-19  44.343822  44.597354  44.223093  ...  37122372  43.163695  42.518955
2024-08-20  44.367966  44.718080  44.355892  ...  43139504  43.562101  42.514312

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




計算出來的 SMA 數值與用 pandas_ta 計算的結果相同, 可見即使沒學過技術指標套件, 也可以利用 LLM 來進行技術指標的量化分析. 


3. 串接 Gemini API 計算 SMA 指標 : 

Gemini 版本的函式要改成 ask_gemini(), 而 ai_helper() 函式基本不變, 只有提示詞類型不同, OpenAI 的提示詞為字典字串, 而 Gemini 則是純字串. 程式碼如下 :

# ai_stock_test_3.py
from google import genai
from google.genai.errors import APIError
import yfinance as yf
import pandas as pd 
from dotenv import dotenv_values
from kbar import KBar

def ask_gemini(messages: str, model: str='gemini-2.5-flash') -> str:
    try:
        reply=client.models.generate_content(
            model=model, 
            contents=messages
            )
        return reply.text or ''
    except APIError as e:
        return e.message

def ai_helper(df, user_msg):
    role=f'''
        作為一個專業的程式碼生成機器人,
        我需要您的協助來根據特定的用戶需求生成 Python 程式碼。
        為了進行下去,我將提供給您一個遵循格式 {list(df.columns)} 的 DataFrame(df)。
        您的任務是仔細分析用戶的需求並相應地生成 Python 程式碼。
        請注意,您的回應須僅包含代碼本身,並且不應包含任何額外的資訊。
        '''
    task=f'''
        您的任務是開發一個名為 'calculate(df)' 的 Python 函式。
        這個函式應接受一個 DataFrame 作為其參數。確保您僅使用資料集中存在的欄,
        特別是 {list(df.columns)}。        
        用戶的具體運算需求為:【 {user_msg} 】        
        處理後,該函式應返回處理過的 DataFrame。
        您的回應應嚴格包含 'calculate(df)' 函式的 Python 程式碼,
        並排除任何無關的內容。
        '''
    # Gemini 的提示詞為字串型態 : 將系統設定與任務直接合併成一段完整的字串
    msg=f"{role}\n\n{task}"    
    # 呼叫 ask_gemini
    reply_data=ask_gemini(msg)
    # 清理傳回 markdown 語法
    cleaned_code=reply_data.replace("```", "")
    cleaned_code=cleaned_code.replace("python", "")      
    cleaned_code=cleaned_code.strip() # 去除頭尾多餘的空白或換行
    # 傳回程式碼
    return cleaned_code
          
if __name__ == "__main__":
    config=dotenv_values('.env') 
    gemini_api_key=config.get('GEMINI_API_KEY')
    client=genai.Client(api_key=gemini_api_key)
    df=yf.download('0050.tw', start='2024-07-01', end='2024-08-21', auto_adjust=True)
    df.columns=df.columns.map(lambda x: x[0])
    code_str=ai_helper(df, "計算 8 日 MA (欄名 SMA_8) 與 13 日 MA (欄名 SMA_13)")
    print(code_str)
    exec(code_str)
    new_df=calculate(df)
    print(new_df.tail())
    kb=KBar(new_df)
    kb.addplot(new_df['SMA_8'], panel=2, ylabel='SMA_8')
    kb.addplot(new_df['SMA_13'], panel=2, ylabel='SMA_13')
    kb.plot(volume=True, mav=[8, 13]) 

結果與上面是一樣的 :

>>> %Run ai_stock_test_3.py
[*********************100%***********************]  1 of 1 completed
import pandas as pd

def calculate(df):
    """
    計算 8 日 MA (欄名 SMA_8) 與 13 日 MA (欄名 SMA_13)。

    Args:
        df (pd.DataFrame): 包含 'Close', 'High', 'Low', 'Open', 'Volume' 欄位的 DataFrame。

    Returns:
        pd.DataFrame: 處理後包含 'SMA_8' 和 'SMA_13' 欄位的 DataFrame。
    """
    df['SMA_8'] = df['Close'].rolling(window=8).mean()
    df['SMA_13'] = df['Close'].rolling(window=13).mean()
    return df
                Close       High        Low  ...    Volume      SMA_8     SMA_13
Date                                         ...                                
2024-08-14  43.643597  43.909202  43.450429  ...  74857276  41.775310  42.438160
2024-08-15  43.305553  43.703958  43.233115  ...  45926588  42.397066  42.414943
2024-08-16  44.283459  44.343823  44.029930  ...  52823660  42.876964  42.466949
2024-08-19  44.343822  44.597354  44.223093  ...  37122372  43.163696  42.518955
2024-08-20  44.367966  44.718080  44.355892  ...  43139504  43.562102  42.514312

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



2025年12月24日 星期三

K 線圖套件 kbar 升版 v0.1.5

這兩天在樹莓派 Pi 3B 與 3A+ 這兩塊主機的 Bulleye OS 上安裝套件時都在安裝 kbar 的依賴套件 pyarrow 時失敗, 再次詢問 ChatGPT 才知道根本不需要將 pyarrow 列入依賴套件, 因為 kbar 原始碼既沒有 import pyarrow, 用來處理資料的 pandas 與畫 K 線的 mplfinance 也沒用到, 為何當時 ChatGPT 幫我寫  project.toml 時會列入 pyarrow? 為了拿掉 pyarrow 依賴只好升版為 v0.1.5 (同時也把版本上限加回去避免依賴套件大升版時出問題). 本系列之前文張參考 :



1. 修改 project.toml :  

黃底色部分為修改之處 : 

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "kbar"
version = "0.1.5"
description = "A lightweight K-line (candlestick) plotting tool with matplotlib and mplfinance."
readme = "README.md"
requires-python = ">=3.9"
license = { file = "LICENSE" }
authors = [
  { name = "Tony Y.H. Huang" }
]
keywords = ["finance", "candlestick", "mplfinance", "k-line", "stock"]
classifiers = [
  "Development Status :: 3 - Alpha",
  "Intended Audience :: Developers",
  "Intended Audience :: Financial and Insurance Industry",
  "Programming Language :: Python :: 3",
  "License :: OSI Approved :: MIT License",
  "Operating System :: OS Independent"
]


dependencies = [
  "numpy>=1.23,<3.0",
  "pandas>=2.0,<3.0",
  "matplotlib>=3.7,<4.0",
  "mplfinance>=0.12.10b0,<1.0",
]

[project.optional-dependencies]
dev = [
  "pytest>=8.0",
  "coverage>=7.0"
]

[tool.pytest.ini_options]
addopts = "-v --maxfail=3 --disable-warnings"
testpaths = ["tests"]

[tool.coverage.run]
branch = true
source = ["kbar"]

[tool.coverage.report]
show_missing = true
skip_covered = true

[tool.setuptools.packages.find]
where = ["src"]  

[project.urls]
"Homepage" = "https://github.com/tony1966/kbar/"
"Bug Tracker" = "https://github.com/tony1966/kbar/issues"
"Documentation" = "https://github.com/tony1966/kbar/#readme"


2. 修改 src/kbar/__init__.py :

修改版本號碼 :

"""
KBar: A lightweight stock candlestick plotting utility with Chinese font auto-detection.
"""

from .kbar import KBar, detect_font

__all__ = ["KBar", "detect_font"]
__version__ = "0.1.5"


3. 修改版本異動記錄檔 CHANGELOG.md : 

修改為 :

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),  
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [0.1.5] - 2025-12-24

### Changed
- 更新依賴,增加上限限制:
  - `numpy>=1.23,<3.0`
  - `pandas>=2.0,<3.0`
  - `matplotlib>=3.7,<4.0`
  - `mplfinance>=0.12.10b0,<1.0`
- 移除對 `pyarrow` 的依賴。
- 修正 `addplot()` 測試錯誤。
- 更新 `README.md` 說明與範例。

---

## [0.1.4] - 2025-11-22

### Changed
- 初版限制:
  - `numpy<2`
  - `pandas<3.0`
  - `matplotlib<4.0`
  - `pyarrow>=14.0.2`

---


4. 清理專案資料夾 & Build 專案 : 

刪除專案資料夾下前次 Build 子目錄 :
  • __pycache__ : 存放 Python 產生的 bytecode 暫存檔的目錄
  • build : 執行 python -m build 生成的中間建構資料夾
  • dist : 上次打包生成的 wheel 與 sdist
  • *.egg-info : setuptools 生成的套件資訊資料夾 (在 src 資料夾下)
用 tree 指令查看是否乾淨 :

D:\PyPi>tree kbar /f  
列出磁碟區 新增磁碟區 的資料夾 PATH
磁碟區序號為 1258-16B8
D:\PYPI\KBAR
│  .gitignore
│  CHANGELOG.md
│  LICENSE
│  pyproject.toml
│  README.md
│
├─src
│  └─kbar
│          kbar.py
│          __init__.py
│
└─tests
        test_kbar.py
        __init__.py


5. 打包專案 : 

用 python -m build 指令打包專案 : 

D:\PyPi>cd kbar

D:\PyPi\kbar>python -m build
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - setuptools>=61.0
  - wheel
* Getting build dependencies for sdist...
C:\Users\tony1\AppData\Local\Temp\build-env-zqe2az72\lib\site-packages\setuptools\config\_apply_pyprojecttoml.py:82: SetuptoolsDeprecationWarning: `project.license` as a TOML table is deprecated
!!
... (略) ...

running install
running install_lib
creating build\bdist.win-amd64\wheel
creating build\bdist.win-amd64\wheel\kbar
copying build\lib\kbar\kbar.py -> build\bdist.win-amd64\wheel\.\kbar
copying build\lib\kbar\__init__.py -> build\bdist.win-amd64\wheel\.\kbar
running install_egg_info
Copying src\kbar.egg-info to build\bdist.win-amd64\wheel\.\kbar-0.1.5-py3.10.egg-info
running install_scripts
creating build\bdist.win-amd64\wheel\kbar-0.1.5.dist-info\WHEEL
creating 'D:\PyPi\kbar\dist\.tmp-j3a6fidi\kbar-0.1.5-py3-none-any.whl' and adding 'build\bdist.win-amd64\wheel' to it
adding 'kbar/__init__.py'
adding 'kbar/kbar.py'
adding 'kbar-0.1.5.dist-info/licenses/LICENSE'
adding 'kbar-0.1.5.dist-info/METADATA'
adding 'kbar-0.1.5.dist-info/WHEEL'
adding 'kbar-0.1.5.dist-info/top_level.txt'
adding 'kbar-0.1.5.dist-info/RECORD'
removing build\bdist.win-amd64\wheel
Successfully built kbar-0.1.5.tar.gz and kbar-0.1.5-py3-none-any.whl

用 tree 指令檢視專案目錄 : 

D:\PyPi\kbar>cd ..  
D:\PyPi>tree kbar /f     
列出磁碟區 新增磁碟區 的資料夾 PATH
磁碟區序號為 1258-16B8
D:\PYPI\KBAR
│  .gitignore
│  CHANGELOG.md
│  LICENSE
│  pyproject.toml
│  README.md
│
├─dist
│      kbar-0.1.5-py3-none-any.whl
│      kbar-0.1.5.tar.gz
│
├─src
│  ├─kbar
│  │      kbar.py
│  │      __init__.py
│  │
│  └─kbar.egg-info
│          dependency_links.txt
│          PKG-INFO
│          requires.txt
│          SOURCES.txt
│          top_level.txt
│
└─tests
        test_kbar.py
        __init__.py

可見已產生打包好的 whl 檔與 gz 原始碼壓縮檔. 


6. 安裝 whl 檔進行發佈前測試 :  

先解除安裝舊版 kbar :

D:\PyPi>pip uninstall kbar   
Found existing installation: kbar 0.1.4
Uninstalling kbar-0.1.4:
  Would remove:
    c:\users\tony1\appdata\local\programs\thonny\lib\site-packages\kbar-0.1.4.dist-info\*
    c:\users\tony1\appdata\local\programs\thonny\lib\site-packages\kbar\*
Proceed (Y/n)? y
  Successfully uninstalled kbar-0.1.4

從 whl 檔安裝新版 v0.1.5 版 kbar : 

D:\PyPi>cd kbar  

D:\PyPi\kbar>pip install dist\kbar-0.1.5-py3-none-any.whl  
Processing d:\pypi\kbar\dist\kbar-0.1.5-py3-none-any.whl
Requirement already satisfied: numpy<3.0,>=1.23 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from kbar==0.1.5) (1.26.4)
Requirement already satisfied: pandas<3.0,>=2.0 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from kbar==0.1.5) (2.3.1)
Requirement already satisfied: matplotlib<4.0,>=3.7 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from kbar==0.1.5) (3.10.5)
Requirement already satisfied: mplfinance<1.0,>=0.12.10b0 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from kbar==0.1.5) (0.12.10b0)
Requirement already satisfied: contourpy>=1.0.1 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from matplotlib<4.0,>=3.7->kbar==0.1.5) (1.3.2)
Requirement already satisfied: cycler>=0.10 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from matplotlib<4.0,>=3.7->kbar==0.1.5) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from matplotlib<4.0,>=3.7->kbar==0.1.5) (4.59.1)
Requirement already satisfied: kiwisolver>=1.3.1 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from matplotlib<4.0,>=3.7->kbar==0.1.5) (1.4.9)
Requirement already satisfied: packaging>=20.0 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from matplotlib<4.0,>=3.7->kbar==0.1.5) (25.0)
Requirement already satisfied: pillow>=8 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from matplotlib<4.0,>=3.7->kbar==0.1.5) (11.3.0)
Requirement already satisfied: pyparsing>=2.3.1 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from matplotlib<4.0,>=3.7->kbar==0.1.5) (3.2.3)
Requirement already satisfied: python-dateutil>=2.7 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from matplotlib<4.0,>=3.7->kbar==0.1.5) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from pandas<3.0,>=2.0->kbar==0.1.5) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from pandas<3.0,>=2.0->kbar==0.1.5) (2025.2)
Requirement already satisfied: six>=1.5 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from python-dateutil>=2.7->matplotlib<4.0,>=3.7->kbar==0.1.5) (1.17.0)
Installing collected packages: kbar
Successfully installed kbar-0.1.5

進入 REPL 檢視 kbar 的 __version__ 屬性 : 

d:\PyPi\kbar>python     
Python 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import kbar     
>>> kbar.__version__    
'0.1.5'

用 kbar 繪製 K 線圖 : 

# kbar_test.py
from kbar import KBar   
import yfinance as yf   
df=yf.download('0050.TW', start='2024-07-01', end='2024-08-21', auto_adjust=False)   
df.columns=df.columns.map(lambda x: x[0])    # 改成舊版單層索引
kb=KBar(df)    # 未傳 font 參數預設使用正黑體
kb.plot(title='台灣五十(0050.TW)', volume=True)




7. 發佈到 PyPI 進行升版 :  

利用 twine 套件將專案上傳到 PyPI : 

D:\PyPi\kbar>python -m twine upload dist/*    
Uploading distributions to https://upload.pypi.org/legacy/
Enter your API token:   (按 Ctrl+V 貼上 PyPI API Token 按 Enter )
Uploading kbar-0.1.5-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 17.2/17.2 kB • 00:00 • ?
Uploading kbar-0.1.5.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 19.2/19.2 kB • 00:00 • ?

View at:




然後到 Pi 3 主機安裝 kbar : 

pi@kaopi3:~ $ pip install kbar
Defaulting to user installation because normal site-packages is not writeable
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting kbar
  Using cached kbar-0.1.5-py3-none-any.whl.metadata (6.1 kB)
Requirement already satisfied: numpy<3.0,>=1.23 in ./.local/lib/python3.9/site-packages (from kbar) (1.26.4)
Requirement already satisfied: pandas<3.0,>=2.0 in ./.local/lib/python3.9/site-packages (from kbar) (2.0.3)
Requirement already satisfied: matplotlib<4.0,>=3.7 in ./.local/lib/python3.9/site-packages (from kbar) (3.9.4)
Requirement already satisfied: mplfinance<1.0,>=0.12.10b0 in ./.local/lib/python3.9/site-packages (from kbar) (0.12.10b0)
Requirement already satisfied: contourpy>=1.0.1 in ./.local/lib/python3.9/site-packages (from matplotlib<4.0,>=3.7->kbar) (1.3.0)
Requirement already satisfied: cycler>=0.10 in ./.local/lib/python3.9/site-packages (from matplotlib<4.0,>=3.7->kbar) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in ./.local/lib/python3.9/site-packages (from matplotlib<4.0,>=3.7->kbar) (4.60.2)
Requirement already satisfied: kiwisolver>=1.3.1 in ./.local/lib/python3.9/site-packages (from matplotlib<4.0,>=3.7->kbar) (1.4.7)
Requirement already satisfied: packaging>=20.0 in ./.local/lib/python3.9/site-packages (from matplotlib<4.0,>=3.7->kbar) (25.0)
Requirement already satisfied: pillow>=8 in /usr/lib/python3/dist-packages (from matplotlib<4.0,>=3.7->kbar) (8.1.2)
Requirement already satisfied: pyparsing>=2.3.1 in ./.local/lib/python3.9/site-packages (from matplotlib<4.0,>=3.7->kbar) (3.3.1)
Requirement already satisfied: python-dateutil>=2.7 in ./.local/lib/python3.9/site-packages (from matplotlib<4.0,>=3.7->kbar) (2.9.0.post0)
Requirement already satisfied: importlib-resources>=3.2.0 in ./.local/lib/python3.9/site-packages (from matplotlib<4.0,>=3.7->kbar) (6.5.2)
Requirement already satisfied: pytz>=2020.1 in ./.local/lib/python3.9/site-packages (from pandas<3.0,>=2.0->kbar) (2025.2)
Requirement already satisfied: tzdata>=2022.1 in ./.local/lib/python3.9/site-packages (from pandas<3.0,>=2.0->kbar) (2025.3)
Requirement already satisfied: zipp>=3.1.0 in ./.local/lib/python3.9/site-packages (from importlib-resources>=3.2.0->matplotlib<4.0,>=3.7->kbar) (3.23.0)
Requirement already satisfied: six>=1.5 in /usr/lib/python3/dist-packages (from python-dateutil>=2.7->matplotlib<4.0,>=3.7->kbar) (1.16.0)
Using cached kbar-0.1.5-py3-none-any.whl (7.9 kB)
Installing collected packages: kbar
Successfully installed kbar-0.1.5

少了不需要的 pyarrow 很快就把 kbar 裝好了. 

升版完成收工.