2025年12月2日 星期二

GitHub Copilot 初體驗

雖然現在我已使用 AI 協作方式產出程式碼與 debug, 主控權與選擇權仍然在我. 但是最近跟明賢表弟在 LINE 上交流軟體經驗後, 發現我在這部分似乎定格在石器時代了, 我原先對要花錢的 Cursor, GitHub Copilot, 或 Claude Code 等 Vibe coding 工具抱持觀望態度, 認為能透過 ChatGPT 交互問答得到答案又何必要花錢? 直到看過他展示目前的作業模式後, 我的態度與觀點就開始動搖了, 於是決定從 GitHub Copilot 著手來了解 Vibe coding. 

我上周末從母校圖書館借到下面這本書, 依照書中說明啟用 GitHub Copilot 功能 :


登入 GitHub 後, 按右上角人頭, 點選 "Copilot settings" :




在 GitHub Copilot 頁面中按 "Start using Copilot Free" :




出現交談頁面, 輸入提示詞按 Enter 或右方三角形按鈕提交詢問 :




預設模型是 GPT-5 mini, 點下拉式選單可選擇模型, 免費版只能選 GPT-5 mini, GPT 4.1, GPT 4o, 以及 Claude Haiku 4.1 四種模型 :




我用經典的九九乘法表來測試 : 




生成如下程式碼 : 




