2026年10月17日 星期六

Ollama 學習筆記 : Qwen2.5-Coder 模型測試 (qwen2.5-coder:14b)

最近偶然得知有一個開源的 OpenCode 程式代理, 可以搭配 Ollama 在本地打造類似 Claude Code 的 AI Coding Agent 功能, 看搭配甚麼模型, 據估計效能大約可達 Claude Code 的 65%~85% 功力 (真的嗎 ?), 足敷中小型專案使用, 參考 : 


本系列全部測試文章索引參考 :
適合在我的 RTX3060 12GB VRAM 顯卡上跑的開源 Coding 模型是 Qwen2.5-Coder-14B, 其大小為 9GB, 可以完全載入顯卡內, 12GB VRAM 扣除 9 GB 模型後, 還剩下約 2.5~3 GB 空間可供 KV Cache 運算, 這讓 Ollama 可以穩穩開出 16k~32k 的 num_ctx, 足夠讓 OpenCode 一次讀取數個 Python 模組並產出完整的 patch, 而不會引發 CUDA OOM 當機, 這剛好位於硬體極限, 上下文長度與 Agent 任務穩定度的最佳平衡點, 推論速度仍能維持在健康的每秒 25~35 Tokens, 足以勝任真實專案開發的推理深度. 

在安裝 OpenCode 之前, 我想先對 Qwen2.5-Coder-14B 模型做個測試, 看看其回應速度與評估其  Coding 能力. 此模型可從 Ollama 網站下載 :




下載 & 載入模型之指令如下 :

ollama run qwen2.5-coder:14b  

注意, 後面指定 :14b 是必要的, 如上面 Qwen 2.5 Coder 模型列表所示, 如果沒有指定模型大小, 預設會下載 latest 的 7b 模型. 

C:\Users\tony1>ollama run qwen2.5-coder:14b    
pulling manifest
pulling ac9bc7a69dab: 100% ▕██████████████████ ▏ 9.0 GB/9.0 GB   40 MB/s      0s
pulling 0578f229f23a: 100% ▕██████████████████▏  488 B
verifying sha256 digest
writing manifest
success


1. 基本測試 : 

先做一些基本的測試 : 

>>> 你是誰?   
我是Qwen,由阿里云开发的超大规模语言模型。我被设计用来回答各种问题、提供信息和与用户进行对话。有什么我可以帮助你的吗
?
>>> 請一律使用繁體中文與我交談
好的,請問有什麼我可以幫助您的嗎?

>>> 請簡介量子糾纏
量子糾纏是量子力學中一個非常有趣且重要的現象,它描述了兩個或多個量子粒子之間的特殊關聯,使得這些
粒子的狀態無法分開來描述。即使這些粒子相隔很遠,一個粒子的狀態改變會立即影響到另一個粒子的狀態,
這種影響是即時的,不受距離的限制。

量子糾纏是量子通信、量子計算和量子機器學習等領域中研究的基礎。例如,量子密鑰分發利用量子糾纏來實
現安全的密鑰分發,即使在第三方試圖竊取密鑰的情況下,也能夠被檢測到。此外,量子計算機也利用量子糾
纏來實現並行計算,從而解決傳統計算機無法高效處理的問題。

量子糾纏的現象和應用是量子力學研究的熱門領域,對於未來的科技發展具有重要的意義。

測試數理邏輯 : 

>>> 3.10 與 3.8 這兩個數值哪一個比較大?   
3.10 比 3.8 大。

顯然是不經大腦的反射式答案, 請它細思量 : 

>>> 請思考推理一下再回答
當然,我們可以一步一步地來比較這兩個數值:

1. 首先比較整數部分:3.10 和 3.8 的整數部分都是 3,所以需要進一步比較小數部分。
2. 接著比較小數部分的第一位:3.10 的小數部分第一位是 1,而 3.8 的小數部分第一位是 8。
3. 顯然,1 小於 8,因此 3.10 小於 3.8。

所以,3.8 比 3.10 大。

這下就正確了.  


2. 基本程式能力測試 : 

接下來測試基本的程式能力 : 

