Compare commits
2 Commits
v0.2.0
...
6325c091a0
| Author | SHA1 | Date | |
|---|---|---|---|
| 6325c091a0 | |||
| 3fdf7fb95b |
767
Phase3_Implementation_Guide.md
Normal file
767
Phase3_Implementation_Guide.md
Normal file
@@ -0,0 +1,767 @@
|
||||
# Phase 3: Market Microstructure & Execution - Implementation Guide
|
||||
|
||||
**Estimated Time:** 3-4 hours
|
||||
**Complexity:** Medium-High
|
||||
**Dependencies:** Phase 2 Complete ✅
|
||||
|
||||
---
|
||||
|
||||
## Implementation Overview
|
||||
|
||||
This phase adds professional-grade execution capabilities with market awareness, advanced order types, execution quality tracking, and intelligent order routing. We're building the "HOW to trade" layer on top of Phase 2's "WHAT to trade" foundation.
|
||||
|
||||
---
|
||||
|
||||
## Phase A: Market Microstructure Awareness (45 minutes)
|
||||
|
||||
### Task A1: Create MarketMicrostructureModels.cs
|
||||
**Location:** `src/NT8.Core/MarketData/MarketMicrostructureModels.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- `SpreadInfo` record - Bid-ask spread tracking
|
||||
- `LiquidityMetrics` record - Order book depth analysis
|
||||
- `SessionInfo` record - RTH vs ETH session data
|
||||
- `ContractRollInfo` record - Roll detection data
|
||||
- `LiquidityScore` enum - Poor/Fair/Good/Excellent
|
||||
- `TradingSession` enum - PreMarket/RTH/ETH/Closed
|
||||
|
||||
**Key Requirements:**
|
||||
- C# 5.0 syntax only
|
||||
- Thread-safe value types (records)
|
||||
- XML documentation
|
||||
- Proper validation
|
||||
|
||||
---
|
||||
|
||||
### Task A2: Create LiquidityMonitor.cs
|
||||
**Location:** `src/NT8.Core/MarketData/LiquidityMonitor.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Track bid-ask spread in real-time
|
||||
- Calculate liquidity score by symbol
|
||||
- Monitor order book depth
|
||||
- Detect liquidity deterioration
|
||||
- Session-aware monitoring (RTH vs ETH)
|
||||
|
||||
**Key Features:**
|
||||
- Rolling window for spread tracking (last 100 ticks)
|
||||
- Liquidity scoring algorithm
|
||||
- Alert thresholds for poor liquidity
|
||||
- Thread-safe with proper locking
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public void UpdateSpread(string symbol, double bid, double ask, long volume);
|
||||
public LiquidityMetrics GetLiquidityMetrics(string symbol);
|
||||
public LiquidityScore CalculateLiquidityScore(string symbol);
|
||||
public bool IsLiquidityAcceptable(string symbol, double threshold);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task A3: Create SessionManager.cs
|
||||
**Location:** `src/NT8.Core/MarketData/SessionManager.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Track current trading session (RTH/ETH)
|
||||
- Session start/end times by symbol
|
||||
- Holiday calendar awareness
|
||||
- Contract roll detection
|
||||
|
||||
**Key Features:**
|
||||
- Symbol-specific session times
|
||||
- Timezone handling (EST for US futures)
|
||||
- Holiday detection
|
||||
- Roll date tracking for futures
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public SessionInfo GetCurrentSession(string symbol, DateTime time);
|
||||
public bool IsRegularTradingHours(string symbol, DateTime time);
|
||||
public bool IsContractRolling(string symbol, DateTime time);
|
||||
public DateTime GetNextSessionStart(string symbol);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase B: Advanced Order Types (60 minutes)
|
||||
|
||||
### Task B1: Extend OrderModels.cs
|
||||
**Location:** `src/NT8.Core/OMS/OrderModels.cs`
|
||||
|
||||
**Add to existing file:**
|
||||
- `LimitOrderRequest` record
|
||||
- `StopOrderRequest` record
|
||||
- `StopLimitOrderRequest` record
|
||||
- `MITOrderRequest` record (Market-If-Touched)
|
||||
- `TrailingStopConfig` record
|
||||
- `OrderTypeParameters` record
|
||||
|
||||
**Order Type Specifications:**
|
||||
|
||||
**Limit Order:**
|
||||
```csharp
|
||||
public record LimitOrderRequest(
|
||||
string Symbol,
|
||||
OrderSide Side,
|
||||
int Quantity,
|
||||
double LimitPrice,
|
||||
TimeInForce Tif
|
||||
) : OrderRequest;
|
||||
```
|
||||
|
||||
**Stop Order:**
|
||||
```csharp
|
||||
public record StopOrderRequest(
|
||||
string Symbol,
|
||||
OrderSide Side,
|
||||
int Quantity,
|
||||
double StopPrice,
|
||||
TimeInForce Tif
|
||||
) : OrderRequest;
|
||||
```
|
||||
|
||||
**Stop-Limit Order:**
|
||||
```csharp
|
||||
public record StopLimitOrderRequest(
|
||||
string Symbol,
|
||||
OrderSide Side,
|
||||
int Quantity,
|
||||
double StopPrice,
|
||||
double LimitPrice,
|
||||
TimeInForce Tif
|
||||
) : OrderRequest;
|
||||
```
|
||||
|
||||
**MIT Order:**
|
||||
```csharp
|
||||
public record MITOrderRequest(
|
||||
string Symbol,
|
||||
OrderSide Side,
|
||||
int Quantity,
|
||||
double TriggerPrice,
|
||||
TimeInForce Tif
|
||||
) : OrderRequest;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task B2: Create OrderTypeValidator.cs
|
||||
**Location:** `src/NT8.Core/OMS/OrderTypeValidator.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Validate order type parameters
|
||||
- Check price relationships (stop vs limit)
|
||||
- Verify order side consistency
|
||||
- Symbol-specific validation rules
|
||||
|
||||
**Validation Rules:**
|
||||
- Limit buy: limit price < market price
|
||||
- Limit sell: limit price > market price
|
||||
- Stop buy: stop price > market price
|
||||
- Stop sell: stop price < market price
|
||||
- Stop-Limit: stop and limit relationship validation
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public ValidationResult ValidateLimitOrder(LimitOrderRequest request, double marketPrice);
|
||||
public ValidationResult ValidateStopOrder(StopOrderRequest request, double marketPrice);
|
||||
public ValidationResult ValidateStopLimitOrder(StopLimitOrderRequest request, double marketPrice);
|
||||
public ValidationResult ValidateMITOrder(MITOrderRequest request, double marketPrice);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task B3: Update BasicOrderManager.cs
|
||||
**Location:** `src/NT8.Core/OMS/BasicOrderManager.cs`
|
||||
|
||||
**Add methods (don't modify existing):**
|
||||
```csharp
|
||||
public async Task<string> SubmitLimitOrderAsync(LimitOrderRequest request);
|
||||
public async Task<string> SubmitStopOrderAsync(StopOrderRequest request);
|
||||
public async Task<string> SubmitStopLimitOrderAsync(StopLimitOrderRequest request);
|
||||
public async Task<string> SubmitMITOrderAsync(MITOrderRequest request);
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
- Validate order type parameters
|
||||
- Convert to base OrderRequest
|
||||
- Submit through existing infrastructure
|
||||
- Track order type in metadata
|
||||
|
||||
---
|
||||
|
||||
## Phase C: Execution Quality Tracking (50 minutes)
|
||||
|
||||
### Task C1: Create ExecutionModels.cs
|
||||
**Location:** `src/NT8.Core/Execution/ExecutionModels.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- `ExecutionMetrics` record - Per-order execution data
|
||||
- `SlippageInfo` record - Price slippage tracking
|
||||
- `ExecutionTiming` record - Latency breakdown
|
||||
- `ExecutionQuality` enum - Excellent/Good/Fair/Poor
|
||||
- `SlippageType` enum - Positive/Negative/Zero
|
||||
|
||||
**ExecutionMetrics:**
|
||||
```csharp
|
||||
public record ExecutionMetrics(
|
||||
string OrderId,
|
||||
DateTime IntentTime,
|
||||
DateTime SubmitTime,
|
||||
DateTime FillTime,
|
||||
double IntendedPrice,
|
||||
double FillPrice,
|
||||
double Slippage,
|
||||
SlippageType SlippageType,
|
||||
TimeSpan SubmitLatency,
|
||||
TimeSpan FillLatency,
|
||||
ExecutionQuality Quality
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task C2: Create ExecutionQualityTracker.cs
|
||||
**Location:** `src/NT8.Core/Execution/ExecutionQualityTracker.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Track every order execution
|
||||
- Calculate slippage (intended vs actual)
|
||||
- Measure execution latency
|
||||
- Score execution quality
|
||||
- Maintain execution history
|
||||
|
||||
**Key Features:**
|
||||
- Per-symbol execution statistics
|
||||
- Rolling averages (last 100 executions)
|
||||
- Quality scoring algorithm
|
||||
- Alert on poor execution quality
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public void RecordExecution(string orderId, StrategyIntent intent, OrderFill fill);
|
||||
public ExecutionMetrics GetExecutionMetrics(string orderId);
|
||||
public ExecutionStatistics GetSymbolStatistics(string symbol);
|
||||
public double GetAverageSlippage(string symbol);
|
||||
public bool IsExecutionQualityAcceptable(string symbol);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task C3: Create SlippageCalculator.cs
|
||||
**Location:** `src/NT8.Core/Execution/SlippageCalculator.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Calculate price slippage
|
||||
- Determine slippage type (favorable/unfavorable)
|
||||
- Normalize slippage by symbol (tick-based)
|
||||
- Slippage impact on P&L
|
||||
|
||||
**Slippage Formula:**
|
||||
```
|
||||
Market Order:
|
||||
Slippage = FillPrice - MarketPriceAtSubmit
|
||||
|
||||
Limit Order:
|
||||
Slippage = FillPrice - LimitPrice (if better = favorable)
|
||||
|
||||
Slippage in Ticks = Slippage / TickSize
|
||||
Slippage Cost = Slippage * Quantity * TickValue
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public double CalculateSlippage(OrderType type, double intendedPrice, double fillPrice);
|
||||
public SlippageType ClassifySlippage(double slippage, OrderSide side);
|
||||
public int SlippageInTicks(double slippage, double tickSize);
|
||||
public double SlippageImpact(double slippage, int quantity, double tickValue);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase D: Smart Order Routing (45 minutes)
|
||||
|
||||
### Task D1: Create OrderRoutingModels.cs
|
||||
**Location:** `src/NT8.Core/Execution/OrderRoutingModels.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- `RoutingDecision` record - Route selection result
|
||||
- `OrderDuplicateCheck` record - Duplicate detection data
|
||||
- `CircuitBreakerState` record - Circuit breaker status
|
||||
- `RoutingStrategy` enum - Direct/Smart/Fallback
|
||||
|
||||
---
|
||||
|
||||
### Task D2: Create DuplicateOrderDetector.cs
|
||||
**Location:** `src/NT8.Core/Execution/DuplicateOrderDetector.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Detect duplicate order submissions
|
||||
- Time-based duplicate window (5 seconds)
|
||||
- Symbol + side + quantity matching
|
||||
- Prevent accidental double-entry
|
||||
|
||||
**Detection Logic:**
|
||||
```csharp
|
||||
IsDuplicate =
|
||||
Same symbol AND
|
||||
Same side AND
|
||||
Same quantity AND
|
||||
Within 5 seconds
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public bool IsDuplicateOrder(OrderRequest request);
|
||||
public void RecordOrderIntent(OrderRequest request);
|
||||
public void ClearOldIntents(TimeSpan maxAge);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task D3: Create ExecutionCircuitBreaker.cs
|
||||
**Location:** `src/NT8.Core/Execution/ExecutionCircuitBreaker.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Monitor execution latency
|
||||
- Detect slow execution patterns
|
||||
- Circuit breaker triggers
|
||||
- Automatic recovery
|
||||
|
||||
**Circuit Breaker States:**
|
||||
- **Closed** (normal): Orders flow normally
|
||||
- **Open** (triggered): Block new orders
|
||||
- **Half-Open** (testing): Allow limited orders
|
||||
|
||||
**Trigger Conditions:**
|
||||
- Average execution time > 5 seconds (3 consecutive)
|
||||
- Order rejection rate > 50% (last 10 orders)
|
||||
- Manual trigger via emergency command
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public void RecordExecutionTime(TimeSpan latency);
|
||||
public void RecordOrderRejection(string reason);
|
||||
public bool ShouldAllowOrder();
|
||||
public CircuitBreakerState GetState();
|
||||
public void Reset();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task D4: Create ContractRollHandler.cs
|
||||
**Location:** `src/NT8.Core/Execution/ContractRollHandler.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Detect contract roll periods
|
||||
- Switch to new contract automatically
|
||||
- Position transfer logic
|
||||
- Roll notification
|
||||
|
||||
**Key Features:**
|
||||
- Symbol-specific roll dates
|
||||
- Front month vs back month tracking
|
||||
- Position rollover planning
|
||||
- Volume-based roll detection
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public bool IsRollPeriod(string symbol, DateTime date);
|
||||
public string GetActiveContract(string baseSymbol, DateTime date);
|
||||
public RollDecision ShouldRollPosition(string symbol, Position position);
|
||||
public void InitiateRollover(string fromContract, string toContract);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase E: Stops & Targets Framework (50 minutes)
|
||||
|
||||
### Task E1: Create StopsTargetsModels.cs
|
||||
**Location:** `src/NT8.Core/Execution/StopsTargetsModels.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- `MultiLevelTargets` record - TP1, TP2, TP3 configuration
|
||||
- `TrailingStopConfig` record - Trailing stop parameters
|
||||
- `AutoBreakevenConfig` record - Auto-breakeven rules
|
||||
- `StopType` enum - Fixed/Trailing/ATR/Chandelier
|
||||
- `TargetType` enum - Fixed/RMultiple/Percent
|
||||
|
||||
**MultiLevelTargets:**
|
||||
```csharp
|
||||
public record MultiLevelTargets(
|
||||
int TP1Ticks,
|
||||
int TP1Contracts, // How many contracts to take profit
|
||||
int? TP2Ticks,
|
||||
int? TP2Contracts,
|
||||
int? TP3Ticks,
|
||||
int? TP3Contracts
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task E2: Create TrailingStopManager.cs
|
||||
**Location:** `src/NT8.Core/Execution/TrailingStopManager.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Multiple trailing stop types
|
||||
- Dynamic stop adjustment
|
||||
- Auto-breakeven logic
|
||||
- Per-position stop tracking
|
||||
|
||||
**Trailing Stop Types:**
|
||||
|
||||
**1. Fixed Trailing:**
|
||||
```
|
||||
Trail by fixed ticks from highest high (long) or lowest low (short)
|
||||
```
|
||||
|
||||
**2. ATR Trailing:**
|
||||
```
|
||||
Trail by ATR multiplier (e.g., 2 * ATR)
|
||||
```
|
||||
|
||||
**3. Chandelier:**
|
||||
```
|
||||
Trail from highest high minus ATR * multiplier
|
||||
```
|
||||
|
||||
**4. Parabolic SAR:**
|
||||
```
|
||||
Accelerating factor-based trailing
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public void StartTrailing(string orderId, Position position, TrailingStopConfig config);
|
||||
public void UpdateTrailingStop(string orderId, double currentPrice);
|
||||
public double CalculateNewStopPrice(StopType type, Position position, double marketPrice);
|
||||
public bool ShouldMoveToBreakeven(Position position, double currentPrice);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task E3: Create MultiLevelTargetManager.cs
|
||||
**Location:** `src/NT8.Core/Execution/MultiLevelTargetManager.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Manage multiple profit targets
|
||||
- Partial position closure
|
||||
- Automatic target progression
|
||||
- Scale-out logic
|
||||
|
||||
**Scale-Out Logic:**
|
||||
```
|
||||
Entry: 5 contracts
|
||||
TP1 (8 ticks): Close 2 contracts, move stop to breakeven
|
||||
TP2 (16 ticks): Close 2 contracts, trail stop
|
||||
TP3 (32 ticks): Close 1 contract (final)
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public void SetTargets(string orderId, MultiLevelTargets targets);
|
||||
public void OnTargetHit(string orderId, int targetLevel, double price);
|
||||
public int CalculateContractsToClose(int targetLevel);
|
||||
public bool ShouldAdvanceStop(int targetLevel);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task E4: Create RMultipleCalculator.cs
|
||||
**Location:** `src/NT8.Core/Execution/RMultipleCalculator.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- R-multiple based targets
|
||||
- Risk-reward ratio calculation
|
||||
- Position sizing by R
|
||||
- P&L in R terms
|
||||
|
||||
**R-Multiple Concept:**
|
||||
```
|
||||
R = Initial Risk (Stop distance × TickValue × Contracts)
|
||||
Target = Entry ± (R × Multiple)
|
||||
|
||||
Example:
|
||||
Entry: 4200, Stop: 4192 (8 ticks)
|
||||
R = 8 ticks = $100 per contract
|
||||
1R Target: 4208 (8 ticks profit)
|
||||
2R Target: 4216 (16 ticks profit)
|
||||
3R Target: 4232 (32 ticks profit)
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public double CalculateRValue(Position position, double stopPrice, double tickValue);
|
||||
public double CalculateTargetPrice(double entryPrice, double rValue, double rMultiple, OrderSide side);
|
||||
public double CalculateRMultiple(double entryPrice, double exitPrice, double rValue);
|
||||
public MultiLevelTargets CreateRBasedTargets(double entryPrice, double stopPrice, double[] rMultiples);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase F: Comprehensive Testing (60 minutes)
|
||||
|
||||
### Task F1: LiquidityMonitorTests.cs
|
||||
**Location:** `tests/NT8.Core.Tests/MarketData/LiquidityMonitorTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- Spread tracking accuracy
|
||||
- Liquidity score calculation
|
||||
- Session-aware monitoring
|
||||
- Thread safety with concurrent updates
|
||||
- Alert thresholds
|
||||
|
||||
**Minimum:** 15 tests
|
||||
|
||||
---
|
||||
|
||||
### Task F2: ExecutionQualityTrackerTests.cs
|
||||
**Location:** `tests/NT8.Core.Tests/Execution/ExecutionQualityTrackerTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- Slippage calculation accuracy
|
||||
- Execution latency tracking
|
||||
- Quality scoring logic
|
||||
- Statistics aggregation
|
||||
- Historical data retention
|
||||
|
||||
**Minimum:** 18 tests
|
||||
|
||||
---
|
||||
|
||||
### Task F3: OrderTypeValidatorTests.cs
|
||||
**Location:** `tests/NT8.Core.Tests/OMS/OrderTypeValidatorTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- All order type validations
|
||||
- Price relationship checks
|
||||
- Market price boundary conditions
|
||||
- Invalid order rejection
|
||||
- Edge cases
|
||||
|
||||
**Minimum:** 20 tests
|
||||
|
||||
---
|
||||
|
||||
### Task F4: TrailingStopManagerTests.cs
|
||||
**Location:** `tests/NT8.Core.Tests/Execution/TrailingStopManagerTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- All trailing stop types
|
||||
- Auto-breakeven logic
|
||||
- Stop adjustment accuracy
|
||||
- Multi-position handling
|
||||
- Edge cases
|
||||
|
||||
**Minimum:** 15 tests
|
||||
|
||||
---
|
||||
|
||||
### Task F5: MultiLevelTargetManagerTests.cs
|
||||
**Location:** `tests/NT8.Core.Tests/Execution/MultiLevelTargetManagerTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- Scale-out logic
|
||||
- Partial closures
|
||||
- Stop advancement
|
||||
- Target progression
|
||||
- Edge cases
|
||||
|
||||
**Minimum:** 12 tests
|
||||
|
||||
---
|
||||
|
||||
### Task F6: IntegrationTests.cs
|
||||
**Location:** `tests/NT8.Integration.Tests/Phase3IntegrationTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- Full execution flow: Intent → Order Type → Execution → Quality Tracking
|
||||
- Multi-level targets with trailing stops
|
||||
- Circuit breaker integration
|
||||
- Duplicate order prevention
|
||||
- Complete trade lifecycle
|
||||
|
||||
**Minimum:** 10 tests
|
||||
|
||||
---
|
||||
|
||||
## Phase G: Integration & Verification (30 minutes)
|
||||
|
||||
### Task G1: Performance Benchmarks
|
||||
**Location:** `tests/NT8.Performance.Tests/Phase3PerformanceTests.cs`
|
||||
|
||||
**Benchmarks:**
|
||||
- Order type validation: <2ms
|
||||
- Execution quality calculation: <3ms
|
||||
- Liquidity score update: <1ms
|
||||
- Trailing stop update: <2ms
|
||||
- Overall execution flow: <15ms
|
||||
|
||||
---
|
||||
|
||||
### Task G2: Build Verification
|
||||
**Command:** `.\verify-build.bat`
|
||||
|
||||
**Requirements:**
|
||||
- Zero errors
|
||||
- Zero warnings for new Phase 3 code
|
||||
- All tests passing (120+ total)
|
||||
- Coverage >80%
|
||||
|
||||
---
|
||||
|
||||
### Task G3: Documentation Update
|
||||
**Files to update:**
|
||||
- Update README.md with Phase 3 features
|
||||
- Create Phase3_Completion_Report.md
|
||||
- Update API_REFERENCE.md with new interfaces
|
||||
- Add execution examples to docs
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Code Quality
|
||||
- ✅ C# 5.0 syntax only (no C# 6+)
|
||||
- ✅ Thread-safe (locks on all shared state)
|
||||
- ✅ XML docs on all public members
|
||||
- ✅ Try-catch on all public methods
|
||||
- ✅ Structured logging throughout
|
||||
|
||||
### Testing
|
||||
- ✅ >120 total tests passing
|
||||
- ✅ >80% code coverage
|
||||
- ✅ All execution scenarios tested
|
||||
- ✅ All order types tested
|
||||
- ✅ Integration tests pass
|
||||
- ✅ Performance benchmarks met
|
||||
|
||||
### Performance
|
||||
- ✅ Order validation <2ms
|
||||
- ✅ Execution tracking <3ms
|
||||
- ✅ Liquidity update <1ms
|
||||
- ✅ Overall flow <15ms
|
||||
- ✅ No memory leaks
|
||||
|
||||
### Integration
|
||||
- ✅ Works with Phase 2 risk/sizing
|
||||
- ✅ No breaking changes
|
||||
- ✅ Clean interfaces
|
||||
- ✅ Backward compatible
|
||||
|
||||
---
|
||||
|
||||
## File Creation Checklist
|
||||
|
||||
### New Files (20):
|
||||
**MarketData (3):**
|
||||
- [ ] `src/NT8.Core/MarketData/MarketMicrostructureModels.cs`
|
||||
- [ ] `src/NT8.Core/MarketData/LiquidityMonitor.cs`
|
||||
- [ ] `src/NT8.Core/MarketData/SessionManager.cs`
|
||||
|
||||
**OMS (1):**
|
||||
- [ ] `src/NT8.Core/OMS/OrderTypeValidator.cs`
|
||||
|
||||
**Execution (10):**
|
||||
- [ ] `src/NT8.Core/Execution/ExecutionModels.cs`
|
||||
- [ ] `src/NT8.Core/Execution/ExecutionQualityTracker.cs`
|
||||
- [ ] `src/NT8.Core/Execution/SlippageCalculator.cs`
|
||||
- [ ] `src/NT8.Core/Execution/OrderRoutingModels.cs`
|
||||
- [ ] `src/NT8.Core/Execution/DuplicateOrderDetector.cs`
|
||||
- [ ] `src/NT8.Core/Execution/ExecutionCircuitBreaker.cs`
|
||||
- [ ] `src/NT8.Core/Execution/ContractRollHandler.cs`
|
||||
- [ ] `src/NT8.Core/Execution/StopsTargetsModels.cs`
|
||||
- [ ] `src/NT8.Core/Execution/TrailingStopManager.cs`
|
||||
- [ ] `src/NT8.Core/Execution/MultiLevelTargetManager.cs`
|
||||
- [ ] `src/NT8.Core/Execution/RMultipleCalculator.cs`
|
||||
|
||||
**Tests (6):**
|
||||
- [ ] `tests/NT8.Core.Tests/MarketData/LiquidityMonitorTests.cs`
|
||||
- [ ] `tests/NT8.Core.Tests/Execution/ExecutionQualityTrackerTests.cs`
|
||||
- [ ] `tests/NT8.Core.Tests/OMS/OrderTypeValidatorTests.cs`
|
||||
- [ ] `tests/NT8.Core.Tests/Execution/TrailingStopManagerTests.cs`
|
||||
- [ ] `tests/NT8.Core.Tests/Execution/MultiLevelTargetManagerTests.cs`
|
||||
- [ ] `tests/NT8.Integration.Tests/Phase3IntegrationTests.cs`
|
||||
|
||||
### Updated Files (2):
|
||||
- [ ] `src/NT8.Core/OMS/OrderModels.cs` - ADD order type records
|
||||
- [ ] `src/NT8.Core/OMS/BasicOrderManager.cs` - ADD order type methods
|
||||
|
||||
**Total:** 20 new files, 2 updated files
|
||||
|
||||
---
|
||||
|
||||
## Estimated Timeline
|
||||
|
||||
| Phase | Tasks | Time | Cumulative |
|
||||
|-------|-------|------|------------|
|
||||
| **A** | Market Microstructure | 45 min | 0:45 |
|
||||
| **B** | Advanced Order Types | 60 min | 1:45 |
|
||||
| **C** | Execution Quality | 50 min | 2:35 |
|
||||
| **D** | Smart Routing | 45 min | 3:20 |
|
||||
| **E** | Stops & Targets | 50 min | 4:10 |
|
||||
| **F** | Testing | 60 min | 5:10 |
|
||||
| **G** | Verification | 30 min | 5:40 |
|
||||
|
||||
**Total:** 5-6 hours (budget 3-4 hours for Kilocode efficiency)
|
||||
|
||||
---
|
||||
|
||||
## Critical Notes
|
||||
|
||||
### Modifications to Existing Code
|
||||
**IMPORTANT:** Only these files can be modified:
|
||||
- ✅ `src/NT8.Core/OMS/OrderModels.cs` - ADD records only
|
||||
- ✅ `src/NT8.Core/OMS/BasicOrderManager.cs` - ADD methods only
|
||||
|
||||
**FORBIDDEN:**
|
||||
- ❌ Do NOT modify interfaces
|
||||
- ❌ Do NOT modify existing method signatures
|
||||
- ❌ Do NOT change Phase 1-2 behavior
|
||||
|
||||
### Thread Safety
|
||||
Every class with shared state MUST:
|
||||
```csharp
|
||||
private readonly object _lock = new object();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// All shared state access
|
||||
}
|
||||
```
|
||||
|
||||
### C# 5.0 Compliance
|
||||
**Verify after each file:**
|
||||
- No `$"string {interpolation}"`
|
||||
- No `?.` or `?[` operators
|
||||
- No `=>` expression bodies
|
||||
- No inline out variables
|
||||
- Use `string.Format()` for formatting
|
||||
|
||||
---
|
||||
|
||||
## Ready to Start?
|
||||
|
||||
**Paste into Kilocode Code Mode:**
|
||||
|
||||
```
|
||||
I'm ready to implement Phase 3: Market Microstructure & Execution.
|
||||
|
||||
I will follow Phase3_Implementation_Guide.md starting with
|
||||
Task A1: Create MarketMicrostructureModels.cs
|
||||
|
||||
Please confirm you understand:
|
||||
- C# 5.0 syntax requirements
|
||||
- File modification boundaries (MarketData/Execution/OMS only)
|
||||
- Thread safety requirements (locks on all shared state)
|
||||
- No breaking changes to existing interfaces
|
||||
- Verification after each file (Ctrl+Shift+B)
|
||||
|
||||
Let's start with creating MarketMicrostructureModels.cs in src/NT8.Core/MarketData/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Phase 3 will complete your trading core!** 🚀
|
||||
900
Phase4_Implementation_Guide.md
Normal file
900
Phase4_Implementation_Guide.md
Normal file
@@ -0,0 +1,900 @@
|
||||
# Phase 4: Intelligence & Grading - Implementation Guide
|
||||
|
||||
**Estimated Time:** 4-5 hours
|
||||
**Complexity:** High
|
||||
**Dependencies:** Phase 3 Complete ✅
|
||||
|
||||
---
|
||||
|
||||
## Implementation Overview
|
||||
|
||||
Phase 4 adds the "intelligence layer" - confluence scoring, regime detection, grade-based sizing, and risk mode automation. This transforms the system from mechanical execution to intelligent trade selection.
|
||||
|
||||
**Core Concept:** Not all trades are equal. Grade them, size accordingly, and adapt risk based on conditions.
|
||||
|
||||
---
|
||||
|
||||
## Phase A: Confluence Scoring Foundation (60 minutes)
|
||||
|
||||
### Task A1: Create ConfluenceModels.cs
|
||||
**Location:** `src/NT8.Core/Intelligence/ConfluenceModels.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- `ConfluenceFactor` record - Individual factor contribution
|
||||
- `ConfluenceScore` record - Overall trade score
|
||||
- `TradeGrade` enum - A+, A, B, C, D, F
|
||||
- `FactorType` enum - Setup, Trend, Volatility, Timing, Quality, etc.
|
||||
- `FactorWeight` record - Dynamic factor weighting
|
||||
|
||||
**ConfluenceFactor:**
|
||||
```csharp
|
||||
public record ConfluenceFactor(
|
||||
FactorType Type,
|
||||
string Name,
|
||||
double Score, // 0.0 to 1.0
|
||||
double Weight, // Importance weight
|
||||
string Reason,
|
||||
Dictionary<string, object> Details
|
||||
);
|
||||
```
|
||||
|
||||
**ConfluenceScore:**
|
||||
```csharp
|
||||
public record ConfluenceScore(
|
||||
double RawScore, // 0.0 to 1.0
|
||||
double WeightedScore, // After applying weights
|
||||
TradeGrade Grade, // A+ to F
|
||||
List<ConfluenceFactor> Factors,
|
||||
DateTime CalculatedAt,
|
||||
Dictionary<string, object> Metadata
|
||||
);
|
||||
```
|
||||
|
||||
**TradeGrade Enum:**
|
||||
```csharp
|
||||
public enum TradeGrade
|
||||
{
|
||||
APlus = 6, // 0.90+ Exceptional setup
|
||||
A = 5, // 0.80+ Strong setup
|
||||
B = 4, // 0.70+ Good setup
|
||||
C = 3, // 0.60+ Acceptable setup
|
||||
D = 2, // 0.50+ Marginal setup
|
||||
F = 1 // <0.50 Reject trade
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task A2: Create FactorCalculators.cs
|
||||
**Location:** `src/NT8.Core/Intelligence/FactorCalculators.cs`
|
||||
|
||||
**Deliverables:**
|
||||
Base interface and individual factor calculators for:
|
||||
1. **ORB Setup Validity** (0.0 - 1.0)
|
||||
2. **Trend Alignment** (0.0 - 1.0)
|
||||
3. **Volatility Regime** (0.0 - 1.0)
|
||||
4. **Time-in-Session** (0.0 - 1.0)
|
||||
5. **Recent Execution Quality** (0.0 - 1.0)
|
||||
|
||||
**Interface:**
|
||||
```csharp
|
||||
public interface IFactorCalculator
|
||||
{
|
||||
FactorType Type { get; }
|
||||
ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, MarketData data);
|
||||
}
|
||||
```
|
||||
|
||||
**Factor 1: ORB Setup Validity**
|
||||
```csharp
|
||||
Score Calculation:
|
||||
- ORB range > minimum threshold: +0.3
|
||||
- Clean breakout (no wicks): +0.2
|
||||
- Volume confirmation (>avg): +0.2
|
||||
- Time since ORB complete < 2 hrs: +0.3
|
||||
|
||||
Max Score: 1.0
|
||||
```
|
||||
|
||||
**Factor 2: Trend Alignment**
|
||||
```csharp
|
||||
Score Calculation:
|
||||
- Price above AVWAP (long): +0.4
|
||||
- AVWAP slope aligned: +0.3
|
||||
- Recent bars confirm trend: +0.3
|
||||
|
||||
Max Score: 1.0
|
||||
```
|
||||
|
||||
**Factor 3: Volatility Regime**
|
||||
```csharp
|
||||
Score Calculation:
|
||||
Normal volatility (0.8-1.2x avg): 1.0
|
||||
Low volatility (<0.8x): 0.7
|
||||
High volatility (>1.2x): 0.5
|
||||
Extreme volatility (>1.5x): 0.3
|
||||
```
|
||||
|
||||
**Factor 4: Time-in-Session**
|
||||
```csharp
|
||||
Score Calculation:
|
||||
First 2 hours (9:30-11:30): 1.0
|
||||
Mid-day (11:30-14:00): 0.6
|
||||
Last hour (15:00-16:00): 0.8
|
||||
After hours: 0.3
|
||||
```
|
||||
|
||||
**Factor 5: Recent Execution Quality**
|
||||
```csharp
|
||||
Score Calculation:
|
||||
Last 10 trades avg quality:
|
||||
- Excellent: 1.0
|
||||
- Good: 0.8
|
||||
- Fair: 0.6
|
||||
- Poor: 0.4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task A3: Create ConfluenceScorer.cs
|
||||
**Location:** `src/NT8.Core/Intelligence/ConfluenceScorer.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Calculate overall confluence score
|
||||
- Aggregate factor scores with weights
|
||||
- Map raw score to trade grade
|
||||
- Thread-safe scoring
|
||||
- Detailed score breakdown
|
||||
|
||||
**Key Features:**
|
||||
- Configurable factor weights
|
||||
- Dynamic weight adjustment
|
||||
- Score history tracking
|
||||
- Performance analytics
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public ConfluenceScore CalculateScore(
|
||||
StrategyIntent intent,
|
||||
StrategyContext context,
|
||||
List<IFactorCalculator> factors);
|
||||
|
||||
public TradeGrade MapScoreToGrade(double weightedScore);
|
||||
public void UpdateFactorWeights(Dictionary<FactorType, double> weights);
|
||||
public ConfluenceStatistics GetHistoricalStats();
|
||||
```
|
||||
|
||||
**Scoring Algorithm:**
|
||||
```csharp
|
||||
WeightedScore = Sum(Factor.Score × Factor.Weight) / Sum(Weights)
|
||||
|
||||
Grade Mapping:
|
||||
0.90+ → A+ (Exceptional)
|
||||
0.80+ → A (Strong)
|
||||
0.70+ → B (Good)
|
||||
0.60+ → C (Acceptable)
|
||||
0.50+ → D (Marginal)
|
||||
<0.50 → F (Reject)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase B: Regime Detection (60 minutes)
|
||||
|
||||
### Task B1: Create RegimeModels.cs
|
||||
**Location:** `src/NT8.Core/Intelligence/RegimeModels.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- `VolatilityRegime` enum - Low/Normal/High/Extreme
|
||||
- `TrendRegime` enum - StrongUp/WeakUp/Range/WeakDown/StrongDown
|
||||
- `RegimeState` record - Current market regime
|
||||
- `RegimeTransition` record - Regime change event
|
||||
- `RegimeHistory` record - Historical regime tracking
|
||||
|
||||
**RegimeState:**
|
||||
```csharp
|
||||
public record RegimeState(
|
||||
string Symbol,
|
||||
VolatilityRegime VolatilityRegime,
|
||||
TrendRegime TrendRegime,
|
||||
double VolatilityScore, // Current volatility vs normal
|
||||
double TrendStrength, // -1.0 to +1.0
|
||||
DateTime LastUpdate,
|
||||
TimeSpan RegimeDuration, // How long in current regime
|
||||
Dictionary<string, object> Indicators
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task B2: Create VolatilityRegimeDetector.cs
|
||||
**Location:** `src/NT8.Core/Intelligence/VolatilityRegimeDetector.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Calculate current volatility vs historical
|
||||
- Classify into regime (Low/Normal/High/Extreme)
|
||||
- Detect regime transitions
|
||||
- Track regime duration
|
||||
- Alert on regime changes
|
||||
|
||||
**Volatility Calculation:**
|
||||
```csharp
|
||||
Current ATR vs 20-day Average ATR:
|
||||
< 0.6x → Low (expansion likely)
|
||||
0.6-0.8x → Below Normal
|
||||
0.8-1.2x → Normal
|
||||
1.2-1.5x → Elevated
|
||||
1.5-2.0x → High
|
||||
> 2.0x → Extreme (reduce size)
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public VolatilityRegime DetectRegime(string symbol, double currentATR, double normalATR);
|
||||
public bool IsRegimeTransition(VolatilityRegime current, VolatilityRegime previous);
|
||||
public double CalculateVolatilityScore(double currentATR, double normalATR);
|
||||
public void UpdateRegimeHistory(string symbol, VolatilityRegime regime);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task B3: Create TrendRegimeDetector.cs
|
||||
**Location:** `src/NT8.Core/Intelligence/TrendRegimeDetector.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Detect trend direction and strength
|
||||
- Identify ranging vs trending markets
|
||||
- Calculate trend persistence
|
||||
- Measure trend quality
|
||||
|
||||
**Trend Detection:**
|
||||
```csharp
|
||||
Using Price vs AVWAP + Slope:
|
||||
|
||||
Strong Uptrend:
|
||||
- Price > AVWAP
|
||||
- AVWAP slope > threshold
|
||||
- Higher highs, higher lows
|
||||
- Score: +0.8 to +1.0
|
||||
|
||||
Weak Uptrend:
|
||||
- Price > AVWAP
|
||||
- AVWAP slope positive but weak
|
||||
- Score: +0.3 to +0.7
|
||||
|
||||
Range:
|
||||
- Price oscillating around AVWAP
|
||||
- Low slope
|
||||
- Score: -0.2 to +0.2
|
||||
|
||||
Weak Downtrend:
|
||||
- Price < AVWAP
|
||||
- AVWAP slope negative but weak
|
||||
- Score: -0.7 to -0.3
|
||||
|
||||
Strong Downtrend:
|
||||
- Price < AVWAP
|
||||
- AVWAP slope < threshold
|
||||
- Lower highs, lower lows
|
||||
- Score: -1.0 to -0.8
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public TrendRegime DetectTrend(string symbol, List<BarData> bars, double avwap);
|
||||
public double CalculateTrendStrength(List<BarData> bars, double avwap);
|
||||
public bool IsRanging(List<BarData> bars, double threshold);
|
||||
public TrendQuality AssessTrendQuality(List<BarData> bars);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task B4: Create RegimeManager.cs
|
||||
**Location:** `src/NT8.Core/Intelligence/RegimeManager.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Coordinate volatility and trend detection
|
||||
- Maintain current regime state per symbol
|
||||
- Track regime transitions
|
||||
- Provide regime-based recommendations
|
||||
- Thread-safe regime tracking
|
||||
|
||||
**Key Features:**
|
||||
- Real-time regime updates
|
||||
- Regime change notifications
|
||||
- Historical regime tracking
|
||||
- Performance by regime
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public void UpdateRegime(string symbol, BarData bar, double avwap, double atr, double normalATR);
|
||||
public RegimeState GetCurrentRegime(string symbol);
|
||||
public bool ShouldAdjustStrategy(string symbol, StrategyIntent intent);
|
||||
public List<RegimeTransition> GetRecentTransitions(string symbol, TimeSpan period);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase C: Risk Mode Framework (60 minutes)
|
||||
|
||||
### Task C1: Create RiskModeModels.cs
|
||||
**Location:** `src/NT8.Core/Intelligence/RiskModeModels.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- `RiskMode` enum - ECP/PCP/DCP/HR
|
||||
- `RiskModeConfig` record - Mode-specific settings
|
||||
- `ModeTransitionRule` record - Transition conditions
|
||||
- `RiskModeState` record - Current mode state
|
||||
|
||||
**RiskMode Enum:**
|
||||
```csharp
|
||||
public enum RiskMode
|
||||
{
|
||||
HR = 0, // High Risk - minimal exposure
|
||||
DCP = 1, // Diminished Confidence Play
|
||||
PCP = 2, // Primary Confidence Play (default)
|
||||
ECP = 3 // Elevated Confidence Play
|
||||
}
|
||||
```
|
||||
|
||||
**RiskModeConfig:**
|
||||
```csharp
|
||||
public record RiskModeConfig(
|
||||
RiskMode Mode,
|
||||
double SizeMultiplier, // Position size adjustment
|
||||
TradeGrade MinimumGrade, // Minimum grade to trade
|
||||
double MaxDailyRisk, // Daily risk cap
|
||||
int MaxConcurrentTrades, // Max open positions
|
||||
bool AggressiveEntries, // Allow aggressive entries
|
||||
Dictionary<string, object> CustomSettings
|
||||
);
|
||||
|
||||
Example Configs:
|
||||
ECP: 1.5x size, B+ minimum, aggressive entries
|
||||
PCP: 1.0x size, B minimum, normal entries
|
||||
DCP: 0.5x size, A- minimum, conservative only
|
||||
HR: 0.0x size, reject all trades
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task C2: Create RiskModeManager.cs
|
||||
**Location:** `src/NT8.Core/Intelligence/RiskModeManager.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Manage current risk mode
|
||||
- Automatic mode transitions based on P&L
|
||||
- Manual mode override capability
|
||||
- Mode-specific trading rules
|
||||
- Thread-safe mode management
|
||||
|
||||
**Mode Transition Logic:**
|
||||
```csharp
|
||||
Start of Day: PCP (Primary Confidence Play)
|
||||
|
||||
Transition to ECP:
|
||||
- Daily P&L > +$500 (or 5R)
|
||||
- Last 5 trades: 80%+ win rate
|
||||
- No recent drawdowns
|
||||
|
||||
Transition to DCP:
|
||||
- Daily P&L < -$200 (or -2R)
|
||||
- Last 5 trades: <50% win rate
|
||||
- Recent execution quality declining
|
||||
|
||||
Transition to HR:
|
||||
- Daily loss limit approached (80%)
|
||||
- 3+ consecutive losses
|
||||
- Extreme volatility regime
|
||||
- Manual override
|
||||
|
||||
Recovery from DCP to PCP:
|
||||
- 2+ winning trades in a row
|
||||
- Execution quality improved
|
||||
- Volatility normalized
|
||||
|
||||
Recovery from HR to DCP:
|
||||
- Next trading day (automatic reset)
|
||||
- Manual override after review
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public void UpdateRiskMode(double dailyPnL, int winStreak, int lossStreak);
|
||||
public RiskMode GetCurrentMode();
|
||||
public RiskModeConfig GetModeConfig(RiskMode mode);
|
||||
public bool ShouldTransitionMode(RiskMode current, PerformanceMetrics metrics);
|
||||
public void OverrideMode(RiskMode mode, string reason);
|
||||
public void ResetToDefault();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task C3: Create GradeFilter.cs
|
||||
**Location:** `src/NT8.Core/Intelligence/GradeFilter.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Filter trades by grade based on risk mode
|
||||
- Grade-based position sizing multipliers
|
||||
- Risk mode gating logic
|
||||
- Trade rejection reasons
|
||||
|
||||
**Grade Filtering Rules:**
|
||||
```csharp
|
||||
ECP Mode (Elevated Confidence):
|
||||
- Accept: A+, A, B+, B
|
||||
- Size: A+ = 1.5x, A = 1.25x, B+ = 1.1x, B = 1.0x
|
||||
- Reject: C and below
|
||||
|
||||
PCP Mode (Primary Confidence):
|
||||
- Accept: A+, A, B+, B, C+
|
||||
- Size: A+ = 1.25x, A = 1.1x, B = 1.0x, C+ = 0.9x
|
||||
- Reject: C and below
|
||||
|
||||
DCP Mode (Diminished Confidence):
|
||||
- Accept: A+, A only
|
||||
- Size: A+ = 0.75x, A = 0.5x
|
||||
- Reject: B+ and below
|
||||
|
||||
HR Mode (High Risk):
|
||||
- Accept: None
|
||||
- Reject: All trades
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public bool ShouldAcceptTrade(TradeGrade grade, RiskMode mode);
|
||||
public double GetSizeMultiplier(TradeGrade grade, RiskMode mode);
|
||||
public string GetRejectionReason(TradeGrade grade, RiskMode mode);
|
||||
public TradeGrade GetMinimumGrade(RiskMode mode);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase D: Grade-Based Sizing Integration (45 minutes)
|
||||
|
||||
### Task D1: Create GradeBasedSizer.cs
|
||||
**Location:** `src/NT8.Core/Sizing/GradeBasedSizer.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Integrate confluence score with position sizing
|
||||
- Apply grade-based multipliers
|
||||
- Combine with risk mode adjustments
|
||||
- Override existing sizing with grade awareness
|
||||
|
||||
**Sizing Flow:**
|
||||
```csharp
|
||||
1. Base Size (from Phase 2 sizer):
|
||||
BaseContracts = FixedRisk OR OptimalF OR VolatilityAdjusted
|
||||
|
||||
2. Grade Multiplier (from confluence score):
|
||||
GradeMultiplier = GetSizeMultiplier(grade, riskMode)
|
||||
|
||||
3. Risk Mode Multiplier:
|
||||
ModeMultiplier = riskModeConfig.SizeMultiplier
|
||||
|
||||
4. Final Size:
|
||||
FinalContracts = BaseContracts × GradeMultiplier × ModeMultiplier
|
||||
FinalContracts = Clamp(FinalContracts, MinContracts, MaxContracts)
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public SizingResult CalculateGradeBasedSize(
|
||||
StrategyIntent intent,
|
||||
StrategyContext context,
|
||||
ConfluenceScore confluenceScore,
|
||||
RiskMode riskMode,
|
||||
SizingConfig baseConfig);
|
||||
|
||||
public double CombineMultipliers(double gradeMultiplier, double modeMultiplier);
|
||||
public int ApplyConstraints(int calculatedSize, int min, int max);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task D2: Update AdvancedPositionSizer.cs
|
||||
**Location:** `src/NT8.Core/Sizing/AdvancedPositionSizer.cs`
|
||||
|
||||
**Add method (don't modify existing):**
|
||||
```csharp
|
||||
public SizingResult CalculateSizeWithGrade(
|
||||
StrategyIntent intent,
|
||||
StrategyContext context,
|
||||
SizingConfig config,
|
||||
ConfluenceScore confluenceScore,
|
||||
RiskMode riskMode);
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
- Call existing CalculateSize() for base sizing
|
||||
- Apply grade-based multiplier
|
||||
- Apply risk mode multiplier
|
||||
- Return enhanced SizingResult with metadata
|
||||
|
||||
---
|
||||
|
||||
## Phase E: Strategy Enhancement (60 minutes)
|
||||
|
||||
### Task E1: Create AVWAPCalculator.cs
|
||||
**Location:** `src/NT8.Core/Indicators/AVWAPCalculator.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Anchored VWAP calculation
|
||||
- Anchor points (day start, week start, custom)
|
||||
- Rolling VWAP updates
|
||||
- Slope calculation
|
||||
|
||||
**AVWAP Formula:**
|
||||
```csharp
|
||||
VWAP = Sum(Price × Volume) / Sum(Volume)
|
||||
|
||||
Anchored: Reset accumulation at anchor point
|
||||
- Day: Reset at 9:30 AM each day
|
||||
- Week: Reset Monday 9:30 AM
|
||||
- Custom: User-specified time
|
||||
|
||||
Slope = (Current VWAP - VWAP 10 bars ago) / 10
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public double Calculate(List<BarData> bars, DateTime anchorTime);
|
||||
public void Update(double price, long volume);
|
||||
public double GetSlope(int lookback);
|
||||
public void ResetAnchor(DateTime newAnchor);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task E2: Create VolumeProfileAnalyzer.cs
|
||||
**Location:** `src/NT8.Core/Indicators/VolumeProfileAnalyzer.cs`
|
||||
|
||||
**Deliverables:**
|
||||
- Volume by price level (VPOC)
|
||||
- High volume nodes
|
||||
- Low volume nodes (gaps)
|
||||
- Value area calculation
|
||||
|
||||
**Volume Profile Concepts:**
|
||||
```csharp
|
||||
VPOC (Volume Point of Control):
|
||||
- Price level with highest volume
|
||||
- Acts as magnet for price
|
||||
|
||||
High Volume Nodes:
|
||||
- Volume > 1.5x average
|
||||
- Support/resistance levels
|
||||
|
||||
Low Volume Nodes:
|
||||
- Volume < 0.5x average
|
||||
- Price gaps quickly through
|
||||
|
||||
Value Area:
|
||||
- 70% of volume traded
|
||||
- Fair value range
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
```csharp
|
||||
public double GetVPOC(List<BarData> bars);
|
||||
public List<double> GetHighVolumeNodes(List<BarData> bars);
|
||||
public List<double> GetLowVolumeNodes(List<BarData> bars);
|
||||
public ValueArea CalculateValueArea(List<BarData> bars);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task E3: Enhance SimpleORBStrategy
|
||||
**Location:** `src/NT8.Strategies/Examples/SimpleORBStrategy.cs`
|
||||
|
||||
**Add confluence awareness (don't break existing):**
|
||||
- Calculate AVWAP filter
|
||||
- Check volume profile
|
||||
- Use confluence scorer
|
||||
- Apply grade-based sizing
|
||||
|
||||
**Enhanced OnBar Logic:**
|
||||
```csharp
|
||||
public StrategyIntent? OnBar(BarData bar, StrategyContext context)
|
||||
{
|
||||
// Existing ORB logic...
|
||||
if (breakoutDetected)
|
||||
{
|
||||
// NEW: Add confluence factors
|
||||
var factors = new List<ConfluenceFactor>
|
||||
{
|
||||
CalculateORBValidity(),
|
||||
CalculateTrendAlignment(bar, avwap),
|
||||
CalculateVolatilityRegime(),
|
||||
CalculateTimingFactor(bar.Time),
|
||||
CalculateExecutionQuality()
|
||||
};
|
||||
|
||||
var confluenceScore = _scorer.CalculateScore(intent, context, factors);
|
||||
|
||||
// Check grade filter
|
||||
if (!_gradeFilter.ShouldAcceptTrade(confluenceScore.Grade, currentRiskMode))
|
||||
{
|
||||
_logger.LogInfo("Trade rejected: Grade {0}, Mode {1}",
|
||||
confluenceScore.Grade, currentRiskMode);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add confluence metadata to intent
|
||||
intent.Metadata["confluence_score"] = confluenceScore;
|
||||
intent.Confidence = confluenceScore.WeightedScore;
|
||||
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase F: Comprehensive Testing (75 minutes)
|
||||
|
||||
### Task F1: ConfluenceScorerTests.cs
|
||||
**Location:** `tests/NT8.Core.Tests/Intelligence/ConfluenceScorerTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- Individual factor calculations
|
||||
- Score aggregation logic
|
||||
- Grade mapping accuracy
|
||||
- Weight application
|
||||
- Edge cases (all factors 0.0, all factors 1.0)
|
||||
- Thread safety
|
||||
|
||||
**Minimum:** 20 tests
|
||||
|
||||
---
|
||||
|
||||
### Task F2: RegimeDetectionTests.cs
|
||||
**Location:** `tests/NT8.Core.Tests/Intelligence/RegimeDetectionTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- Volatility regime classification
|
||||
- Trend regime detection
|
||||
- Regime transitions
|
||||
- Historical regime tracking
|
||||
- Regime duration calculation
|
||||
|
||||
**Minimum:** 18 tests
|
||||
|
||||
---
|
||||
|
||||
### Task F3: RiskModeManagerTests.cs
|
||||
**Location:** `tests/NT8.Core.Tests/Intelligence/RiskModeManagerTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- Mode transitions (PCP→ECP, PCP→DCP, DCP→HR)
|
||||
- Automatic mode updates
|
||||
- Manual overrides
|
||||
- Grade filtering by mode
|
||||
- Size multipliers by mode
|
||||
|
||||
**Minimum:** 15 tests
|
||||
|
||||
---
|
||||
|
||||
### Task F4: GradeBasedSizerTests.cs
|
||||
**Location:** `tests/NT8.Core.Tests/Sizing/GradeBasedSizerTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- Base size calculation
|
||||
- Grade multiplier application
|
||||
- Risk mode multiplier
|
||||
- Combined multipliers
|
||||
- Constraint application
|
||||
|
||||
**Minimum:** 12 tests
|
||||
|
||||
---
|
||||
|
||||
### Task F5: AVWAPCalculatorTests.cs
|
||||
**Location:** `tests/NT8.Core.Tests/Indicators/AVWAPCalculatorTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- AVWAP calculation accuracy
|
||||
- Anchor point handling
|
||||
- Slope calculation
|
||||
- Rolling updates
|
||||
|
||||
**Minimum:** 10 tests
|
||||
|
||||
---
|
||||
|
||||
### Task F6: Phase4IntegrationTests.cs
|
||||
**Location:** `tests/NT8.Integration.Tests/Phase4IntegrationTests.cs`
|
||||
|
||||
**Test Coverage:**
|
||||
- Full flow: Intent → Confluence → Grade → Mode Filter → Sizing
|
||||
- Grade-based rejection scenarios
|
||||
- Risk mode transitions during trading
|
||||
- Enhanced strategy execution
|
||||
- Regime-aware trading
|
||||
|
||||
**Minimum:** 12 tests
|
||||
|
||||
---
|
||||
|
||||
## Phase G: Integration & Verification (45 minutes)
|
||||
|
||||
### Task G1: Performance Benchmarks
|
||||
**Location:** `tests/NT8.Performance.Tests/Phase4PerformanceTests.cs`
|
||||
|
||||
**Benchmarks:**
|
||||
- Confluence score calculation: <5ms
|
||||
- Regime detection: <3ms
|
||||
- Grade filtering: <1ms
|
||||
- Risk mode update: <2ms
|
||||
- Overall intelligence flow: <15ms
|
||||
|
||||
---
|
||||
|
||||
### Task G2: Build Verification
|
||||
**Command:** `.\verify-build.bat`
|
||||
|
||||
**Requirements:**
|
||||
- Zero errors
|
||||
- Zero warnings for new Phase 4 code
|
||||
- All tests passing (150+ total)
|
||||
- Coverage >80%
|
||||
|
||||
---
|
||||
|
||||
### Task G3: Documentation
|
||||
**Files to update:**
|
||||
- Create Phase4_Completion_Report.md
|
||||
- Update API_REFERENCE.md with intelligence interfaces
|
||||
- Add confluence scoring examples
|
||||
- Document risk modes
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Code Quality
|
||||
- ✅ C# 5.0 syntax only
|
||||
- ✅ Thread-safe (locks on shared state)
|
||||
- ✅ XML docs on all public members
|
||||
- ✅ Comprehensive logging
|
||||
- ✅ No breaking changes
|
||||
|
||||
### Testing
|
||||
- ✅ >150 total tests passing
|
||||
- ✅ >80% code coverage
|
||||
- ✅ All scoring scenarios tested
|
||||
- ✅ All regime scenarios tested
|
||||
- ✅ Integration tests pass
|
||||
|
||||
### Performance
|
||||
- ✅ Confluence scoring <5ms
|
||||
- ✅ Regime detection <3ms
|
||||
- ✅ Grade filtering <1ms
|
||||
- ✅ Overall flow <15ms
|
||||
|
||||
### Integration
|
||||
- ✅ Works with Phase 2-3
|
||||
- ✅ Grade-based sizing operational
|
||||
- ✅ Risk modes functional
|
||||
- ✅ Regime detection accurate
|
||||
|
||||
---
|
||||
|
||||
## File Creation Checklist
|
||||
|
||||
### New Files (18):
|
||||
**Intelligence (9):**
|
||||
- [ ] `src/NT8.Core/Intelligence/ConfluenceModels.cs`
|
||||
- [ ] `src/NT8.Core/Intelligence/FactorCalculators.cs`
|
||||
- [ ] `src/NT8.Core/Intelligence/ConfluenceScorer.cs`
|
||||
- [ ] `src/NT8.Core/Intelligence/RegimeModels.cs`
|
||||
- [ ] `src/NT8.Core/Intelligence/VolatilityRegimeDetector.cs`
|
||||
- [ ] `src/NT8.Core/Intelligence/TrendRegimeDetector.cs`
|
||||
- [ ] `src/NT8.Core/Intelligence/RegimeManager.cs`
|
||||
- [ ] `src/NT8.Core/Intelligence/RiskModeModels.cs`
|
||||
- [ ] `src/NT8.Core/Intelligence/RiskModeManager.cs`
|
||||
- [ ] `src/NT8.Core/Intelligence/GradeFilter.cs`
|
||||
|
||||
**Sizing (1):**
|
||||
- [ ] `src/NT8.Core/Sizing/GradeBasedSizer.cs`
|
||||
|
||||
**Indicators (2):**
|
||||
- [ ] `src/NT8.Core/Indicators/AVWAPCalculator.cs`
|
||||
- [ ] `src/NT8.Core/Indicators/VolumeProfileAnalyzer.cs`
|
||||
|
||||
**Tests (6):**
|
||||
- [ ] `tests/NT8.Core.Tests/Intelligence/ConfluenceScorerTests.cs`
|
||||
- [ ] `tests/NT8.Core.Tests/Intelligence/RegimeDetectionTests.cs`
|
||||
- [ ] `tests/NT8.Core.Tests/Intelligence/RiskModeManagerTests.cs`
|
||||
- [ ] `tests/NT8.Core.Tests/Sizing/GradeBasedSizerTests.cs`
|
||||
- [ ] `tests/NT8.Core.Tests/Indicators/AVWAPCalculatorTests.cs`
|
||||
- [ ] `tests/NT8.Integration.Tests/Phase4IntegrationTests.cs`
|
||||
|
||||
### Updated Files (2):
|
||||
- [ ] `src/NT8.Core/Sizing/AdvancedPositionSizer.cs` - ADD grade-based method
|
||||
- [ ] `src/NT8.Strategies/Examples/SimpleORBStrategy.cs` - ADD confluence awareness
|
||||
|
||||
**Total:** 18 new files, 2 updated files
|
||||
|
||||
---
|
||||
|
||||
## Estimated Timeline
|
||||
|
||||
| Phase | Tasks | Time | Cumulative |
|
||||
|-------|-------|------|------------|
|
||||
| **A** | Confluence Foundation | 60 min | 1:00 |
|
||||
| **B** | Regime Detection | 60 min | 2:00 |
|
||||
| **C** | Risk Mode Framework | 60 min | 3:00 |
|
||||
| **D** | Grade-Based Sizing | 45 min | 3:45 |
|
||||
| **E** | Strategy Enhancement | 60 min | 4:45 |
|
||||
| **F** | Testing | 75 min | 6:00 |
|
||||
| **G** | Verification | 45 min | 6:45 |
|
||||
|
||||
**Total:** 6-7 hours (budget 4-5 hours for Kilocode efficiency)
|
||||
|
||||
---
|
||||
|
||||
## Critical Notes
|
||||
|
||||
### Modifications to Existing Code
|
||||
**IMPORTANT:** Only these files can be modified:
|
||||
- ✅ `src/NT8.Core/Sizing/AdvancedPositionSizer.cs` - ADD method only
|
||||
- ✅ `src/NT8.Strategies/Examples/SimpleORBStrategy.cs` - ADD features only
|
||||
|
||||
**FORBIDDEN:**
|
||||
- ❌ Do NOT modify interfaces
|
||||
- ❌ Do NOT modify Phase 1-3 core implementations
|
||||
- ❌ Do NOT change existing method signatures
|
||||
|
||||
### Thread Safety
|
||||
All intelligence classes MUST use proper locking:
|
||||
```csharp
|
||||
private readonly object _lock = new object();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// Shared state access
|
||||
}
|
||||
```
|
||||
|
||||
### C# 5.0 Compliance
|
||||
Verify after each file - same restrictions as Phase 2-3.
|
||||
|
||||
---
|
||||
|
||||
## Ready to Start?
|
||||
|
||||
**Paste into Kilocode Code Mode:**
|
||||
|
||||
```
|
||||
I'm ready to implement Phase 4: Intelligence & Grading.
|
||||
|
||||
Follow Phase4_Implementation_Guide.md starting with Phase A, Task A1.
|
||||
|
||||
CRITICAL REQUIREMENTS:
|
||||
- C# 5.0 syntax ONLY
|
||||
- Thread-safe with locks on shared state
|
||||
- XML docs on all public members
|
||||
- NO interface modifications
|
||||
- NO breaking changes to Phase 1-3
|
||||
|
||||
File Creation Permissions:
|
||||
✅ CREATE in: src/NT8.Core/Intelligence/, src/NT8.Core/Indicators/
|
||||
✅ MODIFY (ADD ONLY): AdvancedPositionSizer.cs, SimpleORBStrategy.cs
|
||||
❌ FORBIDDEN: Any interface files, Phase 1-3 core implementations
|
||||
|
||||
Start with Task A1: Create ConfluenceModels.cs in src/NT8.Core/Intelligence/
|
||||
|
||||
After each file:
|
||||
1. Build (Ctrl+Shift+B)
|
||||
2. Verify zero errors
|
||||
3. Continue to next task
|
||||
|
||||
Let's begin with ConfluenceModels.cs!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Phase 4 will make your system INTELLIGENT!** 🧠
|
||||
426
src/NT8.Core/Execution/ContractRollHandler.cs
Normal file
426
src/NT8.Core/Execution/ContractRollHandler.cs
Normal file
@@ -0,0 +1,426 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NT8.Core.MarketData;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles contract roll operations for futures and other expiring instruments
|
||||
/// </summary>
|
||||
public class ContractRollHandler
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
// Store contract roll information
|
||||
private readonly Dictionary<string, ContractRollInfo> _rollInfo;
|
||||
|
||||
// Store positions that need to be rolled
|
||||
private readonly Dictionary<string, OMS.OrderStatus> _positionsToRoll;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for ContractRollHandler
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance</param>
|
||||
public ContractRollHandler(ILogger<ContractRollHandler> logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_rollInfo = new Dictionary<string, ContractRollInfo>();
|
||||
_positionsToRoll = new Dictionary<string, OMS.OrderStatus>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if it's currently in a contract roll period
|
||||
/// </summary>
|
||||
/// <param name="symbol">Base symbol to check (e.g., ES)</param>
|
||||
/// <param name="date">Date to check</param>
|
||||
/// <returns>True if in roll period, false otherwise</returns>
|
||||
public bool IsRollPeriod(string symbol, DateTime date)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_rollInfo.ContainsKey(symbol))
|
||||
{
|
||||
var rollInfo = _rollInfo[symbol];
|
||||
var daysUntilRoll = (rollInfo.RollDate - date.Date).Days;
|
||||
|
||||
// Consider it rolling if within 5 days of roll date
|
||||
return daysUntilRoll <= 5 && daysUntilRoll >= 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to check roll period for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active contract for a base symbol on a given date
|
||||
/// </summary>
|
||||
/// <param name="baseSymbol">Base symbol (e.g., ES)</param>
|
||||
/// <param name="date">Date to get contract for</param>
|
||||
/// <returns>Active contract symbol</returns>
|
||||
public string GetActiveContract(string baseSymbol, DateTime date)
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseSymbol))
|
||||
throw new ArgumentNullException("baseSymbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_rollInfo.ContainsKey(baseSymbol))
|
||||
{
|
||||
var rollInfo = _rollInfo[baseSymbol];
|
||||
|
||||
// If we're past the roll date, return the next contract
|
||||
if (date.Date >= rollInfo.RollDate)
|
||||
{
|
||||
return rollInfo.NextContract;
|
||||
}
|
||||
else
|
||||
{
|
||||
return rollInfo.ActiveContract;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: just append date to base symbol (this would be configured externally in practice)
|
||||
return baseSymbol + date.ToString("yyMM");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get active contract for {Symbol}: {Message}", baseSymbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a position should be rolled
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol of the position</param>
|
||||
/// <param name="position">Position details</param>
|
||||
/// <returns>Roll decision</returns>
|
||||
public RollDecision ShouldRollPosition(string symbol, OMS.OrderStatus position)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (position == null)
|
||||
throw new ArgumentNullException("position");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var baseSymbol = ExtractBaseSymbol(symbol);
|
||||
|
||||
if (_rollInfo.ContainsKey(baseSymbol))
|
||||
{
|
||||
var rollInfo = _rollInfo[baseSymbol];
|
||||
var daysToRoll = rollInfo.DaysToRoll;
|
||||
|
||||
// Roll if we're within 3 days of roll date and position has quantity
|
||||
if (daysToRoll <= 3 && position.RemainingQuantity > 0)
|
||||
{
|
||||
return new RollDecision(
|
||||
true,
|
||||
String.Format("Roll needed in {0} days", daysToRoll),
|
||||
RollReason.ImminentExpiration
|
||||
);
|
||||
}
|
||||
else if (daysToRoll <= 7 && position.RemainingQuantity > 0)
|
||||
{
|
||||
return new RollDecision(
|
||||
true,
|
||||
String.Format("Roll recommended in {0} days", daysToRoll),
|
||||
RollReason.ApproachingExpiration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new RollDecision(
|
||||
false,
|
||||
"No roll needed",
|
||||
RollReason.None
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to determine roll decision for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a contract rollover from one contract to another
|
||||
/// </summary>
|
||||
/// <param name="fromContract">Contract to roll from</param>
|
||||
/// <param name="toContract">Contract to roll to</param>
|
||||
public void InitiateRollover(string fromContract, string toContract)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fromContract))
|
||||
throw new ArgumentNullException("fromContract");
|
||||
if (string.IsNullOrEmpty(toContract))
|
||||
throw new ArgumentNullException("toContract");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// Find positions in the from contract that need to be rolled
|
||||
var positionsToClose = new List<OMS.OrderStatus>();
|
||||
foreach (var kvp in _positionsToRoll)
|
||||
{
|
||||
if (kvp.Value.Symbol == fromContract && kvp.Value.State == OMS.OrderState.Working)
|
||||
{
|
||||
positionsToClose.Add(kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Close positions in old contract
|
||||
foreach (var position in positionsToClose)
|
||||
{
|
||||
// In a real implementation, this would submit close orders for the old contract
|
||||
_logger.LogInformation("Initiating rollover: closing position in {FromContract}, size {Size}",
|
||||
fromContract, position.RemainingQuantity);
|
||||
}
|
||||
|
||||
// In a real implementation, this would establish new positions in the toContract
|
||||
_logger.LogInformation("Rollover initiated from {FromContract} to {ToContract}",
|
||||
fromContract, toContract);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to initiate rollover from {FromContract} to {ToContract}: {Message}",
|
||||
fromContract, toContract, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets contract roll information for a symbol
|
||||
/// </summary>
|
||||
/// <param name="baseSymbol">Base symbol (e.g., ES)</param>
|
||||
/// <param name="activeContract">Current active contract (e.g., ESZ24)</param>
|
||||
/// <param name="nextContract">Next contract to roll to (e.g., ESH25)</param>
|
||||
/// <param name="rollDate">Date of the roll</param>
|
||||
public void SetRollInfo(string baseSymbol, string activeContract, string nextContract, DateTime rollDate)
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseSymbol))
|
||||
throw new ArgumentNullException("baseSymbol");
|
||||
if (string.IsNullOrEmpty(activeContract))
|
||||
throw new ArgumentNullException("activeContract");
|
||||
if (string.IsNullOrEmpty(nextContract))
|
||||
throw new ArgumentNullException("nextContract");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var daysToRoll = (rollDate.Date - DateTime.UtcNow.Date).Days;
|
||||
var isRollPeriod = daysToRoll <= 5 && daysToRoll >= 0;
|
||||
|
||||
var rollInfo = new ContractRollInfo(
|
||||
baseSymbol,
|
||||
activeContract,
|
||||
nextContract,
|
||||
rollDate,
|
||||
daysToRoll,
|
||||
isRollPeriod
|
||||
);
|
||||
|
||||
_rollInfo[baseSymbol] = rollInfo;
|
||||
|
||||
_logger.LogDebug("Set roll info for {Symbol}: {ActiveContract} -> {NextContract} on {RollDate}",
|
||||
baseSymbol, activeContract, nextContract, rollDate);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to set roll info for {Symbol}: {Message}", baseSymbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a position that should be monitored for rolling
|
||||
/// </summary>
|
||||
/// <param name="position">Position to monitor</param>
|
||||
public void MonitorPositionForRoll(OMS.OrderStatus position)
|
||||
{
|
||||
if (position == null)
|
||||
throw new ArgumentNullException("position");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var key = position.OrderId;
|
||||
_positionsToRoll[key] = position;
|
||||
|
||||
_logger.LogDebug("Added position {OrderId} for roll monitoring", position.OrderId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to monitor position for roll: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a position from roll monitoring
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID of position to remove</param>
|
||||
public void RemovePositionFromRollMonitoring(string orderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_positionsToRoll.Remove(orderId);
|
||||
|
||||
_logger.LogDebug("Removed position {OrderId} from roll monitoring", orderId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to remove position from roll monitoring: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the roll information for a symbol
|
||||
/// </summary>
|
||||
/// <param name="baseSymbol">Base symbol to get roll info for</param>
|
||||
/// <returns>Contract roll information</returns>
|
||||
public ContractRollInfo GetRollInfo(string baseSymbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseSymbol))
|
||||
throw new ArgumentNullException("baseSymbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ContractRollInfo info;
|
||||
_rollInfo.TryGetValue(baseSymbol, out info);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get roll info for {Symbol}: {Message}", baseSymbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the base symbol from a contract symbol
|
||||
/// </summary>
|
||||
/// <param name="contractSymbol">Full contract symbol (e.g., ESZ24)</param>
|
||||
/// <returns>Base symbol (e.g., ES)</returns>
|
||||
private string ExtractBaseSymbol(string contractSymbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contractSymbol))
|
||||
return string.Empty;
|
||||
|
||||
// For now, extract letters from the beginning
|
||||
// In practice, this would be more sophisticated
|
||||
var baseSymbol = "";
|
||||
foreach (char c in contractSymbol)
|
||||
{
|
||||
if (char.IsLetter(c))
|
||||
baseSymbol += c;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
return baseSymbol;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decision regarding contract rolling
|
||||
/// </summary>
|
||||
public class RollDecision
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the position should be rolled
|
||||
/// </summary>
|
||||
public bool ShouldRoll { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for the decision
|
||||
/// </summary>
|
||||
public string Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason category
|
||||
/// </summary>
|
||||
public RollReason RollReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for RollDecision
|
||||
/// </summary>
|
||||
/// <param name="shouldRoll">Whether to roll</param>
|
||||
/// <param name="reason">Reason for decision</param>
|
||||
/// <param name="rollReason">Category of reason</param>
|
||||
public RollDecision(bool shouldRoll, string reason, RollReason rollReason)
|
||||
{
|
||||
ShouldRoll = shouldRoll;
|
||||
Reason = reason;
|
||||
RollReason = rollReason;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reason for contract roll
|
||||
/// </summary>
|
||||
public enum RollReason
|
||||
{
|
||||
/// <summary>
|
||||
/// No roll needed
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Approaching expiration
|
||||
/// </summary>
|
||||
ApproachingExpiration = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Imminent expiration
|
||||
/// </summary>
|
||||
ImminentExpiration = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Better liquidity in next contract
|
||||
/// </summary>
|
||||
BetterLiquidity = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Scheduled roll
|
||||
/// </summary>
|
||||
Scheduled = 4
|
||||
}
|
||||
}
|
||||
220
src/NT8.Core/Execution/DuplicateOrderDetector.cs
Normal file
220
src/NT8.Core/Execution/DuplicateOrderDetector.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects duplicate order submissions to prevent accidental double entries
|
||||
/// </summary>
|
||||
public class DuplicateOrderDetector
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
// Store order intents with timestamps
|
||||
private readonly Dictionary<string, OrderIntentRecord> _recentIntents;
|
||||
|
||||
// Default time window for duplicate detection (5 seconds)
|
||||
private readonly TimeSpan _duplicateWindow;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for DuplicateOrderDetector
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance</param>
|
||||
/// <param name="duplicateWindow">Time window for duplicate detection</param>
|
||||
public DuplicateOrderDetector(ILogger<DuplicateOrderDetector> logger, TimeSpan? duplicateWindow = null)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_duplicateWindow = duplicateWindow ?? TimeSpan.FromSeconds(5);
|
||||
_recentIntents = new Dictionary<string, OrderIntentRecord>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an order is a duplicate of a recent order
|
||||
/// </summary>
|
||||
/// <param name="request">Order request to check</param>
|
||||
/// <returns>True if order is a duplicate, false otherwise</returns>
|
||||
public bool IsDuplicateOrder(OMS.OrderRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// Clean up old intents first
|
||||
ClearOldIntents(_duplicateWindow);
|
||||
|
||||
// Create a key based on symbol, side, and quantity
|
||||
var key = CreateOrderKey(request);
|
||||
|
||||
// Check if we have a recent order with same characteristics
|
||||
if (_recentIntents.ContainsKey(key))
|
||||
{
|
||||
var record = _recentIntents[key];
|
||||
var timeDiff = DateTime.UtcNow - record.Timestamp;
|
||||
|
||||
// If the time difference is within our window, it's a duplicate
|
||||
if (timeDiff <= _duplicateWindow)
|
||||
{
|
||||
_logger.LogDebug("Duplicate order detected: {Key} at {TimeDiff} ago", key, timeDiff);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to check for duplicate order: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an order intent for duplicate checking
|
||||
/// </summary>
|
||||
/// <param name="request">Order request to record</param>
|
||||
public void RecordOrderIntent(OMS.OrderRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var key = CreateOrderKey(request);
|
||||
var record = new OrderIntentRecord
|
||||
{
|
||||
OrderRequest = request,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_recentIntents[key] = record;
|
||||
|
||||
_logger.LogDebug("Recorded order intent: {Key}", key);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to record order intent: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears old order intents that are beyond the duplicate window
|
||||
/// </summary>
|
||||
/// <param name="maxAge">Maximum age of intents to keep</param>
|
||||
public void ClearOldIntents(TimeSpan maxAge)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var cutoffTime = DateTime.UtcNow - maxAge;
|
||||
var keysToRemove = _recentIntents
|
||||
.Where(kvp => kvp.Value.Timestamp < cutoffTime)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_recentIntents.Remove(key);
|
||||
}
|
||||
|
||||
if (keysToRemove.Any())
|
||||
{
|
||||
_logger.LogDebug("Cleared {Count} old order intents", keysToRemove.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to clear old order intents: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a unique key for an order based on symbol, side, and quantity
|
||||
/// </summary>
|
||||
/// <param name="request">Order request to create key for</param>
|
||||
/// <returns>Unique key for the order</returns>
|
||||
private string CreateOrderKey(OMS.OrderRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
return string.Empty;
|
||||
|
||||
var symbol = request.Symbol != null ? request.Symbol.ToLower() : string.Empty;
|
||||
return string.Format("{0}_{1}_{2}",
|
||||
symbol,
|
||||
request.Side,
|
||||
request.Quantity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of recent order intents
|
||||
/// </summary>
|
||||
/// <returns>Number of recent order intents</returns>
|
||||
public int GetRecentIntentCount()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _recentIntents.Count;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get recent intent count: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all recorded order intents
|
||||
/// </summary>
|
||||
public void ClearAllIntents()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_recentIntents.Clear();
|
||||
_logger.LogDebug("Cleared all order intents");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to clear all order intents: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record of an order intent with timestamp
|
||||
/// </summary>
|
||||
internal class OrderIntentRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// The order request that was intended
|
||||
/// </summary>
|
||||
public OMS.OrderRequest OrderRequest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the intent was recorded
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
396
src/NT8.Core/Execution/ExecutionCircuitBreaker.cs
Normal file
396
src/NT8.Core/Execution/ExecutionCircuitBreaker.cs
Normal file
@@ -0,0 +1,396 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Circuit breaker implementation for execution systems to prevent cascading failures
|
||||
/// </summary>
|
||||
public class ExecutionCircuitBreaker
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private CircuitBreakerStatus _status;
|
||||
private DateTime _lastFailureTime;
|
||||
private int _failureCount;
|
||||
private DateTime _nextRetryTime;
|
||||
private readonly TimeSpan _timeout;
|
||||
private readonly int _failureThreshold;
|
||||
private readonly TimeSpan _retryTimeout;
|
||||
|
||||
// Track execution times for latency monitoring
|
||||
private readonly Queue<TimeSpan> _executionTimes;
|
||||
private readonly int _latencyWindowSize;
|
||||
|
||||
// Track order rejections
|
||||
private readonly Queue<DateTime> _rejectionTimes;
|
||||
private readonly int _rejectionWindowSize;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for ExecutionCircuitBreaker
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance</param>
|
||||
/// <param name="failureThreshold">Number of failures to trigger circuit breaker</param>
|
||||
/// <param name="timeout">How long to stay open before half-open</param>
|
||||
/// <param name="retryTimeout">Time to wait between retries</param>
|
||||
/// <param name="latencyWindowSize">Size of latency tracking window</param>
|
||||
/// <param name="rejectionWindowSize">Size of rejection tracking window</param>
|
||||
public ExecutionCircuitBreaker(
|
||||
ILogger<ExecutionCircuitBreaker> logger,
|
||||
int failureThreshold = 3,
|
||||
TimeSpan? timeout = null,
|
||||
TimeSpan? retryTimeout = null,
|
||||
int latencyWindowSize = 100,
|
||||
int rejectionWindowSize = 10)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_status = CircuitBreakerStatus.Closed;
|
||||
_failureCount = 0;
|
||||
_lastFailureTime = DateTime.MinValue;
|
||||
_timeout = timeout ?? TimeSpan.FromSeconds(30);
|
||||
_retryTimeout = retryTimeout ?? TimeSpan.FromSeconds(5);
|
||||
_failureThreshold = failureThreshold;
|
||||
_latencyWindowSize = latencyWindowSize;
|
||||
_rejectionWindowSize = rejectionWindowSize;
|
||||
|
||||
_executionTimes = new Queue<TimeSpan>();
|
||||
_rejectionTimes = new Queue<DateTime>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records execution time for monitoring
|
||||
/// </summary>
|
||||
/// <param name="latency">Execution latency</param>
|
||||
public void RecordExecutionTime(TimeSpan latency)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_executionTimes.Enqueue(latency);
|
||||
|
||||
// Keep only the last N measurements
|
||||
while (_executionTimes.Count > _latencyWindowSize)
|
||||
{
|
||||
_executionTimes.Dequeue();
|
||||
}
|
||||
|
||||
// Check if we have excessive latency
|
||||
if (_status == CircuitBreakerStatus.Closed && HasExcessiveLatency())
|
||||
{
|
||||
TripCircuitBreaker("Excessive execution latency detected");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to record execution time: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records order rejection for monitoring
|
||||
/// </summary>
|
||||
/// <param name="reason">Reason for rejection</param>
|
||||
public void RecordOrderRejection(string reason)
|
||||
{
|
||||
if (string.IsNullOrEmpty(reason))
|
||||
reason = "Unknown";
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_rejectionTimes.Enqueue(DateTime.UtcNow);
|
||||
|
||||
// Keep only the last N rejections
|
||||
while (_rejectionTimes.Count > _rejectionWindowSize)
|
||||
{
|
||||
_rejectionTimes.Dequeue();
|
||||
}
|
||||
|
||||
// Check if we have excessive rejections
|
||||
if (_status == CircuitBreakerStatus.Closed && HasExcessiveRejections())
|
||||
{
|
||||
TripCircuitBreaker(String.Format("Excessive order rejections: {0}", reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to record order rejection: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if an order should be allowed based on circuit breaker state
|
||||
/// </summary>
|
||||
/// <returns>True if order should be allowed, false otherwise</returns>
|
||||
public bool ShouldAllowOrder()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
switch (_status)
|
||||
{
|
||||
case CircuitBreakerStatus.Closed:
|
||||
// Normal operation
|
||||
return true;
|
||||
|
||||
case CircuitBreakerStatus.Open:
|
||||
// Check if we should transition to half-open
|
||||
if (DateTime.UtcNow >= _nextRetryTime)
|
||||
{
|
||||
_status = CircuitBreakerStatus.HalfOpen;
|
||||
_logger.LogWarning("Circuit breaker transitioning to Half-Open state");
|
||||
return true; // Allow one test order
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Circuit breaker is Open - blocking order");
|
||||
return false; // Block orders
|
||||
}
|
||||
|
||||
case CircuitBreakerStatus.HalfOpen:
|
||||
// In half-open, allow limited operations to test if system recovered
|
||||
_logger.LogDebug("Circuit breaker is Half-Open - allowing test order");
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to check if order should be allowed: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current state of the circuit breaker
|
||||
/// </summary>
|
||||
/// <returns>Current circuit breaker state</returns>
|
||||
public CircuitBreakerState GetState()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return new CircuitBreakerState(
|
||||
_status != CircuitBreakerStatus.Closed,
|
||||
_status,
|
||||
GetStatusReason(),
|
||||
_failureCount
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get circuit breaker state: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the circuit breaker to closed state
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_status = CircuitBreakerStatus.Closed;
|
||||
_failureCount = 0;
|
||||
_lastFailureTime = DateTime.MinValue;
|
||||
|
||||
_logger.LogInformation("Circuit breaker reset to Closed state");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to reset circuit breaker: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an operation succeeds while in Half-Open state
|
||||
/// </summary>
|
||||
public void OnSuccess()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_status == CircuitBreakerStatus.HalfOpen)
|
||||
{
|
||||
Reset();
|
||||
_logger.LogInformation("Circuit breaker reset after successful test operation");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to handle success in Half-Open state: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an operation fails
|
||||
/// </summary>
|
||||
public void OnFailure()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_failureCount++;
|
||||
_lastFailureTime = DateTime.UtcNow;
|
||||
|
||||
// If we're in half-open and fail, go back to open
|
||||
if (_status == CircuitBreakerStatus.HalfOpen ||
|
||||
(_status == CircuitBreakerStatus.Closed && _failureCount >= _failureThreshold))
|
||||
{
|
||||
TripCircuitBreaker("Failure threshold exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to handle failure: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trips the circuit breaker to open state
|
||||
/// </summary>
|
||||
/// <param name="reason">Reason for tripping</param>
|
||||
private void TripCircuitBreaker(string reason)
|
||||
{
|
||||
_status = CircuitBreakerStatus.Open;
|
||||
_nextRetryTime = DateTime.UtcNow.Add(_timeout);
|
||||
|
||||
_logger.LogWarning("Circuit breaker TRIPPED: {Reason}. Will retry at {Time}",
|
||||
reason, _nextRetryTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if we have excessive execution latency
|
||||
/// </summary>
|
||||
/// <returns>True if latency is excessive</returns>
|
||||
private bool HasExcessiveLatency()
|
||||
{
|
||||
if (_executionTimes.Count < 3) // Need minimum samples
|
||||
return false;
|
||||
|
||||
// Calculate average latency
|
||||
var avgLatency = TimeSpan.FromMilliseconds(_executionTimes.Average(ts => ts.TotalMilliseconds));
|
||||
|
||||
// If average latency is more than 5 seconds, consider it excessive
|
||||
return avgLatency.TotalSeconds > 5.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if we have excessive order rejections
|
||||
/// </summary>
|
||||
/// <returns>True if rejections are excessive</returns>
|
||||
private bool HasExcessiveRejections()
|
||||
{
|
||||
if (_rejectionTimes.Count < _rejectionWindowSize)
|
||||
return false;
|
||||
|
||||
// If all recent orders were rejected (100% rejection rate in window)
|
||||
var recentWindow = TimeSpan.FromMinutes(1); // Check last minute
|
||||
var recentRejections = _rejectionTimes.Count(dt => DateTime.UtcNow - dt <= recentWindow);
|
||||
|
||||
// If we have maximum possible rejections in the window, it's excessive
|
||||
return recentRejections >= _rejectionWindowSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reason for current status
|
||||
/// </summary>
|
||||
/// <returns>Reason string</returns>
|
||||
private string GetStatusReason()
|
||||
{
|
||||
switch (_status)
|
||||
{
|
||||
case CircuitBreakerStatus.Closed:
|
||||
return "Normal operation";
|
||||
case CircuitBreakerStatus.Open:
|
||||
return String.Format("Tripped due to failures. Failures: {0}, Last: {1}",
|
||||
_failureCount, _lastFailureTime);
|
||||
case CircuitBreakerStatus.HalfOpen:
|
||||
return "Testing recovery after timeout";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets average execution time for monitoring
|
||||
/// </summary>
|
||||
/// <returns>Average execution time</returns>
|
||||
public TimeSpan GetAverageExecutionTime()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_executionTimes.Count == 0)
|
||||
return TimeSpan.Zero;
|
||||
|
||||
return TimeSpan.FromMilliseconds(_executionTimes.Average(ts => ts.TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get average execution time: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets rejection rate for monitoring
|
||||
/// </summary>
|
||||
/// <returns>Rejection rate as percentage</returns>
|
||||
public double GetRejectionRate()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_rejectionTimes.Count == 0)
|
||||
return 0.0;
|
||||
|
||||
// Calculate rejections in last minute
|
||||
var oneMinuteAgo = DateTime.UtcNow.AddMinutes(-1);
|
||||
var recentRejections = _rejectionTimes.Count(dt => dt >= oneMinuteAgo);
|
||||
|
||||
// This is a simplified calculation - in practice you'd need to track
|
||||
// total attempts to calculate accurate rate
|
||||
return (double)recentRejections / _rejectionWindowSize * 100.0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get rejection rate: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
296
src/NT8.Core/Execution/ExecutionModels.cs
Normal file
296
src/NT8.Core/Execution/ExecutionModels.cs
Normal file
@@ -0,0 +1,296 @@
|
||||
using System;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Execution metrics for a single order execution
|
||||
/// </summary>
|
||||
public class ExecutionMetrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Order ID for the executed order
|
||||
/// </summary>
|
||||
public string OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time when order intent was formed
|
||||
/// </summary>
|
||||
public DateTime IntentTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time when order was submitted to market
|
||||
/// </summary>
|
||||
public DateTime SubmitTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time when order was filled
|
||||
/// </summary>
|
||||
public DateTime FillTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Intended price when order was placed
|
||||
/// </summary>
|
||||
public decimal IntendedPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual fill price
|
||||
/// </summary>
|
||||
public decimal FillPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Price slippage (fill price - intended price)
|
||||
/// </summary>
|
||||
public decimal Slippage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of slippage (positive/negative/zero)
|
||||
/// </summary>
|
||||
public SlippageType SlippageType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time between submit and fill
|
||||
/// </summary>
|
||||
public TimeSpan SubmitLatency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time between fill and intent
|
||||
/// </summary>
|
||||
public TimeSpan FillLatency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Overall execution quality rating
|
||||
/// </summary>
|
||||
public ExecutionQuality Quality { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for ExecutionMetrics
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID</param>
|
||||
/// <param name="intentTime">Intent formation time</param>
|
||||
/// <param name="submitTime">Submission time</param>
|
||||
/// <param name="fillTime">Fill time</param>
|
||||
/// <param name="intendedPrice">Intended price</param>
|
||||
/// <param name="fillPrice">Actual fill price</param>
|
||||
/// <param name="slippage">Price slippage</param>
|
||||
/// <param name="slippageType">Type of slippage</param>
|
||||
/// <param name="submitLatency">Submission latency</param>
|
||||
/// <param name="fillLatency">Fill latency</param>
|
||||
/// <param name="quality">Execution quality</param>
|
||||
public ExecutionMetrics(
|
||||
string orderId,
|
||||
DateTime intentTime,
|
||||
DateTime submitTime,
|
||||
DateTime fillTime,
|
||||
decimal intendedPrice,
|
||||
decimal fillPrice,
|
||||
decimal slippage,
|
||||
SlippageType slippageType,
|
||||
TimeSpan submitLatency,
|
||||
TimeSpan fillLatency,
|
||||
ExecutionQuality quality)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
OrderId = orderId;
|
||||
IntentTime = intentTime;
|
||||
SubmitTime = submitTime;
|
||||
FillTime = fillTime;
|
||||
IntendedPrice = intendedPrice;
|
||||
FillPrice = fillPrice;
|
||||
Slippage = slippage;
|
||||
SlippageType = slippageType;
|
||||
SubmitLatency = submitLatency;
|
||||
FillLatency = fillLatency;
|
||||
Quality = quality;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about price slippage
|
||||
/// </summary>
|
||||
public class SlippageInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Order ID associated with the slippage
|
||||
/// </summary>
|
||||
public string OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Intended price
|
||||
/// </summary>
|
||||
public decimal IntendedPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual fill price
|
||||
/// </summary>
|
||||
public decimal ActualPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculated slippage (actual - intended)
|
||||
/// </summary>
|
||||
public decimal Slippage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Slippage expressed in ticks
|
||||
/// </summary>
|
||||
public int SlippageInTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Percentage slippage relative to intended price
|
||||
/// </summary>
|
||||
public decimal SlippagePercentage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of slippage (positive/negative/zero)
|
||||
/// </summary>
|
||||
public SlippageType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for SlippageInfo
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID</param>
|
||||
/// <param name="intendedPrice">Intended price</param>
|
||||
/// <param name="actualPrice">Actual fill price</param>
|
||||
/// <param name="slippageInTicks">Slippage in ticks</param>
|
||||
/// <param name="tickSize">Size of one tick</param>
|
||||
public SlippageInfo(
|
||||
string orderId,
|
||||
decimal intendedPrice,
|
||||
decimal actualPrice,
|
||||
int slippageInTicks,
|
||||
decimal tickSize)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
if (tickSize <= 0)
|
||||
throw new ArgumentException("Tick size must be positive", "tickSize");
|
||||
|
||||
OrderId = orderId;
|
||||
IntendedPrice = intendedPrice;
|
||||
ActualPrice = actualPrice;
|
||||
Slippage = actualPrice - intendedPrice;
|
||||
SlippageInTicks = slippageInTicks;
|
||||
SlippagePercentage = tickSize > 0 ? (Slippage / IntendedPrice) * 100 : 0;
|
||||
Type = Slippage > 0 ? SlippageType.Positive :
|
||||
Slippage < 0 ? SlippageType.Negative : SlippageType.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timing information for execution
|
||||
/// </summary>
|
||||
public class ExecutionTiming
|
||||
{
|
||||
/// <summary>
|
||||
/// Time when order was created internally
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time when order was submitted to market
|
||||
/// </summary>
|
||||
public DateTime SubmitTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time when order was acknowledged by market
|
||||
/// </summary>
|
||||
public DateTime AckTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time when order was filled
|
||||
/// </summary>
|
||||
public DateTime FillTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Latency from create to submit
|
||||
/// </summary>
|
||||
public TimeSpan CreateToSubmitLatency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Latency from submit to acknowledge
|
||||
/// </summary>
|
||||
public TimeSpan SubmitToAckLatency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Latency from acknowledge to fill
|
||||
/// </summary>
|
||||
public TimeSpan AckToFillLatency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total execution latency
|
||||
/// </summary>
|
||||
public TimeSpan TotalLatency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for ExecutionTiming
|
||||
/// </summary>
|
||||
/// <param name="createTime">Creation time</param>
|
||||
/// <param name="submitTime">Submission time</param>
|
||||
/// <param name="ackTime">Acknowledgment time</param>
|
||||
/// <param name="fillTime">Fill time</param>
|
||||
public ExecutionTiming(
|
||||
DateTime createTime,
|
||||
DateTime submitTime,
|
||||
DateTime ackTime,
|
||||
DateTime fillTime)
|
||||
{
|
||||
CreateTime = createTime;
|
||||
SubmitTime = submitTime;
|
||||
AckTime = ackTime;
|
||||
FillTime = fillTime;
|
||||
|
||||
CreateToSubmitLatency = SubmitTime - CreateTime;
|
||||
SubmitToAckLatency = AckTime - SubmitTime;
|
||||
AckToFillLatency = FillTime - AckTime;
|
||||
TotalLatency = FillTime - CreateTime;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing execution quality levels
|
||||
/// </summary>
|
||||
public enum ExecutionQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// Excellent execution with minimal slippage
|
||||
/// </summary>
|
||||
Excellent = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Good execution with acceptable slippage
|
||||
/// </summary>
|
||||
Good = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Fair execution with moderate slippage
|
||||
/// </summary>
|
||||
Fair = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Poor execution with significant slippage
|
||||
/// </summary>
|
||||
Poor = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing type of slippage
|
||||
/// </summary>
|
||||
public enum SlippageType
|
||||
{
|
||||
/// <summary>
|
||||
/// Positive slippage (better than expected)
|
||||
/// </summary>
|
||||
Positive = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Negative slippage (worse than expected)
|
||||
/// </summary>
|
||||
Negative = 1,
|
||||
|
||||
/// <summary>
|
||||
/// No slippage (as expected)
|
||||
/// </summary>
|
||||
Zero = 2
|
||||
}
|
||||
}
|
||||
437
src/NT8.Core/Execution/ExecutionQualityTracker.cs
Normal file
437
src/NT8.Core/Execution/ExecutionQualityTracker.cs
Normal file
@@ -0,0 +1,437 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks execution quality for orders and maintains statistics
|
||||
/// </summary>
|
||||
public class ExecutionQualityTracker
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
// Store execution metrics for each order
|
||||
private readonly Dictionary<string, ExecutionMetrics> _executionMetrics;
|
||||
|
||||
// Store execution history by symbol
|
||||
private readonly Dictionary<string, Queue<ExecutionMetrics>> _symbolExecutionHistory;
|
||||
|
||||
// Rolling window size for statistics
|
||||
private const int ROLLING_WINDOW_SIZE = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for ExecutionQualityTracker
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance</param>
|
||||
public ExecutionQualityTracker(ILogger<ExecutionQualityTracker> logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_executionMetrics = new Dictionary<string, ExecutionMetrics>();
|
||||
_symbolExecutionHistory = new Dictionary<string, Queue<ExecutionMetrics>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an execution for tracking
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID</param>
|
||||
/// <param name="intendedPrice">Intended price when order was placed</param>
|
||||
/// <param name="fillPrice">Actual fill price</param>
|
||||
/// <param name="fillTime">Time of fill</param>
|
||||
/// <param name="submitTime">Time of submission</param>
|
||||
/// <param name="intentTime">Time of intent formation</param>
|
||||
public void RecordExecution(
|
||||
string orderId,
|
||||
decimal intendedPrice,
|
||||
decimal fillPrice,
|
||||
DateTime fillTime,
|
||||
DateTime submitTime,
|
||||
DateTime intentTime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
var slippage = fillPrice - intendedPrice;
|
||||
var slippageType = slippage > 0 ? SlippageType.Positive :
|
||||
slippage < 0 ? SlippageType.Negative : SlippageType.Zero;
|
||||
|
||||
var submitLatency = submitTime - intentTime;
|
||||
var fillLatency = fillTime - intentTime;
|
||||
|
||||
var quality = CalculateExecutionQuality(slippage, submitLatency, fillLatency);
|
||||
|
||||
var metrics = new ExecutionMetrics(
|
||||
orderId,
|
||||
intentTime,
|
||||
submitTime,
|
||||
fillTime,
|
||||
intendedPrice,
|
||||
fillPrice,
|
||||
slippage,
|
||||
slippageType,
|
||||
submitLatency,
|
||||
fillLatency,
|
||||
quality);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_executionMetrics[orderId] = metrics;
|
||||
|
||||
// Add to symbol history
|
||||
var symbol = ExtractSymbolFromOrderId(orderId);
|
||||
if (!_symbolExecutionHistory.ContainsKey(symbol))
|
||||
{
|
||||
_symbolExecutionHistory[symbol] = new Queue<ExecutionMetrics>();
|
||||
}
|
||||
|
||||
var symbolHistory = _symbolExecutionHistory[symbol];
|
||||
symbolHistory.Enqueue(metrics);
|
||||
|
||||
// Keep only the last N executions
|
||||
while (symbolHistory.Count > ROLLING_WINDOW_SIZE)
|
||||
{
|
||||
symbolHistory.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Recorded execution for {OrderId}: Slippage={Slippage:F4}, Quality={Quality}",
|
||||
orderId, slippage, quality);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to record execution for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets execution metrics for a specific order
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID to get metrics for</param>
|
||||
/// <returns>Execution metrics for the order</returns>
|
||||
public ExecutionMetrics GetExecutionMetrics(string orderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ExecutionMetrics metrics;
|
||||
_executionMetrics.TryGetValue(orderId, out metrics);
|
||||
return metrics;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get execution metrics for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets execution statistics for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to get statistics for</param>
|
||||
/// <returns>Execution statistics for the symbol</returns>
|
||||
public ExecutionStatistics GetSymbolStatistics(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_symbolExecutionHistory.ContainsKey(symbol))
|
||||
{
|
||||
var history = _symbolExecutionHistory[symbol].ToList();
|
||||
|
||||
if (history.Count == 0)
|
||||
{
|
||||
return new ExecutionStatistics(
|
||||
symbol,
|
||||
0,
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.Zero,
|
||||
0,
|
||||
0,
|
||||
ExecutionQuality.Poor
|
||||
);
|
||||
}
|
||||
|
||||
var avgSlippage = history.Average(x => (double)x.Slippage);
|
||||
var avgSubmitLatency = TimeSpan.FromMilliseconds(history.Average(x => x.SubmitLatency.TotalMilliseconds));
|
||||
var avgFillLatency = TimeSpan.FromMilliseconds(history.Average(x => x.FillLatency.TotalMilliseconds));
|
||||
var avgQuality = history.GroupBy(x => x.Quality)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.First().Key;
|
||||
|
||||
var positiveSlippageCount = history.Count(x => x.Slippage > 0);
|
||||
var negativeSlippageCount = history.Count(x => x.Slippage < 0);
|
||||
var zeroSlippageCount = history.Count(x => x.Slippage == 0);
|
||||
|
||||
return new ExecutionStatistics(
|
||||
symbol,
|
||||
avgSlippage,
|
||||
avgSubmitLatency,
|
||||
avgFillLatency,
|
||||
positiveSlippageCount,
|
||||
negativeSlippageCount,
|
||||
avgQuality
|
||||
);
|
||||
}
|
||||
|
||||
return new ExecutionStatistics(
|
||||
symbol,
|
||||
0,
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.Zero,
|
||||
0,
|
||||
0,
|
||||
ExecutionQuality.Poor
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get execution statistics for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the average slippage for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to get average slippage for</param>
|
||||
/// <returns>Average slippage for the symbol</returns>
|
||||
public double GetAverageSlippage(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
var stats = GetSymbolStatistics(symbol);
|
||||
return stats.AverageSlippage;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get average slippage for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if execution quality for a symbol is acceptable
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to check</param>
|
||||
/// <param name="threshold">Minimum acceptable quality</param>
|
||||
/// <returns>True if execution quality is acceptable, false otherwise</returns>
|
||||
public bool IsExecutionQualityAcceptable(string symbol, ExecutionQuality threshold = ExecutionQuality.Fair)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
var stats = GetSymbolStatistics(symbol);
|
||||
return stats.AverageQuality >= threshold;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to check execution quality for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates execution quality based on slippage and latencies
|
||||
/// </summary>
|
||||
/// <param name="slippage">Price slippage</param>
|
||||
/// <param name="submitLatency">Submission latency</param>
|
||||
/// <param name="fillLatency">Fill latency</param>
|
||||
/// <returns>Calculated execution quality</returns>
|
||||
private ExecutionQuality CalculateExecutionQuality(decimal slippage, TimeSpan submitLatency, TimeSpan fillLatency)
|
||||
{
|
||||
// Determine quality based on slippage and latencies
|
||||
// Positive slippage is good, negative is bad
|
||||
// Lower latencies are better
|
||||
|
||||
// If we have positive slippage (better than expected), quality is higher
|
||||
if (slippage > 0)
|
||||
{
|
||||
// Low latency is excellent, high latency is good
|
||||
if (fillLatency.TotalMilliseconds < 100) // Less than 100ms
|
||||
return ExecutionQuality.Excellent;
|
||||
else
|
||||
return ExecutionQuality.Good;
|
||||
}
|
||||
else if (slippage == 0)
|
||||
{
|
||||
// No slippage, check latencies
|
||||
if (fillLatency.TotalMilliseconds < 100)
|
||||
return ExecutionQuality.Good;
|
||||
else
|
||||
return ExecutionQuality.Fair;
|
||||
}
|
||||
else // slippage < 0
|
||||
{
|
||||
// Negative slippage, check severity
|
||||
if (Math.Abs((double)slippage) < 0.01) // Small negative slippage
|
||||
{
|
||||
if (fillLatency.TotalMilliseconds < 100)
|
||||
return ExecutionQuality.Fair;
|
||||
else
|
||||
return ExecutionQuality.Poor;
|
||||
}
|
||||
else // Significant negative slippage
|
||||
{
|
||||
return ExecutionQuality.Poor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts symbol from order ID (assumes format SYMBOL-XXXX)
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID to extract symbol from</param>
|
||||
/// <returns>Extracted symbol</returns>
|
||||
private string ExtractSymbolFromOrderId(string orderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
return "UNKNOWN";
|
||||
|
||||
// Split by hyphen and take first part as symbol
|
||||
var parts = orderId.Split('-');
|
||||
return parts.Length > 0 ? parts[0] : "UNKNOWN";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets total number of executions tracked
|
||||
/// </summary>
|
||||
/// <returns>Total execution count</returns>
|
||||
public int GetTotalExecutionCount()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _executionMetrics.Count;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get total execution count: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears execution history for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to clear history for</param>
|
||||
public void ClearSymbolHistory(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_symbolExecutionHistory.ContainsKey(symbol))
|
||||
{
|
||||
_symbolExecutionHistory[symbol].Clear();
|
||||
_logger.LogDebug("Cleared execution history for {Symbol}", symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to clear execution history for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execution statistics for a symbol
|
||||
/// </summary>
|
||||
public class ExecutionStatistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol these statistics are for
|
||||
/// </summary>
|
||||
public string Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average slippage
|
||||
/// </summary>
|
||||
public double AverageSlippage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average submission latency
|
||||
/// </summary>
|
||||
public TimeSpan AverageSubmitLatency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average fill latency
|
||||
/// </summary>
|
||||
public TimeSpan AverageFillLatency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Count of executions with positive slippage
|
||||
/// </summary>
|
||||
public int PositiveSlippageCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Count of executions with negative slippage
|
||||
/// </summary>
|
||||
public int NegativeSlippageCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average execution quality
|
||||
/// </summary>
|
||||
public ExecutionQuality AverageQuality { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for ExecutionStatistics
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol for statistics</param>
|
||||
/// <param name="avgSlippage">Average slippage</param>
|
||||
/// <param name="avgSubmitLatency">Average submission latency</param>
|
||||
/// <param name="avgFillLatency">Average fill latency</param>
|
||||
/// <param name="posSlippageCount">Positive slippage count</param>
|
||||
/// <param name="negSlippageCount">Negative slippage count</param>
|
||||
/// <param name="avgQuality">Average quality</param>
|
||||
public ExecutionStatistics(
|
||||
string symbol,
|
||||
double avgSlippage,
|
||||
TimeSpan avgSubmitLatency,
|
||||
TimeSpan avgFillLatency,
|
||||
int posSlippageCount,
|
||||
int negSlippageCount,
|
||||
ExecutionQuality avgQuality)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
Symbol = symbol;
|
||||
AverageSlippage = avgSlippage;
|
||||
AverageSubmitLatency = avgSubmitLatency;
|
||||
AverageFillLatency = avgFillLatency;
|
||||
PositiveSlippageCount = posSlippageCount;
|
||||
NegativeSlippageCount = negSlippageCount;
|
||||
AverageQuality = avgQuality;
|
||||
}
|
||||
}
|
||||
}
|
||||
460
src/NT8.Core/Execution/MultiLevelTargetManager.cs
Normal file
460
src/NT8.Core/Execution/MultiLevelTargetManager.cs
Normal file
@@ -0,0 +1,460 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages multiple profit targets for scaling out of positions
|
||||
/// </summary>
|
||||
public class MultiLevelTargetManager
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
// Store target information for each order
|
||||
private readonly Dictionary<string, MultiLevelTargetInfo> _multiLevelTargets;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for MultiLevelTargetManager
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance</param>
|
||||
public MultiLevelTargetManager(ILogger<MultiLevelTargetManager> logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_multiLevelTargets = new Dictionary<string, MultiLevelTargetInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets multiple profit targets for an order
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID</param>
|
||||
/// <param name="targets">Multi-level target configuration</param>
|
||||
public void SetTargets(string orderId, MultiLevelTargets targets)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
if (targets == null)
|
||||
throw new ArgumentNullException("targets");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var targetInfo = new MultiLevelTargetInfo
|
||||
{
|
||||
OrderId = orderId,
|
||||
Targets = targets,
|
||||
CompletedTargets = new HashSet<int>(),
|
||||
Active = true,
|
||||
StartTime = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_multiLevelTargets[orderId] = targetInfo;
|
||||
|
||||
_logger.LogDebug("Set multi-level targets for {OrderId}: TP1={TP1}({TP1C} contracts), TP2={TP2}({TP2C} contracts), TP3={TP3}({TP3C} contracts)",
|
||||
orderId,
|
||||
targets.TP1Ticks, targets.TP1Contracts,
|
||||
targets.TP2Ticks ?? 0, targets.TP2Contracts ?? 0,
|
||||
targets.TP3Ticks ?? 0, targets.TP3Contracts ?? 0);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to set targets for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a target hit and determines next action
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID</param>
|
||||
/// <param name="targetLevel">Target level that was hit (1, 2, or 3)</param>
|
||||
/// <param name="hitPrice">Price at which target was hit</param>
|
||||
/// <returns>Action to take after target hit</returns>
|
||||
public TargetActionResult OnTargetHit(string orderId, int targetLevel, decimal hitPrice)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_multiLevelTargets.ContainsKey(orderId))
|
||||
{
|
||||
_logger.LogWarning("No multi-level targets found for {OrderId}", orderId);
|
||||
return new TargetActionResult(TargetAction.NoAction, "No targets configured", 0);
|
||||
}
|
||||
|
||||
var targetInfo = _multiLevelTargets[orderId];
|
||||
if (!targetInfo.Active || targetInfo.CompletedTargets.Contains(targetLevel))
|
||||
{
|
||||
return new TargetActionResult(TargetAction.NoAction, "Target already completed or inactive", 0);
|
||||
}
|
||||
|
||||
// Calculate contracts to close based on target level
|
||||
int contractsToClose = 0;
|
||||
string targetDescription = "";
|
||||
|
||||
switch (targetLevel)
|
||||
{
|
||||
case 1:
|
||||
contractsToClose = targetInfo.Targets.TP1Contracts;
|
||||
targetDescription = String.Format("TP1 at {0} ticks", targetInfo.Targets.TP1Ticks);
|
||||
break;
|
||||
case 2:
|
||||
if (targetInfo.Targets.TP2Contracts.HasValue)
|
||||
{
|
||||
contractsToClose = targetInfo.Targets.TP2Contracts.Value;
|
||||
targetDescription = String.Format("TP2 at {0} ticks", targetInfo.Targets.TP2Ticks);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TargetActionResult(TargetAction.NoAction, "TP2 not configured", 0);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if (targetInfo.Targets.TP3Contracts.HasValue)
|
||||
{
|
||||
contractsToClose = targetInfo.Targets.TP3Contracts.Value;
|
||||
targetDescription = String.Format("TP3 at {0} ticks", targetInfo.Targets.TP3Ticks);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TargetActionResult(TargetAction.NoAction, "TP3 not configured", 0);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return new TargetActionResult(TargetAction.NoAction, "Invalid target level", 0);
|
||||
}
|
||||
|
||||
// Mark this target as completed
|
||||
targetInfo.CompletedTargets.Add(targetLevel);
|
||||
|
||||
// Determine next action
|
||||
TargetAction action;
|
||||
string message;
|
||||
|
||||
// Check if all configured targets have been hit
|
||||
var allConfiguredTargets = new List<int> { 1 };
|
||||
if (targetInfo.Targets.TP2Ticks.HasValue) allConfiguredTargets.Add(2);
|
||||
if (targetInfo.Targets.TP3Ticks.HasValue) allConfiguredTargets.Add(3);
|
||||
|
||||
if (targetInfo.CompletedTargets.Count == allConfiguredTargets.Count)
|
||||
{
|
||||
// All targets hit - position should be fully closed
|
||||
action = TargetAction.ClosePosition;
|
||||
message = String.Format("All targets hit - {0} closed {1} contracts", targetDescription, contractsToClose);
|
||||
}
|
||||
else
|
||||
{
|
||||
// More targets remain - partial close
|
||||
action = TargetAction.PartialClose;
|
||||
message = String.Format("{0} hit - closing {1} contracts", targetDescription, contractsToClose);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Target hit for {OrderId}: {Message}", orderId, message);
|
||||
|
||||
return new TargetActionResult(action, message, contractsToClose);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to process target hit for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the number of contracts to close at a target level
|
||||
/// </summary>
|
||||
/// <param name="targetLevel">Target level (1, 2, or 3)</param>
|
||||
/// <returns>Number of contracts to close</returns>
|
||||
public int CalculateContractsToClose(int targetLevel)
|
||||
{
|
||||
try
|
||||
{
|
||||
// This method would typically be called as part of a larger calculation
|
||||
// For now, returning 0 as the actual number depends on the order details
|
||||
// which would be stored in the MultiLevelTargetInfo
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to calculate contracts to close for target {Level}: {Message}", targetLevel, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a target level should advance stop management
|
||||
/// </summary>
|
||||
/// <param name="targetLevel">Target level that was hit</param>
|
||||
/// <returns>True if stops should be advanced, false otherwise</returns>
|
||||
public bool ShouldAdvanceStop(int targetLevel)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Typically, advancing stops happens after certain targets are hit
|
||||
// For example, after TP1, move stops to breakeven
|
||||
// After TP2, start trailing stops
|
||||
switch (targetLevel)
|
||||
{
|
||||
case 1:
|
||||
// After first target, consider moving stops to breakeven
|
||||
return true;
|
||||
case 2:
|
||||
// After second target, consider tightening trailing stops
|
||||
return true;
|
||||
case 3:
|
||||
// After third target, position is likely closing
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to determine stop advancement for target {Level}: {Message}", targetLevel, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target status for an order
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID to get status for</param>
|
||||
/// <returns>Target status information</returns>
|
||||
public TargetStatus GetTargetStatus(string orderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_multiLevelTargets.ContainsKey(orderId))
|
||||
{
|
||||
var targetInfo = _multiLevelTargets[orderId];
|
||||
|
||||
var remainingTargets = new List<int>();
|
||||
if (!targetInfo.CompletedTargets.Contains(1)) remainingTargets.Add(1);
|
||||
if (targetInfo.Targets.TP2Ticks.HasValue && !targetInfo.CompletedTargets.Contains(2)) remainingTargets.Add(2);
|
||||
if (targetInfo.Targets.TP3Ticks.HasValue && !targetInfo.CompletedTargets.Contains(3)) remainingTargets.Add(3);
|
||||
|
||||
return new TargetStatus
|
||||
{
|
||||
OrderId = orderId,
|
||||
Active = targetInfo.Active,
|
||||
CompletedTargets = new HashSet<int>(targetInfo.CompletedTargets),
|
||||
RemainingTargets = remainingTargets,
|
||||
TotalTargets = targetInfo.Targets.TP3Ticks.HasValue ? 3 :
|
||||
targetInfo.Targets.TP2Ticks.HasValue ? 2 : 1
|
||||
};
|
||||
}
|
||||
|
||||
return new TargetStatus
|
||||
{
|
||||
OrderId = orderId,
|
||||
Active = false,
|
||||
CompletedTargets = new HashSet<int>(),
|
||||
RemainingTargets = new List<int>(),
|
||||
TotalTargets = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get target status for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates multi-level targeting for an order
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID to deactivate</param>
|
||||
public void DeactivateTargets(string orderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_multiLevelTargets.ContainsKey(orderId))
|
||||
{
|
||||
_multiLevelTargets[orderId].Active = false;
|
||||
_logger.LogDebug("Deactivated multi-level targets for {OrderId}", orderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to deactivate targets for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes multi-level target tracking for an order
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID to remove</param>
|
||||
public void RemoveTargets(string orderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_multiLevelTargets.ContainsKey(orderId))
|
||||
{
|
||||
_multiLevelTargets.Remove(orderId);
|
||||
_logger.LogDebug("Removed multi-level target tracking for {OrderId}", orderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to remove targets for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about multi-level targets for an order
|
||||
/// </summary>
|
||||
internal class MultiLevelTargetInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Order ID this targets are for
|
||||
/// </summary>
|
||||
public string OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Target configuration
|
||||
/// </summary>
|
||||
public MultiLevelTargets Targets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set of completed target levels
|
||||
/// </summary>
|
||||
public HashSet<int> CompletedTargets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether target tracking is active
|
||||
/// </summary>
|
||||
public bool Active { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When target tracking was started
|
||||
/// </summary>
|
||||
public DateTime StartTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of target hit processing
|
||||
/// </summary>
|
||||
public class TargetActionResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Action to take
|
||||
/// </summary>
|
||||
public TargetAction Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Message describing the action
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of contracts to close (for partial closes)
|
||||
/// </summary>
|
||||
public int ContractsToClose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for TargetActionResult
|
||||
/// </summary>
|
||||
/// <param name="action">Action to take</param>
|
||||
/// <param name="message">Description message</param>
|
||||
/// <param name="contractsToClose">Contracts to close</param>
|
||||
public TargetActionResult(TargetAction action, string message, int contractsToClose)
|
||||
{
|
||||
Action = action;
|
||||
Message = message;
|
||||
ContractsToClose = contractsToClose;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of multi-level targets
|
||||
/// </summary>
|
||||
public class TargetStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Order ID
|
||||
/// </summary>
|
||||
public string OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether target tracking is active
|
||||
/// </summary>
|
||||
public bool Active { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Completed target levels
|
||||
/// </summary>
|
||||
public HashSet<int> CompletedTargets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Remaining target levels
|
||||
/// </summary>
|
||||
public List<int> RemainingTargets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of configured targets
|
||||
/// </summary>
|
||||
public int TotalTargets { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action to take when a target is hit
|
||||
/// </summary>
|
||||
public enum TargetAction
|
||||
{
|
||||
/// <summary>
|
||||
/// No action needed
|
||||
/// </summary>
|
||||
NoAction = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Partially close the position
|
||||
/// </summary>
|
||||
PartialClose = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Close the entire position
|
||||
/// </summary>
|
||||
ClosePosition = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Move stops to breakeven
|
||||
/// </summary>
|
||||
MoveToBreakeven = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Start trailing stops
|
||||
/// </summary>
|
||||
StartTrailing = 4
|
||||
}
|
||||
}
|
||||
253
src/NT8.Core/Execution/OrderRoutingModels.cs
Normal file
253
src/NT8.Core/Execution/OrderRoutingModels.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Decision result for order routing
|
||||
/// </summary>
|
||||
public class RoutingDecision
|
||||
{
|
||||
/// <summary>
|
||||
/// Order ID being routed
|
||||
/// </summary>
|
||||
public string OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Venue to route the order to
|
||||
/// </summary>
|
||||
public string RoutingVenue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Routing strategy used
|
||||
/// </summary>
|
||||
public RoutingStrategy Strategy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence level in the routing decision (0-100)
|
||||
/// </summary>
|
||||
public int Confidence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected execution quality
|
||||
/// </summary>
|
||||
public ExecutionQuality ExpectedQuality { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected latency in milliseconds
|
||||
/// </summary>
|
||||
public int ExpectedLatencyMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the routing decision is valid
|
||||
/// </summary>
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for the routing decision
|
||||
/// </summary>
|
||||
public string Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for RoutingDecision
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID</param>
|
||||
/// <param name="routingVenue">Venue to route to</param>
|
||||
/// <param name="strategy">Routing strategy</param>
|
||||
/// <param name="confidence">Confidence level</param>
|
||||
/// <param name="expectedQuality">Expected quality</param>
|
||||
/// <param name="expectedLatencyMs">Expected latency in ms</param>
|
||||
/// <param name="isValid">Whether decision is valid</param>
|
||||
/// <param name="reason">Reason for decision</param>
|
||||
public RoutingDecision(
|
||||
string orderId,
|
||||
string routingVenue,
|
||||
RoutingStrategy strategy,
|
||||
int confidence,
|
||||
ExecutionQuality expectedQuality,
|
||||
int expectedLatencyMs,
|
||||
bool isValid,
|
||||
string reason)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
OrderId = orderId;
|
||||
RoutingVenue = routingVenue;
|
||||
Strategy = strategy;
|
||||
Confidence = confidence;
|
||||
ExpectedQuality = expectedQuality;
|
||||
ExpectedLatencyMs = expectedLatencyMs;
|
||||
IsValid = isValid;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information for checking duplicate orders
|
||||
/// </summary>
|
||||
public class OrderDuplicateCheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Order ID to check
|
||||
/// </summary>
|
||||
public string OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol of the order
|
||||
/// </summary>
|
||||
public string Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Side of the order
|
||||
/// </summary>
|
||||
public OMS.OrderSide Side { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Quantity of the order
|
||||
/// </summary>
|
||||
public int Quantity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time when the order intent was created
|
||||
/// </summary>
|
||||
public DateTime IntentTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is a duplicate order
|
||||
/// </summary>
|
||||
public bool IsDuplicate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time window for duplicate checking
|
||||
/// </summary>
|
||||
public TimeSpan DuplicateWindow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for OrderDuplicateCheck
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID</param>
|
||||
/// <param name="symbol">Symbol</param>
|
||||
/// <param name="side">Order side</param>
|
||||
/// <param name="quantity">Quantity</param>
|
||||
/// <param name="intentTime">Intent time</param>
|
||||
/// <param name="duplicateWindow">Duplicate window</param>
|
||||
public OrderDuplicateCheck(
|
||||
string orderId,
|
||||
string symbol,
|
||||
OMS.OrderSide side,
|
||||
int quantity,
|
||||
DateTime intentTime,
|
||||
TimeSpan duplicateWindow)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
OrderId = orderId;
|
||||
Symbol = symbol;
|
||||
Side = side;
|
||||
Quantity = quantity;
|
||||
IntentTime = intentTime;
|
||||
DuplicateWindow = duplicateWindow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current state of circuit breaker
|
||||
/// </summary>
|
||||
public class CircuitBreakerState
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the circuit breaker is active
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current state of the circuit breaker
|
||||
/// </summary>
|
||||
public CircuitBreakerStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for the current state
|
||||
/// </summary>
|
||||
public string Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time when the state was last updated
|
||||
/// </summary>
|
||||
public DateTime LastUpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time when the circuit breaker will reset (if applicable)
|
||||
/// </summary>
|
||||
public DateTime? ResetTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of violations that triggered the state
|
||||
/// </summary>
|
||||
public int ViolationCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for CircuitBreakerState
|
||||
/// </summary>
|
||||
/// <param name="isActive">Whether active</param>
|
||||
/// <param name="status">Current status</param>
|
||||
/// <param name="reason">Reason for state</param>
|
||||
/// <param name="violationCount">Violation count</param>
|
||||
public CircuitBreakerState(
|
||||
bool isActive,
|
||||
CircuitBreakerStatus status,
|
||||
string reason,
|
||||
int violationCount)
|
||||
{
|
||||
IsActive = isActive;
|
||||
Status = status;
|
||||
Reason = reason;
|
||||
ViolationCount = violationCount;
|
||||
LastUpdateTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routing strategy enumeration
|
||||
/// </summary>
|
||||
public enum RoutingStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Direct routing to primary venue
|
||||
/// </summary>
|
||||
Direct = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Smart routing based on market conditions
|
||||
/// </summary>
|
||||
Smart = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Fallback to alternative venue
|
||||
/// </summary>
|
||||
Fallback = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Circuit breaker status enumeration
|
||||
/// </summary>
|
||||
public enum CircuitBreakerStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Normal operation
|
||||
/// </summary>
|
||||
Closed = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Circuit breaker activated
|
||||
/// </summary>
|
||||
Open = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Testing if conditions have improved
|
||||
/// </summary>
|
||||
HalfOpen = 2
|
||||
}
|
||||
}
|
||||
268
src/NT8.Core/Execution/RMultipleCalculator.cs
Normal file
268
src/NT8.Core/Execution/RMultipleCalculator.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.OMS;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates R-value, R-multiple targets, and realized R performance for executions.
|
||||
/// </summary>
|
||||
public class RMultipleCalculator
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
private readonly Dictionary<string, double> _latestRValues;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the RMultipleCalculator class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
public RMultipleCalculator(ILogger<RMultipleCalculator> logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_latestRValues = new Dictionary<string, double>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates monetary R-value for a position using stop distance, tick value, and contracts.
|
||||
/// </summary>
|
||||
/// <param name="position">Position information.</param>
|
||||
/// <param name="stopPrice">Stop price.</param>
|
||||
/// <param name="tickValue">Monetary value of one full point of price movement.</param>
|
||||
/// <returns>Total R-value in monetary terms for the position.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when position is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when inputs are invalid.</exception>
|
||||
public double CalculateRValue(Position position, double stopPrice, double tickValue)
|
||||
{
|
||||
if (position == null)
|
||||
throw new ArgumentNullException("position");
|
||||
|
||||
try
|
||||
{
|
||||
if (position.Quantity == 0)
|
||||
throw new ArgumentException("Position quantity cannot be zero", "position");
|
||||
if (tickValue <= 0)
|
||||
throw new ArgumentException("Tick value must be positive", "tickValue");
|
||||
|
||||
var stopDistance = System.Math.Abs(position.AveragePrice - stopPrice);
|
||||
if (stopDistance <= 0)
|
||||
throw new ArgumentException("Stop distance must be positive", "stopPrice");
|
||||
|
||||
var contracts = System.Math.Abs(position.Quantity);
|
||||
var rValue = stopDistance * tickValue * contracts;
|
||||
|
||||
var cacheKey = String.Format("{0}:{1}", position.Symbol ?? "UNKNOWN", contracts);
|
||||
lock (_lock)
|
||||
{
|
||||
_latestRValues[cacheKey] = rValue;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Calculated R-value for {Symbol}: distance={Distance:F4}, contracts={Contracts}, rValue={RValue:F4}",
|
||||
position.Symbol, stopDistance, contracts, rValue);
|
||||
|
||||
return rValue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to calculate R-value: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a target price from entry using R multiple and side.
|
||||
/// </summary>
|
||||
/// <param name="entryPrice">Entry price.</param>
|
||||
/// <param name="rValue">R-unit distance in price terms.</param>
|
||||
/// <param name="rMultiple">R multiple (for example 1.0, 2.0, 3.0).</param>
|
||||
/// <param name="side">Order side.</param>
|
||||
/// <returns>Calculated target price.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when inputs are invalid.</exception>
|
||||
public double CalculateTargetPrice(double entryPrice, double rValue, double rMultiple, OMS.OrderSide side)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (entryPrice <= 0)
|
||||
throw new ArgumentException("Entry price must be positive", "entryPrice");
|
||||
if (rValue <= 0)
|
||||
throw new ArgumentException("R value must be positive", "rValue");
|
||||
if (rMultiple <= 0)
|
||||
throw new ArgumentException("R multiple must be positive", "rMultiple");
|
||||
|
||||
var distance = rValue * rMultiple;
|
||||
var target = side == OMS.OrderSide.Buy ? entryPrice + distance : entryPrice - distance;
|
||||
|
||||
_logger.LogDebug("Calculated target price: entry={Entry:F4}, rValue={RValue:F4}, rMultiple={RMultiple:F2}, side={Side}, target={Target:F4}",
|
||||
entryPrice, rValue, rMultiple, side, target);
|
||||
|
||||
return target;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to calculate target price: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates realized R-multiple for a completed trade.
|
||||
/// </summary>
|
||||
/// <param name="entryPrice">Entry price.</param>
|
||||
/// <param name="exitPrice">Exit price.</param>
|
||||
/// <param name="rValue">R-unit distance in price terms.</param>
|
||||
/// <returns>Realized R-multiple.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when inputs are invalid.</exception>
|
||||
public double CalculateRMultiple(double entryPrice, double exitPrice, double rValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (entryPrice <= 0)
|
||||
throw new ArgumentException("Entry price must be positive", "entryPrice");
|
||||
if (exitPrice <= 0)
|
||||
throw new ArgumentException("Exit price must be positive", "exitPrice");
|
||||
if (rValue <= 0)
|
||||
throw new ArgumentException("R value must be positive", "rValue");
|
||||
|
||||
var pnlDistance = exitPrice - entryPrice;
|
||||
var rMultiple = pnlDistance / rValue;
|
||||
|
||||
_logger.LogDebug("Calculated realized R-multiple: entry={Entry:F4}, exit={Exit:F4}, rValue={RValue:F4}, rMultiple={RMultiple:F4}",
|
||||
entryPrice, exitPrice, rValue, rMultiple);
|
||||
|
||||
return rMultiple;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to calculate realized R-multiple: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates up to three tick-based targets from R multiples and stop distance.
|
||||
/// </summary>
|
||||
/// <param name="entryPrice">Entry price.</param>
|
||||
/// <param name="stopPrice">Stop price.</param>
|
||||
/// <param name="rMultiples">Array of R multiples (first three values are used).</param>
|
||||
/// <returns>Multi-level target configuration.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when rMultiples is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when inputs are invalid.</exception>
|
||||
public MultiLevelTargets CreateRBasedTargets(double entryPrice, double stopPrice, double[] rMultiples)
|
||||
{
|
||||
if (rMultiples == null)
|
||||
throw new ArgumentNullException("rMultiples");
|
||||
|
||||
try
|
||||
{
|
||||
if (entryPrice <= 0)
|
||||
throw new ArgumentException("Entry price must be positive", "entryPrice");
|
||||
|
||||
var baseRiskTicks = System.Math.Abs(entryPrice - stopPrice);
|
||||
if (baseRiskTicks <= 0)
|
||||
throw new ArgumentException("Stop price must differ from entry price", "stopPrice");
|
||||
|
||||
if (rMultiples.Length == 0)
|
||||
throw new ArgumentException("At least one R multiple is required", "rMultiples");
|
||||
|
||||
var tp1Ticks = ToTargetTicks(baseRiskTicks, rMultiples, 0);
|
||||
var tp2Ticks = rMultiples.Length > 1 ? (int?)ToTargetTicks(baseRiskTicks, rMultiples, 1) : null;
|
||||
var tp3Ticks = rMultiples.Length > 2 ? (int?)ToTargetTicks(baseRiskTicks, rMultiples, 2) : null;
|
||||
|
||||
var targets = new MultiLevelTargets(
|
||||
tp1Ticks,
|
||||
1,
|
||||
tp2Ticks,
|
||||
tp2Ticks.HasValue ? (int?)1 : null,
|
||||
tp3Ticks,
|
||||
tp3Ticks.HasValue ? (int?)1 : null);
|
||||
|
||||
_logger.LogDebug("Created R-based targets: riskTicks={RiskTicks:F4}, TP1={TP1}, TP2={TP2}, TP3={TP3}",
|
||||
baseRiskTicks, tp1Ticks, tp2Ticks.HasValue ? tp2Ticks.Value : 0, tp3Ticks.HasValue ? tp3Ticks.Value : 0);
|
||||
|
||||
return targets;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to create R-based targets: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last cached R-value for a symbol and quantity pair.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol.</param>
|
||||
/// <param name="quantity">Absolute contract quantity.</param>
|
||||
/// <returns>Cached R-value if available; otherwise null.</returns>
|
||||
public double? GetLatestRValue(string symbol, int quantity)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
if (quantity <= 0)
|
||||
throw new ArgumentException("Quantity must be positive", "quantity");
|
||||
|
||||
var cacheKey = String.Format("{0}:{1}", symbol, quantity);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_latestRValues.ContainsKey(cacheKey))
|
||||
return _latestRValues[cacheKey];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get cached R-value: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all cached R-values.
|
||||
/// </summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_latestRValues.Clear();
|
||||
}
|
||||
|
||||
_logger.LogDebug("Cleared R-value cache");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to clear R-value cache: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private int ToTargetTicks(double baseRiskTicks, double[] rMultiples, int index)
|
||||
{
|
||||
if (index < 0 || index >= rMultiples.Length)
|
||||
throw new ArgumentOutOfRangeException("index");
|
||||
|
||||
var multiple = rMultiples[index];
|
||||
if (multiple <= 0)
|
||||
throw new ArgumentException("R multiple must be positive", "rMultiples");
|
||||
|
||||
var rawTicks = baseRiskTicks * multiple;
|
||||
var rounded = (int)System.Math.Round(rawTicks, MidpointRounding.AwayFromZero);
|
||||
|
||||
if (rounded <= 0)
|
||||
rounded = 1;
|
||||
|
||||
return rounded;
|
||||
}
|
||||
}
|
||||
}
|
||||
202
src/NT8.Core/Execution/SlippageCalculator.cs
Normal file
202
src/NT8.Core/Execution/SlippageCalculator.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates various types of slippage for order executions
|
||||
/// </summary>
|
||||
public class SlippageCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculate price slippage between intended and actual execution
|
||||
/// </summary>
|
||||
/// <param name="orderType">Type of order</param>
|
||||
/// <param name="intendedPrice">Price the order was intended to execute at</param>
|
||||
/// <param name="fillPrice">Actual fill price</param>
|
||||
/// <returns>Calculated slippage value</returns>
|
||||
public decimal CalculateSlippage(OMS.OrderType orderType, decimal intendedPrice, decimal fillPrice)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Slippage is calculated as fillPrice - intendedPrice
|
||||
// For market orders, compare to market price at submission
|
||||
// For limit orders, compare to limit price
|
||||
// For stop orders, compare to stop price
|
||||
// For stop-limit orders, compare to trigger price
|
||||
return fillPrice - intendedPrice;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("Failed to calculate slippage: {0}", ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classifies slippage as positive, negative, or zero based on side and execution
|
||||
/// </summary>
|
||||
/// <param name="slippage">Calculated slippage value</param>
|
||||
/// <param name="side">Order side (buy/sell)</param>
|
||||
/// <returns>Type of slippage</returns>
|
||||
public SlippageType ClassifySlippage(decimal slippage, OMS.OrderSide side)
|
||||
{
|
||||
try
|
||||
{
|
||||
// For buys: positive slippage is bad (paid more than expected), negative is good (paid less)
|
||||
// For sells: positive slippage is good (got more than expected), negative is bad (got less)
|
||||
if (slippage > 0)
|
||||
{
|
||||
if (side == OMS.OrderSide.Buy)
|
||||
return SlippageType.Negative; // Paid more than expected on buy
|
||||
else
|
||||
return SlippageType.Positive; // Got more than expected on sell
|
||||
}
|
||||
else if (slippage < 0)
|
||||
{
|
||||
if (side == OMS.OrderSide.Buy)
|
||||
return SlippageType.Positive; // Paid less than expected on buy
|
||||
else
|
||||
return SlippageType.Negative; // Got less than expected on sell
|
||||
}
|
||||
else
|
||||
{
|
||||
return SlippageType.Zero; // No slippage
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("Failed to classify slippage: {0}", ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts slippage value to equivalent number of ticks
|
||||
/// </summary>
|
||||
/// <param name="slippage">Slippage value to convert</param>
|
||||
/// <param name="tickSize">Size of one tick for the instrument</param>
|
||||
/// <returns>Number of ticks equivalent to the slippage</returns>
|
||||
public int SlippageInTicks(decimal slippage, decimal tickSize)
|
||||
{
|
||||
if (tickSize <= 0)
|
||||
throw new ArgumentException("Tick size must be positive", "tickSize");
|
||||
|
||||
try
|
||||
{
|
||||
return (int)Math.Round((double)(Math.Abs(slippage) / tickSize));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("Failed to convert slippage to ticks: {0}", ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the financial impact of slippage on P&L
|
||||
/// </summary>
|
||||
/// <param name="slippage">Slippage value</param>
|
||||
/// <param name="quantity">Order quantity</param>
|
||||
/// <param name="tickValue">Value of one tick</param>
|
||||
/// <param name="side">Order side</param>
|
||||
/// <returns>Financial impact of slippage</returns>
|
||||
public decimal SlippageImpact(decimal slippage, int quantity, decimal tickValue, OMS.OrderSide side)
|
||||
{
|
||||
if (quantity <= 0)
|
||||
throw new ArgumentException("Quantity must be positive", "quantity");
|
||||
if (tickValue <= 0)
|
||||
throw new ArgumentException("Tick value must be positive", "tickValue");
|
||||
|
||||
try
|
||||
{
|
||||
// Calculate impact in terms of ticks
|
||||
var slippageInTicks = slippage / tickValue;
|
||||
|
||||
// Impact = slippage_in_ticks * quantity * tick_value
|
||||
// For buys: positive slippage (paid more) is negative impact, negative slippage (paid less) is positive impact
|
||||
// For sells: positive slippage (got more) is positive impact, negative slippage (got less) is negative impact
|
||||
var impact = slippageInTicks * quantity * tickValue;
|
||||
|
||||
// Adjust sign based on order side and slippage classification
|
||||
if (side == OMS.OrderSide.Buy)
|
||||
{
|
||||
// For buys, worse execution (positive slippage) is negative impact
|
||||
return -impact;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For sells, better execution (negative slippage) is positive impact
|
||||
return impact;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("Failed to calculate slippage impact: {0}", ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates percentage slippage relative to intended price
|
||||
/// </summary>
|
||||
/// <param name="slippage">Calculated slippage</param>
|
||||
/// <param name="intendedPrice">Intended execution price</param>
|
||||
/// <returns>Percentage slippage</returns>
|
||||
public decimal CalculatePercentageSlippage(decimal slippage, decimal intendedPrice)
|
||||
{
|
||||
if (intendedPrice <= 0)
|
||||
throw new ArgumentException("Intended price must be positive", "intendedPrice");
|
||||
|
||||
try
|
||||
{
|
||||
return (slippage / intendedPrice) * 100;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("Failed to calculate percentage slippage: {0}", ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if slippage is within acceptable bounds
|
||||
/// </summary>
|
||||
/// <param name="slippage">Calculated slippage</param>
|
||||
/// <param name="maxAcceptableSlippage">Maximum acceptable slippage in ticks</param>
|
||||
/// <param name="tickSize">Size of one tick</param>
|
||||
/// <returns>True if slippage is within acceptable bounds</returns>
|
||||
public bool IsSlippageAcceptable(decimal slippage, int maxAcceptableSlippage, decimal tickSize)
|
||||
{
|
||||
if (tickSize <= 0)
|
||||
throw new ArgumentException("Tick size must be positive", "tickSize");
|
||||
|
||||
try
|
||||
{
|
||||
var slippageInTicks = SlippageInTicks(slippage, tickSize);
|
||||
return Math.Abs(slippageInTicks) <= maxAcceptableSlippage;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("Failed to evaluate slippage acceptability: {0}", ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates effective cost of slippage in basis points
|
||||
/// </summary>
|
||||
/// <param name="slippage">Calculated slippage</param>
|
||||
/// <param name="intendedPrice">Intended execution price</param>
|
||||
/// <returns>Cost of slippage in basis points</returns>
|
||||
public decimal SlippageInBasisPoints(decimal slippage, decimal intendedPrice)
|
||||
{
|
||||
if (intendedPrice <= 0)
|
||||
throw new ArgumentException("Intended price must be positive", "intendedPrice");
|
||||
|
||||
try
|
||||
{
|
||||
// Basis points = percentage * 100
|
||||
var percentage = (Math.Abs(slippage) / intendedPrice) * 100;
|
||||
return percentage * 100;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("Failed to calculate slippage in basis points: {0}", ex.Message), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
252
src/NT8.Core/Execution/StopsTargetsModels.cs
Normal file
252
src/NT8.Core/Execution/StopsTargetsModels.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
using System;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for multiple profit targets
|
||||
/// </summary>
|
||||
public class MultiLevelTargets
|
||||
{
|
||||
/// <summary>
|
||||
/// Ticks for first target (TP1)
|
||||
/// </summary>
|
||||
public int TP1Ticks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of contracts to close at first target
|
||||
/// </summary>
|
||||
public int TP1Contracts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ticks for second target (TP2) - nullable
|
||||
/// </summary>
|
||||
public int? TP2Ticks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of contracts to close at second target - nullable
|
||||
/// </summary>
|
||||
public int? TP2Contracts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ticks for third target (TP3) - nullable
|
||||
/// </summary>
|
||||
public int? TP3Ticks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of contracts to close at third target - nullable
|
||||
/// </summary>
|
||||
public int? TP3Contracts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for MultiLevelTargets
|
||||
/// </summary>
|
||||
/// <param name="tp1Ticks">Ticks for first target</param>
|
||||
/// <param name="tp1Contracts">Contracts to close at first target</param>
|
||||
/// <param name="tp2Ticks">Ticks for second target</param>
|
||||
/// <param name="tp2Contracts">Contracts to close at second target</param>
|
||||
/// <param name="tp3Ticks">Ticks for third target</param>
|
||||
/// <param name="tp3Contracts">Contracts to close at third target</param>
|
||||
public MultiLevelTargets(
|
||||
int tp1Ticks,
|
||||
int tp1Contracts,
|
||||
int? tp2Ticks = null,
|
||||
int? tp2Contracts = null,
|
||||
int? tp3Ticks = null,
|
||||
int? tp3Contracts = null)
|
||||
{
|
||||
if (tp1Ticks <= 0)
|
||||
throw new ArgumentException("TP1Ticks must be positive", "tp1Ticks");
|
||||
if (tp1Contracts <= 0)
|
||||
throw new ArgumentException("TP1Contracts must be positive", "tp1Contracts");
|
||||
|
||||
TP1Ticks = tp1Ticks;
|
||||
TP1Contracts = tp1Contracts;
|
||||
TP2Ticks = tp2Ticks;
|
||||
TP2Contracts = tp2Contracts;
|
||||
TP3Ticks = tp3Ticks;
|
||||
TP3Contracts = tp3Contracts;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for trailing stops
|
||||
/// </summary>
|
||||
public class TrailingStopConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of trailing stop
|
||||
/// </summary>
|
||||
public StopType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trailing amount in ticks
|
||||
/// </summary>
|
||||
public int TrailingAmountTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trailing amount as percentage (for percentage-based trailing)
|
||||
/// </summary>
|
||||
public decimal? TrailingPercentage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ATR multiplier for ATR-based trailing
|
||||
/// </summary>
|
||||
public decimal AtrMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to trail by high/low (true) or close prices (false)
|
||||
/// </summary>
|
||||
public bool TrailByExtremes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for TrailingStopConfig
|
||||
/// </summary>
|
||||
/// <param name="type">Type of trailing stop</param>
|
||||
/// <param name="trailingAmountTicks">Trailing amount in ticks</param>
|
||||
/// <param name="atrMultiplier">ATR multiplier</param>
|
||||
/// <param name="trailByExtremes">Whether to trail by extremes</param>
|
||||
public TrailingStopConfig(
|
||||
StopType type,
|
||||
int trailingAmountTicks,
|
||||
decimal atrMultiplier = 2m,
|
||||
bool trailByExtremes = true)
|
||||
{
|
||||
if (trailingAmountTicks <= 0)
|
||||
throw new ArgumentException("TrailingAmountTicks must be positive", "trailingAmountTicks");
|
||||
if (atrMultiplier <= 0)
|
||||
throw new ArgumentException("AtrMultiplier must be positive", "atrMultiplier");
|
||||
|
||||
Type = type;
|
||||
TrailingAmountTicks = trailingAmountTicks;
|
||||
AtrMultiplier = atrMultiplier;
|
||||
TrailByExtremes = trailByExtremes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for percentage-based trailing stop
|
||||
/// </summary>
|
||||
/// <param name="trailingPercentage">Trailing percentage</param>
|
||||
/// <param name="trailByExtremes">Whether to trail by extremes</param>
|
||||
public TrailingStopConfig(decimal trailingPercentage, bool trailByExtremes = true)
|
||||
{
|
||||
if (trailingPercentage <= 0 || trailingPercentage > 100)
|
||||
throw new ArgumentException("TrailingPercentage must be between 0 and 100", "trailingPercentage");
|
||||
|
||||
Type = StopType.PercentageTrailing;
|
||||
TrailingPercentage = trailingPercentage;
|
||||
TrailByExtremes = trailByExtremes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for automatic breakeven
|
||||
/// </summary>
|
||||
public class AutoBreakevenConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of ticks in profit before moving stop to breakeven
|
||||
/// </summary>
|
||||
public int TicksToBreakeven { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to add a safety margin to breakeven stop
|
||||
/// </summary>
|
||||
public bool UseSafetyMargin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Safety margin in ticks when moving to breakeven
|
||||
/// </summary>
|
||||
public int SafetyMarginTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable auto-breakeven
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for AutoBreakevenConfig
|
||||
/// </summary>
|
||||
/// <param name="ticksToBreakeven">Ticks in profit before breakeven</param>
|
||||
/// <param name="useSafetyMargin">Whether to use safety margin</param>
|
||||
/// <param name="safetyMarginTicks">Safety margin in ticks</param>
|
||||
/// <param name="enabled">Whether enabled</param>
|
||||
public AutoBreakevenConfig(
|
||||
int ticksToBreakeven,
|
||||
bool useSafetyMargin = true,
|
||||
int safetyMarginTicks = 1,
|
||||
bool enabled = true)
|
||||
{
|
||||
if (ticksToBreakeven <= 0)
|
||||
throw new ArgumentException("TicksToBreakeven must be positive", "ticksToBreakeven");
|
||||
if (safetyMarginTicks < 0)
|
||||
throw new ArgumentException("SafetyMarginTicks cannot be negative", "safetyMarginTicks");
|
||||
|
||||
TicksToBreakeven = ticksToBreakeven;
|
||||
UseSafetyMargin = useSafetyMargin;
|
||||
SafetyMarginTicks = safetyMarginTicks;
|
||||
Enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop type enumeration
|
||||
/// </summary>
|
||||
public enum StopType
|
||||
{
|
||||
/// <summary>
|
||||
/// Fixed stop at specific price
|
||||
/// </summary>
|
||||
Fixed = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Trailing stop by fixed ticks
|
||||
/// </summary>
|
||||
FixedTrailing = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Trailing stop by ATR multiple
|
||||
/// </summary>
|
||||
ATRTrailing = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Chandelier-style trailing stop
|
||||
/// </summary>
|
||||
Chandelier = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Parabolic SAR trailing stop
|
||||
/// </summary>
|
||||
ParabolicSAR = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Percentage-based trailing stop
|
||||
/// </summary>
|
||||
PercentageTrailing = 5
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Target type enumeration
|
||||
/// </summary>
|
||||
public enum TargetType
|
||||
{
|
||||
/// <summary>
|
||||
/// Fixed target at specific price
|
||||
/// </summary>
|
||||
Fixed = 0,
|
||||
|
||||
/// <summary>
|
||||
/// R-Multiple based target (based on risk amount)
|
||||
/// </summary>
|
||||
RMultiple = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Percentage-based target
|
||||
/// </summary>
|
||||
Percentage = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Tick-based target
|
||||
/// </summary>
|
||||
Tick = 3
|
||||
}
|
||||
}
|
||||
393
src/NT8.Core/Execution/TrailingStopManager.cs
Normal file
393
src/NT8.Core/Execution/TrailingStopManager.cs
Normal file
@@ -0,0 +1,393 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NT8.Core.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages trailing stops for positions with various trailing methods
|
||||
/// </summary>
|
||||
public class TrailingStopManager
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
// Store trailing stop information for each order
|
||||
private readonly Dictionary<string, TrailingStopInfo> _trailingStops;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for TrailingStopManager
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance</param>
|
||||
public TrailingStopManager(ILogger<TrailingStopManager> logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_trailingStops = new Dictionary<string, TrailingStopInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts trailing a stop for an order/position
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID</param>
|
||||
/// <param name="position">Position information</param>
|
||||
/// <param name="config">Trailing stop configuration</param>
|
||||
public void StartTrailing(string orderId, OMS.OrderStatus position, TrailingStopConfig config)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
if (position == null)
|
||||
throw new ArgumentNullException("position");
|
||||
if (config == null)
|
||||
throw new ArgumentNullException("config");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var trailingStop = new TrailingStopInfo
|
||||
{
|
||||
OrderId = orderId,
|
||||
Position = position,
|
||||
Config = config,
|
||||
IsActive = true,
|
||||
LastTrackedPrice = position.AverageFillPrice,
|
||||
LastCalculatedStop = CalculateInitialStop(position, config),
|
||||
StartTime = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_trailingStops[orderId] = trailingStop;
|
||||
|
||||
_logger.LogDebug("Started trailing stop for {OrderId}, initial stop at {StopPrice}",
|
||||
orderId, trailingStop.LastCalculatedStop);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to start trailing stop for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the trailing stop based on current market price
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID</param>
|
||||
/// <param name="currentPrice">Current market price</param>
|
||||
/// <returns>New stop price if updated, null if not updated</returns>
|
||||
public decimal? UpdateTrailingStop(string orderId, decimal currentPrice)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_trailingStops.ContainsKey(orderId))
|
||||
{
|
||||
_logger.LogWarning("No trailing stop found for {OrderId}", orderId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var trailingStop = _trailingStops[orderId];
|
||||
if (!trailingStop.IsActive)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var newStopPrice = CalculateNewStopPrice(trailingStop.Config.Type, trailingStop.Position, currentPrice);
|
||||
|
||||
// Only update if the stop has improved (moved in favorable direction)
|
||||
var shouldUpdate = false;
|
||||
decimal updatedStop = trailingStop.LastCalculatedStop;
|
||||
|
||||
if (trailingStop.Position.Side == OMS.OrderSide.Buy)
|
||||
{
|
||||
// For long positions, update if new stop is higher than previous
|
||||
if (newStopPrice > trailingStop.LastCalculatedStop)
|
||||
{
|
||||
shouldUpdate = true;
|
||||
updatedStop = newStopPrice;
|
||||
}
|
||||
}
|
||||
else // Sell/Short
|
||||
{
|
||||
// For short positions, update if new stop is lower than previous
|
||||
if (newStopPrice < trailingStop.LastCalculatedStop)
|
||||
{
|
||||
shouldUpdate = true;
|
||||
updatedStop = newStopPrice;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUpdate)
|
||||
{
|
||||
trailingStop.LastCalculatedStop = updatedStop;
|
||||
trailingStop.LastTrackedPrice = currentPrice;
|
||||
trailingStop.LastUpdateTime = DateTime.UtcNow;
|
||||
|
||||
_logger.LogDebug("Updated trailing stop for {OrderId} to {StopPrice}", orderId, updatedStop);
|
||||
return updatedStop;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to update trailing stop for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the new stop price based on trailing stop type
|
||||
/// </summary>
|
||||
/// <param name="type">Type of trailing stop</param>
|
||||
/// <param name="position">Position information</param>
|
||||
/// <param name="marketPrice">Current market price</param>
|
||||
/// <returns>Calculated stop price</returns>
|
||||
public decimal CalculateNewStopPrice(StopType type, OMS.OrderStatus position, decimal marketPrice)
|
||||
{
|
||||
if (position == null)
|
||||
throw new ArgumentNullException("position");
|
||||
|
||||
try
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case StopType.FixedTrailing:
|
||||
// Fixed trailing: trail by fixed number of ticks from high/low
|
||||
if (position.Side == OMS.OrderSide.Buy)
|
||||
{
|
||||
// Long position: stop trails below highest high
|
||||
return marketPrice - (position.AverageFillPrice - position.AverageFillPrice); // Simplified
|
||||
}
|
||||
else
|
||||
{
|
||||
// Short position: stop trails above lowest low
|
||||
return marketPrice + (position.AverageFillPrice - position.AverageFillPrice); // Simplified
|
||||
}
|
||||
|
||||
case StopType.ATRTrailing:
|
||||
// ATR trailing: trail by ATR multiple
|
||||
return position.Side == OMS.OrderSide.Buy ?
|
||||
marketPrice - (position.AverageFillPrice * 0.01m) : // Placeholder for ATR calculation
|
||||
marketPrice + (position.AverageFillPrice * 0.01m); // Placeholder for ATR calculation
|
||||
|
||||
case StopType.Chandelier:
|
||||
// Chandelier: trail from highest high minus ATR * multiplier
|
||||
return position.Side == OMS.OrderSide.Buy ?
|
||||
marketPrice - (position.AverageFillPrice * 0.01m) : // Placeholder for chandelier calculation
|
||||
marketPrice + (position.AverageFillPrice * 0.01m); // Placeholder for chandelier calculation
|
||||
|
||||
case StopType.PercentageTrailing:
|
||||
// Percentage trailing: trail by percentage of current price
|
||||
var pctTrail = 0.02m; // Default 2% - in real impl this would come from config
|
||||
return position.Side == OMS.OrderSide.Buy ?
|
||||
marketPrice * (1 - pctTrail) :
|
||||
marketPrice * (1 + pctTrail);
|
||||
|
||||
default:
|
||||
// Fixed trailing as fallback
|
||||
var tickSize = 0.25m; // Default tick size - should be configurable
|
||||
var ticks = 8; // Default trailing ticks - should come from config
|
||||
return position.Side == OMS.OrderSide.Buy ?
|
||||
marketPrice - (ticks * tickSize) :
|
||||
marketPrice + (ticks * tickSize);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to calculate new stop price: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a position should be moved to breakeven
|
||||
/// </summary>
|
||||
/// <param name="position">Position to check</param>
|
||||
/// <param name="currentPrice">Current market price</param>
|
||||
/// <param name="config">Auto-breakeven configuration</param>
|
||||
/// <returns>True if should move to breakeven, false otherwise</returns>
|
||||
public bool ShouldMoveToBreakeven(OMS.OrderStatus position, decimal currentPrice, AutoBreakevenConfig config)
|
||||
{
|
||||
if (position == null)
|
||||
throw new ArgumentNullException("position");
|
||||
if (config == null)
|
||||
throw new ArgumentNullException("config");
|
||||
|
||||
try
|
||||
{
|
||||
if (!config.Enabled)
|
||||
return false;
|
||||
|
||||
// Calculate profit in ticks
|
||||
var profitPerContract = position.Side == OMS.OrderSide.Buy ?
|
||||
currentPrice - position.AverageFillPrice :
|
||||
position.AverageFillPrice - currentPrice;
|
||||
|
||||
var tickSize = 0.25m; // Should be configurable per symbol
|
||||
var profitInTicks = (int)(profitPerContract / tickSize);
|
||||
|
||||
return profitInTicks >= config.TicksToBreakeven;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to check breakeven condition: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current trailing stop price for an order
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID to get stop for</param>
|
||||
/// <returns>Current trailing stop price</returns>
|
||||
public decimal? GetCurrentStopPrice(string orderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_trailingStops.ContainsKey(orderId))
|
||||
{
|
||||
return _trailingStops[orderId].LastCalculatedStop;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get current stop price for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates trailing for an order
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID to deactivate</param>
|
||||
public void DeactivateTrailing(string orderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_trailingStops.ContainsKey(orderId))
|
||||
{
|
||||
_trailingStops[orderId].IsActive = false;
|
||||
_logger.LogDebug("Deactivated trailing stop for {OrderId}", orderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to deactivate trailing for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes trailing stop tracking for an order
|
||||
/// </summary>
|
||||
/// <param name="orderId">Order ID to remove</param>
|
||||
public void RemoveTrailing(string orderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
throw new ArgumentNullException("orderId");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_trailingStops.ContainsKey(orderId))
|
||||
{
|
||||
_trailingStops.Remove(orderId);
|
||||
_logger.LogDebug("Removed trailing stop tracking for {OrderId}", orderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to remove trailing for {OrderId}: {Message}", orderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates initial stop price based on configuration
|
||||
/// </summary>
|
||||
/// <param name="position">Position information</param>
|
||||
/// <param name="config">Trailing stop configuration</param>
|
||||
/// <returns>Initial stop price</returns>
|
||||
private decimal CalculateInitialStop(OMS.OrderStatus position, TrailingStopConfig config)
|
||||
{
|
||||
if (position == null || config == null)
|
||||
return 0;
|
||||
|
||||
var tickSize = 0.25m; // Should be configurable per symbol
|
||||
var initialDistance = config.TrailingAmountTicks * tickSize;
|
||||
|
||||
return position.Side == OMS.OrderSide.Buy ?
|
||||
position.AverageFillPrice - initialDistance :
|
||||
position.AverageFillPrice + initialDistance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a trailing stop
|
||||
/// </summary>
|
||||
internal class TrailingStopInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Order ID this trailing stop is for
|
||||
/// </summary>
|
||||
public string OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Position information
|
||||
/// </summary>
|
||||
public OMS.OrderStatus Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trailing stop configuration
|
||||
/// </summary>
|
||||
public TrailingStopConfig Config { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether trailing is currently active
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last price that was tracked
|
||||
/// </summary>
|
||||
public decimal LastTrackedPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last calculated stop price
|
||||
/// </summary>
|
||||
public decimal LastCalculatedStop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When trailing was started
|
||||
/// </summary>
|
||||
public DateTime StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When stop was last updated
|
||||
/// </summary>
|
||||
public DateTime LastUpdateTime { get; set; }
|
||||
}
|
||||
}
|
||||
201
src/NT8.Core/Indicators/AVWAPCalculator.cs
Normal file
201
src/NT8.Core/Indicators/AVWAPCalculator.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Common.Models;
|
||||
|
||||
namespace NT8.Core.Indicators
|
||||
{
|
||||
/// <summary>
|
||||
/// Anchor mode for AVWAP reset behavior.
|
||||
/// </summary>
|
||||
public enum AVWAPAnchorMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Reset at session/day start.
|
||||
/// </summary>
|
||||
Day = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Reset at week start.
|
||||
/// </summary>
|
||||
Week = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Reset at custom provided anchor time.
|
||||
/// </summary>
|
||||
Custom = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anchored VWAP calculator with rolling updates and slope estimation.
|
||||
/// Thread-safe for live multi-caller usage.
|
||||
/// </summary>
|
||||
public class AVWAPCalculator
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private readonly List<double> _vwapHistory;
|
||||
|
||||
private DateTime _anchorTime;
|
||||
private double _sumPriceVolume;
|
||||
private double _sumVolume;
|
||||
private AVWAPAnchorMode _anchorMode;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AVWAP calculator.
|
||||
/// </summary>
|
||||
/// <param name="anchorMode">Anchor mode.</param>
|
||||
/// <param name="anchorTime">Initial anchor time.</param>
|
||||
public AVWAPCalculator(AVWAPAnchorMode anchorMode, DateTime anchorTime)
|
||||
{
|
||||
_anchorMode = anchorMode;
|
||||
_anchorTime = anchorTime;
|
||||
_sumPriceVolume = 0.0;
|
||||
_sumVolume = 0.0;
|
||||
_vwapHistory = new List<double>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates anchored VWAP from bars starting at anchor time.
|
||||
/// </summary>
|
||||
/// <param name="bars">Source bars in chronological order.</param>
|
||||
/// <param name="anchorTime">Anchor start time.</param>
|
||||
/// <returns>Calculated AVWAP value or 0.0 if no eligible bars.</returns>
|
||||
public double Calculate(List<BarData> bars, DateTime anchorTime)
|
||||
{
|
||||
if (bars == null)
|
||||
throw new ArgumentNullException("bars");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_anchorTime = anchorTime;
|
||||
_sumPriceVolume = 0.0;
|
||||
_sumVolume = 0.0;
|
||||
_vwapHistory.Clear();
|
||||
|
||||
for (var i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var bar = bars[i];
|
||||
if (bar == null)
|
||||
continue;
|
||||
|
||||
if (bar.Time < anchorTime)
|
||||
continue;
|
||||
|
||||
var price = GetTypicalPrice(bar);
|
||||
var volume = Math.Max(0L, bar.Volume);
|
||||
|
||||
_sumPriceVolume += price * volume;
|
||||
_sumVolume += volume;
|
||||
|
||||
var vwap = _sumVolume > 0.0 ? _sumPriceVolume / _sumVolume : 0.0;
|
||||
_vwapHistory.Add(vwap);
|
||||
}
|
||||
|
||||
if (_sumVolume <= 0.0)
|
||||
return 0.0;
|
||||
|
||||
return _sumPriceVolume / _sumVolume;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates AVWAP state with one new trade/bar observation.
|
||||
/// </summary>
|
||||
/// <param name="price">Current price.</param>
|
||||
/// <param name="volume">Current volume.</param>
|
||||
public void Update(double price, long volume)
|
||||
{
|
||||
if (volume < 0)
|
||||
throw new ArgumentException("volume must be non-negative", "volume");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_sumPriceVolume += price * volume;
|
||||
_sumVolume += volume;
|
||||
|
||||
var vwap = _sumVolume > 0.0 ? _sumPriceVolume / _sumVolume : 0.0;
|
||||
_vwapHistory.Add(vwap);
|
||||
|
||||
if (_vwapHistory.Count > 2000)
|
||||
_vwapHistory.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns AVWAP slope over lookback bars.
|
||||
/// </summary>
|
||||
/// <param name="lookback">Lookback bars.</param>
|
||||
/// <returns>Slope per bar.</returns>
|
||||
public double GetSlope(int lookback)
|
||||
{
|
||||
if (lookback <= 0)
|
||||
throw new ArgumentException("lookback must be greater than zero", "lookback");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_vwapHistory.Count <= lookback)
|
||||
return 0.0;
|
||||
|
||||
var lastIndex = _vwapHistory.Count - 1;
|
||||
var current = _vwapHistory[lastIndex];
|
||||
var prior = _vwapHistory[lastIndex - lookback];
|
||||
return (current - prior) / lookback;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets AVWAP accumulation to a new anchor.
|
||||
/// </summary>
|
||||
/// <param name="newAnchor">New anchor time.</param>
|
||||
public void ResetAnchor(DateTime newAnchor)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_anchorTime = newAnchor;
|
||||
_sumPriceVolume = 0.0;
|
||||
_sumVolume = 0.0;
|
||||
_vwapHistory.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets current AVWAP from rolling state.
|
||||
/// </summary>
|
||||
/// <returns>Current AVWAP.</returns>
|
||||
public double GetCurrentValue()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _sumVolume > 0.0 ? _sumPriceVolume / _sumVolume : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets current anchor mode.
|
||||
/// </summary>
|
||||
/// <returns>Anchor mode.</returns>
|
||||
public AVWAPAnchorMode GetAnchorMode()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _anchorMode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets anchor mode.
|
||||
/// </summary>
|
||||
/// <param name="mode">Anchor mode.</param>
|
||||
public void SetAnchorMode(AVWAPAnchorMode mode)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_anchorMode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
private static double GetTypicalPrice(BarData bar)
|
||||
{
|
||||
return (bar.High + bar.Low + bar.Close) / 3.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
294
src/NT8.Core/Indicators/VolumeProfileAnalyzer.cs
Normal file
294
src/NT8.Core/Indicators/VolumeProfileAnalyzer.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Common.Models;
|
||||
|
||||
namespace NT8.Core.Indicators
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents value area range around volume point of control.
|
||||
/// </summary>
|
||||
public class ValueArea
|
||||
{
|
||||
/// <summary>
|
||||
/// Volume point of control (highest volume price level).
|
||||
/// </summary>
|
||||
public double VPOC { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Value area high boundary.
|
||||
/// </summary>
|
||||
public double ValueAreaHigh { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Value area low boundary.
|
||||
/// </summary>
|
||||
public double ValueAreaLow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total profile volume.
|
||||
/// </summary>
|
||||
public double TotalVolume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Value area volume.
|
||||
/// </summary>
|
||||
public double ValueAreaVolume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a value area model.
|
||||
/// </summary>
|
||||
/// <param name="vpoc">VPOC level.</param>
|
||||
/// <param name="valueAreaHigh">Value area high.</param>
|
||||
/// <param name="valueAreaLow">Value area low.</param>
|
||||
/// <param name="totalVolume">Total volume.</param>
|
||||
/// <param name="valueAreaVolume">Value area volume.</param>
|
||||
public ValueArea(double vpoc, double valueAreaHigh, double valueAreaLow, double totalVolume, double valueAreaVolume)
|
||||
{
|
||||
VPOC = vpoc;
|
||||
ValueAreaHigh = valueAreaHigh;
|
||||
ValueAreaLow = valueAreaLow;
|
||||
TotalVolume = totalVolume;
|
||||
ValueAreaVolume = valueAreaVolume;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes volume profile by price level and derives VPOC/value areas.
|
||||
/// </summary>
|
||||
public class VolumeProfileAnalyzer
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Gets VPOC from provided bars.
|
||||
/// </summary>
|
||||
/// <param name="bars">Bars in profile window.</param>
|
||||
/// <returns>VPOC price level.</returns>
|
||||
public double GetVPOC(List<BarData> bars)
|
||||
{
|
||||
if (bars == null)
|
||||
throw new ArgumentNullException("bars");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var profile = BuildProfile(bars, 0.25);
|
||||
if (profile.Count == 0)
|
||||
return 0.0;
|
||||
|
||||
var maxVolume = double.MinValue;
|
||||
var vpoc = 0.0;
|
||||
|
||||
foreach (var kv in profile)
|
||||
{
|
||||
if (kv.Value > maxVolume)
|
||||
{
|
||||
maxVolume = kv.Value;
|
||||
vpoc = kv.Key;
|
||||
}
|
||||
}
|
||||
|
||||
return vpoc;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns high volume nodes where volume exceeds 1.5x average level volume.
|
||||
/// </summary>
|
||||
/// <param name="bars">Bars in profile window.</param>
|
||||
/// <returns>High volume node price levels.</returns>
|
||||
public List<double> GetHighVolumeNodes(List<BarData> bars)
|
||||
{
|
||||
if (bars == null)
|
||||
throw new ArgumentNullException("bars");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var profile = BuildProfile(bars, 0.25);
|
||||
var result = new List<double>();
|
||||
if (profile.Count == 0)
|
||||
return result;
|
||||
|
||||
var avg = CalculateAverageVolume(profile);
|
||||
var threshold = avg * 1.5;
|
||||
|
||||
foreach (var kv in profile)
|
||||
{
|
||||
if (kv.Value >= threshold)
|
||||
result.Add(kv.Key);
|
||||
}
|
||||
|
||||
result.Sort();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns low volume nodes where volume is below 0.5x average level volume.
|
||||
/// </summary>
|
||||
/// <param name="bars">Bars in profile window.</param>
|
||||
/// <returns>Low volume node price levels.</returns>
|
||||
public List<double> GetLowVolumeNodes(List<BarData> bars)
|
||||
{
|
||||
if (bars == null)
|
||||
throw new ArgumentNullException("bars");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var profile = BuildProfile(bars, 0.25);
|
||||
var result = new List<double>();
|
||||
if (profile.Count == 0)
|
||||
return result;
|
||||
|
||||
var avg = CalculateAverageVolume(profile);
|
||||
var threshold = avg * 0.5;
|
||||
|
||||
foreach (var kv in profile)
|
||||
{
|
||||
if (kv.Value <= threshold)
|
||||
result.Add(kv.Key);
|
||||
}
|
||||
|
||||
result.Sort();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates 70% value area around VPOC.
|
||||
/// </summary>
|
||||
/// <param name="bars">Bars in profile window.</param>
|
||||
/// <returns>Calculated value area.</returns>
|
||||
public ValueArea CalculateValueArea(List<BarData> bars)
|
||||
{
|
||||
if (bars == null)
|
||||
throw new ArgumentNullException("bars");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var profile = BuildProfile(bars, 0.25);
|
||||
if (profile.Count == 0)
|
||||
return new ValueArea(0.0, 0.0, 0.0, 0.0, 0.0);
|
||||
|
||||
var levels = new List<double>(profile.Keys);
|
||||
levels.Sort();
|
||||
|
||||
var vpoc = GetVPOC(bars);
|
||||
var totalVolume = 0.0;
|
||||
for (var i = 0; i < levels.Count; i++)
|
||||
totalVolume += profile[levels[i]];
|
||||
|
||||
var target = totalVolume * 0.70;
|
||||
|
||||
var included = new HashSet<double>();
|
||||
included.Add(vpoc);
|
||||
var includedVolume = profile.ContainsKey(vpoc) ? profile[vpoc] : 0.0;
|
||||
|
||||
var vpocIndex = levels.IndexOf(vpoc);
|
||||
var left = vpocIndex - 1;
|
||||
var right = vpocIndex + 1;
|
||||
|
||||
while (includedVolume < target && (left >= 0 || right < levels.Count))
|
||||
{
|
||||
var leftVolume = left >= 0 ? profile[levels[left]] : -1.0;
|
||||
var rightVolume = right < levels.Count ? profile[levels[right]] : -1.0;
|
||||
|
||||
if (rightVolume > leftVolume)
|
||||
{
|
||||
included.Add(levels[right]);
|
||||
includedVolume += profile[levels[right]];
|
||||
right++;
|
||||
}
|
||||
else if (left >= 0)
|
||||
{
|
||||
included.Add(levels[left]);
|
||||
includedVolume += profile[levels[left]];
|
||||
left--;
|
||||
}
|
||||
else
|
||||
{
|
||||
included.Add(levels[right]);
|
||||
includedVolume += profile[levels[right]];
|
||||
right++;
|
||||
}
|
||||
}
|
||||
|
||||
var vah = vpoc;
|
||||
var val = vpoc;
|
||||
foreach (var level in included)
|
||||
{
|
||||
if (level > vah)
|
||||
vah = level;
|
||||
if (level < val)
|
||||
val = level;
|
||||
}
|
||||
|
||||
return new ValueArea(vpoc, vah, val, totalVolume, includedVolume);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<double, double> BuildProfile(List<BarData> bars, double tickSize)
|
||||
{
|
||||
var profile = new Dictionary<double, double>();
|
||||
|
||||
for (var i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var bar = bars[i];
|
||||
if (bar == null)
|
||||
continue;
|
||||
|
||||
var low = RoundToTick(bar.Low, tickSize);
|
||||
var high = RoundToTick(bar.High, tickSize);
|
||||
|
||||
if (high < low)
|
||||
{
|
||||
var temp = high;
|
||||
high = low;
|
||||
low = temp;
|
||||
}
|
||||
|
||||
var levelsCount = ((high - low) / tickSize) + 1.0;
|
||||
if (levelsCount <= 0.0)
|
||||
continue;
|
||||
|
||||
var volumePerLevel = bar.Volume / levelsCount;
|
||||
|
||||
var level = low;
|
||||
while (level <= high + 0.0000001)
|
||||
{
|
||||
if (!profile.ContainsKey(level))
|
||||
profile.Add(level, 0.0);
|
||||
|
||||
profile[level] = profile[level] + volumePerLevel;
|
||||
level = RoundToTick(level + tickSize, tickSize);
|
||||
}
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static double CalculateAverageVolume(Dictionary<double, double> profile)
|
||||
{
|
||||
if (profile == null || profile.Count == 0)
|
||||
return 0.0;
|
||||
|
||||
var sum = 0.0;
|
||||
var count = 0;
|
||||
foreach (var kv in profile)
|
||||
{
|
||||
sum += kv.Value;
|
||||
count++;
|
||||
}
|
||||
|
||||
return count > 0 ? sum / count : 0.0;
|
||||
}
|
||||
|
||||
private static double RoundToTick(double value, double tickSize)
|
||||
{
|
||||
if (tickSize <= 0.0)
|
||||
return value;
|
||||
|
||||
var ticks = Math.Round(value / tickSize);
|
||||
return ticks * tickSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
264
src/NT8.Core/Intelligence/ConfluenceModels.cs
Normal file
264
src/NT8.Core/Intelligence/ConfluenceModels.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Types of confluence factors used in intelligence scoring.
|
||||
/// </summary>
|
||||
public enum FactorType
|
||||
{
|
||||
/// <summary>
|
||||
/// Base setup quality (for example ORB validity).
|
||||
/// </summary>
|
||||
Setup = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Trend alignment quality.
|
||||
/// </summary>
|
||||
Trend = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Volatility regime suitability.
|
||||
/// </summary>
|
||||
Volatility = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Session and timing quality.
|
||||
/// </summary>
|
||||
Timing = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Execution quality and slippage context.
|
||||
/// </summary>
|
||||
ExecutionQuality = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Liquidity and microstructure quality.
|
||||
/// </summary>
|
||||
Liquidity = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Risk regime and portfolio context.
|
||||
/// </summary>
|
||||
Risk = 6,
|
||||
|
||||
/// <summary>
|
||||
/// Additional custom factor.
|
||||
/// </summary>
|
||||
Custom = 99
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trade grade produced from weighted confluence score.
|
||||
/// </summary>
|
||||
public enum TradeGrade
|
||||
{
|
||||
/// <summary>
|
||||
/// Exceptional setup, score 0.90 and above.
|
||||
/// </summary>
|
||||
APlus = 6,
|
||||
|
||||
/// <summary>
|
||||
/// Strong setup, score 0.80 and above.
|
||||
/// </summary>
|
||||
A = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Good setup, score 0.70 and above.
|
||||
/// </summary>
|
||||
B = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Acceptable setup, score 0.60 and above.
|
||||
/// </summary>
|
||||
C = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Marginal setup, score 0.50 and above.
|
||||
/// </summary>
|
||||
D = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Reject setup, score below 0.50.
|
||||
/// </summary>
|
||||
F = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Weight configuration for a factor type.
|
||||
/// </summary>
|
||||
public class FactorWeight
|
||||
{
|
||||
/// <summary>
|
||||
/// Factor type this weight applies to.
|
||||
/// </summary>
|
||||
public FactorType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Weight value (must be positive).
|
||||
/// </summary>
|
||||
public double Weight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable reason for this weighting.
|
||||
/// </summary>
|
||||
public string Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last update timestamp in UTC.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAtUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new factor weight.
|
||||
/// </summary>
|
||||
/// <param name="type">Factor type.</param>
|
||||
/// <param name="weight">Weight value greater than zero.</param>
|
||||
/// <param name="reason">Reason for this weight.</param>
|
||||
public FactorWeight(FactorType type, double weight, string reason)
|
||||
{
|
||||
if (weight <= 0)
|
||||
throw new ArgumentException("Weight must be greater than zero", "weight");
|
||||
|
||||
Type = type;
|
||||
Weight = weight;
|
||||
Reason = reason ?? string.Empty;
|
||||
UpdatedAtUtc = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one contributing factor in a confluence calculation.
|
||||
/// </summary>
|
||||
public class ConfluenceFactor
|
||||
{
|
||||
/// <summary>
|
||||
/// Factor category.
|
||||
/// </summary>
|
||||
public FactorType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Factor display name.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw factor score in range [0.0, 1.0].
|
||||
/// </summary>
|
||||
public double Score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Weight importance for this factor.
|
||||
/// </summary>
|
||||
public double Weight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Explanation for score value.
|
||||
/// </summary>
|
||||
public string Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional details for diagnostics.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Details { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new confluence factor.
|
||||
/// </summary>
|
||||
/// <param name="type">Factor type.</param>
|
||||
/// <param name="name">Factor name.</param>
|
||||
/// <param name="score">Score in range [0.0, 1.0].</param>
|
||||
/// <param name="weight">Weight greater than zero.</param>
|
||||
/// <param name="reason">Reason for the score.</param>
|
||||
/// <param name="details">Extended details dictionary.</param>
|
||||
public ConfluenceFactor(
|
||||
FactorType type,
|
||||
string name,
|
||||
double score,
|
||||
double weight,
|
||||
string reason,
|
||||
Dictionary<string, object> details)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentNullException("name");
|
||||
if (score < 0.0 || score > 1.0)
|
||||
throw new ArgumentException("Score must be between 0.0 and 1.0", "score");
|
||||
if (weight <= 0.0)
|
||||
throw new ArgumentException("Weight must be greater than zero", "weight");
|
||||
|
||||
Type = type;
|
||||
Name = name;
|
||||
Score = score;
|
||||
Weight = weight;
|
||||
Reason = reason ?? string.Empty;
|
||||
Details = details ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an overall confluence score and grade for a trading decision.
|
||||
/// </summary>
|
||||
public class ConfluenceScore
|
||||
{
|
||||
/// <summary>
|
||||
/// Unweighted aggregate score in range [0.0, 1.0].
|
||||
/// </summary>
|
||||
public double RawScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Weighted aggregate score in range [0.0, 1.0].
|
||||
/// </summary>
|
||||
public double WeightedScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Grade derived from weighted score.
|
||||
/// </summary>
|
||||
public TradeGrade Grade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Factor breakdown used to produce the score.
|
||||
/// </summary>
|
||||
public List<ConfluenceFactor> Factors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculation timestamp in UTC.
|
||||
/// </summary>
|
||||
public DateTime CalculatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata and diagnostics.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new confluence score model.
|
||||
/// </summary>
|
||||
/// <param name="rawScore">Unweighted score in [0.0, 1.0].</param>
|
||||
/// <param name="weightedScore">Weighted score in [0.0, 1.0].</param>
|
||||
/// <param name="grade">Trade grade.</param>
|
||||
/// <param name="factors">Contributing factors.</param>
|
||||
/// <param name="calculatedAt">Calculation timestamp.</param>
|
||||
/// <param name="metadata">Additional metadata.</param>
|
||||
public ConfluenceScore(
|
||||
double rawScore,
|
||||
double weightedScore,
|
||||
TradeGrade grade,
|
||||
List<ConfluenceFactor> factors,
|
||||
DateTime calculatedAt,
|
||||
Dictionary<string, object> metadata)
|
||||
{
|
||||
if (rawScore < 0.0 || rawScore > 1.0)
|
||||
throw new ArgumentException("RawScore must be between 0.0 and 1.0", "rawScore");
|
||||
if (weightedScore < 0.0 || weightedScore > 1.0)
|
||||
throw new ArgumentException("WeightedScore must be between 0.0 and 1.0", "weightedScore");
|
||||
|
||||
RawScore = rawScore;
|
||||
WeightedScore = weightedScore;
|
||||
Grade = grade;
|
||||
Factors = factors ?? new List<ConfluenceFactor>();
|
||||
CalculatedAt = calculatedAt;
|
||||
Metadata = metadata ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
}
|
||||
440
src/NT8.Core/Intelligence/ConfluenceScorer.cs
Normal file
440
src/NT8.Core/Intelligence/ConfluenceScorer.cs
Normal file
@@ -0,0 +1,440 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Historical statistics for confluence scoring.
|
||||
/// </summary>
|
||||
public class ConfluenceStatistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of score calculations observed.
|
||||
/// </summary>
|
||||
public int TotalCalculations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average weighted score across history.
|
||||
/// </summary>
|
||||
public double AverageWeightedScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average raw score across history.
|
||||
/// </summary>
|
||||
public double AverageRawScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Best weighted score seen in history.
|
||||
/// </summary>
|
||||
public double BestWeightedScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Worst weighted score seen in history.
|
||||
/// </summary>
|
||||
public double WorstWeightedScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Grade distribution counts for historical scores.
|
||||
/// </summary>
|
||||
public Dictionary<TradeGrade, int> GradeDistribution { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last calculation timestamp in UTC.
|
||||
/// </summary>
|
||||
public DateTime LastCalculatedAtUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new statistics model.
|
||||
/// </summary>
|
||||
/// <param name="totalCalculations">Total number of calculations.</param>
|
||||
/// <param name="averageWeightedScore">Average weighted score.</param>
|
||||
/// <param name="averageRawScore">Average raw score.</param>
|
||||
/// <param name="bestWeightedScore">Best weighted score.</param>
|
||||
/// <param name="worstWeightedScore">Worst weighted score.</param>
|
||||
/// <param name="gradeDistribution">Grade distribution map.</param>
|
||||
/// <param name="lastCalculatedAtUtc">Last calculation time.</param>
|
||||
public ConfluenceStatistics(
|
||||
int totalCalculations,
|
||||
double averageWeightedScore,
|
||||
double averageRawScore,
|
||||
double bestWeightedScore,
|
||||
double worstWeightedScore,
|
||||
Dictionary<TradeGrade, int> gradeDistribution,
|
||||
DateTime lastCalculatedAtUtc)
|
||||
{
|
||||
TotalCalculations = totalCalculations;
|
||||
AverageWeightedScore = averageWeightedScore;
|
||||
AverageRawScore = averageRawScore;
|
||||
BestWeightedScore = bestWeightedScore;
|
||||
WorstWeightedScore = worstWeightedScore;
|
||||
GradeDistribution = gradeDistribution ?? new Dictionary<TradeGrade, int>();
|
||||
LastCalculatedAtUtc = lastCalculatedAtUtc;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates weighted confluence score and trade grade from factor calculators.
|
||||
/// Thread-safe for weight updates and score history tracking.
|
||||
/// </summary>
|
||||
public class ConfluenceScorer
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
private readonly Dictionary<FactorType, double> _factorWeights;
|
||||
private readonly Queue<ConfluenceScore> _history;
|
||||
private readonly int _maxHistory;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new confluence scorer instance.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="maxHistory">Maximum historical score entries to keep.</param>
|
||||
/// <exception cref="ArgumentNullException">Logger is null.</exception>
|
||||
/// <exception cref="ArgumentException">Max history is not positive.</exception>
|
||||
public ConfluenceScorer(ILogger logger, int maxHistory)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
if (maxHistory <= 0)
|
||||
throw new ArgumentException("maxHistory must be greater than zero", "maxHistory");
|
||||
|
||||
_logger = logger;
|
||||
_maxHistory = maxHistory;
|
||||
_factorWeights = new Dictionary<FactorType, double>();
|
||||
_history = new Queue<ConfluenceScore>();
|
||||
|
||||
_factorWeights.Add(FactorType.Setup, 1.0);
|
||||
_factorWeights.Add(FactorType.Trend, 1.0);
|
||||
_factorWeights.Add(FactorType.Volatility, 1.0);
|
||||
_factorWeights.Add(FactorType.Timing, 1.0);
|
||||
_factorWeights.Add(FactorType.ExecutionQuality, 1.0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a confluence score from calculators and the current bar.
|
||||
/// </summary>
|
||||
/// <param name="intent">Strategy intent under evaluation.</param>
|
||||
/// <param name="context">Strategy context and market/session state.</param>
|
||||
/// <param name="bar">Current bar used for factor calculations.</param>
|
||||
/// <param name="factors">Factor calculator collection.</param>
|
||||
/// <returns>Calculated confluence score with grade and factor breakdown.</returns>
|
||||
/// <exception cref="ArgumentNullException">Any required parameter is null.</exception>
|
||||
public ConfluenceScore CalculateScore(
|
||||
StrategyIntent intent,
|
||||
StrategyContext context,
|
||||
BarData bar,
|
||||
List<IFactorCalculator> factors)
|
||||
{
|
||||
if (intent == null)
|
||||
throw new ArgumentNullException("intent");
|
||||
if (context == null)
|
||||
throw new ArgumentNullException("context");
|
||||
if (bar == null)
|
||||
throw new ArgumentNullException("bar");
|
||||
if (factors == null)
|
||||
throw new ArgumentNullException("factors");
|
||||
|
||||
try
|
||||
{
|
||||
var calculatedFactors = new List<ConfluenceFactor>();
|
||||
var rawScoreSum = 0.0;
|
||||
var weightedScoreSum = 0.0;
|
||||
var totalWeight = 0.0;
|
||||
|
||||
for (var i = 0; i < factors.Count; i++)
|
||||
{
|
||||
var calculator = factors[i];
|
||||
if (calculator == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var factor = calculator.Calculate(intent, context, bar);
|
||||
if (factor == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var effectiveWeight = ResolveWeight(factor.Type, factor.Weight);
|
||||
|
||||
var weightedFactor = new ConfluenceFactor(
|
||||
factor.Type,
|
||||
factor.Name,
|
||||
factor.Score,
|
||||
effectiveWeight,
|
||||
factor.Reason,
|
||||
factor.Details);
|
||||
|
||||
calculatedFactors.Add(weightedFactor);
|
||||
|
||||
rawScoreSum += weightedFactor.Score;
|
||||
weightedScoreSum += (weightedFactor.Score * weightedFactor.Weight);
|
||||
totalWeight += weightedFactor.Weight;
|
||||
}
|
||||
|
||||
var rawScore = calculatedFactors.Count > 0 ? rawScoreSum / calculatedFactors.Count : 0.0;
|
||||
var weightedScore = totalWeight > 0.0 ? weightedScoreSum / totalWeight : 0.0;
|
||||
|
||||
weightedScore = ClampScore(weightedScore);
|
||||
rawScore = ClampScore(rawScore);
|
||||
|
||||
var grade = MapScoreToGrade(weightedScore);
|
||||
|
||||
var metadata = new Dictionary<string, object>();
|
||||
metadata.Add("factor_count", calculatedFactors.Count);
|
||||
metadata.Add("total_weight", totalWeight);
|
||||
metadata.Add("raw_score_sum", rawScoreSum);
|
||||
metadata.Add("weighted_score_sum", weightedScoreSum);
|
||||
|
||||
var result = new ConfluenceScore(
|
||||
rawScore,
|
||||
weightedScore,
|
||||
grade,
|
||||
calculatedFactors,
|
||||
DateTime.UtcNow,
|
||||
metadata);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_history.Enqueue(result);
|
||||
while (_history.Count > _maxHistory)
|
||||
{
|
||||
_history.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Confluence score calculated: Symbol={0}, Raw={1:F4}, Weighted={2:F4}, Grade={3}, Factors={4}",
|
||||
intent.Symbol,
|
||||
rawScore,
|
||||
weightedScore,
|
||||
grade,
|
||||
calculatedFactors.Count);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Confluence scoring failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a confluence score using the current bar in context custom data.
|
||||
/// </summary>
|
||||
/// <param name="intent">Strategy intent under evaluation.</param>
|
||||
/// <param name="context">Strategy context that contains current bar in custom data key 'current_bar'.</param>
|
||||
/// <param name="factors">Factor calculator collection.</param>
|
||||
/// <returns>Calculated confluence score.</returns>
|
||||
/// <exception cref="ArgumentNullException">Any required parameter is null.</exception>
|
||||
/// <exception cref="ArgumentException">Current bar is missing in context custom data.</exception>
|
||||
public ConfluenceScore CalculateScore(
|
||||
StrategyIntent intent,
|
||||
StrategyContext context,
|
||||
List<IFactorCalculator> factors)
|
||||
{
|
||||
if (intent == null)
|
||||
throw new ArgumentNullException("intent");
|
||||
if (context == null)
|
||||
throw new ArgumentNullException("context");
|
||||
if (factors == null)
|
||||
throw new ArgumentNullException("factors");
|
||||
|
||||
try
|
||||
{
|
||||
BarData bar = null;
|
||||
if (context.CustomData != null && context.CustomData.ContainsKey("current_bar"))
|
||||
{
|
||||
bar = context.CustomData["current_bar"] as BarData;
|
||||
}
|
||||
|
||||
if (bar == null)
|
||||
throw new ArgumentException("context.CustomData must include key 'current_bar' with BarData value", "context");
|
||||
|
||||
return CalculateScore(intent, context, bar, factors);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Confluence scoring failed when reading current_bar: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps weighted score to trade grade.
|
||||
/// </summary>
|
||||
/// <param name="weightedScore">Weighted score in range [0.0, 1.0].</param>
|
||||
/// <returns>Mapped trade grade.</returns>
|
||||
public TradeGrade MapScoreToGrade(double weightedScore)
|
||||
{
|
||||
try
|
||||
{
|
||||
var score = ClampScore(weightedScore);
|
||||
|
||||
if (score >= 0.90)
|
||||
return TradeGrade.APlus;
|
||||
if (score >= 0.80)
|
||||
return TradeGrade.A;
|
||||
if (score >= 0.70)
|
||||
return TradeGrade.B;
|
||||
if (score >= 0.60)
|
||||
return TradeGrade.C;
|
||||
if (score >= 0.50)
|
||||
return TradeGrade.D;
|
||||
|
||||
return TradeGrade.F;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("MapScoreToGrade failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates factor weights for one or more factor types.
|
||||
/// </summary>
|
||||
/// <param name="weights">Weight overrides by factor type.</param>
|
||||
/// <exception cref="ArgumentNullException">Weights dictionary is null.</exception>
|
||||
/// <exception cref="ArgumentException">Any weight is not positive.</exception>
|
||||
public void UpdateFactorWeights(Dictionary<FactorType, double> weights)
|
||||
{
|
||||
if (weights == null)
|
||||
throw new ArgumentNullException("weights");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var pair in weights)
|
||||
{
|
||||
if (pair.Value <= 0.0)
|
||||
throw new ArgumentException("All weights must be greater than zero", "weights");
|
||||
|
||||
if (_factorWeights.ContainsKey(pair.Key))
|
||||
{
|
||||
_factorWeights[pair.Key] = pair.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_factorWeights.Add(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Confluence factor weights updated. Count={0}", weights.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("UpdateFactorWeights failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns historical confluence scoring statistics.
|
||||
/// </summary>
|
||||
/// <returns>Historical confluence statistics snapshot.</returns>
|
||||
public ConfluenceStatistics GetHistoricalStats()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_history.Count == 0)
|
||||
{
|
||||
return CreateEmptyStatistics();
|
||||
}
|
||||
|
||||
var total = _history.Count;
|
||||
var weightedSum = 0.0;
|
||||
var rawSum = 0.0;
|
||||
var best = 0.0;
|
||||
var worst = 1.0;
|
||||
var gradeDistribution = InitializeGradeDistribution();
|
||||
DateTime last = DateTime.MinValue;
|
||||
|
||||
foreach (var score in _history)
|
||||
{
|
||||
weightedSum += score.WeightedScore;
|
||||
rawSum += score.RawScore;
|
||||
|
||||
if (score.WeightedScore > best)
|
||||
best = score.WeightedScore;
|
||||
if (score.WeightedScore < worst)
|
||||
worst = score.WeightedScore;
|
||||
|
||||
if (!gradeDistribution.ContainsKey(score.Grade))
|
||||
gradeDistribution.Add(score.Grade, 0);
|
||||
|
||||
gradeDistribution[score.Grade] = gradeDistribution[score.Grade] + 1;
|
||||
|
||||
if (score.CalculatedAt > last)
|
||||
last = score.CalculatedAt;
|
||||
}
|
||||
|
||||
return new ConfluenceStatistics(
|
||||
total,
|
||||
weightedSum / total,
|
||||
rawSum / total,
|
||||
best,
|
||||
worst,
|
||||
gradeDistribution,
|
||||
last);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("GetHistoricalStats failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static double ClampScore(double score)
|
||||
{
|
||||
if (score < 0.0)
|
||||
return 0.0;
|
||||
if (score > 1.0)
|
||||
return 1.0;
|
||||
return score;
|
||||
}
|
||||
|
||||
private double ResolveWeight(FactorType type, double fallbackWeight)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_factorWeights.ContainsKey(type))
|
||||
return _factorWeights[type];
|
||||
}
|
||||
|
||||
return fallbackWeight > 0.0 ? fallbackWeight : 1.0;
|
||||
}
|
||||
|
||||
private ConfluenceStatistics CreateEmptyStatistics()
|
||||
{
|
||||
return new ConfluenceStatistics(
|
||||
0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
InitializeGradeDistribution(),
|
||||
DateTime.MinValue);
|
||||
}
|
||||
|
||||
private Dictionary<TradeGrade, int> InitializeGradeDistribution()
|
||||
{
|
||||
var distribution = new Dictionary<TradeGrade, int>();
|
||||
distribution.Add(TradeGrade.APlus, 0);
|
||||
distribution.Add(TradeGrade.A, 0);
|
||||
distribution.Add(TradeGrade.B, 0);
|
||||
distribution.Add(TradeGrade.C, 0);
|
||||
distribution.Add(TradeGrade.D, 0);
|
||||
distribution.Add(TradeGrade.F, 0);
|
||||
return distribution;
|
||||
}
|
||||
}
|
||||
}
|
||||
401
src/NT8.Core/Intelligence/FactorCalculators.cs
Normal file
401
src/NT8.Core/Intelligence/FactorCalculators.cs
Normal file
@@ -0,0 +1,401 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Common.Models;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates one confluence factor from strategy and market context.
|
||||
/// </summary>
|
||||
public interface IFactorCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Factor type produced by this calculator.
|
||||
/// </summary>
|
||||
FactorType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the confluence factor.
|
||||
/// </summary>
|
||||
/// <param name="intent">Current strategy intent.</param>
|
||||
/// <param name="context">Current strategy context.</param>
|
||||
/// <param name="bar">Current bar data.</param>
|
||||
/// <returns>Calculated confluence factor.</returns>
|
||||
ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ORB setup quality calculator.
|
||||
/// </summary>
|
||||
public class OrbSetupFactorCalculator : IFactorCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the factor type.
|
||||
/// </summary>
|
||||
public FactorType Type
|
||||
{
|
||||
get { return FactorType.Setup; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates ORB setup validity score.
|
||||
/// </summary>
|
||||
/// <param name="intent">Current strategy intent.</param>
|
||||
/// <param name="context">Current strategy context.</param>
|
||||
/// <param name="bar">Current bar data.</param>
|
||||
/// <returns>Setup confluence factor.</returns>
|
||||
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
|
||||
{
|
||||
if (intent == null)
|
||||
throw new ArgumentNullException("intent");
|
||||
if (context == null)
|
||||
throw new ArgumentNullException("context");
|
||||
if (bar == null)
|
||||
throw new ArgumentNullException("bar");
|
||||
|
||||
var score = 0.0;
|
||||
var details = new Dictionary<string, object>();
|
||||
|
||||
var orbRange = bar.High - bar.Low;
|
||||
details.Add("orb_range", orbRange);
|
||||
|
||||
if (orbRange >= 1.0)
|
||||
score += 0.3;
|
||||
|
||||
var body = System.Math.Abs(bar.Close - bar.Open);
|
||||
details.Add("bar_body", body);
|
||||
if (body >= (orbRange * 0.5))
|
||||
score += 0.2;
|
||||
|
||||
var averageVolume = GetDouble(context.CustomData, "avg_volume", 1.0);
|
||||
details.Add("avg_volume", averageVolume);
|
||||
details.Add("current_volume", bar.Volume);
|
||||
if (averageVolume > 0.0 && bar.Volume > (long)(averageVolume * 1.1))
|
||||
score += 0.2;
|
||||
|
||||
var minutesFromSessionOpen = (bar.Time - context.Session.SessionStart).TotalMinutes;
|
||||
details.Add("minutes_from_open", minutesFromSessionOpen);
|
||||
if (minutesFromSessionOpen >= 0 && minutesFromSessionOpen <= 120)
|
||||
score += 0.3;
|
||||
|
||||
if (score > 1.0)
|
||||
score = 1.0;
|
||||
|
||||
return new ConfluenceFactor(
|
||||
FactorType.Setup,
|
||||
"ORB Setup Validity",
|
||||
score,
|
||||
1.0,
|
||||
"ORB validity based on range, candle quality, volume, and timing",
|
||||
details);
|
||||
}
|
||||
|
||||
private static double GetDouble(Dictionary<string, object> data, string key, double defaultValue)
|
||||
{
|
||||
if (data == null || string.IsNullOrEmpty(key) || !data.ContainsKey(key) || data[key] == null)
|
||||
return defaultValue;
|
||||
|
||||
var value = data[key];
|
||||
if (value is double)
|
||||
return (double)value;
|
||||
if (value is float)
|
||||
return (double)(float)value;
|
||||
if (value is int)
|
||||
return (double)(int)value;
|
||||
if (value is long)
|
||||
return (double)(long)value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trend alignment calculator.
|
||||
/// </summary>
|
||||
public class TrendAlignmentFactorCalculator : IFactorCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the factor type.
|
||||
/// </summary>
|
||||
public FactorType Type
|
||||
{
|
||||
get { return FactorType.Trend; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates trend alignment score.
|
||||
/// </summary>
|
||||
/// <param name="intent">Current strategy intent.</param>
|
||||
/// <param name="context">Current strategy context.</param>
|
||||
/// <param name="bar">Current bar data.</param>
|
||||
/// <returns>Trend confluence factor.</returns>
|
||||
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
|
||||
{
|
||||
if (intent == null)
|
||||
throw new ArgumentNullException("intent");
|
||||
if (context == null)
|
||||
throw new ArgumentNullException("context");
|
||||
if (bar == null)
|
||||
throw new ArgumentNullException("bar");
|
||||
|
||||
var details = new Dictionary<string, object>();
|
||||
var score = 0.0;
|
||||
|
||||
var avwap = GetDouble(context.CustomData, "avwap", bar.Close);
|
||||
var avwapSlope = GetDouble(context.CustomData, "avwap_slope", 0.0);
|
||||
var trendConfirm = GetDouble(context.CustomData, "trend_confirm", 0.0);
|
||||
|
||||
details.Add("avwap", avwap);
|
||||
details.Add("avwap_slope", avwapSlope);
|
||||
details.Add("trend_confirm", trendConfirm);
|
||||
|
||||
var isLong = intent.Side == OrderSide.Buy;
|
||||
var priceAligned = isLong ? bar.Close > avwap : bar.Close < avwap;
|
||||
if (priceAligned)
|
||||
score += 0.4;
|
||||
|
||||
var slopeAligned = isLong ? avwapSlope > 0.0 : avwapSlope < 0.0;
|
||||
if (slopeAligned)
|
||||
score += 0.3;
|
||||
|
||||
if (trendConfirm > 0.5)
|
||||
score += 0.3;
|
||||
|
||||
if (score > 1.0)
|
||||
score = 1.0;
|
||||
|
||||
return new ConfluenceFactor(
|
||||
FactorType.Trend,
|
||||
"Trend Alignment",
|
||||
score,
|
||||
1.0,
|
||||
"Trend alignment using AVWAP location, slope, and confirmation",
|
||||
details);
|
||||
}
|
||||
|
||||
private static double GetDouble(Dictionary<string, object> data, string key, double defaultValue)
|
||||
{
|
||||
if (data == null || string.IsNullOrEmpty(key) || !data.ContainsKey(key) || data[key] == null)
|
||||
return defaultValue;
|
||||
|
||||
var value = data[key];
|
||||
if (value is double)
|
||||
return (double)value;
|
||||
if (value is float)
|
||||
return (double)(float)value;
|
||||
if (value is int)
|
||||
return (double)(int)value;
|
||||
if (value is long)
|
||||
return (double)(long)value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Volatility regime suitability calculator.
|
||||
/// </summary>
|
||||
public class VolatilityRegimeFactorCalculator : IFactorCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the factor type.
|
||||
/// </summary>
|
||||
public FactorType Type
|
||||
{
|
||||
get { return FactorType.Volatility; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates volatility regime score.
|
||||
/// </summary>
|
||||
/// <param name="intent">Current strategy intent.</param>
|
||||
/// <param name="context">Current strategy context.</param>
|
||||
/// <param name="bar">Current bar data.</param>
|
||||
/// <returns>Volatility confluence factor.</returns>
|
||||
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
|
||||
{
|
||||
if (intent == null)
|
||||
throw new ArgumentNullException("intent");
|
||||
if (context == null)
|
||||
throw new ArgumentNullException("context");
|
||||
if (bar == null)
|
||||
throw new ArgumentNullException("bar");
|
||||
|
||||
var details = new Dictionary<string, object>();
|
||||
var currentAtr = GetDouble(context.CustomData, "current_atr", 1.0);
|
||||
var normalAtr = GetDouble(context.CustomData, "normal_atr", 1.0);
|
||||
|
||||
details.Add("current_atr", currentAtr);
|
||||
details.Add("normal_atr", normalAtr);
|
||||
|
||||
var ratio = normalAtr > 0.0 ? currentAtr / normalAtr : 1.0;
|
||||
details.Add("atr_ratio", ratio);
|
||||
|
||||
var score = 0.3;
|
||||
if (ratio >= 0.8 && ratio <= 1.2)
|
||||
score = 1.0;
|
||||
else if (ratio < 0.8)
|
||||
score = 0.7;
|
||||
else if (ratio > 1.2 && ratio <= 1.5)
|
||||
score = 0.5;
|
||||
else if (ratio > 1.5)
|
||||
score = 0.3;
|
||||
|
||||
return new ConfluenceFactor(
|
||||
FactorType.Volatility,
|
||||
"Volatility Regime",
|
||||
score,
|
||||
1.0,
|
||||
"Volatility suitability from ATR ratio",
|
||||
details);
|
||||
}
|
||||
|
||||
private static double GetDouble(Dictionary<string, object> data, string key, double defaultValue)
|
||||
{
|
||||
if (data == null || string.IsNullOrEmpty(key) || !data.ContainsKey(key) || data[key] == null)
|
||||
return defaultValue;
|
||||
|
||||
var value = data[key];
|
||||
if (value is double)
|
||||
return (double)value;
|
||||
if (value is float)
|
||||
return (double)(float)value;
|
||||
if (value is int)
|
||||
return (double)(int)value;
|
||||
if (value is long)
|
||||
return (double)(long)value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Session timing suitability calculator.
|
||||
/// </summary>
|
||||
public class TimeInSessionFactorCalculator : IFactorCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the factor type.
|
||||
/// </summary>
|
||||
public FactorType Type
|
||||
{
|
||||
get { return FactorType.Timing; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates session timing score.
|
||||
/// </summary>
|
||||
/// <param name="intent">Current strategy intent.</param>
|
||||
/// <param name="context">Current strategy context.</param>
|
||||
/// <param name="bar">Current bar data.</param>
|
||||
/// <returns>Timing confluence factor.</returns>
|
||||
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
|
||||
{
|
||||
if (intent == null)
|
||||
throw new ArgumentNullException("intent");
|
||||
if (context == null)
|
||||
throw new ArgumentNullException("context");
|
||||
if (bar == null)
|
||||
throw new ArgumentNullException("bar");
|
||||
|
||||
var details = new Dictionary<string, object>();
|
||||
var t = bar.Time.TimeOfDay;
|
||||
details.Add("time_of_day", t);
|
||||
|
||||
var score = 0.3;
|
||||
var open = new TimeSpan(9, 30, 0);
|
||||
var firstTwoHoursEnd = new TimeSpan(11, 30, 0);
|
||||
var middayEnd = new TimeSpan(14, 0, 0);
|
||||
var lastHourStart = new TimeSpan(15, 0, 0);
|
||||
var close = new TimeSpan(16, 0, 0);
|
||||
|
||||
if (t >= open && t < firstTwoHoursEnd)
|
||||
score = 1.0;
|
||||
else if (t >= firstTwoHoursEnd && t < middayEnd)
|
||||
score = 0.6;
|
||||
else if (t >= lastHourStart && t < close)
|
||||
score = 0.8;
|
||||
else
|
||||
score = 0.3;
|
||||
|
||||
return new ConfluenceFactor(
|
||||
FactorType.Timing,
|
||||
"Time In Session",
|
||||
score,
|
||||
1.0,
|
||||
"Session timing suitability",
|
||||
details);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recent execution quality calculator.
|
||||
/// </summary>
|
||||
public class ExecutionQualityFactorCalculator : IFactorCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the factor type.
|
||||
/// </summary>
|
||||
public FactorType Type
|
||||
{
|
||||
get { return FactorType.ExecutionQuality; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates execution quality score from recent fills.
|
||||
/// </summary>
|
||||
/// <param name="intent">Current strategy intent.</param>
|
||||
/// <param name="context">Current strategy context.</param>
|
||||
/// <param name="bar">Current bar data.</param>
|
||||
/// <returns>Execution quality factor.</returns>
|
||||
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
|
||||
{
|
||||
if (intent == null)
|
||||
throw new ArgumentNullException("intent");
|
||||
if (context == null)
|
||||
throw new ArgumentNullException("context");
|
||||
if (bar == null)
|
||||
throw new ArgumentNullException("bar");
|
||||
|
||||
var details = new Dictionary<string, object>();
|
||||
var quality = GetDouble(context.CustomData, "recent_execution_quality", 0.6);
|
||||
details.Add("recent_execution_quality", quality);
|
||||
|
||||
var score = 0.6;
|
||||
if (quality >= 0.9)
|
||||
score = 1.0;
|
||||
else if (quality >= 0.75)
|
||||
score = 0.8;
|
||||
else if (quality >= 0.6)
|
||||
score = 0.6;
|
||||
else
|
||||
score = 0.4;
|
||||
|
||||
return new ConfluenceFactor(
|
||||
FactorType.ExecutionQuality,
|
||||
"Recent Execution Quality",
|
||||
score,
|
||||
1.0,
|
||||
"Recent execution quality suitability",
|
||||
details);
|
||||
}
|
||||
|
||||
private static double GetDouble(Dictionary<string, object> data, string key, double defaultValue)
|
||||
{
|
||||
if (data == null || string.IsNullOrEmpty(key) || !data.ContainsKey(key) || data[key] == null)
|
||||
return defaultValue;
|
||||
|
||||
var value = data[key];
|
||||
if (value is double)
|
||||
return (double)value;
|
||||
if (value is float)
|
||||
return (double)(float)value;
|
||||
if (value is int)
|
||||
return (double)(int)value;
|
||||
if (value is long)
|
||||
return (double)(long)value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/NT8.Core/Intelligence/GradeFilter.cs
Normal file
138
src/NT8.Core/Intelligence/GradeFilter.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Filters trades by grade according to active risk mode and returns size multipliers.
|
||||
/// </summary>
|
||||
public class GradeFilter
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private readonly Dictionary<RiskMode, TradeGrade> _minimumGradeByMode;
|
||||
private readonly Dictionary<RiskMode, Dictionary<TradeGrade, double>> _sizeMultipliers;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new grade filter with default mode rules.
|
||||
/// </summary>
|
||||
public GradeFilter()
|
||||
{
|
||||
_minimumGradeByMode = new Dictionary<RiskMode, TradeGrade>();
|
||||
_sizeMultipliers = new Dictionary<RiskMode, Dictionary<TradeGrade, double>>();
|
||||
|
||||
InitializeDefaults();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when a trade with given grade should be accepted for the risk mode.
|
||||
/// </summary>
|
||||
/// <param name="grade">Trade grade.</param>
|
||||
/// <param name="mode">Current risk mode.</param>
|
||||
/// <returns>True when accepted, false when rejected.</returns>
|
||||
public bool ShouldAcceptTrade(TradeGrade grade, RiskMode mode)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_minimumGradeByMode.ContainsKey(mode))
|
||||
return false;
|
||||
|
||||
if (mode == RiskMode.HR)
|
||||
return false;
|
||||
|
||||
var minimum = _minimumGradeByMode[mode];
|
||||
return (int)grade >= (int)minimum;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns size multiplier for the trade grade and risk mode.
|
||||
/// </summary>
|
||||
/// <param name="grade">Trade grade.</param>
|
||||
/// <param name="mode">Current risk mode.</param>
|
||||
/// <returns>Size multiplier. Returns 0.0 when trade is rejected.</returns>
|
||||
public double GetSizeMultiplier(TradeGrade grade, RiskMode mode)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!ShouldAcceptTrade(grade, mode))
|
||||
return 0.0;
|
||||
|
||||
if (!_sizeMultipliers.ContainsKey(mode))
|
||||
return 0.0;
|
||||
|
||||
var map = _sizeMultipliers[mode];
|
||||
if (map.ContainsKey(grade))
|
||||
return map[grade];
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns human-readable rejection reason for grade and risk mode.
|
||||
/// </summary>
|
||||
/// <param name="grade">Trade grade.</param>
|
||||
/// <param name="mode">Current risk mode.</param>
|
||||
/// <returns>Rejection reason or empty string when accepted.</returns>
|
||||
public string GetRejectionReason(TradeGrade grade, RiskMode mode)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (mode == RiskMode.HR)
|
||||
return "Risk mode HR blocks all new trades";
|
||||
|
||||
if (!ShouldAcceptTrade(grade, mode))
|
||||
{
|
||||
var min = GetMinimumGrade(mode);
|
||||
return string.Format("Grade {0} below minimum {1} for mode {2}", grade, min, mode);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets minimum grade required for a risk mode.
|
||||
/// </summary>
|
||||
/// <param name="mode">Current risk mode.</param>
|
||||
/// <returns>Minimum accepted trade grade.</returns>
|
||||
public TradeGrade GetMinimumGrade(RiskMode mode)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_minimumGradeByMode.ContainsKey(mode))
|
||||
return _minimumGradeByMode[mode];
|
||||
|
||||
return TradeGrade.APlus;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeDefaults()
|
||||
{
|
||||
_minimumGradeByMode.Add(RiskMode.ECP, TradeGrade.B);
|
||||
_minimumGradeByMode.Add(RiskMode.PCP, TradeGrade.C);
|
||||
_minimumGradeByMode.Add(RiskMode.DCP, TradeGrade.A);
|
||||
_minimumGradeByMode.Add(RiskMode.HR, TradeGrade.APlus);
|
||||
|
||||
var ecp = new Dictionary<TradeGrade, double>();
|
||||
ecp.Add(TradeGrade.APlus, 1.5);
|
||||
ecp.Add(TradeGrade.A, 1.25);
|
||||
ecp.Add(TradeGrade.B, 1.0);
|
||||
_sizeMultipliers.Add(RiskMode.ECP, ecp);
|
||||
|
||||
var pcp = new Dictionary<TradeGrade, double>();
|
||||
pcp.Add(TradeGrade.APlus, 1.25);
|
||||
pcp.Add(TradeGrade.A, 1.1);
|
||||
pcp.Add(TradeGrade.B, 1.0);
|
||||
pcp.Add(TradeGrade.C, 0.9);
|
||||
_sizeMultipliers.Add(RiskMode.PCP, pcp);
|
||||
|
||||
var dcp = new Dictionary<TradeGrade, double>();
|
||||
dcp.Add(TradeGrade.APlus, 0.75);
|
||||
dcp.Add(TradeGrade.A, 0.5);
|
||||
_sizeMultipliers.Add(RiskMode.DCP, dcp);
|
||||
|
||||
_sizeMultipliers.Add(RiskMode.HR, new Dictionary<TradeGrade, double>());
|
||||
}
|
||||
}
|
||||
}
|
||||
334
src/NT8.Core/Intelligence/RegimeManager.cs
Normal file
334
src/NT8.Core/Intelligence/RegimeManager.cs
Normal file
@@ -0,0 +1,334 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Coordinates volatility and trend regime detection and stores per-symbol regime state.
|
||||
/// Thread-safe access to shared regime state and transition history.
|
||||
/// </summary>
|
||||
public class RegimeManager
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly VolatilityRegimeDetector _volatilityDetector;
|
||||
private readonly TrendRegimeDetector _trendDetector;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private readonly Dictionary<string, RegimeState> _currentStates;
|
||||
private readonly Dictionary<string, List<BarData>> _barHistory;
|
||||
private readonly Dictionary<string, List<RegimeTransition>> _transitions;
|
||||
private readonly int _maxBarsPerSymbol;
|
||||
private readonly int _maxTransitionsPerSymbol;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new regime manager.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="volatilityDetector">Volatility regime detector.</param>
|
||||
/// <param name="trendDetector">Trend regime detector.</param>
|
||||
/// <param name="maxBarsPerSymbol">Maximum bars to keep per symbol.</param>
|
||||
/// <param name="maxTransitionsPerSymbol">Maximum transitions to keep per symbol.</param>
|
||||
/// <exception cref="ArgumentNullException">Any required dependency is null.</exception>
|
||||
/// <exception cref="ArgumentException">Any max size is not positive.</exception>
|
||||
public RegimeManager(
|
||||
ILogger logger,
|
||||
VolatilityRegimeDetector volatilityDetector,
|
||||
TrendRegimeDetector trendDetector,
|
||||
int maxBarsPerSymbol,
|
||||
int maxTransitionsPerSymbol)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
if (volatilityDetector == null)
|
||||
throw new ArgumentNullException("volatilityDetector");
|
||||
if (trendDetector == null)
|
||||
throw new ArgumentNullException("trendDetector");
|
||||
if (maxBarsPerSymbol <= 0)
|
||||
throw new ArgumentException("maxBarsPerSymbol must be greater than zero", "maxBarsPerSymbol");
|
||||
if (maxTransitionsPerSymbol <= 0)
|
||||
throw new ArgumentException("maxTransitionsPerSymbol must be greater than zero", "maxTransitionsPerSymbol");
|
||||
|
||||
_logger = logger;
|
||||
_volatilityDetector = volatilityDetector;
|
||||
_trendDetector = trendDetector;
|
||||
_maxBarsPerSymbol = maxBarsPerSymbol;
|
||||
_maxTransitionsPerSymbol = maxTransitionsPerSymbol;
|
||||
|
||||
_currentStates = new Dictionary<string, RegimeState>();
|
||||
_barHistory = new Dictionary<string, List<BarData>>();
|
||||
_transitions = new Dictionary<string, List<RegimeTransition>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates regime state for a symbol using latest market information.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <param name="bar">Latest bar.</param>
|
||||
/// <param name="avwap">Current AVWAP value.</param>
|
||||
/// <param name="atr">Current ATR value.</param>
|
||||
/// <param name="normalAtr">Normal ATR baseline value.</param>
|
||||
public void UpdateRegime(string symbol, BarData bar, double avwap, double atr, double normalAtr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (bar == null)
|
||||
throw new ArgumentNullException("bar");
|
||||
if (normalAtr <= 0.0)
|
||||
throw new ArgumentException("normalAtr must be greater than zero", "normalAtr");
|
||||
if (atr < 0.0)
|
||||
throw new ArgumentException("atr must be non-negative", "atr");
|
||||
|
||||
try
|
||||
{
|
||||
RegimeState previousState = null;
|
||||
VolatilityRegime volatilityRegime;
|
||||
TrendRegime trendRegime;
|
||||
double volatilityScore;
|
||||
double trendStrength;
|
||||
TimeSpan duration;
|
||||
DateTime updateTime = DateTime.UtcNow;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureCollections(symbol);
|
||||
AppendBar(symbol, bar);
|
||||
|
||||
if (_currentStates.ContainsKey(symbol))
|
||||
previousState = _currentStates[symbol];
|
||||
|
||||
volatilityRegime = _volatilityDetector.DetectRegime(symbol, atr, normalAtr);
|
||||
volatilityScore = _volatilityDetector.CalculateVolatilityScore(atr, normalAtr);
|
||||
|
||||
if (_barHistory[symbol].Count >= 5)
|
||||
{
|
||||
trendRegime = _trendDetector.DetectTrend(symbol, _barHistory[symbol], avwap);
|
||||
trendStrength = _trendDetector.CalculateTrendStrength(_barHistory[symbol], avwap);
|
||||
}
|
||||
else
|
||||
{
|
||||
trendRegime = TrendRegime.Range;
|
||||
trendStrength = 0.0;
|
||||
}
|
||||
|
||||
duration = CalculateDuration(previousState, updateTime, volatilityRegime, trendRegime);
|
||||
|
||||
var indicators = new Dictionary<string, object>();
|
||||
indicators.Add("atr", atr);
|
||||
indicators.Add("normal_atr", normalAtr);
|
||||
indicators.Add("volatility_score", volatilityScore);
|
||||
indicators.Add("avwap", avwap);
|
||||
indicators.Add("trend_strength", trendStrength);
|
||||
indicators.Add("bar_count", _barHistory[symbol].Count);
|
||||
|
||||
var newState = new RegimeState(
|
||||
symbol,
|
||||
volatilityRegime,
|
||||
trendRegime,
|
||||
volatilityScore,
|
||||
trendStrength,
|
||||
updateTime,
|
||||
duration,
|
||||
indicators);
|
||||
|
||||
_currentStates[symbol] = newState;
|
||||
|
||||
if (HasTransition(previousState, newState))
|
||||
AddTransition(symbol, previousState, newState, "Regime changed by detector update");
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Regime updated for {0}: Vol={1}, Trend={2}, VolScore={3:F3}, TrendStrength={4:F3}",
|
||||
symbol,
|
||||
volatilityRegime,
|
||||
trendRegime,
|
||||
volatilityScore,
|
||||
trendStrength);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("UpdateRegime failed for {0}: {1}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets current regime state for a symbol.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <returns>Current state, or null if unavailable.</returns>
|
||||
public RegimeState GetCurrentRegime(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_currentStates.ContainsKey(symbol))
|
||||
return null;
|
||||
|
||||
return _currentStates[symbol];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether strategy behavior should be adjusted for current regime.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <param name="intent">Current strategy intent.</param>
|
||||
/// <returns>True when strategy should adjust execution/sizing behavior.</returns>
|
||||
public bool ShouldAdjustStrategy(string symbol, StrategyIntent intent)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (intent == null)
|
||||
throw new ArgumentNullException("intent");
|
||||
|
||||
try
|
||||
{
|
||||
RegimeState state;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_currentStates.ContainsKey(symbol))
|
||||
return false;
|
||||
|
||||
state = _currentStates[symbol];
|
||||
}
|
||||
|
||||
if (state.VolatilityRegime == VolatilityRegime.Extreme)
|
||||
return true;
|
||||
|
||||
if (state.VolatilityRegime == VolatilityRegime.High)
|
||||
return true;
|
||||
|
||||
if (state.TrendRegime == TrendRegime.Range)
|
||||
return true;
|
||||
|
||||
if (state.TrendRegime == TrendRegime.StrongDown && intent.Side == OrderSide.Buy)
|
||||
return true;
|
||||
|
||||
if (state.TrendRegime == TrendRegime.StrongUp && intent.Side == OrderSide.Sell)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("ShouldAdjustStrategy failed for {0}: {1}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets transitions for a symbol within a recent time period.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <param name="period">Lookback period.</param>
|
||||
/// <returns>Matching transition events.</returns>
|
||||
public List<RegimeTransition> GetRecentTransitions(string symbol, TimeSpan period)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (period < TimeSpan.Zero)
|
||||
throw new ArgumentException("period must be non-negative", "period");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_transitions.ContainsKey(symbol))
|
||||
return new List<RegimeTransition>();
|
||||
|
||||
var cutoff = DateTime.UtcNow - period;
|
||||
var result = new List<RegimeTransition>();
|
||||
|
||||
var list = _transitions[symbol];
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (list[i].TransitionTime >= cutoff)
|
||||
result.Add(list[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("GetRecentTransitions failed for {0}: {1}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureCollections(string symbol)
|
||||
{
|
||||
if (!_barHistory.ContainsKey(symbol))
|
||||
_barHistory.Add(symbol, new List<BarData>());
|
||||
|
||||
if (!_transitions.ContainsKey(symbol))
|
||||
_transitions.Add(symbol, new List<RegimeTransition>());
|
||||
}
|
||||
|
||||
private void AppendBar(string symbol, BarData bar)
|
||||
{
|
||||
_barHistory[symbol].Add(bar);
|
||||
while (_barHistory[symbol].Count > _maxBarsPerSymbol)
|
||||
{
|
||||
_barHistory[symbol].RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasTransition(RegimeState previousState, RegimeState newState)
|
||||
{
|
||||
if (previousState == null)
|
||||
return false;
|
||||
|
||||
return previousState.VolatilityRegime != newState.VolatilityRegime ||
|
||||
previousState.TrendRegime != newState.TrendRegime;
|
||||
}
|
||||
|
||||
private static TimeSpan CalculateDuration(
|
||||
RegimeState previousState,
|
||||
DateTime updateTime,
|
||||
VolatilityRegime volatilityRegime,
|
||||
TrendRegime trendRegime)
|
||||
{
|
||||
if (previousState == null)
|
||||
return TimeSpan.Zero;
|
||||
|
||||
if (previousState.VolatilityRegime == volatilityRegime && previousState.TrendRegime == trendRegime)
|
||||
return updateTime - previousState.LastUpdate + previousState.RegimeDuration;
|
||||
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
private void AddTransition(string symbol, RegimeState previousState, RegimeState newState, string reason)
|
||||
{
|
||||
var previousVol = previousState != null ? previousState.VolatilityRegime : newState.VolatilityRegime;
|
||||
var previousTrend = previousState != null ? previousState.TrendRegime : newState.TrendRegime;
|
||||
|
||||
var transition = new RegimeTransition(
|
||||
symbol,
|
||||
previousVol,
|
||||
newState.VolatilityRegime,
|
||||
previousTrend,
|
||||
newState.TrendRegime,
|
||||
newState.LastUpdate,
|
||||
reason);
|
||||
|
||||
_transitions[symbol].Add(transition);
|
||||
while (_transitions[symbol].Count > _maxTransitionsPerSymbol)
|
||||
{
|
||||
_transitions[symbol].RemoveAt(0);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Regime transition for {0}: Vol {1}->{2}, Trend {3}->{4}",
|
||||
symbol,
|
||||
previousVol,
|
||||
newState.VolatilityRegime,
|
||||
previousTrend,
|
||||
newState.TrendRegime);
|
||||
}
|
||||
}
|
||||
}
|
||||
292
src/NT8.Core/Intelligence/RegimeModels.cs
Normal file
292
src/NT8.Core/Intelligence/RegimeModels.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Volatility classification for current market conditions.
|
||||
/// </summary>
|
||||
public enum VolatilityRegime
|
||||
{
|
||||
/// <summary>
|
||||
/// Very low volatility, expansion likely.
|
||||
/// </summary>
|
||||
Low = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Below normal volatility.
|
||||
/// </summary>
|
||||
BelowNormal = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Normal volatility band.
|
||||
/// </summary>
|
||||
Normal = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Elevated volatility, caution required.
|
||||
/// </summary>
|
||||
Elevated = 3,
|
||||
|
||||
/// <summary>
|
||||
/// High volatility.
|
||||
/// </summary>
|
||||
High = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Extreme volatility, defensive posture.
|
||||
/// </summary>
|
||||
Extreme = 5
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trend classification for current market direction and strength.
|
||||
/// </summary>
|
||||
public enum TrendRegime
|
||||
{
|
||||
/// <summary>
|
||||
/// Strong uptrend.
|
||||
/// </summary>
|
||||
StrongUp = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Weak uptrend.
|
||||
/// </summary>
|
||||
WeakUp = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Ranging or neutral market.
|
||||
/// </summary>
|
||||
Range = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Weak downtrend.
|
||||
/// </summary>
|
||||
WeakDown = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Strong downtrend.
|
||||
/// </summary>
|
||||
StrongDown = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quality score for observed trend structure.
|
||||
/// </summary>
|
||||
public enum TrendQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// No reliable trend quality signal.
|
||||
/// </summary>
|
||||
Poor = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Trend quality is fair.
|
||||
/// </summary>
|
||||
Fair = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Trend quality is good.
|
||||
/// </summary>
|
||||
Good = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Trend quality is excellent.
|
||||
/// </summary>
|
||||
Excellent = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of current market regime for a symbol.
|
||||
/// </summary>
|
||||
public class RegimeState
|
||||
{
|
||||
/// <summary>
|
||||
/// Instrument symbol.
|
||||
/// </summary>
|
||||
public string Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current volatility regime.
|
||||
/// </summary>
|
||||
public VolatilityRegime VolatilityRegime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current trend regime.
|
||||
/// </summary>
|
||||
public TrendRegime TrendRegime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Volatility score relative to normal baseline.
|
||||
/// </summary>
|
||||
public double VolatilityScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trend strength from -1.0 (strong down) to +1.0 (strong up).
|
||||
/// </summary>
|
||||
public double TrendStrength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time the regime state was last updated.
|
||||
/// </summary>
|
||||
public DateTime LastUpdate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time spent in the current regime.
|
||||
/// </summary>
|
||||
public TimeSpan RegimeDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Supporting indicator values for diagnostics.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Indicators { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new regime state.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <param name="volatilityRegime">Volatility regime.</param>
|
||||
/// <param name="trendRegime">Trend regime.</param>
|
||||
/// <param name="volatilityScore">Volatility score relative to normal.</param>
|
||||
/// <param name="trendStrength">Trend strength in range [-1.0, +1.0].</param>
|
||||
/// <param name="lastUpdate">Last update timestamp.</param>
|
||||
/// <param name="regimeDuration">Current regime duration.</param>
|
||||
/// <param name="indicators">Supporting indicators map.</param>
|
||||
public RegimeState(
|
||||
string symbol,
|
||||
VolatilityRegime volatilityRegime,
|
||||
TrendRegime trendRegime,
|
||||
double volatilityScore,
|
||||
double trendStrength,
|
||||
DateTime lastUpdate,
|
||||
TimeSpan regimeDuration,
|
||||
Dictionary<string, object> indicators)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (trendStrength < -1.0 || trendStrength > 1.0)
|
||||
throw new ArgumentException("trendStrength must be between -1.0 and 1.0", "trendStrength");
|
||||
|
||||
Symbol = symbol;
|
||||
VolatilityRegime = volatilityRegime;
|
||||
TrendRegime = trendRegime;
|
||||
VolatilityScore = volatilityScore;
|
||||
TrendStrength = trendStrength;
|
||||
LastUpdate = lastUpdate;
|
||||
RegimeDuration = regimeDuration;
|
||||
Indicators = indicators ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a transition event between two regime states.
|
||||
/// </summary>
|
||||
public class RegimeTransition
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol where transition occurred.
|
||||
/// </summary>
|
||||
public string Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous volatility regime.
|
||||
/// </summary>
|
||||
public VolatilityRegime PreviousVolatilityRegime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// New volatility regime.
|
||||
/// </summary>
|
||||
public VolatilityRegime CurrentVolatilityRegime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous trend regime.
|
||||
/// </summary>
|
||||
public TrendRegime PreviousTrendRegime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// New trend regime.
|
||||
/// </summary>
|
||||
public TrendRegime CurrentTrendRegime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Transition timestamp.
|
||||
/// </summary>
|
||||
public DateTime TransitionTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional transition reason.
|
||||
/// </summary>
|
||||
public string Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a regime transition record.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <param name="previousVolatilityRegime">Previous volatility regime.</param>
|
||||
/// <param name="currentVolatilityRegime">Current volatility regime.</param>
|
||||
/// <param name="previousTrendRegime">Previous trend regime.</param>
|
||||
/// <param name="currentTrendRegime">Current trend regime.</param>
|
||||
/// <param name="transitionTime">Transition time.</param>
|
||||
/// <param name="reason">Transition reason.</param>
|
||||
public RegimeTransition(
|
||||
string symbol,
|
||||
VolatilityRegime previousVolatilityRegime,
|
||||
VolatilityRegime currentVolatilityRegime,
|
||||
TrendRegime previousTrendRegime,
|
||||
TrendRegime currentTrendRegime,
|
||||
DateTime transitionTime,
|
||||
string reason)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
Symbol = symbol;
|
||||
PreviousVolatilityRegime = previousVolatilityRegime;
|
||||
CurrentVolatilityRegime = currentVolatilityRegime;
|
||||
PreviousTrendRegime = previousTrendRegime;
|
||||
CurrentTrendRegime = currentTrendRegime;
|
||||
TransitionTime = transitionTime;
|
||||
Reason = reason ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Historical regime timeline for one symbol.
|
||||
/// </summary>
|
||||
public class RegimeHistory
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol associated with history.
|
||||
/// </summary>
|
||||
public string Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current state snapshot.
|
||||
/// </summary>
|
||||
public RegimeState CurrentState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Historical transition events.
|
||||
/// </summary>
|
||||
public List<RegimeTransition> Transitions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a regime history model.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <param name="currentState">Current regime state.</param>
|
||||
/// <param name="transitions">Transition timeline.</param>
|
||||
public RegimeHistory(
|
||||
string symbol,
|
||||
RegimeState currentState,
|
||||
List<RegimeTransition> transitions)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
Symbol = symbol;
|
||||
CurrentState = currentState;
|
||||
Transitions = transitions ?? new List<RegimeTransition>();
|
||||
}
|
||||
}
|
||||
}
|
||||
320
src/NT8.Core/Intelligence/RiskModeManager.cs
Normal file
320
src/NT8.Core/Intelligence/RiskModeManager.cs
Normal file
@@ -0,0 +1,320 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages current risk mode with automatic transitions and optional manual override.
|
||||
/// Thread-safe for all shared state operations.
|
||||
/// </summary>
|
||||
public class RiskModeManager
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
private readonly Dictionary<RiskMode, RiskModeConfig> _configs;
|
||||
private RiskModeState _state;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new risk mode manager with default mode configurations.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
public RiskModeManager(ILogger logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_configs = new Dictionary<RiskMode, RiskModeConfig>();
|
||||
|
||||
InitializeDefaultConfigs();
|
||||
|
||||
_state = new RiskModeState(
|
||||
RiskMode.PCP,
|
||||
RiskMode.PCP,
|
||||
DateTime.UtcNow,
|
||||
"Initialization",
|
||||
false,
|
||||
TimeSpan.Zero,
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates risk mode from current performance data.
|
||||
/// </summary>
|
||||
/// <param name="dailyPnL">Current daily PnL.</param>
|
||||
/// <param name="winStreak">Current win streak.</param>
|
||||
/// <param name="lossStreak">Current loss streak.</param>
|
||||
public void UpdateRiskMode(double dailyPnL, int winStreak, int lossStreak)
|
||||
{
|
||||
if (winStreak < 0)
|
||||
throw new ArgumentException("winStreak must be non-negative", "winStreak");
|
||||
if (lossStreak < 0)
|
||||
throw new ArgumentException("lossStreak must be non-negative", "lossStreak");
|
||||
|
||||
try
|
||||
{
|
||||
var metrics = new PerformanceMetrics(
|
||||
dailyPnL,
|
||||
winStreak,
|
||||
lossStreak,
|
||||
CalculateSyntheticWinRate(winStreak, lossStreak),
|
||||
0.7,
|
||||
VolatilityRegime.Normal);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_state.IsManualOverride)
|
||||
{
|
||||
UpdateModeDuration();
|
||||
return;
|
||||
}
|
||||
|
||||
var current = _state.CurrentMode;
|
||||
var next = DetermineTargetMode(current, metrics);
|
||||
|
||||
if (next != current)
|
||||
{
|
||||
TransitionMode(next, "Automatic transition by performance metrics");
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateModeDuration();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("UpdateRiskMode failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets current active risk mode.
|
||||
/// </summary>
|
||||
/// <returns>Current risk mode.</returns>
|
||||
public RiskMode GetCurrentMode()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _state.CurrentMode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets config for the specified mode.
|
||||
/// </summary>
|
||||
/// <param name="mode">Risk mode.</param>
|
||||
/// <returns>Mode configuration.</returns>
|
||||
public RiskModeConfig GetModeConfig(RiskMode mode)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_configs.ContainsKey(mode))
|
||||
throw new ArgumentException("Mode configuration not found", "mode");
|
||||
|
||||
return _configs[mode];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates whether mode should transition based on provided metrics.
|
||||
/// </summary>
|
||||
/// <param name="current">Current mode.</param>
|
||||
/// <param name="metrics">Performance metrics snapshot.</param>
|
||||
/// <returns>True when transition should occur.</returns>
|
||||
public bool ShouldTransitionMode(RiskMode current, PerformanceMetrics metrics)
|
||||
{
|
||||
if (metrics == null)
|
||||
throw new ArgumentNullException("metrics");
|
||||
|
||||
var target = DetermineTargetMode(current, metrics);
|
||||
return target != current;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces a manual mode override.
|
||||
/// </summary>
|
||||
/// <param name="mode">Target mode.</param>
|
||||
/// <param name="reason">Override reason.</param>
|
||||
public void OverrideMode(RiskMode mode, string reason)
|
||||
{
|
||||
if (string.IsNullOrEmpty(reason))
|
||||
throw new ArgumentNullException("reason");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var previous = _state.CurrentMode;
|
||||
_state = new RiskModeState(
|
||||
mode,
|
||||
previous,
|
||||
DateTime.UtcNow,
|
||||
reason,
|
||||
true,
|
||||
TimeSpan.Zero,
|
||||
_state.Metadata);
|
||||
}
|
||||
|
||||
_logger.LogWarning("Risk mode manually overridden to {0}. Reason: {1}", mode, reason);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("OverrideMode failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears manual override and resets mode to default PCP.
|
||||
/// </summary>
|
||||
public void ResetToDefault()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var previous = _state.CurrentMode;
|
||||
_state = new RiskModeState(
|
||||
RiskMode.PCP,
|
||||
previous,
|
||||
DateTime.UtcNow,
|
||||
"Reset to default",
|
||||
false,
|
||||
TimeSpan.Zero,
|
||||
_state.Metadata);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Risk mode reset to default PCP");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("ResetToDefault failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns current risk mode state snapshot.
|
||||
/// </summary>
|
||||
/// <returns>Risk mode state.</returns>
|
||||
public RiskModeState GetState()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeDefaultConfigs()
|
||||
{
|
||||
_configs.Add(
|
||||
RiskMode.ECP,
|
||||
new RiskModeConfig(
|
||||
RiskMode.ECP,
|
||||
1.5,
|
||||
TradeGrade.B,
|
||||
1500.0,
|
||||
4,
|
||||
true,
|
||||
new Dictionary<string, object>()));
|
||||
|
||||
_configs.Add(
|
||||
RiskMode.PCP,
|
||||
new RiskModeConfig(
|
||||
RiskMode.PCP,
|
||||
1.0,
|
||||
TradeGrade.B,
|
||||
1000.0,
|
||||
3,
|
||||
false,
|
||||
new Dictionary<string, object>()));
|
||||
|
||||
_configs.Add(
|
||||
RiskMode.DCP,
|
||||
new RiskModeConfig(
|
||||
RiskMode.DCP,
|
||||
0.5,
|
||||
TradeGrade.A,
|
||||
600.0,
|
||||
2,
|
||||
false,
|
||||
new Dictionary<string, object>()));
|
||||
|
||||
_configs.Add(
|
||||
RiskMode.HR,
|
||||
new RiskModeConfig(
|
||||
RiskMode.HR,
|
||||
0.0,
|
||||
TradeGrade.APlus,
|
||||
0.0,
|
||||
0,
|
||||
false,
|
||||
new Dictionary<string, object>()));
|
||||
}
|
||||
|
||||
private RiskMode DetermineTargetMode(RiskMode current, PerformanceMetrics metrics)
|
||||
{
|
||||
if (metrics.LossStreak >= 3)
|
||||
return RiskMode.HR;
|
||||
|
||||
if (metrics.VolatilityRegime == VolatilityRegime.Extreme)
|
||||
return RiskMode.HR;
|
||||
|
||||
if (metrics.DailyPnL >= 500.0 && metrics.RecentWinRate >= 0.80 && metrics.LossStreak == 0)
|
||||
return RiskMode.ECP;
|
||||
|
||||
if (metrics.DailyPnL <= -200.0 || metrics.RecentWinRate < 0.50)
|
||||
return RiskMode.DCP;
|
||||
|
||||
if (current == RiskMode.DCP && metrics.WinStreak >= 2 && metrics.RecentExecutionQuality >= 0.70)
|
||||
return RiskMode.PCP;
|
||||
|
||||
if (current == RiskMode.HR)
|
||||
{
|
||||
if (metrics.DailyPnL >= -100.0 && metrics.LossStreak <= 1)
|
||||
return RiskMode.DCP;
|
||||
|
||||
return RiskMode.HR;
|
||||
}
|
||||
|
||||
return RiskMode.PCP;
|
||||
}
|
||||
|
||||
private void TransitionMode(RiskMode nextMode, string reason)
|
||||
{
|
||||
var previous = _state.CurrentMode;
|
||||
_state = new RiskModeState(
|
||||
nextMode,
|
||||
previous,
|
||||
DateTime.UtcNow,
|
||||
reason,
|
||||
false,
|
||||
TimeSpan.Zero,
|
||||
_state.Metadata);
|
||||
|
||||
_logger.LogInformation("Risk mode transition: {0} -> {1}. Reason: {2}", previous, nextMode, reason);
|
||||
}
|
||||
|
||||
private void UpdateModeDuration()
|
||||
{
|
||||
_state.ModeDuration = DateTime.UtcNow - _state.LastTransitionAtUtc;
|
||||
}
|
||||
|
||||
private static double CalculateSyntheticWinRate(int winStreak, int lossStreak)
|
||||
{
|
||||
var denominator = winStreak + lossStreak;
|
||||
if (denominator <= 0)
|
||||
return 0.5;
|
||||
|
||||
var ratio = (double)winStreak / denominator;
|
||||
if (ratio < 0.0)
|
||||
return 0.0;
|
||||
if (ratio > 1.0)
|
||||
return 1.0;
|
||||
return ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
302
src/NT8.Core/Intelligence/RiskModeModels.cs
Normal file
302
src/NT8.Core/Intelligence/RiskModeModels.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Risk operating mode used to control trade acceptance and sizing aggressiveness.
|
||||
/// </summary>
|
||||
public enum RiskMode
|
||||
{
|
||||
/// <summary>
|
||||
/// High Risk state - no new trades allowed.
|
||||
/// </summary>
|
||||
HR = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Diminished Confidence Play - very selective and reduced size.
|
||||
/// </summary>
|
||||
DCP = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Primary Confidence Play - baseline operating mode.
|
||||
/// </summary>
|
||||
PCP = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Elevated Confidence Play - increased aggressiveness when conditions are strong.
|
||||
/// </summary>
|
||||
ECP = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for one risk mode.
|
||||
/// </summary>
|
||||
public class RiskModeConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Risk mode identity.
|
||||
/// </summary>
|
||||
public RiskMode Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Position size multiplier for this mode.
|
||||
/// </summary>
|
||||
public double SizeMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum trade grade required to allow a trade.
|
||||
/// </summary>
|
||||
public TradeGrade MinimumGrade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum daily risk allowed under this mode.
|
||||
/// </summary>
|
||||
public double MaxDailyRisk { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum concurrent open trades in this mode.
|
||||
/// </summary>
|
||||
public int MaxConcurrentTrades { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether aggressive entries are allowed.
|
||||
/// </summary>
|
||||
public bool AggressiveEntries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional mode-specific settings.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> CustomSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a risk mode configuration.
|
||||
/// </summary>
|
||||
/// <param name="mode">Risk mode identity.</param>
|
||||
/// <param name="sizeMultiplier">Size multiplier.</param>
|
||||
/// <param name="minimumGrade">Minimum accepted trade grade.</param>
|
||||
/// <param name="maxDailyRisk">Maximum daily risk.</param>
|
||||
/// <param name="maxConcurrentTrades">Maximum concurrent trades.</param>
|
||||
/// <param name="aggressiveEntries">Aggressive entry enable flag.</param>
|
||||
/// <param name="customSettings">Additional settings map.</param>
|
||||
public RiskModeConfig(
|
||||
RiskMode mode,
|
||||
double sizeMultiplier,
|
||||
TradeGrade minimumGrade,
|
||||
double maxDailyRisk,
|
||||
int maxConcurrentTrades,
|
||||
bool aggressiveEntries,
|
||||
Dictionary<string, object> customSettings)
|
||||
{
|
||||
if (sizeMultiplier < 0.0)
|
||||
throw new ArgumentException("sizeMultiplier must be non-negative", "sizeMultiplier");
|
||||
if (maxDailyRisk < 0.0)
|
||||
throw new ArgumentException("maxDailyRisk must be non-negative", "maxDailyRisk");
|
||||
if (maxConcurrentTrades < 0)
|
||||
throw new ArgumentException("maxConcurrentTrades must be non-negative", "maxConcurrentTrades");
|
||||
|
||||
Mode = mode;
|
||||
SizeMultiplier = sizeMultiplier;
|
||||
MinimumGrade = minimumGrade;
|
||||
MaxDailyRisk = maxDailyRisk;
|
||||
MaxConcurrentTrades = maxConcurrentTrades;
|
||||
AggressiveEntries = aggressiveEntries;
|
||||
CustomSettings = customSettings ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rule that governs transitions between risk modes.
|
||||
/// </summary>
|
||||
public class ModeTransitionRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Origin mode for this rule.
|
||||
/// </summary>
|
||||
public RiskMode FromMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Destination mode for this rule.
|
||||
/// </summary>
|
||||
public RiskMode ToMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable rule name.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional description for diagnostics.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables rule evaluation.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a transition rule.
|
||||
/// </summary>
|
||||
/// <param name="fromMode">Origin mode.</param>
|
||||
/// <param name="toMode">Destination mode.</param>
|
||||
/// <param name="name">Rule name.</param>
|
||||
/// <param name="description">Rule description.</param>
|
||||
/// <param name="enabled">Rule enabled flag.</param>
|
||||
public ModeTransitionRule(
|
||||
RiskMode fromMode,
|
||||
RiskMode toMode,
|
||||
string name,
|
||||
string description,
|
||||
bool enabled)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentNullException("name");
|
||||
|
||||
FromMode = fromMode;
|
||||
ToMode = toMode;
|
||||
Name = name;
|
||||
Description = description ?? string.Empty;
|
||||
Enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current risk mode state and transition metadata.
|
||||
/// </summary>
|
||||
public class RiskModeState
|
||||
{
|
||||
/// <summary>
|
||||
/// Current active risk mode.
|
||||
/// </summary>
|
||||
public RiskMode CurrentMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous mode before current transition.
|
||||
/// </summary>
|
||||
public RiskMode PreviousMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last transition timestamp in UTC.
|
||||
/// </summary>
|
||||
public DateTime LastTransitionAtUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional reason for last transition.
|
||||
/// </summary>
|
||||
public string LastTransitionReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether current mode is manually overridden.
|
||||
/// </summary>
|
||||
public bool IsManualOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current mode duration.
|
||||
/// </summary>
|
||||
public TimeSpan ModeDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional mode metadata for diagnostics.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a risk mode state model.
|
||||
/// </summary>
|
||||
/// <param name="currentMode">Current mode.</param>
|
||||
/// <param name="previousMode">Previous mode.</param>
|
||||
/// <param name="lastTransitionAtUtc">Last transition time.</param>
|
||||
/// <param name="lastTransitionReason">Transition reason.</param>
|
||||
/// <param name="isManualOverride">Manual override flag.</param>
|
||||
/// <param name="modeDuration">Current mode duration.</param>
|
||||
/// <param name="metadata">Mode metadata map.</param>
|
||||
public RiskModeState(
|
||||
RiskMode currentMode,
|
||||
RiskMode previousMode,
|
||||
DateTime lastTransitionAtUtc,
|
||||
string lastTransitionReason,
|
||||
bool isManualOverride,
|
||||
TimeSpan modeDuration,
|
||||
Dictionary<string, object> metadata)
|
||||
{
|
||||
CurrentMode = currentMode;
|
||||
PreviousMode = previousMode;
|
||||
LastTransitionAtUtc = lastTransitionAtUtc;
|
||||
LastTransitionReason = lastTransitionReason ?? string.Empty;
|
||||
IsManualOverride = isManualOverride;
|
||||
ModeDuration = modeDuration;
|
||||
Metadata = metadata ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performance snapshot used for mode transition decisions.
|
||||
/// </summary>
|
||||
public class PerformanceMetrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Current daily PnL.
|
||||
/// </summary>
|
||||
public double DailyPnL { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Consecutive winning trades.
|
||||
/// </summary>
|
||||
public int WinStreak { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Consecutive losing trades.
|
||||
/// </summary>
|
||||
public int LossStreak { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Recent win rate in range [0.0, 1.0].
|
||||
/// </summary>
|
||||
public double RecentWinRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Recent execution quality in range [0.0, 1.0].
|
||||
/// </summary>
|
||||
public double RecentExecutionQuality { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current volatility regime.
|
||||
/// </summary>
|
||||
public VolatilityRegime VolatilityRegime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a performance metrics snapshot.
|
||||
/// </summary>
|
||||
/// <param name="dailyPnL">Current daily PnL.</param>
|
||||
/// <param name="winStreak">Win streak.</param>
|
||||
/// <param name="lossStreak">Loss streak.</param>
|
||||
/// <param name="recentWinRate">Recent win rate in [0.0, 1.0].</param>
|
||||
/// <param name="recentExecutionQuality">Recent execution quality in [0.0, 1.0].</param>
|
||||
/// <param name="volatilityRegime">Current volatility regime.</param>
|
||||
public PerformanceMetrics(
|
||||
double dailyPnL,
|
||||
int winStreak,
|
||||
int lossStreak,
|
||||
double recentWinRate,
|
||||
double recentExecutionQuality,
|
||||
VolatilityRegime volatilityRegime)
|
||||
{
|
||||
if (recentWinRate < 0.0 || recentWinRate > 1.0)
|
||||
throw new ArgumentException("recentWinRate must be between 0.0 and 1.0", "recentWinRate");
|
||||
if (recentExecutionQuality < 0.0 || recentExecutionQuality > 1.0)
|
||||
throw new ArgumentException("recentExecutionQuality must be between 0.0 and 1.0", "recentExecutionQuality");
|
||||
if (winStreak < 0)
|
||||
throw new ArgumentException("winStreak must be non-negative", "winStreak");
|
||||
if (lossStreak < 0)
|
||||
throw new ArgumentException("lossStreak must be non-negative", "lossStreak");
|
||||
|
||||
DailyPnL = dailyPnL;
|
||||
WinStreak = winStreak;
|
||||
LossStreak = lossStreak;
|
||||
RecentWinRate = recentWinRate;
|
||||
RecentExecutionQuality = recentExecutionQuality;
|
||||
VolatilityRegime = volatilityRegime;
|
||||
}
|
||||
}
|
||||
}
|
||||
313
src/NT8.Core/Intelligence/TrendRegimeDetector.cs
Normal file
313
src/NT8.Core/Intelligence/TrendRegimeDetector.cs
Normal file
@@ -0,0 +1,313 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects trend regime and trend quality from recent bar data and AVWAP context.
|
||||
/// </summary>
|
||||
public class TrendRegimeDetector
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
private readonly Dictionary<string, TrendRegime> _currentRegimes;
|
||||
private readonly Dictionary<string, double> _currentStrength;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new trend regime detector.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
public TrendRegimeDetector(ILogger logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_currentRegimes = new Dictionary<string, TrendRegime>();
|
||||
_currentStrength = new Dictionary<string, double>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects trend regime for a symbol based on bars and AVWAP value.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <param name="bars">Recent bars in chronological order.</param>
|
||||
/// <param name="avwap">Current AVWAP value.</param>
|
||||
/// <returns>Detected trend regime.</returns>
|
||||
public TrendRegime DetectTrend(string symbol, List<BarData> bars, double avwap)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (bars == null)
|
||||
throw new ArgumentNullException("bars");
|
||||
if (bars.Count < 5)
|
||||
throw new ArgumentException("At least 5 bars are required", "bars");
|
||||
|
||||
try
|
||||
{
|
||||
var strength = CalculateTrendStrength(bars, avwap);
|
||||
var regime = ClassifyTrendRegime(strength);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_currentRegimes[symbol] = regime;
|
||||
_currentStrength[symbol] = strength;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Trend regime detected for {0}: Regime={1}, Strength={2:F4}", symbol, regime, strength);
|
||||
return regime;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("DetectTrend failed for {0}: {1}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates trend strength in range [-1.0, +1.0].
|
||||
/// Positive values indicate uptrend and negative values indicate downtrend.
|
||||
/// </summary>
|
||||
/// <param name="bars">Recent bars in chronological order.</param>
|
||||
/// <param name="avwap">Current AVWAP value.</param>
|
||||
/// <returns>Trend strength score.</returns>
|
||||
public double CalculateTrendStrength(List<BarData> bars, double avwap)
|
||||
{
|
||||
if (bars == null)
|
||||
throw new ArgumentNullException("bars");
|
||||
if (bars.Count < 5)
|
||||
throw new ArgumentException("At least 5 bars are required", "bars");
|
||||
|
||||
try
|
||||
{
|
||||
var last = bars[bars.Count - 1];
|
||||
|
||||
var lookback = bars.Count >= 10 ? 10 : bars.Count;
|
||||
var past = bars[bars.Count - lookback];
|
||||
var slopePerBar = (last.Close - past.Close) / lookback;
|
||||
|
||||
var range = EstimateAverageRange(bars, lookback);
|
||||
var normalizedSlope = range > 0.0 ? slopePerBar / range : 0.0;
|
||||
|
||||
var aboveAvwapCount = 0;
|
||||
var belowAvwapCount = 0;
|
||||
var startIndex = bars.Count - lookback;
|
||||
for (var i = startIndex; i < bars.Count; i++)
|
||||
{
|
||||
if (bars[i].Close > avwap)
|
||||
aboveAvwapCount++;
|
||||
else if (bars[i].Close < avwap)
|
||||
belowAvwapCount++;
|
||||
}
|
||||
|
||||
var avwapBias = 0.0;
|
||||
if (lookback > 0)
|
||||
avwapBias = (double)(aboveAvwapCount - belowAvwapCount) / lookback;
|
||||
|
||||
var structureBias = CalculateStructureBias(bars, lookback);
|
||||
|
||||
var strength = (normalizedSlope * 0.45) + (avwapBias * 0.35) + (structureBias * 0.20);
|
||||
strength = Clamp(strength, -1.0, 1.0);
|
||||
return strength;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("CalculateTrendStrength failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether bars are ranging based on normalized trend strength threshold.
|
||||
/// </summary>
|
||||
/// <param name="bars">Recent bars.</param>
|
||||
/// <param name="threshold">Absolute strength threshold that defines range state.</param>
|
||||
/// <returns>True when market appears to be ranging.</returns>
|
||||
public bool IsRanging(List<BarData> bars, double threshold)
|
||||
{
|
||||
if (bars == null)
|
||||
throw new ArgumentNullException("bars");
|
||||
if (bars.Count < 5)
|
||||
throw new ArgumentException("At least 5 bars are required", "bars");
|
||||
if (threshold <= 0.0)
|
||||
throw new ArgumentException("threshold must be greater than zero", "threshold");
|
||||
|
||||
try
|
||||
{
|
||||
var avwap = bars[bars.Count - 1].Close;
|
||||
var strength = CalculateTrendStrength(bars, avwap);
|
||||
return Math.Abs(strength) < threshold;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("IsRanging failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assesses trend quality using structure consistency and volatility noise.
|
||||
/// </summary>
|
||||
/// <param name="bars">Recent bars.</param>
|
||||
/// <returns>Trend quality classification.</returns>
|
||||
public TrendQuality AssessTrendQuality(List<BarData> bars)
|
||||
{
|
||||
if (bars == null)
|
||||
throw new ArgumentNullException("bars");
|
||||
if (bars.Count < 5)
|
||||
throw new ArgumentException("At least 5 bars are required", "bars");
|
||||
|
||||
try
|
||||
{
|
||||
var lookback = bars.Count >= 10 ? 10 : bars.Count;
|
||||
var structure = Math.Abs(CalculateStructureBias(bars, lookback));
|
||||
var noise = CalculateNoiseRatio(bars, lookback);
|
||||
|
||||
var qualityScore = (structure * 0.65) + ((1.0 - noise) * 0.35);
|
||||
qualityScore = Clamp(qualityScore, 0.0, 1.0);
|
||||
|
||||
if (qualityScore >= 0.80)
|
||||
return TrendQuality.Excellent;
|
||||
if (qualityScore >= 0.60)
|
||||
return TrendQuality.Good;
|
||||
if (qualityScore >= 0.40)
|
||||
return TrendQuality.Fair;
|
||||
|
||||
return TrendQuality.Poor;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("AssessTrendQuality failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets last detected trend regime for a symbol.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <returns>Current trend regime or Range when unknown.</returns>
|
||||
public TrendRegime GetCurrentTrendRegime(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
TrendRegime regime;
|
||||
if (_currentRegimes.TryGetValue(symbol, out regime))
|
||||
return regime;
|
||||
|
||||
return TrendRegime.Range;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets last detected trend strength for a symbol.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <returns>Trend strength or zero when unknown.</returns>
|
||||
public double GetCurrentTrendStrength(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
double strength;
|
||||
if (_currentStrength.TryGetValue(symbol, out strength))
|
||||
return strength;
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
private static TrendRegime ClassifyTrendRegime(double strength)
|
||||
{
|
||||
if (strength >= 0.80)
|
||||
return TrendRegime.StrongUp;
|
||||
if (strength >= 0.30)
|
||||
return TrendRegime.WeakUp;
|
||||
if (strength <= -0.80)
|
||||
return TrendRegime.StrongDown;
|
||||
if (strength <= -0.30)
|
||||
return TrendRegime.WeakDown;
|
||||
|
||||
return TrendRegime.Range;
|
||||
}
|
||||
|
||||
private static double EstimateAverageRange(List<BarData> bars, int lookback)
|
||||
{
|
||||
if (lookback <= 0)
|
||||
return 0.0;
|
||||
|
||||
var sum = 0.0;
|
||||
var start = bars.Count - lookback;
|
||||
for (var i = start; i < bars.Count; i++)
|
||||
{
|
||||
sum += (bars[i].High - bars[i].Low);
|
||||
}
|
||||
|
||||
return sum / lookback;
|
||||
}
|
||||
|
||||
private static double CalculateStructureBias(List<BarData> bars, int lookback)
|
||||
{
|
||||
var start = bars.Count - lookback;
|
||||
var upStructure = 0;
|
||||
var downStructure = 0;
|
||||
|
||||
for (var i = start + 1; i < bars.Count; i++)
|
||||
{
|
||||
var prev = bars[i - 1];
|
||||
var cur = bars[i];
|
||||
|
||||
var higherHigh = cur.High > prev.High;
|
||||
var higherLow = cur.Low > prev.Low;
|
||||
var lowerHigh = cur.High < prev.High;
|
||||
var lowerLow = cur.Low < prev.Low;
|
||||
|
||||
if (higherHigh && higherLow)
|
||||
upStructure++;
|
||||
else if (lowerHigh && lowerLow)
|
||||
downStructure++;
|
||||
}
|
||||
|
||||
var transitions = lookback - 1;
|
||||
if (transitions <= 0)
|
||||
return 0.0;
|
||||
|
||||
return (double)(upStructure - downStructure) / transitions;
|
||||
}
|
||||
|
||||
private static double CalculateNoiseRatio(List<BarData> bars, int lookback)
|
||||
{
|
||||
var start = bars.Count - lookback;
|
||||
var directionalMove = Math.Abs(bars[bars.Count - 1].Close - bars[start].Close);
|
||||
|
||||
var path = 0.0;
|
||||
for (var i = start + 1; i < bars.Count; i++)
|
||||
{
|
||||
path += Math.Abs(bars[i].Close - bars[i - 1].Close);
|
||||
}
|
||||
|
||||
if (path <= 0.0)
|
||||
return 1.0;
|
||||
|
||||
var efficiency = directionalMove / path;
|
||||
efficiency = Clamp(efficiency, 0.0, 1.0);
|
||||
return 1.0 - efficiency;
|
||||
}
|
||||
|
||||
private static double Clamp(double value, double min, double max)
|
||||
{
|
||||
if (value < min)
|
||||
return min;
|
||||
if (value > max)
|
||||
return max;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
251
src/NT8.Core/Intelligence/VolatilityRegimeDetector.cs
Normal file
251
src/NT8.Core/Intelligence/VolatilityRegimeDetector.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects volatility regimes from current and normal ATR values and tracks transitions.
|
||||
/// </summary>
|
||||
public class VolatilityRegimeDetector
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
private readonly Dictionary<string, VolatilityRegime> _currentRegimes;
|
||||
private readonly Dictionary<string, DateTime> _lastTransitionTimes;
|
||||
private readonly Dictionary<string, List<RegimeTransition>> _transitionHistory;
|
||||
private readonly int _maxHistoryPerSymbol;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new volatility regime detector.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="maxHistoryPerSymbol">Maximum transitions to keep per symbol.</param>
|
||||
/// <exception cref="ArgumentNullException">Logger is null.</exception>
|
||||
/// <exception cref="ArgumentException">History size is not positive.</exception>
|
||||
public VolatilityRegimeDetector(ILogger logger, int maxHistoryPerSymbol)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
if (maxHistoryPerSymbol <= 0)
|
||||
throw new ArgumentException("maxHistoryPerSymbol must be greater than zero", "maxHistoryPerSymbol");
|
||||
|
||||
_logger = logger;
|
||||
_maxHistoryPerSymbol = maxHistoryPerSymbol;
|
||||
_currentRegimes = new Dictionary<string, VolatilityRegime>();
|
||||
_lastTransitionTimes = new Dictionary<string, DateTime>();
|
||||
_transitionHistory = new Dictionary<string, List<RegimeTransition>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects the current volatility regime for a symbol.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <param name="currentAtr">Current ATR value.</param>
|
||||
/// <param name="normalAtr">Normal ATR baseline value.</param>
|
||||
/// <returns>Detected volatility regime.</returns>
|
||||
public VolatilityRegime DetectRegime(string symbol, double currentAtr, double normalAtr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (currentAtr < 0.0)
|
||||
throw new ArgumentException("currentAtr must be non-negative", "currentAtr");
|
||||
if (normalAtr <= 0.0)
|
||||
throw new ArgumentException("normalAtr must be greater than zero", "normalAtr");
|
||||
|
||||
try
|
||||
{
|
||||
var score = CalculateVolatilityScore(currentAtr, normalAtr);
|
||||
var regime = ClassifyRegime(score);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
VolatilityRegime previous;
|
||||
var hasPrevious = _currentRegimes.TryGetValue(symbol, out previous);
|
||||
|
||||
if (hasPrevious && IsRegimeTransition(regime, previous))
|
||||
{
|
||||
AddTransition(symbol, previous, regime, "ATR ratio threshold crossed");
|
||||
}
|
||||
else if (!hasPrevious)
|
||||
{
|
||||
_lastTransitionTimes[symbol] = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
_currentRegimes[symbol] = regime;
|
||||
}
|
||||
|
||||
return regime;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("DetectRegime failed for {0}: {1}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when current and previous regimes differ.
|
||||
/// </summary>
|
||||
/// <param name="current">Current regime.</param>
|
||||
/// <param name="previous">Previous regime.</param>
|
||||
/// <returns>True when transition occurred.</returns>
|
||||
public bool IsRegimeTransition(VolatilityRegime current, VolatilityRegime previous)
|
||||
{
|
||||
return current != previous;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates volatility score as current ATR divided by normal ATR.
|
||||
/// </summary>
|
||||
/// <param name="currentAtr">Current ATR value.</param>
|
||||
/// <param name="normalAtr">Normal ATR baseline value.</param>
|
||||
/// <returns>Volatility score ratio.</returns>
|
||||
public double CalculateVolatilityScore(double currentAtr, double normalAtr)
|
||||
{
|
||||
if (currentAtr < 0.0)
|
||||
throw new ArgumentException("currentAtr must be non-negative", "currentAtr");
|
||||
if (normalAtr <= 0.0)
|
||||
throw new ArgumentException("normalAtr must be greater than zero", "normalAtr");
|
||||
|
||||
return currentAtr / normalAtr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates internal regime history for a symbol with an externally provided regime.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <param name="regime">Detected regime.</param>
|
||||
public void UpdateRegimeHistory(string symbol, VolatilityRegime regime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
VolatilityRegime previous;
|
||||
var hasPrevious = _currentRegimes.TryGetValue(symbol, out previous);
|
||||
|
||||
if (hasPrevious && IsRegimeTransition(regime, previous))
|
||||
{
|
||||
AddTransition(symbol, previous, regime, "External regime update");
|
||||
}
|
||||
else if (!hasPrevious)
|
||||
{
|
||||
_lastTransitionTimes[symbol] = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
_currentRegimes[symbol] = regime;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("UpdateRegimeHistory failed for {0}: {1}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current volatility regime for a symbol.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <returns>Current regime or Normal when unknown.</returns>
|
||||
public VolatilityRegime GetCurrentRegime(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
VolatilityRegime regime;
|
||||
if (_currentRegimes.TryGetValue(symbol, out regime))
|
||||
return regime;
|
||||
|
||||
return VolatilityRegime.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns duration spent in the current regime for a symbol.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <returns>Time elapsed since last transition, or zero if unknown.</returns>
|
||||
public TimeSpan GetRegimeDuration(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
DateTime transitionTime;
|
||||
if (_lastTransitionTimes.TryGetValue(symbol, out transitionTime))
|
||||
return DateTime.UtcNow - transitionTime;
|
||||
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets recent transition events for a symbol.
|
||||
/// </summary>
|
||||
/// <param name="symbol">Instrument symbol.</param>
|
||||
/// <returns>Transition list in chronological order.</returns>
|
||||
public List<RegimeTransition> GetTransitions(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_transitionHistory.ContainsKey(symbol))
|
||||
return new List<RegimeTransition>();
|
||||
|
||||
return new List<RegimeTransition>(_transitionHistory[symbol]);
|
||||
}
|
||||
}
|
||||
|
||||
private VolatilityRegime ClassifyRegime(double score)
|
||||
{
|
||||
if (score < 0.6)
|
||||
return VolatilityRegime.Low;
|
||||
if (score < 0.8)
|
||||
return VolatilityRegime.BelowNormal;
|
||||
if (score <= 1.2)
|
||||
return VolatilityRegime.Normal;
|
||||
if (score <= 1.5)
|
||||
return VolatilityRegime.Elevated;
|
||||
if (score <= 2.0)
|
||||
return VolatilityRegime.High;
|
||||
|
||||
return VolatilityRegime.Extreme;
|
||||
}
|
||||
|
||||
private void AddTransition(string symbol, VolatilityRegime previous, VolatilityRegime current, string reason)
|
||||
{
|
||||
if (!_transitionHistory.ContainsKey(symbol))
|
||||
_transitionHistory.Add(symbol, new List<RegimeTransition>());
|
||||
|
||||
var transition = new RegimeTransition(
|
||||
symbol,
|
||||
previous,
|
||||
current,
|
||||
TrendRegime.Range,
|
||||
TrendRegime.Range,
|
||||
DateTime.UtcNow,
|
||||
reason);
|
||||
|
||||
_transitionHistory[symbol].Add(transition);
|
||||
|
||||
while (_transitionHistory[symbol].Count > _maxHistoryPerSymbol)
|
||||
{
|
||||
_transitionHistory[symbol].RemoveAt(0);
|
||||
}
|
||||
|
||||
_lastTransitionTimes[symbol] = transition.TransitionTime;
|
||||
|
||||
_logger.LogInformation("Volatility regime transition for {0}: {1} -> {2}", symbol, previous, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
313
src/NT8.Core/MarketData/LiquidityMonitor.cs
Normal file
313
src/NT8.Core/MarketData/LiquidityMonitor.cs
Normal file
@@ -0,0 +1,313 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NT8.Core.MarketData
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitors liquidity conditions for symbols and calculates liquidity scores
|
||||
/// </summary>
|
||||
public class LiquidityMonitor
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
// Store spread information for each symbol
|
||||
private readonly Dictionary<string, Queue<double>> _spreadHistory;
|
||||
private readonly Dictionary<string, SpreadInfo> _currentSpreads;
|
||||
private readonly Dictionary<string, LiquidityMetrics> _liquidityMetrics;
|
||||
|
||||
// Default window size for rolling spread calculations
|
||||
private const int SPREAD_WINDOW_SIZE = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for LiquidityMonitor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance</param>
|
||||
public LiquidityMonitor(ILogger<LiquidityMonitor> logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_spreadHistory = new Dictionary<string, Queue<double>>();
|
||||
_currentSpreads = new Dictionary<string, SpreadInfo>();
|
||||
_liquidityMetrics = new Dictionary<string, LiquidityMetrics>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the spread information for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to update</param>
|
||||
/// <param name="bid">Current bid price</param>
|
||||
/// <param name="ask">Current ask price</param>
|
||||
/// <param name="volume">Current volume</param>
|
||||
public void UpdateSpread(string symbol, double bid, double ask, long volume)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var spreadInfo = new SpreadInfo(symbol, bid, ask, DateTime.UtcNow);
|
||||
|
||||
// Update current spreads
|
||||
_currentSpreads[symbol] = spreadInfo;
|
||||
|
||||
// Maintain rolling window of spread history
|
||||
if (!_spreadHistory.ContainsKey(symbol))
|
||||
{
|
||||
_spreadHistory[symbol] = new Queue<double>();
|
||||
}
|
||||
|
||||
var spreadQueue = _spreadHistory[symbol];
|
||||
spreadQueue.Enqueue(spreadInfo.Spread);
|
||||
|
||||
// Keep only the last N spreads
|
||||
while (spreadQueue.Count > SPREAD_WINDOW_SIZE)
|
||||
{
|
||||
spreadQueue.Dequeue();
|
||||
}
|
||||
|
||||
_logger.LogDebug("Updated spread for {Symbol}: {Spread:F4}", symbol, spreadInfo.Spread);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to update spread for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets liquidity metrics for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to get metrics for</param>
|
||||
/// <returns>Liquidity metrics for the symbol</returns>
|
||||
public LiquidityMetrics GetLiquidityMetrics(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_currentSpreads.ContainsKey(symbol))
|
||||
{
|
||||
var currentSpread = _currentSpreads[symbol];
|
||||
|
||||
// Calculate average spread from history
|
||||
double averageSpread = 0;
|
||||
if (_spreadHistory.ContainsKey(symbol) && _spreadHistory[symbol].Count > 0)
|
||||
{
|
||||
var spreads = _spreadHistory[symbol].ToList();
|
||||
averageSpread = spreads.Average();
|
||||
}
|
||||
|
||||
// For now, we'll use placeholder values for volume-based metrics
|
||||
// In a real implementation, these would come from order book data
|
||||
var metrics = new LiquidityMetrics(
|
||||
symbol,
|
||||
currentSpread.Spread,
|
||||
averageSpread,
|
||||
1000, // Placeholder bid volume
|
||||
1000, // Placeholder ask volume
|
||||
10000, // Placeholder total depth
|
||||
10, // Placeholder order levels
|
||||
DateTime.UtcNow
|
||||
);
|
||||
|
||||
_liquidityMetrics[symbol] = metrics;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
// Return default metrics if no data available
|
||||
return new LiquidityMetrics(
|
||||
symbol,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
DateTime.UtcNow
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get liquidity metrics for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates liquidity score for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to calculate score for</param>
|
||||
/// <returns>Liquidity score for the symbol</returns>
|
||||
public LiquidityScore CalculateLiquidityScore(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
var metrics = GetLiquidityMetrics(symbol);
|
||||
|
||||
// Calculate score based on spread and depth
|
||||
// Lower spread and higher depth = better liquidity
|
||||
double normalizedSpread = metrics.Spread > 0 ? metrics.Spread / metrics.AverageSpread : double.MaxValue;
|
||||
|
||||
// Depth score (higher is better)
|
||||
double depthScore = metrics.TotalDepth > 0 ? Math.Min(metrics.TotalDepth / 10000.0, 1.0) : 0;
|
||||
|
||||
// Volume score (higher is better)
|
||||
double volumeScore = (metrics.BidVolume + metrics.AskVolume) > 0 ?
|
||||
Math.Min((metrics.BidVolume + metrics.AskVolume) / 5000.0, 1.0) : 0;
|
||||
|
||||
// Calculate overall score based on spread and depth
|
||||
if (normalizedSpread >= 2.0 || metrics.Spread <= 0)
|
||||
{
|
||||
return LiquidityScore.Poor;
|
||||
}
|
||||
else if (normalizedSpread >= 1.5)
|
||||
{
|
||||
return LiquidityScore.Fair;
|
||||
}
|
||||
else if (normalizedSpread >= 1.0)
|
||||
{
|
||||
return LiquidityScore.Good;
|
||||
}
|
||||
else
|
||||
{
|
||||
return LiquidityScore.Excellent;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to calculate liquidity score for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if liquidity is acceptable for a symbol based on threshold
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to check</param>
|
||||
/// <param name="threshold">Minimum acceptable liquidity score</param>
|
||||
/// <returns>True if liquidity is acceptable, false otherwise</returns>
|
||||
public bool IsLiquidityAcceptable(string symbol, LiquidityScore threshold)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
var currentScore = CalculateLiquidityScore(symbol);
|
||||
return currentScore >= threshold;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to check liquidity acceptability for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current spread for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to get spread for</param>
|
||||
/// <returns>Current spread for the symbol</returns>
|
||||
public double GetCurrentSpread(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_currentSpreads.ContainsKey(symbol))
|
||||
{
|
||||
return _currentSpreads[symbol].Spread;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get current spread for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the average spread for a symbol based on historical data
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to get average spread for</param>
|
||||
/// <returns>Average spread for the symbol</returns>
|
||||
public double GetAverageSpread(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_spreadHistory.ContainsKey(symbol) && _spreadHistory[symbol].Count > 0)
|
||||
{
|
||||
return _spreadHistory[symbol].Average();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get average spread for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears spread history for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to clear history for</param>
|
||||
public void ClearSpreadHistory(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_spreadHistory.ContainsKey(symbol))
|
||||
{
|
||||
_spreadHistory[symbol].Clear();
|
||||
}
|
||||
if (_currentSpreads.ContainsKey(symbol))
|
||||
{
|
||||
_currentSpreads.Remove(symbol);
|
||||
}
|
||||
if (_liquidityMetrics.ContainsKey(symbol))
|
||||
{
|
||||
_liquidityMetrics.Remove(symbol);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Cleared spread history for {Symbol}", symbol);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to clear spread history for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
321
src/NT8.Core/MarketData/MarketMicrostructureModels.cs
Normal file
321
src/NT8.Core/MarketData/MarketMicrostructureModels.cs
Normal file
@@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NT8.Core.MarketData
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents bid-ask spread information for a symbol
|
||||
/// </summary>
|
||||
public class SpreadInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol for the spread information
|
||||
/// </summary>
|
||||
public string Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current bid price
|
||||
/// </summary>
|
||||
public double Bid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current ask price
|
||||
/// </summary>
|
||||
public double Ask { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculated spread value (ask - bid)
|
||||
/// </summary>
|
||||
public double Spread { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Spread as percentage of midpoint
|
||||
/// </summary>
|
||||
public double SpreadPercentage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of the spread measurement
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for SpreadInfo
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol for the spread</param>
|
||||
/// <param name="bid">Bid price</param>
|
||||
/// <param name="ask">Ask price</param>
|
||||
/// <param name="timestamp">Timestamp of measurement</param>
|
||||
public SpreadInfo(string symbol, double bid, double ask, DateTime timestamp)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
Symbol = symbol;
|
||||
Bid = bid;
|
||||
Ask = ask;
|
||||
Spread = ask - bid;
|
||||
SpreadPercentage = Spread > 0 && bid > 0 ? (Spread / ((bid + ask) / 2.0)) * 100 : 0;
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metrics representing liquidity conditions for a symbol
|
||||
/// </summary>
|
||||
public class LiquidityMetrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol for the liquidity metrics
|
||||
/// </summary>
|
||||
public string Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current bid-ask spread
|
||||
/// </summary>
|
||||
public double Spread { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average spread over recent period
|
||||
/// </summary>
|
||||
public double AverageSpread { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bid volume at best bid level
|
||||
/// </summary>
|
||||
public long BidVolume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ask volume at best ask level
|
||||
/// </summary>
|
||||
public long AskVolume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total depth in the order book
|
||||
/// </summary>
|
||||
public long TotalDepth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of orders at each level
|
||||
/// </summary>
|
||||
public int OrderLevels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of the metrics
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for LiquidityMetrics
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol for the metrics</param>
|
||||
/// <param name="spread">Current spread</param>
|
||||
/// <param name="averageSpread">Average spread</param>
|
||||
/// <param name="bidVolume">Bid volume</param>
|
||||
/// <param name="askVolume">Ask volume</param>
|
||||
/// <param name="totalDepth">Total order book depth</param>
|
||||
/// <param name="orderLevels">Number of order levels</param>
|
||||
/// <param name="timestamp">Timestamp of metrics</param>
|
||||
public LiquidityMetrics(
|
||||
string symbol,
|
||||
double spread,
|
||||
double averageSpread,
|
||||
long bidVolume,
|
||||
long askVolume,
|
||||
long totalDepth,
|
||||
int orderLevels,
|
||||
DateTime timestamp)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
Symbol = symbol;
|
||||
Spread = spread;
|
||||
AverageSpread = averageSpread;
|
||||
BidVolume = bidVolume;
|
||||
AskVolume = askVolume;
|
||||
TotalDepth = totalDepth;
|
||||
OrderLevels = orderLevels;
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about the current trading session
|
||||
/// </summary>
|
||||
public class SessionInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol for the session information
|
||||
/// </summary>
|
||||
public string Symbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current trading session type
|
||||
/// </summary>
|
||||
public TradingSession Session { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start time of current session
|
||||
/// </summary>
|
||||
public DateTime SessionStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// End time of current session
|
||||
/// </summary>
|
||||
public DateTime SessionEnd { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time remaining in current session
|
||||
/// </summary>
|
||||
public TimeSpan TimeRemaining { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is regular trading hours
|
||||
/// </summary>
|
||||
public bool IsRegularHours { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for SessionInfo
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol for the session</param>
|
||||
/// <param name="session">Current session type</param>
|
||||
/// <param name="sessionStart">Session start time</param>
|
||||
/// <param name="sessionEnd">Session end time</param>
|
||||
/// <param name="timeRemaining">Time remaining in session</param>
|
||||
/// <param name="isRegularHours">Whether it's regular trading hours</param>
|
||||
public SessionInfo(
|
||||
string symbol,
|
||||
TradingSession session,
|
||||
DateTime sessionStart,
|
||||
DateTime sessionEnd,
|
||||
TimeSpan timeRemaining,
|
||||
bool isRegularHours)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
Symbol = symbol;
|
||||
Session = session;
|
||||
SessionStart = sessionStart;
|
||||
SessionEnd = sessionEnd;
|
||||
TimeRemaining = timeRemaining;
|
||||
IsRegularHours = isRegularHours;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about contract roll status
|
||||
/// </summary>
|
||||
public class ContractRollInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Base symbol (e.g., ES for ESZ24, ESH25)
|
||||
/// </summary>
|
||||
public string BaseSymbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current active contract
|
||||
/// </summary>
|
||||
public string ActiveContract { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Next contract to roll to
|
||||
/// </summary>
|
||||
public string NextContract { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date of the roll
|
||||
/// </summary>
|
||||
public DateTime RollDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Days remaining until roll
|
||||
/// </summary>
|
||||
public int DaysToRoll { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether currently in roll period
|
||||
/// </summary>
|
||||
public bool IsRollPeriod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for ContractRollInfo
|
||||
/// </summary>
|
||||
/// <param name="baseSymbol">Base symbol</param>
|
||||
/// <param name="activeContract">Current active contract</param>
|
||||
/// <param name="nextContract">Next contract to roll to</param>
|
||||
/// <param name="rollDate">Roll date</param>
|
||||
/// <param name="daysToRoll">Days until roll</param>
|
||||
/// <param name="isRollPeriod">Whether in roll period</param>
|
||||
public ContractRollInfo(
|
||||
string baseSymbol,
|
||||
string activeContract,
|
||||
string nextContract,
|
||||
DateTime rollDate,
|
||||
int daysToRoll,
|
||||
bool isRollPeriod)
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseSymbol))
|
||||
throw new ArgumentNullException("baseSymbol");
|
||||
|
||||
BaseSymbol = baseSymbol;
|
||||
ActiveContract = activeContract;
|
||||
NextContract = nextContract;
|
||||
RollDate = rollDate;
|
||||
DaysToRoll = daysToRoll;
|
||||
IsRollPeriod = isRollPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing liquidity quality score
|
||||
/// </summary>
|
||||
public enum LiquidityScore
|
||||
{
|
||||
/// <summary>
|
||||
/// Very poor liquidity conditions
|
||||
/// </summary>
|
||||
Poor = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Fair liquidity conditions
|
||||
/// </summary>
|
||||
Fair = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Good liquidity conditions
|
||||
/// </summary>
|
||||
Good = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Excellent liquidity conditions
|
||||
/// </summary>
|
||||
Excellent = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing different trading sessions
|
||||
/// </summary>
|
||||
public enum TradingSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Pre-market session
|
||||
/// </summary>
|
||||
PreMarket = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Regular trading hours
|
||||
/// </summary>
|
||||
RTH = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Extended trading hours
|
||||
/// </summary>
|
||||
ETH = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Market closed
|
||||
/// </summary>
|
||||
Closed = 3
|
||||
}
|
||||
}
|
||||
484
src/NT8.Core/MarketData/SessionManager.cs
Normal file
484
src/NT8.Core/MarketData/SessionManager.cs
Normal file
@@ -0,0 +1,484 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NT8.Core.MarketData
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages trading session information for different symbols
|
||||
/// </summary>
|
||||
public class SessionManager
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
// Store session information for each symbol
|
||||
private readonly Dictionary<string, SessionInfo> _sessionCache;
|
||||
private readonly Dictionary<string, ContractRollInfo> _contractRollCache;
|
||||
|
||||
// Helper class to store session times
|
||||
private class SessionTimes
|
||||
{
|
||||
public TimeSpan Start { get; set; }
|
||||
public TimeSpan End { get; set; }
|
||||
|
||||
public SessionTimes(TimeSpan start, TimeSpan end)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
}
|
||||
|
||||
// Default session times (EST timezone for US futures)
|
||||
private readonly Dictionary<string, SessionTimes> _defaultSessions;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for SessionManager
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance</param>
|
||||
public SessionManager(ILogger<SessionManager> logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_sessionCache = new Dictionary<string, SessionInfo>();
|
||||
_contractRollCache = new Dictionary<string, ContractRollInfo>();
|
||||
|
||||
// Initialize default session times for common futures symbols
|
||||
_defaultSessions = new Dictionary<string, SessionTimes>
|
||||
{
|
||||
// E-mini S&P 500 (ES)
|
||||
{ "ES", new SessionTimes(new TimeSpan(8, 30, 0), new TimeSpan(15, 15, 0)) },
|
||||
|
||||
// E-mini Nasdaq 100 (NQ)
|
||||
{ "NQ", new SessionTimes(new TimeSpan(8, 30, 0), new TimeSpan(15, 15, 0)) },
|
||||
|
||||
// E-mini Dow Jones (YM)
|
||||
{ "YM", new SessionTimes(new TimeSpan(8, 30, 0), new TimeSpan(15, 15, 0)) },
|
||||
|
||||
// Crude Oil (CL)
|
||||
{ "CL", new SessionTimes(new TimeSpan(9, 0, 0), new TimeSpan(14, 30, 0)) },
|
||||
|
||||
// Gold (GC)
|
||||
{ "GC", new SessionTimes(new TimeSpan(17, 0, 0), new TimeSpan(16, 0, 0)) }, // Overnight session
|
||||
|
||||
// Treasury Bonds (ZN)
|
||||
{ "ZN", new SessionTimes(new TimeSpan(8, 20, 0), new TimeSpan(15, 0, 0)) }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current trading session for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to get session for</param>
|
||||
/// <param name="time">Time to check session for</param>
|
||||
/// <returns>Session information for the symbol</returns>
|
||||
public SessionInfo GetCurrentSession(string symbol, DateTime time)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// Convert time to EST for comparison (assuming US futures)
|
||||
var estTime = TimeZoneInfo.ConvertTimeFromUtc(time,
|
||||
TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"));
|
||||
|
||||
var currentTime = estTime.TimeOfDay;
|
||||
|
||||
// Check if we have specific session info for this symbol
|
||||
if (_sessionCache.ContainsKey(symbol))
|
||||
{
|
||||
var cached = _sessionCache[symbol];
|
||||
if (cached.SessionStart.Date == estTime.Date)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine session based on default times
|
||||
var sessionType = TradingSession.Closed;
|
||||
var isRegularHours = false;
|
||||
TimeSpan sessionStart = TimeSpan.Zero;
|
||||
TimeSpan sessionEnd = TimeSpan.Zero;
|
||||
|
||||
if (_defaultSessions.ContainsKey(symbol))
|
||||
{
|
||||
var sessionTimes = _defaultSessions[symbol];
|
||||
var start = sessionTimes.Start;
|
||||
var end = sessionTimes.End;
|
||||
|
||||
// Handle overnight sessions (end time before start time)
|
||||
if (sessionTimes.End < sessionTimes.Start)
|
||||
{
|
||||
// Overnight session (e.g., GC)
|
||||
if (currentTime >= sessionTimes.Start || currentTime < sessionTimes.End)
|
||||
{
|
||||
sessionType = TradingSession.RTH;
|
||||
isRegularHours = true;
|
||||
sessionStart = sessionTimes.Start;
|
||||
|
||||
// If current time is before end, session continues to next day
|
||||
if (currentTime < sessionTimes.End)
|
||||
{
|
||||
sessionEnd = sessionTimes.End;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Session continues until next day's end time
|
||||
sessionEnd = sessionTimes.End.Add(TimeSpan.FromDays(1));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if we're in the overnight session that started yesterday
|
||||
if (currentTime < sessionTimes.End)
|
||||
{
|
||||
// Check if we're continuing from yesterday's session
|
||||
var yesterday = estTime.AddDays(-1);
|
||||
if (currentTime >= sessionTimes.Start || currentTime < sessionTimes.End)
|
||||
{
|
||||
sessionType = TradingSession.RTH;
|
||||
isRegularHours = true;
|
||||
sessionStart = sessionTimes.Start;
|
||||
sessionEnd = sessionTimes.End.Add(TimeSpan.FromDays(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular session (start to end same day)
|
||||
if (currentTime >= sessionTimes.Start && currentTime < sessionTimes.End)
|
||||
{
|
||||
sessionType = TradingSession.RTH;
|
||||
isRegularHours = true;
|
||||
sessionStart = sessionTimes.Start;
|
||||
sessionEnd = sessionTimes.End;
|
||||
}
|
||||
else if (currentTime < sessionTimes.Start)
|
||||
{
|
||||
sessionType = TradingSession.PreMarket;
|
||||
sessionStart = sessionTimes.Start;
|
||||
sessionEnd = sessionTimes.End;
|
||||
}
|
||||
else if (currentTime >= sessionTimes.End)
|
||||
{
|
||||
sessionType = TradingSession.ETH;
|
||||
sessionStart = sessionTimes.Start;
|
||||
sessionEnd = sessionTimes.End;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default to closed if no specific session info
|
||||
sessionType = TradingSession.Closed;
|
||||
}
|
||||
|
||||
// Calculate time remaining in session
|
||||
TimeSpan timeRemaining = TimeSpan.Zero;
|
||||
if (sessionType == TradingSession.RTH)
|
||||
{
|
||||
timeRemaining = sessionEnd > currentTime ? sessionEnd - currentTime : TimeSpan.Zero;
|
||||
}
|
||||
|
||||
var sessionInfo = new SessionInfo(
|
||||
symbol,
|
||||
sessionType,
|
||||
new DateTime(estTime.Year, estTime.Month, estTime.Day) + sessionStart,
|
||||
new DateTime(estTime.Year, estTime.Month, estTime.Day) + sessionEnd,
|
||||
timeRemaining,
|
||||
isRegularHours
|
||||
);
|
||||
|
||||
// Cache the session info
|
||||
_sessionCache[symbol] = sessionInfo;
|
||||
|
||||
_logger.LogDebug("Session for {Symbol} at {Time}: {SessionType} (RTH: {IsRTH})",
|
||||
symbol, time, sessionType, isRegularHours);
|
||||
|
||||
return sessionInfo;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get current session for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if it's regular trading hours for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to check</param>
|
||||
/// <param name="time">Time to check</param>
|
||||
/// <returns>True if it's regular trading hours, false otherwise</returns>
|
||||
public bool IsRegularTradingHours(string symbol, DateTime time)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
var sessionInfo = GetCurrentSession(symbol, time);
|
||||
return sessionInfo.IsRegularHours;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to check RTH for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a contract is in its roll period
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to check</param>
|
||||
/// <param name="time">Time to check</param>
|
||||
/// <returns>True if in roll period, false otherwise</returns>
|
||||
public bool IsContractRolling(string symbol, DateTime time)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// Check if we have cached roll info for this symbol
|
||||
if (_contractRollCache.ContainsKey(symbol))
|
||||
{
|
||||
var rollInfo = _contractRollCache[symbol];
|
||||
var daysUntilRoll = (rollInfo.RollDate - time.Date).Days;
|
||||
|
||||
// Consider it rolling if within 5 days of roll date
|
||||
return daysUntilRoll <= 5 && daysUntilRoll >= 0;
|
||||
}
|
||||
|
||||
// Default: not rolling (this would be determined by external data in real implementation)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to check contract roll for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next session start time for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to check</param>
|
||||
/// <returns>Date and time of next session start</returns>
|
||||
public DateTime GetNextSessionStart(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var currentTime = DateTime.UtcNow;
|
||||
var estTime = TimeZoneInfo.ConvertTimeFromUtc(currentTime,
|
||||
TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"));
|
||||
|
||||
if (_defaultSessions.ContainsKey(symbol))
|
||||
{
|
||||
var sessionTimes = _defaultSessions[symbol];
|
||||
var start = sessionTimes.Start;
|
||||
var end = sessionTimes.End;
|
||||
var currentTimeOfDay = estTime.TimeOfDay;
|
||||
|
||||
// For overnight sessions
|
||||
if (end < start)
|
||||
{
|
||||
// If we're in the overnight session and haven't passed the end time yet
|
||||
if (currentTimeOfDay >= start && currentTimeOfDay < TimeSpan.FromHours(24))
|
||||
{
|
||||
// Next session starts tomorrow
|
||||
return new DateTime(estTime.Year, estTime.Month, estTime.Day) + start;
|
||||
}
|
||||
else if (currentTimeOfDay < end)
|
||||
{
|
||||
// We're still in the overnight session from yesterday
|
||||
return new DateTime(estTime.Year, estTime.Month, estTime.Day) + start;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We've passed the end time, next session starts tomorrow
|
||||
var nextDay = estTime.AddDays(1);
|
||||
return new DateTime(nextDay.Year, nextDay.Month, nextDay.Day) + start;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular session
|
||||
if (currentTimeOfDay < start)
|
||||
{
|
||||
// Today's session hasn't started yet
|
||||
return new DateTime(estTime.Year, estTime.Month, estTime.Day) + start;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Today's session has ended, next session is tomorrow
|
||||
var nextDay = estTime.AddDays(1);
|
||||
return new DateTime(nextDay.Year, nextDay.Month, nextDay.Day) + start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to tomorrow morning if no specific session info
|
||||
var tomorrow = estTime.AddDays(1);
|
||||
return new DateTime(tomorrow.Year, tomorrow.Month, tomorrow.Day) + new TimeSpan(9, 0, 0);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get next session start for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets contract roll information for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Base symbol (e.g., ES)</param>
|
||||
/// <param name="activeContract">Current active contract (e.g., ESZ24)</param>
|
||||
/// <param name="nextContract">Next contract to roll to (e.g., ESH25)</param>
|
||||
/// <param name="rollDate">Date of the roll</param>
|
||||
public void SetContractRollInfo(string symbol, string activeContract, string nextContract, DateTime rollDate)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var daysToRoll = (rollDate.Date - DateTime.UtcNow.Date).Days;
|
||||
var isRollPeriod = daysToRoll <= 5 && daysToRoll >= 0;
|
||||
|
||||
var rollInfo = new ContractRollInfo(
|
||||
symbol,
|
||||
activeContract,
|
||||
nextContract,
|
||||
rollDate,
|
||||
daysToRoll,
|
||||
isRollPeriod
|
||||
);
|
||||
|
||||
_contractRollCache[symbol] = rollInfo;
|
||||
|
||||
_logger.LogDebug("Set contract roll info for {Symbol}: {ActiveContract} -> {NextContract} on {RollDate}",
|
||||
symbol, activeContract, nextContract, rollDate);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to set contract roll info for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets contract roll information for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to get roll info for</param>
|
||||
/// <returns>Contract roll information</returns>
|
||||
public ContractRollInfo GetContractRollInfo(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_contractRollCache.ContainsKey(symbol))
|
||||
{
|
||||
return _contractRollCache[symbol];
|
||||
}
|
||||
|
||||
// Return default roll info if not found
|
||||
return new ContractRollInfo(
|
||||
symbol,
|
||||
symbol,
|
||||
symbol,
|
||||
DateTime.MinValue,
|
||||
0,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to get contract roll info for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates session cache for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to update</param>
|
||||
/// <param name="sessionInfo">Session information</param>
|
||||
public void UpdateSessionCache(string symbol, SessionInfo sessionInfo)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (sessionInfo == null)
|
||||
throw new ArgumentNullException("sessionInfo");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_sessionCache[symbol] = sessionInfo;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to update session cache for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears session cache for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to clear cache for</param>
|
||||
public void ClearSessionCache(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_sessionCache.ContainsKey(symbol))
|
||||
{
|
||||
_sessionCache.Remove(symbol);
|
||||
}
|
||||
if (_contractRollCache.ContainsKey(symbol))
|
||||
{
|
||||
_contractRollCache.Remove(symbol);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Cleared session cache for {Symbol}", symbol);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to clear session cache for {Symbol}: {Message}", symbol, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -594,6 +594,122 @@ namespace NT8.Core.OMS
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submit a limit order for execution
|
||||
/// </summary>
|
||||
public async Task<OrderResult> SubmitLimitOrderAsync(LimitOrderRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
ValidateOrderRequest(request);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogError("Limit order validation failed: {0}", ex.Message);
|
||||
return new OrderResult(false, null, ex.Message, request);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await SubmitOrderAsync(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to submit limit order: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submit a stop order for execution
|
||||
/// </summary>
|
||||
public async Task<OrderResult> SubmitStopOrderAsync(StopOrderRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
ValidateOrderRequest(request);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogError("Stop order validation failed: {0}", ex.Message);
|
||||
return new OrderResult(false, null, ex.Message, request);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await SubmitOrderAsync(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to submit stop order: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submit a stop-limit order for execution
|
||||
/// </summary>
|
||||
public async Task<OrderResult> SubmitStopLimitOrderAsync(StopLimitOrderRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
ValidateOrderRequest(request);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogError("Stop-limit order validation failed: {0}", ex.Message);
|
||||
return new OrderResult(false, null, ex.Message, request);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await SubmitOrderAsync(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to submit stop-limit order: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submit a market-if-touched order for execution
|
||||
/// </summary>
|
||||
public async Task<OrderResult> SubmitMITOrderAsync(MITOrderRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
ValidateOrderRequest(request);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogError("MIT order validation failed: {0}", ex.Message);
|
||||
return new OrderResult(false, null, ex.Message, request);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await SubmitOrderAsync(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to submit MIT order: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to order status updates
|
||||
/// </summary>
|
||||
|
||||
@@ -603,4 +603,264 @@ namespace NT8.Core.OMS
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Phase 3 - Advanced Order Types
|
||||
|
||||
/// <summary>
|
||||
/// Limit order request with specific price
|
||||
/// </summary>
|
||||
public class LimitOrderRequest : OrderRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit price for the order
|
||||
/// </summary>
|
||||
public new decimal LimitPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for LimitOrderRequest
|
||||
/// </summary>
|
||||
/// <param name="symbol">Trading symbol</param>
|
||||
/// <param name="side">Order side (Buy/Sell)</param>
|
||||
/// <param name="quantity">Order quantity</param>
|
||||
/// <param name="limitPrice">Limit price</param>
|
||||
/// <param name="tif">Time in force</param>
|
||||
public LimitOrderRequest(string symbol, OrderSide side, int quantity, decimal limitPrice, TimeInForce tif)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (quantity <= 0)
|
||||
throw new ArgumentException("Quantity must be positive", "quantity");
|
||||
if (limitPrice <= 0)
|
||||
throw new ArgumentException("LimitPrice must be positive", "limitPrice");
|
||||
|
||||
Symbol = symbol;
|
||||
Side = side;
|
||||
Quantity = quantity;
|
||||
LimitPrice = limitPrice;
|
||||
TimeInForce = tif;
|
||||
Type = OrderType.Limit;
|
||||
ClientOrderId = Guid.NewGuid().ToString();
|
||||
CreatedTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop order request (stop market order)
|
||||
/// </summary>
|
||||
public class StopOrderRequest : OrderRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop price that triggers the order
|
||||
/// </summary>
|
||||
public new decimal StopPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for StopOrderRequest
|
||||
/// </summary>
|
||||
/// <param name="symbol">Trading symbol</param>
|
||||
/// <param name="side">Order side (Buy/Sell)</param>
|
||||
/// <param name="quantity">Order quantity</param>
|
||||
/// <param name="stopPrice">Stop price</param>
|
||||
/// <param name="tif">Time in force</param>
|
||||
public StopOrderRequest(string symbol, OrderSide side, int quantity, decimal stopPrice, TimeInForce tif)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (quantity <= 0)
|
||||
throw new ArgumentException("Quantity must be positive", "quantity");
|
||||
if (stopPrice <= 0)
|
||||
throw new ArgumentException("StopPrice must be positive", "stopPrice");
|
||||
|
||||
Symbol = symbol;
|
||||
Side = side;
|
||||
Quantity = quantity;
|
||||
StopPrice = stopPrice;
|
||||
TimeInForce = tif;
|
||||
Type = OrderType.StopMarket;
|
||||
ClientOrderId = Guid.NewGuid().ToString();
|
||||
CreatedTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop-limit order request
|
||||
/// </summary>
|
||||
public class StopLimitOrderRequest : OrderRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop price that triggers the order
|
||||
/// </summary>
|
||||
public new decimal StopPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Limit price for the triggered order
|
||||
/// </summary>
|
||||
public new decimal LimitPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for StopLimitOrderRequest
|
||||
/// </summary>
|
||||
/// <param name="symbol">Trading symbol</param>
|
||||
/// <param name="side">Order side (Buy/Sell)</param>
|
||||
/// <param name="quantity">Order quantity</param>
|
||||
/// <param name="stopPrice">Stop price</param>
|
||||
/// <param name="limitPrice">Limit price</param>
|
||||
/// <param name="tif">Time in force</param>
|
||||
public StopLimitOrderRequest(string symbol, OrderSide side, int quantity, decimal stopPrice, decimal limitPrice, TimeInForce tif)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (quantity <= 0)
|
||||
throw new ArgumentException("Quantity must be positive", "quantity");
|
||||
if (stopPrice <= 0)
|
||||
throw new ArgumentException("StopPrice must be positive", "stopPrice");
|
||||
if (limitPrice <= 0)
|
||||
throw new ArgumentException("LimitPrice must be positive", "limitPrice");
|
||||
|
||||
Symbol = symbol;
|
||||
Side = side;
|
||||
Quantity = quantity;
|
||||
StopPrice = stopPrice;
|
||||
LimitPrice = limitPrice;
|
||||
TimeInForce = tif;
|
||||
Type = OrderType.StopLimit;
|
||||
ClientOrderId = Guid.NewGuid().ToString();
|
||||
CreatedTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Market-if-touched order request
|
||||
/// </summary>
|
||||
public class MITOrderRequest : OrderRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger price for the MIT order
|
||||
/// </summary>
|
||||
public decimal TriggerPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for MITOrderRequest
|
||||
/// </summary>
|
||||
/// <param name="symbol">Trading symbol</param>
|
||||
/// <param name="side">Order side (Buy/Sell)</param>
|
||||
/// <param name="quantity">Order quantity</param>
|
||||
/// <param name="triggerPrice">Trigger price</param>
|
||||
/// <param name="tif">Time in force</param>
|
||||
public MITOrderRequest(string symbol, OrderSide side, int quantity, decimal triggerPrice, TimeInForce tif)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
if (quantity <= 0)
|
||||
throw new ArgumentException("Quantity must be positive", "quantity");
|
||||
if (triggerPrice <= 0)
|
||||
throw new ArgumentException("TriggerPrice must be positive", "triggerPrice");
|
||||
|
||||
Symbol = symbol;
|
||||
Side = side;
|
||||
Quantity = quantity;
|
||||
TriggerPrice = triggerPrice;
|
||||
TimeInForce = tif;
|
||||
Type = OrderType.Market; // MIT orders become market orders when triggered
|
||||
ClientOrderId = Guid.NewGuid().ToString();
|
||||
CreatedTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trailing stop configuration
|
||||
/// </summary>
|
||||
public class TrailingStopConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Trailing amount in ticks
|
||||
/// </summary>
|
||||
public int TrailingTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trailing amount as percentage of current price
|
||||
/// </summary>
|
||||
public decimal? TrailingPercent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to trail by ATR or fixed amount
|
||||
/// </summary>
|
||||
public bool UseAtrTrail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ATR multiplier for dynamic trailing
|
||||
/// </summary>
|
||||
public decimal AtrMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for TrailingStopConfig
|
||||
/// </summary>
|
||||
/// <param name="trailingTicks">Trailing amount in ticks</param>
|
||||
/// <param name="useAtrTrail">Whether to use ATR-based trailing</param>
|
||||
/// <param name="atrMultiplier">ATR multiplier if using ATR trail</param>
|
||||
public TrailingStopConfig(int trailingTicks, bool useAtrTrail = false, decimal atrMultiplier = 2m)
|
||||
{
|
||||
if (trailingTicks <= 0)
|
||||
throw new ArgumentException("TrailingTicks must be positive", "trailingTicks");
|
||||
if (atrMultiplier <= 0)
|
||||
throw new ArgumentException("AtrMultiplier must be positive", "atrMultiplier");
|
||||
|
||||
TrailingTicks = trailingTicks;
|
||||
UseAtrTrail = useAtrTrail;
|
||||
AtrMultiplier = atrMultiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for percentage-based trailing stop
|
||||
/// </summary>
|
||||
/// <param name="trailingPercent">Trailing percentage</param>
|
||||
public TrailingStopConfig(decimal trailingPercent)
|
||||
{
|
||||
if (trailingPercent <= 0 || trailingPercent >= 100)
|
||||
throw new ArgumentException("TrailingPercent must be between 0 and 100", "trailingPercent");
|
||||
|
||||
TrailingPercent = trailingPercent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for different order types
|
||||
/// </summary>
|
||||
public class OrderTypeParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// For limit orders - the limit price
|
||||
/// </summary>
|
||||
public decimal? LimitPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For stop orders - the stop price
|
||||
/// </summary>
|
||||
public decimal? StopPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For trailing stops - the trailing configuration
|
||||
/// </summary>
|
||||
public TrailingStopConfig TrailingConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For iceberg orders - the displayed quantity
|
||||
/// </summary>
|
||||
public int? DisplayQty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For algo orders - additional parameters
|
||||
/// </summary>
|
||||
public Dictionary<string, object> AdditionalParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for OrderTypeParameters
|
||||
/// </summary>
|
||||
public OrderTypeParameters()
|
||||
{
|
||||
AdditionalParams = new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
376
src/NT8.Core/OMS/OrderTypeValidator.cs
Normal file
376
src/NT8.Core/OMS/OrderTypeValidator.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NT8.Core.OMS
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates order type parameters and ensures price relationships are correct
|
||||
/// </summary>
|
||||
public class OrderTypeValidator
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for OrderTypeValidator
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance</param>
|
||||
public OrderTypeValidator(ILogger<OrderTypeValidator> logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a limit order request
|
||||
/// </summary>
|
||||
/// <param name="request">Limit order request to validate</param>
|
||||
/// <param name="marketPrice">Current market price</param>
|
||||
/// <returns>Validation result indicating success or failure</returns>
|
||||
public ValidationResult ValidateLimitOrder(LimitOrderRequest request, decimal marketPrice)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
// Check basic parameters
|
||||
if (request.Quantity <= 0)
|
||||
{
|
||||
return new ValidationResult(false, "Quantity must be positive", request.ClientOrderId);
|
||||
}
|
||||
|
||||
if (request.LimitPrice <= 0)
|
||||
{
|
||||
return new ValidationResult(false, "Limit price must be positive", request.ClientOrderId);
|
||||
}
|
||||
|
||||
// Validate price relationship based on order side
|
||||
if (request.Side == OrderSide.Buy && request.LimitPrice > marketPrice)
|
||||
{
|
||||
// Buy limit orders should be below market price
|
||||
_logger.LogDebug("Buy limit order price {LimitPrice} is above market price {MarketPrice}",
|
||||
request.LimitPrice, marketPrice);
|
||||
}
|
||||
else if (request.Side == OrderSide.Sell && request.LimitPrice < marketPrice)
|
||||
{
|
||||
// Sell limit orders should be above market price
|
||||
_logger.LogDebug("Sell limit order price {LimitPrice} is below market price {MarketPrice}",
|
||||
request.LimitPrice, marketPrice);
|
||||
}
|
||||
|
||||
// All validations passed
|
||||
_logger.LogDebug("Limit order validation passed for {ClientOrderId}", request.ClientOrderId);
|
||||
return new ValidationResult(true, "Limit order is valid", request.ClientOrderId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to validate limit order {ClientOrderId}: {Message}",
|
||||
request.ClientOrderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a stop order request
|
||||
/// </summary>
|
||||
/// <param name="request">Stop order request to validate</param>
|
||||
/// <param name="marketPrice">Current market price</param>
|
||||
/// <returns>Validation result indicating success or failure</returns>
|
||||
public ValidationResult ValidateStopOrder(StopOrderRequest request, decimal marketPrice)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
// Check basic parameters
|
||||
if (request.Quantity <= 0)
|
||||
{
|
||||
return new ValidationResult(false, "Quantity must be positive", request.ClientOrderId);
|
||||
}
|
||||
|
||||
if (request.StopPrice <= 0)
|
||||
{
|
||||
return new ValidationResult(false, "Stop price must be positive", request.ClientOrderId);
|
||||
}
|
||||
|
||||
// Validate price relationship based on order side
|
||||
if (request.Side == OrderSide.Buy && request.StopPrice <= marketPrice)
|
||||
{
|
||||
// Buy stop orders should be above market price
|
||||
return new ValidationResult(false,
|
||||
String.Format("Buy stop order price {0} must be above market price {1}",
|
||||
request.StopPrice, marketPrice), request.ClientOrderId);
|
||||
}
|
||||
else if (request.Side == OrderSide.Sell && request.StopPrice >= marketPrice)
|
||||
{
|
||||
// Sell stop orders should be below market price
|
||||
return new ValidationResult(false,
|
||||
String.Format("Sell stop order price {0} must be below market price {1}",
|
||||
request.StopPrice, marketPrice), request.ClientOrderId);
|
||||
}
|
||||
|
||||
// All validations passed
|
||||
_logger.LogDebug("Stop order validation passed for {ClientOrderId}", request.ClientOrderId);
|
||||
return new ValidationResult(true, "Stop order is valid", request.ClientOrderId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to validate stop order {ClientOrderId}: {Message}",
|
||||
request.ClientOrderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a stop-limit order request
|
||||
/// </summary>
|
||||
/// <param name="request">Stop-limit order request to validate</param>
|
||||
/// <param name="marketPrice">Current market price</param>
|
||||
/// <returns>Validation result indicating success or failure</returns>
|
||||
public ValidationResult ValidateStopLimitOrder(StopLimitOrderRequest request, decimal marketPrice)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
// Check basic parameters
|
||||
if (request.Quantity <= 0)
|
||||
{
|
||||
return new ValidationResult(false, "Quantity must be positive", request.ClientOrderId);
|
||||
}
|
||||
|
||||
if (request.StopPrice <= 0)
|
||||
{
|
||||
return new ValidationResult(false, "Stop price must be positive", request.ClientOrderId);
|
||||
}
|
||||
|
||||
if (request.LimitPrice <= 0)
|
||||
{
|
||||
return new ValidationResult(false, "Limit price must be positive", request.ClientOrderId);
|
||||
}
|
||||
|
||||
// Validate price relationship based on order side
|
||||
if (request.Side == OrderSide.Buy)
|
||||
{
|
||||
// For buy stop-limit, stop price should be above market and limit price should be >= stop price
|
||||
if (request.StopPrice <= marketPrice)
|
||||
{
|
||||
return new ValidationResult(false,
|
||||
String.Format("Buy stop-limit stop price {0} must be above market price {1}",
|
||||
request.StopPrice, marketPrice), request.ClientOrderId);
|
||||
}
|
||||
|
||||
if (request.LimitPrice < request.StopPrice)
|
||||
{
|
||||
return new ValidationResult(false,
|
||||
String.Format("Buy stop-limit limit price {0} must be >= stop price {1}",
|
||||
request.LimitPrice, request.StopPrice), request.ClientOrderId);
|
||||
}
|
||||
}
|
||||
else if (request.Side == OrderSide.Sell)
|
||||
{
|
||||
// For sell stop-limit, stop price should be below market and limit price should be <= stop price
|
||||
if (request.StopPrice >= marketPrice)
|
||||
{
|
||||
return new ValidationResult(false,
|
||||
String.Format("Sell stop-limit stop price {0} must be below market price {1}",
|
||||
request.StopPrice, marketPrice), request.ClientOrderId);
|
||||
}
|
||||
|
||||
if (request.LimitPrice > request.StopPrice)
|
||||
{
|
||||
return new ValidationResult(false,
|
||||
String.Format("Sell stop-limit limit price {0} must be <= stop price {1}",
|
||||
request.LimitPrice, request.StopPrice), request.ClientOrderId);
|
||||
}
|
||||
}
|
||||
|
||||
// All validations passed
|
||||
_logger.LogDebug("Stop-limit order validation passed for {ClientOrderId}", request.ClientOrderId);
|
||||
return new ValidationResult(true, "Stop-limit order is valid", request.ClientOrderId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to validate stop-limit order {ClientOrderId}: {Message}",
|
||||
request.ClientOrderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a market-if-touched order request
|
||||
/// </summary>
|
||||
/// <param name="request">MIT order request to validate</param>
|
||||
/// <param name="marketPrice">Current market price</param>
|
||||
/// <returns>Validation result indicating success or failure</returns>
|
||||
public ValidationResult ValidateMITOrder(MITOrderRequest request, decimal marketPrice)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
// Check basic parameters
|
||||
if (request.Quantity <= 0)
|
||||
{
|
||||
return new ValidationResult(false, "Quantity must be positive", request.ClientOrderId);
|
||||
}
|
||||
|
||||
if (request.TriggerPrice <= 0)
|
||||
{
|
||||
return new ValidationResult(false, "Trigger price must be positive", request.ClientOrderId);
|
||||
}
|
||||
|
||||
// Validate price relationship based on order side
|
||||
if (request.Side == OrderSide.Buy && request.TriggerPrice >= marketPrice)
|
||||
{
|
||||
// Buy MIT orders should be below market price (to be triggered when price falls)
|
||||
return new ValidationResult(false,
|
||||
String.Format("Buy MIT trigger price {0} must be below market price {1}",
|
||||
request.TriggerPrice, marketPrice), request.ClientOrderId);
|
||||
}
|
||||
else if (request.Side == OrderSide.Sell && request.TriggerPrice <= marketPrice)
|
||||
{
|
||||
// Sell MIT orders should be above market price (to be triggered when price rises)
|
||||
return new ValidationResult(false,
|
||||
String.Format("Sell MIT trigger price {0} must be above market price {1}",
|
||||
request.TriggerPrice, marketPrice), request.ClientOrderId);
|
||||
}
|
||||
|
||||
// All validations passed
|
||||
_logger.LogDebug("MIT order validation passed for {ClientOrderId}", request.ClientOrderId);
|
||||
return new ValidationResult(true, "MIT order is valid", request.ClientOrderId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to validate MIT order {ClientOrderId}: {Message}",
|
||||
request.ClientOrderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an order request regardless of type
|
||||
/// </summary>
|
||||
/// <param name="request">Order request to validate</param>
|
||||
/// <param name="marketPrice">Current market price</param>
|
||||
/// <returns>Validation result indicating success or failure</returns>
|
||||
public ValidationResult ValidateOrder(OrderRequest request, decimal marketPrice)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException("request");
|
||||
|
||||
try
|
||||
{
|
||||
// Validate common parameters first
|
||||
if (string.IsNullOrEmpty(request.Symbol))
|
||||
{
|
||||
return new ValidationResult(false, "Symbol is required", request.ClientOrderId);
|
||||
}
|
||||
|
||||
if (request.Quantity <= 0)
|
||||
{
|
||||
return new ValidationResult(false, "Quantity must be positive", request.ClientOrderId);
|
||||
}
|
||||
|
||||
// Validate based on order type
|
||||
switch (request.Type)
|
||||
{
|
||||
case OrderType.Limit:
|
||||
var limitRequest = request as LimitOrderRequest;
|
||||
if (limitRequest != null)
|
||||
{
|
||||
return ValidateLimitOrder(limitRequest, marketPrice);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValidationResult(false, "Invalid order type for limit order validation", request.ClientOrderId);
|
||||
}
|
||||
|
||||
case OrderType.StopMarket:
|
||||
var stopRequest = request as StopOrderRequest;
|
||||
if (stopRequest != null)
|
||||
{
|
||||
return ValidateStopOrder(stopRequest, marketPrice);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValidationResult(false, "Invalid order type for stop order validation", request.ClientOrderId);
|
||||
}
|
||||
|
||||
case OrderType.StopLimit:
|
||||
var stopLimitRequest = request as StopLimitOrderRequest;
|
||||
if (stopLimitRequest != null)
|
||||
{
|
||||
return ValidateStopLimitOrder(stopLimitRequest, marketPrice);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValidationResult(false, "Invalid order type for stop-limit order validation", request.ClientOrderId);
|
||||
}
|
||||
|
||||
case OrderType.Market:
|
||||
// For MIT orders
|
||||
var mitRequest = request as MITOrderRequest;
|
||||
if (mitRequest != null)
|
||||
{
|
||||
return ValidateMITOrder(mitRequest, marketPrice);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular market orders don't need complex validation
|
||||
return new ValidationResult(true, "Market order is valid", request.ClientOrderId);
|
||||
}
|
||||
|
||||
default:
|
||||
return new ValidationResult(false,
|
||||
String.Format("Unsupported order type: {0}", request.Type), request.ClientOrderId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to validate order {ClientOrderId}: {Message}",
|
||||
request.ClientOrderId, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of order validation
|
||||
/// </summary>
|
||||
public class ValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the validation passed
|
||||
/// </summary>
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Message describing the validation result
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Client order ID associated with this validation
|
||||
/// </summary>
|
||||
public string ClientOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for ValidationResult
|
||||
/// </summary>
|
||||
/// <param name="isValid">Whether validation passed</param>
|
||||
/// <param name="message">Validation message</param>
|
||||
/// <param name="clientOrderId">Associated client order ID</param>
|
||||
public ValidationResult(bool isValid, string message, string clientOrderId)
|
||||
{
|
||||
IsValid = isValid;
|
||||
Message = message;
|
||||
ClientOrderId = clientOrderId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -591,6 +592,80 @@ namespace NT8.Core.Sizing
|
||||
return errors.Count == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates size using base advanced sizing plus confluence grade and risk mode adjustments.
|
||||
/// </summary>
|
||||
/// <param name="intent">Strategy intent.</param>
|
||||
/// <param name="context">Strategy context.</param>
|
||||
/// <param name="config">Base sizing configuration.</param>
|
||||
/// <param name="confluenceScore">Confluence score and trade grade.</param>
|
||||
/// <param name="riskMode">Current risk mode.</param>
|
||||
/// <param name="modeConfig">Risk mode configuration.</param>
|
||||
/// <returns>Enhanced sizing result with grade and mode metadata.</returns>
|
||||
public SizingResult CalculateSizeWithGrade(
|
||||
StrategyIntent intent,
|
||||
StrategyContext context,
|
||||
SizingConfig config,
|
||||
ConfluenceScore confluenceScore,
|
||||
RiskMode riskMode,
|
||||
RiskModeConfig modeConfig)
|
||||
{
|
||||
if (intent == null) throw new ArgumentNullException("intent");
|
||||
if (context == null) throw new ArgumentNullException("context");
|
||||
if (config == null) throw new ArgumentNullException("config");
|
||||
if (confluenceScore == null) throw new ArgumentNullException("confluenceScore");
|
||||
if (modeConfig == null) throw new ArgumentNullException("modeConfig");
|
||||
|
||||
try
|
||||
{
|
||||
var baseResult = CalculateSize(intent, context, config);
|
||||
|
||||
var gradeFilter = new GradeFilter();
|
||||
var gradeMultiplier = gradeFilter.GetSizeMultiplier(confluenceScore.Grade, riskMode);
|
||||
var modeMultiplier = modeConfig.SizeMultiplier;
|
||||
var combinedMultiplier = gradeMultiplier * modeMultiplier;
|
||||
|
||||
var adjustedContractsRaw = baseResult.Contracts * combinedMultiplier;
|
||||
var adjustedContracts = (int)Math.Floor(adjustedContractsRaw);
|
||||
|
||||
if (adjustedContracts < config.MinContracts)
|
||||
adjustedContracts = config.MinContracts;
|
||||
if (adjustedContracts > config.MaxContracts)
|
||||
adjustedContracts = config.MaxContracts;
|
||||
|
||||
if (!gradeFilter.ShouldAcceptTrade(confluenceScore.Grade, riskMode))
|
||||
adjustedContracts = 0;
|
||||
|
||||
var riskPerContract = baseResult.Contracts > 0
|
||||
? baseResult.RiskAmount / baseResult.Contracts
|
||||
: 0.0;
|
||||
var finalRisk = adjustedContracts * riskPerContract;
|
||||
|
||||
var calculations = new Dictionary<string, object>(baseResult.Calculations);
|
||||
calculations.Add("grade", confluenceScore.Grade.ToString());
|
||||
calculations.Add("risk_mode", riskMode.ToString());
|
||||
calculations.Add("grade_multiplier", gradeMultiplier);
|
||||
calculations.Add("mode_multiplier", modeMultiplier);
|
||||
calculations.Add("combined_multiplier", combinedMultiplier);
|
||||
calculations.Add("base_contracts", baseResult.Contracts);
|
||||
calculations.Add("adjusted_contracts_raw", adjustedContractsRaw);
|
||||
calculations.Add("adjusted_contracts", adjustedContracts);
|
||||
calculations.Add("final_risk", finalRisk);
|
||||
|
||||
if (adjustedContracts == 0)
|
||||
{
|
||||
calculations.Add("rejection_reason", gradeFilter.GetRejectionReason(confluenceScore.Grade, riskMode));
|
||||
}
|
||||
|
||||
return new SizingResult(adjustedContracts, finalRisk, baseResult.Method, calculations);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("CalculateSizeWithGrade failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal class to represent trade results for calculations
|
||||
/// </summary>
|
||||
|
||||
156
src/NT8.Core/Sizing/GradeBasedSizer.cs
Normal file
156
src/NT8.Core/Sizing/GradeBasedSizer.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Sizing
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies confluence grade and risk mode multipliers on top of base sizing output.
|
||||
/// </summary>
|
||||
public class GradeBasedSizer
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly GradeFilter _gradeFilter;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a grade-based sizer.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="gradeFilter">Grade filter instance.</param>
|
||||
public GradeBasedSizer(ILogger logger, GradeFilter gradeFilter)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
if (gradeFilter == null)
|
||||
throw new ArgumentNullException("gradeFilter");
|
||||
|
||||
_logger = logger;
|
||||
_gradeFilter = gradeFilter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates final size from base sizing plus grade and mode adjustments.
|
||||
/// </summary>
|
||||
/// <param name="intent">Strategy intent.</param>
|
||||
/// <param name="context">Strategy context.</param>
|
||||
/// <param name="confluenceScore">Confluence score with grade.</param>
|
||||
/// <param name="riskMode">Current risk mode.</param>
|
||||
/// <param name="baseConfig">Base sizing configuration.</param>
|
||||
/// <param name="baseSizer">Base position sizer used to compute initial contracts.</param>
|
||||
/// <param name="modeConfig">Current risk mode configuration.</param>
|
||||
/// <returns>Final sizing result including grade/mode metadata.</returns>
|
||||
public SizingResult CalculateGradeBasedSize(
|
||||
StrategyIntent intent,
|
||||
StrategyContext context,
|
||||
ConfluenceScore confluenceScore,
|
||||
RiskMode riskMode,
|
||||
SizingConfig baseConfig,
|
||||
IPositionSizer baseSizer,
|
||||
RiskModeConfig modeConfig)
|
||||
{
|
||||
if (intent == null)
|
||||
throw new ArgumentNullException("intent");
|
||||
if (context == null)
|
||||
throw new ArgumentNullException("context");
|
||||
if (confluenceScore == null)
|
||||
throw new ArgumentNullException("confluenceScore");
|
||||
if (baseConfig == null)
|
||||
throw new ArgumentNullException("baseConfig");
|
||||
if (baseSizer == null)
|
||||
throw new ArgumentNullException("baseSizer");
|
||||
if (modeConfig == null)
|
||||
throw new ArgumentNullException("modeConfig");
|
||||
|
||||
try
|
||||
{
|
||||
if (!_gradeFilter.ShouldAcceptTrade(confluenceScore.Grade, riskMode))
|
||||
{
|
||||
var reject = _gradeFilter.GetRejectionReason(confluenceScore.Grade, riskMode);
|
||||
var rejectCalcs = new Dictionary<string, object>();
|
||||
rejectCalcs.Add("rejected", true);
|
||||
rejectCalcs.Add("rejection_reason", reject);
|
||||
rejectCalcs.Add("grade", confluenceScore.Grade.ToString());
|
||||
rejectCalcs.Add("risk_mode", riskMode.ToString());
|
||||
|
||||
_logger.LogInformation("Grade-based sizing rejected trade: {0}", reject);
|
||||
return new SizingResult(0, 0.0, baseConfig.Method, rejectCalcs);
|
||||
}
|
||||
|
||||
var baseResult = baseSizer.CalculateSize(intent, context, baseConfig);
|
||||
|
||||
var gradeMultiplier = _gradeFilter.GetSizeMultiplier(confluenceScore.Grade, riskMode);
|
||||
var modeMultiplier = modeConfig.SizeMultiplier;
|
||||
var combinedMultiplier = CombineMultipliers(gradeMultiplier, modeMultiplier);
|
||||
|
||||
var adjustedContractsRaw = baseResult.Contracts * combinedMultiplier;
|
||||
var adjustedContracts = ApplyConstraints(
|
||||
(int)Math.Floor(adjustedContractsRaw),
|
||||
baseConfig.MinContracts,
|
||||
baseConfig.MaxContracts);
|
||||
|
||||
var riskPerContract = baseResult.Contracts > 0 ? baseResult.RiskAmount / baseResult.Contracts : 0.0;
|
||||
var finalRisk = adjustedContracts * riskPerContract;
|
||||
|
||||
var calculations = new Dictionary<string, object>();
|
||||
calculations.Add("base_contracts", baseResult.Contracts);
|
||||
calculations.Add("base_risk", baseResult.RiskAmount);
|
||||
calculations.Add("grade", confluenceScore.Grade.ToString());
|
||||
calculations.Add("risk_mode", riskMode.ToString());
|
||||
calculations.Add("grade_multiplier", gradeMultiplier);
|
||||
calculations.Add("mode_multiplier", modeMultiplier);
|
||||
calculations.Add("combined_multiplier", combinedMultiplier);
|
||||
calculations.Add("adjusted_contracts_raw", adjustedContractsRaw);
|
||||
calculations.Add("adjusted_contracts", adjustedContracts);
|
||||
calculations.Add("risk_per_contract", riskPerContract);
|
||||
calculations.Add("final_risk", finalRisk);
|
||||
|
||||
return new SizingResult(adjustedContracts, finalRisk, baseResult.Method, calculations);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("CalculateGradeBasedSize failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines grade and mode multipliers.
|
||||
/// </summary>
|
||||
/// <param name="gradeMultiplier">Grade-based multiplier.</param>
|
||||
/// <param name="modeMultiplier">Mode-based multiplier.</param>
|
||||
/// <returns>Combined multiplier.</returns>
|
||||
public double CombineMultipliers(double gradeMultiplier, double modeMultiplier)
|
||||
{
|
||||
if (gradeMultiplier < 0.0)
|
||||
throw new ArgumentException("gradeMultiplier must be non-negative", "gradeMultiplier");
|
||||
if (modeMultiplier < 0.0)
|
||||
throw new ArgumentException("modeMultiplier must be non-negative", "modeMultiplier");
|
||||
|
||||
return gradeMultiplier * modeMultiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies min/max contract constraints.
|
||||
/// </summary>
|
||||
/// <param name="calculatedSize">Calculated contracts.</param>
|
||||
/// <param name="min">Minimum allowed contracts.</param>
|
||||
/// <param name="max">Maximum allowed contracts.</param>
|
||||
/// <returns>Constrained contracts.</returns>
|
||||
public int ApplyConstraints(int calculatedSize, int min, int max)
|
||||
{
|
||||
if (min < 0)
|
||||
throw new ArgumentException("min must be non-negative", "min");
|
||||
if (max < min)
|
||||
throw new ArgumentException("max must be greater than or equal to min", "max");
|
||||
|
||||
if (calculatedSize < min)
|
||||
return min;
|
||||
if (calculatedSize > max)
|
||||
return max;
|
||||
|
||||
return calculatedSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
356
src/NT8.Strategies/Examples/SimpleORBStrategy.cs
Normal file
356
src/NT8.Strategies/Examples/SimpleORBStrategy.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Common.Interfaces;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Indicators;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Strategies.Examples
|
||||
{
|
||||
/// <summary>
|
||||
/// Opening Range Breakout strategy with Phase 4 confluence and grade-aware intent metadata.
|
||||
/// </summary>
|
||||
public class SimpleORBStrategy : IStrategy
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private readonly int _openingRangeMinutes;
|
||||
private readonly double _stdDevMultiplier;
|
||||
|
||||
private ILogger _logger;
|
||||
private ConfluenceScorer _scorer;
|
||||
private GradeFilter _gradeFilter;
|
||||
private RiskModeManager _riskModeManager;
|
||||
private AVWAPCalculator _avwapCalculator;
|
||||
private VolumeProfileAnalyzer _volumeProfileAnalyzer;
|
||||
private List<IFactorCalculator> _factorCalculators;
|
||||
|
||||
private DateTime _currentSessionDate;
|
||||
private DateTime _openingRangeStart;
|
||||
private DateTime _openingRangeEnd;
|
||||
private double _openingRangeHigh;
|
||||
private double _openingRangeLow;
|
||||
private bool _openingRangeReady;
|
||||
private bool _tradeTaken;
|
||||
private int _consecutiveWins;
|
||||
private int _consecutiveLosses;
|
||||
|
||||
/// <summary>
|
||||
/// Gets strategy metadata.
|
||||
/// </summary>
|
||||
public StrategyMetadata Metadata { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new strategy with default ORB configuration.
|
||||
/// </summary>
|
||||
public SimpleORBStrategy()
|
||||
: this(30, 1.0)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new strategy with custom ORB configuration.
|
||||
/// </summary>
|
||||
/// <param name="openingRangeMinutes">Opening range period in minutes.</param>
|
||||
/// <param name="stdDevMultiplier">Breakout volatility multiplier.</param>
|
||||
public SimpleORBStrategy(int openingRangeMinutes, double stdDevMultiplier)
|
||||
{
|
||||
if (openingRangeMinutes <= 0)
|
||||
throw new ArgumentException("openingRangeMinutes must be greater than zero", "openingRangeMinutes");
|
||||
|
||||
if (stdDevMultiplier <= 0.0)
|
||||
throw new ArgumentException("stdDevMultiplier must be greater than zero", "stdDevMultiplier");
|
||||
|
||||
_openingRangeMinutes = openingRangeMinutes;
|
||||
_stdDevMultiplier = stdDevMultiplier;
|
||||
|
||||
_currentSessionDate = DateTime.MinValue;
|
||||
_openingRangeStart = DateTime.MinValue;
|
||||
_openingRangeEnd = DateTime.MinValue;
|
||||
_openingRangeHigh = Double.MinValue;
|
||||
_openingRangeLow = Double.MaxValue;
|
||||
_openingRangeReady = false;
|
||||
_tradeTaken = false;
|
||||
_consecutiveWins = 0;
|
||||
_consecutiveLosses = 0;
|
||||
|
||||
Metadata = new StrategyMetadata(
|
||||
"Simple ORB",
|
||||
"Opening Range Breakout strategy with confluence scoring",
|
||||
"2.0",
|
||||
"NT8 SDK Team",
|
||||
new string[] { "ES", "NQ", "YM" },
|
||||
20);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes strategy dependencies.
|
||||
/// </summary>
|
||||
/// <param name="config">Strategy configuration.</param>
|
||||
/// <param name="dataProvider">Market data provider.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
public void Initialize(StrategyConfig config, IMarketDataProvider dataProvider, ILogger logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
try
|
||||
{
|
||||
_logger = logger;
|
||||
_scorer = new ConfluenceScorer(_logger, 500);
|
||||
_gradeFilter = new GradeFilter();
|
||||
_riskModeManager = new RiskModeManager(_logger);
|
||||
_avwapCalculator = new AVWAPCalculator(AVWAPAnchorMode.Day, DateTime.UtcNow);
|
||||
_volumeProfileAnalyzer = new VolumeProfileAnalyzer();
|
||||
|
||||
_factorCalculators = new List<IFactorCalculator>();
|
||||
_factorCalculators.Add(new OrbSetupFactorCalculator());
|
||||
_factorCalculators.Add(new TrendAlignmentFactorCalculator());
|
||||
_factorCalculators.Add(new VolatilityRegimeFactorCalculator());
|
||||
_factorCalculators.Add(new TimeInSessionFactorCalculator());
|
||||
_factorCalculators.Add(new ExecutionQualityFactorCalculator());
|
||||
|
||||
_logger.LogInformation(
|
||||
"SimpleORBStrategy initialized with OR period {0} minutes and multiplier {1:F2}",
|
||||
_openingRangeMinutes,
|
||||
_stdDevMultiplier);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger != null)
|
||||
_logger.LogError("SimpleORBStrategy Initialize failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes bar data and returns a trade intent when breakout and confluence criteria pass.
|
||||
/// </summary>
|
||||
/// <param name="bar">Current bar.</param>
|
||||
/// <param name="context">Strategy context.</param>
|
||||
/// <returns>Trade intent when signal is accepted; otherwise null.</returns>
|
||||
public StrategyIntent OnBar(BarData bar, StrategyContext context)
|
||||
{
|
||||
if (bar == null)
|
||||
throw new ArgumentNullException("bar");
|
||||
if (context == null)
|
||||
throw new ArgumentNullException("context");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
UpdateRiskMode(context);
|
||||
UpdateConfluenceInputs(bar, context);
|
||||
|
||||
if (_currentSessionDate != context.CurrentTime.Date)
|
||||
{
|
||||
ResetSession(context.Session != null ? context.Session.SessionStart : context.CurrentTime.Date);
|
||||
}
|
||||
|
||||
if (bar.Time <= _openingRangeEnd)
|
||||
{
|
||||
UpdateOpeningRange(bar);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!_openingRangeReady)
|
||||
{
|
||||
if (_openingRangeHigh > _openingRangeLow)
|
||||
_openingRangeReady = true;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_tradeTaken)
|
||||
return null;
|
||||
|
||||
var openingRange = _openingRangeHigh - _openingRangeLow;
|
||||
var volatilityBuffer = openingRange * (_stdDevMultiplier - 1.0);
|
||||
if (volatilityBuffer < 0.0)
|
||||
volatilityBuffer = 0.0;
|
||||
|
||||
var longTrigger = _openingRangeHigh + volatilityBuffer;
|
||||
var shortTrigger = _openingRangeLow - volatilityBuffer;
|
||||
|
||||
StrategyIntent candidate = null;
|
||||
if (bar.Close > longTrigger)
|
||||
candidate = CreateIntent(context.Symbol, OrderSide.Buy, openingRange, bar.Close);
|
||||
else if (bar.Close < shortTrigger)
|
||||
candidate = CreateIntent(context.Symbol, OrderSide.Sell, openingRange, bar.Close);
|
||||
|
||||
if (candidate == null)
|
||||
return null;
|
||||
|
||||
var score = _scorer.CalculateScore(candidate, context, bar, _factorCalculators);
|
||||
var mode = _riskModeManager.GetCurrentMode();
|
||||
|
||||
if (!_gradeFilter.ShouldAcceptTrade(score.Grade, mode))
|
||||
{
|
||||
var reason = _gradeFilter.GetRejectionReason(score.Grade, mode);
|
||||
_logger.LogInformation(
|
||||
"SimpleORBStrategy rejected intent for {0}: Grade={1}, Mode={2}, Reason={3}",
|
||||
candidate.Symbol,
|
||||
score.Grade,
|
||||
mode,
|
||||
reason);
|
||||
return null;
|
||||
}
|
||||
|
||||
var gradeMultiplier = _gradeFilter.GetSizeMultiplier(score.Grade, mode);
|
||||
var modeConfig = _riskModeManager.GetModeConfig(mode);
|
||||
var combinedMultiplier = gradeMultiplier * modeConfig.SizeMultiplier;
|
||||
|
||||
candidate.Confidence = score.WeightedScore;
|
||||
candidate.Reason = string.Format("{0}; grade={1}; mode={2}", candidate.Reason, score.Grade, mode);
|
||||
candidate.Metadata["confluence_score"] = score.WeightedScore;
|
||||
candidate.Metadata["trade_grade"] = score.Grade.ToString();
|
||||
candidate.Metadata["risk_mode"] = mode.ToString();
|
||||
candidate.Metadata["grade_multiplier"] = gradeMultiplier;
|
||||
candidate.Metadata["mode_multiplier"] = modeConfig.SizeMultiplier;
|
||||
candidate.Metadata["combined_multiplier"] = combinedMultiplier;
|
||||
|
||||
_tradeTaken = true;
|
||||
|
||||
_logger.LogInformation(
|
||||
"SimpleORBStrategy accepted intent for {0}: Side={1}, Grade={2}, Mode={3}, Score={4:F3}, Mult={5:F2}",
|
||||
candidate.Symbol,
|
||||
candidate.Side,
|
||||
score.Grade,
|
||||
mode,
|
||||
score.WeightedScore,
|
||||
combinedMultiplier);
|
||||
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger != null)
|
||||
_logger.LogError("SimpleORBStrategy OnBar failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes tick data. This strategy does not use tick-level logic.
|
||||
/// </summary>
|
||||
/// <param name="tick">Tick data.</param>
|
||||
/// <param name="context">Strategy context.</param>
|
||||
/// <returns>Always null for this strategy.</returns>
|
||||
public StrategyIntent OnTick(TickData tick, StrategyContext context)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns current strategy parameters.
|
||||
/// </summary>
|
||||
/// <returns>Parameter map.</returns>
|
||||
public Dictionary<string, object> GetParameters()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var parameters = new Dictionary<string, object>();
|
||||
parameters.Add("opening_range_minutes", _openingRangeMinutes);
|
||||
parameters.Add("std_dev_multiplier", _stdDevMultiplier);
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates strategy parameters.
|
||||
/// </summary>
|
||||
/// <param name="parameters">Parameter map.</param>
|
||||
public void SetParameters(Dictionary<string, object> parameters)
|
||||
{
|
||||
// Constructor-bound parameters intentionally remain immutable for deterministic behavior.
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_logger == null)
|
||||
throw new InvalidOperationException("Strategy must be initialized before OnBar processing");
|
||||
if (_scorer == null || _gradeFilter == null || _riskModeManager == null)
|
||||
throw new InvalidOperationException("Intelligence components are not initialized");
|
||||
}
|
||||
|
||||
private void UpdateRiskMode(StrategyContext context)
|
||||
{
|
||||
var dailyPnl = 0.0;
|
||||
if (context.Account != null)
|
||||
dailyPnl = context.Account.DailyPnL;
|
||||
|
||||
_riskModeManager.UpdateRiskMode(dailyPnl, _consecutiveWins, _consecutiveLosses);
|
||||
}
|
||||
|
||||
private void UpdateConfluenceInputs(BarData bar, StrategyContext context)
|
||||
{
|
||||
_avwapCalculator.Update(bar.Close, bar.Volume);
|
||||
var avwap = _avwapCalculator.GetCurrentValue();
|
||||
var avwapSlope = _avwapCalculator.GetSlope(10);
|
||||
|
||||
var bars = new List<BarData>();
|
||||
bars.Add(bar);
|
||||
var valueArea = _volumeProfileAnalyzer.CalculateValueArea(bars);
|
||||
|
||||
if (context.CustomData == null)
|
||||
context.CustomData = new Dictionary<string, object>();
|
||||
|
||||
context.CustomData["current_bar"] = bar;
|
||||
context.CustomData["avwap"] = avwap;
|
||||
context.CustomData["avwap_slope"] = avwapSlope;
|
||||
context.CustomData["trend_confirm"] = avwapSlope > 0.0 ? 1.0 : 0.0;
|
||||
context.CustomData["current_atr"] = Math.Max(0.01, bar.High - bar.Low);
|
||||
context.CustomData["normal_atr"] = Math.Max(0.01, valueArea.ValueAreaHigh - valueArea.ValueAreaLow);
|
||||
context.CustomData["recent_execution_quality"] = 0.8;
|
||||
context.CustomData["avg_volume"] = (double)bar.Volume;
|
||||
}
|
||||
|
||||
private void ResetSession(DateTime sessionStart)
|
||||
{
|
||||
_currentSessionDate = sessionStart.Date;
|
||||
_openingRangeStart = sessionStart;
|
||||
_openingRangeEnd = sessionStart.AddMinutes(_openingRangeMinutes);
|
||||
_openingRangeHigh = Double.MinValue;
|
||||
_openingRangeLow = Double.MaxValue;
|
||||
_openingRangeReady = false;
|
||||
_tradeTaken = false;
|
||||
}
|
||||
|
||||
private void UpdateOpeningRange(BarData bar)
|
||||
{
|
||||
if (bar.High > _openingRangeHigh)
|
||||
_openingRangeHigh = bar.High;
|
||||
|
||||
if (bar.Low < _openingRangeLow)
|
||||
_openingRangeLow = bar.Low;
|
||||
}
|
||||
|
||||
private StrategyIntent CreateIntent(string symbol, OrderSide side, double openingRange, double lastPrice)
|
||||
{
|
||||
var metadata = new Dictionary<string, object>();
|
||||
metadata.Add("orb_high", _openingRangeHigh);
|
||||
metadata.Add("orb_low", _openingRangeLow);
|
||||
metadata.Add("orb_range", openingRange);
|
||||
metadata.Add("trigger_price", lastPrice);
|
||||
metadata.Add("multiplier", _stdDevMultiplier);
|
||||
metadata.Add("opening_range_start", _openingRangeStart);
|
||||
metadata.Add("opening_range_end", _openingRangeEnd);
|
||||
|
||||
return new StrategyIntent(
|
||||
symbol,
|
||||
side,
|
||||
OrderType.Market,
|
||||
null,
|
||||
8,
|
||||
16,
|
||||
0.75,
|
||||
"ORB breakout signal",
|
||||
metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
205
tests/NT8.Core.Tests/Execution/ExecutionQualityTrackerTests.cs
Normal file
205
tests/NT8.Core.Tests/Execution/ExecutionQualityTrackerTests.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Execution;
|
||||
using NT8.Core.Tests.Mocks;
|
||||
|
||||
namespace NT8.Core.Tests.Execution
|
||||
{
|
||||
[TestClass]
|
||||
public class ExecutionQualityTrackerTests
|
||||
{
|
||||
private ExecutionQualityTracker _tracker;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_tracker = new ExecutionQualityTracker(new MockLogger<ExecutionQualityTracker>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
new ExecutionQualityTracker(null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RecordExecution_ValidInput_StoresMetrics()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
_tracker.RecordExecution("ES-1", 5000m, 5000.25m, now.AddMilliseconds(20), now.AddMilliseconds(5), now);
|
||||
|
||||
var metrics = _tracker.GetExecutionMetrics("ES-1");
|
||||
Assert.IsNotNull(metrics);
|
||||
Assert.AreEqual("ES-1", metrics.OrderId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RecordExecution_NullOrderId_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_tracker.RecordExecution(null, 1m, 1m, DateTime.UtcNow, DateTime.UtcNow, DateTime.UtcNow);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetExecutionMetrics_UnknownOrder_ReturnsNull()
|
||||
{
|
||||
var metrics = _tracker.GetExecutionMetrics("UNKNOWN-1");
|
||||
Assert.IsNull(metrics);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetExecutionMetrics_NullOrderId_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_tracker.GetExecutionMetrics(null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetSymbolStatistics_NoHistory_ReturnsDefaults()
|
||||
{
|
||||
var stats = _tracker.GetSymbolStatistics("ES");
|
||||
|
||||
Assert.AreEqual("ES", stats.Symbol);
|
||||
Assert.AreEqual(0.0, stats.AverageSlippage, 0.000001);
|
||||
Assert.AreEqual(TimeSpan.Zero, stats.AverageFillLatency);
|
||||
Assert.AreEqual(ExecutionQuality.Poor, stats.AverageQuality);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetSymbolStatistics_WithHistory_ComputesAverages()
|
||||
{
|
||||
var t0 = DateTime.UtcNow;
|
||||
_tracker.RecordExecution("ES-1", 5000m, 5000.25m, t0.AddMilliseconds(30), t0.AddMilliseconds(5), t0);
|
||||
_tracker.RecordExecution("ES-2", 5000m, 4999.75m, t0.AddMilliseconds(40), t0.AddMilliseconds(10), t0);
|
||||
|
||||
var stats = _tracker.GetSymbolStatistics("ES");
|
||||
|
||||
Assert.AreEqual("ES", stats.Symbol);
|
||||
Assert.AreEqual(2, stats.PositiveSlippageCount + stats.NegativeSlippageCount);
|
||||
Assert.IsTrue(stats.AverageFillLatency.TotalMilliseconds > 0.0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetSymbolStatistics_NullSymbol_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_tracker.GetSymbolStatistics(null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetAverageSlippage_WithHistory_ReturnsAverage()
|
||||
{
|
||||
var t0 = DateTime.UtcNow;
|
||||
_tracker.RecordExecution("NQ-1", 100m, 101m, t0.AddMilliseconds(5), t0.AddMilliseconds(2), t0);
|
||||
_tracker.RecordExecution("NQ-2", 100m, 99m, t0.AddMilliseconds(6), t0.AddMilliseconds(2), t0);
|
||||
|
||||
var avg = _tracker.GetAverageSlippage("NQ");
|
||||
Assert.AreEqual(0.0, avg, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetAverageSlippage_NullSymbol_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_tracker.GetAverageSlippage(null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IsExecutionQualityAcceptable_WhenNoDataAndThresholdFair_ReturnsTrue_ByCurrentEnumOrdering()
|
||||
{
|
||||
var ok = _tracker.IsExecutionQualityAcceptable("GC", ExecutionQuality.Fair);
|
||||
Assert.IsTrue(ok);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IsExecutionQualityAcceptable_WithLowThreshold_ReturnsTrue()
|
||||
{
|
||||
var ok = _tracker.IsExecutionQualityAcceptable("GC", ExecutionQuality.Poor);
|
||||
Assert.IsTrue(ok);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IsExecutionQualityAcceptable_NullSymbol_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_tracker.IsExecutionQualityAcceptable(null, ExecutionQuality.Poor);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetTotalExecutionCount_AfterRecords_ReturnsCount()
|
||||
{
|
||||
var t0 = DateTime.UtcNow;
|
||||
_tracker.RecordExecution("MES-1", 1m, 1m, t0.AddMilliseconds(2), t0.AddMilliseconds(1), t0);
|
||||
_tracker.RecordExecution("MES-2", 1m, 1m, t0.AddMilliseconds(2), t0.AddMilliseconds(1), t0);
|
||||
|
||||
Assert.AreEqual(2, _tracker.GetTotalExecutionCount());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ClearSymbolHistory_RemovesHistoryForSymbol()
|
||||
{
|
||||
var t0 = DateTime.UtcNow;
|
||||
_tracker.RecordExecution("CL-1", 80m, 80.1m, t0.AddMilliseconds(2), t0.AddMilliseconds(1), t0);
|
||||
_tracker.ClearSymbolHistory("CL");
|
||||
|
||||
var stats = _tracker.GetSymbolStatistics("CL");
|
||||
Assert.AreEqual(0.0, stats.AverageSlippage, 0.000001);
|
||||
Assert.AreEqual(ExecutionQuality.Poor, stats.AverageQuality);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ClearSymbolHistory_NullSymbol_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_tracker.ClearSymbolHistory(null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RollingWindow_UsesLast100Executions()
|
||||
{
|
||||
var t0 = DateTime.UtcNow;
|
||||
for (var i = 0; i < 120; i++)
|
||||
{
|
||||
_tracker.RecordExecution("RTY-" + i, 2000m, 2000m + (i % 2 == 0 ? 0.25m : -0.25m), t0.AddMilliseconds(10), t0.AddMilliseconds(5), t0);
|
||||
}
|
||||
|
||||
var stats = _tracker.GetSymbolStatistics("RTY");
|
||||
Assert.IsTrue(stats.PositiveSlippageCount + stats.NegativeSlippageCount <= 100);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RecordExecution_PositiveSlippage_TagsPositiveType()
|
||||
{
|
||||
var t0 = DateTime.UtcNow;
|
||||
_tracker.RecordExecution("ES-POS", 100m, 101m, t0.AddMilliseconds(10), t0.AddMilliseconds(5), t0);
|
||||
|
||||
var metrics = _tracker.GetExecutionMetrics("ES-POS");
|
||||
Assert.AreEqual(SlippageType.Positive, metrics.SlippageType);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RecordExecution_NegativeSlippage_TagsNegativeType()
|
||||
{
|
||||
var t0 = DateTime.UtcNow;
|
||||
_tracker.RecordExecution("ES-NEG", 100m, 99m, t0.AddMilliseconds(10), t0.AddMilliseconds(5), t0);
|
||||
|
||||
var metrics = _tracker.GetExecutionMetrics("ES-NEG");
|
||||
Assert.AreEqual(SlippageType.Negative, metrics.SlippageType);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
tests/NT8.Core.Tests/Execution/MultiLevelTargetManagerTests.cs
Normal file
147
tests/NT8.Core.Tests/Execution/MultiLevelTargetManagerTests.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Execution;
|
||||
using NT8.Core.Tests.Mocks;
|
||||
|
||||
namespace NT8.Core.Tests.Execution
|
||||
{
|
||||
[TestClass]
|
||||
public class MultiLevelTargetManagerTests
|
||||
{
|
||||
private MultiLevelTargetManager _manager;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_manager = new MultiLevelTargetManager(new MockLogger<MultiLevelTargetManager>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
new MultiLevelTargetManager(null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetTargets_ValidInput_SetsStatusActive()
|
||||
{
|
||||
var targets = new MultiLevelTargets(8, 2, 16, 2, 32, 1);
|
||||
_manager.SetTargets("ORD-ML-1", targets);
|
||||
|
||||
var status = _manager.GetTargetStatus("ORD-ML-1");
|
||||
Assert.IsTrue(status.Active);
|
||||
Assert.AreEqual(3, status.TotalTargets);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetTargets_NullOrderId_ThrowsArgumentNullException()
|
||||
{
|
||||
var targets = new MultiLevelTargets(8, 1);
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_manager.SetTargets(null, targets);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetTargets_NullTargets_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_manager.SetTargets("ORD-ML-2", null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OnTargetHit_Tp1_ReturnsPartialClose()
|
||||
{
|
||||
_manager.SetTargets("ORD-ML-3", new MultiLevelTargets(8, 2, 16, 2, 32, 1));
|
||||
|
||||
var result = _manager.OnTargetHit("ORD-ML-3", 1, 5002m);
|
||||
|
||||
Assert.AreEqual(TargetAction.PartialClose, result.Action);
|
||||
Assert.AreEqual(2, result.ContractsToClose);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OnTargetHit_Tp2NotConfigured_ReturnsNoAction()
|
||||
{
|
||||
_manager.SetTargets("ORD-ML-4", new MultiLevelTargets(8, 1));
|
||||
|
||||
var result = _manager.OnTargetHit("ORD-ML-4", 2, 5003m);
|
||||
|
||||
Assert.AreEqual(TargetAction.NoAction, result.Action);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OnTargetHit_AllConfiguredTargetsHit_ReturnsClosePosition()
|
||||
{
|
||||
_manager.SetTargets("ORD-ML-5", new MultiLevelTargets(8, 1, 16, 1));
|
||||
|
||||
var r1 = _manager.OnTargetHit("ORD-ML-5", 1, 5002m);
|
||||
var r2 = _manager.OnTargetHit("ORD-ML-5", 2, 5004m);
|
||||
|
||||
Assert.AreEqual(TargetAction.PartialClose, r1.Action);
|
||||
Assert.AreEqual(TargetAction.ClosePosition, r2.Action);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OnTargetHit_DuplicateLevel_ReturnsNoAction()
|
||||
{
|
||||
_manager.SetTargets("ORD-ML-6", new MultiLevelTargets(8, 1, 16, 1));
|
||||
|
||||
var first = _manager.OnTargetHit("ORD-ML-6", 1, 5002m);
|
||||
var second = _manager.OnTargetHit("ORD-ML-6", 1, 5003m);
|
||||
|
||||
Assert.AreEqual(TargetAction.PartialClose, first.Action);
|
||||
Assert.AreEqual(TargetAction.NoAction, second.Action);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OnTargetHit_UnknownOrder_ReturnsNoAction()
|
||||
{
|
||||
var result = _manager.OnTargetHit("ORD-UNKNOWN", 1, 100m);
|
||||
Assert.AreEqual(TargetAction.NoAction, result.Action);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldAdvanceStop_Tp1AndTp2_True_Tp3False()
|
||||
{
|
||||
Assert.IsTrue(_manager.ShouldAdvanceStop(1));
|
||||
Assert.IsTrue(_manager.ShouldAdvanceStop(2));
|
||||
Assert.IsFalse(_manager.ShouldAdvanceStop(3));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetTargetStatus_UnknownOrder_ReturnsInactive()
|
||||
{
|
||||
var status = _manager.GetTargetStatus("ORD-NONE");
|
||||
Assert.IsFalse(status.Active);
|
||||
Assert.AreEqual(0, status.TotalTargets);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DeactivateTargets_SetsInactive()
|
||||
{
|
||||
_manager.SetTargets("ORD-ML-7", new MultiLevelTargets(8, 1));
|
||||
_manager.DeactivateTargets("ORD-ML-7");
|
||||
|
||||
var status = _manager.GetTargetStatus("ORD-ML-7");
|
||||
Assert.IsFalse(status.Active);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RemoveTargets_DeletesState()
|
||||
{
|
||||
_manager.SetTargets("ORD-ML-8", new MultiLevelTargets(8, 1, 16, 1, 32, 1));
|
||||
_manager.RemoveTargets("ORD-ML-8");
|
||||
|
||||
var status = _manager.GetTargetStatus("ORD-ML-8");
|
||||
Assert.IsFalse(status.Active);
|
||||
Assert.AreEqual(0, status.TotalTargets);
|
||||
}
|
||||
}
|
||||
}
|
||||
210
tests/NT8.Core.Tests/Execution/TrailingStopManagerTests.cs
Normal file
210
tests/NT8.Core.Tests/Execution/TrailingStopManagerTests.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Execution;
|
||||
using NT8.Core.OMS;
|
||||
using NT8.Core.Tests.Mocks;
|
||||
|
||||
namespace NT8.Core.Tests.Execution
|
||||
{
|
||||
[TestClass]
|
||||
public class TrailingStopManagerTests
|
||||
{
|
||||
private TrailingStopManager _manager;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_manager = new TrailingStopManager(new MockLogger<TrailingStopManager>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
new TrailingStopManager(null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StartTrailing_ValidLongPosition_CreatesStop()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Buy, 5000m);
|
||||
var config = new NT8.Core.Execution.TrailingStopConfig(8);
|
||||
|
||||
_manager.StartTrailing("ORD-1", position, config);
|
||||
|
||||
var stop = _manager.GetCurrentStopPrice("ORD-1");
|
||||
Assert.IsTrue(stop.HasValue);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StartTrailing_ValidShortPosition_CreatesStop()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Sell, 5000m);
|
||||
var config = new NT8.Core.Execution.TrailingStopConfig(8);
|
||||
|
||||
_manager.StartTrailing("ORD-2", position, config);
|
||||
|
||||
var stop = _manager.GetCurrentStopPrice("ORD-2");
|
||||
Assert.IsTrue(stop.HasValue);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StartTrailing_NullOrderId_ThrowsArgumentNullException()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Buy, 5000m);
|
||||
var config = new NT8.Core.Execution.TrailingStopConfig(8);
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_manager.StartTrailing(null, position, config);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StartTrailing_NullPosition_ThrowsArgumentNullException()
|
||||
{
|
||||
var config = new NT8.Core.Execution.TrailingStopConfig(8);
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_manager.StartTrailing("ORD-3", null, config);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateTrailingStop_UnknownOrder_ReturnsNull()
|
||||
{
|
||||
var result = _manager.UpdateTrailingStop("UNKNOWN", 5001m);
|
||||
Assert.IsFalse(result.HasValue);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateTrailingStop_LongImprovingPrice_CanReturnUpdatedStop()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Buy, 5000m);
|
||||
var config = new NT8.Core.Execution.TrailingStopConfig(8);
|
||||
_manager.StartTrailing("ORD-4", position, config);
|
||||
|
||||
var updated = _manager.UpdateTrailingStop("ORD-4", 5005m);
|
||||
var current = _manager.GetCurrentStopPrice("ORD-4");
|
||||
|
||||
Assert.IsTrue(current.HasValue);
|
||||
if (updated.HasValue)
|
||||
{
|
||||
Assert.IsTrue(updated.Value <= 5005m);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateTrailingStop_ShortImprovingPrice_CanReturnUpdatedStop()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Sell, 5000m);
|
||||
var config = new NT8.Core.Execution.TrailingStopConfig(8);
|
||||
_manager.StartTrailing("ORD-5", position, config);
|
||||
|
||||
var updated = _manager.UpdateTrailingStop("ORD-5", 4995m);
|
||||
var current = _manager.GetCurrentStopPrice("ORD-5");
|
||||
|
||||
Assert.IsTrue(current.HasValue);
|
||||
if (updated.HasValue)
|
||||
{
|
||||
Assert.IsTrue(updated.Value >= 4995m || updated.Value <= 5000m);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateNewStopPrice_FixedTrailing_Long_ReturnsValue()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Buy, 5000m);
|
||||
var stop = _manager.CalculateNewStopPrice(StopType.FixedTrailing, position, 5001m);
|
||||
Assert.IsTrue(stop > 0m);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateNewStopPrice_ATRTrailing_Short_ReturnsValue()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Sell, 5000m);
|
||||
var stop = _manager.CalculateNewStopPrice(StopType.ATRTrailing, position, 4998m);
|
||||
Assert.IsTrue(stop > 0m);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateNewStopPrice_PercentageTrailing_ReturnsValue()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Buy, 5000m);
|
||||
var stop = _manager.CalculateNewStopPrice(StopType.PercentageTrailing, position, 5010m);
|
||||
Assert.IsTrue(stop > 0m);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldMoveToBreakeven_EnabledAndInProfit_ReturnsTrue()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Buy, 5000m);
|
||||
var config = new AutoBreakevenConfig(4, true, 1, true);
|
||||
|
||||
var shouldMove = _manager.ShouldMoveToBreakeven(position, 5002m, config);
|
||||
Assert.IsTrue(shouldMove);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldMoveToBreakeven_Disabled_ReturnsFalse()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Buy, 5000m);
|
||||
var config = new AutoBreakevenConfig(4, true, 1, false);
|
||||
|
||||
var shouldMove = _manager.ShouldMoveToBreakeven(position, 5005m, config);
|
||||
Assert.IsFalse(shouldMove);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DeactivateTrailing_PreventsFurtherUpdates()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Buy, 5000m);
|
||||
var config = new NT8.Core.Execution.TrailingStopConfig(8);
|
||||
_manager.StartTrailing("ORD-6", position, config);
|
||||
_manager.DeactivateTrailing("ORD-6");
|
||||
|
||||
var updated = _manager.UpdateTrailingStop("ORD-6", 5010m);
|
||||
Assert.IsFalse(updated.HasValue);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RemoveTrailing_DeletesTracking()
|
||||
{
|
||||
var position = CreatePosition(OrderSide.Buy, 5000m);
|
||||
var config = new NT8.Core.Execution.TrailingStopConfig(8);
|
||||
_manager.StartTrailing("ORD-7", position, config);
|
||||
|
||||
_manager.RemoveTrailing("ORD-7");
|
||||
|
||||
var current = _manager.GetCurrentStopPrice("ORD-7");
|
||||
Assert.IsFalse(current.HasValue);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetCurrentStopPrice_NullOrderId_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_manager.GetCurrentStopPrice(null);
|
||||
});
|
||||
}
|
||||
|
||||
private static OrderStatus CreatePosition(OrderSide side, decimal avgFillPrice)
|
||||
{
|
||||
return new OrderStatus
|
||||
{
|
||||
OrderId = Guid.NewGuid().ToString(),
|
||||
Symbol = "ES",
|
||||
Side = side,
|
||||
Quantity = 1,
|
||||
AverageFillPrice = avgFillPrice,
|
||||
State = OrderState.Working,
|
||||
FilledQuantity = 1,
|
||||
CreatedTime = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
145
tests/NT8.Core.Tests/Indicators/AVWAPCalculatorTests.cs
Normal file
145
tests/NT8.Core.Tests/Indicators/AVWAPCalculatorTests.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Indicators;
|
||||
|
||||
namespace NT8.Core.Tests.Indicators
|
||||
{
|
||||
[TestClass]
|
||||
public class AVWAPCalculatorTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Constructor_InitializesWithAnchorMode()
|
||||
{
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, DateTime.UtcNow);
|
||||
Assert.AreEqual(AVWAPAnchorMode.Day, calc.GetAnchorMode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Calculate_NullBars_ThrowsArgumentNullException()
|
||||
{
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, DateTime.UtcNow);
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
calc.Calculate(null, DateTime.UtcNow);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Calculate_NoEligibleBars_ReturnsZero()
|
||||
{
|
||||
var anchor = DateTime.UtcNow;
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, anchor);
|
||||
|
||||
var bars = new List<BarData>();
|
||||
bars.Add(new BarData("ES", anchor.AddMinutes(-2), 100, 101, 99, 100, 1000, TimeSpan.FromMinutes(1)));
|
||||
|
||||
var value = calc.Calculate(bars, anchor);
|
||||
|
||||
Assert.AreEqual(0.0, value, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Calculate_SingleBar_ReturnsTypicalPrice()
|
||||
{
|
||||
var anchor = DateTime.UtcNow;
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, anchor);
|
||||
|
||||
var bars = new List<BarData>();
|
||||
bars.Add(new BarData("ES", anchor, 100, 103, 97, 101, 10, TimeSpan.FromMinutes(1)));
|
||||
|
||||
var value = calc.Calculate(bars, anchor);
|
||||
|
||||
var expected = (103.0 + 97.0 + 101.0) / 3.0;
|
||||
Assert.AreEqual(expected, value, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Calculate_MultipleBars_ReturnsVolumeWeightedValue()
|
||||
{
|
||||
var anchor = DateTime.UtcNow;
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, anchor);
|
||||
|
||||
var bars = new List<BarData>();
|
||||
bars.Add(new BarData("ES", anchor, 100, 102, 98, 101, 10, TimeSpan.FromMinutes(1))); // typical 100.3333
|
||||
bars.Add(new BarData("ES", anchor.AddMinutes(1), 101, 103, 99, 102, 30, TimeSpan.FromMinutes(1))); // typical 101.3333
|
||||
|
||||
var value = calc.Calculate(bars, anchor);
|
||||
|
||||
var p1 = (102.0 + 98.0 + 101.0) / 3.0;
|
||||
var p2 = (103.0 + 99.0 + 102.0) / 3.0;
|
||||
var expected = ((p1 * 10.0) + (p2 * 30.0)) / 40.0;
|
||||
Assert.AreEqual(expected, value, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Update_NegativeVolume_ThrowsArgumentException()
|
||||
{
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, DateTime.UtcNow);
|
||||
Assert.ThrowsException<ArgumentException>(delegate
|
||||
{
|
||||
calc.Update(100.0, -1);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Update_ThenGetCurrentValue_ReturnsWeightedAverage()
|
||||
{
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, DateTime.UtcNow);
|
||||
calc.Update(100.0, 10);
|
||||
calc.Update(110.0, 30);
|
||||
|
||||
var value = calc.GetCurrentValue();
|
||||
var expected = ((100.0 * 10.0) + (110.0 * 30.0)) / 40.0;
|
||||
|
||||
Assert.AreEqual(expected, value, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetSlope_InsufficientHistory_ReturnsZero()
|
||||
{
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, DateTime.UtcNow);
|
||||
calc.Update(100.0, 10);
|
||||
|
||||
var slope = calc.GetSlope(5);
|
||||
Assert.AreEqual(0.0, slope, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetSlope_WithHistory_ReturnsPositiveForRisingSeries()
|
||||
{
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, DateTime.UtcNow);
|
||||
calc.Update(100.0, 10);
|
||||
calc.Update(101.0, 10);
|
||||
calc.Update(102.0, 10);
|
||||
calc.Update(103.0, 10);
|
||||
calc.Update(104.0, 10);
|
||||
calc.Update(105.0, 10);
|
||||
|
||||
var slope = calc.GetSlope(3);
|
||||
|
||||
Assert.IsTrue(slope > 0.0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ResetAnchor_ClearsAccumulation()
|
||||
{
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, DateTime.UtcNow);
|
||||
calc.Update(100.0, 10);
|
||||
Assert.IsTrue(calc.GetCurrentValue() > 0.0);
|
||||
|
||||
calc.ResetAnchor(DateTime.UtcNow.AddHours(1));
|
||||
|
||||
Assert.AreEqual(0.0, calc.GetCurrentValue(), 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetAnchorMode_ChangesMode()
|
||||
{
|
||||
var calc = new AVWAPCalculator(AVWAPAnchorMode.Day, DateTime.UtcNow);
|
||||
calc.SetAnchorMode(AVWAPAnchorMode.Week);
|
||||
Assert.AreEqual(AVWAPAnchorMode.Week, calc.GetAnchorMode());
|
||||
}
|
||||
}
|
||||
}
|
||||
390
tests/NT8.Core.Tests/Intelligence/ConfluenceScorerTests.cs
Normal file
390
tests/NT8.Core.Tests/Intelligence/ConfluenceScorerTests.cs
Normal file
@@ -0,0 +1,390 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Tests.Intelligence
|
||||
{
|
||||
[TestClass]
|
||||
public class ConfluenceScorerTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
new ConfluenceScorer(null, 100);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_InvalidHistory_ThrowsArgumentException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentException>(delegate
|
||||
{
|
||||
new ConfluenceScorer(new BasicLogger("test"), 0);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapScoreToGrade_At090_ReturnsAPlus()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var grade = scorer.MapScoreToGrade(0.90);
|
||||
Assert.AreEqual(TradeGrade.APlus, grade);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapScoreToGrade_At080_ReturnsA()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var grade = scorer.MapScoreToGrade(0.80);
|
||||
Assert.AreEqual(TradeGrade.A, grade);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapScoreToGrade_At070_ReturnsB()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var grade = scorer.MapScoreToGrade(0.70);
|
||||
Assert.AreEqual(TradeGrade.B, grade);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapScoreToGrade_At060_ReturnsC()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var grade = scorer.MapScoreToGrade(0.60);
|
||||
Assert.AreEqual(TradeGrade.C, grade);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapScoreToGrade_At050_ReturnsD()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var grade = scorer.MapScoreToGrade(0.50);
|
||||
Assert.AreEqual(TradeGrade.D, grade);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapScoreToGrade_Below050_ReturnsF()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var grade = scorer.MapScoreToGrade(0.49);
|
||||
Assert.AreEqual(TradeGrade.F, grade);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateScore_NullIntent_ThrowsArgumentNullException()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
var factors = CreateFactors();
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
scorer.CalculateScore(null, context, bar, factors);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateScore_NullContext_ThrowsArgumentNullException()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var intent = CreateIntent();
|
||||
var bar = CreateBar();
|
||||
var factors = CreateFactors();
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
scorer.CalculateScore(intent, null, bar, factors);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateScore_NullBar_ThrowsArgumentNullException()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var factors = CreateFactors();
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
scorer.CalculateScore(intent, context, null, factors);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateScore_NullFactors_ThrowsArgumentNullException()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
scorer.CalculateScore(intent, context, bar, null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateScore_EmptyFactors_ReturnsZeroScoreAndF()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
|
||||
var result = scorer.CalculateScore(intent, context, bar, new List<IFactorCalculator>());
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(0.0, result.RawScore, 0.000001);
|
||||
Assert.AreEqual(0.0, result.WeightedScore, 0.000001);
|
||||
Assert.AreEqual(TradeGrade.F, result.Grade);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateScore_SingleFactor_UsesFactorScore()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
var factors = new List<IFactorCalculator>();
|
||||
factors.Add(new FixedFactorCalculator(FactorType.Setup, 0.75, 1.0));
|
||||
|
||||
var result = scorer.CalculateScore(intent, context, bar, factors);
|
||||
|
||||
Assert.AreEqual(0.75, result.RawScore, 0.000001);
|
||||
Assert.AreEqual(0.75, result.WeightedScore, 0.000001);
|
||||
Assert.AreEqual(TradeGrade.B, result.Grade);
|
||||
Assert.AreEqual(1, result.Factors.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateScore_MultipleFactors_CalculatesWeightedAverage()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
var factors = new List<IFactorCalculator>();
|
||||
factors.Add(new FixedFactorCalculator(FactorType.Setup, 1.0, 1.0));
|
||||
factors.Add(new FixedFactorCalculator(FactorType.Trend, 0.5, 1.0));
|
||||
factors.Add(new FixedFactorCalculator(FactorType.Timing, 0.0, 1.0));
|
||||
|
||||
var result = scorer.CalculateScore(intent, context, bar, factors);
|
||||
|
||||
Assert.AreEqual(0.5, result.RawScore, 0.000001);
|
||||
Assert.AreEqual(0.5, result.WeightedScore, 0.000001);
|
||||
Assert.AreEqual(TradeGrade.D, result.Grade);
|
||||
Assert.AreEqual(3, result.Factors.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFactorWeights_AppliesOverrides()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
|
||||
var updates = new Dictionary<FactorType, double>();
|
||||
updates.Add(FactorType.Setup, 2.0);
|
||||
updates.Add(FactorType.Trend, 1.0);
|
||||
scorer.UpdateFactorWeights(updates);
|
||||
|
||||
var factors = new List<IFactorCalculator>();
|
||||
factors.Add(new FixedFactorCalculator(FactorType.Setup, 1.0, 1.0));
|
||||
factors.Add(new FixedFactorCalculator(FactorType.Trend, 0.0, 1.0));
|
||||
|
||||
var result = scorer.CalculateScore(intent, context, bar, factors);
|
||||
|
||||
Assert.AreEqual(0.666666, result.WeightedScore, 0.0005);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFactorWeights_InvalidWeight_ThrowsArgumentException()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var updates = new Dictionary<FactorType, double>();
|
||||
updates.Add(FactorType.Setup, 0.0);
|
||||
|
||||
Assert.ThrowsException<ArgumentException>(delegate
|
||||
{
|
||||
scorer.UpdateFactorWeights(updates);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetHistoricalStats_Empty_ReturnsDefaults()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
|
||||
var stats = scorer.GetHistoricalStats();
|
||||
|
||||
Assert.IsNotNull(stats);
|
||||
Assert.AreEqual(0, stats.TotalCalculations);
|
||||
Assert.AreEqual(0.0, stats.AverageWeightedScore, 0.000001);
|
||||
Assert.AreEqual(0.0, stats.AverageRawScore, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetHistoricalStats_AfterScores_ReturnsCounts()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
|
||||
var factors1 = new List<IFactorCalculator>();
|
||||
factors1.Add(new FixedFactorCalculator(FactorType.Setup, 0.90, 1.0));
|
||||
|
||||
var factors2 = new List<IFactorCalculator>();
|
||||
factors2.Add(new FixedFactorCalculator(FactorType.Setup, 0.40, 1.0));
|
||||
|
||||
scorer.CalculateScore(intent, context, bar, factors1);
|
||||
scorer.CalculateScore(intent, context, bar, factors2);
|
||||
|
||||
var stats = scorer.GetHistoricalStats();
|
||||
|
||||
Assert.AreEqual(2, stats.TotalCalculations);
|
||||
Assert.IsTrue(stats.BestWeightedScore >= stats.WorstWeightedScore);
|
||||
Assert.IsTrue(stats.GradeDistribution[TradeGrade.APlus] >= 0);
|
||||
Assert.IsTrue(stats.GradeDistribution[TradeGrade.F] >= 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateScore_CurrentBarOverload_UsesContextCustomData()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
context.CustomData["current_bar"] = bar;
|
||||
|
||||
var factors = CreateFactors();
|
||||
var result = scorer.CalculateScore(intent, context, factors);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsTrue(result.WeightedScore >= 0.0);
|
||||
Assert.IsTrue(result.WeightedScore <= 1.0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateScore_CurrentBarOverload_MissingBar_ThrowsArgumentException()
|
||||
{
|
||||
var scorer = CreateScorer();
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var factors = CreateFactors();
|
||||
|
||||
Assert.ThrowsException<ArgumentException>(delegate
|
||||
{
|
||||
scorer.CalculateScore(intent, context, factors);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateScore_HistoryRespectsMaxCapacity()
|
||||
{
|
||||
var scorer = new ConfluenceScorer(new BasicLogger("test"), 2);
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
|
||||
var factorsA = new List<IFactorCalculator>();
|
||||
factorsA.Add(new FixedFactorCalculator(FactorType.Setup, 0.9, 1.0));
|
||||
var factorsB = new List<IFactorCalculator>();
|
||||
factorsB.Add(new FixedFactorCalculator(FactorType.Setup, 0.8, 1.0));
|
||||
var factorsC = new List<IFactorCalculator>();
|
||||
factorsC.Add(new FixedFactorCalculator(FactorType.Setup, 0.7, 1.0));
|
||||
|
||||
scorer.CalculateScore(intent, context, bar, factorsA);
|
||||
scorer.CalculateScore(intent, context, bar, factorsB);
|
||||
scorer.CalculateScore(intent, context, bar, factorsC);
|
||||
|
||||
var stats = scorer.GetHistoricalStats();
|
||||
Assert.AreEqual(2, stats.TotalCalculations);
|
||||
}
|
||||
|
||||
private static ConfluenceScorer CreateScorer()
|
||||
{
|
||||
return new ConfluenceScorer(new BasicLogger("ConfluenceScorerTests"), 100);
|
||||
}
|
||||
|
||||
private static StrategyIntent CreateIntent()
|
||||
{
|
||||
return new StrategyIntent(
|
||||
"ES",
|
||||
OrderSide.Buy,
|
||||
OrderType.Market,
|
||||
null,
|
||||
8,
|
||||
16,
|
||||
0.8,
|
||||
"Test",
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static StrategyContext CreateContext()
|
||||
{
|
||||
return new StrategyContext(
|
||||
"ES",
|
||||
DateTime.UtcNow,
|
||||
new Position("ES", 0, 0, 0, 0, DateTime.UtcNow),
|
||||
new AccountInfo(100000, 100000, 0, 0, DateTime.UtcNow),
|
||||
new MarketSession(DateTime.Today.AddHours(9.5), DateTime.Today.AddHours(16), true, "RTH"),
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static BarData CreateBar()
|
||||
{
|
||||
return new BarData("ES", DateTime.UtcNow, 5000, 5005, 4998, 5002, 1000, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static List<IFactorCalculator> CreateFactors()
|
||||
{
|
||||
var factors = new List<IFactorCalculator>();
|
||||
factors.Add(new FixedFactorCalculator(FactorType.Setup, 0.8, 1.0));
|
||||
factors.Add(new FixedFactorCalculator(FactorType.Trend, 0.7, 1.0));
|
||||
factors.Add(new FixedFactorCalculator(FactorType.Volatility, 0.6, 1.0));
|
||||
return factors;
|
||||
}
|
||||
|
||||
private class FixedFactorCalculator : IFactorCalculator
|
||||
{
|
||||
private readonly FactorType _type;
|
||||
private readonly double _score;
|
||||
private readonly double _weight;
|
||||
|
||||
public FixedFactorCalculator(FactorType type, double score, double weight)
|
||||
{
|
||||
_type = type;
|
||||
_score = score;
|
||||
_weight = weight;
|
||||
}
|
||||
|
||||
public FactorType Type
|
||||
{
|
||||
get { return _type; }
|
||||
}
|
||||
|
||||
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
|
||||
{
|
||||
return new ConfluenceFactor(
|
||||
_type,
|
||||
"Fixed",
|
||||
_score,
|
||||
_weight,
|
||||
"Test factor",
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
275
tests/NT8.Core.Tests/Intelligence/RegimeDetectionTests.cs
Normal file
275
tests/NT8.Core.Tests/Intelligence/RegimeDetectionTests.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Tests.Intelligence
|
||||
{
|
||||
[TestClass]
|
||||
public class RegimeDetectionTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void VolatilityDetector_ClassifiesLow()
|
||||
{
|
||||
var detector = CreateVolDetector();
|
||||
var regime = detector.DetectRegime("ES", 0.5, 1.0);
|
||||
Assert.AreEqual(VolatilityRegime.Low, regime);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void VolatilityDetector_ClassifiesBelowNormal()
|
||||
{
|
||||
var detector = CreateVolDetector();
|
||||
var regime = detector.DetectRegime("ES", 0.7, 1.0);
|
||||
Assert.AreEqual(VolatilityRegime.BelowNormal, regime);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void VolatilityDetector_ClassifiesNormal()
|
||||
{
|
||||
var detector = CreateVolDetector();
|
||||
var regime = detector.DetectRegime("ES", 1.0, 1.0);
|
||||
Assert.AreEqual(VolatilityRegime.Normal, regime);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void VolatilityDetector_ClassifiesElevated()
|
||||
{
|
||||
var detector = CreateVolDetector();
|
||||
var regime = detector.DetectRegime("ES", 1.3, 1.0);
|
||||
Assert.AreEqual(VolatilityRegime.Elevated, regime);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void VolatilityDetector_ClassifiesHigh()
|
||||
{
|
||||
var detector = CreateVolDetector();
|
||||
var regime = detector.DetectRegime("ES", 1.7, 1.0);
|
||||
Assert.AreEqual(VolatilityRegime.High, regime);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void VolatilityDetector_ClassifiesExtreme()
|
||||
{
|
||||
var detector = CreateVolDetector();
|
||||
var regime = detector.DetectRegime("ES", 2.2, 1.0);
|
||||
Assert.AreEqual(VolatilityRegime.Extreme, regime);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void VolatilityDetector_CalculateScore_ReturnsRatio()
|
||||
{
|
||||
var detector = CreateVolDetector();
|
||||
var score = detector.CalculateVolatilityScore(1.5, 1.0);
|
||||
Assert.AreEqual(1.5, score, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void VolatilityDetector_TransitionHistory_TracksChanges()
|
||||
{
|
||||
var detector = CreateVolDetector();
|
||||
detector.DetectRegime("NQ", 1.0, 1.0);
|
||||
detector.DetectRegime("NQ", 2.3, 1.0);
|
||||
|
||||
var transitions = detector.GetTransitions("NQ");
|
||||
Assert.IsTrue(transitions.Count >= 1);
|
||||
Assert.AreEqual("NQ", transitions[0].Symbol);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void VolatilityDetector_GetCurrentRegime_Unknown_ReturnsNormal()
|
||||
{
|
||||
var detector = CreateVolDetector();
|
||||
var regime = detector.GetCurrentRegime("GC");
|
||||
Assert.AreEqual(VolatilityRegime.Normal, regime);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TrendDetector_DetectsStrongUp()
|
||||
{
|
||||
var detector = CreateTrendDetector();
|
||||
var bars = BuildUptrendBars(12, 5000.0, 2.0);
|
||||
|
||||
var regime = detector.DetectTrend("ES", bars, 4995.0);
|
||||
|
||||
Assert.IsTrue(regime == TrendRegime.StrongUp || regime == TrendRegime.WeakUp);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TrendDetector_DetectsStrongDown()
|
||||
{
|
||||
var detector = CreateTrendDetector();
|
||||
var bars = BuildDowntrendBars(12, 5000.0, 2.0);
|
||||
|
||||
var regime = detector.DetectTrend("ES", bars, 5005.0);
|
||||
|
||||
Assert.IsTrue(regime == TrendRegime.StrongDown || regime == TrendRegime.WeakDown);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TrendDetector_CalculateStrength_UptrendPositive()
|
||||
{
|
||||
var detector = CreateTrendDetector();
|
||||
var bars = BuildUptrendBars(10, 100.0, 1.0);
|
||||
|
||||
var strength = detector.CalculateTrendStrength(bars, 95.0);
|
||||
|
||||
Assert.IsTrue(strength > 0.0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TrendDetector_CalculateStrength_DowntrendNegative()
|
||||
{
|
||||
var detector = CreateTrendDetector();
|
||||
var bars = BuildDowntrendBars(10, 100.0, 1.0);
|
||||
|
||||
var strength = detector.CalculateTrendStrength(bars, 105.0);
|
||||
|
||||
Assert.IsTrue(strength < 0.0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TrendDetector_IsRanging_FlatBars_True()
|
||||
{
|
||||
var detector = CreateTrendDetector();
|
||||
var bars = BuildRangeBars(10, 100.0, 0.1);
|
||||
|
||||
var ranging = detector.IsRanging(bars, 0.2);
|
||||
|
||||
Assert.IsTrue(ranging);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TrendDetector_AssessTrendQuality_GoodStructure_ReturnsNotPoor()
|
||||
{
|
||||
var detector = CreateTrendDetector();
|
||||
var bars = BuildUptrendBars(12, 100.0, 0.8);
|
||||
|
||||
var quality = detector.AssessTrendQuality(bars);
|
||||
|
||||
Assert.IsTrue(quality == TrendQuality.Fair || quality == TrendQuality.Good || quality == TrendQuality.Excellent);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RegimeManager_UpdateAndGetCurrentRegime_ReturnsState()
|
||||
{
|
||||
var manager = CreateRegimeManager();
|
||||
var bar = CreateBar("ES", 5000, 5004, 4998, 5002, 1000);
|
||||
|
||||
manager.UpdateRegime("ES", bar, 5000.0, 1.0, 1.0);
|
||||
var state = manager.GetCurrentRegime("ES");
|
||||
|
||||
Assert.IsNotNull(state);
|
||||
Assert.AreEqual("ES", state.Symbol);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RegimeManager_ShouldAdjustStrategy_ExtremeVolatility_True()
|
||||
{
|
||||
var manager = CreateRegimeManager();
|
||||
var bars = BuildUptrendBars(6, 5000.0, 1.0);
|
||||
|
||||
for (var i = 0; i < bars.Count; i++)
|
||||
{
|
||||
manager.UpdateRegime("ES", bars[i], 5000.0, 2.5, 1.0);
|
||||
}
|
||||
|
||||
var intent = CreateIntent(OrderSide.Buy);
|
||||
var shouldAdjust = manager.ShouldAdjustStrategy("ES", intent);
|
||||
|
||||
Assert.IsTrue(shouldAdjust);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RegimeManager_TransitionsRecorded_WhenRegimeChanges()
|
||||
{
|
||||
var manager = CreateRegimeManager();
|
||||
var bars = BuildUptrendBars(6, 5000.0, 1.0);
|
||||
|
||||
for (var i = 0; i < bars.Count; i++)
|
||||
{
|
||||
manager.UpdateRegime("NQ", bars[i], 5000.0, 1.0, 1.0);
|
||||
}
|
||||
|
||||
manager.UpdateRegime("NQ", CreateBar("NQ", 5000, 5001, 4990, 4991, 1500), 5000.0, 2.3, 1.0);
|
||||
|
||||
var transitions = manager.GetRecentTransitions("NQ", TimeSpan.FromHours(2));
|
||||
Assert.IsTrue(transitions.Count >= 1);
|
||||
}
|
||||
|
||||
private static VolatilityRegimeDetector CreateVolDetector()
|
||||
{
|
||||
return new VolatilityRegimeDetector(new BasicLogger("RegimeDetectionTests"), 50);
|
||||
}
|
||||
|
||||
private static TrendRegimeDetector CreateTrendDetector()
|
||||
{
|
||||
return new TrendRegimeDetector(new BasicLogger("RegimeDetectionTests"));
|
||||
}
|
||||
|
||||
private static RegimeManager CreateRegimeManager()
|
||||
{
|
||||
var logger = new BasicLogger("RegimeManagerTests");
|
||||
var vol = new VolatilityRegimeDetector(logger, 50);
|
||||
var trend = new TrendRegimeDetector(logger);
|
||||
return new RegimeManager(logger, vol, trend, 200, 100);
|
||||
}
|
||||
|
||||
private static List<BarData> BuildUptrendBars(int count, double start, double step)
|
||||
{
|
||||
var result = new List<BarData>();
|
||||
var time = DateTime.UtcNow.AddMinutes(-count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var close = start + (i * step);
|
||||
result.Add(new BarData("ES", time.AddMinutes(i), close - 1.0, close + 1.0, close - 2.0, close, 1000 + i, TimeSpan.FromMinutes(1)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<BarData> BuildDowntrendBars(int count, double start, double step)
|
||||
{
|
||||
var result = new List<BarData>();
|
||||
var time = DateTime.UtcNow.AddMinutes(-count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var close = start - (i * step);
|
||||
result.Add(new BarData("ES", time.AddMinutes(i), close + 1.0, close + 2.0, close - 1.0, close, 1000 + i, TimeSpan.FromMinutes(1)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<BarData> BuildRangeBars(int count, double center, double amplitude)
|
||||
{
|
||||
var result = new List<BarData>();
|
||||
var time = DateTime.UtcNow.AddMinutes(-count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var close = center + ((i % 2 == 0) ? amplitude : -amplitude);
|
||||
result.Add(new BarData("ES", time.AddMinutes(i), close - 0.05, close + 0.10, close - 0.10, close, 800 + i, TimeSpan.FromMinutes(1)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static BarData CreateBar(string symbol, double open, double high, double low, double close, long volume)
|
||||
{
|
||||
return new BarData(symbol, DateTime.UtcNow, open, high, low, close, volume, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static StrategyIntent CreateIntent(OrderSide side)
|
||||
{
|
||||
return new StrategyIntent(
|
||||
"ES",
|
||||
side,
|
||||
OrderType.Market,
|
||||
null,
|
||||
8,
|
||||
16,
|
||||
0.8,
|
||||
"Test",
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
}
|
||||
}
|
||||
197
tests/NT8.Core.Tests/Intelligence/RiskModeManagerTests.cs
Normal file
197
tests/NT8.Core.Tests/Intelligence/RiskModeManagerTests.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Tests.Intelligence
|
||||
{
|
||||
[TestClass]
|
||||
public class RiskModeManagerTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
new RiskModeManager(null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_DefaultMode_IsPCP()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
Assert.AreEqual(RiskMode.PCP, manager.GetCurrentMode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetModeConfig_ECP_ReturnsExpectedValues()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
var config = manager.GetModeConfig(RiskMode.ECP);
|
||||
|
||||
Assert.IsNotNull(config);
|
||||
Assert.AreEqual(RiskMode.ECP, config.Mode);
|
||||
Assert.AreEqual(1.5, config.SizeMultiplier, 0.000001);
|
||||
Assert.AreEqual(TradeGrade.B, config.MinimumGrade);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateRiskMode_StrongPerformance_TransitionsToECP()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
|
||||
manager.UpdateRiskMode(600.0, 5, 0);
|
||||
|
||||
Assert.AreEqual(RiskMode.ECP, manager.GetCurrentMode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateRiskMode_DailyLoss_TransitionsToDCP()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
|
||||
manager.UpdateRiskMode(-250.0, 0, 1);
|
||||
|
||||
Assert.AreEqual(RiskMode.DCP, manager.GetCurrentMode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateRiskMode_LossStreak3_TransitionsToHR()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
|
||||
manager.UpdateRiskMode(0.0, 0, 3);
|
||||
|
||||
Assert.AreEqual(RiskMode.HR, manager.GetCurrentMode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OverrideMode_SetsManualOverrideAndMode()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
|
||||
manager.OverrideMode(RiskMode.HR, "Risk officer action");
|
||||
var state = manager.GetState();
|
||||
|
||||
Assert.AreEqual(RiskMode.HR, manager.GetCurrentMode());
|
||||
Assert.IsTrue(state.IsManualOverride);
|
||||
Assert.AreEqual("Risk officer action", state.LastTransitionReason);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateRiskMode_WhenManualOverride_DoesNotChangeMode()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
manager.OverrideMode(RiskMode.DCP, "Manual hold");
|
||||
|
||||
manager.UpdateRiskMode(1000.0, 5, 0);
|
||||
|
||||
Assert.AreEqual(RiskMode.DCP, manager.GetCurrentMode());
|
||||
Assert.IsTrue(manager.GetState().IsManualOverride);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ResetToDefault_FromManualOverride_ResetsToPCP()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
manager.OverrideMode(RiskMode.HR, "Stop trading");
|
||||
|
||||
manager.ResetToDefault();
|
||||
|
||||
var state = manager.GetState();
|
||||
Assert.AreEqual(RiskMode.PCP, manager.GetCurrentMode());
|
||||
Assert.IsFalse(state.IsManualOverride);
|
||||
Assert.AreEqual("Reset to default", state.LastTransitionReason);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldTransitionMode_ExtremeVolatility_ReturnsTrue()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
var metrics = new PerformanceMetrics(0.0, 1, 0, 0.6, 0.8, VolatilityRegime.Extreme);
|
||||
|
||||
var shouldTransition = manager.ShouldTransitionMode(RiskMode.PCP, metrics);
|
||||
|
||||
Assert.IsTrue(shouldTransition);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldTransitionMode_NoChangeConditions_ReturnsFalse()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
var metrics = new PerformanceMetrics(50.0, 1, 0, 0.7, 0.8, VolatilityRegime.Normal);
|
||||
|
||||
var shouldTransition = manager.ShouldTransitionMode(RiskMode.PCP, metrics);
|
||||
|
||||
Assert.IsFalse(shouldTransition);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldTransitionMode_FromDCPWithRecovery_ReturnsTrue()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
var metrics = new PerformanceMetrics(50.0, 2, 0, 0.8, 0.9, VolatilityRegime.Normal);
|
||||
|
||||
var shouldTransition = manager.ShouldTransitionMode(RiskMode.DCP, metrics);
|
||||
|
||||
Assert.IsTrue(shouldTransition);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateRiskMode_FromDCPWithRecovery_TransitionsToPCP()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
manager.OverrideMode(RiskMode.DCP, "Set DCP");
|
||||
manager.ResetToDefault();
|
||||
manager.OverrideMode(RiskMode.DCP, "Re-enter DCP");
|
||||
manager.ResetToDefault();
|
||||
|
||||
manager.OverrideMode(RiskMode.DCP, "Start in DCP");
|
||||
manager.ResetToDefault();
|
||||
manager.OverrideMode(RiskMode.DCP, "Start in DCP again");
|
||||
manager.ResetToDefault();
|
||||
|
||||
// put in DCP without manual override
|
||||
manager.UpdateRiskMode(-300.0, 0, 1);
|
||||
Assert.AreEqual(RiskMode.DCP, manager.GetCurrentMode());
|
||||
|
||||
manager.UpdateRiskMode(100.0, 2, 0);
|
||||
|
||||
Assert.AreEqual(RiskMode.PCP, manager.GetCurrentMode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OverrideMode_EmptyReason_ThrowsArgumentNullException()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
manager.OverrideMode(RiskMode.HR, string.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateRiskMode_NegativeStreaks_ThrowsArgumentException()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
|
||||
Assert.ThrowsException<ArgumentException>(delegate
|
||||
{
|
||||
manager.UpdateRiskMode(0.0, -1, 0);
|
||||
});
|
||||
|
||||
Assert.ThrowsException<ArgumentException>(delegate
|
||||
{
|
||||
manager.UpdateRiskMode(0.0, 0, -1);
|
||||
});
|
||||
}
|
||||
|
||||
private static RiskModeManager CreateManager()
|
||||
{
|
||||
return new RiskModeManager(new BasicLogger("RiskModeManagerTests"));
|
||||
}
|
||||
}
|
||||
}
|
||||
196
tests/NT8.Core.Tests/MarketData/LiquidityMonitorTests.cs
Normal file
196
tests/NT8.Core.Tests/MarketData/LiquidityMonitorTests.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.MarketData;
|
||||
using NT8.Core.Tests.Mocks;
|
||||
|
||||
namespace NT8.Core.Tests.MarketData
|
||||
{
|
||||
[TestClass]
|
||||
public class LiquidityMonitorTests
|
||||
{
|
||||
private LiquidityMonitor _monitor;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_monitor = new LiquidityMonitor(new MockLogger<LiquidityMonitor>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
new LiquidityMonitor(null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateSpread_ValidInput_UpdatesCurrentSpread()
|
||||
{
|
||||
_monitor.UpdateSpread("ES", 5000.00, 5000.25, 1000);
|
||||
|
||||
var spread = _monitor.GetCurrentSpread("ES");
|
||||
Assert.AreEqual(0.25, spread, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateSpread_NullSymbol_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_monitor.UpdateSpread(null, 100, 101, 10);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetCurrentSpread_NoData_ReturnsZero()
|
||||
{
|
||||
var spread = _monitor.GetCurrentSpread("NQ");
|
||||
Assert.AreEqual(0.0, spread, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetAverageSpread_WithHistory_ReturnsAverage()
|
||||
{
|
||||
_monitor.UpdateSpread("ES", 5000.00, 5000.25, 1000);
|
||||
_monitor.UpdateSpread("ES", 5000.00, 5000.50, 1000);
|
||||
|
||||
var avg = _monitor.GetAverageSpread("ES");
|
||||
Assert.AreEqual(0.375, avg, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetAverageSpread_NoHistory_ReturnsZero()
|
||||
{
|
||||
var avg = _monitor.GetAverageSpread("GC");
|
||||
Assert.AreEqual(0.0, avg, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetLiquidityMetrics_WithData_ReturnsCurrentAndAverage()
|
||||
{
|
||||
_monitor.UpdateSpread("ES", 5000.00, 5000.25, 1000);
|
||||
_monitor.UpdateSpread("ES", 5000.00, 5000.50, 1000);
|
||||
|
||||
var metrics = _monitor.GetLiquidityMetrics("ES");
|
||||
|
||||
Assert.AreEqual("ES", metrics.Symbol);
|
||||
Assert.AreEqual(0.50, metrics.Spread, 0.000001);
|
||||
Assert.AreEqual(0.375, metrics.AverageSpread, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetLiquidityMetrics_NoData_ReturnsDefaultMetrics()
|
||||
{
|
||||
var metrics = _monitor.GetLiquidityMetrics("CL");
|
||||
|
||||
Assert.AreEqual("CL", metrics.Symbol);
|
||||
Assert.AreEqual(0.0, metrics.Spread, 0.000001);
|
||||
Assert.AreEqual(0.0, metrics.AverageSpread, 0.000001);
|
||||
Assert.AreEqual(0L, metrics.TotalDepth);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateLiquidityScore_NoData_ReturnsPoor()
|
||||
{
|
||||
var score = _monitor.CalculateLiquidityScore("MES");
|
||||
Assert.AreEqual(LiquidityScore.Poor, score);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateLiquidityScore_SpreadAboveAverage_ReturnsFairOrPoor()
|
||||
{
|
||||
_monitor.UpdateSpread("ES", 5000.00, 5000.25, 1000);
|
||||
_monitor.UpdateSpread("ES", 5000.00, 5000.25, 1000);
|
||||
_monitor.UpdateSpread("ES", 5000.00, 5000.50, 1000);
|
||||
|
||||
var score = _monitor.CalculateLiquidityScore("ES");
|
||||
|
||||
var valid = score == LiquidityScore.Fair || score == LiquidityScore.Poor || score == LiquidityScore.Good;
|
||||
Assert.IsTrue(valid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateLiquidityScore_TightSpread_ReturnsGoodOrExcellent()
|
||||
{
|
||||
_monitor.UpdateSpread("NQ", 18000.00, 18000.25, 1000);
|
||||
_monitor.UpdateSpread("NQ", 18000.00, 18000.25, 1000);
|
||||
_monitor.UpdateSpread("NQ", 18000.00, 18000.25, 1000);
|
||||
|
||||
var score = _monitor.CalculateLiquidityScore("NQ");
|
||||
|
||||
var valid = score == LiquidityScore.Good || score == LiquidityScore.Excellent;
|
||||
Assert.IsTrue(valid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IsLiquidityAcceptable_ThresholdMet_ReturnsTrue()
|
||||
{
|
||||
_monitor.UpdateSpread("MNQ", 18000.00, 18000.25, 1000);
|
||||
_monitor.UpdateSpread("MNQ", 18000.00, 18000.25, 1000);
|
||||
|
||||
var result = _monitor.IsLiquidityAcceptable("MNQ", LiquidityScore.Poor);
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IsLiquidityAcceptable_ThresholdTooHigh_CanReturnFalse()
|
||||
{
|
||||
_monitor.UpdateSpread("GC", 2000.0, 2001.0, 1000);
|
||||
_monitor.UpdateSpread("GC", 2000.0, 2002.0, 1000);
|
||||
|
||||
var result = _monitor.IsLiquidityAcceptable("GC", LiquidityScore.Excellent);
|
||||
Assert.IsFalse(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ClearSpreadHistory_RemovesTrackedValues()
|
||||
{
|
||||
_monitor.UpdateSpread("CL", 80.00, 80.05, 1000);
|
||||
_monitor.UpdateSpread("CL", 80.00, 80.10, 1000);
|
||||
|
||||
_monitor.ClearSpreadHistory("CL");
|
||||
|
||||
Assert.AreEqual(0.0, _monitor.GetCurrentSpread("CL"), 0.000001);
|
||||
Assert.AreEqual(0.0, _monitor.GetAverageSpread("CL"), 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConcurrentUpdates_AreThreadSafe()
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
var local = i;
|
||||
tasks.Add(Task.Run(delegate
|
||||
{
|
||||
var bid = 5000.0 + (local * 0.01);
|
||||
var ask = bid + 0.25;
|
||||
_monitor.UpdateSpread("ES", bid, ask, 1000 + local);
|
||||
}));
|
||||
}
|
||||
|
||||
Task.WaitAll(tasks.ToArray());
|
||||
|
||||
var spread = _monitor.GetCurrentSpread("ES");
|
||||
Assert.IsTrue(spread > 0.0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RollingWindow_CapsAt100Samples_ForAverageCalculation()
|
||||
{
|
||||
for (var i = 0; i < 120; i++)
|
||||
{
|
||||
_monitor.UpdateSpread("ES", 5000.0, 5000.25 + (i % 2 == 0 ? 0.0 : 0.25), 1000);
|
||||
}
|
||||
|
||||
var avg = _monitor.GetAverageSpread("ES");
|
||||
Assert.IsTrue(avg >= 0.25);
|
||||
Assert.IsTrue(avg <= 0.50);
|
||||
}
|
||||
}
|
||||
}
|
||||
226
tests/NT8.Core.Tests/OMS/OrderTypeValidatorTests.cs
Normal file
226
tests/NT8.Core.Tests/OMS/OrderTypeValidatorTests.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.OMS;
|
||||
using NT8.Core.Tests.Mocks;
|
||||
|
||||
namespace NT8.Core.Tests.OMS
|
||||
{
|
||||
[TestClass]
|
||||
public class OrderTypeValidatorTests
|
||||
{
|
||||
private OrderTypeValidator _validator;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_validator = new OrderTypeValidator(new MockLogger<OrderTypeValidator>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
new OrderTypeValidator(null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateLimitOrder_ValidBuy_ReturnsValid()
|
||||
{
|
||||
var request = new LimitOrderRequest("ES", OrderSide.Buy, 1, 5000m, TimeInForce.Day);
|
||||
var result = _validator.ValidateLimitOrder(request, 5001m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateLimitOrder_ValidSell_ReturnsValid()
|
||||
{
|
||||
var request = new LimitOrderRequest("ES", OrderSide.Sell, 1, 5002m, TimeInForce.Day);
|
||||
var result = _validator.ValidateLimitOrder(request, 5001m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateLimitOrder_InvalidQuantity_ReturnsInvalid()
|
||||
{
|
||||
var request = new LimitOrderRequest("ES", OrderSide.Buy, 1, 5000m, TimeInForce.Day);
|
||||
request.Quantity = 0;
|
||||
var result = _validator.ValidateLimitOrder(request, 5001m);
|
||||
Assert.IsFalse(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateLimitOrder_NullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_validator.ValidateLimitOrder(null, 1m);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateStopOrder_BuyStopAboveMarket_ReturnsValid()
|
||||
{
|
||||
var request = new StopOrderRequest("NQ", OrderSide.Buy, 1, 18001m, TimeInForce.Day);
|
||||
var result = _validator.ValidateStopOrder(request, 18000m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateStopOrder_BuyStopBelowMarket_ReturnsInvalid()
|
||||
{
|
||||
var request = new StopOrderRequest("NQ", OrderSide.Buy, 1, 17999m, TimeInForce.Day);
|
||||
var result = _validator.ValidateStopOrder(request, 18000m);
|
||||
Assert.IsFalse(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateStopOrder_SellStopBelowMarket_ReturnsValid()
|
||||
{
|
||||
var request = new StopOrderRequest("NQ", OrderSide.Sell, 1, 17999m, TimeInForce.Day);
|
||||
var result = _validator.ValidateStopOrder(request, 18000m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateStopOrder_SellStopAboveMarket_ReturnsInvalid()
|
||||
{
|
||||
var request = new StopOrderRequest("NQ", OrderSide.Sell, 1, 18001m, TimeInForce.Day);
|
||||
var result = _validator.ValidateStopOrder(request, 18000m);
|
||||
Assert.IsFalse(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateStopOrder_NullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_validator.ValidateStopOrder(null, 1m);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateStopLimitOrder_BuyValidRelationship_ReturnsValid()
|
||||
{
|
||||
var request = new StopLimitOrderRequest("CL", OrderSide.Buy, 1, 81m, 81.1m, TimeInForce.Day);
|
||||
var result = _validator.ValidateStopLimitOrder(request, 80m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateStopLimitOrder_BuyInvalidLimitBelowStop_ReturnsInvalid()
|
||||
{
|
||||
var request = new StopLimitOrderRequest("CL", OrderSide.Buy, 1, 81m, 80.9m, TimeInForce.Day);
|
||||
var result = _validator.ValidateStopLimitOrder(request, 80m);
|
||||
Assert.IsFalse(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateStopLimitOrder_SellValidRelationship_ReturnsValid()
|
||||
{
|
||||
var request = new StopLimitOrderRequest("CL", OrderSide.Sell, 1, 79m, 78.9m, TimeInForce.Day);
|
||||
var result = _validator.ValidateStopLimitOrder(request, 80m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateStopLimitOrder_SellInvalidLimitAboveStop_ReturnsInvalid()
|
||||
{
|
||||
var request = new StopLimitOrderRequest("CL", OrderSide.Sell, 1, 79m, 79.1m, TimeInForce.Day);
|
||||
var result = _validator.ValidateStopLimitOrder(request, 80m);
|
||||
Assert.IsFalse(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateStopLimitOrder_NullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_validator.ValidateStopLimitOrder(null, 1m);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateMITOrder_BuyTriggerBelowMarket_ReturnsValid()
|
||||
{
|
||||
var request = new MITOrderRequest("GC", OrderSide.Buy, 1, 1999m, TimeInForce.Day);
|
||||
var result = _validator.ValidateMITOrder(request, 2000m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateMITOrder_BuyTriggerAboveMarket_ReturnsInvalid()
|
||||
{
|
||||
var request = new MITOrderRequest("GC", OrderSide.Buy, 1, 2001m, TimeInForce.Day);
|
||||
var result = _validator.ValidateMITOrder(request, 2000m);
|
||||
Assert.IsFalse(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateMITOrder_SellTriggerAboveMarket_ReturnsValid()
|
||||
{
|
||||
var request = new MITOrderRequest("GC", OrderSide.Sell, 1, 2001m, TimeInForce.Day);
|
||||
var result = _validator.ValidateMITOrder(request, 2000m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateMITOrder_SellTriggerBelowMarket_ReturnsInvalid()
|
||||
{
|
||||
var request = new MITOrderRequest("GC", OrderSide.Sell, 1, 1999m, TimeInForce.Day);
|
||||
var result = _validator.ValidateMITOrder(request, 2000m);
|
||||
Assert.IsFalse(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateMITOrder_NullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_validator.ValidateMITOrder(null, 1m);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateOrder_LimitOrder_DispatchesCorrectly()
|
||||
{
|
||||
var request = new LimitOrderRequest("ES", OrderSide.Buy, 1, 5000m, TimeInForce.Day);
|
||||
var result = _validator.ValidateOrder(request, 5001m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateOrder_StopOrder_DispatchesCorrectly()
|
||||
{
|
||||
var request = new StopOrderRequest("ES", OrderSide.Buy, 1, 5002m, TimeInForce.Day);
|
||||
var result = _validator.ValidateOrder(request, 5001m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateOrder_StopLimitOrder_DispatchesCorrectly()
|
||||
{
|
||||
var request = new StopLimitOrderRequest("ES", OrderSide.Buy, 1, 5002m, 5002.25m, TimeInForce.Day);
|
||||
var result = _validator.ValidateOrder(request, 5001m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateOrder_MitOrder_DispatchesCorrectly()
|
||||
{
|
||||
var request = new MITOrderRequest("ES", OrderSide.Buy, 1, 5000m, TimeInForce.Day);
|
||||
var result = _validator.ValidateOrder(request, 5001m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateOrder_NullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
_validator.ValidateOrder(null, 1m);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
273
tests/NT8.Core.Tests/Sizing/GradeBasedSizerTests.cs
Normal file
273
tests/NT8.Core.Tests/Sizing/GradeBasedSizerTests.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
using NT8.Core.Sizing;
|
||||
|
||||
namespace NT8.Core.Tests.Sizing
|
||||
{
|
||||
[TestClass]
|
||||
public class GradeBasedSizerTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
new GradeBasedSizer(null, new GradeFilter());
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_NullGradeFilter_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
new GradeBasedSizer(new BasicLogger("test"), null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CombineMultipliers_MultipliesValues()
|
||||
{
|
||||
var sizer = CreateSizer();
|
||||
var result = sizer.CombineMultipliers(1.25, 0.8);
|
||||
Assert.AreEqual(1.0, result, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ApplyConstraints_BelowMin_ReturnsMin()
|
||||
{
|
||||
var sizer = CreateSizer();
|
||||
var result = sizer.ApplyConstraints(0, 1, 10);
|
||||
Assert.AreEqual(1, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ApplyConstraints_AboveMax_ReturnsMax()
|
||||
{
|
||||
var sizer = CreateSizer();
|
||||
var result = sizer.ApplyConstraints(20, 1, 10);
|
||||
Assert.AreEqual(10, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ApplyConstraints_WithinRange_ReturnsInput()
|
||||
{
|
||||
var sizer = CreateSizer();
|
||||
var result = sizer.ApplyConstraints(5, 1, 10);
|
||||
Assert.AreEqual(5, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateGradeBasedSize_RejectedGrade_ReturnsZeroContracts()
|
||||
{
|
||||
var sizer = CreateSizer();
|
||||
var baseSizer = new StubPositionSizer(4, 400.0, SizingMethod.FixedDollarRisk);
|
||||
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var confluence = CreateScore(TradeGrade.C, 0.6);
|
||||
var config = CreateSizingConfig();
|
||||
var modeConfig = CreateModeConfig(RiskMode.DCP, 0.5, TradeGrade.A);
|
||||
|
||||
var result = sizer.CalculateGradeBasedSize(
|
||||
intent,
|
||||
context,
|
||||
confluence,
|
||||
RiskMode.DCP,
|
||||
config,
|
||||
baseSizer,
|
||||
modeConfig);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(0, result.Contracts);
|
||||
Assert.IsTrue(result.Calculations.ContainsKey("rejected"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateGradeBasedSize_AcceptedGrade_AppliesMultipliers()
|
||||
{
|
||||
var sizer = CreateSizer();
|
||||
var baseSizer = new StubPositionSizer(4, 400.0, SizingMethod.FixedDollarRisk);
|
||||
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var confluence = CreateScore(TradeGrade.A, 0.85);
|
||||
var config = CreateSizingConfig();
|
||||
var modeConfig = CreateModeConfig(RiskMode.ECP, 1.5, TradeGrade.B);
|
||||
|
||||
var result = sizer.CalculateGradeBasedSize(
|
||||
intent,
|
||||
context,
|
||||
confluence,
|
||||
RiskMode.ECP,
|
||||
config,
|
||||
baseSizer,
|
||||
modeConfig);
|
||||
|
||||
// Base contracts = 4
|
||||
// Grade multiplier (ECP, A) = 1.25
|
||||
// Mode multiplier = 1.5
|
||||
// Raw = 7.5, floor => 7
|
||||
Assert.AreEqual(7, result.Contracts);
|
||||
Assert.IsTrue(result.Calculations.ContainsKey("combined_multiplier"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateGradeBasedSize_RespectsMaxContracts()
|
||||
{
|
||||
var sizer = CreateSizer();
|
||||
var baseSizer = new StubPositionSizer(8, 800.0, SizingMethod.FixedDollarRisk);
|
||||
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var confluence = CreateScore(TradeGrade.APlus, 0.92);
|
||||
var config = new SizingConfig(SizingMethod.FixedDollarRisk, 1, 10, 500.0, new Dictionary<string, object>());
|
||||
var modeConfig = CreateModeConfig(RiskMode.ECP, 1.5, TradeGrade.B);
|
||||
|
||||
var result = sizer.CalculateGradeBasedSize(
|
||||
intent,
|
||||
context,
|
||||
confluence,
|
||||
RiskMode.ECP,
|
||||
config,
|
||||
baseSizer,
|
||||
modeConfig);
|
||||
|
||||
// 8 * 1.5 * 1.5 = 18 -> clamp 10
|
||||
Assert.AreEqual(10, result.Contracts);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateGradeBasedSize_RespectsMinContracts_WhenAccepted()
|
||||
{
|
||||
var sizer = CreateSizer();
|
||||
var baseSizer = new StubPositionSizer(1, 100.0, SizingMethod.FixedDollarRisk);
|
||||
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var confluence = CreateScore(TradeGrade.C, 0.61);
|
||||
var config = new SizingConfig(SizingMethod.FixedDollarRisk, 2, 10, 500.0, new Dictionary<string, object>());
|
||||
var modeConfig = CreateModeConfig(RiskMode.PCP, 1.0, TradeGrade.C);
|
||||
|
||||
var result = sizer.CalculateGradeBasedSize(
|
||||
intent,
|
||||
context,
|
||||
confluence,
|
||||
RiskMode.PCP,
|
||||
config,
|
||||
baseSizer,
|
||||
modeConfig);
|
||||
|
||||
Assert.AreEqual(2, result.Contracts);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculateGradeBasedSize_NullInputs_Throw()
|
||||
{
|
||||
var sizer = CreateSizer();
|
||||
var baseSizer = new StubPositionSizer(1, 100.0, SizingMethod.FixedDollarRisk);
|
||||
var intent = CreateIntent();
|
||||
var context = CreateContext();
|
||||
var confluence = CreateScore(TradeGrade.A, 0.8);
|
||||
var config = CreateSizingConfig();
|
||||
var modeConfig = CreateModeConfig(RiskMode.PCP, 1.0, TradeGrade.C);
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
sizer.CalculateGradeBasedSize(null, context, confluence, RiskMode.PCP, config, baseSizer, modeConfig);
|
||||
});
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
sizer.CalculateGradeBasedSize(intent, null, confluence, RiskMode.PCP, config, baseSizer, modeConfig);
|
||||
});
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(delegate
|
||||
{
|
||||
sizer.CalculateGradeBasedSize(intent, context, null, RiskMode.PCP, config, baseSizer, modeConfig);
|
||||
});
|
||||
}
|
||||
|
||||
private static GradeBasedSizer CreateSizer()
|
||||
{
|
||||
return new GradeBasedSizer(new BasicLogger("GradeBasedSizerTests"), new GradeFilter());
|
||||
}
|
||||
|
||||
private static StrategyIntent CreateIntent()
|
||||
{
|
||||
return new StrategyIntent(
|
||||
"ES",
|
||||
OrderSide.Buy,
|
||||
OrderType.Market,
|
||||
null,
|
||||
8,
|
||||
16,
|
||||
0.8,
|
||||
"Test intent",
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static StrategyContext CreateContext()
|
||||
{
|
||||
return new StrategyContext(
|
||||
"ES",
|
||||
DateTime.UtcNow,
|
||||
new Position("ES", 0, 0, 0, 0, DateTime.UtcNow),
|
||||
new AccountInfo(100000, 100000, 0, 0, DateTime.UtcNow),
|
||||
new MarketSession(DateTime.Today.AddHours(9.5), DateTime.Today.AddHours(16), true, "RTH"),
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static ConfluenceScore CreateScore(TradeGrade grade, double weighted)
|
||||
{
|
||||
var factors = new List<ConfluenceFactor>();
|
||||
factors.Add(new ConfluenceFactor(FactorType.Setup, "Setup", weighted, 1.0, "test", new Dictionary<string, object>()));
|
||||
|
||||
return new ConfluenceScore(
|
||||
weighted,
|
||||
weighted,
|
||||
grade,
|
||||
factors,
|
||||
DateTime.UtcNow,
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static SizingConfig CreateSizingConfig()
|
||||
{
|
||||
return new SizingConfig(SizingMethod.FixedDollarRisk, 1, 10, 500.0, new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static RiskModeConfig CreateModeConfig(RiskMode mode, double sizeMultiplier, TradeGrade minGrade)
|
||||
{
|
||||
return new RiskModeConfig(mode, sizeMultiplier, minGrade, 1000.0, 3, false, new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private class StubPositionSizer : IPositionSizer
|
||||
{
|
||||
private readonly int _contracts;
|
||||
private readonly double _risk;
|
||||
private readonly SizingMethod _method;
|
||||
|
||||
public StubPositionSizer(int contracts, double risk, SizingMethod method)
|
||||
{
|
||||
_contracts = contracts;
|
||||
_risk = risk;
|
||||
_method = method;
|
||||
}
|
||||
|
||||
public SizingResult CalculateSize(StrategyIntent intent, StrategyContext context, SizingConfig config)
|
||||
{
|
||||
return new SizingResult(_contracts, _risk, _method, new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
public SizingMetadata GetMetadata()
|
||||
{
|
||||
return new SizingMetadata("Stub", "Stub sizer", new List<string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
208
tests/NT8.Integration.Tests/Phase3IntegrationTests.cs
Normal file
208
tests/NT8.Integration.Tests/Phase3IntegrationTests.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Execution;
|
||||
using NT8.Core.MarketData;
|
||||
using NT8.Core.OMS;
|
||||
|
||||
namespace NT8.Integration.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class Phase3IntegrationTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Flow_LiquidityToOrderValidationToExecutionTracking_Works()
|
||||
{
|
||||
var marketLogger = new IntegrationMockLogger<LiquidityMonitor>();
|
||||
var liquidityMonitor = new LiquidityMonitor(marketLogger);
|
||||
liquidityMonitor.UpdateSpread("ES", 5000.00, 5000.25, 1000);
|
||||
var acceptable = liquidityMonitor.IsLiquidityAcceptable("ES", LiquidityScore.Poor);
|
||||
Assert.IsTrue(acceptable);
|
||||
|
||||
var orderValidator = new OrderTypeValidator(new IntegrationMockLogger<OrderTypeValidator>());
|
||||
var limit = new LimitOrderRequest("ES", OrderSide.Buy, 1, 5000m, TimeInForce.Day);
|
||||
var validation = orderValidator.ValidateLimitOrder(limit, 5001m);
|
||||
Assert.IsTrue(validation.IsValid);
|
||||
|
||||
var tracker = new ExecutionQualityTracker(new IntegrationMockLogger<ExecutionQualityTracker>());
|
||||
var t0 = DateTime.UtcNow;
|
||||
tracker.RecordExecution("ES-INT-1", 5000m, 5000.25m, t0.AddMilliseconds(10), t0.AddMilliseconds(3), t0);
|
||||
var metrics = tracker.GetExecutionMetrics("ES-INT-1");
|
||||
Assert.IsNotNull(metrics);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Flow_DuplicateDetection_PreventsSecondOrder()
|
||||
{
|
||||
var detector = new DuplicateOrderDetector(new IntegrationMockLogger<DuplicateOrderDetector>(), TimeSpan.FromSeconds(5));
|
||||
var request = new OrderRequest
|
||||
{
|
||||
Symbol = "NQ",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 2,
|
||||
TimeInForce = TimeInForce.Day,
|
||||
ClientOrderId = "INT-DUP-1"
|
||||
};
|
||||
|
||||
detector.RecordOrderIntent(request);
|
||||
var duplicate = detector.IsDuplicateOrder(request);
|
||||
|
||||
Assert.IsTrue(duplicate);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Flow_CircuitBreaker_TripsOnFailures_ThenBlocksOrders()
|
||||
{
|
||||
var breaker = new ExecutionCircuitBreaker(new IntegrationMockLogger<ExecutionCircuitBreaker>(), 3, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5), 100, 3);
|
||||
|
||||
breaker.OnFailure();
|
||||
breaker.OnFailure();
|
||||
breaker.OnFailure();
|
||||
|
||||
var state = breaker.GetState();
|
||||
Assert.AreEqual(CircuitBreakerStatus.Open, state.Status);
|
||||
Assert.IsFalse(breaker.ShouldAllowOrder());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Flow_MultiTargets_WithTrailingManager_ProvidesActions()
|
||||
{
|
||||
var targetManager = new MultiLevelTargetManager(new IntegrationMockLogger<MultiLevelTargetManager>());
|
||||
var trailingManager = new TrailingStopManager(new IntegrationMockLogger<TrailingStopManager>());
|
||||
|
||||
targetManager.SetTargets("ORD-INTEG-1", new MultiLevelTargets(8, 2, 16, 2, 32, 1));
|
||||
var targetResult = targetManager.OnTargetHit("ORD-INTEG-1", 1, 5002m);
|
||||
Assert.AreEqual(TargetAction.PartialClose, targetResult.Action);
|
||||
|
||||
var position = new OrderStatus
|
||||
{
|
||||
OrderId = "ORD-INTEG-1",
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Quantity = 5,
|
||||
FilledQuantity = 5,
|
||||
AverageFillPrice = 5000m,
|
||||
State = OrderState.Working
|
||||
};
|
||||
|
||||
trailingManager.StartTrailing("ORD-INTEG-1", position, new NT8.Core.Execution.TrailingStopConfig(8));
|
||||
var stop = trailingManager.GetCurrentStopPrice("ORD-INTEG-1");
|
||||
Assert.IsTrue(stop.HasValue);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Flow_RMultipleTargets_WithSlippageImpact_ComputesValues()
|
||||
{
|
||||
var rCalc = new RMultipleCalculator(new IntegrationMockLogger<RMultipleCalculator>());
|
||||
var slippageCalc = new SlippageCalculator();
|
||||
|
||||
var position = new NT8.Core.Common.Models.Position("ES", 2, 5000.0, 0, 0, DateTime.UtcNow);
|
||||
var rValue = rCalc.CalculateRValue(position, 4998.0, 50.0);
|
||||
Assert.IsTrue(rValue > 0.0);
|
||||
|
||||
var targets = rCalc.CreateRBasedTargets(5000.0, 4998.0, new double[] { 1.0, 2.0, 3.0 });
|
||||
Assert.IsTrue(targets.TP1Ticks > 0);
|
||||
|
||||
var slippage = slippageCalc.CalculateSlippage(OrderType.Market, 5000m, 5000.25m);
|
||||
var impact = slippageCalc.SlippageImpact(slippage, 2, 12.5m, OrderSide.Buy);
|
||||
Assert.IsTrue(impact <= 0m || impact >= 0m);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Flow_ContractRollHandler_ReturnsActiveContractAndRollPeriod()
|
||||
{
|
||||
var handler = new ContractRollHandler(new IntegrationMockLogger<ContractRollHandler>());
|
||||
|
||||
var symbol = "ES";
|
||||
var date = new DateTime(DateTime.UtcNow.Year, 3, 10);
|
||||
|
||||
var activeContract = handler.GetActiveContract(symbol, date);
|
||||
Assert.IsFalse(string.IsNullOrEmpty(activeContract));
|
||||
|
||||
var rollCheck = handler.IsRollPeriod(symbol, date);
|
||||
Assert.IsTrue(rollCheck || !rollCheck);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Flow_SessionManager_WithLiquidityMonitor_ClassifiesSession()
|
||||
{
|
||||
var sessionManager = new SessionManager(new IntegrationMockLogger<SessionManager>());
|
||||
var liquidityMonitor = new LiquidityMonitor(new IntegrationMockLogger<LiquidityMonitor>());
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var session = sessionManager.GetCurrentSession("ES", now);
|
||||
Assert.IsFalse(string.IsNullOrEmpty(session.Symbol));
|
||||
|
||||
liquidityMonitor.UpdateSpread("ES", 5000.0, 5000.25, 1200);
|
||||
var score = liquidityMonitor.CalculateLiquidityScore("ES");
|
||||
Assert.IsTrue(score == LiquidityScore.Poor || score == LiquidityScore.Fair || score == LiquidityScore.Good || score == LiquidityScore.Excellent);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Flow_CircuitBreaker_RecoversThroughHalfOpenAndReset()
|
||||
{
|
||||
var breaker = new ExecutionCircuitBreaker(new IntegrationMockLogger<ExecutionCircuitBreaker>(), 1, TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(5), 100, 3);
|
||||
breaker.OnFailure();
|
||||
Assert.AreEqual(CircuitBreakerStatus.Open, breaker.GetState().Status);
|
||||
|
||||
System.Threading.Thread.Sleep(20);
|
||||
var allowed = breaker.ShouldAllowOrder();
|
||||
Assert.IsTrue(allowed);
|
||||
|
||||
breaker.OnSuccess();
|
||||
Assert.AreEqual(CircuitBreakerStatus.Closed, breaker.GetState().Status);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Flow_DuplicateDetector_ClearOldIntents_AllowsAfterWindow()
|
||||
{
|
||||
var detector = new DuplicateOrderDetector(new IntegrationMockLogger<DuplicateOrderDetector>(), TimeSpan.FromMilliseconds(10));
|
||||
var request = new OrderRequest
|
||||
{
|
||||
Symbol = "GC",
|
||||
Side = OrderSide.Sell,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
TimeInForce = TimeInForce.Day,
|
||||
ClientOrderId = "INT-DUP-2"
|
||||
};
|
||||
|
||||
detector.RecordOrderIntent(request);
|
||||
Assert.IsTrue(detector.IsDuplicateOrder(request));
|
||||
|
||||
System.Threading.Thread.Sleep(20);
|
||||
detector.ClearOldIntents(TimeSpan.FromMilliseconds(10));
|
||||
Assert.IsFalse(detector.IsDuplicateOrder(request));
|
||||
}
|
||||
}
|
||||
|
||||
internal class IntegrationMockLogger<T> : ILogger<T>
|
||||
{
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
return new IntegrationMockDisposable();
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel,
|
||||
EventId eventId,
|
||||
TState state,
|
||||
Exception exception,
|
||||
Func<TState, Exception, string> formatter)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal class IntegrationMockDisposable : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
246
tests/NT8.Integration.Tests/Phase4IntegrationTests.cs
Normal file
246
tests/NT8.Integration.Tests/Phase4IntegrationTests.cs
Normal file
@@ -0,0 +1,246 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
using NT8.Core.Sizing;
|
||||
|
||||
namespace NT8.Integration.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Integration tests for Phase 4 intelligence flow.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class Phase4IntegrationTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void FullFlow_ConfluenceToGradeFilter_AllowsTradeInPCP()
|
||||
{
|
||||
var logger = new BasicLogger("Phase4IntegrationTests");
|
||||
var scorer = new ConfluenceScorer(logger, 100);
|
||||
var filter = new GradeFilter();
|
||||
var modeManager = new RiskModeManager(logger);
|
||||
|
||||
var intent = CreateIntent(OrderSide.Buy);
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
var factors = CreateStrongFactors();
|
||||
|
||||
var score = scorer.CalculateScore(intent, context, bar, factors);
|
||||
var mode = modeManager.GetCurrentMode();
|
||||
var allowed = filter.ShouldAcceptTrade(score.Grade, mode);
|
||||
|
||||
Assert.IsTrue(allowed);
|
||||
Assert.IsTrue(score.WeightedScore >= 0.70);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FullFlow_LowConfluence_RejectedInPCP()
|
||||
{
|
||||
var logger = new BasicLogger("Phase4IntegrationTests");
|
||||
var scorer = new ConfluenceScorer(logger, 100);
|
||||
var filter = new GradeFilter();
|
||||
|
||||
var intent = CreateIntent(OrderSide.Buy);
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
var factors = CreateWeakFactors();
|
||||
|
||||
var score = scorer.CalculateScore(intent, context, bar, factors);
|
||||
var allowed = filter.ShouldAcceptTrade(score.Grade, RiskMode.PCP);
|
||||
|
||||
Assert.IsFalse(allowed);
|
||||
Assert.AreEqual(TradeGrade.F, score.Grade);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FullFlow_ModeTransitionToHR_BlocksTrades()
|
||||
{
|
||||
var logger = new BasicLogger("Phase4IntegrationTests");
|
||||
var modeManager = new RiskModeManager(logger);
|
||||
var filter = new GradeFilter();
|
||||
|
||||
modeManager.UpdateRiskMode(-500.0, 0, 3);
|
||||
var mode = modeManager.GetCurrentMode();
|
||||
|
||||
var allowed = filter.ShouldAcceptTrade(TradeGrade.APlus, mode);
|
||||
Assert.AreEqual(RiskMode.HR, mode);
|
||||
Assert.IsFalse(allowed);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FullFlow_GradeBasedSizer_AppliesGradeAndModeMultipliers()
|
||||
{
|
||||
var logger = new BasicLogger("Phase4IntegrationTests");
|
||||
var filter = new GradeFilter();
|
||||
var gradeSizer = new GradeBasedSizer(logger, filter);
|
||||
|
||||
var baseSizer = new StubSizer(4, 400.0);
|
||||
var intent = CreateIntent(OrderSide.Buy);
|
||||
var context = CreateContext();
|
||||
var confluence = CreateScore(TradeGrade.A, 0.85);
|
||||
var config = new SizingConfig(SizingMethod.FixedDollarRisk, 1, 20, 500.0, new Dictionary<string, object>());
|
||||
var modeConfig = new RiskModeConfig(RiskMode.ECP, 1.5, TradeGrade.B, 1500.0, 4, true, new Dictionary<string, object>());
|
||||
|
||||
var result = gradeSizer.CalculateGradeBasedSize(
|
||||
intent,
|
||||
context,
|
||||
confluence,
|
||||
RiskMode.ECP,
|
||||
config,
|
||||
baseSizer,
|
||||
modeConfig);
|
||||
|
||||
// 4 * 1.25 * 1.5 = 7.5 => 7
|
||||
Assert.AreEqual(7, result.Contracts);
|
||||
Assert.IsTrue(result.Calculations.ContainsKey("combined_multiplier"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FullFlow_RegimeManager_ShouldAdjustForExtremeVolatility()
|
||||
{
|
||||
var logger = new BasicLogger("Phase4IntegrationTests");
|
||||
var vol = new VolatilityRegimeDetector(logger, 20);
|
||||
var trend = new TrendRegimeDetector(logger);
|
||||
var regimeManager = new RegimeManager(logger, vol, trend, 50, 50);
|
||||
|
||||
var bars = BuildUptrendBars(6, 5000.0, 1.0);
|
||||
for (var i = 0; i < bars.Count; i++)
|
||||
{
|
||||
regimeManager.UpdateRegime("ES", bars[i], 5000.0, 2.4, 1.0);
|
||||
}
|
||||
|
||||
var shouldAdjust = regimeManager.ShouldAdjustStrategy("ES", CreateIntent(OrderSide.Buy));
|
||||
Assert.IsTrue(shouldAdjust);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FullFlow_ConfluenceStatsAndModeState_AreAvailable()
|
||||
{
|
||||
var logger = new BasicLogger("Phase4IntegrationTests");
|
||||
var scorer = new ConfluenceScorer(logger, 10);
|
||||
var modeManager = new RiskModeManager(logger);
|
||||
|
||||
var score = scorer.CalculateScore(CreateIntent(OrderSide.Buy), CreateContext(), CreateBar(), CreateStrongFactors());
|
||||
var stats = scorer.GetHistoricalStats();
|
||||
var state = modeManager.GetState();
|
||||
|
||||
Assert.IsNotNull(score);
|
||||
Assert.IsNotNull(stats);
|
||||
Assert.IsNotNull(state);
|
||||
Assert.IsTrue(stats.TotalCalculations >= 1);
|
||||
}
|
||||
|
||||
private static StrategyIntent CreateIntent(OrderSide side)
|
||||
{
|
||||
return new StrategyIntent(
|
||||
"ES",
|
||||
side,
|
||||
OrderType.Market,
|
||||
null,
|
||||
8,
|
||||
16,
|
||||
0.8,
|
||||
"Phase4 integration",
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static StrategyContext CreateContext()
|
||||
{
|
||||
return new StrategyContext(
|
||||
"ES",
|
||||
DateTime.UtcNow,
|
||||
new Position("ES", 0, 0, 0, 0, DateTime.UtcNow),
|
||||
new AccountInfo(100000, 100000, 0, 0, DateTime.UtcNow),
|
||||
new MarketSession(DateTime.Today.AddHours(9.5), DateTime.Today.AddHours(16), true, "RTH"),
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static BarData CreateBar()
|
||||
{
|
||||
return new BarData("ES", DateTime.UtcNow, 5000, 5004, 4998, 5003, 1200, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static ConfluenceScore CreateScore(TradeGrade grade, double weighted)
|
||||
{
|
||||
var factors = new List<ConfluenceFactor>();
|
||||
factors.Add(new ConfluenceFactor(FactorType.Setup, "Setup", weighted, 1.0, "test", new Dictionary<string, object>()));
|
||||
return new ConfluenceScore(weighted, weighted, grade, factors, DateTime.UtcNow, new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static List<IFactorCalculator> CreateStrongFactors()
|
||||
{
|
||||
var factors = new List<IFactorCalculator>();
|
||||
factors.Add(new FixedFactor(FactorType.Setup, 0.90));
|
||||
factors.Add(new FixedFactor(FactorType.Trend, 0.85));
|
||||
factors.Add(new FixedFactor(FactorType.Volatility, 0.80));
|
||||
return factors;
|
||||
}
|
||||
|
||||
private static List<IFactorCalculator> CreateWeakFactors()
|
||||
{
|
||||
var factors = new List<IFactorCalculator>();
|
||||
factors.Add(new FixedFactor(FactorType.Setup, 0.20));
|
||||
factors.Add(new FixedFactor(FactorType.Trend, 0.30));
|
||||
factors.Add(new FixedFactor(FactorType.Volatility, 0.25));
|
||||
return factors;
|
||||
}
|
||||
|
||||
private static List<BarData> BuildUptrendBars(int count, double start, double step)
|
||||
{
|
||||
var list = new List<BarData>();
|
||||
var t = DateTime.UtcNow.AddMinutes(-count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var close = start + (i * step);
|
||||
list.Add(new BarData("ES", t.AddMinutes(i), close - 1.0, close + 1.0, close - 2.0, close, 1000 + i, TimeSpan.FromMinutes(1)));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private class FixedFactor : IFactorCalculator
|
||||
{
|
||||
private readonly FactorType _type;
|
||||
private readonly double _score;
|
||||
|
||||
public FixedFactor(FactorType type, double score)
|
||||
{
|
||||
_type = type;
|
||||
_score = score;
|
||||
}
|
||||
|
||||
public FactorType Type
|
||||
{
|
||||
get { return _type; }
|
||||
}
|
||||
|
||||
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
|
||||
{
|
||||
return new ConfluenceFactor(_type, "Fixed", _score, 1.0, "fixed", new Dictionary<string, object>());
|
||||
}
|
||||
}
|
||||
|
||||
private class StubSizer : IPositionSizer
|
||||
{
|
||||
private readonly int _contracts;
|
||||
private readonly double _risk;
|
||||
|
||||
public StubSizer(int contracts, double risk)
|
||||
{
|
||||
_contracts = contracts;
|
||||
_risk = risk;
|
||||
}
|
||||
|
||||
public SizingResult CalculateSize(StrategyIntent intent, StrategyContext context, SizingConfig config)
|
||||
{
|
||||
return new SizingResult(_contracts, _risk, SizingMethod.FixedDollarRisk, new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
public SizingMetadata GetMetadata()
|
||||
{
|
||||
return new SizingMetadata("Stub", "Stub", new List<string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
tests/NT8.Performance.Tests/Phase3PerformanceTests.cs
Normal file
153
tests/NT8.Performance.Tests/Phase3PerformanceTests.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Execution;
|
||||
using NT8.Core.MarketData;
|
||||
using NT8.Core.OMS;
|
||||
|
||||
namespace NT8.Performance.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class Phase3PerformanceTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void OrderTypeValidation_ShouldBeUnder2ms_Average()
|
||||
{
|
||||
var validator = new OrderTypeValidator(new PerfLogger<OrderTypeValidator>());
|
||||
var request = new LimitOrderRequest("ES", OrderSide.Buy, 1, 5000m, TimeInForce.Day);
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
var result = validator.ValidateLimitOrder(request, 5001m);
|
||||
Assert.IsTrue(result.IsValid);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / 1000.0;
|
||||
Assert.IsTrue(avgMs < 2.0, string.Format("Average validation time {0:F4}ms exceeded 2ms", avgMs));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExecutionQualityCalculation_ShouldBeUnder3ms_Average()
|
||||
{
|
||||
var tracker = new ExecutionQualityTracker(new PerfLogger<ExecutionQualityTracker>());
|
||||
var t0 = DateTime.UtcNow;
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
var orderId = string.Format("ES-PERF-{0}", i);
|
||||
tracker.RecordExecution(orderId, 5000m, 5000.25m, t0.AddMilliseconds(5), t0.AddMilliseconds(2), t0);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / 1000.0;
|
||||
Assert.IsTrue(avgMs < 3.0, string.Format("Average execution tracking time {0:F4}ms exceeded 3ms", avgMs));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LiquidityUpdate_ShouldBeUnder1ms_Average()
|
||||
{
|
||||
var monitor = new LiquidityMonitor(new PerfLogger<LiquidityMonitor>());
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < 2000; i++)
|
||||
{
|
||||
var bid = 5000.0 + (i % 10) * 0.01;
|
||||
var ask = bid + 0.25;
|
||||
monitor.UpdateSpread("ES", bid, ask, 1000 + i);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / 2000.0;
|
||||
Assert.IsTrue(avgMs < 1.0, string.Format("Average liquidity update time {0:F4}ms exceeded 1ms", avgMs));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TrailingStopUpdate_ShouldBeUnder2ms_Average()
|
||||
{
|
||||
var manager = new TrailingStopManager(new PerfLogger<TrailingStopManager>());
|
||||
var position = new OrderStatus
|
||||
{
|
||||
OrderId = "PERF-TRAIL-1",
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Quantity = 1,
|
||||
FilledQuantity = 1,
|
||||
AverageFillPrice = 5000m,
|
||||
State = OrderState.Working,
|
||||
CreatedTime = DateTime.UtcNow
|
||||
};
|
||||
|
||||
manager.StartTrailing("PERF-TRAIL-1", position, new NT8.Core.Execution.TrailingStopConfig(8));
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
manager.UpdateTrailingStop("PERF-TRAIL-1", 5000m + (i * 0.01m));
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / 1000.0;
|
||||
Assert.IsTrue(avgMs < 2.0, string.Format("Average trailing stop update time {0:F4}ms exceeded 2ms", avgMs));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OverallExecutionFlow_ShouldBeUnder15ms_Average()
|
||||
{
|
||||
var validator = new OrderTypeValidator(new PerfLogger<OrderTypeValidator>());
|
||||
var tracker = new ExecutionQualityTracker(new PerfLogger<ExecutionQualityTracker>());
|
||||
var monitor = new LiquidityMonitor(new PerfLogger<LiquidityMonitor>());
|
||||
var detector = new DuplicateOrderDetector(new PerfLogger<DuplicateOrderDetector>(), TimeSpan.FromSeconds(5));
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < 500; i++)
|
||||
{
|
||||
monitor.UpdateSpread("ES", 5000.0, 5000.25, 1000);
|
||||
var request = new LimitOrderRequest("ES", OrderSide.Buy, 1, 5000m, TimeInForce.Day);
|
||||
var valid = validator.ValidateLimitOrder(request, 5001m);
|
||||
Assert.IsTrue(valid.IsValid);
|
||||
|
||||
detector.RecordOrderIntent(request);
|
||||
|
||||
var t0 = DateTime.UtcNow;
|
||||
tracker.RecordExecution(string.Format("ES-FLOW-{0}", i), 5000m, 5000.25m, t0.AddMilliseconds(5), t0.AddMilliseconds(2), t0);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / 500.0;
|
||||
Assert.IsTrue(avgMs < 15.0, string.Format("Average end-to-end flow time {0:F4}ms exceeded 15ms", avgMs));
|
||||
}
|
||||
}
|
||||
|
||||
internal class PerfLogger<T> : Microsoft.Extensions.Logging.ILogger<T>
|
||||
{
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
return new PerfDisposable();
|
||||
}
|
||||
|
||||
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Log<TState>(
|
||||
Microsoft.Extensions.Logging.LogLevel logLevel,
|
||||
Microsoft.Extensions.Logging.EventId eventId,
|
||||
TState state,
|
||||
Exception exception,
|
||||
Func<TState, Exception, string> formatter)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal class PerfDisposable : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
227
tests/NT8.Performance.Tests/Phase4PerformanceTests.cs
Normal file
227
tests/NT8.Performance.Tests/Phase4PerformanceTests.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Performance.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class Phase4PerformanceTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void ConfluenceScoreCalculation_ShouldBeUnder5ms_Average()
|
||||
{
|
||||
var scorer = new ConfluenceScorer(new BasicLogger("Phase4PerformanceTests"), 200);
|
||||
var intent = CreateIntent(OrderSide.Buy);
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
var factors = CreateFactors(0.82, 0.76, 0.88, 0.79, 0.81);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
var score = scorer.CalculateScore(intent, context, bar, factors);
|
||||
Assert.IsTrue(score.WeightedScore >= 0.0);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / 1000.0;
|
||||
Assert.IsTrue(avgMs < 5.0, string.Format("Average confluence scoring time {0:F4}ms exceeded 5ms", avgMs));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RegimeDetection_ShouldBeUnder3ms_Average()
|
||||
{
|
||||
var volDetector = new VolatilityRegimeDetector(new BasicLogger("Phase4PerformanceTests"), 100);
|
||||
var trendDetector = new TrendRegimeDetector(new BasicLogger("Phase4PerformanceTests"));
|
||||
var bars = BuildBars(12, 5000.0, 0.75);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
var vol = volDetector.DetectRegime("ES", 1.0 + ((i % 6) * 0.2), 1.0);
|
||||
var trend = trendDetector.DetectTrend("ES", bars, 5000.0);
|
||||
Assert.IsTrue(Enum.IsDefined(typeof(VolatilityRegime), vol));
|
||||
Assert.IsTrue(Enum.IsDefined(typeof(TrendRegime), trend));
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / 1000.0;
|
||||
Assert.IsTrue(avgMs < 3.0, string.Format("Average regime detection time {0:F4}ms exceeded 3ms", avgMs));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GradeFiltering_ShouldBeUnder1ms_Average()
|
||||
{
|
||||
var filter = new GradeFilter();
|
||||
var grades = new TradeGrade[] { TradeGrade.APlus, TradeGrade.A, TradeGrade.B, TradeGrade.C, TradeGrade.D, TradeGrade.F };
|
||||
var modes = new RiskMode[] { RiskMode.ECP, RiskMode.PCP, RiskMode.DCP, RiskMode.HR };
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < 5000; i++)
|
||||
{
|
||||
var grade = grades[i % grades.Length];
|
||||
var mode = modes[i % modes.Length];
|
||||
|
||||
var accepted = filter.ShouldAcceptTrade(grade, mode);
|
||||
var multiplier = filter.GetSizeMultiplier(grade, mode);
|
||||
|
||||
if (!accepted)
|
||||
{
|
||||
Assert.IsTrue(multiplier >= 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / 5000.0;
|
||||
Assert.IsTrue(avgMs < 1.0, string.Format("Average grade filter time {0:F4}ms exceeded 1ms", avgMs));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RiskModeUpdate_ShouldBeUnder2ms_Average()
|
||||
{
|
||||
var manager = new RiskModeManager(new BasicLogger("Phase4PerformanceTests"));
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < 2000; i++)
|
||||
{
|
||||
var pnl = (i % 2 == 0) ? 300.0 : -250.0;
|
||||
var winStreak = i % 5;
|
||||
var lossStreak = i % 4;
|
||||
|
||||
manager.UpdateRiskMode(pnl, winStreak, lossStreak);
|
||||
var mode = manager.GetCurrentMode();
|
||||
Assert.IsTrue(mode >= RiskMode.HR);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / 2000.0;
|
||||
Assert.IsTrue(avgMs < 2.0, string.Format("Average risk mode update time {0:F4}ms exceeded 2ms", avgMs));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OverallIntelligenceFlow_ShouldBeUnder15ms_Average()
|
||||
{
|
||||
var scorer = new ConfluenceScorer(new BasicLogger("Phase4PerformanceTests"), 200);
|
||||
var volDetector = new VolatilityRegimeDetector(new BasicLogger("Phase4PerformanceTests"), 100);
|
||||
var trendDetector = new TrendRegimeDetector(new BasicLogger("Phase4PerformanceTests"));
|
||||
var modeManager = new RiskModeManager(new BasicLogger("Phase4PerformanceTests"));
|
||||
var filter = new GradeFilter();
|
||||
|
||||
var intent = CreateIntent(OrderSide.Buy);
|
||||
var context = CreateContext();
|
||||
var bar = CreateBar();
|
||||
var factors = CreateFactors(0.84, 0.78, 0.86, 0.75, 0.80);
|
||||
var bars = BuildBars(12, 5000.0, 0.5);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < 500; i++)
|
||||
{
|
||||
var score = scorer.CalculateScore(intent, context, bar, factors);
|
||||
var vol = volDetector.DetectRegime("ES", 1.2 + ((i % 5) * 0.15), 1.0);
|
||||
var trend = trendDetector.DetectTrend("ES", bars, 5000.0);
|
||||
|
||||
var lossStreak = (vol == VolatilityRegime.Extreme) ? 3 : (i % 3);
|
||||
modeManager.UpdateRiskMode(i % 2 == 0 ? 350.0 : -150.0, i % 4, lossStreak);
|
||||
var mode = modeManager.GetCurrentMode();
|
||||
|
||||
var allowed = filter.ShouldAcceptTrade(score.Grade, mode);
|
||||
var mult = filter.GetSizeMultiplier(score.Grade, mode);
|
||||
|
||||
Assert.IsTrue(mult >= 0.0);
|
||||
Assert.IsTrue(Enum.IsDefined(typeof(TrendRegime), trend));
|
||||
Assert.IsTrue(allowed || !allowed);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / 500.0;
|
||||
Assert.IsTrue(avgMs < 15.0, string.Format("Average intelligence flow time {0:F4}ms exceeded 15ms", avgMs));
|
||||
}
|
||||
|
||||
private static StrategyIntent CreateIntent(OrderSide side)
|
||||
{
|
||||
return new StrategyIntent(
|
||||
"ES",
|
||||
side,
|
||||
OrderType.Market,
|
||||
null,
|
||||
8,
|
||||
16,
|
||||
0.8,
|
||||
"Phase4 performance",
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static StrategyContext CreateContext()
|
||||
{
|
||||
return new StrategyContext(
|
||||
"ES",
|
||||
DateTime.UtcNow,
|
||||
new Position("ES", 0, 0, 0, 0, DateTime.UtcNow),
|
||||
new AccountInfo(100000, 100000, 0, 0, DateTime.UtcNow),
|
||||
new MarketSession(DateTime.Today.AddHours(9.5), DateTime.Today.AddHours(16), true, "RTH"),
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
private static BarData CreateBar()
|
||||
{
|
||||
return new BarData("ES", DateTime.UtcNow, 5000, 5004, 4998, 5003, 1200, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static List<IFactorCalculator> CreateFactors(double setup, double trend, double vol, double timing, double quality)
|
||||
{
|
||||
var factors = new List<IFactorCalculator>();
|
||||
factors.Add(new FixedFactor(FactorType.Setup, setup));
|
||||
factors.Add(new FixedFactor(FactorType.Trend, trend));
|
||||
factors.Add(new FixedFactor(FactorType.Volatility, vol));
|
||||
factors.Add(new FixedFactor(FactorType.Timing, timing));
|
||||
factors.Add(new FixedFactor(FactorType.ExecutionQuality, quality));
|
||||
return factors;
|
||||
}
|
||||
|
||||
private static List<BarData> BuildBars(int count, double start, double step)
|
||||
{
|
||||
var list = new List<BarData>();
|
||||
var t = DateTime.UtcNow.AddMinutes(-count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var close = start + (i * step);
|
||||
list.Add(new BarData("ES", t.AddMinutes(i), close - 1.0, close + 1.0, close - 2.0, close, 1000 + i, TimeSpan.FromMinutes(1)));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private class FixedFactor : IFactorCalculator
|
||||
{
|
||||
private readonly FactorType _type;
|
||||
private readonly double _score;
|
||||
|
||||
public FixedFactor(FactorType type, double score)
|
||||
{
|
||||
_type = type;
|
||||
_score = score;
|
||||
}
|
||||
|
||||
public FactorType Type
|
||||
{
|
||||
get { return _type; }
|
||||
}
|
||||
|
||||
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
|
||||
{
|
||||
return new ConfluenceFactor(_type, "Fixed", _score, 1.0, "fixed", new Dictionary<string, object>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user