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(