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

momo 購買泰達椅 Pro

因下周要帶爸與岳父母去沖繩旅遊, 為了在排隊時沒椅子坐, 上 momo 買了僅 1 kg 的泰達椅 Pro : 





滿一件折 350, 還另送 350 元 momo 幣 : 




實付 2630 元, 送 momo 幣 350 元, 實際上只花 2280 元. 

2026年4月30日 星期四

momo 購買小米 20000 mAh 行動電源

因下周要帶爸與岳父母去沖繩玩, 今天上網買了一顆小米行動電源 :


100Wh 以下可帶上飛機




用掉 74 元 momo 幣實付 525 元. 

(補記) 購買 HiSKIO 課程 : Claude Code 深度應用

我可能線上課程買太多買到頭昏了, 最近在臉書看到一門 Claude Code 課程 56 折即將結束, 趁中午截止前上網想先買下 (雖然可能要半年後才會開啟 Claude Code 付費) : 


沒想到登入 HiSKIO 赫然看到網站提醒 : 你已購買本課程! 蝦米? 查了訂購記錄, 這門課我今年 2/6 就買了, 當時可能一忙忘了記下來. 這就像我存摺有很多本 (但也不至於到 100 本啦) 藏錢都藏到忘記自己很有錢了. 補記一下免得又忘記了 :




我在 HiSKIO 目前就只有林彥文老師的 "Vibe Coding 全能實戰課" (已上完, 要複習) 與這堂 Claude Code 課, 等沖繩回來要開始上課了 (還有 TibaMe 與 Hahow 的呵呵). 

Python 學習筆記 : 用 plotly 繪製互動式圖表 (三)

本篇旨在測試 Plotly 的圖表匯出功能. 


Plotly 支援多種檔案類型匯出, 可呼叫下表中 Figure 物件的方法匯出 :


Figure 物件的檔案匯出方法 說明
fig.write_image(file, **kwargs) 匯出為靜態圖片(PNG、JPEG、SVG、PDF 等),需安裝 kaleido
fig.write_html(file, **kwargs) 匯出為互動式 HTML 檔,可直接用瀏覽器開啟
fig.to_html(full_html=True, include_plotlyjs='cdn') 將圖表轉為 HTML 字串(用於網頁內嵌或 API 回傳)
fig.to_json() 將圖表轉為 JSON 格式(適合儲存、API 傳遞、版本控管)
fig.write_json(file) 將圖表 JSON 結構直接寫入檔案
fig.to_dict() 將圖表轉為 Python 字典格式,可進一步程式操作


注意, fig.write_image() 方法須依賴 kaleido 模組, 這是 Plotly 官方推出的匯圖引擎模組, 用來將 Plotly 圖表儲存為靜態圖片.


1. 匯出圖檔 : 

呼叫 Figure 物件的 write_image() 方法可將繪製的圖表匯出成圖片檔 (支援 PNG, JPG, SVG, PDF 等檔案類型), 其參數結構如下 :

fig.write_image(
    file,             # 必填,檔案路徑字串或類似檔案的物件
    format=None,      # 圖片格式,如 'png'、'jpeg'、'svg'、'pdf',若省略會自動從副檔名判斷
    width=None,       # 圖片寬度(像素),預設為圖表原始寬度
    height=None,      # 圖片高度(像素),預設為圖表原始高度
    scale=1,          # 圖片縮放倍數(例如 2 表示解析度加倍)
    validate=True,    # 是否檢查圖表是否有效(預設 True)
    engine='kaleido'  # 使用的圖像引擎,預設為 'kaleido'
    )

不過使用此方法之前須先安裝 Plotly 的 kaleido 模組, 而且 plotly 也要提升至最新版 :

pip install kaleido   
pip install plotly -U

在前一篇測試中使用了 plotly.express 來繪製長條圖, 下列沿用此範例來匯出所繪製的圖檔 : 

# plotly_chart_export_1.py
import plotly.express as px
import pandas as pd
import os

# 1. 資料來源
data={
    '月份': ['一月', '二月', '三月', '四月', '五月'],
    '營收': [120000, 135000, 99000, 150000, 170000]    
    }

# 2. 建立 Figure 圖表物件
# 注意:這裡設定了 width 和 height,匯出圖片時會以此為基準
fig=px.bar(data, x='月份', y='營收', width=800, height=600, title="月營收統計圖")

# 3. 顯示圖表 (選用)
fig.show()

# 4. 匯出圖檔 
# 建立儲存目錄(選用,避免檔案雜亂)
if not os.path.exists("output"):
    os.mkdir("output")