#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
multiplication_table.py
印出傳統的九九乘法表(1 到 9)
"""

def multiplication_table(n: int = 9) -> None:
    """印出 1..n 的乘法表,每列只顯示到當前被乘數(傳統九九表格式)。"""
    for i in range(1, n + 1):
        for j in range(1, i + 1):
            # i*j 以 2 個字元寬度對齊,方便看齊
            print(f"{j}×{i}={i*j:2}", end="  ")
        print()

if __name__ == "__main__":
    multiplication_table()

我把程式丟到 Thonny 去執行, 結果如下 :

>>> %Run multiplication_table.py  
1×1= 1  
1×2= 2  2×2= 4  
1×3= 3  2×3= 6  3×3= 9  
1×4= 4  2×4= 8  3×4=12  4×4=16  
1×5= 5  2×5=10  3×5=15  4×5=20  5×5=25  
1×6= 6  2×6=12  3×6=18  4×6=24  5×6=30  6×6=36  
1×7= 7  2×7=14  3×7=21  4×7=28  5×7=35  6×7=42  7×7=49  
1×8= 8  2×8=16  3×8=24  4×8=32  5×8=40  6×8=48  7×8=56  8×8=64  
1×9= 9  2×9=18  3×9=27  4×9=36  5×9=45  6×9=54  7×9=63  8×9=72  9×9=81  


看起來還不錯, 接下來改用難一點的, 叫它幫我寫一個用 pandas-ta 套件計算 DMI 指標並繪製 K 線圖的程式, 提示詞如下 :

"請用 pandas-ta 套件的函式 ta.adx() 寫一個計算趨向指標 DMI (Directional Movement Index), 並使用 kbar 套件繪製 K 線圖 (顯示成交量) 的 Python 程式"

生成的程式碼看起來有點複雜 : 

# dmi_with_pandas_ta_and_kbar.py
from __future__ import annotations
import sys
import warnings

import pandas as pd
import pandas_ta as ta
import yfinance as yf

# plotting imports will be attempted lazily
import matplotlib.pyplot as plt

warnings.filterwarnings("ignore", category=FutureWarning)

def download_ohlcv(ticker: str = "AAPL", period: str = "6mo", interval: str = "1d") -> pd.DataFrame:
    """
    從 yfinance 下載 OHLCV 資料並回傳 DataFrame,index 為 DatetimeIndex。
    """
    df = yf.download(ticker, period=period, interval=interval, progress=False)
    if df.empty:
        raise RuntimeError(f"無法從 yfinance 取得資料: {ticker} {period} {interval}")
    # 確保欄位小寫且名稱為 open, high, low, close, volume
    df = df.rename(columns={c: c.lower() for c in df.columns})
    # 有些 yfinance 回傳 'adj close',確保 'close' 存在
    if "close" not in df.columns and "adj close" in df.columns:
        df["close"] = df["adj close"]
    return df[["open", "high", "low", "close", "volume"]]

def compute_adx_dmi(df: pd.DataFrame, length: int = 14) -> pd.DataFrame:
    """
    使用 pandas-ta 的 DataFrame accessor .ta.adx() 計算 ADX 與 DMP/DMN(+DI, -DI)。
    會 append 欄位到原始 DataFrame:
      - ADX_{length}
      - DMP_{length}  (通常視為 +DI)
      - DMN_{length}  (通常視為 -DI)
    另外建立便於使用的標準欄位: '+DI'、'-DI'、'ADX'
    """
    # pandas_ta 的 DataFrame accessor
    df = df.copy()
    df.ta.adx(length=length, append=True)  # 會生成 ADX_{len}, DMP_{len}, DMN_{len}
    adx_col = f"ADX_{length}"
    dmp_col = f"DMP_{length}"
    dmn_col = f"DMN_{length}"
    if adx_col not in df.columns or dmp_col not in df.columns or dmn_col not in df.columns:
        raise RuntimeError("pandas-ta 未生成預期欄位,請確認 pandas-ta 版本與使用方法。")
    # 方便使用的別名
    df["+DI"] = df[dmp_col]
    df["-DI"] = df[dmn_col]
    df["ADX"] = df[adx_col]
    return df

def plot_with_kbar_if_available(df: pd.DataFrame, ticker: str = "AAPL") -> None:
    """
    嘗試使用 kbar 繪圖;若失敗則退回到 mplfinance。
    kbar API 在不同版本可能有差異,所以這邊採用保守的嘗試方式:
    - 嘗試呼叫常見的 kbar.plot 或 kbar.kline.plot 等函式
    - 若都不存在或呼叫失敗,使用 mplfinance 繪圖
    """
    try:
        import kbar
    except Exception:
        kbar = None

    # 準備要在副圖上畫的線
    add_indicator_lines = {
        "+DI": df["+DI"],
        "-DI": df["-DI"],
        "ADX": df["ADX"],
    }

    # 嘗試 kbar 的常見畫法(多數 kbar 版本會接受 DataFrame)
    if kbar is not None:
        try:
            # 嘗試幾種常見的 kbar API 風格
            # 1) kbar.plot(df, ...) 或 kbar.kline.plot(...)
            if hasattr(kbar, "plot"):
                try:
                    # 這種呼叫是最簡短直觀的嘗試
                    kbar.plot(df, type="candle", volume=True, title=f"{ticker} - K 線 (kbar)")
                    # kbar.plot 不一定支援 overlay 指標,若需要可另外畫
                    # 畫 +DI, -DI, ADX 在新視窗
                    fig, ax = plt.subplots(1, 1, figsize=(10, 3))
                    ax.plot(df.index, df["+DI"], label="+DI", color="green")
                    ax.plot(df.index, df["-DI"], label="-DI", color="red")
                    ax.plot(df.index, df["ADX"], label="ADX", color="blue")
                    ax.set_title(f"{ticker} - +DI / -DI / ADX")
                    ax.legend()
                    plt.show()
                    return
                except Exception:
                    # 如果 kbar.plot 存在但執行失敗,嘗試其他 API
                    pass
            # 2) kbar.kline.plot
            if hasattr(kbar, "kline") and hasattr(kbar.kline, "plot"):
                kbar.kline.plot(df, volume=True, title=f"{ticker} - K 線 (kbar.kline)")
                fig, ax = plt.subplots(1, 1, figsize=(10, 3))
                ax.plot(df.index, df["+DI"], label="+DI", color="green")
                ax.plot(df.index, df["-DI"], label="-DI", color="red")
                ax.plot(df.index, df["ADX"], label="ADX", color="blue")
                ax.set_title(f"{ticker} - +DI / -DI / ADX")
                ax.legend()
                plt.show()
                return
            # 3) kbar.KLine 或其他類別介面
            if hasattr(kbar, "KLine"):
                try:
                    k = kbar.KLine(df)
                    if hasattr(k, "plot"):
                        k.plot(volume=True, title=f"{ticker} - K 線 (kbar.KLine)")
                        fig, ax = plt.subplots(1, 1, figsize=(10, 3))
                        ax.plot(df.index, df["+DI"], label="+DI", color="green")
                        ax.plot(df.index, df["-DI"], label="-DI", color="red")
                        ax.plot(df.index, df["ADX"], label="ADX", color="blue")
                        ax.set_title(f"{ticker} - +DI / -DI / ADX")
                        ax.legend()
                        plt.show()
                        return
                except Exception:
                    pass
            # 如果 kbar 存在但上述方法都失敗,拋出例外讓程式使用 mplfinance
            raise RuntimeError("kbar 已安裝但常見 API 呼叫失敗或不相容,改回使用 mplfinance。")
        except Exception as e:
            print("kbar 繪圖失敗,原因:", e, file=sys.stderr)
            print("將使用 mplfinance 作為備援繪圖。", file=sys.stderr)

    # --- mplfinance 備援繪圖 ---
    try:
        import mplfinance as mpf
    except Exception as e:
        raise RuntimeError("未安裝 kbar,也未安裝 mplfinance。請安裝至少一個繪圖套件 (pip install kbar mplfinance)。") from e

    # 準備 mplfinance 的 addplot
    ap = [
        mpf.make_addplot(df["+DI"], color="g", width=1.0, panel=1, ylabel="+DI / -DI"),
        mpf.make_addplot(df["-DI"], color="r", width=1.0, panel=1),
        mpf.make_addplot(df["ADX"], color="b", width=1.0, panel=2, ylabel="ADX"),
    ]
    # 長條圖顏色根據漲跌
    mc = mpf.make_marketcolors(up="tab:red", down="tab:green", volume="gray")
    s = mpf.make_mpf_style(marketcolors=mc)

    mpf.plot(
        df,
        type="candle",
        volume=True,
        style=s,
        addplot=ap,
        title=f"{ticker} - K 線 (mplfinance 備援)",
        figsize=(12, 8),
        datetime_format="%Y-%m-%d",
        tight_layout=True,
    )


def main():
    # 這裡可以改成從 CLI 參數讀入
    TICKER = "AAPL"
    PERIOD = "6mo"
    INTERVAL = "1d"
    ADX_LENGTH = 14

    print(f"下載 {TICKER} 的 OHLCV 資料 ({PERIOD}, {INTERVAL})...")
    df = download_ohlcv(TICKER, PERIOD, INTERVAL)
    print(f"資料筆數: {len(df)},樣本時間區間: {df.index[0]} ~ {df.index[-1]}")

    print(f"計算 ADX / DMI (length={ADX_LENGTH})...")
    df = compute_adx_dmi(df, length=ADX_LENGTH)

    # 顯示最後幾列結果以供檢視
    print(df[["open", "high", "low", "close", "volume", "+DI", "-DI", "ADX"]].tail(8))

    print("嘗試以 kbar 繪圖(若 kbar 不可用或 API 不相容,將退回到 mplfinance)...")
    plot_with_kbar_if_available(df, ticker=TICKER)


if __name__ == "__main__":
    main()

執行結果出現錯誤 : 

>>> %Run dmi_with_pandas_ta_and_kbar.py   
下載 AAPL 的 OHLCV 資料 (6mo, 1d)...
Traceback (most recent call last):
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 177, in <module>
    main()
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 163, in main
    df = download_ohlcv(TICKER, PERIOD, INTERVAL)
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 23, in download_ohlcv
    df = df.rename(columns={c: c.lower() for c in df.columns})
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 23, in <dictcomp>
    df = df.rename(columns={c: c.lower() for c in df.columns})
AttributeError: 'tuple' object has no attribute 'lower'

顯然它並不知道 yfinance 已經改版了, 新版的傳回值是多層 columns 結構, 必須先轉成傳統的單層 DataFrame, 我將此錯誤訊息回饋給它 :

"上面的程式執行出現如下錯誤, 請 debug 並修正"

但是仍然出現錯誤, 乾脆提供我之前寫的 AOBV 指標測試程式給它 :

"請參考下面 AOBV 指標的寫法 :
# pandas_ta_aobv.py
import yfinance as yf     
import pandas_ta as ta
from kbar import KBar  

df=yf.download('0050.TW', start='2024-11-06', end='2025-01-09', auto_adjust=False)
df.columns=df.columns.map(lambda x: x[0])
df.ta.aobv(close='Close', append=True)
kb=KBar(df)
kb.addplot(df['OBV'], panel=2, color='red', width=1.2, ylabel='OBV')
kb.addplot(df['AOBV_LR_2'], panel=3, color='red', width=1.2, ylabel='AOBV')
kb.addplot(df['AOBV_SR_2'], panel=3, color='blue', width=1.2)
kb.plot(volume=True, mav=(5, 10), title='K 線圖與 OBV, AOBV 指標')"

終於給出可以正確執行的程式碼 : 

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
dmi_with_pandas_ta_and_kbar.py

說明:
- 參考你提供的 AOBV 範例寫法,使用 kbar.KBar 來繪製 K 線(顯示成交量)並把 +DI / -DI / ADX 加到副圖。
- 使用 pandas-ta 的 .ta.adx() 計算 ADX / +DI / -DI。
- 若欄位為 MultiIndex(yfinance 在某些情況會回傳),會取每個 tuple 的第一個元素,如同你的 AOBV 範例。
- 若 kbar 不可用,會退回到 mplfinance 繪圖(與前一版行為一致)。

用法:
python dmi_with_pandas_ta_and_kbar.py --ticker AAPL --period 6mo --interval 1d --length 14
或用 start/end:
python dmi_with_pandas_ta_and_kbar.py --ticker 0050.TW --start 2024-11-06 --end 2025-01-09 --interval 1d --length 14
"""
from __future__ import annotations
import argparse
import sys
import warnings

