Appearance
Stops, targets and trailing
There are three mechanisms to protect a position: stop loss, take profit, and trailing stop. TessTrade offers native stop and target (attached to the position and monitored by the engine) and also allows manual stop/trailing (where the script computes and fires the exit).
Native stop loss and native take profit
Pass them as kwargs on the entry order:
python
close = sdk.candles[-1]["close"]
sdk.buy(
action="buy_to_open",
qty=1,
order_type="market",
stop_loss=close * 0.98, # 2% below
take_profit=close * 1.05, # 5% above
)After entry, the engine monitors the position on every candle:
- If the low of the candle touches
stop_loss-> emits an automatic exit order (for long:sell_to_close). - If the high of the candle touches
take_profit-> emits an automatic exit order. - If both are touched on the same candle, the result depends on the configured execution model (pessimistic: stop wins; optimistic: target wins).
The script does not need to do anything for the stop/target to work. The engine takes care of it.
Modify stop/target of a live position
Use update_position_exits (details in canonical actions):
python
if sdk.position > 0:
close = sdk.candles[-1]["close"]
new_stop = close * 0.97 # raises the stop (manual trailing)
sdk.close(
action="update_position_exits",
stop_loss=new_stop,
# take_profit omitted - keeps the previous one
)Or use the semantic shortcut:
python
sdk.update_exits(stop_loss=new_stop)
sdk.set_trailing_stop(new_stop) # same effect; clearer namePassing None does not clear the stop, it only omits the update of that field. To remove the stop, pass 0 and the engine will ignore it (stops <= 0 are not considered).
Trailing stop - manual implementation
There is no native trailing_stop=. The implementation is done in the script with sdk.state and update_exits. It requires more code but allows full flexibility.
Classic trailing stop (by percentage)
python
def on_bar_strategy(sdk, params):
if not isinstance(sdk.state, dict):
sdk.state = {}
if "high_water" not in sdk.state:
sdk.state["high_water"] = None
close = sdk.candles[-1]["close"]
trail_pct = float((params or {}).get("trail_pct", 0.02))
if sdk.position > 0:
# Long open: track the high-water.
hw = sdk.state["high_water"]
sdk.state["high_water"] = close if hw is None else max(hw, close)
# Stop always trail_pct below the highest price seen.
new_stop = sdk.state["high_water"] * (1 - trail_pct)
# Fire exit if the close falls below the stop.
if close <= new_stop:
sdk.sell(
action="sell_to_close",
qty=abs(sdk.position),
order_type="market",
)
sdk.state["high_water"] = None
else:
# Update the native stop to reflect the new level.
sdk.update_exits(stop_loss=new_stop)
else:
# No position, reset.
sdk.state["high_water"] = NoneWhat happens:
- Bar by bar,
high_watergrows as the price rises. - The stop is shifted up proportionally (
trail_pctbelow the high_water). - If the price falls below the current stop, the script closes manually (
sell_to_close). Even if the native stop also fires, the manual exit arrives first. - When the position flattens,
high_wateris reset.
Trailing with trailing_stop_pct
The buy/sell actions accept trailing_stop_pct as a kwarg, which the engine forwards to the execution model. In theory this simplifies the case above, but the logic of when to update is still the script's responsibility. The kwarg only records the preference in the signal. For effective trailing, implement it as above.
Short trailing
For a short position, track the low_water (lowest price seen) and place the stop above it:
python
if sdk.position < 0:
lw = sdk.state.get("low_water")
close = sdk.candles[-1]["close"]
sdk.state["low_water"] = close if lw is None else min(lw, close)
new_stop = sdk.state["low_water"] * (1 + trail_pct)
if close >= new_stop:
sdk.buy(
action="buy_to_cover",
qty=abs(sdk.position),
order_type="market",
)
sdk.state["low_water"] = None
else:
sdk.update_exits(stop_loss=new_stop)Exit strategies beyond stop/target
Time-based exit (maximum bars in position)
python
if sdk.position != 0:
entered_at = sdk.state.get("entry_time")
now = sdk.candles[-1]["time"]
max_hold_ms = 3_600_000 * 4 # 4 hours
if entered_at is not None and now - entered_at > max_hold_ms:
side = "sell_to_close" if sdk.position > 0 else "buy_to_cover"
sdk.close(action="close_position", qty=abs(sdk.position), order_type="market")
sdk.state["entry_time"] = None
elif sdk.position == 0 and sdk.state.get("entry_time") is None:
# (entry moment; save the timestamp)
passSet entry_time at the moment of entry.
Exit on RSI reversal
Combines persistent state with an indicator:
python
if sdk.position > 0:
rsi = _rsi_last([c["close"] for c in sdk.candles], 14)
if rsi is not None and rsi >= 70: # left the overbought zone
sdk.sell(action="sell_to_close", qty=abs(sdk.position), order_type="market")Combining native stop + manual trailing
Recommended pattern: place an initial native stop at entry (protection against an extreme gap) and apply manual trailing as the price advances:
python
if sdk.position == 0 and buy_signal:
close = sdk.candles[-1]["close"]
initial_stop = close * 0.95 # 5% stop as a safety net
sdk.buy(
action="buy_to_open",
qty=1,
order_type="market",
stop_loss=initial_stop,
)
sdk.state["high_water"] = close
elif sdk.position > 0:
close = sdk.candles[-1]["close"]
sdk.state["high_water"] = max(sdk.state.get("high_water", close), close)
new_stop = max(initial_stop, sdk.state["high_water"] * 0.97) # 3% trailing
sdk.update_exits(stop_loss=new_stop)Common mistakes
- A stop below the current price on a buy fires an immediate close. Always use
close * 0.98or lower. - Forgetting to reset
high_waterwhen the position flattens. On the next entry, the script inherits the high_water from the previous position. - Updating the stop on every candle without a real change.
update_exitsgenerates a signal and multiple updates pollute the log (the engine merges them, but it is noise). Checkif new_stop != sdk.state.get("last_stop")to avoid it. - Relying on native
stop_lossduring explosive volatility. On an opening gap, the stop may execute well below (slippage). For volatile markets, consider a volatility-based stop (ATR) or an early manual exit.
Next steps
- Persistent state - deep dive into
sdk.statewith complex trailing. - Canonical actions - how
update_position_exitsworks in detail.