跳轉到

回測:委託、成交價與風控

所屬章節:回測 成交價規則表 12 個案例全部以合成資料實測驗證(2026-08-21)

所有下單都經由 self.Condition。本頁說明怎麼描述一張委託、五種委託型別的差異、 各型別在跳空與盤中觸價時的實際成交價,以及四個停損停利方法。

Condition.Global

self.Condition.Global(triggered, add_qty, trade_price, trade_type,
                      cancel_triggered, rule, match_uid='')

描述「什麼條件下送出什麼單」。掛單、更新、取消與撮合都由引擎處理。

參數 說明
triggered 觸發條件。True 時送出/更新委託
add_qty 數量。正=買進/建多,負=賣出/建空。小於 1 的小數代表「佔資金池的比例」,實際口數由 config.lot_size 換算
trade_price 委託價。0 代表市價
trade_type 委託型別,見下節
cancel_triggered True 時取消此委託(依 rule / match_uid 定位)。不需要時傳 None 或 False
rule 規則名稱。用於識別委託、對應 Position.rule,也是 set_STL 等出場條件的比對依據
match_uid 要平掉哪一筆持倉的 uid。預設 '' 視為新倉,有部位時依序沖銷

同一組 (rule, match_uid) 重複呼叫是「更新既有委託」,不是再送一張新單。 要同時掛兩張不同的單,必須用不同的 rule。

# 進場:空手時送市價買單
self.Condition.Global(self.global_qty == 0, 1, 0,
                      self.PriceType.market_ioc, None, 'bk')

# 反手:多單訊號成立時一次補齊到 +qty,並以空單訊號作為取消條件
self.Condition.Global(long and self.global_qty < 1,
                      -self.global_qty + qty, 0,
                      self.PriceType.market_ioc, short, 'long')

綁定特定持倉出場

多筆持倉時,出場單不帶 match_uid 會依序沖銷,容易互相干擾。 要每筆獨立出場就在 on_position 用 P.uid 綁:

def on_position(self, P):
    if P.rule == 'bk':
        self.Condition.Global(True, -1, P.entry_price + 5,
                              self.PriceType.limit_rod, None, 'exit', P.uid)

PriceType — 五種委託型別

self.PriceType.xxx,傳給 trade_type。

值 說明 沒成交時
market_ioc 市價單,一定成交 —
limit_ioc 限價單,只看下一根 當場取消,不留單
limit_rod 限價單(追求更好的價:買在低點、賣在高點) 留單到之後的 bar
stop_rod 觸價/突破單(追價:買在突破、賣在跌破,停損用) 留單到之後的 bar
compel_now 強制立即成交,不經撮合判斷,直接以指定 trade_price 成交。價格不能給 0(會以 0 元成交),結算時帶 row['Close'] —

成交價規則(實測)

在 on_bar 下的單要等下一根才撮合,所以所有規則都是拿「下一根的 OHLC」判斷。

下表以 bar0 全部 100(在這裡下單)、bar1 = O100 H110 L90 C100(用它撮合) 實測:

委託 成交價 為什麼
market_ioc 買 100 下一根開盤價
market_ioc 賣 100 同上
limit_ioc 買 @95 未成交 開盤 100 > 限價 95,IOC 不留單直接取消
limit_ioc 買 @105 100 開盤就優於限價 → 用開盤價(對你有利)
limit_rod 買 @105 100 同上
limit_rod 買 @95 95 開盤沒滿足、盤中最低 90 觸及 → 用限價
limit_rod 買 @80 未成交(留單) 最低只到 90,沒碰到 80
stop_rod 買 @95 100 開盤 100 已越過觸價 → 用開盤價(跳空滑價)
stop_rod 買 @105 105 開盤未觸發、盤中最高 110 穿越 → 用觸價
stop_rod 買 @120 未成交(留單) 最高只到 110
limit_rod 賣 @105 105 盤中最高 110 觸及
stop_rod 賣 @95 95 盤中最低 90 跌破

歸納成兩句話:

  • 限價單(limit_*):開盤價若已優於你的限價 → 用開盤價成交(多賺); 否則盤中觸價 → 用限價成交。
  • 觸價單(stop_rod):開盤價若已越過觸價 → 用開盤價成交 (跳空滑價,這是刻意模擬真實情況,不是 bug);否則盤中穿越 → 用觸價成交。

stop_rod 的跳空行為是回測比實盤樂觀/悲觀的關鍵。用它做停損時, 大跳空的那幾根會以開盤價出場,損失可能遠大於你設定的停損點——這是對的,實盤就是這樣。

什麼時候會「同一根內」成交

只有 tick 路徑(feed_tick / feed_tick_df)才可能,因為那時 bar 還在形成中:

情境 market_ioc 成交價
在 on_bar 下單 下一根的開盤價
在 on_intrabar / on_position 下單,且 bar 還在形成中 下一筆 tick 進來時的當時價格(同一根內)

用 feed_data 時每列就是一根完整 K 棒、沒有盤中過程,所以一定是下一根開盤價。


四個停損停利方法

