From a2af272d731858ccb0204ff06115530536dc4ff2 Mon Sep 17 00:00:00 2001 From: mo Date: Sun, 22 Mar 2026 17:28:03 -0400 Subject: [PATCH] Fix deploy script: add NT8.Strategies.dll to deployment pipeline --- .kilocode/rules/csharp_50_syntax.md | 7 +- .kilocode/rules/nt8compilespec.md | 129 ++++++++++++- deployment/deploy-to-nt8.bat | 22 ++- .../Strategies/NT8StrategyBase.cs | 179 ++++++++++++++++-- src/NT8.Adapters/Strategies/SimpleORBNT8.cs | 31 ++- src/NT8.Core/Risk/PortfolioRiskManager.cs | 27 ++- .../Examples/SimpleORBStrategy.cs | 13 +- .../Risk/PortfolioRiskManagerTests.cs | 25 ++- 8 files changed, 393 insertions(+), 40 deletions(-) diff --git a/.kilocode/rules/csharp_50_syntax.md b/.kilocode/rules/csharp_50_syntax.md index 7c15900..327e861 100644 --- a/.kilocode/rules/csharp_50_syntax.md +++ b/.kilocode/rules/csharp_50_syntax.md @@ -1,6 +1,6 @@ # C# 5.0 Syntax — Required for NT8 SDK -This project targets **.NET Framework 4.8** and must use **C# 5.0 syntax only**. +This project targets **.NET Framework 4.8** and must use **C# 5.0 syntax only**. NinjaTrader 8's NinjaScript compiler does not support C# 6+ features. --- @@ -124,3 +124,8 @@ Math.Floor(x); // (via standard using System;) - Search for `=>` — if on a property or method, rewrite as full block - Search for `nameof` — replace with string literal - Search for `out var` — split into declaration + assignment + +## NinjaScript attributes + +`[Optimizable]` does not exist in NinjaScript. Use `[NinjaScriptProperty]` +and `[Range(min, max)]` instead. See nt8compilespec.md for full NT8 API rules. diff --git a/.kilocode/rules/nt8compilespec.md b/.kilocode/rules/nt8compilespec.md index b79015c..a717805 100644 --- a/.kilocode/rules/nt8compilespec.md +++ b/.kilocode/rules/nt8compilespec.md @@ -6,8 +6,8 @@ A single source of truth to ensure **first-time compile** success for all NinjaT ## Golden Rules (Pin These) 1. **NT8 only.** No NT7 APIs. If NT7 concepts appear, **silently upgrade** to NT8 (proper `OnStateChange()` and `protected override` signatures). 2. **One file, one public class.** File name = class name. Put at the top: `// File: .cs`. -3. **Namespaces:** - - Strategies → `NinjaTrader.NinjaScript.Strategies` +3. **Namespaces:** + - Strategies → `NinjaTrader.NinjaScript.Strategies` - Indicators → `NinjaTrader.NinjaScript.Indicators` 4. **Correct override access:** All NT8 overrides are `protected override` (never `public` or `private`). 5. **Lifecycle:** Use `OnStateChange()` with `State.SetDefaults`, `State.Configure`, `State.DataLoaded` to set defaults, add data series, and instantiate indicators/Series. @@ -136,4 +136,127 @@ Compile Checklist (Preflight) DebugMode gating for Print() calls. """).format(date=datetime.date.today().isoformat()) -files["NT8_Templates.md"] = textwrap.dedent(""" \ No newline at end of file +files["NT8_Templates.md"] = textwrap.dedent(""" + +--- + +# NinjaScript Compiler Constraints + +## CRITICAL: Two separate compilers exist in this project + +This project uses TWO compilers with DIFFERENT rules: + +1. **dotnet build** — compiles src/NT8.Core/ and src/NT8.Adapters/ as + standard .NET Framework 4.8 assemblies. Errors here show up in the + terminal. + +2. **NinjaTrader NinjaScript compiler** — compiles the .cs files deployed + to `C:\Users\billy\Documents\NinjaTrader 8\bin\Custom\Strategies\`. + Errors here only show up inside the NT8 NinjaScript Editor, NOT in + dotnet build output. + +**dotnet build passing does NOT mean NT8 will compile successfully.** +Always treat NT8 compilation as a required separate verification step. + +--- + +## NinjaScript-specific API rules + +### OnConnectionStatusUpdate — correct signature + +```csharp +// CORRECT — single ConnectionStatusEventArgs parameter +protected override void OnConnectionStatusUpdate( + ConnectionStatusEventArgs connectionStatusUpdate) +{ + connectionStatusUpdate.Status // ConnectionStatus enum + connectionStatusUpdate.PriceStatus // ConnectionStatus enum +} + +// WRONG — this signature does not exist in NT8 +protected override void OnConnectionStatusUpdate( + Connection connection, + ConnectionStatus status, + DateTime time) // CS0115 — no suitable method found to override +``` + +### [Optimizable] attribute — does not exist + +```csharp +// WRONG — OptimizableAttribute does not exist in NinjaScript +[Optimizable] +public int StopTicks { get; set; } // CS0246 + +// CORRECT — all [NinjaScriptProperty] params are optimizer-eligible +// Use [Range] to set optimizer bounds +[NinjaScriptProperty] +[Range(1, 50)] +public int StopTicks { get; set; } +``` + +### Attributes that DO exist in NinjaScript + +```csharp +[NinjaScriptProperty] // exposes to UI and optimizer +[Display(...)] // controls UI label, group, order +[Range(min, max)] // sets optimizer/validation bounds +[Browsable(false)] // hides from UI +[XmlIgnore] // excludes from serialization +``` + +### Attributes that do NOT exist in NinjaScript + +``` +[Optimizable] — use [NinjaScriptProperty] + [Range] instead +[JsonProperty] — not available +[Required] — not available +``` + +### Method overrides — always verify against NT8 documentation + +Before adding any `protected override` method to NT8StrategyBase.cs or +any NinjaScript file, verify the exact signature at: +https://developer.ninjatrader.com/docs/desktop + +Common signatures that differ from intuition: + +| Method | Correct NT8 Signature | +|---|---| +| OnConnectionStatusUpdate | `(ConnectionStatusEventArgs e)` | +| OnMarketData | `(MarketDataEventArgs e)` | +| OnMarketDepth | `(MarketDepthEventArgs e)` | +| OnOrderUpdate | `(Order order, double limitPrice, double stopPrice, int quantity, int filled, double avgFillPrice, OrderState orderState, DateTime time, ErrorCode error, string nativeError)` | +| OnExecutionUpdate | `(Execution execution, string executionId, double price, int quantity, MarketPosition marketPosition, string orderId, DateTime time)` | + +--- + +## Deployment reminder + +The full deployment sequence is always: + +``` +1. dotnet build NT8-SDK.sln --configuration Release +2. deployment\deploy-to-nt8.bat +3. NT8: Tools → NinjaScript Editor → Compile All +4. Reload any open Strategy Analyzer or chart instances +``` + +Steps 1-2 passing does NOT mean step 3 will pass. NT8 compilation is +the only authoritative check for NinjaScript-specific code. + +--- + +## Files compiled by NT8 (not dotnet build) + +These files are deployed as source and compiled by NT8's embedded +compiler. Any NT8-specific API usage in these files is invisible to +dotnet build: + +- `src/NT8.Adapters/Strategies/NT8StrategyBase.cs` +- `src/NT8.Adapters/Strategies/SimpleORBNT8.cs` +- `src/NT8.Adapters/Wrappers/BaseNT8StrategyWrapper.cs` +- `src/NT8.Adapters/Wrappers/SimpleORBNT8Wrapper.cs` + +Any new strategy or wrapper file added to these locations inherits the +same constraint. When modifying these files, the NT8 compiler is the +only valid test. diff --git a/deployment/deploy-to-nt8.bat b/deployment/deploy-to-nt8.bat index 6d2c8e0..e9880ce 100644 --- a/deployment/deploy-to-nt8.bat +++ b/deployment/deploy-to-nt8.bat @@ -10,6 +10,7 @@ set "NT8_CUSTOM=%USERPROFILE%\Documents\NinjaTrader 8\bin\Custom" set "NT8_STRATEGIES=%NT8_CUSTOM%\Strategies" set "CORE_BIN=%PROJECT_ROOT%\src\NT8.Core\bin\Release\net48" set "ADAPTERS_BIN=%PROJECT_ROOT%\src\NT8.Adapters\bin\Release\net48" +set "STRATEGIES_BIN=%PROJECT_ROOT%\src\NT8.Strategies\bin\Release\net48" set "WRAPPERS_SRC=%PROJECT_ROOT%\src\NT8.Adapters\Wrappers" set "BACKUP_ROOT=%SCRIPT_DIR%backups" @@ -40,6 +41,13 @@ if not exist "%ADAPTERS_BIN%\NT8.Adapters.dll" ( exit /b 1 ) +if not exist "%STRATEGIES_BIN%\NT8.Strategies.dll" ( + echo ERROR: Strategies DLL not found: %STRATEGIES_BIN%\NT8.Strategies.dll + echo Build release artifacts first: + echo dotnet build NT8-SDK.sln --configuration Release + exit /b 1 +) + if not exist "%NT8_STRATEGIES%" ( mkdir "%NT8_STRATEGIES%" ) @@ -52,6 +60,7 @@ mkdir "%BACKUP_ROOT%\%STAMP%" >nul 2>&1 echo Backing up existing NT8 SDK files... if exist "%NT8_CUSTOM%\NT8.Core.dll" copy /Y "%NT8_CUSTOM%\NT8.Core.dll" "%BACKUP_DIR%\NT8.Core.dll" >nul if exist "%NT8_CUSTOM%\NT8.Adapters.dll" copy /Y "%NT8_CUSTOM%\NT8.Adapters.dll" "%BACKUP_DIR%\NT8.Adapters.dll" >nul +if exist "%NT8_CUSTOM%\NT8.Strategies.dll" copy /Y "%NT8_CUSTOM%\NT8.Strategies.dll" "%BACKUP_DIR%\NT8.Strategies.dll" >nul if exist "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" copy /Y "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" "%BACKUP_DIR%\BaseNT8StrategyWrapper.cs" >nul if exist "%NT8_STRATEGIES%\SimpleORBNT8Wrapper.cs" copy /Y "%NT8_STRATEGIES%\SimpleORBNT8Wrapper.cs" "%BACKUP_DIR%\SimpleORBNT8Wrapper.cs" >nul @@ -59,6 +68,7 @@ echo Deployment manifest > "%MANIFEST_FILE%" echo Timestamp: %STAMP%>> "%MANIFEST_FILE%" echo Source Core DLL: %CORE_BIN%\NT8.Core.dll>> "%MANIFEST_FILE%" echo Source Adapters DLL: %ADAPTERS_BIN%\NT8.Adapters.dll>> "%MANIFEST_FILE%" +echo Source Strategies DLL: %STRATEGIES_BIN%\NT8.Strategies.dll>> "%MANIFEST_FILE%" echo Destination Custom Folder: %NT8_CUSTOM%>> "%MANIFEST_FILE%" echo Destination Strategies Folder: %NT8_STRATEGIES%>> "%MANIFEST_FILE%" @@ -75,6 +85,12 @@ if errorlevel 1 ( exit /b 1 ) +copy /Y "%STRATEGIES_BIN%\NT8.Strategies.dll" "%NT8_CUSTOM%\NT8.Strategies.dll" >nul +if errorlevel 1 ( + echo ERROR: Failed to copy NT8.Strategies.dll + exit /b 1 +) + echo Deploying wrapper sources... copy /Y "%WRAPPERS_SRC%\BaseNT8StrategyWrapper.cs" "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" >nul if errorlevel 1 ( @@ -112,6 +128,11 @@ if not exist "%NT8_CUSTOM%\NT8.Adapters.dll" ( exit /b 1 ) +if not exist "%NT8_CUSTOM%\NT8.Strategies.dll" ( + echo ERROR: Verification failed for NT8.Strategies.dll + exit /b 1 +) + if not exist "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" ( echo ERROR: Verification failed for BaseNT8StrategyWrapper.cs exit /b 1 @@ -133,4 +154,3 @@ echo 2. Open NinjaScript Editor and press F5 (Compile). echo 3. Verify strategies appear in the Strategies list. exit /b 0 - diff --git a/src/NT8.Adapters/Strategies/NT8StrategyBase.cs b/src/NT8.Adapters/Strategies/NT8StrategyBase.cs index 646e198..6eb7a8c 100644 --- a/src/NT8.Adapters/Strategies/NT8StrategyBase.cs +++ b/src/NT8.Adapters/Strategies/NT8StrategyBase.cs @@ -203,9 +203,12 @@ namespace NinjaTrader.NinjaScript.Strategies { try { + InitFileLog(); InitializeSdkComponents(); _sdkInitialized = true; Print(string.Format("[SDK] {0} initialized successfully", Name)); + WriteSettingsFile(); + WriteSessionHeader(); } catch (Exception ex) { @@ -217,8 +220,7 @@ namespace NinjaTrader.NinjaScript.Strategies } else if (State == State.Realtime) { - InitFileLog(); - WriteSessionHeader(); + WriteSettingsFile(); } else if (State == State.Terminated) { @@ -229,6 +231,12 @@ namespace NinjaTrader.NinjaScript.Strategies protected override void OnBarUpdate() { + // Only process primary bar series — ignore secondary data series updates. + // Secondary series (e.g. daily bars for confluence) trigger OnBarUpdate separately + // and must never generate strategy signals. + if (BarsInProgress != 0) + return; + if (!_sdkInitialized || _sdkStrategy == null) { return; @@ -244,6 +252,9 @@ namespace NinjaTrader.NinjaScript.Strategies _lastBarTime = Time[0]; + // Sync actual open position to portfolio manager on every bar + PortfolioRiskManager.Instance.UpdateOpenContracts(Name, Math.Abs(Position.Quantity)); + // Kill switch — checked AFTER bar guards so ExitLong/ExitShort are valid if (EnableKillSwitch) { @@ -272,6 +283,34 @@ namespace NinjaTrader.NinjaScript.Strategies return; } + // Hard RTH guard using NT8 bar time converted from CT to ET. + // Belt-and-suspenders against SDK session timezone issues. + DateTime ntBarTimeEt; + try + { + var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); + var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); + DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc( + DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified), + centralZone); + ntBarTimeEt = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone); + } + catch + { + ntBarTimeEt = Time[0]; + } + + bool isRthBar = ntBarTimeEt.TimeOfDay >= new TimeSpan(9, 30, 0) + && ntBarTimeEt.TimeOfDay < new TimeSpan(16, 0, 0); + + if (!isRthBar) + { + if (EnableVerboseLogging && CurrentBar % 500 == 0) + Print(string.Format("[SDK] Skipping ETH bar {0} at {1:HH:mm} ET", + CurrentBar, ntBarTimeEt)); + return; + } + // Log first processable bar and every 100th bar. if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0) { @@ -384,34 +423,35 @@ namespace NinjaTrader.NinjaScript.Strategies /// /// Handles broker connection status changes. Halts new orders on disconnect, /// logs reconnect, and resets the connection flag when restored. + /// NinjaScript signature: single ConnectionStatusEventArgs parameter. /// protected override void OnConnectionStatusUpdate( - Connection connection, - ConnectionStatus status, - DateTime time) + ConnectionStatusEventArgs connectionStatusUpdate) { - if (connection == null) return; + if (connectionStatusUpdate == null) return; - if (status == ConnectionStatus.Connected) + if (connectionStatusUpdate.Status == ConnectionStatus.Connected) { if (_connectionLost) { _connectionLost = false; Print(string.Format("[NT8-SDK] Connection RESTORED at {0} — trading resumed.", - time.ToString("HH:mm:ss"))); - FileLog(string.Format("CONNECTION RESTORED at {0}", time.ToString("HH:mm:ss"))); + DateTime.Now.ToString("HH:mm:ss"))); + FileLog(string.Format("CONNECTION RESTORED at {0}", DateTime.Now.ToString("HH:mm:ss"))); } } - else if (status == ConnectionStatus.Disconnected || - status == ConnectionStatus.ConnectionLost) + else if (connectionStatusUpdate.Status == ConnectionStatus.Disconnected || + connectionStatusUpdate.Status == ConnectionStatus.ConnectionLost) { if (!_connectionLost) { _connectionLost = true; Print(string.Format("[NT8-SDK] Connection LOST at {0} — halting new orders. Status={1}", - time.ToString("HH:mm:ss"), - status)); - FileLog(string.Format("CONNECTION LOST at {0} Status={1}", time.ToString("HH:mm:ss"), status)); + DateTime.Now.ToString("HH:mm:ss"), + connectionStatusUpdate.Status)); + FileLog(string.Format("CONNECTION LOST at {0} Status={1}", + DateTime.Now.ToString("HH:mm:ss"), + connectionStatusUpdate.Status)); } } } @@ -465,6 +505,7 @@ namespace NinjaTrader.NinjaScript.Strategies private void WriteSessionHeader() { FileLog("=== SESSION START " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ==="); + FileLog(string.Format("Mode : {0}", State == State.Historical ? "BACKTEST" : "LIVE/SIM")); 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")); @@ -566,8 +607,12 @@ namespace NinjaTrader.NinjaScript.Strategies DateTime etTime; try { + var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); - etTime = TimeZoneInfo.ConvertTime(Time[0], easternZone); + DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc( + DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified), + centralZone); + etTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone); } catch { @@ -630,16 +675,37 @@ namespace NinjaTrader.NinjaScript.Strategies DateTime etTime; try { + var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); - etTime = TimeZoneInfo.ConvertTime(Time[0], easternZone); + DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc( + DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified), + centralZone); + etTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone); } catch { etTime = Time[0]; } - var sessionStart = etTime.Date.AddHours(9).AddMinutes(30); - var sessionEnd = etTime.Date.AddHours(16); + // Futures trade nearly 24 hours. Bars at/after 17:00 ET belong to the next + // calendar day's RTH trading session. + DateTime tradingDate; + if (etTime.TimeOfDay >= new TimeSpan(17, 0, 0)) + tradingDate = etTime.Date.AddDays(1); + else + tradingDate = etTime.Date; + + if (EnableVerboseLogging && (CurrentBar == BarsRequiredToTrade || CurrentBar % 500 == 0)) + { + Print(string.Format("[SDK-TZ] Bar {0}: NT8 Time[0]={1:yyyy-MM-dd HH:mm:ss} | etTime={2:yyyy-MM-dd HH:mm:ss} | isRth={3}", + CurrentBar, + Time[0], + etTime, + etTime.TimeOfDay >= TimeSpan.FromHours(9.5) && etTime.TimeOfDay < TimeSpan.FromHours(16.0))); + } + + var sessionStart = tradingDate.AddHours(9).AddMinutes(30); + var sessionEnd = tradingDate.AddHours(16); var isRth = etTime.TimeOfDay >= TimeSpan.FromHours(9.5) && etTime.TimeOfDay < TimeSpan.FromHours(16.0); @@ -843,5 +909,82 @@ namespace NinjaTrader.NinjaScript.Strategies return null; return _executionAdapter.GetOrderStatus(orderName); } + + /// + /// Returns all strategy parameter lines for the settings export file. + /// Override in subclasses to append strategy-specific parameters. + /// Call base.GetStrategySettingsLines() first then add to the list. + /// + protected virtual List GetStrategySettingsLines() + { + var lines = new List(); + lines.Add("=== STRATEGY SETTINGS EXPORT ==="); + lines.Add(string.Format("ExportTime : {0:yyyy-MM-dd HH:mm:ss}", DateTime.Now)); + lines.Add(string.Format("StrategyName : {0}", Name)); + lines.Add(string.Format("Description : {0}", Description)); + lines.Add(string.Format("Account : {0}", Account != null ? Account.Name : "N/A")); + lines.Add(string.Format("Instrument : {0}", Instrument != null ? Instrument.FullName : "N/A")); + lines.Add(string.Format("BarsPeriod : {0} {1}", BarsPeriod != null ? BarsPeriod.Value.ToString() : "N/A", BarsPeriod != null ? BarsPeriod.BarsPeriodType.ToString() : string.Empty)); + lines.Add(string.Format("BarsRequiredToTrade: {0}", BarsRequiredToTrade)); + lines.Add(string.Format("Calculate : {0}", Calculate)); + lines.Add("--- Risk ---"); + lines.Add(string.Format("DailyLossLimit : {0:C}", DailyLossLimit)); + lines.Add(string.Format("MaxTradeRisk : {0:C}", MaxTradeRisk)); + lines.Add(string.Format("MaxOpenPositions : {0}", MaxOpenPositions)); + lines.Add(string.Format("RiskPerTrade : {0:C}", RiskPerTrade)); + lines.Add("--- Sizing ---"); + lines.Add(string.Format("MinContracts : {0}", MinContracts)); + lines.Add(string.Format("MaxContracts : {0}", MaxContracts)); + lines.Add("--- Direction ---"); + lines.Add(string.Format("EnableLongTrades : {0}", EnableLongTrades)); + lines.Add(string.Format("EnableShortTrades : {0}", EnableShortTrades)); + lines.Add("--- Controls ---"); + lines.Add(string.Format("EnableKillSwitch : {0}", EnableKillSwitch)); + lines.Add(string.Format("EnableVerboseLogging: {0}", EnableVerboseLogging)); + lines.Add(string.Format("EnableFileLogging : {0}", EnableFileLogging)); + lines.Add(string.Format("LogDirectory : {0}", string.IsNullOrEmpty(LogDirectory) ? "(default)" : LogDirectory)); + lines.Add("--- Portfolio ---"); + lines.Add(string.Format("PortfolioStatus : {0}", PortfolioRiskManager.Instance.GetStatusSnapshot())); + lines.Add("=== END SETTINGS ==="); + return lines; + } + + /// + /// Writes a settings export file to the same directory as the session log. + /// File is named settings_STRATEGYNAME_YYYYMMDD_HHmmss.txt. + /// Only writes when EnableVerboseLogging is true. + /// + private void WriteSettingsFile() + { + if (!EnableVerboseLogging) 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 safeName = Name.Replace(" ", "_").Replace("/", "_").Replace("\\", "_"); + string path = System.IO.Path.Combine(dir, + string.Format("settings_{0}_{1}.txt", + safeName, + DateTime.Now.ToString("yyyyMMdd_HHmmss"))); + + var lines = GetStrategySettingsLines(); + + System.IO.File.WriteAllLines(path, lines.ToArray()); + + Print(string.Format("[NT8-SDK] Settings exported: {0}", path)); + FileLog(string.Format("SETTINGS FILE: {0}", path)); + } + catch (Exception ex) + { + Print(string.Format("[NT8-SDK] WARNING: Could not write settings file: {0}", ex.Message)); + } + } } } diff --git a/src/NT8.Adapters/Strategies/SimpleORBNT8.cs b/src/NT8.Adapters/Strategies/SimpleORBNT8.cs index 300523f..be84fa7 100644 --- a/src/NT8.Adapters/Strategies/SimpleORBNT8.cs +++ b/src/NT8.Adapters/Strategies/SimpleORBNT8.cs @@ -24,7 +24,6 @@ namespace NinjaTrader.NinjaScript.Strategies public class SimpleORBNT8 : NT8StrategyBase { [NinjaScriptProperty] - [Optimizable] [Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)] [Range(5, 120)] public int OpeningRangeMinutes { get; set; } @@ -35,13 +34,11 @@ namespace NinjaTrader.NinjaScript.Strategies public double StdDevMultiplier { get; set; } [NinjaScriptProperty] - [Optimizable] [Display(Name = "Stop Loss Ticks", GroupName = "ORB Risk", Order = 1)] [Range(1, 50)] public int StopTicks { get; set; } [NinjaScriptProperty] - [Optimizable] [Display(Name = "Profit Target Ticks", GroupName = "ORB Risk", Order = 2)] [Range(1, 100)] public int TargetTicks { get; set; } @@ -51,7 +48,7 @@ namespace NinjaTrader.NinjaScript.Strategies if (State == State.SetDefaults) { Name = "Simple ORB NT8"; - Description = "Opening Range Breakout with NT8 SDK integration"; + Description = "v0.4.0 | 2026-03-19 | NR7+ORB factors, PortfolioRiskManager, connection recovery, live account balance"; // Daily bar series is added automatically via AddDataSeries in Configure. @@ -139,6 +136,32 @@ namespace NinjaTrader.NinjaScript.Strategies } } + /// + /// Appends ORB-specific parameters to the base settings export. + /// + protected override List GetStrategySettingsLines() + { + var lines = base.GetStrategySettingsLines(); + + // Insert ORB section before the final === END SETTINGS === line + int endIdx = lines.Count - 1; + lines.Insert(endIdx, "--- ORB Strategy ---"); + lines.Insert(endIdx + 1, string.Format("OpeningRangeMinutes: {0}", OpeningRangeMinutes)); + lines.Insert(endIdx + 2, string.Format("StdDevMultiplier : {0:F2}", StdDevMultiplier)); + lines.Insert(endIdx + 3, string.Format("StopTicks : {0}", StopTicks)); + lines.Insert(endIdx + 4, string.Format("TargetTicks : {0}", TargetTicks)); + + double tickDollarValue = 0.25 * 50.0; + if (Instrument != null && Instrument.MasterInstrument != null) + tickDollarValue = Instrument.MasterInstrument.TickSize * Instrument.MasterInstrument.PointValue; + + lines.Insert(endIdx + 5, string.Format("StopDollars : {0:C}", StopTicks * tickDollarValue)); + lines.Insert(endIdx + 6, string.Format("TargetDollars : {0:C}", TargetTicks * tickDollarValue)); + lines.Insert(endIdx + 7, string.Format("RR_Ratio : {0:F2}:1", (double)TargetTicks / StopTicks)); + + return lines; + } + /// /// Builds a DailyBarContext from the secondary daily bar series. /// Returns a context with Count=0 if fewer than 2 daily bars are available. diff --git a/src/NT8.Core/Risk/PortfolioRiskManager.cs b/src/NT8.Core/Risk/PortfolioRiskManager.cs index 65f7429..7746bac 100644 --- a/src/NT8.Core/Risk/PortfolioRiskManager.cs +++ b/src/NT8.Core/Risk/PortfolioRiskManager.cs @@ -188,23 +188,34 @@ namespace NT8.Core.Risk } /// - /// Reports a fill to the portfolio manager. Updates open contract count for the strategy. - /// Called from NT8StrategyBase.OnExecutionUpdate() after each fill. + /// Reports a fill to the portfolio manager. + /// Contract tracking is handled by UpdateOpenContracts(). /// /// Strategy that received the fill. /// Fill details. public void ReportFill(string strategyId, OrderFill fill) { + // Contract tracking is now handled by UpdateOpenContracts() called + // from OnBarUpdate with the actual position size. + // This method is retained for API compatibility and future P&L attribution use. if (string.IsNullOrEmpty(strategyId) || fill == null) return; + } + + /// + /// Updates the open contract count for a strategy to the actual current + /// position size. Called from NT8StrategyBase on each bar close. + /// This replaces fill-based inference with authoritative position data. + /// + /// Strategy identifier. + /// Actual number of open contracts (0 when flat). + public void UpdateOpenContracts(string strategyId, int openContracts) + { + if (string.IsNullOrEmpty(strategyId)) return; lock (_lock) { - if (!_strategyOpenContracts.ContainsKey(strategyId)) - _strategyOpenContracts[strategyId] = 0; - - _strategyOpenContracts[strategyId] += fill.Quantity; - if (_strategyOpenContracts[strategyId] < 0) - _strategyOpenContracts[strategyId] = 0; + if (openContracts < 0) openContracts = 0; + _strategyOpenContracts[strategyId] = openContracts; } } diff --git a/src/NT8.Strategies/Examples/SimpleORBStrategy.cs b/src/NT8.Strategies/Examples/SimpleORBStrategy.cs index 51a4fdc..f95879a 100644 --- a/src/NT8.Strategies/Examples/SimpleORBStrategy.cs +++ b/src/NT8.Strategies/Examples/SimpleORBStrategy.cs @@ -176,6 +176,16 @@ namespace NT8.Strategies.Examples return null; } + if (_logger != null && _openingRangeReady) + { + _logger.LogDebug( + "ORB ready: High={0:F2} Low={1:F2} Range={2:F2} TradeTaken={3}", + _openingRangeHigh, + _openingRangeLow, + _openingRangeHigh - _openingRangeLow, + _tradeTaken); + } + if (_tradeTaken) return null; @@ -219,7 +229,8 @@ namespace NT8.Strategies.Examples candidate.Confidence = score.WeightedScore; candidate.Reason = string.Format("{0}; grade={1}; mode={2}", candidate.Reason, score.Grade, mode); - candidate.Metadata["confluence_score"] = score.WeightedScore; + candidate.Metadata["confluence_score"] = score; + candidate.Metadata["confluence_weighted_score"] = score.WeightedScore; candidate.Metadata["trade_grade"] = score.Grade.ToString(); candidate.Metadata["risk_mode"] = mode.ToString(); candidate.Metadata["grade_multiplier"] = gradeMultiplier; diff --git a/tests/NT8.Core.Tests/Risk/PortfolioRiskManagerTests.cs b/tests/NT8.Core.Tests/Risk/PortfolioRiskManagerTests.cs index e586221..414f0a2 100644 --- a/tests/NT8.Core.Tests/Risk/PortfolioRiskManagerTests.cs +++ b/tests/NT8.Core.Tests/Risk/PortfolioRiskManagerTests.cs @@ -52,10 +52,7 @@ namespace NT8.Core.Tests.Risk _manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig()); _manager.MaxTotalOpenContracts = 2; - var fill1 = new OrderFill("ord1", "ES", 1, 5000.0, System.DateTime.UtcNow, 0.0, "exec1"); - var fill2 = new OrderFill("ord2", "ES", 1, 5001.0, System.DateTime.UtcNow, 0.0, "exec2"); - _manager.ReportFill("strat1", fill1); - _manager.ReportFill("strat1", fill2); + _manager.UpdateOpenContracts("strat1", 2); var intent = TestDataBuilder.CreateValidIntent(); // Act @@ -65,6 +62,26 @@ namespace NT8.Core.Tests.Risk Assert.IsFalse(decision.Allow); } + [TestMethod] + public void UpdateOpenContracts_WhenPositionCloses_UnblocksTrading() + { + // Arrange + _manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig()); + _manager.MaxTotalOpenContracts = 6; + var intent = TestDataBuilder.CreateValidIntent(); + + // Act + _manager.UpdateOpenContracts("strat1", 6); + var blocked = _manager.ValidatePortfolioRisk("strat1", intent); + + _manager.UpdateOpenContracts("strat1", 0); + var unblocked = _manager.ValidatePortfolioRisk("strat1", intent); + + // Assert + Assert.IsFalse(blocked.Allow); + Assert.IsTrue(unblocked.Allow); + } + [TestMethod] public void PortfolioKillSwitch_WhenTrue_BlocksAllOrders() {