Fix stop/target ordering, P&L wiring, dynamic targets, confluence wiring
Some checks failed
Build and Test / build (push) Has been cancelled

- 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:
2026-03-23 22:43:21 -04:00
parent ae8ac05017
commit c9e8b35f15
4 changed files with 183 additions and 16 deletions

View File

@@ -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;
}
}