>>> 請用 Python 寫一個基本的 GUI 四則運算計算器程式
當然可以!下面是一個使用 Python 和 Tkinter 庫編寫的基本四則運算計算器程式的範例:

```python
import tkinter as tk

class Calculator:
    def __init__(self, root):
        self.root = root
        self.root.title("Calculator")

        self.display = tk.Entry(root, width=40, borderwidth=5)
        self.display.grid(row=0, column=0, columnspan=4, padx=10, pady=10)

        buttons = [
            ('7', 1, 0), ('8', 1, 1), ('9', 1, 2), ('/', 1, 3),
            ('4', 2, 0), ('5', 2, 1), ('6', 2, 2), ('*', 2, 3),
            ('1', 3, 0), ('2', 3, 1), ('3', 3, 2), ('-', 3, 3),
            ('0', 4, 0), ('.', 4, 1), ('=', 4, 2), ('+', 4, 3)
        ]

        for (text, row, col) in buttons:
            button = tk.Button(root, text=text, width=10, height=3, command=lambda t=text:
self.click_button(t))
            button.grid(row=row, column=col, sticky="nsew")

        for i in range(4):
            root.grid_columnconfigure(i, weight=1)
            root.grid_rowconfigure(i+1, weight=1)

    def click_button(self, text):
        if text == '=':
            try:
                result = eval(self.display.get())
                self.display.delete(0, tk.END)
                self.display.insert(0, str(result))
            except Exception as e:
                self.display.delete(0, tk.END)
                self.display.insert(0, "Error")
        else:
            self.display.insert(tk.END, text)

if __name__ == "__main__":
    root = tk.Tk()
    calculator = Calculator(root)
    root.mainloop()
```

這個程式碼創建了一個簡單的四則運算計算器,包含數字按鈕、運算符號按鈕和等號按鈕。當用戶點擊按鈕時
,計算器會將輸入的數字和運算符號顯示在輸入框中,當用戶點擊等號時,計算器會計算並顯示結果。如果計
算過程中出現錯誤,計算器會顯示 "Error"。

執行此程式功能正常 : 





叫它加入 C 與 CE 按鈕 : 

>>> 請加上 C 或 CE 清除輸入框的按鈕   
當然可以!我們可以在計算器中添加一個 "C" 或 "CE" 按鈕,用於清除輸入框中的內容。下面是修改後的程式
碼:

```python
import tkinter as tk

class Calculator:
    def __init__(self, root):
        self.root = root
        self.root.title("Calculator")

        self.display = tk.Entry(root, width=40, borderwidth=5)
        self.display.grid(row=0, column=0, columnspan=4, padx=10, pady=10)

        buttons = [
            ('7', 1, 0), ('8', 1, 1), ('9', 1, 2), ('/', 1, 3),
            ('4', 2, 0), ('5', 2, 1), ('6', 2, 2), ('*', 2, 3),
            ('1', 3, 0), ('2', 3, 1), ('3', 3, 2), ('-', 3, 3),
            ('0', 4, 0), ('.', 4, 1), ('=', 4, 2), ('+', 4, 3),
            ('C', 5, 0), ('CE', 5, 1)  # 添加 C 和 CE 按鈕
        ]

        for (text, row, col) in buttons:
            button = tk.Button(root, text=text, width=10, height=3, command=lambda t=text:
self.click_button(t))
            button.grid(row=row, column=col, sticky="nsew")

        for i in range(4):
            root.grid_columnconfigure(i, weight=1)
            root.grid_rowconfigure(i+1, weight=1)

    def click_button(self, text):
        if text == '=':
            try:
                result = eval(self.display.get())
                self.display.delete(0, tk.END)
                self.display.insert(0, str(result))
            except Exception as e:
                self.display.delete(0, tk.END)
                self.display.insert(0, "Error")
        elif text == 'C' or text == 'CE':
            self.display.delete(0, tk.END)  # 清除輸入框中的內容
        else:
            self.display.insert(tk.END, text)

if __name__ == "__main__":
    root = tk.Tk()
    calculator = Calculator(root)
    root.mainloop()
```