方法 說明
self.Condition.set_STL(name, STL) 固定停損:進場價往不利方向移動 STL%(例 2.0)時出場
self.Condition.set_PFT(name, PFT) 固定停利:進場價往有利方向移動 PFT%(例 5.0)時出場
self.Condition.set_MoveLoss(name, DS) 移動停損:自持倉期間的極值價格回撤 DS% 時出場
self.Condition.set_TGT_PFT(name, TGT, PFT) 目標追蹤:獲利先達進場價的 TGT% 後啟動,再從最高獲利點數回吐 PFT% 時出場

四個都必須在 on_bar 裡無條件、每根 bar 呼叫一次。 它們的作用是「依當前持倉重算出場價並更新掛單」——不呼叫,出場價就不會更新。 不要包在 if 裡。

name 要與下單時 Condition.Global 的 rule 一致。

出場價公式

以多單為例(極值價 = entry_price + P.max_earn)。空單方向相反。

方法 出場價公式 委託型別 內部 rule 名稱
set_STL(name, STL) entry_price × (1 − STL/100) stop_rod STL / STLs
set_PFT(name, PFT) entry_price × (1 + PFT/100) limit_rod PFT / PFTs
set_MoveLoss(name, DS) 極值價 × (1 − DS/100) ← 價格的 % stop_rod DS / DSs
set_TGT_PFT(name, TGT, PFT) 極值價 − max_earn × PFT/100 ← 獲利點數的 %
且需先滿足 極值價 ≥ entry_price × (1 + TGT/100) 才啟動
stop_rod TGT / TGTs

實測(進場價 100,價格最高到 120,即 max_earn = 20):

set_STL('bk', 5)          → stop_rod @ 95.0    = 100 × 0.95
set_MoveLoss('bk', 5)     → stop_rod @ 114.0   = 120 × 0.95        ← 極值「價格」的 5%
set_TGT_PFT('bk', 1, 30)  → stop_rod @ 114.0   = 120 − 20 × 0.30   ← 獲利「點數」的 30%

set_MoveLoss 的 DS 是極值價格的百分比,set_TGT_PFT 的 PFT 是獲利點數的百分比。 基準不同,數值不能互相套用(上例剛好都是 114 是巧合)。

內部 rule 名稱是保留字

出場單用 match_uid=P.uid 綁在個別持倉上,並且會佔用上表的內部 rule 名稱。 你自己下單不要用 STL / PFT / DS / TGT 這幾個名字(含 s 結尾的空單版)。

self.Condition.Global(con1, 1, 0, self.PriceType.market_ioc, None, 'long')
self.Condition.set_STL('long', 2.0)
self.Condition.set_PFT('long', 5.0)
self.Condition.set_TGT_PFT('long', 2.7, 32.0)
self.Condition.set_MoveLoss('long', 5.0)

這四個在原始碼裡是底線開頭的方法(_set_STL、_set_PFT、_set_TGT_PFT、_set_moveloss), 對外一律透過 self.Condition.set_XXX(...) 別名呼叫。


委託價會依 tick_map 四捨五入(期貨要改)

所有非 0 的委託價(限價、停損觸發價、compel_now 的價格、set_STL 等算出的出場價)都會依 self.tick_map 四捨五入到合法跳動單位。預設是台股現股的規則,價格 ≥ 1000 時以 5 為單位, 所以台指期 18000 點附近的價格會被修到 5 點的倍數。期貨策略要在 on_init 改掉:

def on_init(self):
    self.tick_map = [(float('inf'), 1)]      # 台指期、小台:1 點

實測一支台指期 MA 策略(2017~2026,62 次結算):沒設時結算的 compel_now 價格 100% 是 5 的倍數, 每次最多偏 2 點、累計 9 點。只用市價單影響很小;用限價/停損單的策略每一筆都會被修。

常用寫法對照

你想做的事 寫法
空手時進場做多 1 口 Global(sig and self.global_qty == 0, 1, 0, PriceType.market_ioc, None, 'bk')
有多單就全部平掉 Global(sig and self.global_qty > 0, -self.global_qty, 0, PriceType.market_ioc, None, 'bp')
多空反手(一次平舊倉+開新倉) Global(long and self.global_qty < 1, -self.global_qty + qty, 0, PriceType.market_ioc, short, 'long')
加碼(不管現有部位再加 1 口) Global(sig, 1, 0, PriceType.market_ioc, None, 'add')
用資金的 30% 進場 Global(sig, 0.3, ...),並在 config 設 lot_size
掛低接(限價買在現價下方) Global(sig, 1, row['Close'] * 0.99, PriceType.limit_rod, None, 'bk')
突破買進(觸價單) Global(sig, 1, self.H, PriceType.stop_rod, None, 'bk')
每筆持倉獨立出場 在 on_position 裡用 match_uid=P.uid
條件不成立時撤單 Global(..., cancel_triggered=撤單條件, ...)
結算日強制平倉 Global(row['DateTime'] == row['Settlement'], -self.global_qty, row['Close'], PriceType.compel_now, False, '結算')
收盤前平倉(不留倉) 判斷 row['DateTime'].strftime('%H%M') == '1345' 再用 market_ioc 或 compel_now
連勝後加碼 在 on_bar 裡讀 self.stats.get('bk', {}).get('continue_wins', 0)
只在特定時段交易 hm = row['DateTime'].strftime('%H%M'),再用 '0900' <= hm <= '1100' 當條件

相關頁