Files
nt8-sdk/TASK-03-trailing-stop.md
2026-02-24 15:00:41 -05:00

5.3 KiB

TASK-03: Fix TrailingStopManager Placeholder Math

File: src/NT8.Core/Execution/TrailingStopManager.cs Priority: HIGH No dependencies Estimated time: 1 hour


Background

CalculateNewStopPrice() has three broken cases:

FixedTrailing (broken):

return marketPrice - (position.AverageFillPrice - position.AverageFillPrice); // always 0

ATRTrailing (placeholder):

return marketPrice - (position.AverageFillPrice * 0.01m); // uses fill price as ATR proxy

Chandelier (placeholder):

return marketPrice - (position.AverageFillPrice * 0.01m); // same placeholder

The TrailingStopConfig class has these fields available — use them:

  • TrailingAmountTicks — integer, tick count for fixed trailing distance
  • AtrMultiplier — decimal, multiplier for ATR-based methods
  • TypeStopType enum

Look at TrailingStopConfig in the same file or nearby to confirm field names before editing.


Exact Changes Required

Replace the entire switch body inside CalculateNewStopPrice() with correct math.

Use tick size = 0.25m as the default (ES/NQ standard). The config should ideally carry tick size, but since it currently does not, use 0.25m as the constant for now with a comment explaining it.

switch (type)
{
    case StopType.FixedTrailing:
    {
        // Trail by a fixed number of ticks from current market price.
        // TrailingAmountTicks comes from config; default 8 if zero.
        var tickSize = 0.25m;
        var trailingTicks = config.TrailingAmountTicks > 0 ? config.TrailingAmountTicks : 8;
        var distance = trailingTicks * tickSize;

        return position.Side == OMS.OrderSide.Buy
            ? marketPrice - distance
            : marketPrice + distance;
    }

    case StopType.ATRTrailing:
    {
        // Trail by AtrMultiplier * estimated ATR.
        // We do not have live ATR here, so approximate ATR as (EntryPrice * 0.005)
        // which is ~0.5% — a conservative proxy for ES/NQ.
        // TODO: pass actual ATR value via config when available.
        var atrMultiplier = config.AtrMultiplier > 0 ? config.AtrMultiplier : 2.0m;
        var estimatedAtr = position.AverageFillPrice * 0.005m;
        var distance = atrMultiplier * estimatedAtr;

        return position.Side == OMS.OrderSide.Buy
            ? marketPrice - distance
            : marketPrice + distance;
    }

    case StopType.Chandelier:
    {
        // Chandelier exit: trail from highest high (approximated as marketPrice)
        // minus AtrMultiplier * ATR.
        // Full implementation requires bar history; use same ATR proxy for now.
        // TODO: pass highest-high and actual ATR via config.
        var chanMultiplier = config.AtrMultiplier > 0 ? config.AtrMultiplier : 3.0m;
        var estimatedAtr = position.AverageFillPrice * 0.005m;
        var distance = chanMultiplier * estimatedAtr;

        return position.Side == OMS.OrderSide.Buy
            ? marketPrice - distance
            : marketPrice + distance;
    }

    case StopType.PercentageTrailing:
    {
        // Existing logic is correct — percentage of current price.
        var pctTrail = 0.02m;
        return position.Side == OMS.OrderSide.Buy
            ? marketPrice * (1 - pctTrail)
            : marketPrice * (1 + pctTrail);
    }

    default:
    {
        // Fixed trailing as fallback
        var tickSize = 0.25m;
        var ticks = config.TrailingAmountTicks > 0 ? config.TrailingAmountTicks : 8;
        return position.Side == OMS.OrderSide.Buy
            ? marketPrice - (ticks * tickSize)
            : marketPrice + (ticks * tickSize);
    }
}

IMPORTANT: The config variable is NOT currently a parameter to CalculateNewStopPrice(). The current signature is:

public decimal CalculateNewStopPrice(StopType type, OMS.OrderStatus position, decimal marketPrice)

You need to add config as a parameter:

public decimal CalculateNewStopPrice(StopType type, OMS.OrderStatus position, decimal marketPrice, TrailingStopConfig config)

Then fix the ONE call site inside UpdateTrailingStop():

var newStopPrice = CalculateNewStopPrice(trailingStop.Config.Type, trailingStop.Position, currentPrice, trailingStop.Config);

Also Create: New Unit Tests

Create tests/NT8.Core.Tests/Execution/TrailingStopManagerFixedTests.cs

// Tests that verify FixedTrailing actually moves the stop
// Test 1: Long position, FixedTrailing 8 ticks → stop = marketPrice - (8 * 0.25) = marketPrice - 2.0
// Test 2: Short position, FixedTrailing 8 ticks → stop = marketPrice + 2.0
// Test 3: ATRTrailing multiplier 2 → stop distance > 0
// Test 4: Stop only updates when favorable (existing UpdateTrailingStop logic test)

Acceptance Criteria

  • FixedTrailing for a long position at price 5100 with 8 ticks returns 5100 - 2.0 = 5098.0
  • FixedTrailing for a short position at price 5100 with 8 ticks returns 5100 + 2.0 = 5102.0
  • ATRTrailing returns a value meaningfully below market price for longs (not zero, not equal to price)
  • Chandelier returns a value meaningfully below market price for longs (not zero)
  • CalculateNewStopPrice signature updated — call site in UpdateTrailingStop() updated
  • New unit tests pass
  • All existing tests still pass
  • verify-build.bat passes