這個周末花了大半時間對 Qwen 2.5 Coder, Gemma 4, Phi 4, 與 Deepseek Coder V2 這四個 14B 以下輕量模型進行進階推理與程式能力考驗, 本篇旨在總結測試結果, 並對如何馴服 14B 本地小模型, 用高約束提示詞來防止小模型生成有 Bug 的程式碼, 提出一個結合本地與雲端模型打造低成本的軟體專案開發方案.
測試結果摘要如下表 :
| 模型名稱 |
架構/參數量 |
邏輯推理能力 |
程式能力 (弱約束 Prompt) |
程式能力 (強約束 Prompt) |
| Qwen 2.5 Coder 14B |
稠密 14B |
❌ 陷入陷阱,硬湊 5 步三分法,最後產生幻覺 |
❌ 語法齊全但有 Runtime Bug(idxmin 空切片崩潰、索引混淆) |
🌟 完美(100 分) 精準落實向量化與防禦邊界,代碼零 Bug |
| Gemma 4 |
具備推論思維鏈 (Thinking)8B |
🌟 唯一識破悖論 以資訊論 3^3=27 < 200 精準證明數學不可能 |
⚠️ 思考死循環,Token 吃滿後無輸出中斷 |
✔️ 有效 但需強制壓制思考長度並調大 num_predict |
| Phi-4 14B |
稠密 14B (微軟合成資料) |
❌ 算術混亂(11 × 4 = 33),主觀假設輕重,次數超標 |
❌ 邏輯互斥死代碼(在 > 0.2 的子集內找 <= 0.2,輸出永為空) |
✔️ 可用 能被修正,但在極端邊界上仍偶有小瑕疵 |
| DeepSeek-Coder-V2-Lite |
混合專家 (MoE) 16B (活化約 2.4B) |
❌ 算術全面失真(憑空多出球),分支失控爆 Token 斷頭 |
❌ 偽造輸出結果,文案說明與實際執行邏輯 |
❌不推薦用於複雜邏輯活化參數量太小,注意力衰減嚴重,多輪約束下容易顧此失彼。 |
從測試結果來看, Qwen 2.5 Coder 14B 在使用本地模型進行軟體開發表現上拔得頭籌, 其次是 Phi 4, 但條件是開發者必須嫻熟 Python 技術棧與領域知識才有能力寫出具有強約束性的提示詞, 這表示想用本地 14B 模型進行 Vibe coding 幾乎是不可能.
解決辦法是先以聊天方式與具備足夠推理能力與領域常識的雲端模型 (Claude, ChatGPT, 與 Gemini 等) 討論, 以其為架構師讓它產出可被本機模型精確執行的規格說明書 (即高約術性 Spec Prompt), 然後再餵給本地 Qwen 2.5 Coder 14B 模型, 以零 API 成本產出 100% 正確的 Python 代碼. 這是最佳的降本增效策略, 因為如果把專案全丟給雲端 API (例如 Claude Code), 當專案一大, 上下文變長, 每次修改檔案都要反覆讀取整個專案, API 費用會以驚人的速度飆升!
然而採用先雲端聊架構, 再用本機吃規格的降本策略則能極度省 Token 成本, 我們不需要把整個專案的大量檔案丟給雲端, 只需要開一個網頁視窗, 使用免費額度的 ChatGPT/Gemini/Claude, 用 1 到 2 輪對話 (消耗不到 1000 個 tokens), 用純文字把需求講清楚, 它就會幫我們把狀態機數學公式與高約束規格推導出來, 把產出的高約束 Prompt 直接複製到本地命令列餵給 Ollama 的 Qwen 2.5 Coder 14B, 本機模型就能像吃了定心丸一樣, 在 RTX 3060 上以每秒 40 tokens 的極速, 零 API 成本產出 100% 正確的 Python 代碼.
以下是萬用架構師的提示詞模板 (元提示詞, meta-prompt) :
我是 Python 演算法開發者。我需要實作一個函式:
名稱:[填入單一函式名稱例如:atr_trailing_stop()]。
功能:[計算 ATR 移動停損與持倉狀態機]。
【嚴格限制】:僅限實作此單一函式,嚴禁設計多模式切換引擎。
請注意:現在先不要寫最終的 Python 實作代碼。
請扮演資深系統架構師,梳理邏輯,並輸出一份『專門餵給本機 14B 代碼模型(Qwen 2.5 Coder 14B)執行的極簡結構化 Prompt』。
為了防止本機小模型產生「陣列長度不匹配廣播崩潰」、「偽向量化偷換數學算式」、「時序穿透」或「遺漏輸入條件」,產出的 Prompt 必須嚴格遵守以下 5 項架構設計原則:
1. 運算範式與數值計算契約(Numerical & Alignment Guard):
- 【純無副作用】:函式開頭必須強制執行 `df = df.copy()`,嚴禁修改傳入的原始物件;所有計算出的核心特徵與狀態欄位必須完整回填輸出。
- 【時序差分對齊】:凡涉及前一期資訊(t-1 lag)的向量化運算,必須明確給出切片對齊語法(當期切片 [1:] 與前期切片 [:-1] 長度對齊,第 0 筆獨立初始化),嚴禁不同長度陣列直接相減引發廣播崩潰。
- 【拒絕偽向量化】:明確區分有限滑動視窗與遞迴平滑(如 EMA、Wilder 等依賴前一期數值之濾波)。凡具備遞迴相依特徵,指示直接以純數值迴圈計算,嚴禁用 np.convolve 或滑動平均冒充。
2. 迴圈拓撲與單一 Pass 限制(Single-Pass FSM Topology):
- 明確區分「可全域預先向量化特徵」與「路徑依賴狀態機」。
- 【單一迴圈紅線】:若存在狀態轉移依賴,所有相互關聯的事件、進出場判定與動態軌跡更新,必須嚴格封裝在唯一一個 `for t in range(n)` 迴圈內,嚴禁拆成多個獨立迴圈預算事件。
3. 依賴閉環與變數綁定(Dependency Closure & Memory Scaffold):
- 【依賴閉環原則】:輸入 Schema 中定義的所有決策驅動欄位,在迴圈前必須轉為 1D NumPy 陣列;且必須在狀態機迴圈的 `if` 條件分支中被顯式引用,嚴禁輸入欄位被閒置或遺忘。
- 所有輸出欄位,在進入迴圈前於記憶體預先宣告配置好 1D NumPy 陣列(禁止在迴圈內對 DataFrame 逐列賦值)。
4. 具體化狀態轉移骨架(Explicit FSM Skeleton):
- 必須將迴圈內的條件分支寫出「具體布林運算式代碼骨架」,禁止使用抽象註解(例如必須寫出明確的 `if state == A and condition_var[t]:`)。
- 狀態流動順序必須保證「先讀取/凍結前一期已確認狀態進行離場或決策 → 再處理進場/重設 → 再更新累積極值與動態邊界 → 寫入當期陣列」,杜絕未來函數。
5. 極限邊界防禦與 Negative Constraints:
- 空資料、長度不足時安全回傳帶有完整目標 Schema 的空 DataFrame。
- 數據中斷(NaN/None)時明確規定狀態凍結或沿用規則,不可使用全域 fillna(0) 破壞缺失值語意。
- 嚴格禁止使用 iterrows、itertuples、未定義欄位預先存取。
- 產出的 Prompt 篇幅控制在 600~800 字內,要求小模型輸出完整代碼與 pytest 驗證案例。
這是我與 Gemini 來回討論, 經多次實測才定稿的版本, 解決了輕量模型在語法執行, 時序拓撲, 業務邏輯, 演算法數學以及 Prompt 交互五大層面的關鍵問題, 包含 :
- 執行期致命錯誤 :
解決變數未宣告先引用引發的 KeyError; 解決 NumPy 維度廣播不匹配 (Shape Mismatch); 以及位元運算子優先權錯誤. - 效能與架構拓撲問題 :
強制要求無狀態特徵全域向量化與狀態機 1D NumPy 陣列迭代, 根除記憶體爆炸; 打破既要向量化又要路徑依賴的邏輯矛盾; 消除多迴圈拆分 (Multiple Passes) 造成的死鎖. - 時序因果律問題
根除未來函數穿透 (Look-ahead Bias), 確立依賴閉環原則避免幻覺. - 數值演算法與代碼潔淨度
防範小模型的算法偷換, 防止竄改外部原始資料. - Meta-Prompt 本身的工程收斂 :
擺脫冗長規格書, 排除 LaTeX 符號干擾, 嚴格禁止多模式分發器, 保證單一任務代碼行數在 100 行以內.
此模板的演進主要目的是要將軟體架構決策前置化, 將小模型死穴在元提示詞層面完全阻斷, 讓本地 14B 模型不再需要承擔架構推理的負擔, 真正發揮其作為 "毫秒級高效語法轉譯機" 的最佳價值. 注意, 通用模板的本質是做為 "架構約束器", 它能保證代碼不崩潰, 不穿透時序, 不拆多迴圈, 但任務交代 (即模板中的功能欄) 時必須提供充分的資訊 (例如輸入長怎樣等), 這樣生成的高約束 prompt 才會完整, 不致於漏失重要環節.
不管是 Claude, ChatGPT, 還是 Gemini 的網頁版或 App 都有免費額度可用 (通常免費帳戶有五小時重置機制), 把這三家任何一家的架構產出拿來當輸入, 再餵給本地的 Qwen 2.5 Coder 14B, 它立刻就從容易寫出 Bug 的菜鳥搖身一變成為產能極高且代碼精準的資深打字員啦!
不過這三家雲端模型強項不同 :
- Claude (Sonnet) :
架構感與工程美感最強, 最擅長找出向量化的巧妙寫法 (例如自動聯想到用布林 cumsum 做波段切片), 禁令清單下得最精準. - ChatGPT (o1/o3-mini/GPT-4o) :
邊界條件 (Edge Cases) 抓得最滴水不漏, 會主動考慮輸入是 None, 只有 1 筆資料, 全平盤等各種極端情境. - Gemini (Pro / Thinking) :
數學理論與離散邏輯推導非常嚴謹, 解釋狀態轉移方程時條理最清晰.
其中 Gemini 免費額度最大, 各維度比較如下表 :
| 評比維度 |
Google Gemini |
OpenAI ChatGPT |
Anthropic Claude |
| 旗艦模型可用性 |
極高 開放主流長文本與思考模型(Flash / Pro 輪替),無嚴格斷崖限制。 |
中等 有限次數體驗 GPT-4o,用畢降級至 mini;思考模型(o1/o3)額度極少。 |
極低 雖可選 Sonnet,但免費對話數輪後極易觸發冷卻鎖定。 |
| 對話輪數與冷卻 |
最慷慨 一般文字對話極少撞牆,可保持長時間多輪連續互動。 |
中規中矩 高階模型約 3~5 小時給予 10~15 則額度,降級後 mini 不限次數。 |
最嚴格 連續 5~10 輪或傳送長代碼即可能需等待數小時。 |
| 上下文長度 (Context) |
極長 免費即支援百萬級長文本,可直接整本讀取大文件或大量程式碼。 |
普通 支援標準對話記憶長度,過長資料需分批或分段輸入。 |
嚴格受限 模型雖支援長文本,但免費版傳大檔案會迅速耗盡配額。 |
| 檔案上傳與程式執行 |
完整開放 支援各類文件上傳,內建 Python 沙盒可即時分析資料與產圖。 |
完整開放 支援進階資料分析(Code Interpreter),但計入高階配額。 |
代價較高 支援文件/代碼上傳解析,但無沙盒執行環境且劇烈消耗額度。 |
| AI 圖片生成 |
免費無上限(極寬鬆) 內建 Imagen 模型,生成速度快且無嚴苛次數限制。 |
受限 內建 DALL-E 3,免費帳號每日限制生圖約 2~3 張。 |
無 原生介面完全不支援圖像生成功能。 |
| 聯網即時搜尋 |
原生深度整合 即時串接 Google 搜尋引導,取得時效資訊最敏捷。 |
具備 支援外網 Web Search,搜尋品質與整合度穩定。 |
無 免費 Web 版未整合即時外網搜尋,依賴靜態訓練切斷點。 |
| 工具擴充與生態 |
原生串接 Google Workspace(Gmail、Drive、Docs、Maps)。 |
GPT Store 第三方應用生態豐富,可自選各類專家工具。 |
支援 Artifacts 預覽,但協同 Projects 多鎖定於付費方案。 |
以下測試是我將通用模板中的佔位符填上具體函式名稱與功能描述後的提示詞 (功能描述要盡量具體充分) :
我是 Python 演算法開發者。我需要實作一個函式:
名稱:atr_trailing_stop(df, entry_col="entry_signal", period=14, multiplier=3.0)。
功能:由外部布林訊號 entry_col 觸發進場,並以 ATR 進行後續移動停損追蹤。。
【嚴格限制】:僅限實作此單一函式,嚴禁設計多模式切換引擎。
請注意:現在先不要寫最終的 Python 實作代碼。
請扮演資深系統架構師,梳理邏輯,並輸出一份『專門餵給本機 14B 代碼模型(Qwen 2.5 Coder 14B)執行的極簡結構化 Prompt』。
為了防止本機小模型產生「陣列長度不匹配廣播崩潰」、「偽向量化偷換數學算式」、「時序穿透」或「遺漏輸入條件」,產出的 Prompt 必須嚴格遵守以下 5 項架構設計原則:
1. 運算範式與數值計算契約(Numerical & Alignment Guard):
- 【純無副作用】:函式開頭必須強制執行 `df = df.copy()`,嚴禁修改傳入的原始物件;所有計算出的核心特徵與狀態欄位必須完整回填輸出。
- 【時序差分對齊】:凡涉及前一期資訊(t-1 lag)的向量化運算,必須明確給出切片對齊語法(當期切片 [1:] 與前期切片 [:-1] 長度對齊,第 0 筆獨立初始化),嚴禁不同長度陣列直接相減引發廣播崩潰。
- 【拒絕偽向量化】:明確區分有限滑動視窗與遞迴平滑(如 EMA、Wilder 等依賴前一期數值之濾波)。凡具備遞迴相依特徵,指示直接以純數值迴圈計算,嚴禁用 np.convolve 或滑動平均冒充。
- 【0-based 視窗邊界對齊】:凡涉及長度為 $W$ 之視窗統計或初始暖機計算,首個有效聚合值必須精確對齊至視窗末端索引 $W - 1$,後續時序推進嚴格自索引 $W$ 起算,嚴禁因 0-based 索引混淆導致邊界偏移(Off-by-one)。
2. 迴圈拓撲與單一 Pass 限制(Single-Pass FSM Topology):
- 明確區分「可全域預先向量化特徵」與「路徑依賴狀態機」。
- 【單一迴圈紅線】:若存在狀態轉移依賴,所有相互關聯的事件、進出場判定與動態軌跡更新,必須嚴格封裝在唯一一個 `for t in range(n)` 迴圈內,嚴禁拆成多個獨立迴圈預算事件。
3. 依賴閉環與變數綁定(Dependency Closure & Memory Scaffold):
- 【依賴閉環原則】:輸入 Schema 中定義的所有決策驅動欄位,在迴圈前必須轉為 1D NumPy 陣列;且必須在狀態機迴圈的 `if` 條件分支中被顯式引用,嚴禁輸入欄位被閒置或遺忘。
- 所有輸出欄位,在進入迴圈前於記憶體預先宣告配置好 1D NumPy 陣列(禁止在迴圈內對 DataFrame 逐列賦值)。
4. 具體化狀態轉移骨架(Explicit FSM Skeleton):
- 必須將迴圈內的條件分支寫出「具體布林運算式代碼骨架」,禁止使用抽象註解(例如必須寫出明確的 `if state == A and condition_var[t]:`)。
- 狀態流動順序必須保證「先讀取/凍結前一期已確認狀態進行離場或決策 → 再處理進場/重設 → 再更新累積極值與動態邊界 → 寫入當期陣列」,杜絕未來函數。
5. 極限邊界防禦與 Negative Constraints:
- 空資料、長度不足時安全回傳帶有完整目標 Schema 的空 DataFrame。
- 數據中斷(NaN/None)時明確規定狀態凍結或沿用規則,不可使用全域 fillna(0) 破壞缺失值語意。
- 嚴格禁止使用 iterrows、itertuples、未定義欄位預先存取。
- 產出的 Prompt 篇幅控制在 600~800 字內,要求小模型輸出完整代碼與 pytest 驗證案例。
將此提示詞貼給 ChatGPT 後生成如下的高約束提示詞 :
# Role
你是資深 Python 量化演算法架構師。僅實作 `atr_trailing_stop(df, period=14, multiplier=3.0)`,功能為 ATR 移動停損。禁止多模式引擎、無關功能或只提供偽代碼。最終輸出完整程式碼及 pytest 測試。
## 1. 數值與對齊契約
- 函式第一行必須執行 `df = df.copy()`,不得修改原始 DataFrame。
- 必要輸入為 `High`、`Low`、`Close`;驗證欄位存在、數值可轉換及參數有效。輸出保留原欄位,新增 `TR`、`ATR`、`Peak`、`Stop`、`Position`、`Entry_Event`、`Exit_Event`。
- 所有輸入先轉成長度一致的 1D NumPy 陣列。前一期差分明確使用 `delta[1:] = x[1:] - x[:-1]`,第 0 筆獨立初始化,禁止不同長度陣列直接運算。
- `TR = max(High-Low, abs(High-PrevClose), abs(Low-PrevClose))`;首筆 `TR = High-Low`。ATR 使用 Wilder 平滑:首個有效 ATR 為前 `period` 筆有效 TR 的算術平均,其後 `ATR[t] = (ATR[t-1]*(period-1)+TR[t])/period`。遞迴計算使用數值迴圈,禁止以 `np.convolve` 或簡單移動平均替代。
## 2. 單一狀態機迴圈
TR 可向量化;ATR 遞迴初始化與平滑可在狀態機前計算。所有進出場、累積最高價、移動停損及持倉更新必須集中於唯一一個 `for t in range(n)` 狀態機迴圈,不得另設事件預算迴圈。
## 3. 依賴閉環與預配置
迴圈前將 `high`、`close`、`atr` 轉成 1D NumPy 陣列;預先配置全部輸出陣列。每個決策輸入必須實際參與對應條件或公式。不得在迴圈內逐列修改 DataFrame。
## 4. 明確 FSM 骨架
採用 `Position=0` 空倉、`Position=1` 持倉;以 `Close` 確認停損,避免盤中資料與收盤判定混用。固定流程如下:
```python
for t in range(n):
prev_position = position
prev_peak = peak
prev_stop = stop
current_atr = atr[t]
if prev_position == 1 and np.isfinite(close[t]) and np.isfinite(prev_stop) and close[t] <= prev_stop:
position, peak, stop = 0, np.nan, np.nan
exit_event[t] = True
elif prev_position == 0 and np.isfinite(current_atr) and current_atr >= 0 and np.isfinite(close[t]):
position = 1
peak = high[t]
stop = close[t] - multiplier * current_atr
entry_event[t] = True
elif prev_position == 1:
# 僅在輸入有效時更新 peak 與 stop
# 新 stop 不得回頭觸發同一期 exit
pass
# 將當期狀態寫入預配置陣列
```
補齊第三分支:有效 `High` 更新累積最高價;有效 ATR 更新 `stop = max(prev_stop, peak - multiplier * current_atr)`,不得放寬停損。缺失資料不得觸發進出場;依欄位規則凍結狀態及沿用前值。離場當期不重新進場。
## 5. 邊界與測試
空資料或不足以形成首個 ATR 的資料,回傳保留完整目標 Schema 的 DataFrame,不得索引越界。禁止全域 `fillna(0)`、`iterrows`、`itertuples`、未定義變數、未驗證欄位存取及未來資料。輸出完整可執行函式與 pytest 案例,涵蓋 ATR 數值、切片對齊、空/短資料、NaN、停損單調性、進出場、同一期不重複進出場及原始資料不變。
然後將此高約束提示詞貼給 Qwen 2.5 Coder 去生成程式碼 :
C:\Users\tony1>ollama run qwen2.5-coder:14b
>>> Role:
... 你是資深 Python 量化演算法架構師。僅實作單一專用函式 `atr_trailing_stop(df, entry_col="entry_sign
... al", period=14, multiplier=3.0)`,功能為「由外部進場訊號觸發部位,並以 ATR 進行動態移動停損追蹤」
... 。禁止多模式引擎、禁止無關策略,禁止只提供偽代碼。最終輸出完整可執行程式碼及 pytest 測試。
...
... 1. 數值與對齊契約
... - 函式第一行必須執行 `df = df.copy()`,嚴禁修改傳入的原始 DataFrame。
... - 必要輸入為:`High`、`Low`、`Close` 與指定的 `entry_col`。驗證欄位存在(缺失時拋出 KeyError)、
... 數值可轉換及參數合法性。輸出保留原欄位,新增 TR、ATR、Peak、Stop、Position、Entry_Event、Exit_Eve
... nt。
... - 所有輸入先轉成長度一致的 1D NumPy 陣列,`entry = df[entry_col].fillna(0).astype(bool).values`。
... 前一期差分明確使用 `delta[1:] = x[1:] - x[:-1]`,第 0 筆獨立初始化,禁止不同長度陣列直接運算以防
... 維度廣播崩潰。
... - TR 計算:首筆 `TR[0] = High[0] - Low[0]`;後續切片向量化 `TR[1:] = np.maximum(High[1:] - Low[1:
... ], np.maximum(np.abs(High[1:] - Close[:-1]), np.abs(Low[1:] - Close[:-1])))`。
... - ATR 使用 Wilder 平滑:首個有效 ATR 為前 period 筆有效 TR 的算術平均,其後依遞迴式 `ATR[t] = (AT
... R[t-1] * (period - 1) + TR[t]) / period` 以純數值迴圈計算,嚴禁以 np.convolve 或簡單移動平均替代
... 。TR 為 NaN 時 ATR 傳播為 NaN。
...
... 2. 單一狀態機迴圈
... - TR 與 ATR 在進入狀態機前計算完畢。
... - 所有進出場判定、累積最高價、移動停損調升及持倉更新,必須集中於唯一一個 `for t in range(n)` 狀態
... 機迴圈內完成,嚴禁另設迴圈預算事件。
...
... 3. 依賴閉環與預配置
... - 迴圈前預先配置好全部 1D 輸出陣列:`position` (np.int8)、`stop` (float, np.nan)、`peak` (float,
... np.nan)、`entry_event` (bool)、`exit_event` (bool)。
... - 依賴閉環:外部進場訊號 `entry[t]` 必須在進場條件分支中顯式引用。禁止在迴圈內逐列修改 DataFrame
... 。
...
... 4. 明確 FSM 骨架(嚴格落實單向因果流)
... position = 0
... peak = np.nan
... stop = np.nan
...
... for t in range(n):
... prev_position = position
... prev_peak = peak
... prev_stop = stop
... current_atr = atr[t]
...
... # A. 使用前一期已確認停損判斷離場(防未來函數)
... if prev_position == 1 and np.isfinite(close[t]) and np.isfinite(prev_stop) and close[t] <= pr
... ev_stop:
... position = 0
... peak = np.nan
... stop = np.nan
... exit_event[t] = True
... continue # 離場當期不重新進場
...
... # B. 依賴閉環:僅在空倉且收到外部進場訊號時進場
... elif prev_position == 0 and entry[t] and np.isfinite(current_atr) and current_atr > 0 and np.
... isfinite(close[t]):
... position = 1
... peak = high[t]
... stop = close[t] - multiplier * current_atr
... entry_event[t] = True
...
... # C. 延續持倉:更新累積最高價與單向調升停損(只升不降)
... elif prev_position == 1:
... if np.isfinite(high[t]):
... peak = max(prev_peak, high[t]) if np.isfinite(prev_peak) else high[t]
... if np.isfinite(current_atr) and np.isfinite(high[t]):
... candidate_stop = peak - multiplier * current_atr
... stop = max(prev_stop, candidate_stop) if np.isfinite(prev_stop) else candidate_stop
...
... # D. 將當期狀態寫入預配置陣列
... position_arr[t] = position
... peak_arr[t] = peak
... stop_arr[t] = stop
...
... 5. 邊界與測試
... - 空 DataFrame 或長度小於 period 時,安全回傳包含原欄位與所有新增 Schema 欄位的空 DataFrame。
... - 嚴禁全域 fillna(0)、iterrows、itertuples、未定義變數引用。
... - 輸出完整可執行程式碼與 pytest 測試案例,必須驗證:
... 1. 外部進場訊號延遲觸發(例如前 5 天無訊號,第 6 天才進場)時不會提早偷跑。
... 2. 停損價單調不減特性(Monotonicity)。
... 3. 切片差分維度正確無廣播報錯。
... 4. 傳入之原始 DataFrame 未被修改(df 不可變性)。
Qwen 生成如下程式碼 :
```python
import pandas as pd
import numpy as np
def atr_trailing_stop(df, entry_col="entry_signal", period=14, multiplier=3.0):
# Ensure the input DataFrame is not modified
df = df.copy()
# Validate input columns
required_columns = ['High', 'Low', 'Close', entry_col]
for col in required_columns:
if col not in df.columns:
raise KeyError(f"Column '{col}' is missing from the DataFrame.")
# Convert input to NumPy arrays
high = df['High'].values
low = df['Low'].values
close = df['Close'].values
entry = df[entry_col].fillna(0).astype(bool).values
# Calculate TR
n = len(df)
if n < period:
return df.assign(TR=np.nan, ATR=np.nan, Peak=np.nan, Stop=np.nan, Position=0,
Entry_Event=False, Exit_Event=False)
tr = np.empty(n)
tr[0] = high[0] - low[0]
tr[1:] = np.maximum(high[1:] - low[1:], np.maximum(np.abs(high[1:] - close[:-1]),
np.abs(low[1:] - close[:-1])))
# Calculate ATR
atr = np.empty(n)
atr[:period] = np.nan
atr[period] = np.mean(tr[:period])
for t in range(period + 1, n):
atr[t] = (atr[t - 1] * (period - 1) + tr[t]) / period
# Initialize state variables
position = 0
peak = np.nan
stop = np.nan
# Preconfigure output arrays
position_arr = np.zeros(n, dtype=np.int8)
peak_arr = np.full(n, np.nan, dtype=float)
stop_arr = np.full(n, np.nan, dtype=float)
entry_event = np.zeros(n, dtype=bool)
exit_event = np.zeros(n, dtype=bool)
# State machine loop
for t in range(n):
prev_position = position
prev_peak = peak
prev_stop = stop
current_atr = atr[t]
# A. Exit if previous position was 1 and current price is below stop
if prev_position == 1 and np.isfinite(close[t]) and np.isfinite(prev_stop) and close[t]
<= prev_stop:
position = 0
peak = np.nan
stop = np.nan
exit_event[t] = True
continue
# B. Enter if position is 0 and entry signal is True
elif prev_position == 0 and entry[t] and np.isfinite(current_atr) and current_atr > 0
and np.isfinite(close[t]):
position = 1
peak = high[t]
stop = close[t] - multiplier * current_atr
entry_event[t] = True
# C. Continue holding and update peak and stop
elif prev_position == 1:
if np.isfinite(high[t]):
peak = max(prev_peak, high[t]) if np.isfinite(prev_peak) else high[t]
if np.isfinite(current_atr) and np.isfinite(high[t]):
candidate_stop = peak - multiplier * current_atr
stop = max(prev_stop, candidate_stop) if np.isfinite(prev_stop) else
candidate_stop
# D. Write current state to arrays
position_arr[t] = position
peak_arr[t] = peak
stop_arr[t] = stop
# Assign results to DataFrame
df = df.assign(TR=tr, ATR=atr, Peak=peak_arr, Stop=stop_arr, Position=position_arr,
Entry_Event=entry_event, Exit_Event=exit_event)
return df
# pytest test case
def test_atr_trailing_stop():
# Create a test DataFrame
data = {
'High': [100, 102, 101, 103, 104, 105, 106, 107, 108, 109],
'Low': [98, 99, 97, 96, 95, 94, 93, 92, 91, 90],
'Close': [99, 100, 98, 97, 96, 95, 94, 93, 92, 91],
'entry_signal': [0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
}
df = pd.DataFrame(data)
# Run the function
result = atr_trailing_stop(df, entry_col="entry_signal", period=2, multiplier=2)
# Expected output
expected_position = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]
expected_stop_price = [np.nan, np.nan, np.nan, np.nan, np.nan, 97.0, 97.0, 97.0, 97.0,
97.0]
expected_highest_high = [np.nan, np.nan, np.nan, np.nan, np.nan, 101.0, 101.0, 101.0,
101.0, 101.0]
expected_entry_event = [False, False, False, False, False, True, False, False, False,
False]
expected_exit_event = [False, False, False, False, False, False, False, False, False,
False]
assert np.array_equal(result['Position'], expected_position)
assert np.allclose(result['Stop'], expected_stop_price, equal_nan=True)
assert np.allclose(result['Peak'], expected_highest_high, equal_nan=True)
assert np.array_equal(result['Entry_Event'], expected_entry_event)
assert np.array_equal(result['Exit_Event'], expected_exit_event)
# Check that the original DataFrame is not modified
assert 'TR' not in df.columns
assert 'ATR' not in df.columns
assert 'Peak' not in df.columns
assert 'Stop' not in df.columns
assert 'Position' not in df.columns
assert 'Entry_Event' not in df.columns
assert 'Exit_Event' not in df.columns
print("All tests passed.")
# Run the test
test_atr_trailing_stop()
```
這段程式碼經 Gemini 審查, 結果它認為這份由 Qwen 2.5 Coder 14B 生成的程式碼整體架構水準極高, 先前設定的數值契約, 拓撲約束與依賴閉環已經全數生效 :
- 依賴閉環完全成功 :
entry[t] 順利進入 Step B 判斷, 徹底解決了天天自動進場的幻覺. - 無副作用契約成立 :
開頭執行 df = df.copy() 且測試案例最後檢查 assert 'TR' not in df.columns, 呼叫端的原始物件完全未被污染. - 時序差分維度完全對齊 :
tr[1:] 正確對齊 close[:-1], 徹底消除了廣播形狀不匹配錯誤. - 記憶體預配置到位 :
狀態機內部無慢速 DataFrame 賦值, 純陣列操作效能極佳.
雖然程式碼可以順利執行, 但有兩個指標算法與下標的邊界細節需要微調 :
這些小問題完全是 Qwen 2.5 Coder 14B 模型本身的典型弱點造成的, Prompt 本身已經寫得非常清楚, 14B 小模型在將前 N 筆 (長度為 N) 轉譯成 Python 0-based 下標時, 觸發了經典的 Off-by-one 偏差, 忽略了長度為 14 的陣列最後一個元素索引其實是 13.
Qwen 知道切片 tr[:period] 代表取前 14 筆 (索引 0 到 13), 但在為結果賦值時沒有意識到這個平均值應該填在切片的最後一個位置 (period - 1), 而是順手填進了緊接著切片結尾的下一個位置 period. 這屬於輕量代碼模型在處理邊界索引時最常見的小瑕疵, 甚至很多資深工程師手寫時也常在此處不小心多算 1 筆. 這個小問題並不是模板或提示詞的瑕疵, 而是 14B 等級輕量模型的客觀物理邊界. 為了避免此問題, 我已在通用模板的第一項架構設計規範 "運算範式與數值計算契約" 中添加 "0-based 視窗邊界對齊" 約束.
整體來說, 透過通用模板 Meta-Prompt 的高約束, Qwen 已經能生成幾乎 100% 正確的程式碼, 不再有任何大方向的架構漏洞.