# 匯出為 PNG
fig.write_image("output/revenue_report.png", scale=2)
# 匯出為 JPG
fig.write_image("output/revenue_report.jpg", scale=2)
print("圖檔已匯出至 output 資料夾中。")

執行結果除了 fig.show() 會開啟瀏覽器顯示長條圖外, 也會在目前工作目錄下建立 output 子目錄存放匯出的兩個圖檔 :

>>> %Run plotly_chart_export_1.py   
圖檔已匯出至 output 資料夾中。




2. 匯出網頁 : 

呼叫 fig.write_html() 可將繪製之圖表匯出為 HTML 檔, 若傳入 include_plotlyjs='cdn' 參數會使用 CDN 的 plotly 函式庫, 這樣匯出的 HTML 檔較小但須連網才能看到互動圖表; 否則會將 plotly 函式庫一同匯出, 檔案較大些 (約 4MB) 但不須連網, 離線開啟網頁即可看到互動圖表.

程式碼如下 : 

# plotly_chart_export_2.py
import plotly.express as px
import pandas as pd
import os

# 1. 資料來源
data={
    '月份': ['一月', '二月', '三月', '四月', '五月'],
    '營收': [120000, 135000, 99000, 150000, 170000]    
    }

# 2. 建立 Figure 圖表物件
fig=px.bar(data, x='月份', y='營收', width=800, height=600, title="月營收統計圖 (互動式 HTML)")

# 3. 建立儲存目錄
output_dir="output"
if not os.path.exists(output_dir):
    os.makedirs(output_dir)

# --- 匯出 HTML  ---
# 方式 A:標準匯出 (將 Plotly.js 核心程式碼打包進去,檔案約 4 MB,可離線開啟)
fig.write_html(os.path.join(output_dir, "report_full.html"))

# 方式 B:輕量化匯出 (使用 CDN 連結,檔案僅約 50KB,開啟時需連網載入 JS)
fig.write_html(
    os.path.join(output_dir, "report_cdn.html"), 
    include_plotlyjs='cdn'
    )

print(f"✅ HTML 檔案已匯出至 {output_dir} 資料夾。")
print("- report_full.html (可離線檢視)")
print("- report_cdn.html (體積小,需連網)")

# 顯示圖表
fig.show()

執行結果如下 :

>>> %Run plotly_chart_export_2.py
✅ HTML 檔案已匯出至 output 資料夾。
- report_full.html (可離線檢視)
- report_cdn.html (體積小,需連網)

開啟 output 資料夾下的網頁檔即可看到長條圖 :




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']



2026年4月28日 星期二

市圖還書兩本 (React)

前陣子因為在 Vibe Coding 開發中, 發現 AI 經常使用 React 作為前端框架, 於是興起一股學學看的念頭, 從市圖借來幾本 React 的書, 沒有打算深入研究, 只想對核心運作有個基本認識. 不過還沒開卷呢, 下面兩本已被預約須還 :
No.1 作者是 ReacJS 新聞站長, 此書雖較舊了, 但前半部有豐富的 ES6 語法介紹; No.2 書況極新 (2024 年出版), 內容也更豐富, 包含伺服端 React, 與 Next.js 框架等, 下次再回借. 

2026年4月26日 星期日

2026 年第 16 周記事

週五天氣轉陰, 下班時差點淋到雨, 傍晚回鄉下時開始下起小雨, 整個晚上都在下, 甚至連周六也是下整天, 雨天無訪客剛好在家趕 SDD 線上課程的作業, 花了整整一天終於在周六晚上午夜關檔前完成作業上傳, 好險! 45 個學員也只有 9 個趕上截止期限 (我是最後一個哈哈).

幸好今天出太陽, 趕緊將沙發罩洗好拿去曬, 因下周大帥與仲仔要造訪鄉下家, 得事先整理一下客廳. 下午把馬路邊的三棵芒果樹都套袋完畢, 約莫 60 顆左右, 樹梢還有很多太高無法套袋, 下周要去小漢買網子攤開綁在樹下, 這樣等自然熟掉下來時才不會摔壞. 




由於雨季即將來臨, 蔥價會攀高, 早上跑了一趟種子行買了 20 株青蔥苗+一株九層塔+六株皇宮菜, 年初種了一盆香菜長不好, 傍晚全部拔掉改種青蔥, 九層塔也是盆栽, 皇宮菜則暫時放著澆水, 小舅五月初要叫小耕耘機把菜園的土翻一遍, 說之後保留兩畦給他秋天時種小番茄, 其餘我要種菜或種果樹均好, 因為他家那邊也有一個菜園要顧 (去年他朋友借他使用). 菜園南側因為較遠, 種菜澆水較不便, 打算再種兩棵芭樂樹與木瓜樹. 

