152 lines
5.3 KiB
Markdown
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
|