import pandas as pd
import pandas_ta as ta
import yfinance as yf

warnings.filterwarnings("ignore", category=FutureWarning)


def download_ohlcv(ticker: str, period: str | None, start: str | None, end: str | None, interval: str) -> pd.DataFrame:
    """
    下載 OHLCV,並針對 MultiIndex 欄位做簡單 normalize(取 tuple[0])。
    最後確保至少有 Open/High/Low/Close/Volume (TitleCase 或 Close/close) 可用。
    """
    # yfinance: 若有 start/end 則通常不用 period
    if start or end:
        df = yf.download(ticker, start=start, end=end, interval=interval, auto_adjust=False, progress=False)
    else:
        df = yf.download(ticker, period=period or "6mo", interval=interval, auto_adjust=False, progress=False)

    if df.empty:
        raise RuntimeError(f"無法從 yfinance 取得資料: {ticker} (period={period}, start={start}, end={end}, interval={interval})")

    # 如果是 MultiIndex,像 (('Open',''),('High','')) 等情況,取 tuple 的第 0 個元素 (與你的 AOBV 範例相同)
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.map(lambda x: x[0])

    # 有時候欄位是小寫、有時是 Title Case,統一處理:
    cols = {c: str(c) for c in df.columns}

    # 若只有 'Adj Close' 而沒有 'Close',建立 Close 欄位(跟 AOBV 範例做法一致)
    if "Close" not in df.columns and "Adj Close" in df.columns:
        df["Close"] = df["Adj Close"]
    if "close" not in df.columns and "adj close" in [c.lower() for c in df.columns]:
        # handle lowercase adj close
        # find actual column name that lower() == 'adj close'
        for c in df.columns:
            if str(c).lower() == "adj close":
                df["close"] = df[c]
                break

    # 最後檢查至少能找到 High/Low/Close (或 high/low/close)
    found_high = any(c.lower() == "high" for c in df.columns)
    found_low = any(c.lower() == "low" for c in df.columns)
    found_close = any(c.lower() == "close" for c in df.columns)
    found_open = any(c.lower() == "open" for c in df.columns)
    found_volume = any(c.lower() == "volume" for c in df.columns)

    if not (found_high and found_low and found_close):
        raise RuntimeError(f"yfinance 回傳欄位缺少 high/low/close (available: {list(df.columns)})")

    # 為了方便 pandas-ta 與 kbar,建立常見 TitleCase 欄位 (Open/High/Low/Close/Volume) 如果尚未有
    def ensure_title(col_lower, title):
        if title not in df.columns:
            for c in df.columns:
                if str(c).lower() == col_lower:
                    df[title] = df[c]
                    return

    ensure_title("open", "Open")
    ensure_title("high", "High")
    ensure_title("low", "Low")
    ensure_title("close", "Close")
    ensure_title("volume", "Volume")

    return df