毛小妹第二胎 (也是四隻) 小貓現在都在室外了, 這梯都很怕人不親, 我一靠近就跑掉. 上一梯的四隻目前只剩小黑與吉哇哇在家, 小乖與哇哇吉都離家超過一個月不回來了. 毛小妹的妹妹捲尾阿姨我也一周未見她出現, 恐怕也不回來了. 雖說不回來, 其實我認為它們應該都是遇險 (中毒/車禍) 回不來. 




車庫雜物實在太多了, 等沖繩回來得來個斷捨離大清理了. 

2026年4月25日 星期六

Gemini CLI 學習筆記 : OpenSpec 初體驗 (三)

離作業交卷只剩 2.5 小時, 刻不容緩繼續進行第三次迭代.

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


第三次迭代要在前次基礎上添加對數與三角函數科學計算功能, 同樣使用逐步推進模式, 工作流所需指令如下 : 
  • /opsx:new <iteration_name> (建立迭代之專屬的工作區)
  • /opsx:propose <requirements> (依需求起草提案書, 規格定義書, 架構設計書, 與任務清單)
  • /opsx:apply <iteration_name> (依照 tasks.md 中的任務清單逐一實作此迭代功能)
  • /opsx:archive (迭代完成歸檔)
廢話不多說以免誤了軍期, 馬上開工. 


1. 建立迭代之專屬工作區 :   

第三次迭代工作區取名為 calc-scientific :

/opsx:new calc-scientific   



... (略) ...



2. 根據需求填寫提案書 :   

> /opsx:propose "在現有計算器專案上擴充科學計算功能, 包含三角函數 (sin, cos, tan) 與對數 (以 10 為底的 log, 以及自然對數 ln), 請務必處理以下邊界與轉換邏輯:1. 三角函數的輸入值預設為「角度 (Degree)」, 後端需自行轉換為弧度進行計算, 2. 處理 tan(90) 等無效角度的防呆機制, 3. 對數運算需阻擋小於或等於 0 的無效輸入, 並回傳明確的 HTTP 錯誤, 4. 前端介面需優雅地加入這些新按鈕. "



... (略) ...



3. 依據任務清單實作程式碼 :   

> /opsx:apply calc-scientific   



... (略) ...



完成專案實作馬上作人工測試, 開啟 127.0.0.1:5000 網頁果然多了很多科學計算按鈕 :



輸入 30 或 390 按 sin 都會得到正確結果 0.5 :




輸入 -30 按 sin 也得到正確結果 -0.5 :




接下來做 cos 的精度測試, 輸入 90 按 cos 得到一個接近 0 的極小值而非 0, 這是因為我們忘了要 AI 做微小誤差抹零處理之故, 可以在後續迭代中處理掉. 做對數測試, 輸入 1 按 log 得到正確 0, 輸入 0 按 log 則得到 Error (無限大) :




4. 歸檔結案 : 

/opsx: archive   



... (略) ...


第三次迭代歸檔完畢, 終於搞定了, 趕緊來去交作業啦! 


5. 打包專案上傳 GitHub : 

作業繳交要求將專案上傳 GitHub, 然後將 repo 網址填入 Google 試算表內. 


(1). 將所有變更加入暫存區 (打包) : 

D:\gemini\calculator-project>git add .  
warning: in the working copy of '.gemini/commands/opsx/apply.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.gemini/commands/opsx/archive.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.gemini/commands/opsx/explore.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.gemini/commands/opsx/propose.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.gemini/skills/openspec-apply-change/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.gemini/skills/openspec-archive-change/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.gemini/skills/openspec-explore/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.gemini/skills/openspec-propose/SKILL.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.gitignore', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.python-version', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'main.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'openspec/changes/archive/2026-04-25-calc-basic/.openspec.yaml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'openspec/changes/archive/2026-04-25-calc-power-root/.openspec.yaml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'openspec/changes/archive/2026-04-25-calc-scientific/.openspec.yaml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'openspec/config.yaml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'pyproject.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'uv.lock', LF will be replaced by CRLF the next time Git touches it

