using System; using System.Collections.Generic; using NT8.Core.Common.Models; using NT8.Core.Logging; namespace NT8.Core.Intelligence { /// /// Calculates one confluence factor from strategy and market context. /// public interface IFactorCalculator { /// /// Factor type produced by this calculator. /// FactorType Type { get; } /// /// Calculates the confluence factor. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Calculated confluence factor. ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar); } /// /// ORB setup quality calculator. /// public class OrbSetupFactorCalculator : IFactorCalculator { /// /// Gets the factor type. /// public FactorType Type { get { return FactorType.Setup; } } /// /// Calculates ORB setup validity score. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Setup confluence factor. 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(); 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 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; } } /// /// Trend alignment calculator. /// public class TrendAlignmentFactorCalculator : IFactorCalculator { /// /// Gets the factor type. /// public FactorType Type { get { return FactorType.Trend; } } /// /// Calculates trend alignment score. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Trend confluence factor. 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(); 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 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; } } /// /// Volatility regime suitability calculator. /// public class VolatilityRegimeFactorCalculator : IFactorCalculator { /// /// Gets the factor type. /// public FactorType Type { get { return FactorType.Volatility; } } /// /// Calculates volatility regime score. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Volatility confluence factor. 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(); 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 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; } } /// /// Session timing suitability calculator. /// public class TimeInSessionFactorCalculator : IFactorCalculator { /// /// Gets the factor type. /// public FactorType Type { get { return FactorType.Timing; } } /// /// Calculates session timing score. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Timing confluence factor. 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(); 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); } } /// /// Recent execution quality calculator. /// public class ExecutionQualityFactorCalculator : IFactorCalculator { /// /// Gets the factor type. /// public FactorType Type { get { return FactorType.ExecutionQuality; } } /// /// Calculates execution quality score from recent fills. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Execution quality factor. 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(); 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 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; } } /// /// Daily bar data passed to ORB-specific factor calculators. /// Contains a lookback window of recent daily bars in chronological order, /// oldest first, with index [Count-1] being the most recent completed day. /// public struct DailyBarContext { /// Daily high prices, oldest first. public double[] Highs; /// Daily low prices, oldest first. public double[] Lows; /// Daily close prices, oldest first. public double[] Closes; /// Daily open prices, oldest first. public double[] Opens; /// Daily volume values, oldest first. public long[] Volumes; /// Number of valid bars populated. public int Count; /// Today's RTH open price. public double TodayOpen; /// Volume of the breakout bar (current intraday bar). public double BreakoutBarVolume; /// Average intraday volume per bar for today's session so far. public double AvgIntradayBarVolume; /// Trade direction: 1 for long, -1 for short. public int TradeDirection; } /// /// Scores the setup based on narrow range day concepts. /// An NR7 (range is the narrowest of the last 7 days) scores highest, /// indicating volatility contraction and likely expansion on breakout. /// Requires at least 7 completed daily bars in DailyBarContext. /// public class NarrowRangeFactorCalculator : IFactorCalculator { private readonly ILogger _logger; /// /// Initializes a new instance of the NarrowRangeFactorCalculator class. /// /// Logger instance. public NarrowRangeFactorCalculator(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; } /// /// Gets the factor type identifier. /// public FactorType Type { get { return FactorType.NarrowRange; } } /// /// Calculates narrow range score. Expects DailyBarContext in /// intent.Metadata["daily_bars"]. Returns 0.3 if context is missing. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Calculated confluence factor. public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar) { double score = 0.3; string reason = "No daily bar context available"; if (intent != null && intent.Metadata != null && intent.Metadata.ContainsKey("daily_bars")) { DailyBarContext daily = (DailyBarContext)intent.Metadata["daily_bars"]; if (daily.Count >= 7 && daily.Highs != null && daily.Lows != null) { double todayRange = daily.Highs[daily.Count - 1] - daily.Lows[daily.Count - 1]; bool isNR4 = true; int start4 = daily.Count - 4; int end = daily.Count - 2; for (int i = start4; i <= end; i++) { double r = daily.Highs[i] - daily.Lows[i]; if (todayRange >= r) { isNR4 = false; break; } } bool isNR7 = true; int start7 = daily.Count - 7; for (int i = start7; i <= end; i++) { double r = daily.Highs[i] - daily.Lows[i]; if (todayRange >= r) { isNR7 = false; break; } } if (isNR7) { score = 1.0; reason = "NR7: Narrowest range in 7 days — strong volatility contraction"; } else if (isNR4) { score = 0.75; reason = "NR4: Narrowest range in 4 days — moderate volatility contraction"; } else { double sumRanges = 0.0; int lookback = Math.Min(7, daily.Count - 1); int start = daily.Count - 1 - lookback; int finish = daily.Count - 2; for (int i = start; i <= finish; i++) sumRanges += daily.Highs[i] - daily.Lows[i]; double avgRange = lookback > 0 ? sumRanges / lookback : todayRange; double ratio = avgRange > 0.0 ? todayRange / avgRange : 1.0; if (ratio <= 0.7) { score = 0.6; reason = "Range below 70% of avg — mild contraction"; } else if (ratio <= 0.9) { score = 0.45; reason = "Range near avg — no significant contraction"; } else { score = 0.2; reason = "Range above avg — expansion day, low NR score"; } } } else { reason = String.Format("Insufficient daily bars: {0} of 7 required", daily.Count); } } return new ConfluenceFactor( FactorType.NarrowRange, "Narrow Range (NR4/NR7)", score, 0.20, reason, new Dictionary()); } } /// /// Scores the ORB range relative to average daily range. /// Prevents trading when the ORB has already consumed most of the /// day's expected range, leaving little room for continuation. /// public class OrbRangeVsAtrFactorCalculator : IFactorCalculator { private readonly ILogger _logger; /// /// Initializes a new instance of the OrbRangeVsAtrFactorCalculator class. /// /// Logger instance. public OrbRangeVsAtrFactorCalculator(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; } /// /// Gets the factor type identifier. /// public FactorType Type { get { return FactorType.OrbRangeVsAtr; } } /// /// Calculates ORB range vs ATR score. Expects DailyBarContext in /// intent.Metadata["daily_bars"] and double in intent.Metadata["orb_range_ticks"]. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Calculated confluence factor. public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar) { double score = 0.5; string reason = "No daily bar context available"; if (intent != null && intent.Metadata != null && intent.Metadata.ContainsKey("daily_bars") && intent.Metadata.ContainsKey("orb_range_ticks")) { DailyBarContext daily = (DailyBarContext)intent.Metadata["daily_bars"]; double orbRangeTicks = ToDouble(intent.Metadata["orb_range_ticks"], 0.0); if (daily.Count >= 5 && daily.Highs != null && daily.Lows != null) { double sumAtr = 0.0; int lookback = Math.Min(10, daily.Count - 1); int start = daily.Count - 1 - lookback; int end = daily.Count - 2; for (int i = start; i <= end; i++) sumAtr += daily.Highs[i] - daily.Lows[i]; double avgDailyRange = lookback > 0 ? sumAtr / lookback : 0.0; double orbRangePoints = orbRangeTicks / 4.0; double ratio = avgDailyRange > 0.0 ? orbRangePoints / avgDailyRange : 0.5; if (ratio <= 0.20) { score = 1.0; reason = String.Format("ORB is {0:P0} of daily ATR — tight range, high expansion potential", ratio); } else if (ratio <= 0.35) { score = 0.80; reason = String.Format("ORB is {0:P0} of daily ATR — good room to run", ratio); } else if (ratio <= 0.50) { score = 0.60; reason = String.Format("ORB is {0:P0} of daily ATR — moderate room remaining", ratio); } else if (ratio <= 0.70) { score = 0.35; reason = String.Format("ORB is {0:P0} of daily ATR — limited room, caution", ratio); } else { score = 0.10; reason = String.Format("ORB is {0:P0} of daily ATR — range nearly exhausted", ratio); } } } return new ConfluenceFactor( FactorType.OrbRangeVsAtr, "ORB Range vs ATR", score, 0.15, reason, new Dictionary()); } private static double ToDouble(object value, double defaultValue) { if (value == null) return defaultValue; 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; } } /// /// Scores alignment between today's overnight gap direction and the /// trade direction. A gap-and-go setup (gap up + long trade) scores /// highest. A gap fade setup penalizes the score. /// public class GapDirectionAlignmentCalculator : IFactorCalculator { private readonly ILogger _logger; /// /// Initializes a new instance of the GapDirectionAlignmentCalculator class. /// /// Logger instance. public GapDirectionAlignmentCalculator(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; } /// /// Gets the factor type identifier. /// public FactorType Type { get { return FactorType.GapDirectionAlignment; } } /// /// Calculates gap alignment score. Expects DailyBarContext in /// intent.Metadata["daily_bars"] with TodayOpen and TradeDirection populated. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Calculated confluence factor. public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar) { double score = 0.5; string reason = "No daily bar context available"; if (intent != null && intent.Metadata != null && intent.Metadata.ContainsKey("daily_bars")) { DailyBarContext daily = (DailyBarContext)intent.Metadata["daily_bars"]; if (daily.Count >= 2 && daily.Closes != null) { double prevClose = daily.Closes[daily.Count - 2]; double todayOpen = daily.TodayOpen; double gapPoints = todayOpen - prevClose; int gapDirection = gapPoints > 0.25 ? 1 : (gapPoints < -0.25 ? -1 : 0); int tradeDir = daily.TradeDirection; if (gapDirection == 0) { score = 0.55; reason = "Flat open — no gap bias, neutral score"; } else if (gapDirection == tradeDir) { double gapSize = Math.Abs(gapPoints); if (gapSize >= 5.0) { score = 1.0; reason = String.Format("Large gap {0:+0.00;-0.00} aligns with trade — strong gap-and-go", gapPoints); } else if (gapSize >= 2.0) { score = 0.85; reason = String.Format("Moderate gap {0:+0.00;-0.00} aligns with trade", gapPoints); } else { score = 0.65; reason = String.Format("Small gap {0:+0.00;-0.00} aligns with trade", gapPoints); } } else { double gapSize = Math.Abs(gapPoints); if (gapSize >= 5.0) { score = 0.10; reason = String.Format("Large gap {0:+0.00;-0.00} opposes trade — high fade risk", gapPoints); } else if (gapSize >= 2.0) { score = 0.25; reason = String.Format("Moderate gap {0:+0.00;-0.00} opposes trade", gapPoints); } else { score = 0.40; reason = String.Format("Small gap {0:+0.00;-0.00} opposes trade — minor headwind", gapPoints); } } } } return new ConfluenceFactor( FactorType.GapDirectionAlignment, "Gap Direction Alignment", score, 0.15, reason, new Dictionary()); } } /// /// Scores the volume of the breakout bar relative to the average /// volume of bars seen so far in today's session. /// A volume surge on the breakout bar strongly confirms the move. /// public class BreakoutVolumeStrengthCalculator : IFactorCalculator { private readonly ILogger _logger; /// /// Initializes a new instance of the BreakoutVolumeStrengthCalculator class. /// /// Logger instance. public BreakoutVolumeStrengthCalculator(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; } /// /// Gets the factor type identifier. /// public FactorType Type { get { return FactorType.BreakoutVolumeStrength; } } /// /// Calculates breakout volume score. Expects DailyBarContext in /// intent.Metadata["daily_bars"] with BreakoutBarVolume and /// AvgIntradayBarVolume populated. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Calculated confluence factor. public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar) { double score = 0.4; string reason = "No daily bar context available"; if (intent != null && intent.Metadata != null && intent.Metadata.ContainsKey("daily_bars")) { DailyBarContext daily = (DailyBarContext)intent.Metadata["daily_bars"]; double breakoutVol = daily.BreakoutBarVolume; double avgVol = daily.AvgIntradayBarVolume; if (avgVol > 0.0) { double ratio = breakoutVol / avgVol; if (ratio >= 3.0) { score = 1.0; reason = String.Format("Breakout volume {0:F1}x avg — exceptional surge", ratio); } else if (ratio >= 2.0) { score = 0.85; reason = String.Format("Breakout volume {0:F1}x avg — strong confirmation", ratio); } else if (ratio >= 1.5) { score = 0.70; reason = String.Format("Breakout volume {0:F1}x avg — solid confirmation", ratio); } else if (ratio >= 1.0) { score = 0.50; reason = String.Format("Breakout volume {0:F1}x avg — average, neutral", ratio); } else if (ratio >= 0.7) { score = 0.25; reason = String.Format("Breakout volume {0:F1}x avg — below avg, low conviction", ratio); } else { score = 0.10; reason = String.Format("Breakout volume {0:F1}x avg — weak breakout, high false-break risk", ratio); } } else { reason = "Avg intraday volume not available"; } } return new ConfluenceFactor( FactorType.BreakoutVolumeStrength, "Breakout Volume Strength", score, 0.20, reason, new Dictionary()); } } /// /// Scores where the prior day closed within its own range. /// A strong prior close (top 25% for longs, bottom 25% for shorts) /// indicates momentum continuation into today's session. /// public class PriorDayCloseStrengthCalculator : IFactorCalculator { private readonly ILogger _logger; /// /// Initializes a new instance of the PriorDayCloseStrengthCalculator class. /// /// Logger instance. public PriorDayCloseStrengthCalculator(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; } /// /// Gets the factor type identifier. /// public FactorType Type { get { return FactorType.PriorDayCloseStrength; } } /// /// Calculates prior close strength score. Expects DailyBarContext in /// intent.Metadata["daily_bars"] with at least 2 completed bars and /// TradeDirection populated. /// /// Current strategy intent. /// Current strategy context. /// Current bar data. /// Calculated confluence factor. public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar) { double score = 0.5; string reason = "No daily bar context available"; if (intent != null && intent.Metadata != null && intent.Metadata.ContainsKey("daily_bars")) { DailyBarContext daily = (DailyBarContext)intent.Metadata["daily_bars"]; if (daily.Count >= 2 && daily.Highs != null && daily.Lows != null && daily.Closes != null) { int prev = daily.Count - 2; double prevHigh = daily.Highs[prev]; double prevLow = daily.Lows[prev]; double prevClose = daily.Closes[prev]; double prevRange = prevHigh - prevLow; int tradeDir = daily.TradeDirection; if (prevRange > 0.0) { double closePosition = (prevClose - prevLow) / prevRange; if (tradeDir == 1) { if (closePosition >= 0.75) { score = 1.0; reason = String.Format("Prior close in top {0:P0} — strong bullish close", 1.0 - closePosition); } else if (closePosition >= 0.50) { score = 0.70; reason = "Prior close in upper half — moderate bullish bias"; } else if (closePosition >= 0.25) { score = 0.40; reason = "Prior close in lower half — weak prior close for long"; } else { score = 0.15; reason = "Prior close near low — bearish close, headwind for long"; } } else { if (closePosition <= 0.25) { score = 1.0; reason = String.Format("Prior close in bottom {0:P0} — strong bearish close", closePosition); } else if (closePosition <= 0.50) { score = 0.70; reason = "Prior close in lower half — moderate bearish bias"; } else if (closePosition <= 0.75) { score = 0.40; reason = "Prior close in upper half — weak prior close for short"; } else { score = 0.15; reason = "Prior close near high — bullish close, headwind for short"; } } } else { reason = "Prior day range is zero — cannot score"; } } } return new ConfluenceFactor( FactorType.PriorDayCloseStrength, "Prior Day Close Strength", score, 0.15, reason, new Dictionary()); } } }