Fix stop/target ordering, P&L wiring, dynamic targets, confluence wiring
- Move SetStopLoss/SetProfitTarget before EnterLong/EnterShort in SubmitOrderToNT8(). NT8 requires stop/target pre-registered to the signal name before entry fires — calling after caused silent fallback to strategy defaults in backtest, nullifying all dynamic target scaling. Add [dynamic] tag to SUBMIT log line for observability. - Wire OnPositionUpdate() in NT8StrategyBase to call _riskManager.OnPnLUpdate() and PortfolioRiskManager.ReportPnL() on every position change. Daily loss limit checks were evaluating against PnL permanently — safety net now live. - Fix BasicRiskManager.CheckEmergencyConditions() hardcoded 1000 limit. Add dailyLossLimit constructor overload; emergency halt now fires at 90% of the configured DailyLossLimit, not a hardcoded . - Add AVWAP slope hard veto in SimpleORBStrategy.OnBar(). Trend=0.00 (AVWAP working against trade direction) can no longer pass grade filter. Pre-scoring veto prevents high gap/volume scores from masking a directionally broken setup. - Raise default MinTradeGrade from 4 (B) to 5 (A) in both SimpleORBStrategy and SimpleORBNT8 defaults. - Scale TargetTicks dynamically in CreateIntent() based on ORB/ATR ratio (0.75x-1.75x of base target). Tight ORBs get extended targets; wide ORBs get tightened. Hard floor at stopTicks+4. - Add NarrowRange, OrbRangeVsAtr, GapDirectionAlignment, BreakoutVolumeStrength, PriorDayCloseStrength default weights in ConfluenceScorer constructor. All 10 factors now registered. - Fix session_open_price tracking for GapDirectionAlignment factor. TodayOpen in DailyBarContext now reflects first RTH bar open, not the breakout bar open. - Log all 10 factor scores at signal acceptance for observability.
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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<string, double> _symbolExposure = new Dictionary<string, double>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user