Compare commits

..

5 Commits

Author SHA1 Message Date
mo
2f623dc2f8 S2-B3/S3-05: PortfolioRiskManager, connection loss recovery, long-only lock
Some checks failed
Build and Test / build (push) Has been cancelled
2026-03-19 16:17:02 -04:00
mo
3282254572 S2-B1/B2: live account balance, Optimizable attrs, BarsRequired=50, archive NT8.Core.Orders
Some checks failed
Build and Test / build (push) Has been cancelled
2026-03-19 14:48:22 -04:00
mo
498f298975 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
2026-03-19 12:16:39 -04:00
mo
ee4da1b607 chore: add deployment/backups and DLLs to .gitignore
Some checks failed
Build and Test / build (push) Has been cancelled
2026-03-10 15:50:53 -04:00
mo
a283ef4673 chore: checkpoint before NT8 execution wiring fix
Current state: Strategy builds and loads correctly, passes 240+ tests,
backtest (Strategy Analyzer) works but zero trades execute on live/SIM.

Root cause identified: NT8OrderAdapter.ExecuteInNT8() is a stub - it logs
to an internal list but never calls EnterLong/EnterShort/SetStopLoss/
SetProfitTarget. Fix is ready in TASK_01_WIRE_NT8_EXECUTION.md.

Task files added (ready for Kilocode):
- TASK_01_WIRE_NT8_EXECUTION.md (CRITICAL - INT8ExecutionBridge + wiring)
- TASK_02_EMERGENCY_KILL_SWITCH.md (CRITICAL - kill switch + verbose logging)
- TASK_03_WIRE_CIRCUIT_BREAKER.md (HIGH - wire ExecutionCircuitBreaker)

Build Status: All 240+ tests passing, zero errors
Next: Run Kilocode against TASK_01, TASK_02, TASK_03 in order
2026-03-10 15:49:59 -04:00
20 changed files with 2208 additions and 203 deletions

7
.gitignore vendored
View File

