Fix deploy script: add NT8.Strategies.dll to deployment pipeline
Some checks failed
Build and Test / build (push) Has been cancelled

This commit is contained in:
2026-03-22 17:28:03 -04:00
parent 2f623dc2f8
commit a2af272d73
8 changed files with 393 additions and 40 deletions

View File

@@ -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.

View File

@@ -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: <ClassName>.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("""
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.

View File

@@ -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

View File

@@ -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
/// <summary>
/// Handles broker connection status changes. Halts new orders on disconnect,
/// logs reconnect, and resets the connection flag when restored.
/// NinjaScript signature: single ConnectionStatusEventArgs parameter.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
protected virtual List<string> GetStrategySettingsLines()
{
var lines = new List<string>();
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;
}
/// <summary>
/// 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.
/// </summary>
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));
}
}
}
}

View File

@@ -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
}
}
/// <summary>
/// Appends ORB-specific parameters to the base settings export.
/// </summary>
protected override List<string> 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;
}
/// <summary>
/// Builds a DailyBarContext from the secondary daily bar series.
/// Returns a context with Count=0 if fewer than 2 daily bars are available.

View File

@@ -188,23 +188,34 @@ namespace NT8.Core.Risk
}
/// <summary>
/// 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().
/// </summary>
/// <param name="strategyId">Strategy that received the fill.</param>
/// <param name="fill">Fill details.</param>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="strategyId">Strategy identifier.</param>
/// <param name="openContracts">Actual number of open contracts (0 when flat).</param>
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;
}
}

View File

@@ -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;

View File

@@ -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()
{