Production hardening: kill switch, circuit breaker, trailing stops, log level, holiday calendar
Some checks failed
Build and Test / build (push) Has been cancelled
Some checks failed
Build and Test / build (push) Has been cancelled
This commit is contained in:
151
TASK-03-trailing-stop.md
Normal file
151
TASK-03-trailing-stop.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user