現在,計算器中新增了 "C" 和 "CE" 按鈕。點擊 "C" 或 "CE" 按鈕時,計算器會清除輸入框中的內容。

執行結果功能正常 :




再叫它添加一個倒退鈕 : 

>>> 可以再加一個倒退刪除鍵嗎?  
當然可以!我們可以在計算器中添加一個倒退刪除鍵,用於刪除輸入框中的最後一個字符。下面是修改後的程式碼:

```python
import tkinter as tk

class Calculator:
    def __init__(self, root):
        self.root = root
        self.root.title("Calculator")

        self.display = tk.Entry(root, width=40, borderwidth=5)
        self.display.grid(row=0, column=0, columnspan=4, padx=10, pady=10)

        buttons = [
            ('7', 1, 0), ('8', 1, 1), ('9', 1, 2), ('/', 1, 3),
            ('4', 2, 0), ('5', 2, 1), ('6', 2, 2), ('*', 2, 3),
            ('1', 3, 0), ('2', 3, 1), ('3', 3, 2), ('-', 3, 3),
            ('0', 4, 0), ('.', 4, 1), ('=', 4, 2), ('+', 4, 3),
            ('C', 5, 0), ('CE', 5, 1), ('<', 5, 2)  # 添加倒退刪除鍵
        ]

        for (text, row, col) in buttons:
            button = tk.Button(root, text=text, width=10, height=3, command=lambda t=text:
self.click_button(t))
            button.grid(row=row, column=col, sticky="nsew")

        for i in range(4):
            root.grid_columnconfigure(i, weight=1)
            root.grid_rowconfigure(i+1, weight=1)

    def click_button(self, text):
        if text == '=':
            try:
                result = eval(self.display.get())
                self.display.delete(0, tk.END)
                self.display.insert(0, str(result))
            except Exception as e:
                self.display.delete(0, tk.END)
                self.display.insert(0, "Error")
        elif text == 'C':
            self.display.delete(0, tk.END)  # 清除輸入框中的內容
        elif text == 'CE':
            self.display.delete(0, tk.END)  # 清除輸入框中的內容
        elif text == '<':
            current_text = self.display.get()
            if current_text:
                self.display.delete(len(current_text) - 1)  # 刪除最後一個字符
        else:
            self.display.insert(tk.END, text)

if __name__ == "__main__":
    root = tk.Tk()
    calculator = Calculator(root)
    root.mainloop()
```

現在,計算器中新增了倒退刪除鍵("<")。點擊倒退刪除鍵時,計算器會刪除輸入框中的最後一個字符。

結果 OK : 




看來簡單的程式似乎難不倒它, 模型回應速度也很快, 幾乎是秒回. 


3. 進階推理測試 : 

接下來考驗它進階的推理能力 :

>>> 有 100 顆球,其中 1 顆重量不同,但不知道它比較重還是比較輕。只有一個沒有砝碼的天平,最多可以秤 3 次。請找出異常球,並判斷它是比較重還是比較輕。請完整說明策略。

要解決這個問題,我們可以使用三步驟的策略來找出異常球,並判斷它是比較重還是比較輕。以下是詳細的步
驟:

### 步驟 1: 第一次秤量
1. 將 100 顆球分成三組:33 顆、33 顆和 34 顆。
2. 將兩組各 33 顆球放到天平的兩端。

#### 情況分析:
- 如果天平平衡,異常球在剩下的 34 顆球中。
- 如果天平不平衡,異常球在天平上較重或較輕的那一端的 33 顆球中。

### 步驟 2: 第二次秤量
1. 如果第一次天平平衡,從剩下的 34 顆球中取出 11 顆球,分成三組:11 顆、11 顆和 12 顆。
2. 將兩組各 11 顆球放到天平的兩端。

#### 情況分析:
- 如果天平平衡,異常球在剩下的 12 顆球中。
- 如果天平不平衡,異常球在天平上較重或較輕的那一端的 11 顆球中。

### 步驟 3: 第三次秤量
1. 如果第二次天平平衡,從剩下的 12 顆球中取出 4 顆球,分成三組:4 顆、4 顆和 4 顆。
2. 將兩組各 4 顆球放到天平的兩端。

