diff --git a/cleanup-repo.ps1 b/cleanup-repo.ps1 new file mode 100644 index 0000000..20c808b --- /dev/null +++ b/cleanup-repo.ps1 @@ -0,0 +1,130 @@ +# cleanup-repo.ps1 +# Removes stale, superseded, and AI-process artifacts from the repo root +# Run from: C:\dev\nt8-sdk + +Set-Location "C:\dev\nt8-sdk" + +$filesToDelete = @( + # Archon planning docs (tool was never used) + "archon_task_mapping.md", + "archon_update_plan.md", + + # AI team/agent process docs (internal scaffolding, no ongoing value) + "ai_agent_tasks.md", + "ai_success_metrics.md", + "AI_DEVELOPMENT_GUIDELINES.md", + "AI_TEAM_SETUP_DOCUMENTATION.md", + "ai_workflow_templates.md", + + # Phase A/B/C planning docs (all phases complete, superseded by PROJECT_HANDOVER) + "PHASE_A_READY_FOR_KILOCODE.md", + "PHASE_A_SPECIFICATION.md", + "PHASE_B_SPECIFICATION.md", + "PHASE_C_SPECIFICATION.md", + "PHASES_ABC_COMPLETION_REPORT.md", + + # Old TASK- files superseded by TASK_ files (better versions exist) + "TASK-01-kill-switch.md", + "TASK-02-circuit-breaker.md", + "TASK-03-trailing-stop.md", + "TASK-04-log-level.md", + "TASK-05-session-holidays.md", + + # Fix specs already applied to codebase + "COMPILE_FIX_SPECIFICATION.md", + "DROPDOWN_FIX_SPECIFICATION.md", + "STRATEGY_DROPDOWN_COMPLETE_FIX.md", + + # One-time historical docs + "NET_FRAMEWORK_CONVERSION.md", + "FIX_GIT_AUTH.md", + "GIT_COMMIT_INSTRUCTIONS.md", + + # Superseded implementation docs + "implementation_guide.md", + "implementation_guide_summary.md", + "implementation_attention_points.md", + "OMS_IMPLEMENTATION_START.md", + "nt8_sdk_phase0_completion.md", + "NT8_INTEGRATION_COMPLETE_SPECS.md", + "nt8_integration_guidelines.md", + "POST_INTEGRATION_ROADMAP.md", + + # Superseded project planning (PROJECT_HANDOVER.md is canonical now) + "project_plan.md", + "project_summary.md", + "architecture_summary.md", + "development_workflow.md", + + # Kilocode setup (already done, no ongoing value) + "KILOCODE_SETUP_COMPLETE.md", + "setup-kilocode-files.ps1", + + # Utility scripts (one-time use) + "commit-now.ps1", + "cleanup-repo.ps1" # self-delete at end +) + +$dirsToDelete = @( + "plans", # single stale analysis report + "Specs" # original spec packages, all implemented +) + +Write-Host "`n=== NT8-SDK Repository Cleanup ===" -ForegroundColor Cyan +Write-Host "Removing stale and superseded files...`n" + +$deleted = 0 +$notFound = 0 + +foreach ($file in $filesToDelete) { + $path = Join-Path (Get-Location) $file + if (Test-Path $path) { + Remove-Item $path -Force + Write-Host " DELETED: $file" -ForegroundColor Green + $deleted++ + } else { + Write-Host " SKIP (not found): $file" -ForegroundColor DarkGray + $notFound++ + } +} + +foreach ($dir in $dirsToDelete) { + $path = Join-Path (Get-Location) $dir + if (Test-Path $path) { + Remove-Item $path -Recurse -Force + Write-Host " DELETED DIR: $dir\" -ForegroundColor Green + $deleted++ + } else { + Write-Host " SKIP DIR (not found): $dir\" -ForegroundColor DarkGray + $notFound++ + } +} + +Write-Host "`n=== Staging changes ===" -ForegroundColor Cyan +git add -A + +Write-Host "`n=== Committing ===" -ForegroundColor Cyan +git commit -m "chore: repo housekeeping - remove stale and superseded files + +Removed categories: +- Archon planning docs (tool never used) +- AI team/agent scaffolding docs +- Phase A/B/C specs (complete, superseded by PROJECT_HANDOVER) +- Old TASK-0x files (superseded by TASK_0x versions) +- Applied fix specs (COMPILE, DROPDOWN, STRATEGY_DROPDOWN) +- One-time historical docs (NET_FRAMEWORK_CONVERSION, FIX_GIT_AUTH) +- Superseded implementation guides and planning docs +- plans/ and Specs/ directories (all implemented) + +Kept: +- All active TASK_0x work items (TASK_01/02/03 execution wiring) +- PROJECT_HANDOVER, NEXT_STEPS_RECOMMENDED, GAP_ANALYSIS +- Phase3/4/5 Implementation Guides +- KILOCODE_RUNBOOK, OPTIMIZATION_GUIDE +- All spec files for pending work (RTH, CONFIG_EXPORT, DIAGNOSTIC_LOGGING) +- src/, tests/, docs/, deployment/, rules/, .kilocode/ unchanged" + +Write-Host "`nDeleted: $deleted items" -ForegroundColor Green +Write-Host "Skipped: $notFound items (already gone)" -ForegroundColor DarkGray +Write-Host "`n=== Done! Current root files: ===" -ForegroundColor Cyan +Get-ChildItem -File | Where-Object { $_.Name -notlike ".*" } | Select-Object Name | Format-Table -HideTableHeaders diff --git a/src/NT8.Adapters/NinjaTrader/INT8ExecutionBridge.cs b/src/NT8.Adapters/NinjaTrader/INT8ExecutionBridge.cs new file mode 100644 index 0000000..906932b --- /dev/null +++ b/src/NT8.Adapters/NinjaTrader/INT8ExecutionBridge.cs @@ -0,0 +1,26 @@ +using System; + +namespace NT8.Adapters.NinjaTrader +{ + /// + /// Provides NT8OrderAdapter access to NinjaScript execution methods. + /// Implemented by NT8StrategyBase. + /// + public interface INT8ExecutionBridge + { + /// Submit a long entry with stop and target. + void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize); + + /// Submit a short entry with stop and target. + void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize); + + /// Exit all long positions. + void ExitLongManaged(string signalName); + + /// Exit all short positions. + void ExitShortManaged(string signalName); + + /// Flatten the full position immediately. + void FlattenAll(); + } +} diff --git a/src/NT8.Adapters/NinjaTrader/NT8Adapter.cs b/src/NT8.Adapters/NinjaTrader/NT8Adapter.cs index e139867..4d93297 100644 --- a/src/NT8.Adapters/NinjaTrader/NT8Adapter.cs +++ b/src/NT8.Adapters/NinjaTrader/NT8Adapter.cs @@ -27,11 +27,34 @@ namespace NT8.Adapters.NinjaTrader public NT8Adapter() { _dataAdapter = new NT8DataAdapter(); - _orderAdapter = new NT8OrderAdapter(); + _orderAdapter = new NT8OrderAdapter(new NullExecutionBridge()); _loggingAdapter = new NT8LoggingAdapter(); _executionHistory = new List(); } + private class NullExecutionBridge : INT8ExecutionBridge + { + public void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize) + { + } + + public void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize) + { + } + + public void ExitLongManaged(string signalName) + { + } + + public void ExitShortManaged(string signalName) + { + } + + public void FlattenAll() + { + } + } + /// /// Initialize the adapter with required components /// diff --git a/src/NT8.Adapters/NinjaTrader/NT8OrderAdapter.cs b/src/NT8.Adapters/NinjaTrader/NT8OrderAdapter.cs index 0fe20e2..efdd915 100644 --- a/src/NT8.Adapters/NinjaTrader/NT8OrderAdapter.cs +++ b/src/NT8.Adapters/NinjaTrader/NT8OrderAdapter.cs @@ -12,6 +12,7 @@ namespace NT8.Adapters.NinjaTrader public class NT8OrderAdapter { private readonly object _lock = new object(); + private readonly INT8ExecutionBridge _bridge; private IRiskManager _riskManager; private IPositionSizer _positionSizer; private readonly List _executionHistory; @@ -19,8 +20,11 @@ namespace NT8.Adapters.NinjaTrader /// /// Constructor for NT8OrderAdapter. /// - public NT8OrderAdapter() + public NT8OrderAdapter(INT8ExecutionBridge bridge) { + if (bridge == null) + throw new ArgumentNullException("bridge"); + _bridge = bridge; _executionHistory = new List(); } @@ -127,18 +131,30 @@ namespace NT8.Adapters.NinjaTrader private void ExecuteInNT8(StrategyIntent intent, SizingResult sizing) { if (intent == null) - { throw new ArgumentNullException("intent"); - } - if (sizing == null) - { throw new ArgumentNullException("sizing"); - } - // This is where the actual NT8 order execution would happen - // In a real implementation, this would call NT8's EnterLong/EnterShort methods - // along with SetStopLoss, SetProfitTarget, etc. + var signalName = string.Format("SDK_{0}_{1}", intent.Symbol, intent.Side); + + if (intent.Side == OrderSide.Buy) + { + _bridge.EnterLongManaged( + sizing.Contracts, + signalName, + intent.StopTicks, + intent.TargetTicks.HasValue ? intent.TargetTicks.Value : 0, + 0.25); + } + else if (intent.Side == OrderSide.Sell) + { + _bridge.EnterShortManaged( + sizing.Contracts, + signalName, + intent.StopTicks, + intent.TargetTicks.HasValue ? intent.TargetTicks.Value : 0, + 0.25); + } lock (_lock) { @@ -151,28 +167,6 @@ namespace NT8.Adapters.NinjaTrader intent.TargetTicks, DateTime.UtcNow)); } - - // Example of what this might look like in NT8: - /* - if (intent.Side == OrderSide.Buy) - { - EnterLong(sizing.Contracts, "SDK_Entry"); - SetStopLoss("SDK_Entry", CalculationMode.Ticks, intent.StopTicks); - if (intent.TargetTicks.HasValue) - { - SetProfitTarget("SDK_Entry", CalculationMode.Ticks, intent.TargetTicks.Value); - } - } - else if (intent.Side == OrderSide.Sell) - { - EnterShort(sizing.Contracts, "SDK_Entry"); - SetStopLoss("SDK_Entry", CalculationMode.Ticks, intent.StopTicks); - if (intent.TargetTicks.HasValue) - { - SetProfitTarget("SDK_Entry", CalculationMode.Ticks, intent.TargetTicks.Value); - } - } - */ } /// diff --git a/src/NT8.Adapters/Strategies/NT8StrategyBase.cs b/src/NT8.Adapters/Strategies/NT8StrategyBase.cs index b5ed2b3..388dbca 100644 --- a/src/NT8.Adapters/Strategies/NT8StrategyBase.cs +++ b/src/NT8.Adapters/Strategies/NT8StrategyBase.cs @@ -32,7 +32,7 @@ namespace NinjaTrader.NinjaScript.Strategies /// /// Base class for strategies that integrate NT8 SDK components. /// - public abstract class NT8StrategyBase : Strategy + public abstract class NT8StrategyBase : Strategy, INT8ExecutionBridge { private readonly object _lock = new object(); @@ -54,6 +54,8 @@ namespace NinjaTrader.NinjaScript.Strategies private DateTime _lastBarTime; private bool _killSwitchTriggered; private ExecutionCircuitBreaker _circuitBreaker; + private System.IO.StreamWriter _fileLog; + private readonly object _fileLock = new object(); #region User-Configurable Properties @@ -93,8 +95,59 @@ namespace NinjaTrader.NinjaScript.Strategies [Display(Name = "Verbose Logging", GroupName = "Debug", Order = 1)] public bool EnableVerboseLogging { get; set; } + [NinjaScriptProperty] + [Display(Name = "Enable File Logging", GroupName = "Diagnostics", Order = 10)] + public bool EnableFileLogging { get; set; } + + [NinjaScriptProperty] + [Display(Name = "Log Directory", GroupName = "Diagnostics", Order = 11)] + public string LogDirectory { get; set; } + + [NinjaScriptProperty] + [Display(Name = "Enable Long Trades", GroupName = "Trade Direction", Order = 1)] + public bool EnableLongTrades { get; set; } + + [NinjaScriptProperty] + [Display(Name = "Enable Short Trades", GroupName = "Trade Direction", Order = 2)] + public bool EnableShortTrades { get; set; } + #endregion + // INT8ExecutionBridge implementation + public void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize) + { + if (stopTicks > 0) + SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false); + if (targetTicks > 0) + SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks); + EnterLong(quantity, signalName); + } + + public void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize) + { + if (stopTicks > 0) + SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false); + if (targetTicks > 0) + SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks); + EnterShort(quantity, signalName); + } + + public void ExitLongManaged(string signalName) + { + ExitLong(signalName); + } + + public void ExitShortManaged(string signalName) + { + ExitShort(signalName); + } + + public void FlattenAll() + { + ExitLong("EmergencyFlatten"); + ExitShort("EmergencyFlatten"); + } + /// /// Create the SDK strategy instance. /// @@ -136,6 +189,10 @@ namespace NinjaTrader.NinjaScript.Strategies MaxContracts = 10; EnableKillSwitch = false; EnableVerboseLogging = false; + EnableFileLogging = true; + LogDirectory = string.Empty; + EnableLongTrades = true; + EnableShortTrades = true; _killSwitchTriggered = false; } else if (State == State.DataLoaded) @@ -156,11 +213,35 @@ namespace NinjaTrader.NinjaScript.Strategies } } } + else if (State == State.Realtime) + { + InitFileLog(); + WriteSessionHeader(); + } + else if (State == State.Terminated) + { + WriteSessionFooter(); + } } protected override void OnBarUpdate() { - // Kill switch check — must be first + if (!_sdkInitialized || _sdkStrategy == null) + { + return; + } + + if (CurrentBar < BarsRequiredToTrade) + { + return; + } + + if (Time[0] == _lastBarTime) + return; + + _lastBarTime = Time[0]; + + // Kill switch — checked AFTER bar guards so ExitLong/ExitShort are valid if (EnableKillSwitch) { if (!_killSwitchTriggered) @@ -177,29 +258,9 @@ namespace NinjaTrader.NinjaScript.Strategies Print(string.Format("[SDK] Kill switch flatten error: {0}", ex.Message)); } } - return; } - if (!_sdkInitialized || _sdkStrategy == null) - { - if (CurrentBar == 0) - Print(string.Format("[SDK] Not initialized: sdkInit={0}, strategy={1}", _sdkInitialized, _sdkStrategy != null)); - return; - } - - if (CurrentBar < BarsRequiredToTrade) - { - if (CurrentBar == 0) - Print(string.Format("[SDK] Waiting for bars: current={0}, required={1}", CurrentBar, BarsRequiredToTrade)); - return; - } - - if (Time[0] == _lastBarTime) - return; - - _lastBarTime = Time[0]; - // Log first processable bar and every 100th bar. if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0) { @@ -290,9 +351,98 @@ namespace NinjaTrader.NinjaScript.Strategies if (string.IsNullOrEmpty(execution.Order.Name) || !execution.Order.Name.StartsWith("SDK_")) return; + FileLog(string.Format("FILL {0} {1} @ {2:F2} | OrderId={3}", + execution.MarketPosition, + execution.Quantity, + execution.Price, + execution.OrderId)); + _executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time); } + private void InitFileLog() + { + if (!EnableFileLogging) + return; + + try + { + string dir = string.IsNullOrEmpty(LogDirectory) + ? System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + "NinjaTrader 8", "log", "nt8-sdk") + : LogDirectory; + + System.IO.Directory.CreateDirectory(dir); + + string path = System.IO.Path.Combine( + dir, + string.Format("session_{0}.log", DateTime.Now.ToString("yyyyMMdd_HHmmss"))); + + _fileLog = new System.IO.StreamWriter(path, false); + _fileLog.AutoFlush = true; + Print(string.Format("[NT8-SDK] File log started: {0}", path)); + } + catch (Exception ex) + { + Print(string.Format("[NT8-SDK] Failed to open file log: {0}", ex.Message)); + } + } + + private void FileLog(string message) + { + if (_fileLog == null) + return; + + lock (_fileLock) + { + try + { + _fileLog.WriteLine(string.Format("[{0:HH:mm:ss.fff}] {1}", DateTime.Now, message)); + } + catch + { + } + } + } + + private void WriteSessionHeader() + { + FileLog("=== SESSION START " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ==="); + FileLog(string.Format("Strategy : {0}", Name)); + FileLog(string.Format("Account : {0}", Account != null ? Account.Name : "N/A")); + FileLog(string.Format("Symbol : {0}", Instrument != null ? Instrument.FullName : "N/A")); + FileLog(string.Format("Risk : DailyLimit=${0} MaxTradeRisk=${1} RiskPerTrade=${2}", + DailyLossLimit, + MaxTradeRisk, + RiskPerTrade)); + FileLog(string.Format("Sizing : MinContracts={0} MaxContracts={1}", MinContracts, MaxContracts)); + FileLog(string.Format("VerboseLog : {0} FileLog: {1}", EnableVerboseLogging, EnableFileLogging)); + FileLog("---"); + } + + private void WriteSessionFooter() + { + FileLog("---"); + FileLog("=== SESSION END " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ==="); + + if (_fileLog != null) + { + lock (_fileLock) + { + try + { + _fileLog.Close(); + } + catch + { + } + + _fileLog = null; + } + } + } + private void InitializeSdkComponents() { _logger = new BasicLogger(Name); @@ -354,6 +504,17 @@ namespace NinjaTrader.NinjaScript.Strategies private StrategyContext BuildStrategyContext() { + DateTime etTime; + try + { + var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); + etTime = TimeZoneInfo.ConvertTime(Time[0], easternZone); + } + catch + { + etTime = Time[0]; + } + var customData = new Dictionary(); customData.Add("CurrentBar", CurrentBar); customData.Add("BarsRequiredToTrade", BarsRequiredToTrade); @@ -361,7 +522,7 @@ namespace NinjaTrader.NinjaScript.Strategies return NT8DataConverter.ConvertContext( Instrument.MasterInstrument.Name, - Time[0], + etTime, BuildPositionInfo(), BuildAccountInfo(), BuildSessionInfo(), @@ -391,20 +552,42 @@ namespace NinjaTrader.NinjaScript.Strategies private MarketSession BuildSessionInfo() { - if (_currentSession != null && _currentSession.SessionStart.Date == Time[0].Date) - return _currentSession; + DateTime etTime; + try + { + var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); + etTime = TimeZoneInfo.ConvertTime(Time[0], easternZone); + } + catch + { + etTime = Time[0]; + } - var sessionStart = Time[0].Date.AddHours(9).AddMinutes(30); - var sessionEnd = Time[0].Date.AddHours(16); - var isRth = Time[0].Hour >= 9 && Time[0].Hour < 16; - var sessionName = isRth ? "RTH" : "ETH"; + var sessionStart = etTime.Date.AddHours(9).AddMinutes(30); + var sessionEnd = etTime.Date.AddHours(16); + var isRth = etTime.TimeOfDay >= TimeSpan.FromHours(9.5) + && etTime.TimeOfDay < TimeSpan.FromHours(16.0); - _currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, sessionName); + _currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, isRth ? "RTH" : "ETH"); return _currentSession; } private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context) { + // Direction filter — checked before risk to avoid unnecessary processing + if (intent.Side == SdkOrderSide.Buy && !EnableLongTrades) + { + if (EnableVerboseLogging) + Print(string.Format("[SDK] Long trade filtered by direction setting: {0}", intent.Symbol)); + return; + } + if (intent.Side == SdkOrderSide.Sell && !EnableShortTrades) + { + if (EnableVerboseLogging) + Print(string.Format("[SDK] Short trade filtered by direction setting: {0}", intent.Symbol)); + return; + } + if (EnableVerboseLogging) Print(string.Format("[SDK] Validating intent: {0} {1}", intent.Side, intent.Symbol)); @@ -461,7 +644,11 @@ namespace NinjaTrader.NinjaScript.Strategies private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent) { // Circuit breaker gate - if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder()) + if (State == State.Historical) + { + // Skip circuit breaker during backtest — wall-clock timeout is meaningless on historical data. + } + else if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder()) { var state = _circuitBreaker.GetState(); Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason)); @@ -472,7 +659,39 @@ namespace NinjaTrader.NinjaScript.Strategies try { - var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, DateTime.Now.Ticks); + var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, Guid.NewGuid().ToString("N").Substring(0, 12)); + + if (EnableFileLogging) + { + string grade = "N/A"; + string score = "N/A"; + string factors = string.Empty; + + if (intent.Metadata != null && intent.Metadata.ContainsKey("confluence_score")) + { + var cs = intent.Metadata["confluence_score"] as NT8.Core.Intelligence.ConfluenceScore; + if (cs != null) + { + grade = cs.Grade.ToString(); + score = cs.WeightedScore.ToString("F3"); + + var sb = new System.Text.StringBuilder(); + foreach (var f in cs.Factors) + sb.Append(string.Format("{0}={1:F2} ", f.Type, f.Score)); + factors = sb.ToString().TrimEnd(); + } + } + + 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}", + intent.Side, + request.Quantity, + intent.StopTicks, + intent.TargetTicks)); + } + _executionAdapter.SubmitOrder(request, orderName); if (request.Side == OmsOrderSide.Buy) @@ -541,4 +760,3 @@ namespace NinjaTrader.NinjaScript.Strategies } } } - diff --git a/src/NT8.Adapters/Strategies/SimpleORBNT8.cs b/src/NT8.Adapters/Strategies/SimpleORBNT8.cs index 70294ef..d3ce10b 100644 --- a/src/NT8.Adapters/Strategies/SimpleORBNT8.cs +++ b/src/NT8.Adapters/Strategies/SimpleORBNT8.cs @@ -12,6 +12,7 @@ using NinjaTrader.NinjaScript; using NinjaTrader.NinjaScript.Indicators; using NinjaTrader.NinjaScript.Strategies; using NT8.Core.Common.Interfaces; +using NT8.Core.Intelligence; using NT8.Strategies.Examples; using SdkSimpleORB = NT8.Strategies.Examples.SimpleORBStrategy; @@ -49,6 +50,8 @@ namespace NinjaTrader.NinjaScript.Strategies Name = "Simple ORB NT8"; Description = "Opening Range Breakout with NT8 SDK integration"; + // Daily bar series is added automatically via AddDataSeries in Configure. + OpeningRangeMinutes = 30; StdDevMultiplier = 1.0; StopTicks = 8; @@ -63,11 +66,28 @@ namespace NinjaTrader.NinjaScript.Strategies Calculate = Calculate.OnBarClose; BarsRequiredToTrade = 50; + EnableLongTrades = true; + EnableShortTrades = false; + } + else if (State == State.Configure) + { + AddDataSeries(BarsPeriodType.Day, 1); } base.OnStateChange(); } + protected override void OnBarUpdate() + { + if (_strategyConfig != null && BarsArray != null && BarsArray.Length > 1) + { + DailyBarContext dailyContext = BuildDailyBarContext(0, 0.0, (double)Volume[0]); + _strategyConfig.Parameters["daily_bars"] = dailyContext; + } + + base.OnBarUpdate(); + } + protected override IStrategy CreateSdkStrategy() { return new SdkSimpleORB(OpeningRangeMinutes, StdDevMultiplier); @@ -98,15 +118,82 @@ namespace NinjaTrader.NinjaScript.Strategies _strategyConfig.Parameters["TargetTicks"] = TargetTicks; _strategyConfig.Parameters["OpeningRangeMinutes"] = OpeningRangeMinutes; + if (Instrument != null && Instrument.MasterInstrument != null) + { + _strategyConfig.Parameters["TickSize"] = Instrument.MasterInstrument.TickSize; + } + if (_logger != null) { _logger.LogInformation( - "Simple ORB configured: OR={0}min, Stop={1}ticks, Target={2}ticks", + "Simple ORB configured: OR={0}min, Stop={1}ticks, Target={2}ticks, Long={3}, Short={4}", OpeningRangeMinutes, StopTicks, - TargetTicks); + TargetTicks, + EnableLongTrades, + EnableShortTrades); } } + + /// + /// Builds a DailyBarContext from the secondary daily bar series. + /// Returns a context with Count=0 if fewer than 2 daily bars are available. + /// + /// 1 for long, -1 for short. + /// ORB range in ticks for ORB range factor. + /// Volume of the current breakout bar. + /// Populated daily context for confluence scoring. + private DailyBarContext BuildDailyBarContext(int tradeDirection, double orbRangeTicks, double breakoutBarVolume) + { + DailyBarContext ctx = new DailyBarContext(); + ctx.TradeDirection = tradeDirection; + ctx.BreakoutBarVolume = breakoutBarVolume; + ctx.TodayOpen = Open[0]; + + if (BarsArray == null || BarsArray.Length < 2 || CurrentBars == null || CurrentBars.Length < 2) + { + ctx.Count = 0; + return ctx; + } + + int dailyBarsAvailable = CurrentBars[1] + 1; + int lookback = Math.Min(10, dailyBarsAvailable); + + if (lookback < 2) + { + ctx.Count = 0; + return ctx; + } + + ctx.Highs = new double[lookback]; + ctx.Lows = new double[lookback]; + ctx.Closes = new double[lookback]; + ctx.Opens = new double[lookback]; + ctx.Volumes = new long[lookback]; + ctx.Count = lookback; + + for (int i = 0; i < lookback; i++) + { + int barsAgo = lookback - 1 - i; + ctx.Highs[i] = Highs[1][barsAgo]; + ctx.Lows[i] = Lows[1][barsAgo]; + ctx.Closes[i] = Closes[1][barsAgo]; + ctx.Opens[i] = Opens[1][barsAgo]; + ctx.Volumes[i] = (long)Volumes[1][barsAgo]; + } + + double sumVol = 0.0; + int intradayCount = 0; + int maxBars = Math.Min(78, CurrentBar + 1); + for (int i = 0; i < maxBars; i++) + { + sumVol += Volume[i]; + intradayCount++; + } + + ctx.AvgIntradayBarVolume = intradayCount > 0 ? sumVol / intradayCount : Volume[0]; + return ctx; + } } } diff --git a/src/NT8.Core/Intelligence/ConfluenceModels.cs b/src/NT8.Core/Intelligence/ConfluenceModels.cs index 19001b9..6c1654c 100644 --- a/src/NT8.Core/Intelligence/ConfluenceModels.cs +++ b/src/NT8.Core/Intelligence/ConfluenceModels.cs @@ -43,6 +43,31 @@ namespace NT8.Core.Intelligence /// Risk = 6, + /// + /// Narrow range contraction quality (NR4/NR7 concepts). + /// + NarrowRange = 7, + + /// + /// Opening range size relative to average daily ATR/range. + /// + OrbRangeVsAtr = 8, + + /// + /// Alignment between overnight gap direction and trade direction. + /// + GapDirectionAlignment = 9, + + /// + /// Breakout bar volume strength relative to intraday average volume. + /// + BreakoutVolumeStrength = 10, + + /// + /// Prior day close location strength in prior day range. + /// + PriorDayCloseStrength = 11, + /// /// Additional custom factor. /// diff --git a/src/NT8.Core/Intelligence/FactorCalculators.cs b/src/NT8.Core/Intelligence/FactorCalculators.cs index fa34395..01f93d3 100644 --- a/src/NT8.Core/Intelligence/FactorCalculators.cs +++ b/src/NT8.Core/Intelligence/FactorCalculators.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NT8.Core.Common.Models; +using NT8.Core.Logging; namespace NT8.Core.Intelligence { @@ -398,4 +399,625 @@ namespace NT8.Core.Intelligence 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()); + } + } } diff --git a/src/NT8.Strategies/Examples/SimpleORBStrategy.cs b/src/NT8.Strategies/Examples/SimpleORBStrategy.cs index b747218..51a4fdc 100644 --- a/src/NT8.Strategies/Examples/SimpleORBStrategy.cs +++ b/src/NT8.Strategies/Examples/SimpleORBStrategy.cs @@ -112,6 +112,11 @@ namespace NT8.Strategies.Examples _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}", @@ -191,6 +196,8 @@ namespace NT8.Strategies.Examples if (candidate == null) return null; + AttachDailyBarContext(candidate, bar, context); + var score = _scorer.CalculateScore(candidate, context, bar, _factorCalculators); var mode = _riskModeManager.GetCurrentMode(); @@ -349,6 +356,24 @@ namespace NT8.Strategies.Examples 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; + + 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); @@ -365,5 +390,40 @@ namespace NT8.Strategies.Examples "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; + daily.TodayOpen = bar.Open; + + 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; + } + + intent.Metadata["daily_bars"] = daily; + } } } diff --git a/tests/NT8.Core.Tests/Intelligence/OrbConfluenceFactorTests.cs b/tests/NT8.Core.Tests/Intelligence/OrbConfluenceFactorTests.cs new file mode 100644 index 0000000..635c189 --- /dev/null +++ b/tests/NT8.Core.Tests/Intelligence/OrbConfluenceFactorTests.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NT8.Core.Common.Models; +using NT8.Core.Intelligence; +using NT8.Core.Logging; + +namespace NT8.Core.Tests.Intelligence +{ + [TestClass] + public class OrbConfluenceFactorTests + { + [TestMethod] + public void NarrowRange_NR7_ScoresOne() + { + var calc = new NarrowRangeFactorCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + intent.Metadata["daily_bars"] = CreateDailyContext(new double[] { 10, 10, 10, 10, 10, 10, 5 }); + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(1.0, result.Score, 0.000001); + } + + [TestMethod] + public void NarrowRange_NR4_Scores075() + { + var calc = new NarrowRangeFactorCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + intent.Metadata["daily_bars"] = CreateDailyContext(new double[] { 5, 5, 5, 10, 9, 8, 7 }); + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(0.75, result.Score, 0.000001); + } + + [TestMethod] + public void NarrowRange_WideRange_ScoresLow() + { + var calc = new NarrowRangeFactorCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + intent.Metadata["daily_bars"] = CreateDailyContext(new double[] { 5, 5, 5, 5, 5, 5, 12 }); + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.IsTrue(result.Score <= 0.3); + } + + [TestMethod] + public void NarrowRange_MissingContext_DefaultsTo03() + { + var calc = new NarrowRangeFactorCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(0.3, result.Score, 0.000001); + } + + [TestMethod] + public void NarrowRange_InsufficientBars_DefaultsTo03() + { + var calc = new NarrowRangeFactorCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + intent.Metadata["daily_bars"] = CreateDailyContext(new double[] { 8, 7, 6, 5 }); + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(0.3, result.Score, 0.000001); + } + + [TestMethod] + public void OrbRangeVsAtr_SmallRange_ScoresOne() + { + var calc = new OrbRangeVsAtrFactorCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + var daily = CreateDailyContext(new double[] { 10, 10, 10, 10, 10, 10, 10 }); + intent.Metadata["daily_bars"] = daily; + intent.Metadata["orb_range_ticks"] = 8.0; + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(1.0, result.Score, 0.000001); + } + + [TestMethod] + public void OrbRangeVsAtr_LargeRange_ScoresVeryLow() + { + var calc = new OrbRangeVsAtrFactorCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + var daily = CreateDailyContext(new double[] { 10, 10, 10, 10, 10, 10, 10 }); + intent.Metadata["daily_bars"] = daily; + intent.Metadata["orb_range_ticks"] = 40.0; + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.IsTrue(result.Score <= 0.15); + } + + [TestMethod] + public void OrbRangeVsAtr_MissingContext_DefaultsTo05() + { + var calc = new OrbRangeVsAtrFactorCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(0.5, result.Score, 0.000001); + } + + [TestMethod] + public void GapDirection_LargeAlignedGap_ScoresOne() + { + var calc = new GapDirectionAlignmentCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 }); + daily.Closes[daily.Count - 2] = 100.0; + daily.TodayOpen = 106.0; + daily.TradeDirection = 1; + intent.Metadata["daily_bars"] = daily; + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(1.0, result.Score, 0.000001); + } + + [TestMethod] + public void GapDirection_LargeOpposingGap_ScoresVeryLow() + { + var calc = new GapDirectionAlignmentCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 }); + daily.Closes[daily.Count - 2] = 100.0; + daily.TodayOpen = 106.0; + daily.TradeDirection = -1; + intent.Metadata["daily_bars"] = daily; + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.IsTrue(result.Score <= 0.15); + } + + [TestMethod] + public void GapDirection_FlatOpen_ScoresNeutral() + { + var calc = new GapDirectionAlignmentCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 }); + daily.Closes[daily.Count - 2] = 100.0; + daily.TodayOpen = 100.1; + daily.TradeDirection = 1; + intent.Metadata["daily_bars"] = daily; + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(0.55, result.Score, 0.000001); + } + + [TestMethod] + public void BreakoutVolume_ThreeX_ScoresOne() + { + var calc = new BreakoutVolumeStrengthCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 }); + daily.BreakoutBarVolume = 3000.0; + daily.AvgIntradayBarVolume = 1000.0; + intent.Metadata["daily_bars"] = daily; + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(1.0, result.Score, 0.000001); + } + + [TestMethod] + public void BreakoutVolume_BelowAverage_ScoresLow() + { + var calc = new BreakoutVolumeStrengthCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 }); + daily.BreakoutBarVolume = 800.0; + daily.AvgIntradayBarVolume = 1200.0; + intent.Metadata["daily_bars"] = daily; + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.IsTrue(result.Score <= 0.25); + } + + [TestMethod] + public void PriorCloseStrength_LongTopQuartile_ScoresOne() + { + var calc = new PriorDayCloseStrengthCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 }); + int prev = daily.Count - 2; + daily.Lows[prev] = 100.0; + daily.Highs[prev] = 120.0; + daily.Closes[prev] = 118.0; + daily.TradeDirection = 1; + intent.Metadata["daily_bars"] = daily; + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(1.0, result.Score, 0.000001); + } + + [TestMethod] + public void PriorCloseStrength_LongBottomQuartile_ScoresLow() + { + var calc = new PriorDayCloseStrengthCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 }); + int prev = daily.Count - 2; + daily.Lows[prev] = 100.0; + daily.Highs[prev] = 120.0; + daily.Closes[prev] = 101.0; + daily.TradeDirection = 1; + intent.Metadata["daily_bars"] = daily; + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.IsTrue(result.Score <= 0.20); + } + + [TestMethod] + public void PriorCloseStrength_ShortBottomQuartile_ScoresOne() + { + var calc = new PriorDayCloseStrengthCalculator(new BasicLogger("test")); + var intent = CreateIntent(); + var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 }); + int prev = daily.Count - 2; + daily.Lows[prev] = 100.0; + daily.Highs[prev] = 120.0; + daily.Closes[prev] = 101.0; + daily.TradeDirection = -1; + intent.Metadata["daily_bars"] = daily; + + var result = calc.Calculate(intent, CreateContext(), CreateBar()); + + Assert.AreEqual(1.0, result.Score, 0.000001); + } + + private static StrategyIntent CreateIntent() + { + return new StrategyIntent( + "ES", + OrderSide.Buy, + OrderType.Market, + null, + 8, + 16, + 0.8, + "test", + new Dictionary()); + } + + private static StrategyContext CreateContext() + { + return new StrategyContext( + "ES", + DateTime.UtcNow, + new Position("ES", 0, 0, 0, 0, DateTime.UtcNow), + new AccountInfo(100000, 100000, 0, 0, DateTime.UtcNow), + new MarketSession(DateTime.Today.AddHours(9.5), DateTime.Today.AddHours(16), true, "RTH"), + new Dictionary()); + } + + private static BarData CreateBar() + { + return new BarData("ES", DateTime.UtcNow, 5000, 5005, 4998, 5002, 1000, TimeSpan.FromMinutes(5)); + } + + private static DailyBarContext CreateDailyContext(double[] ranges) + { + DailyBarContext context = new DailyBarContext(); + context.Count = ranges.Length; + context.Highs = new double[ranges.Length]; + context.Lows = new double[ranges.Length]; + context.Closes = new double[ranges.Length]; + context.Opens = new double[ranges.Length]; + context.Volumes = new long[ranges.Length]; + + for (int i = 0; i < ranges.Length; i++) + { + context.Lows[i] = 100.0; + context.Highs[i] = 100.0 + ranges[i]; + context.Opens[i] = 100.0 + (ranges[i] * 0.25); + context.Closes[i] = 100.0 + (ranges[i] * 0.75); + context.Volumes[i] = 100000; + } + + context.TodayOpen = context.Closes[Math.Max(0, context.Count - 2)] + 1.0; + context.BreakoutBarVolume = 1000.0; + context.AvgIntradayBarVolume = 1000.0; + context.TradeDirection = 1; + return context; + } + } +} diff --git a/tests/NT8.Integration.Tests/NT8OrderAdapterIntegrationTests.cs b/tests/NT8.Integration.Tests/NT8OrderAdapterIntegrationTests.cs index 0f44866..57de7a1 100644 --- a/tests/NT8.Integration.Tests/NT8OrderAdapterIntegrationTests.cs +++ b/tests/NT8.Integration.Tests/NT8OrderAdapterIntegrationTests.cs @@ -15,11 +15,20 @@ namespace NT8.Integration.Tests [TestClass] public class NT8OrderAdapterIntegrationTests { + private class FakeBridge : INT8ExecutionBridge + { + public void EnterLongManaged(int q, string n, int s, int t, double ts) { } + public void EnterShortManaged(int q, string n, int s, int t, double ts) { } + public void ExitLongManaged(string n) { } + public void ExitShortManaged(string n) { } + public void FlattenAll() { } + } + [TestMethod] public void Initialize_NullRiskManager_ThrowsArgumentNullException() { // Arrange - var adapter = new NT8OrderAdapter(); + var adapter = new NT8OrderAdapter(new FakeBridge()); var sizer = new TestPositionSizer(1); // Act / Assert @@ -31,7 +40,7 @@ namespace NT8.Integration.Tests public void Initialize_NullPositionSizer_ThrowsArgumentNullException() { // Arrange - var adapter = new NT8OrderAdapter(); + var adapter = new NT8OrderAdapter(new FakeBridge()); var risk = new TestRiskManager(true); // Act / Assert @@ -43,7 +52,7 @@ namespace NT8.Integration.Tests public void ExecuteIntent_NotInitialized_ThrowsInvalidOperationException() { // Arrange - var adapter = new NT8OrderAdapter(); + var adapter = new NT8OrderAdapter(new FakeBridge()); // Act / Assert Assert.ThrowsException( @@ -54,7 +63,7 @@ namespace NT8.Integration.Tests public void ExecuteIntent_RiskRejected_DoesNotRecordExecution() { // Arrange - var adapter = new NT8OrderAdapter(); + var adapter = new NT8OrderAdapter(new FakeBridge()); var risk = new TestRiskManager(false); var sizer = new TestPositionSizer(3); adapter.Initialize(risk, sizer); @@ -71,7 +80,7 @@ namespace NT8.Integration.Tests public void ExecuteIntent_AllowedAndSized_RecordsExecution() { // Arrange - var adapter = new NT8OrderAdapter(); + var adapter = new NT8OrderAdapter(new FakeBridge()); var risk = new TestRiskManager(true); var sizer = new TestPositionSizer(4); adapter.Initialize(risk, sizer); @@ -94,7 +103,7 @@ namespace NT8.Integration.Tests public void GetExecutionHistory_ReturnsCopy_NotMutableInternalReference() { // Arrange - var adapter = new NT8OrderAdapter(); + var adapter = new NT8OrderAdapter(new FakeBridge()); var risk = new TestRiskManager(true); var sizer = new TestPositionSizer(2); adapter.Initialize(risk, sizer); @@ -113,7 +122,7 @@ namespace NT8.Integration.Tests public void OnOrderUpdate_EmptyOrderId_ThrowsArgumentException() { // Arrange - var adapter = new NT8OrderAdapter(); + var adapter = new NT8OrderAdapter(new FakeBridge()); // Act / Assert Assert.ThrowsException( @@ -124,7 +133,7 @@ namespace NT8.Integration.Tests public void OnExecutionUpdate_EmptyExecutionId_ThrowsArgumentException() { // Arrange - var adapter = new NT8OrderAdapter(); + var adapter = new NT8OrderAdapter(new FakeBridge()); // Act / Assert Assert.ThrowsException( @@ -135,7 +144,7 @@ namespace NT8.Integration.Tests public void OnExecutionUpdate_EmptyOrderId_ThrowsArgumentException() { // Arrange - var adapter = new NT8OrderAdapter(); + var adapter = new NT8OrderAdapter(new FakeBridge()); // Act / Assert Assert.ThrowsException(