def compute_adx_dmi(df: pd.DataFrame, length: int = 14) -> pd.DataFrame:
    """
    使用 pandas-ta 計算 ADX/DMP/DMN,並增加 '+DI','-DI','ADX' 欄位 (以 length 為 suffix)。
    需要欄位 High/Low/Close(不論大小寫)。
    """
    # pandas-ta 在 DataFrame accessor 上運作,接受 high/low/close 欄位名稱參數
    # 我們先嘗試用 TitleCase 欄位 (High/Low/Close)(符合 yfinance 常態)
    df2 = df.copy()

    # 指定欄位名稱(若沒有 TitleCase,改用小寫)
    high_col = "High" if "High" in df2.columns else next((c for c in df2.columns if c.lower() == "high"), None)
    low_col = "Low" if "Low" in df2.columns else next((c for c in df2.columns if c.lower() == "low"), None)
    close_col = "Close" if "Close" in df2.columns else next((c for c in df2.columns if c.lower() == "close"), None)

    if not (high_col and low_col and close_col):
        raise RuntimeError("需要 high/low/close 欄位才能計算 ADX/DMI")

    # pandas-ta 的 ta.adx 可以接受參數 high/low/close
    df2.ta.adx(length=length, high=high_col, low=low_col, close=close_col, append=True)

    # pandas-ta 會建立 ADX_{length}, DMP_{length}, DMN_{length}
    adx_col = f"ADX_{length}"
    dmp_col = f"DMP_{length}"
    dmn_col = f"DMN_{length}"

    if adx_col not in df2.columns or dmp_col not in df2.columns or dmn_col not in df2.columns:
        raise RuntimeError("pandas-ta 未生成預期欄位,請確認 pandas-ta 版本 (生成 ADX_{len}, DMP_{len}, DMN_{len})")

    # 建立更方便的欄位名稱(與你先前程式一致)
    df2["+DI"] = df2[dmp_col]
    df2["-DI"] = df2[dmn_col]
    df2["ADX"] = df2[adx_col]

    return df2