#### 情況分析:
- 如果天平平衡,異常球在剩下的 4 顆球中。
- 如果天平不平衡,異常球在天平上較重或較輕的那一端的 4 顆球中。

### 步驟 4: 第四步(可選)
1. 如果第三次天平平衡,從剩下的 4 顆球中取出 2 顆球,分成三組:2 顆、2 顆和 0 顆。
2. 將兩組各 2 顆球放到天平的兩端。

#### 情況分析:
- 如果天平平衡,異常球在剩下的 2 顆球中。
- 如果天平不平衡,異常球在天平上較重或較輕的那一端的 2 顆球中。

### 步驟 5: 確定異常球
1. 如果第三次天平不平衡,從天平上較重或較輕的那一端的 4 顆球中取出 2 顆球,分成三組:1 顆、1 顆和
 2 顆。
2. 將兩組各 1 顆球放到天平的兩端。

#### 情況分析:
- 如果天平平衡,異常球在剩下的 2 顆球中。
- 如果天平不平衡,異常球在天平上較重或較輕的那一端的 1 顆球中。

### 確定異常球的重量
1. 如果在最後一次秤量中,天平平衡,異常球在剩下的 2 顆球中。取出這兩颗球,其中一顆放到天平上,另
一顆作為標準。如果天平平衡,則剩下的球是異常球;如果天平不平衡,則另一顆球是異常球。
2. 如果在最後一次秤量中,天平不平衡,異常球在天平上較重或較輕的那一端的 1 顆球中,即為異常球。

這樣,我們就可以在最多三次秤量內找出異常球,並判斷它是比較重還是比較輕。

可見 qwen2.5-coder:14b 的基本程式能力雖然還不錯, 但它的推理能力不足, 上面的回應完全錯誤, 因為在數學理論上, 100 顆球秤 3 次找出異常球是不可能的 (至少需要 5 次秤重). 參考之前 測試 OpenClaw 時, 對 GPT 5.6 Luna 的推理測試 : 



4. 進階程式能力測試 : 

接下來真正的大魔王來了, 下面是一個金融相關任務 : 

>>> 請寫一個 Python 函式:
...
... def detect_drawdown(prices, threshold=0.2):
...
... 輸入是一個每日收盤價 list,找出所有從歷史高點回撤超過 threshold 的區段。
...
... 要求:
... - 使用 pandas
... - 回傳每個 drawdown 的 peak date、trough date、peak price、trough price、drawdown
... - 處理空資料
... - 提供可直接執行的完整程式
... - 附一組測試資料

好的,下面是一個使用 pandas 的 Python 函式 `detect_drawdown`,它可以找出所有從歷史高點回撤超過指
定閾值的區段,並返回每個回撤的峰值日期、谷值日期、峰值價格、谷值價格和回撤百分比。

```python
import pandas as pd
import numpy as np

def detect_drawdown(prices, threshold=0.2):
    # 檢查輸入是否為有效資料
    if prices is None or len(prices) == 0:
        return pd.DataFrame(columns=['peak_date', 'trough_date', 'peak_price', 'trough_price',
'drawdown'])

    # 轉換為 pandas Series
    prices = pd.Series(prices, name='price')

    # 計算歷史高點
    rolling_max = prices.cummax()

    # 計算回撤百分比
    drawdown = (prices - rolling_max) / rolling_max

    # 找出回撤超過阈值的點
    drawdown_mask = drawdown < -threshold

    # 找出峰值和谷值的索引
    peak_indices = drawdown_mask[drawdown_mask].index
    trough_indices = []

    for peak_idx in peak_indices:
        # 從峰值後面的資料中找出谷值
        trough_idx = drawdown.iloc[peak_idx+1:].idxmin()
        trough_indices.append(trough_idx)

    # 轉換索引為日期
    peak_dates = prices.index[peak_indices]
    trough_dates = prices.index[trough_indices]
    peak_prices = prices.loc[peak_indices]
    trough_prices = prices.loc[trough_indices]
    drawdown_values = drawdown.loc[peak_indices]

    # 創建結果 DataFrame
    result = pd.DataFrame({
        'peak_date': peak_dates,
        'trough_date': trough_dates,
        'peak_price': peak_prices,
        'trough_price': trough_prices,
        'drawdown': drawdown_values
    })

    return result

# 測試資料
prices = [100, 98, 95, 102, 101, 90, 88, 85, 92, 91, 80, 78, 75, 82, 81, 70, 68, 65, 72, 71]
prices = pd.Series(prices, index=pd.date_range(start='2023-01-01', periods=len(prices),
freq='D'))

# 執行函式並打印結果
result = detect_drawdown(prices, threshold=0.2)
print(result)
```