出現的這些 warning 是在 Windows 環境下執行 Git 時常見的警告, 它完全不影響程式碼功能或 GitHub 的上傳結果, 這只是 Git 在提醒換行符號的格式要統一, 因為 OpenSpec 工具或 AI 產生的檔案可能預設使用了 Unix 格式的換行字符 LF, 在 Windows 的命令提示字元下操作時 Git 偵測到這種不一致, 所以主動告知它會自動把這些檔案轉換成 Windows 標準的 CRLF, 因此毋須理會. 


(2). 設定使用者名稱與 Email : 
 
D:\gemini\calculator-project>git config --global user.name "Tony"
D:\gemini\calculator-project>git config --global user.email "blablabla@ms5.hinet.net"   


(3). 提交變更 (貼標籤/存檔) : 

D:\gemini\calculator-project>git commit -m "feat: 完成計算機專案 (基礎運算、次方根號、科學計算)"   
[master (root-commit) 17a5d99] feat: 完成計算機專案 (基礎運算、次方根號、科學計算)   
 44 files changed, 2523 insertions(+)
 create mode 100644 .gemini/commands/opsx/apply.toml
 create mode 100644 .gemini/commands/opsx/archive.toml
 create mode 100644 .gemini/commands/opsx/explore.toml
 create mode 100644 .gemini/commands/opsx/propose.toml
 create mode 100644 .gemini/skills/openspec-apply-change/SKILL.md
 create mode 100644 .gemini/skills/openspec-archive-change/SKILL.md
 create mode 100644 .gemini/skills/openspec-explore/SKILL.md
 create mode 100644 .gemini/skills/openspec-propose/SKILL.md
 create mode 100644 .gitignore
 create mode 100644 .python-version
 create mode 100644 GEMINI.md
 create mode 100644 README.md
 create mode 100644 calculator/__init__.py
 create mode 100644 calculator/logic.py
 create mode 100644 main.py
 create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/.openspec.yaml
 create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/design.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/proposal.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/specs/arithmetic-api/spec.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/specs/calculator-ui/spec.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/tasks.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/.openspec.yaml
 create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/design.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/proposal.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/specs/advanced-arithmetic/spec.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/specs/calculator-ui/spec.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/tasks.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/.openspec.yaml
 create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/design.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/proposal.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/specs/calculator-ui/spec.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/specs/scientific-functions/spec.md
 create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/tasks.md
 create mode 100644 openspec/config.yaml
 create mode 100644 openspec/specs/advanced-arithmetic/spec.md
 create mode 100644 openspec/specs/arithmetic-api/spec.md
 create mode 100644 openspec/specs/calculator-ui/spec.md
 create mode 100644 openspec/specs/scientific-functions/spec.md
 create mode 100644 pyproject.toml
 create mode 100644 static/index.html
 create mode 100644 static/script.js
 create mode 100644 static/style.css
 create mode 100644 test_api.py
 create mode 100644 uv.lock


(4). 在 GitHub 建立空的儲存庫 (Repository) : 

建立一個空專案 (剛好 calculator-project 可用), 注意, 因為在本機已經有 README.md 與 .gitignore 檔案了, 不要勾選 "Add a README" 或 "Add .gitignore" 這兩項, 保持預設的空專案即可, 點擊 Create repository 新增 repo. 



(5). 綁定並推上雲端 : 

告訴本機 Git 這個專案要連線到哪個 GitHub 網址 : 

D:\gemini\calculator-project>git remote add origin https://github.com/tony1966/calculator-project.git  

把專案推上雲端  : 

D:\gemini\calculator-project>git push -u origin main   
Enumerating objects: 67, done.
Counting objects: 100% (67/67), done.
Delta compression using up to 16 threads
Compressing objects: 100% (51/51), done.
Writing objects: 100% (67/67), 39.46 KiB | 3.04 MiB/s, done.
Total 67 (delta 3), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (3/3), done.
To https://github.com/tony1966/calculator-project.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

此指令會出現詢問視窗, 用預設 manager 按 Select 鈕即可, 然後登入 GitHub 帳號即可上傳. 成功後到 Google sheet 登錄專案 repo 的 GitHub 網址即完成作業繳交啦! 我原以為時間太趕只能聽完課程, 沒想到忙了一整天居然搞定作業了, 哈哈. 

心得 : 親自動手做一遍才能真正學會. 


6. 跳出 Gemini CLI : 

專案結束, 輸入 exit 離開專案 : 




used 從 2% 到 10%, 用掉了 8% 資源. 連續按兩次 Ctrl+C 即可跳出 Gemini CLI 回到 PS 視窗.