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 distanceAtrMultiplier— decimal, multiplier for ATR-based methodsType—StopTypeenum
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
FixedTrailingfor a long position at price 5100 with 8 ticks returns5100 - 2.0 = 5098.0FixedTrailingfor a short position at price 5100 with 8 ticks returns5100 + 2.0 = 5102.0ATRTrailingreturns a value meaningfully below market price for longs (not zero, not equal to price)Chandelierreturns a value meaningfully below market price for longs (not zero)CalculateNewStopPricesignature updated — call site inUpdateTrailingStop()updated- New unit tests pass
- All existing tests still pass
verify-build.batpasses