這段程式碼定義了一個 `detect_drawdown` 函式,它接受一個包含每日收盤價的 pandas Series 和一個回撤
閾值(預設為 0.2)。函式會返回一個包含每個回撤區段峰值日期、谷值日期、峰值價格、谷值價格和回撤百
分比的 DataFrame。

### 測試資料
測試資料是一個包含 20 個收盤價的 pandas Series,索引為從 2023-01-01 開始的日期範圍。這個範例中,
有一些從歷史高點回撤超過 20% 的區段。

### 結果
執行 `detect_drawdown(prices, threshold=0.2)` 後,會得到一個包含所有回撤區段的 DataFrame,其中每
一行代表一個回撤區段,包含峰值日期、谷值日期、峰值價格、谷值價格和回撤百分比。

希望這段程式碼能夠幫助你找出所有的回撤區段!

我把程式碼提交給 Gemini 審查, 結果發現模型欠缺高階 Python 程式技巧, 且對金融方面知識不足, 導致程式碼存在致命缺陷無法執行 : 

>>> 你這段程式碼存在嚴重的執行期錯誤(Runtime Bug)與金融邏輯錯誤,無法正常運作。模型對 pandas API 的
... 基礎語法具備基本認識,並寫出了基本的輸入保護與測試資料架構,但在金融指標定義、pandas 索引定位與區
... 段分割(Segmentation)上出現了致命缺陷。
... 1. 執行期報錯:idxmin() 在全空切片上引發例外
... 2. 定義混淆:誤把「回撤超標點」當成「歷史高點 Peak」
... 3. 缺乏「區段聚合(Grouping)」:重複輸出點而非區段
... 請檢查問題在哪裡, 並提出新版程式碼.

回應如下 : 

非常感謝你的指正,你提到的問題非常關鍵。讓我們一步步來解決這些問題:

1. **執行期報錯**:`idxmin()` 在全空切片上引發例外。
2. **定義混淆**:誤把「回撤超標點」當成「歷史高點 Peak」。
3. **缺乏「區段聚合(Grouping)」**:重複輸出點而非區段。

我們將修正這些問題,並提供一個新的版本。

### 步驟 1: 確保輸入無空值
首先,我們需要確保輸入的 `prices` 無空值,因為 `cummax` 和 `idxmin` 在空值上會引發問題。

### 步驟 2: 正確識別峰值和谷值
峰值應該是從當前日期之前的歷史高點,而谷值是回撤超過閾值後的最低點。

### 步驟 3: 區段聚合
我們需要確保只返回每個回撤區段的起始和結束點,而不是每個點。

### 步驟 4: 處理空資料
如果 `prices` 為空,則返回一個空的 DataFrame。

### 步驟 5: 修正執行期錯誤
確保在計算峰值和谷值時,不會在空切片上調用 `idxmin`。

### 新版程式碼

