998 lines
40 KiB
C#
998 lines
40 KiB
C#
// File: NT8StrategyBase.cs
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using NinjaTrader.Cbi;
|
|
using NinjaTrader.Data;
|
|
using NinjaTrader.Gui;
|
|
using NinjaTrader.Gui.Chart;
|
|
using NinjaTrader.Gui.Tools;
|
|
using NinjaTrader.NinjaScript;
|
|
using NinjaTrader.NinjaScript.Indicators;
|
|
using NinjaTrader.NinjaScript.Strategies;
|
|
using NT8.Adapters.NinjaTrader;
|
|
using NT8.Core.Common.Interfaces;
|
|
using NT8.Core.Common.Models;
|
|
using NT8.Core.Execution;
|
|
using NT8.Core.Logging;
|
|
using NT8.Core.Risk;
|
|
using NT8.Core.Sizing;
|
|
using SdkPosition = NT8.Core.Common.Models.Position;
|
|
using SdkOrderSide = NT8.Core.Common.Models.OrderSide;
|
|
using SdkOrderType = NT8.Core.Common.Models.OrderType;
|
|
using OmsOrderRequest = NT8.Core.OMS.OrderRequest;
|
|
using OmsOrderSide = NT8.Core.OMS.OrderSide;
|
|
using OmsOrderType = NT8.Core.OMS.OrderType;
|
|
using OmsOrderState = NT8.Core.OMS.OrderState;
|
|
using OmsOrderStatus = NT8.Core.OMS.OrderStatus;
|
|
|
|
namespace NinjaTrader.NinjaScript.Strategies
|
|
{
|
|
/// <summary>
|
|
/// Base class for strategies that integrate NT8 SDK components.
|
|
/// </summary>
|
|
public abstract class NT8StrategyBase : Strategy, INT8ExecutionBridge
|
|
{
|
|
private readonly object _lock = new object();
|
|
|
|
protected IStrategy _sdkStrategy;
|
|
protected IRiskManager _riskManager;
|
|
protected IPositionSizer _positionSizer;
|
|
protected NT8ExecutionAdapter _executionAdapter;
|
|
protected ILogger _logger;
|
|
|
|
protected StrategyConfig _strategyConfig;
|
|
protected RiskConfig _riskConfig;
|
|
protected SizingConfig _sizingConfig;
|
|
|
|
private bool _sdkInitialized;
|
|
private AccountInfo _lastAccountInfo;
|
|
private SdkPosition _lastPosition;
|
|
private MarketSession _currentSession;
|
|
private int _ordersSubmittedToday;
|
|
private DateTime _lastBarTime;
|
|
private bool _killSwitchTriggered;
|
|
private bool _connectionLost;
|
|
private ExecutionCircuitBreaker _circuitBreaker;
|
|
private System.IO.StreamWriter _fileLog;
|
|
private readonly object _fileLock = new object();
|
|
|
|
#region User-Configurable Properties
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Enable SDK", GroupName = "SDK", Order = 1)]
|
|
public bool EnableSDK { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Daily Loss Limit", GroupName = "Risk", Order = 1)]
|
|
public double DailyLossLimit { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Max Trade Risk", GroupName = "Risk", Order = 2)]
|
|
public double MaxTradeRisk { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Max Positions", GroupName = "Risk", Order = 3)]
|
|
public int MaxOpenPositions { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Risk Per Trade", GroupName = "Sizing", Order = 1)]
|
|
public double RiskPerTrade { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Min Contracts", GroupName = "Sizing", Order = 2)]
|
|
public int MinContracts { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Max Contracts", GroupName = "Sizing", Order = 3)]
|
|
public int MaxContracts { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Kill Switch (Flatten + Stop)", GroupName = "Emergency Controls", Order = 1)]
|
|
public bool EnableKillSwitch { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Verbose Logging", GroupName = "Debug", Order = 1)]
|
|
public bool EnableVerboseLogging { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Min Trade Grade (1=F,2=D,3=C,4=B,5=A,6=A+)", GroupName = "Confluence", Order = 1)]
|
|
[Range(0, 6)]
|
|
public int MinTradeGrade { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Enable File Logging", GroupName = "Diagnostics", Order = 10)]
|
|
public bool EnableFileLogging { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Log Directory", GroupName = "Diagnostics", Order = 11)]
|
|
public string LogDirectory { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Enable Long Trades", GroupName = "Trade Direction", Order = 1)]
|
|
public bool EnableLongTrades { get; set; }
|
|
|
|
[NinjaScriptProperty]
|
|
[Display(Name = "Enable Short Trades", GroupName = "Trade Direction", Order = 2)]
|
|
public bool EnableShortTrades { get; set; }
|
|
|
|
#endregion
|
|
|
|
// INT8ExecutionBridge implementation
|
|
public void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
|
|
{
|
|
if (stopTicks > 0)
|
|
SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false);
|
|
if (targetTicks > 0)
|
|
SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks);
|
|
EnterLong(quantity, signalName);
|
|
}
|
|
|
|
public void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
|
|
{
|
|
if (stopTicks > 0)
|
|
SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false);
|
|
if (targetTicks > 0)
|
|
SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks);
|
|
EnterShort(quantity, signalName);
|
|
}
|
|
|
|
public void ExitLongManaged(string signalName)
|
|
{
|
|
ExitLong(signalName);
|
|
}
|
|
|
|
public void ExitShortManaged(string signalName)
|
|
{
|
|
ExitShort(signalName);
|
|
}
|
|
|
|
public void FlattenAll()
|
|
{
|
|
ExitLong("EmergencyFlatten");
|
|
ExitShort("EmergencyFlatten");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create the SDK strategy instance.
|
|
/// </summary>
|
|
protected abstract IStrategy CreateSdkStrategy();
|
|
|
|
/// <summary>
|
|
/// Configure strategy-specific values after initialization.
|
|
/// </summary>
|
|
protected abstract void ConfigureStrategyParameters();
|
|
|
|
protected override void OnStateChange()
|
|
{
|
|
if (State == State.SetDefaults)
|
|
{
|
|
Description = "SDK-integrated strategy base";
|
|
// Name intentionally not set - this is an abstract base class
|
|
Calculate = Calculate.OnBarClose;
|
|
EntriesPerDirection = 1;
|
|
EntryHandling = EntryHandling.AllEntries;
|
|
IsExitOnSessionCloseStrategy = true;
|
|
ExitOnSessionCloseSeconds = 30;
|
|
IsFillLimitOnTouch = false;
|
|
MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix;
|
|
OrderFillResolution = OrderFillResolution.Standard;
|
|
Slippage = 0;
|
|
StartBehavior = StartBehavior.WaitUntilFlat;
|
|
TimeInForce = TimeInForce.Gtc;
|
|
TraceOrders = false;
|
|
RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
|
|
StopTargetHandling = StopTargetHandling.PerEntryExecution;
|
|
BarsRequiredToTrade = 50;
|
|
|
|
EnableSDK = true;
|
|
DailyLossLimit = 1000.0;
|
|
MaxTradeRisk = 200.0;
|
|
MaxOpenPositions = 3;
|
|
RiskPerTrade = 100.0;
|
|
MinContracts = 1;
|
|
MaxContracts = 10;
|
|
EnableKillSwitch = false;
|
|
EnableVerboseLogging = false;
|
|
MinTradeGrade = 4;
|
|
EnableFileLogging = true;
|
|
LogDirectory = string.Empty;
|
|
EnableLongTrades = true;
|
|
EnableShortTrades = true;
|
|
_killSwitchTriggered = false;
|
|
_connectionLost = false;
|
|
}
|
|
else if (State == State.DataLoaded)
|
|
{
|
|
if (EnableSDK)
|
|
{
|
|
try
|
|
{
|
|
InitFileLog();
|
|
InitializeSdkComponents();
|
|
_sdkInitialized = true;
|
|
Print(string.Format("[SDK] {0} initialized successfully", Name));
|
|
WriteSettingsFile();
|
|
WriteSessionHeader();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Print(string.Format("[SDK ERROR] Initialization failed: {0}", ex.Message));
|
|
Log(string.Format("[SDK ERROR] {0}", ex.ToString()), NinjaTrader.Cbi.LogLevel.Error);
|
|
_sdkInitialized = false;
|
|
}
|
|
}
|
|
}
|
|
else if (State == State.Realtime)
|
|
{
|
|
WriteSettingsFile();
|
|
}
|
|
else if (State == State.Terminated)
|
|
{
|
|
PortfolioRiskManager.Instance.UnregisterStrategy(Name);
|
|
WriteSessionFooter();
|
|
}
|
|
}
|
|
|
|
protected override void OnBarUpdate()
|
|
{
|
|
// Only process primary bar series — ignore secondary data series updates.
|
|
// Secondary series (e.g. daily bars for confluence) trigger OnBarUpdate separately
|
|
// and must never generate strategy signals.
|
|
if (BarsInProgress != 0)
|
|
return;
|
|
|
|
if (!_sdkInitialized || _sdkStrategy == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (CurrentBar < BarsRequiredToTrade)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Time[0] == _lastBarTime)
|
|
return;
|
|
|
|
_lastBarTime = Time[0];
|
|
|
|
// Sync actual open position to portfolio manager on every bar
|
|
PortfolioRiskManager.Instance.UpdateOpenContracts(Name, Math.Abs(Position.Quantity));
|
|
|
|
// Kill switch — checked AFTER bar guards so ExitLong/ExitShort are valid
|
|
if (EnableKillSwitch)
|
|
{
|
|
if (!_killSwitchTriggered)
|
|
{
|
|
_killSwitchTriggered = true;
|
|
Print(string.Format("[SDK] KILL SWITCH ACTIVATED at {0} — flattening all positions.", Time[0]));
|
|
try
|
|
{
|
|
ExitLong("KillSwitch");
|
|
ExitShort("KillSwitch");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Print(string.Format("[SDK] Kill switch flatten error: {0}", ex.Message));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Connection loss guard — do not submit new orders if broker is disconnected
|
|
if (_connectionLost)
|
|
{
|
|
if (EnableVerboseLogging)
|
|
Print(string.Format("[NT8-SDK] Bar skipped — connection lost: {0}", Time[0]));
|
|
return;
|
|
}
|
|
|
|
// Hard RTH guard using NT8 bar time converted from CT to ET.
|
|
// Belt-and-suspenders against SDK session timezone issues.
|
|
DateTime ntBarTimeEt;
|
|
try
|
|
{
|
|
var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
|
|
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
|
|
DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(
|
|
DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified),
|
|
centralZone);
|
|
ntBarTimeEt = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone);
|
|
}
|
|
catch
|
|
{
|
|
ntBarTimeEt = Time[0];
|
|
}
|
|
|
|
bool isRthBar = ntBarTimeEt.TimeOfDay >= new TimeSpan(9, 30, 0)
|
|
&& ntBarTimeEt.TimeOfDay < new TimeSpan(16, 0, 0);
|
|
|
|
if (!isRthBar)
|
|
{
|
|
if (EnableVerboseLogging && CurrentBar % 500 == 0)
|
|
Print(string.Format("[SDK] Skipping ETH bar {0} at {1:HH:mm} ET",
|
|
CurrentBar, ntBarTimeEt));
|
|
return;
|
|
}
|
|
|
|
// Log first processable bar and every 100th bar.
|
|
if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0)
|
|
{
|
|
Print(string.Format("[SDK] Processing bar {0}: {1} O={2:F2} H={3:F2} L={4:F2} C={5:F2}",
|
|
CurrentBar,
|
|
Time[0].ToString("yyyy-MM-dd HH:mm"),
|
|
Open[0],
|
|
High[0],
|
|
Low[0],
|
|
Close[0]));
|
|
}
|
|
|
|
try
|
|
{
|
|
var barData = ConvertCurrentBar();
|
|
var context = BuildStrategyContext();
|
|
|
|
StrategyIntent intent;
|
|
lock (_lock)
|
|
{
|
|
intent = _sdkStrategy.OnBar(barData, context);
|
|
}
|
|
|
|
if (intent != null)
|
|
{
|
|
Print(string.Format("[SDK] Intent generated: {0} {1} @ {2}", intent.Side, intent.Symbol, intent.EntryType));
|
|
ProcessStrategyIntent(intent, context);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_logger != null)
|
|
_logger.LogError("OnBarUpdate failed: {0}", ex.Message);
|
|
|
|
Print(string.Format("[SDK ERROR] OnBarUpdate: {0}", ex.Message));
|
|
Log(string.Format("[SDK ERROR] {0}", ex.ToString()), NinjaTrader.Cbi.LogLevel.Error);
|
|
}
|
|
}
|
|
|
|
protected override void OnOrderUpdate(
|
|
Order order,
|
|
double limitPrice,
|
|
double stopPrice,
|
|
int quantity,
|
|
int filled,
|
|
double averageFillPrice,
|
|
NinjaTrader.Cbi.OrderState orderState,
|
|
DateTime time,
|
|
ErrorCode errorCode,
|
|
string nativeError)
|
|
{
|
|
if (!_sdkInitialized || _executionAdapter == null || order == null)
|
|
return;
|
|
|
|
if (string.IsNullOrEmpty(order.Name) || !order.Name.StartsWith("SDK_"))
|
|
return;
|
|
|
|
// Record NT8 rejections in circuit breaker
|
|
if (orderState == NinjaTrader.Cbi.OrderState.Rejected && _circuitBreaker != null)
|
|
{
|
|
var reason = string.Format("{0} {1}", errorCode, nativeError ?? string.Empty);
|
|
_circuitBreaker.RecordOrderRejection(reason);
|
|
Print(string.Format("[SDK] Order rejected by NT8: {0}", reason));
|
|
}
|
|
|
|
_executionAdapter.ProcessOrderUpdate(
|
|
order.OrderId,
|
|
order.Name,
|
|
orderState.ToString(),
|
|
filled,
|
|
averageFillPrice,
|
|
(int)errorCode,
|
|
nativeError);
|
|
}
|
|
|
|
protected override void OnExecutionUpdate(
|
|
Execution execution,
|
|
string executionId,
|
|
double price,
|
|
int quantity,
|
|
MarketPosition marketPosition,
|
|
string orderId,
|
|
DateTime time)
|
|
{
|
|
if (!_sdkInitialized || _executionAdapter == null || execution == null || execution.Order == null)
|
|
return;
|
|
|
|
if (string.IsNullOrEmpty(execution.Order.Name) || !execution.Order.Name.StartsWith("SDK_"))
|
|
return;
|
|
|
|
FileLog(string.Format("FILL {0} {1} @ {2:F2} | OrderId={3}",
|
|
execution.MarketPosition,
|
|
execution.Quantity,
|
|
execution.Price,
|
|
execution.OrderId));
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles broker connection status changes. Halts new orders on disconnect,
|
|
/// logs reconnect, and resets the connection flag when restored.
|
|
/// NinjaScript signature: single ConnectionStatusEventArgs parameter.
|
|
/// </summary>
|
|
protected override void OnConnectionStatusUpdate(
|
|
ConnectionStatusEventArgs connectionStatusUpdate)
|
|
{
|
|
if (connectionStatusUpdate == null) return;
|
|
|
|
if (connectionStatusUpdate.Status == ConnectionStatus.Connected)
|
|
{
|
|
if (_connectionLost)
|
|
{
|
|
_connectionLost = false;
|
|
Print(string.Format("[NT8-SDK] Connection RESTORED at {0} — trading resumed.",
|
|
DateTime.Now.ToString("HH:mm:ss")));
|
|
FileLog(string.Format("CONNECTION RESTORED at {0}", DateTime.Now.ToString("HH:mm:ss")));
|
|
}
|
|
}
|
|
else if (connectionStatusUpdate.Status == ConnectionStatus.Disconnected ||
|
|
connectionStatusUpdate.Status == ConnectionStatus.ConnectionLost)
|
|
{
|
|
if (!_connectionLost)
|
|
{
|
|
_connectionLost = true;
|
|
Print(string.Format("[NT8-SDK] Connection LOST at {0} — halting new orders. Status={1}",
|
|
DateTime.Now.ToString("HH:mm:ss"),
|
|
connectionStatusUpdate.Status));
|
|
FileLog(string.Format("CONNECTION LOST at {0} Status={1}",
|
|
DateTime.Now.ToString("HH:mm:ss"),
|
|
connectionStatusUpdate.Status));
|
|
}
|
|
}
|
|
}
|
|
|
|
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("Mode : {0}", State == State.Historical ? "BACKTEST" : "LIVE/SIM"));
|
|
FileLog(string.Format("Strategy : {0}", Name));
|
|
FileLog(string.Format("Account : {0}", Account != null ? Account.Name : "N/A"));
|
|
FileLog(string.Format("Symbol : {0}", Instrument != null ? Instrument.FullName : "N/A"));
|
|
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()
|
|
{
|
|
_logger = new BasicLogger(Name);
|
|
|
|
Print(string.Format("[SDK] Initializing with: DailyLoss={0:C}, TradeRisk={1:C}, MaxPos={2}",
|
|
DailyLossLimit,
|
|
MaxTradeRisk,
|
|
MaxOpenPositions));
|
|
|
|
_riskConfig = new RiskConfig(DailyLossLimit, MaxTradeRisk, MaxOpenPositions, true);
|
|
_sizingConfig = new SizingConfig(
|
|
SizingMethod.FixedDollarRisk,
|
|
MinContracts,
|
|
MaxContracts,
|
|
RiskPerTrade,
|
|
new Dictionary<string, object>());
|
|
|
|
_strategyConfig = new StrategyConfig(
|
|
Name,
|
|
Instrument.MasterInstrument.Name,
|
|
new Dictionary<string, object>(),
|
|
_riskConfig,
|
|
_sizingConfig);
|
|
|
|
_riskManager = new BasicRiskManager(_logger);
|
|
_positionSizer = new BasicPositionSizer(_logger);
|
|
_circuitBreaker = new ExecutionCircuitBreaker(
|
|
_logger,
|
|
failureThreshold: 3,
|
|
timeout: TimeSpan.FromSeconds(30));
|
|
_executionAdapter = new NT8ExecutionAdapter();
|
|
|
|
_sdkStrategy = CreateSdkStrategy();
|
|
if (_sdkStrategy == null)
|
|
throw new InvalidOperationException("CreateSdkStrategy returned null");
|
|
|
|
_sdkStrategy.Initialize(_strategyConfig, null, _logger);
|
|
ConfigureStrategyParameters();
|
|
PortfolioRiskManager.Instance.RegisterStrategy(Name, _riskConfig);
|
|
Print(string.Format("[NT8-SDK] Registered with PortfolioRiskManager: {0}", PortfolioRiskManager.Instance.GetStatusSnapshot()));
|
|
|
|
_ordersSubmittedToday = 0;
|
|
_lastBarTime = DateTime.MinValue;
|
|
_lastAccountInfo = null;
|
|
_lastPosition = null;
|
|
_currentSession = null;
|
|
}
|
|
|
|
private BarData ConvertCurrentBar()
|
|
{
|
|
return NT8DataConverter.ConvertBar(
|
|
Instrument.MasterInstrument.Name,
|
|
Time[0],
|
|
Open[0],
|
|
High[0],
|
|
Low[0],
|
|
Close[0],
|
|
(long)Volume[0],
|
|
(int)BarsPeriod.Value);
|
|
}
|
|
|
|
private StrategyContext BuildStrategyContext()
|
|
{
|
|
DateTime etTime;
|
|
try
|
|
{
|
|
var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
|
|
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
|
|
DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(
|
|
DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified),
|
|
centralZone);
|
|
etTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone);
|
|
}
|
|
catch
|
|
{
|
|
etTime = Time[0];
|
|
}
|
|
|
|
var customData = new Dictionary<string, object>();
|
|
customData.Add("CurrentBar", CurrentBar);
|
|
customData.Add("BarsRequiredToTrade", BarsRequiredToTrade);
|
|
customData.Add("OrdersToday", _ordersSubmittedToday);
|
|
|
|
return NT8DataConverter.ConvertContext(
|
|
Instrument.MasterInstrument.Name,
|
|
etTime,
|
|
BuildPositionInfo(),
|
|
BuildAccountInfo(),
|
|
BuildSessionInfo(),
|
|
customData);
|
|
}
|
|
|
|
private AccountInfo BuildAccountInfo()
|
|
{
|
|
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;
|
|
return accountInfo;
|
|
}
|
|
|
|
private SdkPosition BuildPositionInfo()
|
|
{
|
|
var p = NT8DataConverter.ConvertPosition(
|
|
Instrument.MasterInstrument.Name,
|
|
Position.Quantity,
|
|
Position.AveragePrice,
|
|
0.0,
|
|
0.0,
|
|
DateTime.UtcNow);
|
|
|
|
_lastPosition = p;
|
|
return p;
|
|
}
|
|
|
|
private MarketSession BuildSessionInfo()
|
|
{
|
|
DateTime etTime;
|
|
try
|
|
{
|
|
var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
|
|
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
|
|
DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(
|
|
DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified),
|
|
centralZone);
|
|
etTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone);
|
|
}
|
|
catch
|
|
{
|
|
etTime = Time[0];
|
|
}
|
|
|
|
// Futures trade nearly 24 hours. Bars at/after 17:00 ET belong to the next
|
|
// calendar day's RTH trading session.
|
|
DateTime tradingDate;
|
|
if (etTime.TimeOfDay >= new TimeSpan(17, 0, 0))
|
|
tradingDate = etTime.Date.AddDays(1);
|
|
else
|
|
tradingDate = etTime.Date;
|
|
|
|
if (EnableVerboseLogging && (CurrentBar == BarsRequiredToTrade || CurrentBar % 500 == 0))
|
|
{
|
|
Print(string.Format("[SDK-TZ] Bar {0}: NT8 Time[0]={1:yyyy-MM-dd HH:mm:ss} | etTime={2:yyyy-MM-dd HH:mm:ss} | isRth={3}",
|
|
CurrentBar,
|
|
Time[0],
|
|
etTime,
|
|
etTime.TimeOfDay >= TimeSpan.FromHours(9.5) && etTime.TimeOfDay < TimeSpan.FromHours(16.0)));
|
|
}
|
|
|
|
var sessionStart = tradingDate.AddHours(9).AddMinutes(30);
|
|
var sessionEnd = tradingDate.AddHours(16);
|
|
var isRth = etTime.TimeOfDay >= TimeSpan.FromHours(9.5)
|
|
&& etTime.TimeOfDay < TimeSpan.FromHours(16.0);
|
|
|
|
_currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, isRth ? "RTH" : "ETH");
|
|
return _currentSession;
|
|
}
|
|
|
|
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)
|
|
Print(string.Format("[SDK] Validating intent: {0} {1}", intent.Side, intent.Symbol));
|
|
|
|
var riskDecision = _riskManager.ValidateOrder(intent, context, _riskConfig);
|
|
if (!riskDecision.Allow)
|
|
{
|
|
if (EnableVerboseLogging)
|
|
Print(string.Format("[SDK] Risk REJECTED: {0}", riskDecision.RejectReason));
|
|
if (_logger != null)
|
|
_logger.LogWarning("Intent rejected by risk manager: {0}", riskDecision.RejectReason);
|
|
return;
|
|
}
|
|
|
|
if (EnableVerboseLogging)
|
|
Print(string.Format("[SDK] Risk approved"));
|
|
|
|
var sizingResult = _positionSizer.CalculateSize(intent, context, _sizingConfig);
|
|
if (EnableVerboseLogging)
|
|
{
|
|
Print(string.Format("[SDK] Position size: {0} contracts (min={1}, max={2})",
|
|
sizingResult.Contracts,
|
|
MinContracts,
|
|
MaxContracts));
|
|
}
|
|
|
|
if (sizingResult.Contracts < MinContracts)
|
|
{
|
|
if (EnableVerboseLogging)
|
|
Print(string.Format("[SDK] Size too small: {0} < {1}", sizingResult.Contracts, MinContracts));
|
|
return;
|
|
}
|
|
|
|
var request = new OmsOrderRequest();
|
|
request.Symbol = intent.Symbol;
|
|
request.Side = MapOrderSide(intent.Side);
|
|
request.Type = MapOrderType(intent.EntryType);
|
|
request.Quantity = sizingResult.Contracts;
|
|
request.LimitPrice = intent.LimitPrice.HasValue ? (decimal?)intent.LimitPrice.Value : null;
|
|
request.StopPrice = null;
|
|
|
|
if (EnableVerboseLogging)
|
|
{
|
|
Print(string.Format("[SDK] Submitting order: {0} {1} {2} @ {3}",
|
|
request.Side,
|
|
request.Quantity,
|
|
request.Symbol,
|
|
request.Type));
|
|
}
|
|
|
|
SubmitOrderToNT8(request, intent);
|
|
_ordersSubmittedToday++;
|
|
}
|
|
|
|
private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent)
|
|
{
|
|
// Circuit breaker gate
|
|
if (State == State.Historical)
|
|
{
|
|
// Skip circuit breaker during backtest — wall-clock timeout is meaningless on historical data.
|
|
}
|
|
else if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder())
|
|
{
|
|
var state = _circuitBreaker.GetState();
|
|
Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason));
|
|
if (_logger != null)
|
|
_logger.LogWarning("Circuit breaker blocked order: {0}", state.Reason);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, Guid.NewGuid().ToString("N").Substring(0, 12));
|
|
|
|
if (EnableFileLogging)
|
|
{
|
|
string grade = "N/A";
|
|
string score = "N/A";
|
|
string factors = string.Empty;
|
|
|
|
if (intent.Metadata != null && intent.Metadata.ContainsKey("confluence_score"))
|
|
{
|
|
var cs = intent.Metadata["confluence_score"] as NT8.Core.Intelligence.ConfluenceScore;
|
|
if (cs != null)
|
|
{
|
|
grade = cs.Grade.ToString();
|
|
score = cs.WeightedScore.ToString("F3");
|
|
|
|
var sb = new System.Text.StringBuilder();
|
|
foreach (var f in cs.Factors)
|
|
sb.Append(string.Format("{0}={1:F2} ", f.Type, f.Score));
|
|
factors = sb.ToString().TrimEnd();
|
|
}
|
|
}
|
|
|
|
FileLog(string.Format("SIGNAL {0} | Grade={1} | Score={2}", intent.Side, grade, score));
|
|
if (!string.IsNullOrEmpty(factors))
|
|
FileLog(string.Format(" Factors: {0}", factors));
|
|
FileLog(string.Format("SUBMIT {0} {1} @ Market | Stop={2} Target={3}",
|
|
intent.Side,
|
|
request.Quantity,
|
|
intent.StopTicks,
|
|
intent.TargetTicks));
|
|
}
|
|
|
|
_executionAdapter.SubmitOrder(request, orderName);
|
|
|
|
if (request.Side == OmsOrderSide.Buy)
|
|
{
|
|
if (request.Type == OmsOrderType.Market)
|
|
EnterLong(request.Quantity, orderName);
|
|
else if (request.Type == OmsOrderType.Limit && request.LimitPrice.HasValue)
|
|
EnterLongLimit(request.Quantity, (double)request.LimitPrice.Value, orderName);
|
|
else if (request.Type == OmsOrderType.StopMarket && request.StopPrice.HasValue)
|
|
EnterLongStopMarket(request.Quantity, (double)request.StopPrice.Value, orderName);
|
|
}
|
|
else if (request.Side == OmsOrderSide.Sell)
|
|
{
|
|
if (request.Type == OmsOrderType.Market)
|
|
EnterShort(request.Quantity, orderName);
|
|
else if (request.Type == OmsOrderType.Limit && request.LimitPrice.HasValue)
|
|
EnterShortLimit(request.Quantity, (double)request.LimitPrice.Value, orderName);
|
|
else if (request.Type == OmsOrderType.StopMarket && request.StopPrice.HasValue)
|
|
EnterShortStopMarket(request.Quantity, (double)request.StopPrice.Value, orderName);
|
|
}
|
|
|
|
if (intent.StopTicks > 0)
|
|
SetStopLoss(orderName, CalculationMode.Ticks, (int)intent.StopTicks, false);
|
|
|
|
if (intent.TargetTicks.HasValue && intent.TargetTicks.Value > 0)
|
|
SetProfitTarget(orderName, CalculationMode.Ticks, (int)intent.TargetTicks.Value);
|
|
|
|
if (_circuitBreaker != null)
|
|
_circuitBreaker.OnSuccess();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_circuitBreaker != null)
|
|
_circuitBreaker.OnFailure();
|
|
|
|
Print(string.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message));
|
|
if (_logger != null)
|
|
_logger.LogError("SubmitOrderToNT8 failed: {0}", ex.Message);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private static OmsOrderSide MapOrderSide(SdkOrderSide side)
|
|
{
|
|
if (side == SdkOrderSide.Buy)
|
|
return OmsOrderSide.Buy;
|
|
return OmsOrderSide.Sell;
|
|
}
|
|
|
|
private static OmsOrderType MapOrderType(SdkOrderType type)
|
|
{
|
|
if (type == SdkOrderType.Market)
|
|
return OmsOrderType.Market;
|
|
if (type == SdkOrderType.Limit)
|
|
return OmsOrderType.Limit;
|
|
if (type == SdkOrderType.StopLimit)
|
|
return OmsOrderType.StopLimit;
|
|
return OmsOrderType.StopMarket;
|
|
}
|
|
|
|
protected OmsOrderStatus GetSdkOrderStatus(string orderName)
|
|
{
|
|
if (_executionAdapter == null)
|
|
return null;
|
|
return _executionAdapter.GetOrderStatus(orderName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all strategy parameter lines for the settings export file.
|
|
/// Override in subclasses to append strategy-specific parameters.
|
|
/// Call base.GetStrategySettingsLines() first then add to the list.
|
|
/// </summary>
|
|
protected virtual List<string> GetStrategySettingsLines()
|
|
{
|
|
var lines = new List<string>();
|
|
lines.Add("=== STRATEGY SETTINGS EXPORT ===");
|
|
lines.Add(string.Format("ExportTime : {0:yyyy-MM-dd HH:mm:ss}", DateTime.Now));
|
|
lines.Add(string.Format("StrategyName : {0}", Name));
|
|
lines.Add(string.Format("Description : {0}", Description));
|
|
lines.Add(string.Format("Account : {0}", Account != null ? Account.Name : "N/A"));
|
|
lines.Add(string.Format("Instrument : {0}", Instrument != null ? Instrument.FullName : "N/A"));
|
|
lines.Add(string.Format("BarsPeriod : {0} {1}", BarsPeriod != null ? BarsPeriod.Value.ToString() : "N/A", BarsPeriod != null ? BarsPeriod.BarsPeriodType.ToString() : string.Empty));
|
|
lines.Add(string.Format("BarsRequiredToTrade: {0}", BarsRequiredToTrade));
|
|
lines.Add(string.Format("Calculate : {0}", Calculate));
|
|
lines.Add("--- Risk ---");
|
|
lines.Add(string.Format("DailyLossLimit : {0:C}", DailyLossLimit));
|
|
lines.Add(string.Format("MaxTradeRisk : {0:C}", MaxTradeRisk));
|
|
lines.Add(string.Format("MaxOpenPositions : {0}", MaxOpenPositions));
|
|
lines.Add(string.Format("RiskPerTrade : {0:C}", RiskPerTrade));
|
|
lines.Add("--- Sizing ---");
|
|
lines.Add(string.Format("MinContracts : {0}", MinContracts));
|
|
lines.Add(string.Format("MaxContracts : {0}", MaxContracts));
|
|
lines.Add("--- Direction ---");
|
|
lines.Add(string.Format("EnableLongTrades : {0}", EnableLongTrades));
|
|
lines.Add(string.Format("EnableShortTrades : {0}", EnableShortTrades));
|
|
lines.Add("--- Controls ---");
|
|
lines.Add(string.Format("EnableKillSwitch : {0}", EnableKillSwitch));
|
|
lines.Add(string.Format("EnableVerboseLogging: {0}", EnableVerboseLogging));
|
|
lines.Add(string.Format("MinTradeGrade : {0}", MinTradeGrade));
|
|
lines.Add(string.Format("EnableFileLogging : {0}", EnableFileLogging));
|
|
lines.Add(string.Format("LogDirectory : {0}", string.IsNullOrEmpty(LogDirectory) ? "(default)" : LogDirectory));
|
|
lines.Add("--- Portfolio ---");
|
|
lines.Add(string.Format("PortfolioStatus : {0}", PortfolioRiskManager.Instance.GetStatusSnapshot()));
|
|
lines.Add("=== END SETTINGS ===");
|
|
return lines;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a settings export file to the same directory as the session log.
|
|
/// File is named settings_STRATEGYNAME_YYYYMMDD_HHmmss.txt.
|
|
/// Only writes when EnableVerboseLogging is true.
|
|
/// </summary>
|
|
private void WriteSettingsFile()
|
|
{
|
|
if (!EnableVerboseLogging) return;
|
|
|
|
try
|
|
{
|
|
string dir = string.IsNullOrEmpty(LogDirectory)
|
|
? System.IO.Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
|
|
"NinjaTrader 8", "log", "nt8-sdk")
|
|
: LogDirectory;
|
|
|
|
System.IO.Directory.CreateDirectory(dir);
|
|
|
|
string safeName = Name.Replace(" ", "_").Replace("/", "_").Replace("\\", "_");
|
|
string path = System.IO.Path.Combine(dir,
|
|
string.Format("settings_{0}_{1}.txt",
|
|
safeName,
|
|
DateTime.Now.ToString("yyyyMMdd_HHmmss")));
|
|
|
|
var lines = GetStrategySettingsLines();
|
|
|
|
System.IO.File.WriteAllLines(path, lines.ToArray());
|
|
|
|
Print(string.Format("[NT8-SDK] Settings exported: {0}", path));
|
|
FileLog(string.Format("SETTINGS FILE: {0}", path));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Print(string.Format("[NT8-SDK] WARNING: Could not write settings file: {0}", ex.Message));
|
|
}
|
|
}
|
|
}
|
|
}
|