回測:委託、成交價與風控
所屬章節:回測 成交價規則表 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 改掉:
實測一支台指期 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' 當條件 |