Exit Rules
תוכן זה אינו זמין עדיין בשפה שלך.
Exit rules are function calls, not assignments. They register a price-based exit that the platform checks tick-by-tick — not just on bar close.
self.stop_loss(2.0) # exit if drawdown ≥ 2% from entryself.take_profit(5.0) # exit if gain ≥ 5% from entryself.break_even(1.0) # once up 1%, a stop at the entry priceself.trailing_stop(1.0, 0.5) # once up 1%, trail 0.5% behind the best priceA bare positional value means percent from entry. The unit is the keyword, so the same rules also take an absolute distance or an absolute level:
self.stop_loss(2.0) # 2% below entry (long)self.stop_loss(percent=2.0) # the same thing, spelled outself.stop_loss(points=5) # 5 points below entryself.stop_loss(price=412.50) # an absolute price leveltake_profit takes the same three units. break_even takes a percent or points=, but not price=, because it is measured from the entry.
Break-even
self.break_even(1.0) # once up 1% from entry, a stop at the entry priceself.break_even(percent=1.0) # the same thing, spelled outself.break_even(points=20) # once up 20 points, a stop at the entry pricebreak_even takes one number, the trigger. The moment the trade is up by that much, a stop appears at exactly the entry price. This happens mid-bar, on the tick that reaches it.
- It is its own stop, separate from
stop_loss. Both are watched, and whichever price is hit first closes the trade. - Once it appears, it stays until the trade closes. Declaring a different trigger on a later bar only matters before it has appeared.
- A trigger of 0 (or less) is no break-even, the same as
stop_loss(0), so an optimizer sweep can include 0 to mean “off”. - A trade it closes shows the exit reason
BreakEven.
Trailing stop
self.trailing_stop(1.0, 0.5) # start at +1%, trail 0.5% of the peakself.trailing_stop(activate_percent=1.0, trail_percent=0.5) # the same thing, spelled outself.trailing_stop(activate_points=20, trail_points=10) # start at +20 points, trail 10 pointsself.trailing_stop(0, 1.5) # trail 1.5% from the moment of entrytrailing_stop takes two numbers:
- Activation. How far the trade must be up before trailing starts.
0means it trails from the moment of entry. - Distance. How far behind the best price the stop follows: a percent of the peak (long) or trough (short), or a number of points.
Details:
- Two positional numbers are both percent.
- To use points, name the numbers. Each number carries its own unit, so
activate_points=20, trail_percent=0.5is fine. - Once trailing has started, it keeps trailing until the trade closes.
- A distance of 0 is no trailing stop.
The old one-number spelling self.trailing_stop(1.5) is refused on new code with RC2_TRAILING_TWO_NUMBERS. Write self.trailing_stop(0, 1.5), which means exactly the same. Strategies already saved with one number keep running as they always did. points= and price= on trailing_stop raise RC2_EXIT_UNIT. Name the numbers instead.
Assigning instead of calling fails SILENTLY
self.stop_loss = 2.0 # ❌ — no error, and NO STOP IS SETself.stop_loss(2.0) # ✅stop_loss is not a reserved name, so the assignment is accepted as an ordinary persistent-state slot called stop_loss holding 2.0. Nothing warns you; the position simply runs without a stop. This is the most expensive typo on this page — exit rules are registered by calling them, never set by assignment.
When they fire
Exit rules are checked tick-by-tick by the platform — not just on bar close. That means your stop fires the moment price touches it, not at the next bar’s open.
| Rule | Triggers when | Fills at |
|---|---|---|
stop_loss(p) |
Long: low ≤ entry × (1 − p/100). Short: high ≥ entry × (1 + p/100). | The trigger price. |
take_profit(p) |
Long: high ≥ entry × (1 + p/100). Short: low ≤ entry × (1 − p/100). | The trigger price. |
break_even(t) |
It has appeared (the best price reached entry × (1 + t/100) long, or entry × (1 − t/100) short), and price comes back to the entry. | The entry price. |
trailing_stop(a, d) |
It has started (the peak reached entry × (1 + a/100) long, or the trough reached entry × (1 − a/100) short), and long: low ≤ peak × (1 − d/100); short: high ≥ trough × (1 + d/100). | The trigger price. |
With points=, replace the percentage with a distance in points: for example, a long break-even appears at entry + t.
If a bar opens beyond a level (a gap), the rule fills at that bar’s open instead, the way a real stop would.
When an exit rule fires, the position closes at the trigger price. execute() still runs on that bar — your indicators and your state see every bar that happened — but no order fills on it: an order placed on the previous bar rests and gets its chance on the next one, because the exit already used that bar.
Dynamic exits
Exit rules can be re-registered every bar with different values. The most recent call wins.
class Tightening(Strategy): trade = Symbol() sizing = FixedCapital() bars_in_pos = 0
def execute(self): # Count bars in position with persistent state if self.trade.is_long: self.bars_in_pos = self.bars_in_pos + 1 else: self.bars_in_pos = 0
# Tighten the trailing stop after 20 bars in position if self.trade.is_long and self.bars_in_pos > 20: self.trailing_stop(0, 1.0) elif self.trade.is_long: self.trailing_stop(0, 2.0)
self.stop_loss(3.0) # always active
if self.trade.is_flat: self.buy(self.trade)Exit rules registered on bar T take effect from bar T+1 onward.
Multiple rules
You can register all four at once. The first to trigger wins.
self.stop_loss(2.0)self.take_profit(5.0)self.break_even(1.0)self.trailing_stop(1.0, 0.5)If two trigger on the same tick (extremely rare; usually requires a gap), the order is: stop_loss → break_even → trailing_stop → take_profit.
A switch Param is how you make a rule optional, and it keeps the strategy on the optimizer’s fast path:
class OptionalExits(Strategy): enable_trailing_stop = Param(True) trailing_floor_percent = Param(1.0, min=0.0, max=5.0) trailing_amount = Param(0.5, min=0.1, max=3.0) enable_break_even = Param(False) break_even_floor_percent = Param(1.0, min=0.0, max=5.0) trade = Symbol() sizing = FixedCapital()
def execute(self): self.stop_loss(3.0) if self.enable_trailing_stop: self.trailing_stop(self.trailing_floor_percent, self.trailing_amount) if self.enable_break_even: self.break_even(self.break_even_floor_percent)
if self.trade.is_flat: self.buy(self.trade)Manual exits vs. rule exits
Rule exits (stop_loss, take_profit, break_even, trailing_stop) fire intra-bar at the trigger price. Manual exits (self.sell(), self.buy_to_cover()) fire at the next bar’s open. Use rules for risk management, manual exits for indicator-driven decisions.
class RuleAndManual(Strategy): trade = Symbol() sizing = FixedCapital()
def execute(self): rsi = RSI(14, on=self.trade)
# Risk management — always on self.stop_loss(3.0)
if rsi.crossed_above(30) and self.trade.is_flat: self.buy(self.trade)
# Indicator-driven exit if self.trade.is_long and rsi.crossed_below(70): self.sell(self.trade, self.trade.position, reason="RSI exit")Common diagnostics
| Code | What it means |
|---|---|
RC2_EXIT_ARG |
Exit rule called with the wrong number of positional args, or with no threshold. |
RC2_EXIT_UNIT |
A stale keyword (pct, pts, unit, dollars), two unit sources at once, price= on break_even, or points=/price= on trailing_stop. |
RC2_TRAILING_TWO_NUMBERS |
trailing_stop with one number. Write both: self.trailing_stop(0, 1.5). |
There is deliberately no diagnostic for self.stop_loss = 2.0 — see the danger note above. Nothing catches it but reading your own code.