643 lines
28 KiB
C#
643 lines
28 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using NT8.Core.Common.Interfaces;
|
|
using NT8.Core.Common.Models;
|
|
using NT8.Core.Indicators;
|
|
using NT8.Core.Intelligence;
|
|
using NT8.Core.Logging;
|
|
|
|
namespace NT8.Strategies.Examples
|
|
{
|
|
/// <summary>
|
|
/// Opening Range Breakout strategy with Phase 4 confluence and grade-aware intent metadata.
|
|
/// </summary>
|
|
public class SimpleORBStrategy : IStrategy
|
|
{
|
|
private readonly object _lock = new object();
|
|
|
|
private readonly int _openingRangeMinutes;
|
|
private readonly double _stdDevMultiplier;
|
|
|
|
private ILogger _logger;
|
|
private StrategyConfig _config;
|
|
private ConfluenceScorer _scorer;
|
|
private GradeFilter _gradeFilter;
|
|
private RiskModeManager _riskModeManager;
|
|
private AVWAPCalculator _avwapCalculator;
|
|
private VolumeProfileAnalyzer _volumeProfileAnalyzer;
|
|
private List<IFactorCalculator> _factorCalculators;
|
|
|
|
private DateTime _currentSessionDate;
|
|
private DateTime _openingRangeStart;
|
|
private DateTime _openingRangeEnd;
|
|
private double _openingRangeHigh;
|
|
private double _openingRangeLow;
|
|
private bool _openingRangeReady;
|
|
private bool _tradeTaken;
|
|
private int _consecutiveWins;
|
|
private int _consecutiveLosses;
|
|
|
|
/// <summary>
|
|
/// Gets strategy metadata.
|
|
/// </summary>
|
|
public StrategyMetadata Metadata { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Creates a new strategy with default ORB configuration.
|
|
/// </summary>
|
|
public SimpleORBStrategy()
|
|
: this(30, 1.0)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new strategy with custom ORB configuration.
|
|
/// </summary>
|
|
/// <param name="openingRangeMinutes">Opening range period in minutes.</param>
|
|
/// <param name="stdDevMultiplier">Breakout volatility multiplier.</param>
|
|
public SimpleORBStrategy(int openingRangeMinutes, double stdDevMultiplier)
|
|
{
|
|
if (openingRangeMinutes <= 0)
|
|
throw new ArgumentException("openingRangeMinutes must be greater than zero", "openingRangeMinutes");
|
|
|
|
if (stdDevMultiplier <= 0.0)
|
|
throw new ArgumentException("stdDevMultiplier must be greater than zero", "stdDevMultiplier");
|
|
|
|
_openingRangeMinutes = openingRangeMinutes;
|
|
_stdDevMultiplier = stdDevMultiplier;
|
|
|
|
_currentSessionDate = DateTime.MinValue;
|
|
_openingRangeStart = DateTime.MinValue;
|
|
_openingRangeEnd = DateTime.MinValue;
|
|
_openingRangeHigh = Double.MinValue;
|
|
_openingRangeLow = Double.MaxValue;
|
|
_openingRangeReady = false;
|
|
_tradeTaken = false;
|
|
_consecutiveWins = 0;
|
|
_consecutiveLosses = 0;
|
|
|
|
Metadata = new StrategyMetadata(
|
|
"Simple ORB",
|
|
"Opening Range Breakout strategy with confluence scoring",
|
|
"2.0",
|
|
"NT8 SDK Team",
|
|
new string[] { "ES", "NQ", "YM" },
|
|
20);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes strategy dependencies.
|
|
/// </summary>
|
|
/// <param name="config">Strategy configuration.</param>
|
|
/// <param name="dataProvider">Market data provider.</param>
|
|
/// <param name="logger">Logger instance.</param>
|
|
public void Initialize(StrategyConfig config, IMarketDataProvider dataProvider, ILogger logger)
|
|
{
|
|
if (logger == null)
|
|
throw new ArgumentNullException("logger");
|
|
|
|
try
|
|
{
|
|
_logger = logger;
|
|
_config = config;
|
|
_scorer = new ConfluenceScorer(_logger, 500);
|
|
_gradeFilter = new GradeFilter();
|
|
_riskModeManager = new RiskModeManager(_logger);
|
|
_avwapCalculator = new AVWAPCalculator(AVWAPAnchorMode.Day, DateTime.UtcNow);
|
|
_volumeProfileAnalyzer = new VolumeProfileAnalyzer();
|
|
|
|
_factorCalculators = new List<IFactorCalculator>();
|
|
_factorCalculators.Add(new OrbSetupFactorCalculator());
|
|
_factorCalculators.Add(new TrendAlignmentFactorCalculator());
|
|
_factorCalculators.Add(new VolatilityRegimeFactorCalculator());
|
|
_factorCalculators.Add(new TimeInSessionFactorCalculator());
|
|
_factorCalculators.Add(new ExecutionQualityFactorCalculator());
|
|
_factorCalculators.Add(new NarrowRangeFactorCalculator(_logger));
|
|
_factorCalculators.Add(new OrbRangeVsAtrFactorCalculator(_logger));
|
|
_factorCalculators.Add(new GapDirectionAlignmentCalculator(_logger));
|
|
_factorCalculators.Add(new BreakoutVolumeStrengthCalculator(_logger));
|
|
_factorCalculators.Add(new PriorDayCloseStrengthCalculator(_logger));
|
|
|
|
_logger.LogInformation(
|
|
"SimpleORBStrategy initialized with OR period {0} minutes and multiplier {1:F2}",
|
|
_openingRangeMinutes,
|
|
_stdDevMultiplier);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_logger != null)
|
|
_logger.LogError("SimpleORBStrategy Initialize failed: {0}", ex.Message);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes bar data and returns a trade intent when breakout and confluence criteria pass.
|
|
/// </summary>
|
|
/// <param name="bar">Current bar.</param>
|
|
/// <param name="context">Strategy context.</param>
|
|
/// <returns>Trade intent when signal is accepted; otherwise null.</returns>
|
|
public StrategyIntent OnBar(BarData bar, StrategyContext context)
|
|
{
|
|
if (bar == null)
|
|
throw new ArgumentNullException("bar");
|
|
if (context == null)
|
|
throw new ArgumentNullException("context");
|
|
|
|
try
|
|
{
|
|
lock (_lock)
|
|
{
|
|
EnsureInitialized();
|
|
|
|
UpdateRiskMode(context);
|
|
UpdateConfluenceInputs(bar, context);
|
|
|
|
DateTime thisSessionStart = context.Session != null
|
|
? context.Session.SessionStart
|
|
: context.CurrentTime.Date.AddHours(9.5);
|
|
|
|
// Guard: only reset when the TRADING DATE has changed, not just the session
|
|
// start timestamp. Using trading date prevents mid-session resets caused by
|
|
// _openingRangeStart holding a stale value from a previous day.
|
|
DateTime thisTradingDate = thisSessionStart.Date;
|
|
|
|
TimeSpan sessionStartTime = thisSessionStart.TimeOfDay;
|
|
bool isValidRthSessionStart = sessionStartTime >= new TimeSpan(8, 0, 0)
|
|
&& sessionStartTime <= new TimeSpan(10, 30, 0);
|
|
|
|
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
|
|
if (context.Session == null || !context.Session.IsRth)
|
|
return null;
|
|
|
|
if (bar.Time <= _openingRangeEnd)
|
|
{
|
|
UpdateOpeningRange(bar);
|
|
return null;
|
|
}
|
|
|
|
if (!_openingRangeReady)
|
|
{
|
|
if (_openingRangeHigh > _openingRangeLow)
|
|
_openingRangeReady = true;
|
|
else
|
|
return null;
|
|
}
|
|
|
|
if (_logger != null && _openingRangeReady)
|
|
{
|
|
_logger.LogDebug(
|
|
"ORB ready: High={0:F2} Low={1:F2} Range={2:F2} TradeTaken={3}",
|
|
_openingRangeHigh,
|
|
_openingRangeLow,
|
|
_openingRangeHigh - _openingRangeLow,
|
|
_tradeTaken);
|
|
}
|
|
|
|
if (_tradeTaken)
|
|
{
|
|
if (_logger != null)
|
|
_logger.LogDebug(
|
|
"SimpleORBStrategy skip: trade already taken for session {0:yyyy-MM-dd}; bar={1:yyyy-MM-dd HH:mm}; symbol={2}",
|
|
_currentSessionDate,
|
|
bar.Time,
|
|
context.Symbol);
|
|
return null;
|
|
}
|
|
|
|
var openingRange = _openingRangeHigh - _openingRangeLow;
|
|
var volatilityBuffer = openingRange * (_stdDevMultiplier - 1.0);
|
|
if (volatilityBuffer < 0.0)
|
|
volatilityBuffer = 0.0;
|
|
|
|
var longTrigger = _openingRangeHigh + volatilityBuffer;
|
|
var shortTrigger = _openingRangeLow - volatilityBuffer;
|
|
|
|
StrategyIntent candidate = null;
|
|
if (bar.Close > longTrigger)
|
|
candidate = CreateIntent(context.Symbol, OrderSide.Buy, openingRange, bar.Close);
|
|
else if (bar.Close < shortTrigger)
|
|
candidate = CreateIntent(context.Symbol, OrderSide.Sell, openingRange, bar.Close);
|
|
|
|
if (candidate == null)
|
|
return null;
|
|
|
|
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 = 5;
|
|
if (_config != null && _config.Parameters != null && _config.Parameters.ContainsKey("MinTradeGrade"))
|
|
{
|
|
var mgv = _config.Parameters["MinTradeGrade"];
|
|
if (mgv is int)
|
|
minGradeValue = (int)mgv;
|
|
}
|
|
|
|
TradeGrade minGrade = (TradeGrade)minGradeValue;
|
|
if ((int)score.Grade < (int)minGrade)
|
|
{
|
|
if (_logger != null)
|
|
_logger.LogInformation(
|
|
"SimpleORBStrategy filtered by grade: Score={0:F3} Grade={1} MinGrade={2}",
|
|
score.WeightedScore,
|
|
score.Grade,
|
|
minGrade);
|
|
return null;
|
|
}
|
|
|
|
var gradeMultiplier = _gradeFilter.GetSizeMultiplier(score.Grade, mode);
|
|
var modeConfig = _riskModeManager.GetModeConfig(mode);
|
|
var combinedMultiplier = gradeMultiplier * modeConfig.SizeMultiplier;
|
|
|
|
candidate.Confidence = score.WeightedScore;
|
|
candidate.Reason = string.Format("{0}; grade={1}; mode={2}", candidate.Reason, score.Grade, mode);
|
|
candidate.Metadata["confluence_score"] = score;
|
|
candidate.Metadata["confluence_weighted_score"] = score.WeightedScore;
|
|
candidate.Metadata["trade_grade"] = score.Grade.ToString();
|
|
candidate.Metadata["risk_mode"] = mode.ToString();
|
|
candidate.Metadata["grade_multiplier"] = gradeMultiplier;
|
|
candidate.Metadata["mode_multiplier"] = modeConfig.SizeMultiplier;
|
|
candidate.Metadata["combined_multiplier"] = combinedMultiplier;
|
|
|
|
_tradeTaken = true;
|
|
|
|
if (_logger != null)
|
|
_logger.LogDebug(
|
|
"SimpleORBStrategy flag set: tradeTaken={0} session={1:yyyy-MM-dd}; bar={2:yyyy-MM-dd HH:mm}; side={3}; symbol={4}",
|
|
_tradeTaken,
|
|
_currentSessionDate,
|
|
bar.Time,
|
|
candidate.Side,
|
|
candidate.Symbol);
|
|
|
|
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,
|
|
candidate.Side,
|
|
score.Grade,
|
|
mode,
|
|
score.WeightedScore,
|
|
combinedMultiplier);
|
|
|
|
return candidate;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_logger != null)
|
|
_logger.LogError("SimpleORBStrategy OnBar failed: {0}", ex.Message);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes tick data. This strategy does not use tick-level logic.
|
|
/// </summary>
|
|
/// <param name="tick">Tick data.</param>
|
|
/// <param name="context">Strategy context.</param>
|
|
/// <returns>Always null for this strategy.</returns>
|
|
public StrategyIntent OnTick(TickData tick, StrategyContext context)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns current strategy parameters.
|
|
/// </summary>
|
|
/// <returns>Parameter map.</returns>
|
|
public Dictionary<string, object> GetParameters()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var parameters = new Dictionary<string, object>();
|
|
parameters.Add("opening_range_minutes", _openingRangeMinutes);
|
|
parameters.Add("std_dev_multiplier", _stdDevMultiplier);
|
|
return parameters;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates strategy parameters.
|
|
/// </summary>
|
|
/// <param name="parameters">Parameter map.</param>
|
|
public void SetParameters(Dictionary<string, object> parameters)
|
|
{
|
|
if (parameters == null)
|
|
return;
|
|
|
|
// force_session_reset: clear _tradeTaken and ORB state so a fresh live session
|
|
// can trade even if historical replay set _tradeTaken before going realtime.
|
|
if (parameters.ContainsKey("force_session_reset"))
|
|
{
|
|
var val = parameters["force_session_reset"];
|
|
if (val is bool && (bool)val)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_tradeTaken = false;
|
|
_openingRangeReady = false;
|
|
_openingRangeHigh = Double.MinValue;
|
|
_openingRangeLow = Double.MaxValue;
|
|
if (_logger != null)
|
|
_logger.LogInformation("ForceSessionReset: _tradeTaken cleared, ORB state reset for live session");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void EnsureInitialized()
|
|
{
|
|
if (_logger == null)
|
|
throw new InvalidOperationException("Strategy must be initialized before OnBar processing");
|
|
if (_scorer == null || _gradeFilter == null || _riskModeManager == null)
|
|
throw new InvalidOperationException("Intelligence components are not initialized");
|
|
}
|
|
|
|
private void UpdateRiskMode(StrategyContext context)
|
|
{
|
|
var dailyPnl = 0.0;
|
|
if (context.Account != null)
|
|
dailyPnl = context.Account.DailyPnL;
|
|
|
|
_riskModeManager.UpdateRiskMode(dailyPnl, _consecutiveWins, _consecutiveLosses);
|
|
}
|
|
|
|
private void UpdateConfluenceInputs(BarData bar, StrategyContext context)
|
|
{
|
|
_avwapCalculator.Update(bar.Close, bar.Volume);
|
|
var avwap = _avwapCalculator.GetCurrentValue();
|
|
var avwapSlope = _avwapCalculator.GetSlope(10);
|
|
|
|
if (context.CustomData == null)
|
|
context.CustomData = new Dictionary<string, object>();
|
|
|
|
// Use pre-calculated intraday average from daily bar context when available.
|
|
// Fall back to current bar volume only if daily context is not yet populated.
|
|
double avgVol = (double)bar.Volume;
|
|
double normalAtr = bar.High - bar.Low;
|
|
|
|
if (_config != null && _config.Parameters != null && _config.Parameters.ContainsKey("daily_bars"))
|
|
{
|
|
var src = _config.Parameters["daily_bars"];
|
|
if (src is DailyBarContext)
|
|
{
|
|
DailyBarContext dc = (DailyBarContext)src;
|
|
|
|
if (dc.AvgIntradayBarVolume > 0.0)
|
|
avgVol = dc.AvgIntradayBarVolume;
|
|
|
|
// Use 10-day average daily range as the volatility baseline.
|
|
if (dc.Count >= 5 && dc.Highs != null && dc.Lows != null)
|
|
{
|
|
double sumRanges = 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++)
|
|
sumRanges += dc.Highs[i] - dc.Lows[i];
|
|
|
|
if (lookback > 0)
|
|
normalAtr = sumRanges / lookback;
|
|
}
|
|
}
|
|
}
|
|
|
|
context.CustomData["current_bar"] = bar;
|
|
context.CustomData["avwap"] = avwap;
|
|
context.CustomData["avwap_slope"] = avwapSlope;
|
|
context.CustomData["trend_confirm"] = avwapSlope > 0.0 ? 1.0 : 0.0;
|
|
context.CustomData["current_atr"] = Math.Max(0.01, bar.High - bar.Low);
|
|
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)
|
|
{
|
|
_currentSessionDate = sessionStart.Date;
|
|
_openingRangeStart = sessionStart;
|
|
_openingRangeEnd = sessionStart.AddMinutes(_openingRangeMinutes);
|
|
_openingRangeHigh = Double.MinValue;
|
|
_openingRangeLow = Double.MaxValue;
|
|
_openingRangeReady = false;
|
|
_tradeTaken = false;
|
|
|
|
if (_logger != null)
|
|
_logger.LogInformation(
|
|
"Session reset: Date={0:yyyy-MM-dd} ORB window={1:HH:mm}-{2:HH:mm}",
|
|
_currentSessionDate,
|
|
_openingRangeStart,
|
|
_openingRangeEnd);
|
|
}
|
|
|
|
private void UpdateOpeningRange(BarData bar)
|
|
{
|
|
if (bar.High > _openingRangeHigh)
|
|
_openingRangeHigh = bar.High;
|
|
|
|
if (bar.Low < _openingRangeLow)
|
|
_openingRangeLow = bar.Low;
|
|
}
|
|
|
|
private StrategyIntent CreateIntent(string symbol, OrderSide side, double openingRange, double lastPrice)
|
|
{
|
|
var stopTicks = _config != null && _config.Parameters.ContainsKey("StopTicks")
|
|
? (int)_config.Parameters["StopTicks"]
|
|
: 8;
|
|
int baseTargetTicks = _config != null && _config.Parameters.ContainsKey("TargetTicks")
|
|
? (int)_config.Parameters["TargetTicks"]
|
|
: 16;
|
|
|
|
var metadata = new Dictionary<string, object>();
|
|
metadata.Add("orb_high", _openingRangeHigh);
|
|
metadata.Add("orb_low", _openingRangeLow);
|
|
metadata.Add("orb_range", openingRange);
|
|
|
|
double tickSize = 0.25;
|
|
if (_config != null && _config.Parameters != null && _config.Parameters.ContainsKey("TickSize"))
|
|
{
|
|
var tickValue = _config.Parameters["TickSize"];
|
|
if (tickValue is double)
|
|
tickSize = (double)tickValue;
|
|
else if (tickValue is decimal)
|
|
tickSize = (double)(decimal)tickValue;
|
|
else if (tickValue is float)
|
|
tickSize = (double)(float)tickValue;
|
|
}
|
|
|
|
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,
|
|
side,
|
|
OrderType.Market,
|
|
null,
|
|
stopTicks,
|
|
targetTicks,
|
|
0.75,
|
|
"ORB breakout signal",
|
|
metadata);
|
|
}
|
|
|
|
private void AttachDailyBarContext(StrategyIntent intent, BarData bar, StrategyContext context)
|
|
{
|
|
if (intent == null || intent.Metadata == null)
|
|
return;
|
|
|
|
if (_config == null || _config.Parameters == null || !_config.Parameters.ContainsKey("daily_bars"))
|
|
return;
|
|
|
|
var source = _config.Parameters["daily_bars"];
|
|
if (!(source is DailyBarContext))
|
|
return;
|
|
|
|
DailyBarContext baseContext = (DailyBarContext)source;
|
|
DailyBarContext daily = baseContext;
|
|
|
|
daily.TradeDirection = intent.Side == OrderSide.Buy ? 1 : -1;
|
|
daily.BreakoutBarVolume = (double)bar.Volume;
|
|
|
|
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"))
|
|
{
|
|
var avg = context.CustomData["avg_volume"];
|
|
if (avg is double)
|
|
daily.AvgIntradayBarVolume = (double)avg;
|
|
else if (avg is float)
|
|
daily.AvgIntradayBarVolume = (double)(float)avg;
|
|
else if (avg is int)
|
|
daily.AvgIntradayBarVolume = (double)(int)avg;
|
|
else if (avg is long)
|
|
daily.AvgIntradayBarVolume = (double)(long)avg;
|
|
}
|
|
|
|
// orb_range_ticks is set in CreateIntent() and preserved here.
|
|
intent.Metadata["daily_bars"] = daily;
|
|
}
|
|
}
|
|
}
|