Production hardening: kill switch, circuit breaker, trailing stops, log level, holiday calendar
Some checks failed
Build and Test / build (push) Has been cancelled

This commit is contained in:
2026-02-24 15:00:41 -05:00
parent 0e36fe5d23
commit a87152effb
50 changed files with 12849 additions and 752 deletions

View File

@@ -98,7 +98,7 @@ namespace NT8.Core.Execution
return null;
}
var newStopPrice = CalculateNewStopPrice(trailingStop.Config.Type, trailingStop.Position, currentPrice);
var newStopPrice = CalculateNewStopPrice(trailingStop.Config.Type, trailingStop.Position, currentPrice, trailingStop.Config);
// Only update if the stop has improved (moved in favorable direction)
var shouldUpdate = false;
@@ -149,55 +149,73 @@ namespace NT8.Core.Execution
/// <param name="type">Type of trailing stop</param>
/// <param name="position">Position information</param>
/// <param name="marketPrice">Current market price</param>
/// <param name="config">Trailing stop configuration</param>
/// <returns>Calculated stop price</returns>
public decimal CalculateNewStopPrice(StopType type, OMS.OrderStatus position, decimal marketPrice)
public decimal CalculateNewStopPrice(StopType type, OMS.OrderStatus position, decimal marketPrice, TrailingStopConfig config = null)
{
if (position == null)
throw new ArgumentNullException("position");
try
{
if (config == null)
{
config = new TrailingStopConfig(StopType.FixedTrailing, 8, 2m, true);
}
switch (type)
{
case StopType.FixedTrailing:
// Fixed trailing: trail by fixed number of ticks from high/low
if (position.Side == OMS.OrderSide.Buy)
{
// Long position: stop trails below highest high
return marketPrice - (position.AverageFillPrice - position.AverageFillPrice); // Simplified
}
else
{
// Short position: stop trails above lowest low
return marketPrice + (position.AverageFillPrice - position.AverageFillPrice); // Simplified
// Tick size is fixed here as a temporary default (ES/NQ standard).
// TODO: provide symbol-specific tick size via configuration.
var tickSize = 0.25m;
var trailingTicks = config.TrailingAmountTicks > 0 ? config.TrailingAmountTicks : 8;
var distance = trailingTicks * tickSize;
return position.Side == OMS.OrderSide.Buy
? marketPrice - distance
: marketPrice + distance;
}
case StopType.ATRTrailing:
// ATR trailing: trail by ATR multiple
return position.Side == OMS.OrderSide.Buy ?
marketPrice - (position.AverageFillPrice * 0.01m) : // Placeholder for ATR calculation
marketPrice + (position.AverageFillPrice * 0.01m); // Placeholder for ATR calculation
{
// ATR is approximated here until live ATR is provided in config/context.
var atrMultiplier = config.AtrMultiplier > 0 ? config.AtrMultiplier : 2.0m;
var estimatedAtr = position.AverageFillPrice * 0.005m;
var distance = atrMultiplier * estimatedAtr;
return position.Side == OMS.OrderSide.Buy
? marketPrice - distance
: marketPrice + distance;
}
case StopType.Chandelier:
// Chandelier: trail from highest high minus ATR * multiplier
return position.Side == OMS.OrderSide.Buy ?
marketPrice - (position.AverageFillPrice * 0.01m) : // Placeholder for chandelier calculation
marketPrice + (position.AverageFillPrice * 0.01m); // Placeholder for chandelier calculation
{
// Chandelier approximation uses the same ATR proxy until bar history is wired in.
var chanMultiplier = config.AtrMultiplier > 0 ? config.AtrMultiplier : 3.0m;
var estimatedAtr = position.AverageFillPrice * 0.005m;
var distance = chanMultiplier * estimatedAtr;
return position.Side == OMS.OrderSide.Buy
? marketPrice - distance
: marketPrice + distance;
}
case StopType.PercentageTrailing:
// Percentage trailing: trail by percentage of current price
var pctTrail = 0.02m; // Default 2% - in real impl this would come from config
var pctTrail = 0.02m;
return position.Side == OMS.OrderSide.Buy ?
marketPrice * (1 - pctTrail) :
marketPrice * (1 + pctTrail);
default:
// Fixed trailing as fallback
var tickSize = 0.25m; // Default tick size - should be configurable
var ticks = 8; // Default trailing ticks - should come from config
var tickSizeFallback = 0.25m;
var ticks = config.TrailingAmountTicks > 0 ? config.TrailingAmountTicks : 8;
return position.Side == OMS.OrderSide.Buy ?
marketPrice - (ticks * tickSize) :
marketPrice + (ticks * tickSize);
marketPrice - (ticks * tickSizeFallback) :
marketPrice + (ticks * tickSizeFallback);
}
}
catch (Exception ex)

View File

@@ -2,6 +2,18 @@ using System;
namespace NT8.Core.Logging
{
/// <summary>
/// Log severity levels.
/// </summary>
public enum LogLevel
{
Debug = 0,
Information = 1,
Warning = 2,
Error = 3,
Critical = 4
}
/// <summary>
/// Basic console logger implementation for .NET Framework 4.8
/// </summary>
@@ -9,43 +21,53 @@ namespace NT8.Core.Logging
{
private readonly string _categoryName;
/// <summary>
/// Minimum log level to write. Messages below this level are suppressed.
/// Default is Information.
/// </summary>
public LogLevel MinimumLevel { get; set; }
public BasicLogger(string categoryName = "")
{
_categoryName = categoryName;
MinimumLevel = LogLevel.Information;
}
public void LogDebug(string message, params object[] args)
{
WriteLog("DEBUG", message, args);
WriteLog(LogLevel.Debug, "DEBUG", message, args);
}
public void LogInformation(string message, params object[] args)
{
WriteLog("INFO", message, args);
WriteLog(LogLevel.Information, "INFO", message, args);
}
public void LogWarning(string message, params object[] args)
{
WriteLog("WARN", message, args);
WriteLog(LogLevel.Warning, "WARN", message, args);
}
public void LogError(string message, params object[] args)
{
WriteLog("ERROR", message, args);
WriteLog(LogLevel.Error, "ERROR", message, args);
}
public void LogCritical(string message, params object[] args)
{
WriteLog("CRITICAL", message, args);
WriteLog(LogLevel.Critical, "CRITICAL", message, args);
}
private void WriteLog(string level, string message, params object[] args)
private void WriteLog(LogLevel level, string levelLabel, string message, params object[] args)
{
if (level < MinimumLevel)
return;
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff");
var formattedMessage = args.Length > 0 ? String.Format(message, args) : message;
var category = !String.IsNullOrEmpty(_categoryName) ? String.Format("[{0}] ", _categoryName) : "";
Console.WriteLine(String.Format("{0} [{1}] {2}{3}", timestamp, level, category, formattedMessage));
Console.WriteLine(String.Format("{0} [{1}] {2}{3}", timestamp, levelLabel, category, formattedMessage));
}
}
}
}