def plot_with_kbar(df: pd.DataFrame, ticker: str = "AAPL", mav: tuple | None = (5, 10)):
    """
    依照 AOBV 的寫法使用 kbar.KBar:
      - kb = KBar(df)
      - kb.addplot(series, panel=..., color=..., width=..., ylabel=...)
      - kb.plot(volume=True, mav=(...), title=...)
    若 kbar 不可用,退回到 mplfinance。
    """
    try:
        from kbar import KBar
    except Exception as e:
        KBar = None
        print("kbar 無法匯入,將使用 mplfinance 作為備援繪圖。", file=sys.stderr)

    # kbar 範例通常以 TitleCase 欄位 (Open/High/Low/Close/Volume)
    # 確保 DataFrame 中有 TitleCase 欄位,否則建立對應欄位
    df_plot = df.copy()
    for lower, title in (("open", "Open"), ("high", "High"), ("low", "Low"), ("close", "Close"), ("volume", "Volume")):
        if title not in df_plot.columns:
            for c in df_plot.columns:
                if str(c).lower() == lower:
                    df_plot[title] = df_plot[c]
                    break

    if KBar is not None:
        try:
            kb = KBar(df_plot)
            # +DI / -DI 放 panel=2,ADX 放 panel=3(參照你給的 AOBV 範例 panel 用法)
            kb.addplot(df_plot["+DI"], panel=2, color="green", width=1.2, ylabel="+DI")
            kb.addplot(df_plot["-DI"], panel=2, color="red", width=1.2)
            kb.addplot(df_plot["ADX"], panel=3, color="blue", width=1.2, ylabel="ADX")
            kb.plot(volume=True, mav=mav, title=f"{ticker} - K 線圖與 DMI (+DI/-DI/ADX)")
            return
        except Exception as e:
            print("使用 kbar 繪圖時發生錯誤,將退回到 mplfinance。錯誤:", e, file=sys.stderr)

    # mplfinance 備援繪圖
    try:
        import mplfinance as mpf
    except Exception as e:
        raise RuntimeError("未安裝 kbar,也未安裝 mplfinance。請安裝至少一個繪圖套件 (pip install kbar mplfinance)。") from e

    # mplfinance 需要 TitleCase 欄位
    df_mpf = df_plot.copy()

    ap = [
        mpf.make_addplot(df_mpf["+DI"], color="g", width=1.0, panel=1, ylabel="+DI / -DI"),
        mpf.make_addplot(df_mpf["-DI"], color="r", width=1.0, panel=1),
        mpf.make_addplot(df_mpf["ADX"], color="b", width=1.0, panel=2, ylabel="ADX"),
    ]
    mc = mpf.make_marketcolors(up="tab:red", down="tab:green", volume="gray")
    s = mpf.make_mpf_style(marketcolors=mc)

    mpf.plot(
        df_mpf.rename(columns={c: (c if isinstance(c, str) else str(c)) for c in df_mpf.columns}),
        type="candle",
        volume=True,
        style=s,
        addplot=ap,
        title=f"{ticker} - K 線圖與 DMI (+DI/-DI/ADX) (mplfinance)",
        figsize=(12, 8),
        mav=mav,
        datetime_format="%Y-%m-%d",
        tight_layout=True,
    )