```python
import pandas as pd
import numpy as np

def detect_drawdown(prices, threshold=0.2):
    # 檢查輸入是否為有效資料
    if prices is None or len(prices) == 0:
        return pd.DataFrame(columns=['peak_date', 'trough_date', 'peak_price', 'trough_price',
'drawdown'])

    # 移除空值
    prices = prices.dropna()

    if len(prices) == 0:
        return pd.DataFrame(columns=['peak_date', 'trough_date', 'peak_price', 'trough_price',
'drawdown'])

    # 計算歷史高點
    rolling_max = prices.cummax()

    # 計算回撤百分比
    drawdown = (prices - rolling_max) / rolling_max

    # 找出回撤超過閾值的點
    drawdown_mask = drawdown < -threshold

    # 找出峰值和谷值的索引
    peak_indices = []
    trough_indices = []

    # 記錄上一個峰值的索引
    last_peak_idx = None

    for idx in range(len(prices)):
        if drawdown_mask[idx]:
            if last_peak_idx is None or prices[idx] > prices[last_peak_idx]:
                last_peak_idx = idx
        else:
            if last_peak_idx is not None:
                # 找出谷值
                trough_idx = drawdown.iloc[last_peak_idx+1:idx+1].idxmin()
                peak_indices.append(last_peak_idx)
                trough_indices.append(trough_idx)
                last_peak_idx = None

    # 轉換索引為日期
    peak_dates = prices.index[peak_indices]
    trough_dates = prices.index[trough_indices]
    peak_prices = prices.loc[peak_indices]
    trough_prices = prices.loc[trough_indices]
    drawdown_values = drawdown.loc[peak_indices]

    # 創建結果 DataFrame
    result = pd.DataFrame({
        'peak_date': peak_dates,
        'trough_date': trough_dates,
        'peak_price': peak_prices,
        'trough_price': trough_prices,
        'drawdown': drawdown_values
    })

    return result

# 測試資料
prices = [100, 98, 95, 102, 101, 90, 88, 85, 92, 91, 80, 78, 75, 82, 81, 70, 68, 65, 72, 71]
prices = pd.Series(prices, index=pd.date_range(start='2023-01-01', periods=len(prices),
freq='D'))

# 執行函式並打印結果
result = detect_drawdown(prices, threshold=0.2)
print(result)
```

### 說明
1. **移除空值**:`prices = prices.dropna()` 確保在計算時沒有空值。
2. **峰值和谷值的識別**:使用一個迴圈來遍歷 `prices`,並記錄峰值和谷值的索引。
3. **區段聚合**:確保只返回每個回撤區段的起始和結束點。
4. **執行期錯誤處理**:確保在計算峰值和谷值時,不會在空切片上調用 `idxmin`。

### 測試結果
執行新版程式碼後,應該能夠正確找出所有的回撤區段,並返回每個回撤區段的起始和結束點。

希望這段新版程式碼能夠解決你的問題!

再次審查結果更糟, 不僅沒有改對, 反而引入了更離譜的邏輯與執行時期錯誤. 

總評如下 :

qwen2.5-coder:14b 在這種時序狀態機 (State Machine)/區間聚合 (Segmentation) 的邏輯上看似有反思能力, 能完美覆述所指出的問題且講得頭頭是道, 但缺乏真正的程式模擬運算能力, 它在 prompt 裡承諾會修好, 但在生成程式碼時只是把原來的 for...in 換成了 for idx in range(...) 的傳統 C 語言思維, 既沒搞懂 pandas 的標籤/位置索引差異, 也沒有在腦中虛擬執行過變數生命週期. 它並沒有改對, 只是從寫法 A 的錯誤換成了更複雜但依然錯得離譜的寫法 B 而已. 

總之, qwen2.5-coder:14b 模型大概只能應付小規模單檔重構, 單元測試補全, Docstring, 與純語法翻譯而已, 其內部注意力機制無法維持長鏈條的變數生命週期, 欠缺領域語意 (Domain Semantics) 能力, 很容易寫出語法合法但邏輯顛倒的程式碼 (例如在暴跌日找 Peak). 唉, 看來 qwen2.5-coder:14b 只能應付學校的作業而已. 


2026-10-17 補充 :

我使用 Gemini 建議的高約束 Prompt (明確虛擬碼 + 向量化約束 + 禁令清單) :

你是一位嚴謹的量化工程師。請為我實作 Python 函式 `detect_drawdown(prices, threshold=0.2)`。

【演算法指定邏輯(必須嚴格依此實作,不得自行發明迴圈或自創狀態機)】:
1. 防禦性檢查:
   - 檢查 `prices is None` 或為空,若是則直接回傳符合欄位定義的空 DataFrame。
   - 轉為 pd.Series,執行 dropna()。
