Add ORB confluence factors (NR4/NR7, gap alignment, breakout volume, prior close) + session file logger
Some checks failed
Build and Test / build (push) Has been cancelled

This commit is contained in:
2026-03-19 12:16:39 -04:00
parent ee4da1b607
commit 498f298975
11 changed files with 1569 additions and 76 deletions

View File

@@ -0,0 +1,26 @@
using System;
namespace NT8.Adapters.NinjaTrader
{
/// <summary>
/// Provides NT8OrderAdapter access to NinjaScript execution methods.
/// Implemented by NT8StrategyBase.
/// </summary>
public interface INT8ExecutionBridge
{
/// <summary>Submit a long entry with stop and target.</summary>
void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize);
/// <summary>Submit a short entry with stop and target.</summary>
void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize);
/// <summary>Exit all long positions.</summary>
void ExitLongManaged(string signalName);
/// <summary>Exit all short positions.</summary>
void ExitShortManaged(string signalName);
/// <summary>Flatten the full position immediately.</summary>
void FlattenAll();
}
}

View File

@@ -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<NT8OrderExecutionRecord>();
}
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()
{
}
}
/// <summary>
/// Initialize the adapter with required components
/// </summary>

View File

@@ -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<NT8OrderExecutionRecord> _executionHistory;
@@ -19,8 +20,11 @@ namespace NT8.Adapters.NinjaTrader
/// <summary>
/// Constructor for NT8OrderAdapter.
/// </summary>
public NT8OrderAdapter()
public NT8OrderAdapter(INT8ExecutionBridge bridge)
{
if (bridge == null)
throw new ArgumentNullException("bridge");
_bridge = bridge;
_executionHistory = new List<NT8OrderExecutionRecord>();
}
@@ -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);
}
}
*/
}
/// <summary>

View File

@@ -32,7 +32,7 @@ namespace NinjaTrader.NinjaScript.Strategies
/// <summary>
/// Base class for strategies that integrate NT8 SDK components.
/// </summary>
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");
}
/// <summary>
/// Create the SDK strategy instance.
/// </summary>
@@ -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<string, object>();
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
}
}
}

View File

@@ -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);
}
}
/// <summary>
/// Builds a DailyBarContext from the secondary daily bar series.
/// Returns a context with Count=0 if fewer than 2 daily bars are available.
/// </summary>
/// <param name="tradeDirection">1 for long, -1 for short.</param>
/// <param name="orbRangeTicks">ORB range in ticks for ORB range factor.</param>
/// <param name="breakoutBarVolume">Volume of the current breakout bar.</param>
/// <returns>Populated daily context for confluence scoring.</returns>
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;
}
}
}