Production hardening: kill switch, circuit breaker, trailing stops, log level, holiday calendar
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:
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user