def parse_args():
    p = argparse.ArgumentParser(description="計算 DMI (ADX/+DI/-DI) 並繪製 K 線 (kbar 或 mplfinance)")
    p.add_argument("--ticker", "-t", default="AAPL", help="股票代號,例如 AAPL 或 0050.TW")
    p.add_argument("--period", "-p", default="6mo", help="若未指定 start/end,使用 yfinance period (ex: 6mo, 1y)")
    p.add_argument("--start", help="開始日期 (YYYY-MM-DD),若指定則使用 start/end 而非 period")
    p.add_argument("--end", help="結束日期 (YYYY-MM-DD)")
    p.add_argument("--interval", "-i", default="1d", help="yfinance interval,例如 1d, 1h")
    p.add_argument("--length", "-l", type=int, default=14, help="ADX 計算長度 (預設 14)")
    p.add_argument("--mav", nargs="*", type=int, default=(5, 10), help="移動平均線 mav,例如 --mav 5 10")
    return p.parse_args()


def main():
    args = parse_args()
    try:
        print(f"下載 {args.ticker} 的 OHLCV 資料 (period={args.period}, start={args.start}, end={args.end}, interval={args.interval})...")
        df = download_ohlcv(args.ticker, args.period, args.start, args.end, args.interval)
        print(f"資料筆數: {len(df)},時間區間: {df.index[0]} ~ {df.index[-1]}")
        print(f"計算 ADX / DMI (length={args.length})...")
        df2 = compute_adx_dmi(df, length=args.length)
        # 顯示最後幾列確認
        display_cols = [c for c in ["Open", "High", "Low", "Close", "Volume", "+DI", "-DI", "ADX"] if c in df2.columns]
        print(df2[display_cols].tail(8))
        print("繪圖(優先使用 kbar,如不可用則使用 mplfinance)...")
        plot_with_kbar(df2, ticker=args.ticker, mav=tuple(args.mav) if args.mav else None)
    except Exception as e:
        print("執行發生錯誤:", e, file=sys.stderr)
        raise

if __name__ == "__main__":
    main()

執行結果 :




唉, 明明 11 行程式碼就能搞定, 為何要寫得這麼複雜呢? 


沒有留言 :