View File

@@ -16,6 +16,35 @@ namespace NT8.Core.MarketData
private readonly Dictionary<string, SessionInfo> _sessionCache;
private readonly Dictionary<string, ContractRollInfo> _contractRollCache;
// CME US Futures holidays — markets closed all day on these dates.
// Update annually. Dates are in Eastern Time calendar dates.
private static readonly HashSet<DateTime> _cmeHolidays = new HashSet<DateTime>
{
// 2025 holidays
new DateTime(2025, 1, 1),
new DateTime(2025, 1, 20),
new DateTime(2025, 2, 17),
new DateTime(2025, 4, 18),
new DateTime(2025, 5, 26),
new DateTime(2025, 6, 19),
new DateTime(2025, 7, 4),
new DateTime(2025, 9, 1),
new DateTime(2025, 11, 27),
new DateTime(2025, 12, 25),
// 2026 holidays
new DateTime(2026, 1, 1),
new DateTime(2026, 1, 19),
new DateTime(2026, 2, 16),
new DateTime(2026, 4, 3),
new DateTime(2026, 5, 25),
new DateTime(2026, 6, 19),
new DateTime(2026, 7, 4),
new DateTime(2026, 9, 7),
new DateTime(2026, 11, 26),
new DateTime(2026, 12, 25)
};
// Helper class to store session times
private class SessionTimes
{
@@ -224,6 +253,13 @@ namespace NT8.Core.MarketData
try
{
// Markets are fully closed on CME holidays
if (IsCmeHoliday(time))
{
_logger.LogInformation("Holiday detected for {Symbol} on {Date} - market closed.", symbol, time.Date);
return false;
}
var sessionInfo = GetCurrentSession(symbol, time);
return sessionInfo.IsRegularHours;
}
@@ -234,6 +270,25 @@ namespace NT8.Core.MarketData
}
}
/// <summary>
/// Returns true if the given UTC date is a CME holiday (market closed all day).
/// </summary>
/// <param name="utcTime">UTC timestamp to evaluate</param>
/// <returns>True if the Eastern date is a known CME holiday, false otherwise</returns>
private static bool IsCmeHoliday(DateTime utcTime)
{
try
{
var eastern = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var estTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, eastern);
return _cmeHolidays.Contains(estTime.Date);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Checks if a contract is in its roll period
/// </summary>