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:
140
TASK-02-circuit-breaker.md
Normal file
140
TASK-02-circuit-breaker.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# TASK-02: Wire ExecutionCircuitBreaker into NT8StrategyBase
|
||||
|
||||
**File:** `src/NT8.Adapters/Strategies/NT8StrategyBase.cs`
|
||||
**Priority:** CRITICAL
|
||||
**Depends on:** TASK-01 must be done first (file already open/modified)
|
||||
**Estimated time:** 45 min
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
`ExecutionCircuitBreaker` at `src/NT8.Core/Execution/ExecutionCircuitBreaker.cs` is complete and tested.
|
||||
Its public API is:
|
||||
- `bool ShouldAllowOrder()` — returns false when circuit is Open
|
||||
- `void OnSuccess()` — call after a successful order submission
|
||||
- `void OnFailure()` — call after a failed order submission
|
||||
- `void RecordOrderRejection(string reason)` — call when NT8 rejects an order
|
||||
- `void Reset()` — resets to Closed state
|
||||
|
||||
The `ExecutionCircuitBreaker` constructor:
|
||||
```csharp
|
||||
public ExecutionCircuitBreaker(
|
||||
ILogger<ExecutionCircuitBreaker> logger,
|
||||
int failureThreshold = 3,
|
||||
TimeSpan? timeout = null,
|
||||
TimeSpan? retryTimeout = null,
|
||||
int latencyWindowSize = 100,
|
||||
int rejectionWindowSize = 10)
|
||||
```
|
||||
|
||||
**Problem:** It is never instantiated. `NT8StrategyBase` submits orders with no circuit breaker gate.
|
||||
|
||||
---
|
||||
|
||||
## Exact Changes Required
|
||||
|
||||
### 1. Add using statement at top of `NT8StrategyBase.cs`
|
||||
|
||||
```csharp
|
||||
using NT8.Core.Execution;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
```
|
||||
|
||||
### 2. Add private field alongside the other private fields
|
||||
|
||||
```csharp
|
||||
private ExecutionCircuitBreaker _circuitBreaker;
|
||||
```
|
||||
|
||||
### 3. Initialize in `InitializeSdkComponents()`, after `_positionSizer = new BasicPositionSizer(_logger);`
|
||||
|
||||
```csharp
|
||||
_circuitBreaker = new ExecutionCircuitBreaker(
|
||||
NullLogger<ExecutionCircuitBreaker>.Instance,
|
||||
failureThreshold: 3,
|
||||
timeout: TimeSpan.FromSeconds(30));
|
||||
```
|
||||
|
||||
### 4. Gate `SubmitOrderToNT8()` — add check at top of the method
|
||||
|
||||
```csharp
|
||||
private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent)
|
||||
{
|
||||
// Circuit breaker gate
|
||||
if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder())
|
||||
{
|
||||
var state = _circuitBreaker.GetState();
|
||||
Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason));
|
||||
if (_logger != null)
|
||||
_logger.LogWarning("Circuit breaker blocked order: {0}", state.Reason);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// ... EXISTING submit logic unchanged ...
|
||||
var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, DateTime.Now.Ticks);
|
||||
_executionAdapter.SubmitOrder(request, orderName);
|
||||
|
||||
if (request.Side == OmsOrderSide.Buy)
|
||||
{ ... existing EnterLong/EnterLongLimit/etc ... }
|
||||
else if (request.Side == OmsOrderSide.Sell)
|
||||
{ ... existing EnterShort/etc ... }
|
||||
|
||||
if (intent.StopTicks > 0)
|
||||
SetStopLoss(orderName, CalculationMode.Ticks, (int)intent.StopTicks, false);
|
||||
|
||||
if (intent.TargetTicks.HasValue && intent.TargetTicks.Value > 0)
|
||||
SetProfitTarget(orderName, CalculationMode.Ticks, (int)intent.TargetTicks.Value);
|
||||
|
||||
// Mark success after submission
|
||||
if (_circuitBreaker != null)
|
||||
_circuitBreaker.OnSuccess();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_circuitBreaker != null)
|
||||
_circuitBreaker.OnFailure();
|
||||
|
||||
Print(string.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message));
|
||||
if (_logger != null)
|
||||
_logger.LogError("SubmitOrderToNT8 failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Wire rejections in `OnOrderUpdate()`
|
||||
|
||||
In the existing `OnOrderUpdate()` override, after the null/name checks, add:
|
||||
|
||||
```csharp
|
||||
// Record NT8 rejections in circuit breaker
|
||||
if (orderState == NinjaTrader.Cbi.OrderState.Rejected && _circuitBreaker != null)
|
||||
{
|
||||
var reason = string.Format("{0} {1}", errorCode, nativeError ?? string.Empty);
|
||||
_circuitBreaker.RecordOrderRejection(reason);
|
||||
Print(string.Format("[SDK] Order rejected by NT8: {0}", reason));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ExecutionCircuitBreaker` is instantiated in `InitializeSdkComponents()`
|
||||
- [ ] `SubmitOrderToNT8()` checks `ShouldAllowOrder()` before submitting — if false, prints message and returns
|
||||
- [ ] `OnOrderUpdate()` calls `RecordOrderRejection()` when `orderState == OrderState.Rejected`
|
||||
- [ ] `OnSuccess()` called after successful order submission
|
||||
- [ ] `OnFailure()` called in catch block
|
||||
- [ ] `verify-build.bat` passes with zero errors
|
||||
- [ ] Existing 240+ tests still pass: `dotnet test NT8-SDK.sln --verbosity minimal`
|
||||
|
||||
---
|
||||
|
||||
## Do NOT Change
|
||||
|
||||
- `ExecutionCircuitBreaker.cs` — already correct, just use it
|
||||
- Any Core layer files
|
||||
- Any test files
|
||||
Reference in New Issue
Block a user