Advanced trade management

Strategies

Advanced trade management (ATM)

Attach a bracket plan once and the framework auto-brackets every position you open: multi-target scale-out, auto-breakeven and progressive trailing, all OCO-managed for you.

ADVANCED Advanced Trade Management

ATM is for strategies that need real bracket management rather than a single stop and target. You describe a plan once with UseAtm(…) in OnInitialize or OnStart, and from then on every position the strategy opens is automatically bracketed according to that plan: a protective stop plus scale-out targets, with break-even and trailing applied as the trade develops. The framework handles the OCO and the trailing, so your OnBar shrinks to just emitting entries. ATM is orthogonal to managed intent, so a typical strategy calls EnterLong for the intent and lets the ATM protect the fill.

The plan, an AtmStrategy, is one-or-more brackets (each a slice of the position with its own stop and target) plus one embedded stop strategy that governs break-even and trailing. Bracket quantities are relative shares, so one template scales to any position size.

C#using SabrTrader.Pipeline.Venues.Trading.Atm;

protected override void OnStart()
{
    var atm = new AtmStrategy(
        Name: "1:3 + auto-BE",
        Unit: AtmParameterUnit.Ticks,
        Brackets: new[]
        {
            new AtmBracket(Quantity: 0.5m, StopLoss: 10m, ProfitTarget: 30m), // scale half at 3R
            new AtmBracket(Quantity: 0.5m, StopLoss: 10m, ProfitTarget: 0m),  // runner (0 = no target)
        },
        StopStrategy: new AtmStopStrategy(
            StopOrderType: AtmStopOrderType.StopMarket,
            StopLimitOffsetTicks: 0m,
            AutoBreakeven: new AtmAutoBreakeven(ProfitTrigger: 15m, Plus: 5m),
            AutoTrail: null));

    UseAtm(atm, tickSize: 0.25);   // tickSize / pointValue optional if the run config provides them
}

The plan composes from a handful of small records:

  • AtmBracket(Quantity, StopLoss, ProfitTarget): one target slice; ProfitTarget = 0 is a runner (exited only by its stop). Its IsRunner reports this. Every bracket must carry a positive stop.
  • AtmAutoBreakeven(ProfitTrigger, Plus): once the trade moves ProfitTrigger ticks in favour, move every stop to entry ± Plus ticks (positive locks profit, negative leaves room).
  • AtmAutoTrail(Steps) with up to AtmAutoTrail.MaxSteps (3) AtmAutoTrailStep(ProfitTrigger, StopLoss, Frequency): one to three progressively tightening trail steps, ordered by ascending trigger.
  • AtmTimeExit(TimeSpan CloseAt, string TimeZoneId): optional end-of-day time exit (flatten at a wall-clock time in a named zone).

The stop order type is set on AtmStopStrategy.StopOrderType (AtmStopOrderType): StopMarket (a native stop-market at the venue), StopLimit (needs a positive StopLimitOffsetTicks; caps slippage at the risk of a non-fill), or Simulated (the engine watches price and submits a market exit on touch; it works on venues without a native stop, such as crypto spot, and protects only while the engine runs). AtmStopStrategy.StaticMarket is a ready-made plain stop-market with no break-even or trail. Read HasAtmBracket to know when a live bracket is in place.

Call UseAtm once. UseAtm throws if called twice, and it requires a positive tick size (passed explicitly or resolved from the run config / instrument metadata). Configure the plan in OnInitialize or OnStart, never per bar. Calling it also claims position management for the run (see below).

Units

Every distance in the plan is interpreted through the AtmParameterUnit you pass to AtmStrategy. The example uses Ticks, but you can also express stops and targets in Currency (cash per contract; needs the instrument point value, supplied to UseAtm or resolved from the run config), Percent (of entry price), or absolute Price distance. AtmStrategy.TotalQuantity is the summed share across brackets; a strategy that wants ATM-total sizing passes it to EnterLong.

A bracket can override the unit per side (AtmBracket.StopLossUnit / ProfitTargetUnit), and three more units adapt to the trade:

  • Atr: a multiple of the Average True Range, so the distance is value × ATR. AtmStrategy.AtrPeriod is the Wilder lookback (default 14). A stop in this unit also carries AtmBracket.FallbackStopTicks, used while the ATR is not live yet.
  • RiskReward: a target at a multiple of the bracket's resolved stop distance. Targets only.
  • Pips: forex pips (value × pipSize). Forex only.

