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

152 lines
5.3 KiB
Markdown

# 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):**
```csharp
return marketPrice - (position.AverageFillPrice - position.AverageFillPrice); // always 0
```
**ATRTrailing (placeholder):**
```csharp
return marketPrice - (position.AverageFillPrice * 0.01m); // uses fill price as ATR proxy
```
**Chandelier (placeholder):**
```csharp
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
- `Type``StopType` 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.
```csharp
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:
```csharp
public decimal CalculateNewStopPrice(StopType type, OMS.OrderStatus position, decimal marketPrice)
```
You need to add `config` as a parameter:
```csharp
public decimal CalculateNewStopPrice(StopType type, OMS.OrderStatus position, decimal marketPrice, TrailingStopConfig config)
```
Then fix the ONE call site inside `UpdateTrailingStop()`:
```csharp
var newStopPrice = CalculateNewStopPrice(trailingStop.Config.Type, trailingStop.Position, currentPrice, trailingStop.Config);
```
---
## Also Create: New Unit Tests
Create `tests/NT8.Core.Tests/Execution/TrailingStopManagerFixedTests.cs`
```csharp
// 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