@@ -85,3 +85,10 @@ Thumbs.db
tools/output/ tools/output/
market-data/*.csv market-data/*.csv
replay-data/ replay-data/
# Deployment backups (local only)
deployment/backups/
# Build artifacts in deployment
*.dll
*.pdb

130
cleanup-repo.ps1 Normal file
View File

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

44
commit-now.ps1 Normal file
View File

@@ -0,0 +1,44 @@
# commit-now.ps1 - Stage and commit all current changes to Gitea
# Run from: C:\dev\nt8-sdk
Set-Location "C:\dev\nt8-sdk"
Write-Host "`n=== Current Git Status ===" -ForegroundColor Cyan
git status
Write-Host "`n=== Recent Commits ===" -ForegroundColor Cyan
git log --oneline -5
Write-Host "`n=== Staging all changes ===" -ForegroundColor Cyan
git add -A
Write-Host "`n=== Staged Files ===" -ForegroundColor Cyan
git status
$commitMessage = @"
chore: checkpoint before NT8 execution wiring fix
Current state: Strategy builds and loads correctly, passes 240+ tests,
backtest (Strategy Analyzer) works but zero trades execute on live/SIM.
Root cause identified: NT8OrderAdapter.ExecuteInNT8() is a stub - it logs
to an internal list but never calls EnterLong/EnterShort/SetStopLoss/
SetProfitTarget. Fix is ready in TASK_01_WIRE_NT8_EXECUTION.md.
Task files added (ready for Kilocode):
- TASK_01_WIRE_NT8_EXECUTION.md (CRITICAL - INT8ExecutionBridge + wiring)
- TASK_02_EMERGENCY_KILL_SWITCH.md (CRITICAL - kill switch + verbose logging)
- TASK_03_WIRE_CIRCUIT_BREAKER.md (HIGH - wire ExecutionCircuitBreaker)
Build Status: All 240+ tests passing, zero errors
Next: Run Kilocode against TASK_01, TASK_02, TASK_03 in order
"@
Write-Host "`n=== Committing ===" -ForegroundColor Cyan
git commit -m $commitMessage
Write-Host "`n=== Pushing to Gitea ===" -ForegroundColor Cyan
git push
Write-Host "`n=== Done! ===" -ForegroundColor Green
git log --oneline -3

View File

@@ -12,7 +12,6 @@ set "CORE_BIN=%PROJECT_ROOT%\src\NT8.Core\bin\Release\net48"
set "ADAPTERS_BIN=%PROJECT_ROOT%\src\NT8.Adapters\bin\Release\net48" set "ADAPTERS_BIN=%PROJECT_ROOT%\src\NT8.Adapters\bin\Release\net48"
set "WRAPPERS_SRC=%PROJECT_ROOT%\src\NT8.Adapters\Wrappers" set "WRAPPERS_SRC=%PROJECT_ROOT%\src\NT8.Adapters\Wrappers"
set "BACKUP_ROOT=%SCRIPT_DIR%backups" set "BACKUP_ROOT=%SCRIPT_DIR%backups"
set "MANIFEST_FILE=%BACKUP_DIR%\manifest.txt"
echo ============================================================ echo ============================================================
echo NT8 SDK Deployment echo NT8 SDK Deployment
@@ -47,7 +46,8 @@ if not exist "%NT8_STRATEGIES%" (
for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd_HHmmss"') do set "STAMP=%%i" for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd_HHmmss"') do set "STAMP=%%i"
set "BACKUP_DIR=%BACKUP_ROOT%\%STAMP%" set "BACKUP_DIR=%BACKUP_ROOT%\%STAMP%"
mkdir "%BACKUP_DIR%" >nul 2>&1 set "MANIFEST_FILE=%BACKUP_ROOT%\%STAMP%\manifest.txt"
mkdir "%BACKUP_ROOT%\%STAMP%" >nul 2>&1
echo Backing up existing NT8 SDK files... 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.Core.dll" copy /Y "%NT8_CUSTOM%\NT8.Core.dll" "%BACKUP_DIR%\NT8.Core.dll" >nul
@@ -88,6 +88,19 @@ if errorlevel 1 (
exit /b 1 exit /b 1
) )
set "STRATEGIES_SRC=%PROJECT_ROOT%\src\NT8.Adapters\Strategies"
copy /Y "%STRATEGIES_SRC%\NT8StrategyBase.cs" "%NT8_STRATEGIES%\NT8StrategyBase.cs" >nul
if errorlevel 1 (
echo ERROR: Failed to copy NT8StrategyBase.cs
exit /b 1
)
copy /Y "%STRATEGIES_SRC%\SimpleORBNT8.cs" "%NT8_STRATEGIES%\SimpleORBNT8.cs" >nul
if errorlevel 1 (
echo ERROR: Failed to copy SimpleORBNT8.cs
exit /b 1
)
echo Verifying deployment files... echo Verifying deployment files...
if not exist "%NT8_CUSTOM%\NT8.Core.dll" ( if not exist "%NT8_CUSTOM%\NT8.Core.dll" (
echo ERROR: Verification failed for NT8.Core.dll echo ERROR: Verification failed for NT8.Core.dll

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() public NT8Adapter()
{ {
_dataAdapter = new NT8DataAdapter(); _dataAdapter = new NT8DataAdapter();
_orderAdapter = new NT8OrderAdapter(); _orderAdapter = new NT8OrderAdapter(new NullExecutionBridge());
_loggingAdapter = new NT8LoggingAdapter(); _loggingAdapter = new NT8LoggingAdapter();
_executionHistory = new List<NT8OrderExecutionRecord>(); _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> /// <summary>
/// Initialize the adapter with required components /// Initialize the adapter with required components
/// </summary> /// </summary>

View File

@@ -12,6 +12,7 @@ namespace NT8.Adapters.NinjaTrader
public class NT8OrderAdapter public class NT8OrderAdapter
{ {
private readonly object _lock = new object(); private readonly object _lock = new object();
private readonly INT8ExecutionBridge _bridge;
private IRiskManager _riskManager; private IRiskManager _riskManager;
private IPositionSizer _positionSizer; private IPositionSizer _positionSizer;
private readonly List<NT8OrderExecutionRecord> _executionHistory; private readonly List<NT8OrderExecutionRecord> _executionHistory;
@@ -19,8 +20,11 @@ namespace NT8.Adapters.NinjaTrader
/// <summary> /// <summary>
/// Constructor for NT8OrderAdapter. /// Constructor for NT8OrderAdapter.
/// </summary> /// </summary>
public NT8OrderAdapter() public NT8OrderAdapter(INT8ExecutionBridge bridge)
{ {
if (bridge == null)
throw new ArgumentNullException("bridge");
_bridge = bridge;
_executionHistory = new List<NT8OrderExecutionRecord>(); _executionHistory = new List<NT8OrderExecutionRecord>();
} }
@@ -127,18 +131,30 @@ namespace NT8.Adapters.NinjaTrader
private void ExecuteInNT8(StrategyIntent intent, SizingResult sizing) private void ExecuteInNT8(StrategyIntent intent, SizingResult sizing)
{ {
if (intent == null) if (intent == null)
{
throw new ArgumentNullException("intent"); throw new ArgumentNullException("intent");
}
if (sizing == null) if (sizing == null)
{
throw new ArgumentNullException("sizing"); throw new ArgumentNullException("sizing");
}
// This is where the actual NT8 order execution would happen var signalName = string.Format("SDK_{0}_{1}", intent.Symbol, intent.Side);
// In a real implementation, this would call NT8's EnterLong/EnterShort methods
// along with SetStopLoss, SetProfitTarget, etc. 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) lock (_lock)
{ {
@@ -151,28 +167,6 @@ namespace NT8.Adapters.NinjaTrader
intent.TargetTicks, intent.TargetTicks,
DateTime.UtcNow)); 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> /// <summary>

View File

@@ -11,7 +11,6 @@ using NinjaTrader.Gui.Tools;
using NinjaTrader.NinjaScript; using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Indicators; using NinjaTrader.NinjaScript.Indicators;
using NinjaTrader.NinjaScript.Strategies; using NinjaTrader.NinjaScript.Strategies;
using Microsoft.Extensions.Logging.Abstractions;
using NT8.Adapters.NinjaTrader; using NT8.Adapters.NinjaTrader;
using NT8.Core.Common.Interfaces; using NT8.Core.Common.Interfaces;
using NT8.Core.Common.Models; using NT8.Core.Common.Models;
@@ -33,7 +32,7 @@ namespace NinjaTrader.NinjaScript.Strategies
/// <summary> /// <summary>
/// Base class for strategies that integrate NT8 SDK components. /// Base class for strategies that integrate NT8 SDK components.
/// </summary> /// </summary>
public abstract class NT8StrategyBase : Strategy public abstract class NT8StrategyBase : Strategy, INT8ExecutionBridge
{ {
private readonly object _lock = new object(); private readonly object _lock = new object();
@@ -54,7 +53,10 @@ namespace NinjaTrader.NinjaScript.Strategies
private int _ordersSubmittedToday; private int _ordersSubmittedToday;
private DateTime _lastBarTime; private DateTime _lastBarTime;
private bool _killSwitchTriggered; private bool _killSwitchTriggered;
private bool _connectionLost;
private ExecutionCircuitBreaker _circuitBreaker; private ExecutionCircuitBreaker _circuitBreaker;
private System.IO.StreamWriter _fileLog;
private readonly object _fileLock = new object();
#region User-Configurable Properties #region User-Configurable Properties
@@ -94,8 +96,59 @@ namespace NinjaTrader.NinjaScript.Strategies
[Display(Name = "Verbose Logging", GroupName = "Debug", Order = 1)] [Display(Name = "Verbose Logging", GroupName = "Debug", Order = 1)]
public bool EnableVerboseLogging { get; set; } 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 #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> /// <summary>
/// Create the SDK strategy instance. /// Create the SDK strategy instance.
/// </summary> /// </summary>
@@ -126,7 +179,7 @@ namespace NinjaTrader.NinjaScript.Strategies
TraceOrders = false; TraceOrders = false;
RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose; RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
StopTargetHandling = StopTargetHandling.PerEntryExecution; StopTargetHandling = StopTargetHandling.PerEntryExecution;
BarsRequiredToTrade = 20; BarsRequiredToTrade = 50;
EnableSDK = true; EnableSDK = true;
DailyLossLimit = 1000.0; DailyLossLimit = 1000.0;
@@ -137,7 +190,12 @@ namespace NinjaTrader.NinjaScript.Strategies
MaxContracts = 10; MaxContracts = 10;
EnableKillSwitch = false; EnableKillSwitch = false;
EnableVerboseLogging = false; EnableVerboseLogging = false;
EnableFileLogging = true;
LogDirectory = string.Empty;
EnableLongTrades = true;
EnableShortTrades = true;
_killSwitchTriggered = false; _killSwitchTriggered = false;
_connectionLost = false;
} }
else if (State == State.DataLoaded) else if (State == State.DataLoaded)
{ {
@@ -152,16 +210,41 @@ namespace NinjaTrader.NinjaScript.Strategies
catch (Exception ex) catch (Exception ex)
{ {
Print(string.Format("[SDK ERROR] Initialization failed: {0}", ex.Message)); Print(string.Format("[SDK ERROR] Initialization failed: {0}", ex.Message));
Log(string.Format("[SDK ERROR] {0}", ex.ToString()), LogLevel.Error); Log(string.Format("[SDK ERROR] {0}", ex.ToString()), NinjaTrader.Cbi.LogLevel.Error);
_sdkInitialized = false; _sdkInitialized = false;
} }
} }
} }
else if (State == State.Realtime)
{
InitFileLog();
WriteSessionHeader();
}
else if (State == State.Terminated)
{
PortfolioRiskManager.Instance.UnregisterStrategy(Name);
WriteSessionFooter();
}
} }
protected override void OnBarUpdate() 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 (EnableKillSwitch)
{ {
if (!_killSwitchTriggered) if (!_killSwitchTriggered)
@@ -178,29 +261,17 @@ namespace NinjaTrader.NinjaScript.Strategies
Print(string.Format("[SDK] Kill switch flatten error: {0}", ex.Message)); Print(string.Format("[SDK] Kill switch flatten error: {0}", ex.Message));
} }
} }
return; return;
} }
if (!_sdkInitialized || _sdkStrategy == null) // Connection loss guard — do not submit new orders if broker is disconnected
if (_connectionLost)
{ {
if (CurrentBar == 0) if (EnableVerboseLogging)
Print(string.Format("[SDK] Not initialized: sdkInit={0}, strategy={1}", _sdkInitialized, _sdkStrategy != null)); Print(string.Format("[NT8-SDK] Bar skipped — connection lost: {0}", Time[0]));
return; 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. // Log first processable bar and every 100th bar.
if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0) if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0)
{ {
@@ -236,7 +307,7 @@ namespace NinjaTrader.NinjaScript.Strategies
_logger.LogError("OnBarUpdate failed: {0}", ex.Message); _logger.LogError("OnBarUpdate failed: {0}", ex.Message);
Print(string.Format("[SDK ERROR] OnBarUpdate: {0}", ex.Message)); Print(string.Format("[SDK ERROR] OnBarUpdate: {0}", ex.Message));
Log(string.Format("[SDK ERROR] {0}", ex.ToString()), LogLevel.Error); Log(string.Format("[SDK ERROR] {0}", ex.ToString()), NinjaTrader.Cbi.LogLevel.Error);
} }
} }
@@ -291,9 +362,144 @@ namespace NinjaTrader.NinjaScript.Strategies
if (string.IsNullOrEmpty(execution.Order.Name) || !execution.Order.Name.StartsWith("SDK_")) if (string.IsNullOrEmpty(execution.Order.Name) || !execution.Order.Name.StartsWith("SDK_"))
return; return;
FileLog(string.Format("FILL {0} {1} @ {2:F2} | OrderId={3}",
execution.MarketPosition,
execution.Quantity,
execution.Price,
execution.OrderId));
var fill = new NT8.Core.Common.Models.OrderFill(
orderId,
execution.Order != null ? execution.Order.Instrument.MasterInstrument.Name : string.Empty,
execution.Quantity,
execution.Price,
time,
0.0,
executionId);
PortfolioRiskManager.Instance.ReportFill(Name, fill);
_executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time); _executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time);
} }
/// <summary>
/// Handles broker connection status changes. Halts new orders on disconnect,
/// logs reconnect, and resets the connection flag when restored.
/// </summary>
protected override void OnConnectionStatusUpdate(
Connection connection,
ConnectionStatus status,
DateTime time)
{
if (connection == null) return;
if (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")));
}
}
else if (status == ConnectionStatus.Disconnected ||
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));
}
}
}
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(string.Format("ConnectionLost : {0}", _connectionLost));
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() private void InitializeSdkComponents()
{ {
_logger = new BasicLogger(Name); _logger = new BasicLogger(Name);
@@ -321,7 +527,7 @@ namespace NinjaTrader.NinjaScript.Strategies
_riskManager = new BasicRiskManager(_logger); _riskManager = new BasicRiskManager(_logger);
_positionSizer = new BasicPositionSizer(_logger); _positionSizer = new BasicPositionSizer(_logger);
_circuitBreaker = new ExecutionCircuitBreaker( _circuitBreaker = new ExecutionCircuitBreaker(
NullLogger<ExecutionCircuitBreaker>.Instance, _logger,
failureThreshold: 3, failureThreshold: 3,
timeout: TimeSpan.FromSeconds(30)); timeout: TimeSpan.FromSeconds(30));
_executionAdapter = new NT8ExecutionAdapter(); _executionAdapter = new NT8ExecutionAdapter();
@@ -332,6 +538,8 @@ namespace NinjaTrader.NinjaScript.Strategies
_sdkStrategy.Initialize(_strategyConfig, null, _logger); _sdkStrategy.Initialize(_strategyConfig, null, _logger);
ConfigureStrategyParameters(); ConfigureStrategyParameters();
PortfolioRiskManager.Instance.RegisterStrategy(Name, _riskConfig);
Print(string.Format("[NT8-SDK] Registered with PortfolioRiskManager: {0}", PortfolioRiskManager.Instance.GetStatusSnapshot()));
_ordersSubmittedToday = 0; _ordersSubmittedToday = 0;
_lastBarTime = DateTime.MinValue; _lastBarTime = DateTime.MinValue;
@@ -355,6 +563,17 @@ namespace NinjaTrader.NinjaScript.Strategies
private StrategyContext BuildStrategyContext() 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>(); var customData = new Dictionary<string, object>();
customData.Add("CurrentBar", CurrentBar); customData.Add("CurrentBar", CurrentBar);
customData.Add("BarsRequiredToTrade", BarsRequiredToTrade); customData.Add("BarsRequiredToTrade", BarsRequiredToTrade);
@@ -362,7 +581,7 @@ namespace NinjaTrader.NinjaScript.Strategies
return NT8DataConverter.ConvertContext( return NT8DataConverter.ConvertContext(
Instrument.MasterInstrument.Name, Instrument.MasterInstrument.Name,
Time[0], etTime,
BuildPositionInfo(), BuildPositionInfo(),
BuildAccountInfo(), BuildAccountInfo(),
BuildSessionInfo(), BuildSessionInfo(),
@@ -371,7 +590,23 @@ namespace NinjaTrader.NinjaScript.Strategies
private AccountInfo BuildAccountInfo() private AccountInfo BuildAccountInfo()
{ {
var accountInfo = NT8DataConverter.ConvertAccount(100000.0, 250000.0, 0.0, 0.0, DateTime.UtcNow); double cashValue = 100000.0;
double buyingPower = 250000.0;
try
{
if (Account != null)
{
cashValue = Account.Get(AccountItem.CashValue, Currency.UsDollar);
buyingPower = Account.Get(AccountItem.BuyingPower, Currency.UsDollar);
}
}
catch (Exception ex)
{
Print(string.Format("[NT8-SDK] WARNING: Could not read live account balance, using defaults: {0}", ex.Message));
}
var accountInfo = NT8DataConverter.ConvertAccount(cashValue, buyingPower, 0.0, 0.0, DateTime.UtcNow);
_lastAccountInfo = accountInfo; _lastAccountInfo = accountInfo;
return accountInfo; return accountInfo;
} }
@@ -392,20 +627,52 @@ namespace NinjaTrader.NinjaScript.Strategies
private MarketSession BuildSessionInfo() private MarketSession BuildSessionInfo()
{ {
if (_currentSession != null && _currentSession.SessionStart.Date == Time[0].Date) DateTime etTime;
return _currentSession; 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 sessionStart = etTime.Date.AddHours(9).AddMinutes(30);
var sessionEnd = Time[0].Date.AddHours(16); var sessionEnd = etTime.Date.AddHours(16);
var isRth = Time[0].Hour >= 9 && Time[0].Hour < 16; var isRth = etTime.TimeOfDay >= TimeSpan.FromHours(9.5)
var sessionName = isRth ? "RTH" : "ETH"; && etTime.TimeOfDay < TimeSpan.FromHours(16.0);
_currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, sessionName); _currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, isRth ? "RTH" : "ETH");
return _currentSession; return _currentSession;
} }
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context) private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context)
{ {
// Portfolio-level risk check — runs before per-strategy risk validation
var portfolioDecision = PortfolioRiskManager.Instance.ValidatePortfolioRisk(Name, intent);
if (!portfolioDecision.Allow)
{
Print(string.Format("[SDK] Portfolio blocked: {0}", portfolioDecision.RejectReason));
if (_logger != null)
_logger.LogWarning("Portfolio risk blocked order: {0}", portfolioDecision.RejectReason);
return;
}
// 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) if (EnableVerboseLogging)
Print(string.Format("[SDK] Validating intent: {0} {1}", intent.Side, intent.Symbol)); Print(string.Format("[SDK] Validating intent: {0} {1}", intent.Side, intent.Symbol));
@@ -462,7 +729,11 @@ namespace NinjaTrader.NinjaScript.Strategies
private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent) private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent)
{ {
// Circuit breaker gate // 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(); var state = _circuitBreaker.GetState();
Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason)); Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason));
@@ -473,7 +744,39 @@ namespace NinjaTrader.NinjaScript.Strategies
try 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); _executionAdapter.SubmitOrder(request, orderName);
if (request.Side == OmsOrderSide.Buy) if (request.Side == OmsOrderSide.Buy)
@@ -542,4 +845,3 @@ namespace NinjaTrader.NinjaScript.Strategies
} }
} }
} }

View File

@@ -12,6 +12,7 @@ using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Indicators; using NinjaTrader.NinjaScript.Indicators;
using NinjaTrader.NinjaScript.Strategies; using NinjaTrader.NinjaScript.Strategies;
using NT8.Core.Common.Interfaces; using NT8.Core.Common.Interfaces;
using NT8.Core.Intelligence;
using NT8.Strategies.Examples; using NT8.Strategies.Examples;
using SdkSimpleORB = NT8.Strategies.Examples.SimpleORBStrategy; using SdkSimpleORB = NT8.Strategies.Examples.SimpleORBStrategy;
@@ -23,6 +24,7 @@ namespace NinjaTrader.NinjaScript.Strategies
public class SimpleORBNT8 : NT8StrategyBase public class SimpleORBNT8 : NT8StrategyBase
{ {
[NinjaScriptProperty] [NinjaScriptProperty]
[Optimizable]
[Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)] [Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)]
[Range(5, 120)] [Range(5, 120)]
public int OpeningRangeMinutes { get; set; } public int OpeningRangeMinutes { get; set; }
@@ -33,11 +35,13 @@ namespace NinjaTrader.NinjaScript.Strategies
public double StdDevMultiplier { get; set; } public double StdDevMultiplier { get; set; }
[NinjaScriptProperty] [NinjaScriptProperty]
[Optimizable]
[Display(Name = "Stop Loss Ticks", GroupName = "ORB Risk", Order = 1)] [Display(Name = "Stop Loss Ticks", GroupName = "ORB Risk", Order = 1)]
[Range(1, 50)] [Range(1, 50)]
public int StopTicks { get; set; } public int StopTicks { get; set; }
[NinjaScriptProperty] [NinjaScriptProperty]
[Optimizable]
[Display(Name = "Profit Target Ticks", GroupName = "ORB Risk", Order = 2)] [Display(Name = "Profit Target Ticks", GroupName = "ORB Risk", Order = 2)]
[Range(1, 100)] [Range(1, 100)]
public int TargetTicks { get; set; } public int TargetTicks { get; set; }
@@ -49,6 +53,8 @@ namespace NinjaTrader.NinjaScript.Strategies
Name = "Simple ORB NT8"; Name = "Simple ORB NT8";
Description = "Opening Range Breakout with NT8 SDK integration"; Description = "Opening Range Breakout with NT8 SDK integration";
// Daily bar series is added automatically via AddDataSeries in Configure.
OpeningRangeMinutes = 30; OpeningRangeMinutes = 30;
StdDevMultiplier = 1.0; StdDevMultiplier = 1.0;
StopTicks = 8; StopTicks = 8;
@@ -63,11 +69,29 @@ namespace NinjaTrader.NinjaScript.Strategies
Calculate = Calculate.OnBarClose; Calculate = Calculate.OnBarClose;
BarsRequiredToTrade = 50; BarsRequiredToTrade = 50;
EnableLongTrades = true;
// Long-only: short trades permanently disabled pending backtest confirmation
EnableShortTrades = false;
}
else if (State == State.Configure)
{
AddDataSeries(BarsPeriodType.Day, 1);
} }
base.OnStateChange(); 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() protected override IStrategy CreateSdkStrategy()
{ {
return new SdkSimpleORB(OpeningRangeMinutes, StdDevMultiplier); return new SdkSimpleORB(OpeningRangeMinutes, StdDevMultiplier);
@@ -98,15 +122,82 @@ namespace NinjaTrader.NinjaScript.Strategies
_strategyConfig.Parameters["TargetTicks"] = TargetTicks; _strategyConfig.Parameters["TargetTicks"] = TargetTicks;
_strategyConfig.Parameters["OpeningRangeMinutes"] = OpeningRangeMinutes; _strategyConfig.Parameters["OpeningRangeMinutes"] = OpeningRangeMinutes;
if (Instrument != null && Instrument.MasterInstrument != null)
{
_strategyConfig.Parameters["TickSize"] = Instrument.MasterInstrument.TickSize;
}
if (_logger != null) if (_logger != null)
{ {
_logger.LogInformation( _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, OpeningRangeMinutes,
StopTicks, 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;
}
} }
} }

View File

@@ -1,8 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
[assembly: InternalsVisibleTo("NT8.Core.Tests")]
[assembly: InternalsVisibleTo("NT8.Integration.Tests")]
namespace NT8.Core.Execution namespace NT8.Core.Execution
{ {
/// <summary> /// <summary>
@@ -11,6 +15,7 @@ namespace NT8.Core.Execution
public class ExecutionCircuitBreaker public class ExecutionCircuitBreaker
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly NT8.Core.Logging.ILogger _sdkLogger;
private readonly object _lock = new object(); private readonly object _lock = new object();
private CircuitBreakerStatus _status; private CircuitBreakerStatus _status;
@@ -21,24 +26,49 @@ namespace NT8.Core.Execution
private readonly int _failureThreshold; private readonly int _failureThreshold;
private readonly TimeSpan _retryTimeout; private readonly TimeSpan _retryTimeout;
// Track execution times for latency monitoring
private readonly Queue<TimeSpan> _executionTimes; private readonly Queue<TimeSpan> _executionTimes;
private readonly int _latencyWindowSize; private readonly int _latencyWindowSize;
// Track order rejections
private readonly Queue<DateTime> _rejectionTimes; private readonly Queue<DateTime> _rejectionTimes;
private readonly int _rejectionWindowSize; private readonly int _rejectionWindowSize;
// Log helpers — route through whichever logger is available
private void LogDebug(string message) { if (_logger != null) _logger.LogDebug(message); else if (_sdkLogger != null) _sdkLogger.LogDebug(message); }
private void LogInfo(string message) { if (_logger != null) _logger.LogInformation(message); else if (_sdkLogger != null) _sdkLogger.LogInformation(message); }
private void LogWarn(string message) { if (_logger != null) _logger.LogWarning(message); else if (_sdkLogger != null) _sdkLogger.LogWarning(message); }
private void LogErr(string message) { if (_logger != null) _logger.LogError(message); else if (_sdkLogger != null) _sdkLogger.LogError(message); }
/// <summary> /// <summary>
/// Constructor for ExecutionCircuitBreaker /// Constructor accepting NT8.Core.Logging.ILogger.
/// Use this overload from NinjaScript (.cs) files — no Microsoft.Extensions.Logging reference required.
/// </summary> /// </summary>
/// <param name="logger">Logger instance</param>
/// <param name="failureThreshold">Number of failures to trigger circuit breaker</param>
/// <param name="timeout">How long to stay open before half-open</param>
/// <param name="retryTimeout">Time to wait between retries</param>
/// <param name="latencyWindowSize">Size of latency tracking window</param>
/// <param name="rejectionWindowSize">Size of rejection tracking window</param>
public ExecutionCircuitBreaker( public ExecutionCircuitBreaker(
NT8.Core.Logging.ILogger sdkLogger,
int failureThreshold = 3,
TimeSpan? timeout = null,
TimeSpan? retryTimeout = null,
int latencyWindowSize = 100,
int rejectionWindowSize = 10)
{
_sdkLogger = sdkLogger;
_logger = null;
_status = CircuitBreakerStatus.Closed;
_failureCount = 0;
_lastFailureTime = DateTime.MinValue;
_timeout = timeout ?? TimeSpan.FromSeconds(30);
_retryTimeout = retryTimeout ?? TimeSpan.FromSeconds(5);
_failureThreshold = failureThreshold;
_latencyWindowSize = latencyWindowSize;
_rejectionWindowSize = rejectionWindowSize;
_executionTimes = new Queue<TimeSpan>();
_rejectionTimes = new Queue<DateTime>();
}
/// <summary>
/// Constructor accepting Microsoft.Extensions.Logging.ILogger.
/// Use this overload from DLL projects and unit tests.
/// </summary>
internal ExecutionCircuitBreaker(
ILogger<ExecutionCircuitBreaker> logger, ILogger<ExecutionCircuitBreaker> logger,
int failureThreshold = 3, int failureThreshold = 3,
TimeSpan? timeout = null, TimeSpan? timeout = null,
@@ -50,6 +80,7 @@ namespace NT8.Core.Execution
throw new ArgumentNullException("logger"); throw new ArgumentNullException("logger");
_logger = logger; _logger = logger;
_sdkLogger = null;
_status = CircuitBreakerStatus.Closed; _status = CircuitBreakerStatus.Closed;
_failureCount = 0; _failureCount = 0;
_lastFailureTime = DateTime.MinValue; _lastFailureTime = DateTime.MinValue;
@@ -58,15 +89,11 @@ namespace NT8.Core.Execution
_failureThreshold = failureThreshold; _failureThreshold = failureThreshold;
_latencyWindowSize = latencyWindowSize; _latencyWindowSize = latencyWindowSize;
_rejectionWindowSize = rejectionWindowSize; _rejectionWindowSize = rejectionWindowSize;
_executionTimes = new Queue<TimeSpan>(); _executionTimes = new Queue<TimeSpan>();
_rejectionTimes = new Queue<DateTime>(); _rejectionTimes = new Queue<DateTime>();
} }
/// <summary> /// <summary>Records execution time for latency monitoring.</summary>
/// Records execution time for monitoring
/// </summary>
/// <param name="latency">Execution latency</param>
public void RecordExecutionTime(TimeSpan latency) public void RecordExecutionTime(TimeSpan latency)
{ {
try try
@@ -74,31 +101,21 @@ namespace NT8.Core.Execution
lock (_lock) lock (_lock)
{ {
_executionTimes.Enqueue(latency); _executionTimes.Enqueue(latency);
// Keep only the last N measurements
while (_executionTimes.Count > _latencyWindowSize) while (_executionTimes.Count > _latencyWindowSize)
{
_executionTimes.Dequeue(); _executionTimes.Dequeue();
}
// Check if we have excessive latency
if (_status == CircuitBreakerStatus.Closed && HasExcessiveLatency()) if (_status == CircuitBreakerStatus.Closed && HasExcessiveLatency())
{
TripCircuitBreaker("Excessive execution latency detected"); TripCircuitBreaker("Excessive execution latency detected");
} }
} }
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Failed to record execution time: {Message}", ex.Message); LogErr(string.Format("Failed to record execution time: {0}", ex.Message));
throw; throw;
} }
} }
/// <summary> /// <summary>Records an order rejection.</summary>
/// Records order rejection for monitoring
/// </summary>
/// <param name="reason">Reason for rejection</param>
public void RecordOrderRejection(string reason) public void RecordOrderRejection(string reason)
{ {
if (string.IsNullOrEmpty(reason)) if (string.IsNullOrEmpty(reason))
@@ -109,31 +126,21 @@ namespace NT8.Core.Execution
lock (_lock) lock (_lock)
{ {
_rejectionTimes.Enqueue(DateTime.UtcNow); _rejectionTimes.Enqueue(DateTime.UtcNow);
// Keep only the last N rejections
while (_rejectionTimes.Count > _rejectionWindowSize) while (_rejectionTimes.Count > _rejectionWindowSize)
{
_rejectionTimes.Dequeue(); _rejectionTimes.Dequeue();
}
// Check if we have excessive rejections
if (_status == CircuitBreakerStatus.Closed && HasExcessiveRejections()) if (_status == CircuitBreakerStatus.Closed && HasExcessiveRejections())
{ TripCircuitBreaker(string.Format("Excessive order rejections: {0}", reason));
TripCircuitBreaker(String.Format("Excessive order rejections: {0}", reason));
}
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Failed to record order rejection: {Message}", ex.Message); LogErr(string.Format("Failed to record order rejection: {0}", ex.Message));
throw; throw;
} }
} }
/// <summary> /// <summary>Returns true if an order should be allowed through.</summary>
/// Determines if an order should be allowed based on circuit breaker state
/// </summary>
/// <returns>True if order should be allowed, false otherwise</returns>
public bool ShouldAllowOrder() public bool ShouldAllowOrder()
{ {
try try
@@ -143,26 +150,20 @@ namespace NT8.Core.Execution
switch (_status) switch (_status)
{ {
case CircuitBreakerStatus.Closed: case CircuitBreakerStatus.Closed:
// Normal operation
return true; return true;
case CircuitBreakerStatus.Open: case CircuitBreakerStatus.Open:
// Check if we should transition to half-open
if (DateTime.UtcNow >= _nextRetryTime) if (DateTime.UtcNow >= _nextRetryTime)
{ {
_status = CircuitBreakerStatus.HalfOpen; _status = CircuitBreakerStatus.HalfOpen;
_logger.LogWarning("Circuit breaker transitioning to Half-Open state"); LogWarn("Circuit breaker transitioning to Half-Open state");
return true; // Allow one test order return true;
}
else
{
_logger.LogDebug("Circuit breaker is Open - blocking order");
return false; // Block orders
} }
LogDebug("Circuit breaker is Open - blocking order");
return false;
case CircuitBreakerStatus.HalfOpen: case CircuitBreakerStatus.HalfOpen:
// In half-open, allow limited operations to test if system recovered LogDebug("Circuit breaker is Half-Open - allowing test order");
_logger.LogDebug("Circuit breaker is Half-Open - allowing test order");
return true; return true;
default: default:
@@ -172,15 +173,12 @@ namespace NT8.Core.Execution
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Failed to check if order should be allowed: {Message}", ex.Message); LogErr(string.Format("Failed to check ShouldAllowOrder: {0}", ex.Message));
throw; throw;
} }
} }
/// <summary> /// <summary>Returns the current circuit breaker state.</summary>
/// Gets the current state of the circuit breaker
/// </summary>
/// <returns>Current circuit breaker state</returns>
public CircuitBreakerState GetState() public CircuitBreakerState GetState()
{ {
try try
@@ -191,20 +189,17 @@ namespace NT8.Core.Execution
_status != CircuitBreakerStatus.Closed, _status != CircuitBreakerStatus.Closed,
_status, _status,
GetStatusReason(), GetStatusReason(),
_failureCount _failureCount);
);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Failed to get circuit breaker state: {Message}", ex.Message); LogErr(string.Format("Failed to get state: {0}", ex.Message));
throw; throw;
} }
} }
/// <summary> /// <summary>Resets the circuit breaker to Closed state.</summary>
/// Resets the circuit breaker to closed state
/// </summary>
public void Reset() public void Reset()
{ {
try try
@@ -214,20 +209,17 @@ namespace NT8.Core.Execution
_status = CircuitBreakerStatus.Closed; _status = CircuitBreakerStatus.Closed;
_failureCount = 0; _failureCount = 0;
_lastFailureTime = DateTime.MinValue; _lastFailureTime = DateTime.MinValue;
LogInfo("Circuit breaker reset to Closed state");
_logger.LogInformation("Circuit breaker reset to Closed state");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Failed to reset circuit breaker: {Message}", ex.Message); LogErr(string.Format("Failed to reset circuit breaker: {0}", ex.Message));
throw; throw;
} }
} }
/// <summary> /// <summary>Call after a successful order submission.</summary>
/// Called when an operation succeeds while in Half-Open state
/// </summary>
public void OnSuccess() public void OnSuccess()
{ {
try try
@@ -237,20 +229,18 @@ namespace NT8.Core.Execution
if (_status == CircuitBreakerStatus.HalfOpen) if (_status == CircuitBreakerStatus.HalfOpen)
{ {
Reset(); Reset();
_logger.LogInformation("Circuit breaker reset after successful test operation"); LogInfo("Circuit breaker reset after successful test operation");
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Failed to handle success in Half-Open state: {Message}", ex.Message); LogErr(string.Format("Failed to handle OnSuccess: {0}", ex.Message));
throw; throw;
} }
} }
/// <summary> /// <summary>Call after a failed order submission.</summary>
/// Called when an operation fails
/// </summary>
public void OnFailure() public void OnFailure()
{ {
try try
@@ -260,7 +250,6 @@ namespace NT8.Core.Execution
_failureCount++; _failureCount++;
_lastFailureTime = DateTime.UtcNow; _lastFailureTime = DateTime.UtcNow;
// If we're in half-open and fail, go back to open
if (_status == CircuitBreakerStatus.HalfOpen || if (_status == CircuitBreakerStatus.HalfOpen ||
(_status == CircuitBreakerStatus.Closed && _failureCount >= _failureThreshold)) (_status == CircuitBreakerStatus.Closed && _failureCount >= _failureThreshold))
{ {
@@ -270,61 +259,35 @@ namespace NT8.Core.Execution
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Failed to handle failure: {Message}", ex.Message); LogErr(string.Format("Failed to handle OnFailure: {0}", ex.Message));
throw; throw;
} }
} }
/// <summary>
/// Trips the circuit breaker to open state
/// </summary>
/// <param name="reason">Reason for tripping</param>
private void TripCircuitBreaker(string reason) private void TripCircuitBreaker(string reason)
{ {
_status = CircuitBreakerStatus.Open; _status = CircuitBreakerStatus.Open;
_nextRetryTime = DateTime.UtcNow.Add(_timeout); _nextRetryTime = DateTime.UtcNow.Add(_timeout);
LogWarn(string.Format("Circuit breaker TRIPPED: {0}. Will retry at {1}", reason, _nextRetryTime));
_logger.LogWarning("Circuit breaker TRIPPED: {Reason}. Will retry at {Time}",
reason, _nextRetryTime);
} }
/// <summary>
/// Checks if we have excessive execution latency
/// </summary>
/// <returns>True if latency is excessive</returns>
private bool HasExcessiveLatency() private bool HasExcessiveLatency()
{ {
if (_executionTimes.Count < 3) // Need minimum samples if (_executionTimes.Count < 3)
return false; return false;
// Calculate average latency
var avgLatency = TimeSpan.FromMilliseconds(_executionTimes.Average(ts => ts.TotalMilliseconds)); var avgLatency = TimeSpan.FromMilliseconds(_executionTimes.Average(ts => ts.TotalMilliseconds));
// If average latency is more than 5 seconds, consider it excessive
return avgLatency.TotalSeconds > 5.0; return avgLatency.TotalSeconds > 5.0;
} }
/// <summary>
/// Checks if we have excessive order rejections
/// </summary>
/// <returns>True if rejections are excessive</returns>
private bool HasExcessiveRejections() private bool HasExcessiveRejections()
{ {
if (_rejectionTimes.Count < _rejectionWindowSize) if (_rejectionTimes.Count < _rejectionWindowSize)
return false; return false;
var recentWindow = TimeSpan.FromMinutes(1);
// If all recent orders were rejected (100% rejection rate in window)
var recentWindow = TimeSpan.FromMinutes(1); // Check last minute
var recentRejections = _rejectionTimes.Count(dt => DateTime.UtcNow - dt <= recentWindow); var recentRejections = _rejectionTimes.Count(dt => DateTime.UtcNow - dt <= recentWindow);
// If we have maximum possible rejections in the window, it's excessive
return recentRejections >= _rejectionWindowSize; return recentRejections >= _rejectionWindowSize;
} }
/// <summary>
/// Gets the reason for current status
/// </summary>
/// <returns>Reason string</returns>
private string GetStatusReason() private string GetStatusReason()
{ {
switch (_status) switch (_status)
@@ -332,8 +295,7 @@ namespace NT8.Core.Execution
case CircuitBreakerStatus.Closed: case CircuitBreakerStatus.Closed:
return "Normal operation"; return "Normal operation";
case CircuitBreakerStatus.Open: case CircuitBreakerStatus.Open:
return String.Format("Tripped due to failures. Failures: {0}, Last: {1}", return string.Format("Tripped due to failures. Count: {0}, Last: {1}", _failureCount, _lastFailureTime);
_failureCount, _lastFailureTime);
case CircuitBreakerStatus.HalfOpen: case CircuitBreakerStatus.HalfOpen:
return "Testing recovery after timeout"; return "Testing recovery after timeout";
default: default:
@@ -341,10 +303,7 @@ namespace NT8.Core.Execution
} }
} }
/// <summary> /// <summary>Returns average execution latency.</summary>
/// Gets average execution time for monitoring
/// </summary>
/// <returns>Average execution time</returns>
public TimeSpan GetAverageExecutionTime() public TimeSpan GetAverageExecutionTime()
{ {
try try
@@ -353,21 +312,17 @@ namespace NT8.Core.Execution
{ {
if (_executionTimes.Count == 0) if (_executionTimes.Count == 0)
return TimeSpan.Zero; return TimeSpan.Zero;
return TimeSpan.FromMilliseconds(_executionTimes.Average(ts => ts.TotalMilliseconds)); return TimeSpan.FromMilliseconds(_executionTimes.Average(ts => ts.TotalMilliseconds));
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Failed to get average execution time: {Message}", ex.Message); LogErr(string.Format("Failed to get average execution time: {0}", ex.Message));
throw; throw;
} }
} }
/// <summary> /// <summary>Returns rejection rate as a percentage.</summary>
/// Gets rejection rate for monitoring
/// </summary>
/// <returns>Rejection rate as percentage</returns>
public double GetRejectionRate() public double GetRejectionRate()
{ {
try try
@@ -376,19 +331,14 @@ namespace NT8.Core.Execution
{ {
if (_rejectionTimes.Count == 0) if (_rejectionTimes.Count == 0)
return 0.0; return 0.0;
// Calculate rejections in last minute
var oneMinuteAgo = DateTime.UtcNow.AddMinutes(-1); var oneMinuteAgo = DateTime.UtcNow.AddMinutes(-1);
var recentRejections = _rejectionTimes.Count(dt => dt >= oneMinuteAgo); var recentRejections = _rejectionTimes.Count(dt => dt >= oneMinuteAgo);
// This is a simplified calculation - in practice you'd need to track
// total attempts to calculate accurate rate
return (double)recentRejections / _rejectionWindowSize * 100.0; return (double)recentRejections / _rejectionWindowSize * 100.0;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Failed to get rejection rate: {Message}", ex.Message); LogErr(string.Format("Failed to get rejection rate: {0}", ex.Message));
throw; throw;
} }
} }

View File

@@ -43,6 +43,31 @@ namespace NT8.Core.Intelligence
/// </summary> /// </summary>
Risk = 6, Risk = 6,
/// <summary>
/// Narrow range contraction quality (NR4/NR7 concepts).
/// </summary>
NarrowRange = 7,
/// <summary>
/// Opening range size relative to average daily ATR/range.
/// </summary>
OrbRangeVsAtr = 8,
/// <summary>
/// Alignment between overnight gap direction and trade direction.
/// </summary>
GapDirectionAlignment = 9,
/// <summary>
/// Breakout bar volume strength relative to intraday average volume.
/// </summary>
BreakoutVolumeStrength = 10,
/// <summary>
/// Prior day close location strength in prior day range.
/// </summary>
PriorDayCloseStrength = 11,
/// <summary> /// <summary>
/// Additional custom factor. /// Additional custom factor.
/// </summary> /// </summary>

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NT8.Core.Common.Models; using NT8.Core.Common.Models;
using NT8.Core.Logging;
namespace NT8.Core.Intelligence namespace NT8.Core.Intelligence
{ {
@@ -398,4 +399,625 @@ namespace NT8.Core.Intelligence
return defaultValue; return defaultValue;
} }
} }
/// <summary>
/// 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.
/// </summary>
public struct DailyBarContext
{
/// <summary>Daily high prices, oldest first.</summary>
public double[] Highs;
/// <summary>Daily low prices, oldest first.</summary>
public double[] Lows;
/// <summary>Daily close prices, oldest first.</summary>
public double[] Closes;
/// <summary>Daily open prices, oldest first.</summary>
public double[] Opens;
/// <summary>Daily volume values, oldest first.</summary>
public long[] Volumes;
/// <summary>Number of valid bars populated.</summary>
public int Count;
/// <summary>Today's RTH open price.</summary>
public double TodayOpen;
/// <summary>Volume of the breakout bar (current intraday bar).</summary>
public double BreakoutBarVolume;
/// <summary>Average intraday volume per bar for today's session so far.</summary>
public double AvgIntradayBarVolume;
/// <summary>Trade direction: 1 for long, -1 for short.</summary>
public int TradeDirection;
}
/// <summary>
/// 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.
/// </summary>
public class NarrowRangeFactorCalculator : IFactorCalculator
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the NarrowRangeFactorCalculator class.
/// </summary>
/// <param name="logger">Logger instance.</param>
public NarrowRangeFactorCalculator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
}
/// <summary>
/// Gets the factor type identifier.
/// </summary>
public FactorType Type
{
get { return FactorType.NarrowRange; }
}
/// <summary>
/// Calculates narrow range score. Expects DailyBarContext in
/// intent.Metadata["daily_bars"]. Returns 0.3 if context is missing.
/// </summary>
/// <param name="intent">Current strategy intent.</param>
/// <param name="context">Current strategy context.</param>
/// <param name="bar">Current bar data.</param>
/// <returns>Calculated confluence factor.</returns>
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<string, object>());
}
}
/// <summary>
/// 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.
/// </summary>
public class OrbRangeVsAtrFactorCalculator : IFactorCalculator
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the OrbRangeVsAtrFactorCalculator class.
/// </summary>
/// <param name="logger">Logger instance.</param>
public OrbRangeVsAtrFactorCalculator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
}
/// <summary>
/// Gets the factor type identifier.
/// </summary>
public FactorType Type
{
get { return FactorType.OrbRangeVsAtr; }
}
/// <summary>
/// Calculates ORB range vs ATR score. Expects DailyBarContext in
/// intent.Metadata["daily_bars"] and double in intent.Metadata["orb_range_ticks"].
/// </summary>
/// <param name="intent">Current strategy intent.</param>
/// <param name="context">Current strategy context.</param>
/// <param name="bar">Current bar data.</param>
/// <returns>Calculated confluence factor.</returns>
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<string, object>());
}
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class GapDirectionAlignmentCalculator : IFactorCalculator
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the GapDirectionAlignmentCalculator class.
/// </summary>
/// <param name="logger">Logger instance.</param>
public GapDirectionAlignmentCalculator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
}
/// <summary>
/// Gets the factor type identifier.
/// </summary>
public FactorType Type
{
get { return FactorType.GapDirectionAlignment; }
}
/// <summary>
/// Calculates gap alignment score. Expects DailyBarContext in
/// intent.Metadata["daily_bars"] with TodayOpen and TradeDirection populated.
/// </summary>
/// <param name="intent">Current strategy intent.</param>
/// <param name="context">Current strategy context.</param>
/// <param name="bar">Current bar data.</param>
/// <returns>Calculated confluence factor.</returns>
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<string, object>());
}
}
/// <summary>
/// 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.
/// </summary>
public class BreakoutVolumeStrengthCalculator : IFactorCalculator
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the BreakoutVolumeStrengthCalculator class.
/// </summary>
/// <param name="logger">Logger instance.</param>
public BreakoutVolumeStrengthCalculator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
}
/// <summary>
/// Gets the factor type identifier.
/// </summary>
public FactorType Type
{
get { return FactorType.BreakoutVolumeStrength; }
}
/// <summary>
/// Calculates breakout volume score. Expects DailyBarContext in
/// intent.Metadata["daily_bars"] with BreakoutBarVolume and
/// AvgIntradayBarVolume populated.
/// </summary>
/// <param name="intent">Current strategy intent.</param>
/// <param name="context">Current strategy context.</param>
/// <param name="bar">Current bar data.</param>
/// <returns>Calculated confluence factor.</returns>
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<string, object>());
}
}
/// <summary>
/// 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.
/// </summary>
public class PriorDayCloseStrengthCalculator : IFactorCalculator
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the PriorDayCloseStrengthCalculator class.
/// </summary>
/// <param name="logger">Logger instance.</param>
public PriorDayCloseStrengthCalculator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
}
/// <summary>
/// Gets the factor type identifier.
/// </summary>
public FactorType Type
{
get { return FactorType.PriorDayCloseStrength; }
}
/// <summary>
/// Calculates prior close strength score. Expects DailyBarContext in
/// intent.Metadata["daily_bars"] with at least 2 completed bars and
/// TradeDirection populated.
/// </summary>
/// <param name="intent">Current strategy intent.</param>
/// <param name="context">Current strategy context.</param>
/// <param name="bar">Current bar data.</param>
/// <returns>Calculated confluence factor.</returns>
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<string, object>());
}
}
} }

View File

@@ -4,6 +4,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
// ARCHIVED: This namespace (NT8.Core.Orders) is superseded by NT8.Core.OMS.
// NT8.Core.OMS is the canonical order management implementation used by NT8StrategyBase.
// These files are retained for reference only and are not referenced by any active code.
// Do not add new code here. Do not remove these files until a full audit confirms zero references.
namespace NT8.Core.Orders namespace NT8.Core.Orders
{ {
/// <summary> /// <summary>

View File

@@ -6,6 +6,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
// ARCHIVED: This namespace (NT8.Core.Orders) is superseded by NT8.Core.OMS.
// NT8.Core.OMS is the canonical order management implementation used by NT8StrategyBase.
// These files are retained for reference only and are not referenced by any active code.
// Do not add new code here. Do not remove these files until a full audit confirms zero references.
namespace NT8.Core.Orders namespace NT8.Core.Orders
{ {
/// <summary> /// <summary>

View File

@@ -2,6 +2,11 @@ using NT8.Core.Common.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
// ARCHIVED: This namespace (NT8.Core.Orders) is superseded by NT8.Core.OMS.
// NT8.Core.OMS is the canonical order management implementation used by NT8StrategyBase.
// These files are retained for reference only and are not referenced by any active code.
// Do not add new code here. Do not remove these files until a full audit confirms zero references.
namespace NT8.Core.Orders namespace NT8.Core.Orders
{ {
#region Core Order Models #region Core Order Models

View File

@@ -0,0 +1,265 @@
// File: PortfolioRiskManager.cs
using System;
using System.Collections.Generic;
using NT8.Core.Common.Models;
using NT8.Core.Logging;
namespace NT8.Core.Risk
{
/// <summary>
/// Portfolio-level risk coordinator. Singleton. Enforces cross-strategy
/// daily loss limits, maximum open contract caps, and a portfolio kill switch.
/// Must be registered by each strategy on init and unregistered on terminate.
/// Thread-safe via a single lock object.
/// </summary>
public class PortfolioRiskManager
{
private static readonly object _instanceLock = new object();
private static PortfolioRiskManager _instance;
/// <summary>
/// Gets the singleton instance of PortfolioRiskManager.
/// </summary>
public static PortfolioRiskManager Instance
{
get
{
if (_instance == null)
{
lock (_instanceLock)
{
if (_instance == null)
_instance = new PortfolioRiskManager();
}
}
return _instance;
}
}
private readonly object _lock = new object();
private readonly Dictionary<string, RiskConfig> _registeredStrategies;
private readonly Dictionary<string, double> _strategyPnL;
private readonly Dictionary<string, int> _strategyOpenContracts;
/// <summary>
/// Maximum combined daily loss across all registered strategies before all trading halts.
/// Default: 2000.0
/// </summary>
public double PortfolioDailyLossLimit { get; set; }
/// <summary>
/// Maximum total open contracts across all registered strategies simultaneously.
/// Default: 6
/// </summary>
public int MaxTotalOpenContracts { get; set; }
/// <summary>
/// When true, all new orders across all strategies are blocked immediately.
/// Set to true to perform an emergency halt of the entire portfolio.
/// </summary>
public bool PortfolioKillSwitch { get; set; }
private PortfolioRiskManager()
{
_registeredStrategies = new Dictionary<string, RiskConfig>();
_strategyPnL = new Dictionary<string, double>();
_strategyOpenContracts = new Dictionary<string, int>();
PortfolioDailyLossLimit = 2000.0;
MaxTotalOpenContracts = 6;
PortfolioKillSwitch = false;
}
/// <summary>
/// Registers a strategy with the portfolio manager. Called from
/// NT8StrategyBase.InitializeSdkComponents() during State.DataLoaded.
/// </summary>
/// <param name="strategyId">Unique strategy identifier (use Name from NT8StrategyBase).</param>
/// <param name="config">The strategy's risk configuration.</param>
/// <exception cref="ArgumentNullException">strategyId or config is null.</exception>
public void RegisterStrategy(string strategyId, RiskConfig config)
{
if (string.IsNullOrEmpty(strategyId)) throw new ArgumentNullException("strategyId");
if (config == null) throw new ArgumentNullException("config");
lock (_lock)
{
_registeredStrategies[strategyId] = config;
if (!_strategyPnL.ContainsKey(strategyId))
_strategyPnL[strategyId] = 0.0;
if (!_strategyOpenContracts.ContainsKey(strategyId))
_strategyOpenContracts[strategyId] = 0;
}
}
/// <summary>
/// Unregisters a strategy. Called from NT8StrategyBase during State.Terminated.
/// </summary>
/// <param name="strategyId">Strategy identifier to unregister.</param>
public void UnregisterStrategy(string strategyId)
{
if (string.IsNullOrEmpty(strategyId)) return;
lock (_lock)
{
_registeredStrategies.Remove(strategyId);
_strategyPnL.Remove(strategyId);
_strategyOpenContracts.Remove(strategyId);
}
}
/// <summary>
/// Validates a new order intent against portfolio-level risk limits.
/// Called before per-strategy risk validation in ProcessStrategyIntent().
/// </summary>
/// <param name="strategyId">The strategy requesting the order.</param>
/// <param name="intent">The trade intent to validate.</param>
/// <returns>RiskDecision indicating whether the order is allowed.</returns>
public RiskDecision ValidatePortfolioRisk(string strategyId, StrategyIntent intent)
{
if (string.IsNullOrEmpty(strategyId)) throw new ArgumentNullException("strategyId");
if (intent == null) throw new ArgumentNullException("intent");
lock (_lock)
{
// Kill switch — blocks everything immediately
if (PortfolioKillSwitch)
{
var ksMetrics = new Dictionary<string, object>();
ksMetrics.Add("kill_switch", true);
return new RiskDecision(
allow: false,
rejectReason: "Portfolio kill switch is active — all trading halted",
modifiedIntent: null,
riskLevel: RiskLevel.Critical,
riskMetrics: ksMetrics);
}
// Portfolio daily loss limit
double totalPnL = 0.0;
foreach (var kvp in _strategyPnL)
totalPnL += kvp.Value;
if (totalPnL <= -PortfolioDailyLossLimit)
{
var pnlMetrics = new Dictionary<string, object>();
pnlMetrics.Add("portfolio_pnl", totalPnL);
pnlMetrics.Add("limit", PortfolioDailyLossLimit);
return new RiskDecision(
allow: false,
rejectReason: String.Format(
"Portfolio daily loss limit breached: {0:C} <= -{1:C}",
totalPnL, PortfolioDailyLossLimit),
modifiedIntent: null,
riskLevel: RiskLevel.Critical,
riskMetrics: pnlMetrics);
}
// Total open contract cap
int totalContracts = 0;
foreach (var kvp in _strategyOpenContracts)
totalContracts += kvp.Value;
if (totalContracts >= MaxTotalOpenContracts)
{
var contractMetrics = new Dictionary<string, object>();
contractMetrics.Add("total_contracts", totalContracts);
contractMetrics.Add("limit", MaxTotalOpenContracts);
return new RiskDecision(
allow: false,
rejectReason: String.Format(
"Portfolio contract cap reached: {0} >= {1}",
totalContracts, MaxTotalOpenContracts),
modifiedIntent: null,
riskLevel: RiskLevel.High,
riskMetrics: contractMetrics);
}
// All portfolio checks passed
var okMetrics = new Dictionary<string, object>();
okMetrics.Add("portfolio_pnl", totalPnL);
okMetrics.Add("total_contracts", totalContracts);
return new RiskDecision(
allow: true,
rejectReason: null,
modifiedIntent: null,
riskLevel: RiskLevel.Low,
riskMetrics: okMetrics);
}
}
/// <summary>
/// Reports a fill to the portfolio manager. Updates open contract count for the strategy.
/// Called from NT8StrategyBase.OnExecutionUpdate() after each fill.
/// </summary>
/// <param name="strategyId">Strategy that received the fill.</param>
/// <param name="fill">Fill details.</param>
public void ReportFill(string strategyId, OrderFill fill)
{
if (string.IsNullOrEmpty(strategyId) || fill == null) return;
lock (_lock)
{
if (!_strategyOpenContracts.ContainsKey(strategyId))
_strategyOpenContracts[strategyId] = 0;
_strategyOpenContracts[strategyId] += fill.Quantity;
if (_strategyOpenContracts[strategyId] < 0)
_strategyOpenContracts[strategyId] = 0;
}
}
/// <summary>
/// Reports a P&L update for a strategy. Called from NT8StrategyBase
/// whenever the strategy's realized P&L changes (typically on position close).
/// </summary>
/// <param name="strategyId">Strategy reporting P&L.</param>
/// <param name="pnl">Current cumulative day P&L for this strategy.</param>
public void ReportPnL(string strategyId, double pnl)
{
if (string.IsNullOrEmpty(strategyId)) return;
lock (_lock)
{
_strategyPnL[strategyId] = pnl;
}
}
/// <summary>
/// Resets daily P&L accumulators for all strategies. Does not clear registrations
/// or open contract counts. Typically called at the start of a new trading day.
/// </summary>
public void ResetDaily()
{
lock (_lock)
{
var keys = new List<string>(_strategyPnL.Keys);
foreach (var key in keys)
_strategyPnL[key] = 0.0;
}
}
/// <summary>
/// Returns a snapshot of current portfolio state for diagnostics.
/// </summary>
public string GetStatusSnapshot()
{
lock (_lock)
{
double totalPnL = 0.0;
foreach (var kvp in _strategyPnL)
totalPnL += kvp.Value;
int totalContracts = 0;
foreach (var kvp in _strategyOpenContracts)
totalContracts += kvp.Value;
return String.Format(
"Portfolio: strategies={0} totalPnL={1:C} totalContracts={2} killSwitch={3}",
_registeredStrategies.Count,
totalPnL,
totalContracts,
PortfolioKillSwitch);
}
}
}
}

View File

@@ -19,6 +19,7 @@ namespace NT8.Strategies.Examples
private readonly double _stdDevMultiplier; private readonly double _stdDevMultiplier;
private ILogger _logger; private ILogger _logger;
private StrategyConfig _config;
private ConfluenceScorer _scorer; private ConfluenceScorer _scorer;
private GradeFilter _gradeFilter; private GradeFilter _gradeFilter;
private RiskModeManager _riskModeManager; private RiskModeManager _riskModeManager;
@@ -98,6 +99,7 @@ namespace NT8.Strategies.Examples
try try
{ {
_logger = logger; _logger = logger;
_config = config;
_scorer = new ConfluenceScorer(_logger, 500); _scorer = new ConfluenceScorer(_logger, 500);
_gradeFilter = new GradeFilter(); _gradeFilter = new GradeFilter();
_riskModeManager = new RiskModeManager(_logger); _riskModeManager = new RiskModeManager(_logger);
@@ -110,6 +112,11 @@ namespace NT8.Strategies.Examples
_factorCalculators.Add(new VolatilityRegimeFactorCalculator()); _factorCalculators.Add(new VolatilityRegimeFactorCalculator());
_factorCalculators.Add(new TimeInSessionFactorCalculator()); _factorCalculators.Add(new TimeInSessionFactorCalculator());
_factorCalculators.Add(new ExecutionQualityFactorCalculator()); _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( _logger.LogInformation(
"SimpleORBStrategy initialized with OR period {0} minutes and multiplier {1:F2}", "SimpleORBStrategy initialized with OR period {0} minutes and multiplier {1:F2}",
@@ -151,6 +158,10 @@ namespace NT8.Strategies.Examples
ResetSession(context.Session != null ? context.Session.SessionStart : context.CurrentTime.Date); ResetSession(context.Session != null ? context.Session.SessionStart : context.CurrentTime.Date);
} }
// Only trade during RTH
if (context.Session == null || !context.Session.IsRth)
return null;
if (bar.Time <= _openingRangeEnd) if (bar.Time <= _openingRangeEnd)
{ {
UpdateOpeningRange(bar); UpdateOpeningRange(bar);
@@ -185,6 +196,8 @@ namespace NT8.Strategies.Examples
if (candidate == null) if (candidate == null)
return null; return null;
AttachDailyBarContext(candidate, bar, context);
var score = _scorer.CalculateScore(candidate, context, bar, _factorCalculators); var score = _scorer.CalculateScore(candidate, context, bar, _factorCalculators);
var mode = _riskModeManager.GetCurrentMode(); var mode = _riskModeManager.GetCurrentMode();
@@ -332,10 +345,35 @@ namespace NT8.Strategies.Examples
private StrategyIntent CreateIntent(string symbol, OrderSide side, double openingRange, double lastPrice) private StrategyIntent CreateIntent(string symbol, OrderSide side, double openingRange, double lastPrice)
{ {
var stopTicks = _config != null && _config.Parameters.ContainsKey("StopTicks")
? (int)_config.Parameters["StopTicks"]
: 8;
var targetTicks = _config != null && _config.Parameters.ContainsKey("TargetTicks")
? (int)_config.Parameters["TargetTicks"]
: 16;
var metadata = new Dictionary<string, object>(); var metadata = new Dictionary<string, object>();
metadata.Add("orb_high", _openingRangeHigh); metadata.Add("orb_high", _openingRangeHigh);
metadata.Add("orb_low", _openingRangeLow); metadata.Add("orb_low", _openingRangeLow);
metadata.Add("orb_range", openingRange); 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("trigger_price", lastPrice);
metadata.Add("multiplier", _stdDevMultiplier); metadata.Add("multiplier", _stdDevMultiplier);
metadata.Add("opening_range_start", _openingRangeStart); metadata.Add("opening_range_start", _openingRangeStart);
@@ -346,11 +384,46 @@ namespace NT8.Strategies.Examples
side, side,
OrderType.Market, OrderType.Market,
null, null,
8, stopTicks,
16, targetTicks,
0.75, 0.75,
"ORB breakout signal", "ORB breakout signal",
metadata); 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;
}
} }
} }

View File

@@ -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<string, object>());
}
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<string, object>());
}
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;
}
}
}

View File

@@ -0,0 +1,117 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NT8.Core.Common.Models;
using NT8.Core.Risk;
namespace NT8.Core.Tests.Risk
{
[TestClass]
public class PortfolioRiskManagerTests
{
private PortfolioRiskManager _manager;
[TestInitialize]
public void TestInitialize()
{
_manager = PortfolioRiskManager.Instance;
}
[TestCleanup]
public void TestCleanup()
{
_manager.UnregisterStrategy("strat1");
_manager.UnregisterStrategy("strat2");
_manager.UnregisterStrategy("strat3");
_manager.UnregisterStrategy("strat4");
_manager.UnregisterStrategy("strat5");
_manager.PortfolioKillSwitch = false;
_manager.PortfolioDailyLossLimit = 2000.0;
_manager.MaxTotalOpenContracts = 6;
_manager.ResetDaily();
}
[TestMethod]
public void PortfolioDailyLossLimit_WhenBreached_BlocksNewOrder()
{
// Arrange
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
_manager.PortfolioDailyLossLimit = 500;
_manager.ReportPnL("strat1", -501);
var intent = TestDataBuilder.CreateValidIntent();
// Act
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsFalse(decision.Allow);
}
[TestMethod]
public void MaxTotalOpenContracts_WhenAtCap_BlocksNewOrder()
{
// Arrange
_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);
var intent = TestDataBuilder.CreateValidIntent();
// Act
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsFalse(decision.Allow);
}
[TestMethod]
public void PortfolioKillSwitch_WhenTrue_BlocksAllOrders()
{
// Arrange
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
_manager.PortfolioKillSwitch = true;
var intent = TestDataBuilder.CreateValidIntent();
// Act
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsFalse(decision.Allow);
Assert.IsTrue(decision.RejectReason.ToLowerInvariant().Contains("kill switch"));
}
[TestMethod]
public void ValidatePortfolioRisk_WhenWithinLimits_Passes()
{
// Arrange
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
var intent = TestDataBuilder.CreateValidIntent();
// Act
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsTrue(decision.Allow);
}
[TestMethod]
public void ResetDaily_ClearsPnL_UnblocksTrading()
{
// Arrange
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
_manager.PortfolioDailyLossLimit = 500;
_manager.ReportPnL("strat1", -600);
var intent = TestDataBuilder.CreateValidIntent();
// Act
var blocked = _manager.ValidatePortfolioRisk("strat1", intent);
_manager.ResetDaily();
var unblocked = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsFalse(blocked.Allow);
Assert.IsTrue(unblocked.Allow);
}
}
}

View File

@@ -15,11 +15,20 @@ namespace NT8.Integration.Tests
[TestClass] [TestClass]
public class NT8OrderAdapterIntegrationTests 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] [TestMethod]
public void Initialize_NullRiskManager_ThrowsArgumentNullException() public void Initialize_NullRiskManager_ThrowsArgumentNullException()
{ {
// Arrange // Arrange
var adapter = new NT8OrderAdapter(); var adapter = new NT8OrderAdapter(new FakeBridge());
var sizer = new TestPositionSizer(1); var sizer = new TestPositionSizer(1);
// Act / Assert // Act / Assert
@@ -31,7 +40,7 @@ namespace NT8.Integration.Tests
public void Initialize_NullPositionSizer_ThrowsArgumentNullException() public void Initialize_NullPositionSizer_ThrowsArgumentNullException()
{ {
// Arrange // Arrange
var adapter = new NT8OrderAdapter(); var adapter = new NT8OrderAdapter(new FakeBridge());
var risk = new TestRiskManager(true); var risk = new TestRiskManager(true);
// Act / Assert // Act / Assert
@@ -43,7 +52,7 @@ namespace NT8.Integration.Tests
public void ExecuteIntent_NotInitialized_ThrowsInvalidOperationException() public void ExecuteIntent_NotInitialized_ThrowsInvalidOperationException()
{ {
// Arrange // Arrange
var adapter = new NT8OrderAdapter(); var adapter = new NT8OrderAdapter(new FakeBridge());
// Act / Assert // Act / Assert
Assert.ThrowsException<InvalidOperationException>( Assert.ThrowsException<InvalidOperationException>(
@@ -54,7 +63,7 @@ namespace NT8.Integration.Tests
public void ExecuteIntent_RiskRejected_DoesNotRecordExecution() public void ExecuteIntent_RiskRejected_DoesNotRecordExecution()
{ {
// Arrange // Arrange
var adapter = new NT8OrderAdapter(); var adapter = new NT8OrderAdapter(new FakeBridge());
var risk = new TestRiskManager(false); var risk = new TestRiskManager(false);
var sizer = new TestPositionSizer(3); var sizer = new TestPositionSizer(3);
adapter.Initialize(risk, sizer); adapter.Initialize(risk, sizer);
@@ -71,7 +80,7 @@ namespace NT8.Integration.Tests
public void ExecuteIntent_AllowedAndSized_RecordsExecution() public void ExecuteIntent_AllowedAndSized_RecordsExecution()
{ {
// Arrange // Arrange
var adapter = new NT8OrderAdapter(); var adapter = new NT8OrderAdapter(new FakeBridge());
var risk = new TestRiskManager(true); var risk = new TestRiskManager(true);
var sizer = new TestPositionSizer(4); var sizer = new TestPositionSizer(4);
adapter.Initialize(risk, sizer); adapter.Initialize(risk, sizer);
@@ -94,7 +103,7 @@ namespace NT8.Integration.Tests
public void GetExecutionHistory_ReturnsCopy_NotMutableInternalReference() public void GetExecutionHistory_ReturnsCopy_NotMutableInternalReference()
{ {
// Arrange // Arrange
var adapter = new NT8OrderAdapter(); var adapter = new NT8OrderAdapter(new FakeBridge());
var risk = new TestRiskManager(true); var risk = new TestRiskManager(true);
var sizer = new TestPositionSizer(2); var sizer = new TestPositionSizer(2);
adapter.Initialize(risk, sizer); adapter.Initialize(risk, sizer);
@@ -113,7 +122,7 @@ namespace NT8.Integration.Tests
public void OnOrderUpdate_EmptyOrderId_ThrowsArgumentException() public void OnOrderUpdate_EmptyOrderId_ThrowsArgumentException()
{ {
// Arrange // Arrange
var adapter = new NT8OrderAdapter(); var adapter = new NT8OrderAdapter(new FakeBridge());
// Act / Assert // Act / Assert
Assert.ThrowsException<ArgumentException>( Assert.ThrowsException<ArgumentException>(
@@ -124,7 +133,7 @@ namespace NT8.Integration.Tests
public void OnExecutionUpdate_EmptyExecutionId_ThrowsArgumentException() public void OnExecutionUpdate_EmptyExecutionId_ThrowsArgumentException()
{ {
// Arrange // Arrange
var adapter = new NT8OrderAdapter(); var adapter = new NT8OrderAdapter(new FakeBridge());
// Act / Assert // Act / Assert
Assert.ThrowsException<ArgumentException>( Assert.ThrowsException<ArgumentException>(
@@ -135,7 +144,7 @@ namespace NT8.Integration.Tests
public void OnExecutionUpdate_EmptyOrderId_ThrowsArgumentException() public void OnExecutionUpdate_EmptyOrderId_ThrowsArgumentException()
{ {
// Arrange // Arrange
var adapter = new NT8OrderAdapter(); var adapter = new NT8OrderAdapter(new FakeBridge());
// Act / Assert // Act / Assert
Assert.ThrowsException<ArgumentException>( Assert.ThrowsException<ArgumentException>(