策略進階#

上一章介紹了策略的基本進出場指令。本章深入探討影響回測結果準確性的重要設定:手續費、滑點、部位大小,以及如何避免常見的回測陷阱。

手續費設定#

手續費是回測中不可忽略的成本。在 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_points 後移動停損啟動;停損線固定落後「進場後最高價」trail_offset 的距離,只升不降;價格回檔觸及停損線時出場。時間trail_pointstrail_offsettrail_offset進場後最高價移動停損線啟動價位進場價trail_price進場啟動出場
移動停損啟動後,停損線(紅)永遠等於「進場後最高價 − 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)

Reference#