2. 向量化波段分組(Episode Segmentation):
   - 計算歷史累積最高價:`cummax = prices.cummax()`
   - 計算回撤比例(負值):`drawdown = (prices - cummax) / cummax`
   - 利用是否創歷史新高建立波段編號:`episodes = (prices == cummax).cumsum()`
3. 波段聚合計算(GroupBy):
   - 對 `prices.groupby(episodes)` 進行分組迭代:
     * 該組第 1 個點(`group.index[0]`, `group.iloc[0]`)必定是該波段的歷史最高點(peak_date, peak_price)。
     * 取出該組對應的 `drawdown` 子序列,找出最小回撤值 `min_dd = group_dd.min()`。
     * 若 `min_dd <= -threshold`(跌破門檻):
       - 谷底日期 `trough_date` 為 `group_dd.idxmin()`。
       - 谷底價格 `trough_price` 為 `group.loc[trough_date]`。
       - 記錄 peak_date, trough_date, peak_price, trough_price, drawdown (取 min_dd 浮點數)。
4. 回傳格式:
   - 回傳包含上述 5 個欄位的 pandas DataFrame。

【嚴格禁令(違反即視為失敗)】:
- 嚴禁使用 `iterrows()`。
- 嚴禁使用 `drawdown.iloc[peak+1:].idxmin()` 這種可能切出空陣列的切片。
- 嚴禁在已經過濾門檻的資料集內部再去比對相反的條件。
- 不要輸出 LaTeX 數學符號,只回傳純 Python 代碼與一組可直接運行的測試範例。

得到如下 100% 正確的程式碼 : 

import pandas as pd

def detect_drawdown(prices, threshold=0.2):
    # 防禦性檢查
    if prices is None or len(prices) == 0:
        return pd.DataFrame(columns=['peak_date', 'trough_date', 'peak_price', 'trough_price',
'drawdown'])

    # 轉為 pd.Series,執行 dropna()
    prices = pd.Series(prices).dropna()

    # 向量化波段分組(Episode Segmentation)
    cummax = prices.cummax()
    drawdown = (prices - cummax) / cummax
    episodes = (prices == cummax).cumsum()

    # 波段聚合計算(GroupBy)
    results = []
    for name, group in prices.groupby(episodes):
        peak_date = group.index[0]
        peak_price = group.iloc[0]
        group_dd = drawdown[group.index]
        min_dd = group_dd.min()

        if min_dd <= -threshold:
            trough_date = group_dd.idxmin()
            trough_price = group.loc[trough_date]
            results.append([peak_date, trough_date, peak_price, trough_price, min_dd])

    # 回傳格式
    return pd.DataFrame(results, columns=['peak_date', 'trough_date', 'peak_price',
'trough_price', 'drawdown'])

# 測試範例
if __name__ == "__main__":
    prices = [100, 98, 95, 102, 101, 97, 96, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88,
87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64,
63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40,
39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16,
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
    df = detect_drawdown(prices, threshold=0.5)
    print(df)

這段程式碼完全正確, 不僅邏輯嚴謹且能直接通過標準執行與邊界檢驗. 相較於最初由它自由發揮時寫出的 idxmin() 空切片例外與崩潰, 這次 Qwen 2.5 Coder 14B 展現了身為頂級語法執行單元的完整水準. 

但這表示小白無法只靠簡單的任務提示完成專案, 因為撰寫這樣的提示詞需要對 Python 生態系有較深入的了解, 且須具備領域知識. 能寫出這種提示詞的人必然具備如下知識 :
  • 在金融領域上, 回撤波段是用創新高來定義邊界而非任意價格下跌.
  • 在 Pandas 的向量化操作中, is_new_high.cumsum() 是最優雅且無 Bug 的波段分組方式. 
  • idxmin() 在切片為空時會崩潰, Series 在 DatetimeIndex 下用整數下標查找會拋出 KeyError.
這表示本機小模型在自動化工作流中的最佳調用方式是不要讓它自己設計演算法架構, 而是由架構師定義好虛擬碼規格後交由它執行細節實現與語法生成, 這無疑是加重了開發者負擔. 

沒有留言 :