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
Some checks failed
Build and Test / build (push) Has been cancelled
This commit is contained in:
26
src/NT8.Adapters/NinjaTrader/INT8ExecutionBridge.cs
Normal file
26
src/NT8.Adapters/NinjaTrader/INT8ExecutionBridge.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,34 @@ namespace NT8.Adapters.NinjaTrader
|
||||
public NT8Adapter()
|
||||
{
|
||||
_dataAdapter = new NT8DataAdapter();
|
||||
_orderAdapter = new NT8OrderAdapter();
|
||||
_orderAdapter = new NT8OrderAdapter(new NullExecutionBridge());
|
||||
_loggingAdapter = new NT8LoggingAdapter();
|
||||
_executionHistory = new List<NT8OrderExecutionRecord>();
|
||||
}
|
||||
|
||||
private class NullExecutionBridge : INT8ExecutionBridge
|
||||
{
|
||||
public void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
|
||||
{
|
||||
}
|
||||
|
||||
public void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
|
||||
{
|
||||
}
|
||||
|
||||
public void ExitLongManaged(string signalName)
|
||||
{
|
||||
}
|
||||
|
||||
public void ExitShortManaged(string signalName)
|
||||
{
|
||||
}
|
||||
|
||||
public void FlattenAll()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the adapter with required components
|
||||
/// </summary>
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace NT8.Adapters.NinjaTrader
|
||||
public class NT8OrderAdapter
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private readonly INT8ExecutionBridge _bridge;
|
||||
private IRiskManager _riskManager;
|
||||
private IPositionSizer _positionSizer;
|
||||
private readonly List<NT8OrderExecutionRecord> _executionHistory;
|
||||
@@ -19,8 +20,11 @@ namespace NT8.Adapters.NinjaTrader
|
||||
/// <summary>
|
||||
/// Constructor for NT8OrderAdapter.
|
||||
/// </summary>
|
||||
public NT8OrderAdapter()
|
||||
public NT8OrderAdapter(INT8ExecutionBridge bridge)
|
||||
{
|
||||
if (bridge == null)
|
||||
throw new ArgumentNullException("bridge");
|
||||
_bridge = bridge;
|
||||
_executionHistory = new List<NT8OrderExecutionRecord>();
|
||||
}
|
||||
|
||||
@@ -127,18 +131,30 @@ namespace NT8.Adapters.NinjaTrader
|
||||
private void ExecuteInNT8(StrategyIntent intent, SizingResult sizing)
|
||||
{
|
||||
if (intent == null)
|
||||
{
|
||||
throw new ArgumentNullException("intent");
|
||||
}
|
||||
|
||||
if (sizing == null)
|
||||
{
|
||||
throw new ArgumentNullException("sizing");
|
||||
}
|
||||
|
||||
// This is where the actual NT8 order execution would happen
|
||||
// In a real implementation, this would call NT8's EnterLong/EnterShort methods
|
||||
// along with SetStopLoss, SetProfitTarget, etc.
|
||||
var signalName = string.Format("SDK_{0}_{1}", intent.Symbol, intent.Side);
|
||||
|
||||
if (intent.Side == OrderSide.Buy)
|
||||
{
|
||||
_bridge.EnterLongManaged(
|
||||
sizing.Contracts,
|
||||
signalName,
|
||||
intent.StopTicks,
|
||||
intent.TargetTicks.HasValue ? intent.TargetTicks.Value : 0,
|
||||
0.25);
|
||||
}
|
||||
else if (intent.Side == OrderSide.Sell)
|
||||
{
|
||||
_bridge.EnterShortManaged(
|
||||
sizing.Contracts,
|
||||
signalName,
|
||||
intent.StopTicks,
|
||||
intent.TargetTicks.HasValue ? intent.TargetTicks.Value : 0,
|
||||
0.25);
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
@@ -151,28 +167,6 @@ namespace NT8.Adapters.NinjaTrader
|
||||
intent.TargetTicks,
|
||||
DateTime.UtcNow));
|
||||
}
|
||||
|
||||
// Example of what this might look like in NT8:
|
||||
/*
|
||||
if (intent.Side == OrderSide.Buy)
|
||||
{
|
||||
EnterLong(sizing.Contracts, "SDK_Entry");
|
||||
SetStopLoss("SDK_Entry", CalculationMode.Ticks, intent.StopTicks);
|
||||
if (intent.TargetTicks.HasValue)
|
||||
{
|
||||
SetProfitTarget("SDK_Entry", CalculationMode.Ticks, intent.TargetTicks.Value);
|
||||
}
|
||||
}
|
||||
else if (intent.Side == OrderSide.Sell)
|
||||
{
|
||||
EnterShort(sizing.Contracts, "SDK_Entry");
|
||||
SetStopLoss("SDK_Entry", CalculationMode.Ticks, intent.StopTicks);
|
||||
if (intent.TargetTicks.HasValue)
|
||||
{
|
||||
SetProfitTarget("SDK_Entry", CalculationMode.Ticks, intent.TargetTicks.Value);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
/// <summary>
|
||||
/// Base class for strategies that integrate NT8 SDK components.
|
||||
/// </summary>
|
||||
public abstract class NT8StrategyBase : Strategy
|
||||
public abstract class NT8StrategyBase : Strategy, INT8ExecutionBridge
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
|
||||
@@ -54,6 +54,8 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
private DateTime _lastBarTime;
|
||||
private bool _killSwitchTriggered;
|
||||
private ExecutionCircuitBreaker _circuitBreaker;
|
||||
private System.IO.StreamWriter _fileLog;
|
||||
private readonly object _fileLock = new object();
|
||||
|
||||
#region User-Configurable Properties
|
||||
|
||||
@@ -93,8 +95,59 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
[Display(Name = "Verbose Logging", GroupName = "Debug", Order = 1)]
|
||||
public bool EnableVerboseLogging { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Enable File Logging", GroupName = "Diagnostics", Order = 10)]
|
||||
public bool EnableFileLogging { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Log Directory", GroupName = "Diagnostics", Order = 11)]
|
||||
public string LogDirectory { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Enable Long Trades", GroupName = "Trade Direction", Order = 1)]
|
||||
public bool EnableLongTrades { get; set; }
|
||||
|
||||
[NinjaScriptProperty]
|
||||
[Display(Name = "Enable Short Trades", GroupName = "Trade Direction", Order = 2)]
|
||||
public bool EnableShortTrades { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
// INT8ExecutionBridge implementation
|
||||
public void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
|
||||
{
|
||||
if (stopTicks > 0)
|
||||
SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false);
|
||||
if (targetTicks > 0)
|
||||
SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks);
|
||||
EnterLong(quantity, signalName);
|
||||
}
|
||||
|
||||
public void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
|
||||
{
|
||||
if (stopTicks > 0)
|
||||
SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false);
|
||||
if (targetTicks > 0)
|
||||
SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks);
|
||||
EnterShort(quantity, signalName);
|
||||
}
|
||||
|
||||
public void ExitLongManaged(string signalName)
|
||||
{
|
||||
ExitLong(signalName);
|
||||
}
|
||||
|
||||
public void ExitShortManaged(string signalName)
|
||||
{
|
||||
ExitShort(signalName);
|
||||
}
|
||||
|
||||
public void FlattenAll()
|
||||
{
|
||||
ExitLong("EmergencyFlatten");
|
||||
ExitShort("EmergencyFlatten");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the SDK strategy instance.
|
||||
/// </summary>
|
||||
@@ -136,6 +189,10 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
MaxContracts = 10;
|
||||
EnableKillSwitch = false;
|
||||
EnableVerboseLogging = false;
|
||||
EnableFileLogging = true;
|
||||
LogDirectory = string.Empty;
|
||||
EnableLongTrades = true;
|
||||
EnableShortTrades = true;
|
||||
_killSwitchTriggered = false;
|
||||
}
|
||||
else if (State == State.DataLoaded)
|
||||
@@ -156,11 +213,35 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (State == State.Realtime)
|
||||
{
|
||||
InitFileLog();
|
||||
WriteSessionHeader();
|
||||
}
|
||||
else if (State == State.Terminated)
|
||||
{
|
||||
WriteSessionFooter();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnBarUpdate()
|
||||
{
|
||||
// Kill switch check — must be first
|
||||
if (!_sdkInitialized || _sdkStrategy == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentBar < BarsRequiredToTrade)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Time[0] == _lastBarTime)
|
||||
return;
|
||||
|
||||
_lastBarTime = Time[0];
|
||||
|
||||
// Kill switch — checked AFTER bar guards so ExitLong/ExitShort are valid
|
||||
if (EnableKillSwitch)
|
||||
{
|
||||
if (!_killSwitchTriggered)
|
||||
@@ -177,29 +258,9 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
Print(string.Format("[SDK] Kill switch flatten error: {0}", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_sdkInitialized || _sdkStrategy == null)
|
||||
{
|
||||
if (CurrentBar == 0)
|
||||
Print(string.Format("[SDK] Not initialized: sdkInit={0}, strategy={1}", _sdkInitialized, _sdkStrategy != null));
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentBar < BarsRequiredToTrade)
|
||||
{
|
||||
if (CurrentBar == 0)
|
||||
Print(string.Format("[SDK] Waiting for bars: current={0}, required={1}", CurrentBar, BarsRequiredToTrade));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Time[0] == _lastBarTime)
|
||||
return;
|
||||
|
||||
_lastBarTime = Time[0];
|
||||
|
||||
// Log first processable bar and every 100th bar.
|
||||
if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0)
|
||||
{
|
||||
@@ -290,9 +351,98 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
if (string.IsNullOrEmpty(execution.Order.Name) || !execution.Order.Name.StartsWith("SDK_"))
|
||||
return;
|
||||
|
||||
FileLog(string.Format("FILL {0} {1} @ {2:F2} | OrderId={3}",
|
||||
execution.MarketPosition,
|
||||
execution.Quantity,
|
||||
execution.Price,
|
||||
execution.OrderId));
|
||||
|
||||
_executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time);
|
||||
}
|
||||
|
||||
private void InitFileLog()
|
||||
{
|
||||
if (!EnableFileLogging)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
string dir = string.IsNullOrEmpty(LogDirectory)
|
||||
? System.IO.Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
|
||||
"NinjaTrader 8", "log", "nt8-sdk")
|
||||
: LogDirectory;
|
||||
|
||||
System.IO.Directory.CreateDirectory(dir);
|
||||
|
||||
string path = System.IO.Path.Combine(
|
||||
dir,
|
||||
string.Format("session_{0}.log", DateTime.Now.ToString("yyyyMMdd_HHmmss")));
|
||||
|
||||
_fileLog = new System.IO.StreamWriter(path, false);
|
||||
_fileLog.AutoFlush = true;
|
||||
Print(string.Format("[NT8-SDK] File log started: {0}", path));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Print(string.Format("[NT8-SDK] Failed to open file log: {0}", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
private void FileLog(string message)
|
||||
{
|
||||
if (_fileLog == null)
|
||||
return;
|
||||
|
||||
lock (_fileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
_fileLog.WriteLine(string.Format("[{0:HH:mm:ss.fff}] {1}", DateTime.Now, message));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteSessionHeader()
|
||||
{
|
||||
FileLog("=== SESSION START " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ===");
|
||||
FileLog(string.Format("Strategy : {0}", Name));
|
||||
FileLog(string.Format("Account : {0}", Account != null ? Account.Name : "N/A"));
|
||||
FileLog(string.Format("Symbol : {0}", Instrument != null ? Instrument.FullName : "N/A"));
|
||||
FileLog(string.Format("Risk : DailyLimit=${0} MaxTradeRisk=${1} RiskPerTrade=${2}",
|
||||
DailyLossLimit,
|
||||
MaxTradeRisk,
|
||||
RiskPerTrade));
|
||||
FileLog(string.Format("Sizing : MinContracts={0} MaxContracts={1}", MinContracts, MaxContracts));
|
||||
FileLog(string.Format("VerboseLog : {0} FileLog: {1}", EnableVerboseLogging, EnableFileLogging));
|
||||
FileLog("---");
|
||||
}
|
||||
|
||||
private void WriteSessionFooter()
|
||||
{
|
||||
FileLog("---");
|
||||
FileLog("=== SESSION END " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ===");
|
||||
|
||||
if (_fileLog != null)
|
||||
{
|
||||
lock (_fileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
_fileLog.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_fileLog = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeSdkComponents()
|
||||
{
|
||||
_logger = new BasicLogger(Name);
|
||||
@@ -354,6 +504,17 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
|
||||
private StrategyContext BuildStrategyContext()
|
||||
{
|
||||
DateTime etTime;
|
||||
try
|
||||
{
|
||||
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
|
||||
etTime = TimeZoneInfo.ConvertTime(Time[0], easternZone);
|
||||
}
|
||||
catch
|
||||
{
|
||||
etTime = Time[0];
|
||||
}
|
||||
|
||||
var customData = new Dictionary<string, object>();
|
||||
customData.Add("CurrentBar", CurrentBar);
|
||||
customData.Add("BarsRequiredToTrade", BarsRequiredToTrade);
|
||||
@@ -361,7 +522,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
|
||||
return NT8DataConverter.ConvertContext(
|
||||
Instrument.MasterInstrument.Name,
|
||||
Time[0],
|
||||
etTime,
|
||||
BuildPositionInfo(),
|
||||
BuildAccountInfo(),
|
||||
BuildSessionInfo(),
|
||||
@@ -391,20 +552,42 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
|
||||
private MarketSession BuildSessionInfo()
|
||||
{
|
||||
if (_currentSession != null && _currentSession.SessionStart.Date == Time[0].Date)
|
||||
return _currentSession;
|
||||
DateTime etTime;
|
||||
try
|
||||
{
|
||||
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
|
||||
etTime = TimeZoneInfo.ConvertTime(Time[0], easternZone);
|
||||
}
|
||||
catch
|
||||
{
|
||||
etTime = Time[0];
|
||||
}
|
||||
|
||||
var sessionStart = Time[0].Date.AddHours(9).AddMinutes(30);
|
||||
var sessionEnd = Time[0].Date.AddHours(16);
|
||||
var isRth = Time[0].Hour >= 9 && Time[0].Hour < 16;
|
||||
var sessionName = isRth ? "RTH" : "ETH";
|
||||
var sessionStart = etTime.Date.AddHours(9).AddMinutes(30);
|
||||
var sessionEnd = etTime.Date.AddHours(16);
|
||||
var isRth = etTime.TimeOfDay >= TimeSpan.FromHours(9.5)
|
||||
&& etTime.TimeOfDay < TimeSpan.FromHours(16.0);
|
||||
|
||||
_currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, sessionName);
|
||||
_currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, isRth ? "RTH" : "ETH");
|
||||
return _currentSession;
|
||||
}
|
||||
|
||||
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context)
|
||||
{
|
||||
// Direction filter — checked before risk to avoid unnecessary processing
|
||||
if (intent.Side == SdkOrderSide.Buy && !EnableLongTrades)
|
||||
{
|
||||
if (EnableVerboseLogging)
|
||||
Print(string.Format("[SDK] Long trade filtered by direction setting: {0}", intent.Symbol));
|
||||
return;
|
||||
}
|
||||
if (intent.Side == SdkOrderSide.Sell && !EnableShortTrades)
|
||||
{
|
||||
if (EnableVerboseLogging)
|
||||
Print(string.Format("[SDK] Short trade filtered by direction setting: {0}", intent.Symbol));
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnableVerboseLogging)
|
||||
Print(string.Format("[SDK] Validating intent: {0} {1}", intent.Side, intent.Symbol));
|
||||
|
||||
@@ -461,7 +644,11 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent)
|
||||
{
|
||||
// Circuit breaker gate
|
||||
if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder())
|
||||
if (State == State.Historical)
|
||||
{
|
||||
// Skip circuit breaker during backtest — wall-clock timeout is meaningless on historical data.
|
||||
}
|
||||
else if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder())
|
||||
{
|
||||
var state = _circuitBreaker.GetState();
|
||||
Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason));
|
||||
@@ -472,7 +659,39 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
|
||||
try
|
||||
{
|
||||
var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, DateTime.Now.Ticks);
|
||||
var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, Guid.NewGuid().ToString("N").Substring(0, 12));
|
||||
|
||||
if (EnableFileLogging)
|
||||
{
|
||||
string grade = "N/A";
|
||||
string score = "N/A";
|
||||
string factors = string.Empty;
|
||||
|
||||
if (intent.Metadata != null && intent.Metadata.ContainsKey("confluence_score"))
|
||||
{
|
||||
var cs = intent.Metadata["confluence_score"] as NT8.Core.Intelligence.ConfluenceScore;
|
||||
if (cs != null)
|
||||
{
|
||||
grade = cs.Grade.ToString();
|
||||
score = cs.WeightedScore.ToString("F3");
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (var f in cs.Factors)
|
||||
sb.Append(string.Format("{0}={1:F2} ", f.Type, f.Score));
|
||||
factors = sb.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
FileLog(string.Format("SIGNAL {0} | Grade={1} | Score={2}", intent.Side, grade, score));
|
||||
if (!string.IsNullOrEmpty(factors))
|
||||
FileLog(string.Format(" Factors: {0}", factors));
|
||||
FileLog(string.Format("SUBMIT {0} {1} @ Market | Stop={2} Target={3}",
|
||||
intent.Side,
|
||||
request.Quantity,
|
||||
intent.StopTicks,
|
||||
intent.TargetTicks));
|
||||
}
|
||||
|
||||
_executionAdapter.SubmitOrder(request, orderName);
|
||||
|
||||
if (request.Side == OmsOrderSide.Buy)
|
||||
@@ -541,4 +760,3 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using NinjaTrader.NinjaScript;
|
||||
using NinjaTrader.NinjaScript.Indicators;
|
||||
using NinjaTrader.NinjaScript.Strategies;
|
||||
using NT8.Core.Common.Interfaces;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Strategies.Examples;
|
||||
using SdkSimpleORB = NT8.Strategies.Examples.SimpleORBStrategy;
|
||||
|
||||
@@ -49,6 +50,8 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
Name = "Simple ORB NT8";
|
||||
Description = "Opening Range Breakout with NT8 SDK integration";
|
||||
|
||||
// Daily bar series is added automatically via AddDataSeries in Configure.
|
||||
|
||||
OpeningRangeMinutes = 30;
|
||||
StdDevMultiplier = 1.0;
|
||||
StopTicks = 8;
|
||||
@@ -63,11 +66,28 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
|
||||
Calculate = Calculate.OnBarClose;
|
||||
BarsRequiredToTrade = 50;
|
||||
EnableLongTrades = true;
|
||||
EnableShortTrades = false;
|
||||
}
|
||||
else if (State == State.Configure)
|
||||
{
|
||||
AddDataSeries(BarsPeriodType.Day, 1);
|
||||
}
|
||||
|
||||
base.OnStateChange();
|
||||
}
|
||||
|
||||
protected override void OnBarUpdate()
|
||||
{
|
||||
if (_strategyConfig != null && BarsArray != null && BarsArray.Length > 1)
|
||||
{
|
||||
DailyBarContext dailyContext = BuildDailyBarContext(0, 0.0, (double)Volume[0]);
|
||||
_strategyConfig.Parameters["daily_bars"] = dailyContext;
|
||||
}
|
||||
|
||||
base.OnBarUpdate();
|
||||
}
|
||||
|
||||
protected override IStrategy CreateSdkStrategy()
|
||||
{
|
||||
return new SdkSimpleORB(OpeningRangeMinutes, StdDevMultiplier);
|
||||
@@ -98,15 +118,82 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
_strategyConfig.Parameters["TargetTicks"] = TargetTicks;
|
||||
_strategyConfig.Parameters["OpeningRangeMinutes"] = OpeningRangeMinutes;
|
||||
|
||||
if (Instrument != null && Instrument.MasterInstrument != null)
|
||||
{
|
||||
_strategyConfig.Parameters["TickSize"] = Instrument.MasterInstrument.TickSize;
|
||||
}
|
||||
|
||||
if (_logger != null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Simple ORB configured: OR={0}min, Stop={1}ticks, Target={2}ticks",
|
||||
"Simple ORB configured: OR={0}min, Stop={1}ticks, Target={2}ticks, Long={3}, Short={4}",
|
||||
OpeningRangeMinutes,
|
||||
StopTicks,
|
||||
TargetTicks);
|
||||
TargetTicks,
|
||||
EnableLongTrades,
|
||||
EnableShortTrades);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a DailyBarContext from the secondary daily bar series.
|
||||
/// Returns a context with Count=0 if fewer than 2 daily bars are available.
|
||||
/// </summary>
|
||||
/// <param name="tradeDirection">1 for long, -1 for short.</param>
|
||||
/// <param name="orbRangeTicks">ORB range in ticks for ORB range factor.</param>
|
||||
/// <param name="breakoutBarVolume">Volume of the current breakout bar.</param>
|
||||
/// <returns>Populated daily context for confluence scoring.</returns>
|
||||
private DailyBarContext BuildDailyBarContext(int tradeDirection, double orbRangeTicks, double breakoutBarVolume)
|
||||
{
|
||||
DailyBarContext ctx = new DailyBarContext();
|
||||
ctx.TradeDirection = tradeDirection;
|
||||
ctx.BreakoutBarVolume = breakoutBarVolume;
|
||||
ctx.TodayOpen = Open[0];
|
||||
|
||||
if (BarsArray == null || BarsArray.Length < 2 || CurrentBars == null || CurrentBars.Length < 2)
|
||||
{
|
||||
ctx.Count = 0;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
int dailyBarsAvailable = CurrentBars[1] + 1;
|
||||
int lookback = Math.Min(10, dailyBarsAvailable);
|
||||
|
||||
if (lookback < 2)
|
||||
{
|
||||
ctx.Count = 0;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
ctx.Highs = new double[lookback];
|
||||
ctx.Lows = new double[lookback];
|
||||
ctx.Closes = new double[lookback];
|
||||
ctx.Opens = new double[lookback];
|
||||
ctx.Volumes = new long[lookback];
|
||||
ctx.Count = lookback;
|
||||
|
||||
for (int i = 0; i < lookback; i++)
|
||||
{
|
||||
int barsAgo = lookback - 1 - i;
|
||||
ctx.Highs[i] = Highs[1][barsAgo];
|
||||
ctx.Lows[i] = Lows[1][barsAgo];
|
||||
ctx.Closes[i] = Closes[1][barsAgo];
|
||||
ctx.Opens[i] = Opens[1][barsAgo];
|
||||
ctx.Volumes[i] = (long)Volumes[1][barsAgo];
|
||||
}
|
||||
|
||||
double sumVol = 0.0;
|
||||
int intradayCount = 0;
|
||||
int maxBars = Math.Min(78, CurrentBar + 1);
|
||||
for (int i = 0; i < maxBars; i++)
|
||||
{
|
||||
sumVol += Volume[i];
|
||||
intradayCount++;
|
||||
}
|
||||
|
||||
ctx.AvgIntradayBarVolume = intradayCount > 0 ? sumVol / intradayCount : Volume[0];
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,31 @@ namespace NT8.Core.Intelligence
|
||||
/// </summary>
|
||||
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>
|
||||
/// Additional custom factor.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NT8.Core.Common.Models;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Intelligence
|
||||
{
|
||||
@@ -398,4 +399,625 @@ namespace NT8.Core.Intelligence
|
||||
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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,11 @@ namespace NT8.Strategies.Examples
|
||||
_factorCalculators.Add(new VolatilityRegimeFactorCalculator());
|
||||
_factorCalculators.Add(new TimeInSessionFactorCalculator());
|
||||
_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(
|
||||
"SimpleORBStrategy initialized with OR period {0} minutes and multiplier {1:F2}",
|
||||
@@ -191,6 +196,8 @@ namespace NT8.Strategies.Examples
|
||||
if (candidate == null)
|
||||
return null;
|
||||
|
||||
AttachDailyBarContext(candidate, bar, context);
|
||||
|
||||
var score = _scorer.CalculateScore(candidate, context, bar, _factorCalculators);
|
||||
var mode = _riskModeManager.GetCurrentMode();
|
||||
|
||||
@@ -349,6 +356,24 @@ namespace NT8.Strategies.Examples
|
||||
metadata.Add("orb_high", _openingRangeHigh);
|
||||
metadata.Add("orb_low", _openingRangeLow);
|
||||
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("multiplier", _stdDevMultiplier);
|
||||
metadata.Add("opening_range_start", _openingRangeStart);
|
||||
@@ -365,5 +390,40 @@ namespace NT8.Strategies.Examples
|
||||
"ORB breakout signal",
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user