策略進階#
上一章介紹了策略的基本進出場指令。本章深入探討影響回測結果準確性的重要設定:手續費、滑點、部位大小,以及如何避免常見的回測陷阱。
手續費設定#
手續費是回測中不可忽略的成本。在 strategy() 函式中設定:
//@version=6
strategy("含手續費策略",
overlay=true,
initial_capital=100000,
commission_type=strategy.commission.percent, // 百分比手續費
commission_value=0.1425) // 0.1425%(台股買進費率)commission_type 選項:
| 常數 | 說明 |
|---|---|
strategy.commission.percent | 按成交金額的百分比收取 |
strategy.commission.cash_per_contract | 每口合約收取固定費用 |
strategy.commission.cash_per_order | 每筆訂單收取固定費用 |
台股參考費率:買進 0.1425%、賣出 0.1425% 手續費 + 0.3% 證交稅,合計單邊約 0.4425%,來回約 0.585%。
滑點設定#
滑點 (Slippage) 模擬實際成交價與訊號價格的差異:
strategy("含滑點策略",
overlay=true,
slippage=2) // 每次成交滑動 2 個最小變動單位部位大小#
固定數量#
strategy("固定口數",
default_qty_type=strategy.fixed,
default_qty_value=1) // 每次固定交易 1 口/股固定金額#
strategy("固定金額",
default_qty_type=strategy.cash,
default_qty_value=10000) // 每次投入 10,000 元資金比例#
strategy("資金比例",
default_qty_type=strategy.percent_of_equity,
default_qty_value=10) // 每次投入總資金的 10%動態計算口數#
在 strategy.entry 中直接指定 qty,可根據當下資金動態計算:
//@version=6
strategy("動態口數", overlay=true, initial_capital=100000)
riskPct = input.float(2.0, "每筆風險(%)") / 100
atrMult = input.float(2.0, "停損ATR倍數")
atrValue = ta.atr(14)
stopAmount = atrValue * atrMult // 每股停損金額
riskAmount = strategy.equity * riskPct // 本次可承受虧損
qty = math.floor(riskAmount / stopAmount) // 計算口數
ma20 = ta.sma(close, 20)
if ta.crossover(close, ma20) and strategy.position_size == 0
strategy.entry("Long", strategy.long, qty=qty)
strategy.exit("Exit", "Long", stop=close - stopAmount)移動停損(Trailing Stop)#
上面的範例用的是固定停損:進場後停損價就不再變動。固定停損能限制虧損,卻無法在行情走順時鎖住已經到手的獲利。strategy.exit 內建的移動停損會隨價格往有利方向推進而自動上移(多單)或下移(空單),且只進不退。
一組移動停損由 啟動條件 與 跟隨距離 組成,兩者缺一不可:
| 參數 | 角色 | 單位 |
|---|---|---|
trail_price | 啟動條件:價格觸及指定價位時啟動 | 價格 |
trail_points | 啟動條件:從進場價起算,獲利達到指定距離時啟動 | 跳動點 (tick) |
trail_offset | 跟隨距離:停損掛在最佳價後方多遠 | 跳動點 (tick) |
trail_price 與 trail_points 擇一即可;兩者都給時,以較早觸發的那個為準。
三者的關係用一張圖最清楚——多單為例:
trail_offset」,直到價格回檔碰到停損線才出場。圖中三件事值得注意:停損線在啟動前並不存在;啟動後價格每創新高它就同步上移,價格回檔時則停在原處不動;最後出場價必然低於最高價,差距就是 trail_offset。
基本用法#
//@version=6
strategy("移動停損", overlay=true)
ma20 = ta.sma(close, 20)
if ta.crossover(close, ma20) and strategy.position_size == 0
strategy.entry("Long", strategy.long)
// 獲利達 100 跳動點後啟動,停損跟在最高價後方 50 跳動點
strategy.exit("Exit", "Long", trail_points=100, trail_offset=50)多單啟動後,停損價會跟著「進場後出現過的最高價」往上調整;一旦價格從最高點回檔超過 trail_offset,就以停損單出場。空單方向相反,跟的是最低價。
兩個參數的相對大小決定會不會賺:剛啟動時的停損價是「進場價 +
trail_points−trail_offset」。上例 100 > 50,停損一啟動就落在進場價上方,之後觸發必定獲利;但若把trail_offset設得比trail_points大,啟動時的停損反而在進場價下方,觸發時是虧損出場(尚未計入手續費與滑點)。
常見錯誤:只給 trail_offset#
trail_offset 只定義「跟多遠」,沒有定義「何時開始跟」。單獨使用不會產生任何移動停損:
// ❌ 缺少啟動條件,移動停損不會生效
strategy.exit("Exit", "Long", trail_offset=50)
// ✅ 補上 trail_points(或 trail_price)
strategy.exit("Exit", "Long", trail_points=100, trail_offset=50)單位陷阱:跳動點不是價格#
trail_points 和 trail_offset 的單位都是 跳動點,不是價格金額。把 ATR 這類「價格距離」直接丟進去,實際的停損距離會與預期差上好幾個數量級——必須先除以 syminfo.mintick 換算:
//@version=6
strategy("ATR 移動停損", overlay=true)
atrValue = ta.atr(14)
trailDist = atrValue * 2 // 價格距離
trailTick = trailDist / syminfo.mintick // 換算成跳動點
if ta.crossover(close, ta.sma(close, 20)) and strategy.position_size == 0
strategy.entry("Long", strategy.long)
strategy.exit("Exit", "Long",
trail_points = trailTick,
trail_offset = trailTick)若想直接用價格思考啟動時機,可改用
trail_price(例如strategy.position_avg_price * 1.02),但trail_offset仍然只接受跳動點。
pyramiding — 加碼#
pyramiding 參數設定同方向最多可以有幾筆未平倉單:
strategy("加碼策略", overlay=true, pyramiding=3) // 最多同時 3 筆多單
// 每次拉回到均線附近都加碼
ma = ta.sma(close, 20)
if close > ma and close < ma * 1.02
strategy.entry("Long", strategy.long, qty=1)避免回測陷阱#
1. 未來資料 (Lookahead Bias)#
Pine Script 的 strategy.entry 預設 在訊號K棒收盤後的下一根K棒開盤成交,這是正確的行為。
但某些寫法會造成「偷看未來」:
// ❌ 錯誤:用當根 K 棒的高點做為進場價(該價格在訊號產生前就存在)
if close > ma
strategy.entry("Long", strategy.long, limit=high)
// ✅ 正確:下一根 K 棒的開盤成交(預設行為)
if close > ma
strategy.entry("Long", strategy.long)2. 過度優化 (Overfitting)#
避免在同一段歷史數據上反覆調整參數,建議:
- 留出「樣本外」測試區間(例如最近 20% 的數據)
- 參數越少越好,越簡單的策略越穩健
3. barmerge.lookahead 設定#
使用 request.security 取得其他週期數據時(詳見 CH15),必須使用 barmerge.lookahead_off 避免偷看未來 K 棒的收盤價。
常用的 strategy.* 查詢變數#
| 變數 | 說明 |
|---|---|
strategy.equity | 當前總資產(含未實現損益) |
strategy.netprofit | 已實現淨損益 |
strategy.position_size | 當前持倉數量(正多負空) |
strategy.position_avg_price | 持倉平均成本 |
strategy.opentrades | 當前未平倉交易筆數 |
strategy.closedtrades | 已完成交易總筆數 |
strategy.wintrades | 獲利交易筆數 |
strategy.losstrades | 虧損交易筆數 |
實用範例:RSI 策略含完整風控#
//@version=6
strategy("RSI 策略", overlay=false,
initial_capital=100000,
default_qty_type=strategy.percent_of_equity,
default_qty_value=10,
commission_type=strategy.commission.percent,
commission_value=0.1425)
// 參數
rsiLen = input.int(14, "RSI 週期")
oversold = input.int(30, "超賣門檻")
overbought = input.int(70, "超買門檻")
stopPct = input.float(3.0, "停損(%)", step=0.5) / 100
targetPct = input.float(6.0, "停利(%)", step=0.5) / 100
rsiValue = ta.rsi(close, rsiLen)
// 進場:RSI 從超賣回升
buySignal = ta.crossover(rsiValue, oversold)
sellSignal = ta.crossunder(rsiValue, overbought)
if buySignal
strategy.entry("Long", strategy.long)
if strategy.position_size > 0
entryPx = strategy.position_avg_price
strategy.exit("Exit Long", "Long",
stop=entryPx * (1 - stopPct),
limit=entryPx * (1 + targetPct))
// 繪圖
plot(rsiValue, "RSI", color=color.purple)
hline(overbought, color=color.red, linestyle=hline.style_dashed)
hline(oversold, color=color.green, linestyle=hline.style_dashed)
hline(50, color=color.gray, linestyle=hline.style_dotted)