diff --git a/src/NT8.Adapters/Strategies/NT8StrategyBase.cs b/src/NT8.Adapters/Strategies/NT8StrategyBase.cs index 120bd4e..68f60b5 100644 --- a/src/NT8.Adapters/Strategies/NT8StrategyBase.cs +++ b/src/NT8.Adapters/Strategies/NT8StrategyBase.cs @@ -426,6 +426,46 @@ namespace NinjaTrader.NinjaScript.Strategies _executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time); } + protected override void OnPositionUpdate( + Position position, + double averagePrice, + int quantity, + MarketPosition marketPosition) + { + if (!_sdkInitialized || _riskManager == null) + return; + + try + { + double dayPnL = 0.0; + if (Account != null) + { + try + { + dayPnL = Account.Get(AccountItem.GainLoss, Currency.UsDollar); + } + catch + { + dayPnL = 0.0; + } + } + + _riskManager.OnPnLUpdate(dayPnL, dayPnL); + PortfolioRiskManager.Instance.ReportPnL(Name, dayPnL); + + if (EnableVerboseLogging) + Print(string.Format("[SDK] P&L update: DayPnL={0:C} Position={1} Qty={2}", + dayPnL, marketPosition, quantity)); + + FileLog(string.Format("PNL_UPDATE DayPnL={0:C} Position={1} Qty={2}", + dayPnL, marketPosition, quantity)); + } + catch (Exception ex) + { + Print(string.Format("[SDK] OnPositionUpdate error: {0}", ex.Message)); + } + } + /// /// Handles broker connection status changes. Halts new orders on disconnect, /// logs reconnect, and resets the connection flag when restored. @@ -571,7 +611,7 @@ namespace NinjaTrader.NinjaScript.Strategies _riskConfig, _sizingConfig); - _riskManager = new BasicRiskManager(_logger); + _riskManager = new BasicRiskManager(_logger, DailyLossLimit); _positionSizer = new BasicPositionSizer(_logger); _circuitBreaker = new ExecutionCircuitBreaker( _logger, @@ -842,15 +882,25 @@ namespace NinjaTrader.NinjaScript.Strategies FileLog(string.Format("SIGNAL {0} | Grade={1} | Score={2}", intent.Side, grade, score)); if (!string.IsNullOrEmpty(factors)) FileLog(string.Format(" Factors: {0}", factors)); - FileLog(string.Format("SUBMIT {0} {1} @ Market | Stop={2} Target={3}", + FileLog(string.Format("SUBMIT {0} {1} @ Market | Stop={2} Target={3}{4}", intent.Side, request.Quantity, intent.StopTicks, - intent.TargetTicks)); + intent.TargetTicks, + intent.Metadata != null && intent.Metadata.ContainsKey("dynamic_target_ticks") ? " [dynamic]" : "")); } _executionAdapter.SubmitOrder(request, orderName); + // Register stop and target BEFORE submitting the entry order. + // NT8 requires stop/target to be pre-registered to the signal name + // so they are applied correctly in both backtest and live/SIM modes. + if (intent.StopTicks > 0) + SetStopLoss(orderName, CalculationMode.Ticks, (int)intent.StopTicks, false); + + if (intent.TargetTicks.HasValue && intent.TargetTicks.Value > 0) + SetProfitTarget(orderName, CalculationMode.Ticks, (int)intent.TargetTicks.Value); + if (request.Side == OmsOrderSide.Buy) { if (request.Type == OmsOrderType.Market) @@ -870,12 +920,6 @@ namespace NinjaTrader.NinjaScript.Strategies EnterShortStopMarket(request.Quantity, (double)request.StopPrice.Value, orderName); } - if (intent.StopTicks > 0) - SetStopLoss(orderName, CalculationMode.Ticks, (int)intent.StopTicks, false); - - if (intent.TargetTicks.HasValue && intent.TargetTicks.Value > 0) - SetProfitTarget(orderName, CalculationMode.Ticks, (int)intent.TargetTicks.Value); - if (_circuitBreaker != null) _circuitBreaker.OnSuccess(); } diff --git a/src/NT8.Core/Intelligence/ConfluenceScorer.cs b/src/NT8.Core/Intelligence/ConfluenceScorer.cs index 4a3357a..fbb0ed0 100644 --- a/src/NT8.Core/Intelligence/ConfluenceScorer.cs +++ b/src/NT8.Core/Intelligence/ConfluenceScorer.cs @@ -110,6 +110,11 @@ namespace NT8.Core.Intelligence _factorWeights.Add(FactorType.Volatility, 1.0); _factorWeights.Add(FactorType.Timing, 1.0); _factorWeights.Add(FactorType.ExecutionQuality, 1.0); + _factorWeights.Add(FactorType.NarrowRange, 1.0); + _factorWeights.Add(FactorType.OrbRangeVsAtr, 1.0); + _factorWeights.Add(FactorType.GapDirectionAlignment, 1.0); + _factorWeights.Add(FactorType.BreakoutVolumeStrength, 1.0); + _factorWeights.Add(FactorType.PriorDayCloseStrength, 1.0); } /// diff --git a/src/NT8.Core/Risk/BasicRiskManager.cs b/src/NT8.Core/Risk/BasicRiskManager.cs index 2a232da..1e8f793 100644 --- a/src/NT8.Core/Risk/BasicRiskManager.cs +++ b/src/NT8.Core/Risk/BasicRiskManager.cs @@ -20,6 +20,7 @@ namespace NT8.Core.Risk private double _dailyPnL; private double _maxDrawdown; private bool _tradingHalted; + private double _configuredDailyLossLimit; private DateTime _lastUpdate = DateTime.UtcNow; private readonly Dictionary _symbolExposure = new Dictionary(); @@ -27,6 +28,15 @@ namespace NT8.Core.Risk { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; + _configuredDailyLossLimit = 1000.0; + } + + public BasicRiskManager(ILogger logger, double dailyLossLimit) + { + if (logger == null) throw new ArgumentNullException("logger"); + if (dailyLossLimit <= 0.0) throw new ArgumentException("dailyLossLimit must be positive", "dailyLossLimit"); + _logger = logger; + _configuredDailyLossLimit = dailyLossLimit; } public RiskDecision ValidateOrder(StrategyIntent intent, StrategyContext context, RiskConfig config) @@ -216,10 +226,8 @@ namespace NT8.Core.Risk private void CheckEmergencyConditions(double dayPnL) { - // Emergency halt if daily loss exceeds 90% of limit - // Using a default limit of 1000 as this method doesn't have access to config - // In Phase 1, this should be improved to use the actual config value - if (dayPnL <= -(1000 * 0.9) && !_tradingHalted) + // Emergency halt if daily loss exceeds 90% of configured limit + if (dayPnL <= -(_configuredDailyLossLimit * 0.9) && !_tradingHalted) { _tradingHalted = true; _logger.LogCritical("Emergency halt triggered at 90% of daily loss limit: {0:C}", dayPnL); diff --git a/src/NT8.Strategies/Examples/SimpleORBStrategy.cs b/src/NT8.Strategies/Examples/SimpleORBStrategy.cs index 88e00a5..5d1469d 100644 --- a/src/NT8.Strategies/Examples/SimpleORBStrategy.cs +++ b/src/NT8.Strategies/Examples/SimpleORBStrategy.cs @@ -169,6 +169,9 @@ namespace NT8.Strategies.Examples if (isValidRthSessionStart && thisTradingDate != _currentSessionDate) { ResetSession(thisSessionStart); + + if (context.CustomData != null && context.CustomData.ContainsKey("session_open_price")) + context.CustomData.Remove("session_open_price"); } // Only trade during RTH @@ -221,10 +224,33 @@ namespace NT8.Strategies.Examples AttachDailyBarContext(candidate, bar, context); + // Hard veto: Trend against trade direction is a disqualifying condition. + // AVWAP trend alignment is checked before confluence scoring to prevent + // high scores from other factors masking a directionally broken setup. + if (_config != null && _config.Parameters != null) + { + if (context.CustomData != null && context.CustomData.ContainsKey("avwap_slope")) + { + var slope = context.CustomData["avwap_slope"]; + if (slope is double) + { + double slopeVal = (double)slope; + bool longTrade = candidate.Side == OrderSide.Buy; + bool trendAgainst = (longTrade && slopeVal < 0) || (!longTrade && slopeVal > 0); + if (trendAgainst) + { + if (_logger != null) + _logger.LogInformation("Trade vetoed: AVWAP slope {0:F4} against {1} direction", slopeVal, candidate.Side); + return null; + } + } + } + } + var score = _scorer.CalculateScore(candidate, context, bar, _factorCalculators); var mode = _riskModeManager.GetCurrentMode(); - int minGradeValue = 4; + int minGradeValue = 5; if (_config != null && _config.Parameters != null && _config.Parameters.ContainsKey("MinTradeGrade")) { var mgv = _config.Parameters["MinTradeGrade"]; @@ -260,6 +286,15 @@ namespace NT8.Strategies.Examples _tradeTaken = true; + if (_logger != null && score.Factors != null) + { + System.Text.StringBuilder sb = new System.Text.StringBuilder(); + sb.Append("Factors: "); + foreach (ConfluenceFactor f in score.Factors) + sb.Append(string.Format("{0}={1:F2}({2}) ", f.Type, f.Score, f.Weight.ToString("F2"))); + _logger.LogInformation("Confluence detail: {0}", sb.ToString().TrimEnd()); + } + _logger.LogInformation( "SimpleORBStrategy accepted intent for {0}: Side={1}, Grade={2}, Mode={3}, Score={4:F3}, Mult={5:F2}", candidate.Symbol, @@ -380,6 +415,13 @@ namespace NT8.Strategies.Examples context.CustomData["normal_atr"] = Math.Max(0.01, normalAtr); context.CustomData["recent_execution_quality"] = 0.8; context.CustomData["avg_volume"] = avgVol; + + // Track the first bar open of the RTH session as the session open price. + // Only set once per session (when session_open_price is not yet in custom data). + if (!context.CustomData.ContainsKey("session_open_price")) + { + context.CustomData["session_open_price"] = bar.Open; + } } private void ResetSession(DateTime sessionStart) @@ -414,7 +456,7 @@ namespace NT8.Strategies.Examples var stopTicks = _config != null && _config.Parameters.ContainsKey("StopTicks") ? (int)_config.Parameters["StopTicks"] : 8; - var targetTicks = _config != null && _config.Parameters.ContainsKey("TargetTicks") + int baseTargetTicks = _config != null && _config.Parameters.ContainsKey("TargetTicks") ? (int)_config.Parameters["TargetTicks"] : 16; @@ -438,12 +480,71 @@ namespace NT8.Strategies.Examples if (tickSize <= 0.0) tickSize = 0.25; + // Scale target dynamically based on ORB range vs average daily range. + // Tight ORBs (< 20% of daily ATR) leave the most room — extend target. + // Wide ORBs (> 50% of daily ATR) have consumed range — keep target tight. + // Requires daily bar context with at least 5 bars; falls back to base target otherwise. + int targetTicks = baseTargetTicks; + if (_config != null && _config.Parameters != null && _config.Parameters.ContainsKey("daily_bars")) + { + var dailySrc = _config.Parameters["daily_bars"]; + if (dailySrc is DailyBarContext) + { + DailyBarContext dc = (DailyBarContext)dailySrc; + if (dc.Count >= 5 && dc.Highs != null && dc.Lows != null && tickSize > 0.0) + { + double sumAtr = 0.0; + int lookback = Math.Min(10, dc.Count - 1); + int start = dc.Count - 1 - lookback; + int end = dc.Count - 2; + for (int i = start; i <= end; i++) + sumAtr += dc.Highs[i] - dc.Lows[i]; + + double avgDailyRange = lookback > 0 ? sumAtr / lookback : 0.0; + double orbRangePoints = openingRange; + double ratio = avgDailyRange > 0.0 ? orbRangePoints / avgDailyRange : 0.5; + + // Ratio tiers map to target multipliers: + // <= 0.20 (tight ORB) → 1.75x (28 ticks on 16-tick base) + // <= 0.30 → 1.50x (24 ticks) + // <= 0.45 → 1.25x (20 ticks) + // <= 0.60 → 1.00x (16 ticks — base, no change) + // > 0.60 (wide ORB) → 0.75x (12 ticks — tighten) + double multiplier; + if (ratio <= 0.20) + multiplier = 1.75; + else if (ratio <= 0.30) + multiplier = 1.50; + else if (ratio <= 0.45) + multiplier = 1.25; + else if (ratio <= 0.60) + multiplier = 1.00; + else + multiplier = 0.75; + + targetTicks = (int)Math.Round(baseTargetTicks * multiplier); + + // Enforce hard floor of stopTicks + 4 (minimum 1:1 R plus 4 ticks) + int minTarget = stopTicks + 4; + if (targetTicks < minTarget) + targetTicks = minTarget; + } + } + } + + if (_logger != null && targetTicks != baseTargetTicks) + _logger.LogInformation( + "Dynamic target: base={0} adjusted={1} (ORB/ATR scaling)", + baseTargetTicks, targetTicks); + var orbRangeTicks = openingRange / tickSize; metadata.Add("orb_range_ticks", orbRangeTicks); metadata.Add("trigger_price", lastPrice); metadata.Add("multiplier", _stdDevMultiplier); metadata.Add("opening_range_start", _openingRangeStart); metadata.Add("opening_range_end", _openingRangeEnd); + metadata.Add("base_target_ticks", baseTargetTicks); + metadata.Add("dynamic_target_ticks", targetTicks); return new StrategyIntent( symbol, @@ -474,7 +575,15 @@ namespace NT8.Strategies.Examples daily.TradeDirection = intent.Side == OrderSide.Buy ? 1 : -1; daily.BreakoutBarVolume = (double)bar.Volume; - daily.TodayOpen = bar.Open; + + double todayOpen = bar.Open; + if (context != null && context.CustomData != null && context.CustomData.ContainsKey("session_open_price")) + { + object sop = context.CustomData["session_open_price"]; + if (sop is double) + todayOpen = (double)sop; + } + daily.TodayOpen = todayOpen; if (context != null && context.CustomData != null && context.CustomData.ContainsKey("avg_volume")) { @@ -489,6 +598,7 @@ namespace NT8.Strategies.Examples daily.AvgIntradayBarVolume = (double)(long)avg; } + // orb_range_ticks is set in CreateIntent() and preserved here. intent.Metadata["daily_bars"] = daily; } }