In a strategy run the ATR is measured on the strategy's primary series: it is seeded from the bars the run already holds and updated on every closed bar, in or out of a position. On the Chart Trader and SuperDOM a template names its own bars instead, with AtmStrategy.AtrTimeframe (new AtmAtrTimeframe(AtmAtrTimeframeUnit.Minute, 5); units Minute, Hour, Day, Tick), so the same template sizes the same stop from every window. There an ATR template without a timeframe (RequiresAtr true, AtrTimeframe null) does not arm; the timeframe is never guessed. If your plugin saves templates for the trader, run AtmStrategyAuthoring.Problems(strategy, adaptiveBracketsEntitled, pipsAvailable) first and show its sentences: it is the same check the ATM editor applies on Save, and an empty list means the template may be saved.

ATR not live at the fill. When a bracket with ATR legs is placed before the ATR has enough closed bars, the stop goes in at FallbackStopTicks and an ATR target rides as a runner. The position is protected, the legs stay where they were placed, and SabrTrader warns the trader once for that placement.

Volatility-scaled & level-anchored stops

Two helpers adjust the protective geometry of the next bracketed entry without rebuilding the plan, for strategies that size the stop from live volatility (ATR at entry) or anchor it to an indicator level. Both are no-ops if UseAtm was never called, and any already-live bracket keeps the distances it was placed with.

  • SetAtmBracketGeometry(stopOffset, targetOffset): replace the stop and target offset (in the ATM strategy's unit) for the next entry. A target ≤ 0 means runner.
  • SetNextAtmStopPrice(double? stopPrice): set an absolute stop price for the next entry (e.g. the opening-range low). One-shot; honoured only when the level is on the protective side of the fill, else the bracket falls back to its template distance. Pass null to clear.
C#using System;

protected override void OnBar()
{
    // Entry condition: this bar closed above the prior bar's high.
    if (IsFlat && TryGetBar(1, out var prior) && CurrentBar.Close > prior.High)
    {
        double atrTicks = AverageTrueRange(14) / TickSize!.Value;          // the plan's unit is Ticks
        if (!double.IsNaN(atrTicks))
            SetAtmBracketGeometry(stopOffset: atrTicks, targetOffset: 2 * atrTicks);
        EnterLong(1);   // the next fill is bracketed with the freshly sized stop and target
    }
}

// Mean true range of the last n bars, in price.
private double AverageTrueRange(int n)
{
    double sum = 0;
    int count = 0;
    for (int i = 0; i < n; i++)
    {
        if (!TryGetBar(i, out var bar) || !TryGetBar(i + 1, out var prev)) break;
        sum += Math.Max(bar.High, prev.Close) - Math.Min(bar.Low, prev.Close);
        count++;
    }
    return count > 0 ? sum / count : double.NaN;
}

Position-management claims

When an automated strategy manages its own protective exits, a manual Chart-Trader / SuperDOM ATM on the same account+instrument must defer rather than place a second, parallel bracket. UseAtm calls ClaimPositionManagement() for you. A strategy that brackets through some other mechanism calls it explicitly once that mechanism is wired; it is idempotent and released automatically when the run terminates.

A strategy's claim is EXCLUSIVE. Two runs on the same account and instrument each read the same account position as their own trade and each bracket it in full: one stop hit then fills both stops and reverses the position instead of closing it. So a run claims with ManagedInstrument.ExclusiveExits set and refuses to start while another exclusive claim is live on that scope, surfacing a PositionManagementConflictException naming the account and instrument. Manual Chart Trader and SuperDOM engines claim non-exclusively and defer to strategies.

Let that exception propagate. A run that swallowed it would trade unprotected, or duplicate the bracket that the other run already owns; faulting visibly is the safe outcome. The claim is taken under the registry's lock, so two runs started together can never both win.

One thing can displace you: the trade copier. On an account it mirrors into, the copier takes the claim and wins by decision: the slave's exits are its to manage. Your run then loses its token and its bracket is torn down, so it stops placing stops on a position it no longer owns. Nothing else displaces a live claim.

The service seam

UseAtm is sugar over IStrategyContext.CreateAtmService(atm, tickSize, pointValue), which returns an IAtmService: HasBracket, UpdateBracketGeometry, SetNextBracketStopPrice, plus the OnBar / OnOrderUpdate / OnPositionUpdate pumps the base class forwards into. You rarely touch it directly; a test double of IStrategyContext can return its own IAtmService to observe what the strategy asks of the ATM. For the full AtmStrategy model and the manual side, see the Trading reference.