1024 lines
38 KiB
C#
1024 lines
38 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using NT8.Core.Common.Models;
|
|
using NT8.Core.Logging;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public struct DailyBarContext
|
|
{
|
|
/// <summary>Daily high prices, oldest first.</summary>
|
|
public double[] Highs;
|
|
|
|
/// <summary>Daily low prices, oldest first.</summary>
|
|
public double[] Lows;
|
|
|
|
/// <summary>Daily close prices, oldest first.</summary>
|
|
public double[] Closes;
|
|
|
|
/// <summary>Daily open prices, oldest first.</summary>
|
|
public double[] Opens;
|
|
|
|
/// <summary>Daily volume values, oldest first.</summary>
|
|
public long[] Volumes;
|
|
|
|
/// <summary>Number of valid bars populated.</summary>
|
|
public int Count;
|
|
|
|
/// <summary>Today's RTH open price.</summary>
|
|
public double TodayOpen;
|
|
|
|
/// <summary>Volume of the breakout bar (current intraday bar).</summary>
|
|
public double BreakoutBarVolume;
|
|
|
|
/// <summary>Average intraday volume per bar for today's session so far.</summary>
|
|
public double AvgIntradayBarVolume;
|
|
|
|
/// <summary>Trade direction: 1 for long, -1 for short.</summary>
|
|
public int TradeDirection;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class NarrowRangeFactorCalculator : IFactorCalculator
|
|
{
|
|
private readonly ILogger _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the NarrowRangeFactorCalculator class.
|
|
/// </summary>
|
|
/// <param name="logger">Logger instance.</param>
|
|
public NarrowRangeFactorCalculator(ILogger logger)
|
|
{
|
|
if (logger == null)
|
|
throw new ArgumentNullException("logger");
|
|
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the factor type identifier.
|
|
/// </summary>
|
|
public FactorType Type
|
|
{
|
|
get { return FactorType.NarrowRange; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates narrow range score. Expects DailyBarContext in
|
|
/// intent.Metadata["daily_bars"]. Returns 0.3 if context is missing.
|
|
/// </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>
|
|
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<string, object>());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class OrbRangeVsAtrFactorCalculator : IFactorCalculator
|
|
{
|
|
private readonly ILogger _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the OrbRangeVsAtrFactorCalculator class.
|
|
/// </summary>
|
|
/// <param name="logger">Logger instance.</param>
|
|
public OrbRangeVsAtrFactorCalculator(ILogger logger)
|
|
{
|
|
if (logger == null)
|
|
throw new ArgumentNullException("logger");
|
|
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the factor type identifier.
|
|
/// </summary>
|
|
public FactorType Type
|
|
{
|
|
get { return FactorType.OrbRangeVsAtr; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates ORB range vs ATR score. Expects DailyBarContext in
|
|
/// intent.Metadata["daily_bars"] and double in intent.Metadata["orb_range_ticks"].
|
|
/// </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>
|
|
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<string, object>());
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class GapDirectionAlignmentCalculator : IFactorCalculator
|
|
{
|
|
private readonly ILogger _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the GapDirectionAlignmentCalculator class.
|
|
/// </summary>
|
|
/// <param name="logger">Logger instance.</param>
|
|
public GapDirectionAlignmentCalculator(ILogger logger)
|
|
{
|
|
if (logger == null)
|
|
throw new ArgumentNullException("logger");
|
|
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the factor type identifier.
|
|
/// </summary>
|
|
public FactorType Type
|
|
{
|
|
get { return FactorType.GapDirectionAlignment; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates gap alignment score. Expects DailyBarContext in
|
|
/// intent.Metadata["daily_bars"] with TodayOpen and TradeDirection populated.
|
|
/// </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>
|
|
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<string, object>());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class BreakoutVolumeStrengthCalculator : IFactorCalculator
|
|
{
|
|
private readonly ILogger _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the BreakoutVolumeStrengthCalculator class.
|
|
/// </summary>
|
|
/// <param name="logger">Logger instance.</param>
|
|
public BreakoutVolumeStrengthCalculator(ILogger logger)
|
|
{
|
|
if (logger == null)
|
|
throw new ArgumentNullException("logger");
|
|
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the factor type identifier.
|
|
/// </summary>
|
|
public FactorType Type
|
|
{
|
|
get { return FactorType.BreakoutVolumeStrength; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates breakout volume score. Expects DailyBarContext in
|
|
/// intent.Metadata["daily_bars"] with BreakoutBarVolume and
|
|
/// AvgIntradayBarVolume populated.
|
|
/// </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>
|
|
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<string, object>());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class PriorDayCloseStrengthCalculator : IFactorCalculator
|
|
{
|
|
private readonly ILogger _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the PriorDayCloseStrengthCalculator class.
|
|
/// </summary>
|
|
/// <param name="logger">Logger instance.</param>
|
|
public PriorDayCloseStrengthCalculator(ILogger logger)
|
|
{
|
|
if (logger == null)
|
|
throw new ArgumentNullException("logger");
|
|
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the factor type identifier.
|
|
/// </summary>
|
|
public FactorType Type
|
|
{
|
|
get { return FactorType.PriorDayCloseStrength; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates prior close strength score. Expects DailyBarContext in
|
|
/// intent.Metadata["daily_bars"] with at least 2 completed bars and
|
|
/// TradeDirection populated.
|
|
/// </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>
|
|
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<string, object>());
|
|
}
|
|
}
|
|
}
|