| Overall Statistics |
|
Total Orders 13627 Average Win 0.08% Average Loss -0.06% Compounding Annual Return 9849739.115% Drawdown 88.100% Expectancy 0.146 Start Equity 1000000.0 End Equity 2467050.25 Net Profit 146.705% Sharpe Ratio 153238367634.002 Sortino Ratio 338359227592.585 Probabilistic Sharpe Ratio 88.519% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 1.31 Alpha 0 Beta 0 Annual Standard Deviation 7.448 Annual Variance 55.469 Information Ratio 153238367634.009 Tracking Error 7.448 Treynor Ratio 0 Total Fees $31425.51 Estimated Strategy Capacity $420000.00 Lowest Capacity Asset AMD R735QTJ8XC9X Portfolio Turnover 4619.18% Drawdown Recovery 12 |
#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Selection;
using QuantConnect.Api;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Commands;
using QuantConnect.Configuration;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.Shortable;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.OptionExercise;
using QuantConnect.Orders.Slippage;
using QuantConnect.Orders.TimeInForces;
using QuantConnect.Python;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Positions;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using Calendar = QuantConnect.Data.Consolidators.Calendar;
#endregion
namespace CoreAlgo.Architecture.Core.Attributes
{
/// <summary>
/// Attribute to mark properties as strategy parameters
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class StrategyParameterAttribute : Attribute
{
/// <summary>
/// Gets or sets the parameter name for QC GetParameter()
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the parameter description
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets the default value
/// </summary>
public object DefaultValue { get; set; }
/// <summary>
/// Gets or sets whether the parameter is required
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Gets or sets the minimum value (for numeric types)
/// </summary>
public object MinValue { get; set; }
/// <summary>
/// Gets or sets the maximum value (for numeric types)
/// </summary>
public object MaxValue { get; set; }
/// <summary>
/// Gets or sets the parameter group for organization
/// </summary>
public string Group { get; set; }
/// <summary>
/// Gets or sets the display order
/// </summary>
public int Order { get; set; }
/// <summary>
/// Creates a new instance of StrategyParameterAttribute
/// </summary>
public StrategyParameterAttribute()
{
}
/// <summary>
/// Creates a new instance of StrategyParameterAttribute with name and default value
/// </summary>
public StrategyParameterAttribute(string name, object defaultValue = null)
{
Name = name;
DefaultValue = defaultValue;
}
}
}using System.Collections.Generic;
namespace CoreAlgo.Architecture.Core.Configuration
{
/// <summary>
/// Embedded configuration constants that are guaranteed to sync with QuantConnect
/// </summary>
public static class EmbeddedConfiguration
{
/// <summary>
/// Development environment configuration as JSON string
/// </summary>
public const string DevelopmentConfig = @"{
""environment"": ""development"",
""algorithm"": {
""startDate"": ""2023-12-25"",
""endDate"": ""2024-12-25"",
""startingCash"": 100000,
""dataNormalizationMode"": ""Raw""
},
""strategies"": {
""default"": {
""maxActivePositions"": 1,
""maxOpenPositions"": 2,
""targetPremiumPct"": 0.01,
""scheduleStartTime"": ""09:30:00"",
""scheduleStopTime"": ""16:00:00"",
""scheduleFrequency"": ""00:05:00""
},
""SPXic"": {
""ticker"": ""SPX"",
""maxActivePositions"": 10,
""dte"": 0,
""putWingSize"": 10,
""callWingSize"": 10,
""minPremium"": 0.9,
""maxPremium"": 1.2
}
},
""riskManagement"": {
""maxDrawdownPercent"": 0.2,
""maxLeverageRatio"": 2.0,
""stopLossPercent"": 0.05
},
""logging"": {
""level"": ""Debug"",
""enableFileLogging"": true,
""logFilePath"": ""logs/corealgo-dev.log""
},
""backtesting"": {
""slippageModel"": ""ConstantSlippage"",
""feeModel"": ""InteractiveBrokersFeeModel"",
""fillModel"": ""ImmediateFillModel""
}
}";
/// <summary>
/// Production environment configuration as JSON string
/// </summary>
public const string ProductionConfig = @"{
""environment"": ""production"",
""algorithm"": {
""startDate"": ""2024-01-01"",
""endDate"": ""2024-12-31"",
""startingCash"": 1000000,
""dataNormalizationMode"": ""Adjusted""
},
""strategies"": {
""default"": {
""maxActivePositions"": 5,
""maxOpenPositions"": 10,
""targetPremiumPct"": 0.015,
""scheduleStartTime"": ""09:45:00"",
""scheduleStopTime"": ""15:30:00"",
""scheduleFrequency"": ""00:15:00""
},
""SPXic"": {
""ticker"": ""SPX"",
""maxActivePositions"": 20,
""dte"": 0,
""putWingSize"": 15,
""callWingSize"": 15,
""minPremium"": 0.8,
""maxPremium"": 1.5
}
},
""riskManagement"": {
""maxDrawdownPercent"": 0.15,
""maxLeverageRatio"": 1.5,
""stopLossPercent"": 0.03
},
""logging"": {
""level"": ""Information"",
""enableFileLogging"": true,
""logFilePath"": ""logs/corealgo-prod.log""
},
""backtesting"": {
""slippageModel"": ""AlphaStreamsSlippage"",
""feeModel"": ""AlphaStreamsFeeModel"",
""fillModel"": ""PartialFillModel""
},
""notifications"": {
""email"": {
""enabled"": true,
""recipients"": [""trading@example.com""],
""sendOnError"": true,
""sendOnTrade"": true
}
}
}";
/// <summary>
/// Gets configuration by environment name
/// </summary>
/// <param name="environment">Environment name (development, production)</param>
/// <returns>JSON configuration string</returns>
public static string GetConfiguration(string environment)
{
return environment?.ToLowerInvariant() switch
{
"development" or "dev" => DevelopmentConfig,
"production" or "prod" => ProductionConfig,
_ => DevelopmentConfig // Default to development
};
}
/// <summary>
/// Gets all available environment names
/// </summary>
/// <returns>List of environment names</returns>
public static List<string> GetAvailableEnvironments()
{
return new List<string> { "development", "production" };
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Architecture.Core.Configuration
{
/// <summary>
/// Lightweight entry restriction checks that work with StrategyConfig parameters.
/// QC-First approach - uses QC's native market data and portfolio state.
/// </summary>
public class EntryRestrictions
{
private readonly StrategyConfig _config;
private readonly QCAlgorithm _algorithm;
public EntryRestrictions(StrategyConfig config, QCAlgorithm algorithm)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_algorithm = algorithm ?? throw new ArgumentNullException(nameof(algorithm));
}
/// <summary>
/// Check if all entry restrictions are satisfied for a potential trade.
/// Returns true if trade is allowed, false otherwise.
/// </summary>
public bool CanEnterPosition(Symbol symbol, Slice slice, out string reason)
{
reason = string.Empty;
var shouldLog = _config.LogEntryRestrictions;
if (shouldLog)
{
_algorithm.Debug($"[DEBUG EntryRestrictions] Checking restrictions at {slice.Time}");
}
// 1. Trading Hours Check
var withinHours = IsWithinTradingHours(slice.Time);
if (shouldLog)
{
_algorithm.Debug($" [1/5] Trading Hours: {withinHours} | Current: {slice.Time.TimeOfDay} | Window: {_config.TradingStartTime}-{_config.TradingEndTime}");
}
if (!withinHours)
{
reason = $"Outside trading hours ({_config.TradingStartTime}-{_config.TradingEndTime})";
return false;
}
// 2. Max Positions Check (skip if MaxPositions <= 0, which means no limit)
var hasCapacity = true;
var currentPositionCount = 0;
if (_config.MaxPositions > 0)
{
hasCapacity = HasCapacityForNewPosition();
currentPositionCount = _algorithm.Portfolio.Where(kvp => kvp.Value.Invested).Count();
if (shouldLog)
{
_algorithm.Debug($" [2/5] Max Positions: {hasCapacity} | Current: {currentPositionCount}/{_config.MaxPositions}");
}
if (!hasCapacity)
{
reason = $"Max positions reached ({_config.MaxPositions})";
return false;
}
}
else if (shouldLog)
{
_algorithm.Debug($" [2/5] Max Positions: DISABLED (MaxPositions={_config.MaxPositions})");
}
// 3. Overlap Prevention Check (respects config)
if (_config.EnableOverlapPrevention && _config.MaxPositionsPerUnderlying > 0)
{
var existingCount = CountExistingPositions(symbol);
var limit = _config.MaxPositionsPerUnderlying;
if (shouldLog)
{
_algorithm.Debug($" [3/5] Overlap Prevention: Enabled | Existing: {existingCount}/{limit} | Mode={_config.OverlapPreventionMode}");
}
if (existingCount >= limit)
{
var mode = (_config.OverlapPreventionMode ?? "Block").ToUpperInvariant();
var msg = $"Underlying limit reached ({existingCount}/{limit}) for {symbol.Underlying ?? symbol}";
if (mode == "BLOCK")
{
reason = msg;
return false;
}
else if (mode == "WARN")
{
_algorithm.Debug($"[ENTRY WARN] {msg}");
}
else // LOG mode
{
_algorithm.Debug($"[ENTRY LOG] {msg}");
}
}
}
else if (shouldLog)
{
_algorithm.Debug($" [3/5] Overlap Prevention: DISABLED - allowing multiple entries");
}
// 4. Available Capital Check
var hasSufficientCapital = HasSufficientCapital();
if (shouldLog)
{
var portfolioValue = _algorithm.Portfolio.TotalPortfolioValue;
var requiredCapital = portfolioValue * _config.AllocationPerPosition;
var availableCash = _algorithm.Portfolio.Cash;
_algorithm.Debug($" [4/5] Capital: {hasSufficientCapital} | Available: ${availableCash:F2} | Required: ${requiredCapital:F2} (Allocation: {_config.AllocationPerPosition:P0})");
}
if (!hasSufficientCapital)
{
reason = "Insufficient capital for new position";
return false;
}
// 5. Volatility Check (if configured)
if (_config.MinImpliedVolatility > 0)
{
var meetsVol = MeetsVolatilityRequirement(symbol, slice);
if (shouldLog)
{
_algorithm.Debug($" [5/5] Volatility: {meetsVol} | Min IV: {_config.MinImpliedVolatility:P0}");
}
if (!meetsVol)
{
reason = $"Implied volatility below minimum ({_config.MinImpliedVolatility:P0})";
return false;
}
}
else if (shouldLog)
{
_algorithm.Debug($" [5/5] Volatility: DISABLED (MinIV={_config.MinImpliedVolatility:P0})");
}
// All checks passed
if (shouldLog)
{
_algorithm.Debug($"[DEBUG EntryRestrictions] ✓ ALL CHECKS PASSED");
}
return true;
}
/// <summary>
/// Check if entry is allowed for options based on delta requirements.
/// </summary>
public bool CanEnterOptionPosition(dynamic contract, out string reason)
{
reason = string.Empty;
// Check if Greeks are available
if (contract.Greeks == null)
{
reason = "Greeks not available for option contract";
return false;
}
var delta = Math.Abs(contract.Greeks.Delta);
// Check delta is within configured range
if (delta < _config.EntryDeltaMin || delta > _config.EntryDeltaMax)
{
reason = $"Delta {delta:F2} outside range [{_config.EntryDeltaMin:F2}-{_config.EntryDeltaMax:F2}]";
return false;
}
return true;
}
/// <summary>
/// Check if current time is within configured trading hours.
/// If both TradingStartTime and TradingEndTime are 00:00:00, the window is disabled (always returns true).
/// Handles cross-midnight windows (e.g., 22:00–02:00).
/// </summary>
private bool IsWithinTradingHours(DateTime currentTime)
{
var timeOfDay = currentTime.TimeOfDay;
// Special case: 00:00–00:00 means "no restriction" (always allow)
if (_config.TradingStartTime == TimeSpan.Zero && _config.TradingEndTime == TimeSpan.Zero)
{
return true;
}
// Handle cross-midnight windows (e.g., 22:00–02:00)
if (_config.TradingEndTime < _config.TradingStartTime)
{
// Allow if time >= start OR time <= end
return timeOfDay >= _config.TradingStartTime || timeOfDay <= _config.TradingEndTime;
}
// Normal window (e.g., 09:30–15:30)
return timeOfDay >= _config.TradingStartTime && timeOfDay <= _config.TradingEndTime;
}
/// <summary>
/// Check if we have capacity for a new position based on MaxPositions.
/// Note: This should only be called when MaxPositions > 0 (checked in CanEnterPosition)
/// </summary>
private bool HasCapacityForNewPosition()
{
var currentPositions = _algorithm.Portfolio
.Where(kvp => kvp.Value.Invested)
.Count();
// MaxPositions should be > 0 when this is called
// If MaxPositions is 0 or negative, caller should skip this check
return currentPositions < _config.MaxPositions;
}
/// <summary>
/// Count existing positions for the given symbol or its underlying.
/// For options, counts all positions on the same underlying.
/// For equities, counts positions on that specific symbol.
/// </summary>
private int CountExistingPositions(Symbol symbol)
{
// For options, check positions on the underlying
if (symbol.SecurityType == SecurityType.Option)
{
var underlying = symbol.Underlying;
return _algorithm.Portfolio.Count(kvp =>
kvp.Value.Invested &&
(kvp.Key == underlying || (kvp.Key.SecurityType == SecurityType.Option && kvp.Key.Underlying == underlying)));
}
// For equities/futures, count direct positions on this symbol
return _algorithm.Portfolio.Count(kvp => kvp.Key == symbol && kvp.Value.Invested);
}
/// <summary>
/// Check if we have sufficient capital for a new position.
/// </summary>
private bool HasSufficientCapital()
{
var portfolioValue = _algorithm.Portfolio.TotalPortfolioValue;
var requiredCapital = portfolioValue * _config.AllocationPerPosition;
var availableCash = _algorithm.Portfolio.Cash;
// Need at least the allocation amount in cash
return availableCash >= requiredCapital;
}
/// <summary>
/// Check if the symbol meets minimum volatility requirements.
/// This is primarily for options strategies.
/// </summary>
private bool MeetsVolatilityRequirement(Symbol symbol, Slice slice)
{
// For options, IV check would be done in the strategy template
// where the option chain data is available
// For now, return true to allow entry restrictions to focus on position/capital limits
return true;
}
/// <summary>
/// Get a summary of current restriction status.
/// Useful for logging and debugging.
/// </summary>
public Dictionary<string, object> GetRestrictionStatus()
{
var currentTime = _algorithm.Time;
var activePositions = _algorithm.Portfolio.Where(kvp => kvp.Value.Invested).Count();
var portfolioValue = _algorithm.Portfolio.TotalPortfolioValue;
var availableCash = _algorithm.Portfolio.Cash;
return new Dictionary<string, object>
{
["CurrentTime"] = currentTime,
["TradingHoursActive"] = IsWithinTradingHours(currentTime),
["ActivePositions"] = activePositions,
["MaxPositions"] = _config.MaxPositions,
["AvailableSlots"] = _config.MaxPositions - activePositions,
["PortfolioValue"] = portfolioValue,
["AvailableCash"] = availableCash,
["AllocationPerPosition"] = _config.AllocationPerPosition,
["RequiredCapitalPerPosition"] = portfolioValue * _config.AllocationPerPosition
};
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Architecture.Core.Configuration
{
/// <summary>
/// Lightweight exit restriction checks that work with StrategyConfig parameters.
/// QC-First approach - uses QC's native portfolio metrics and market data.
/// </summary>
public class ExitRestrictions
{
private readonly StrategyConfig _config;
private readonly QCAlgorithm _algorithm;
private readonly Dictionary<Symbol, DateTime> _positionEntryTimes;
public ExitRestrictions(StrategyConfig config, QCAlgorithm algorithm)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_algorithm = algorithm ?? throw new ArgumentNullException(nameof(algorithm));
_positionEntryTimes = new Dictionary<Symbol, DateTime>();
}
/// <summary>
/// Record when a position was entered for time-based exit rules.
/// </summary>
public void RecordPositionEntry(Symbol symbol, DateTime entryTime)
{
_positionEntryTimes[symbol] = entryTime;
}
/// <summary>
/// Check if a position should be exited based on configured rules.
/// Returns true if position should be closed, false otherwise.
/// </summary>
public bool ShouldExitPosition(Symbol symbol, Slice slice, out string reason)
{
reason = string.Empty;
var holding = _algorithm.Portfolio[symbol];
if (!holding.Invested)
{
reason = "No position to exit";
return false;
}
// 1. Profit Target Check
if (IsProfitTargetReached(holding))
{
reason = $"Profit target reached ({holding.UnrealizedProfitPercent:P2} >= {_config.ProfitTarget:P2})";
return true;
}
// 2. Stop Loss Check
if (IsStopLossTriggered(holding))
{
reason = $"Stop loss triggered ({holding.UnrealizedProfitPercent:P2} <= {_config.StopLoss:P2})";
return true;
}
// 3. Time-Based Exit Check
if (IsMaxTimeReached(symbol, slice.Time))
{
var daysInTrade = GetDaysInTrade(symbol, slice.Time);
reason = $"Max time in trade reached ({daysInTrade:F1} days >= {_config.MaxDaysInTrade} days)";
return true;
}
// 4. Trading Hours Exit (optional - exit outside hours)
if (!IsWithinTradingHours(slice.Time))
{
// Optional: some strategies may want to exit outside trading hours
// For now, we don't force exit, but strategies can override
}
// No exit conditions met
return false;
}
/// <summary>
/// Check if an option position should be exited based on delta.
/// </summary>
public bool ShouldExitOptionPosition(Symbol symbol, dynamic contract, out string reason)
{
reason = string.Empty;
// First check standard exit rules
if (ShouldExitPosition(symbol, _algorithm.CurrentSlice, out reason))
{
return true;
}
// Check option-specific exit rules
if (contract.Greeks != null)
{
var delta = Math.Abs(contract.Greeks.Delta);
// Exit if delta drops below threshold
if (delta <= _config.ExitDelta)
{
reason = $"Delta exit triggered ({delta:F3} <= {_config.ExitDelta:F3})";
return true;
}
}
// Check days to expiration
var daysToExpiry = (contract.Expiry - _algorithm.Time).TotalDays;
if (daysToExpiry <= 1) // Exit if expiring tomorrow
{
reason = $"Near expiration ({daysToExpiry:F1} days)";
return true;
}
return false;
}
/// <summary>
/// Check if profit target has been reached.
/// Returns false if profit target is disabled (0).
/// </summary>
private bool IsProfitTargetReached(SecurityHolding holding)
{
// Skip check if profit target is disabled
if (_config.ProfitTarget == 0) return false;
return holding.UnrealizedProfitPercent >= _config.ProfitTarget;
}
/// <summary>
/// Check if stop loss has been triggered.
/// Returns false if stop loss is disabled (0).
/// </summary>
private bool IsStopLossTriggered(SecurityHolding holding)
{
// Skip check if stop loss is disabled
if (_config.StopLoss == 0) return false;
return holding.UnrealizedProfitPercent <= _config.StopLoss;
}
/// <summary>
/// Check if position has been held for maximum allowed time.
/// </summary>
private bool IsMaxTimeReached(Symbol symbol, DateTime currentTime)
{
if (!_positionEntryTimes.ContainsKey(symbol))
return false;
var daysInTrade = (currentTime - _positionEntryTimes[symbol]).TotalDays;
return daysInTrade >= _config.MaxDaysInTrade;
}
/// <summary>
/// Get the number of days a position has been held.
/// </summary>
private double GetDaysInTrade(Symbol symbol, DateTime currentTime)
{
if (!_positionEntryTimes.ContainsKey(symbol))
return 0;
return (currentTime - _positionEntryTimes[symbol]).TotalDays;
}
/// <summary>
/// Check if current time is within trading hours.
/// </summary>
private bool IsWithinTradingHours(DateTime currentTime)
{
var timeOfDay = currentTime.TimeOfDay;
return timeOfDay >= _config.TradingStartTime && timeOfDay <= _config.TradingEndTime;
}
/// <summary>
/// Get exit urgency level (for prioritizing exits).
/// Higher values mean more urgent exit.
/// </summary>
public double GetExitUrgency(Symbol symbol)
{
var holding = _algorithm.Portfolio[symbol];
if (!holding.Invested)
return 0;
var urgency = 0.0;
// Stop loss is most urgent (if enabled)
if (_config.StopLoss != 0 && holding.UnrealizedProfitPercent <= _config.StopLoss)
{
urgency = 1.0;
}
// Profit target is high priority (if enabled)
else if (_config.ProfitTarget != 0 && holding.UnrealizedProfitPercent >= _config.ProfitTarget)
{
urgency = 0.8;
}
// Time-based exit increases urgency as we approach max days
else if (_positionEntryTimes.ContainsKey(symbol))
{
var daysInTrade = GetDaysInTrade(symbol, _algorithm.Time);
var timeUrgency = Math.Min(daysInTrade / _config.MaxDaysInTrade, 1.0);
urgency = Math.Max(urgency, timeUrgency * 0.6);
}
return urgency;
}
/// <summary>
/// Get a summary of exit conditions for all positions.
/// Useful for logging and strategy decisions.
/// </summary>
public List<PositionExitStatus> GetAllPositionExitStatus()
{
var results = new List<PositionExitStatus>();
foreach (var kvp in _algorithm.Portfolio.Where(p => p.Value.Invested))
{
var symbol = kvp.Key;
var holding = kvp.Value;
var shouldExit = ShouldExitPosition(symbol, _algorithm.CurrentSlice, out var reason);
var urgency = GetExitUrgency(symbol);
var daysHeld = _positionEntryTimes.ContainsKey(symbol)
? GetDaysInTrade(symbol, _algorithm.Time)
: 0;
results.Add(new PositionExitStatus
{
Symbol = symbol,
UnrealizedProfitPercent = holding.UnrealizedProfitPercent,
DaysHeld = daysHeld,
ShouldExit = shouldExit,
ExitReason = reason,
ExitUrgency = urgency
});
}
return results.OrderByDescending(r => r.ExitUrgency).ToList();
}
/// <summary>
/// Clear entry time for a symbol after position is closed.
/// </summary>
public void ClearPositionEntry(Symbol symbol)
{
_positionEntryTimes.Remove(symbol);
}
}
/// <summary>
/// Status information for position exit decisions.
/// </summary>
public class PositionExitStatus
{
public Symbol Symbol { get; set; }
public decimal UnrealizedProfitPercent { get; set; }
public double DaysHeld { get; set; }
public bool ShouldExit { get; set; }
public string ExitReason { get; set; }
public double ExitUrgency { get; set; }
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Orders;
using QuantConnect.Scheduling;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// Tracks combo orders as single atomic units for progressive net pricing.
/// Unlike single-leg orders, combo orders are managed through their collective net price.
/// </summary>
public class ComboOrderTracker
{
/// <summary>
/// List of order tickets returned by ComboLimitOrder (one per leg)
/// </summary>
public List<OrderTicket> ComboTickets { get; }
/// <summary>
/// The legs that define this combo order structure
/// </summary>
public List<Leg> Legs { get; }
/// <summary>
/// Current net limit price for the combo order
/// </summary>
public decimal CurrentNetPrice { get; private set; }
/// <summary>
/// Last market quote used for pricing calculations
/// </summary>
public ComboQuote LastQuote { get; private set; }
/// <summary>
/// Overall direction of the combo order (Buy = net debit, Sell = net credit)
/// </summary>
public OrderDirection ComboDirection { get; }
/// <summary>
/// Smart pricing mode being used for this combo
/// </summary>
public SmartPricingMode PricingMode { get; }
/// <summary>
/// Current attempt number for progressive pricing (1-based)
/// </summary>
public int AttemptNumber { get; private set; }
/// <summary>
/// When this combo order was first placed
/// </summary>
public DateTime StartTime { get; }
/// <summary>
/// Scheduled event for the next pricing update (if any)
/// </summary>
public ScheduledEvent ScheduledEvent { get; set; }
/// <summary>
/// Whether all legs of the combo have been filled
/// </summary>
public bool IsCompletelyFilled => ComboTickets.All(ticket =>
ticket.Status == OrderStatus.Filled);
/// <summary>
/// Whether any leg has been partially filled
/// </summary>
public bool HasPartialFills => ComboTickets.Any(ticket =>
ticket.Status == OrderStatus.PartiallyFilled ||
(ticket.Status == OrderStatus.Filled && ticket.QuantityFilled != ticket.Quantity));
/// <summary>
/// Whether the combo order is still active (not filled, cancelled, or invalid)
/// </summary>
public bool IsActive => ComboTickets.Any(ticket =>
ticket.Status == OrderStatus.Submitted ||
ticket.Status == OrderStatus.PartiallyFilled);
/// <summary>
/// Gets the primary order ticket (first leg) for identification purposes
/// </summary>
public OrderTicket PrimaryTicket => ComboTickets.FirstOrDefault();
/// <summary>
/// Gets the primary order ID for logging and tracking
/// </summary>
public int PrimaryOrderId => PrimaryTicket?.OrderId ?? -1;
/// <summary>
/// Creates a new combo order tracker
/// </summary>
/// <param name="comboTickets">Order tickets returned by ComboLimitOrder</param>
/// <param name="legs">Legs that define the combo structure</param>
/// <param name="initialQuote">Initial market quote used for pricing</param>
/// <param name="comboDirection">Overall direction of the combo order</param>
/// <param name="pricingMode">Smart pricing mode to use</param>
/// <param name="initialNetPrice">Initial net limit price</param>
public ComboOrderTracker(List<OrderTicket> comboTickets, List<Leg> legs, ComboQuote initialQuote,
OrderDirection comboDirection, SmartPricingMode pricingMode, decimal initialNetPrice)
{
ComboTickets = new List<OrderTicket>(comboTickets ?? throw new ArgumentNullException(nameof(comboTickets)));
Legs = new List<Leg>(legs ?? throw new ArgumentNullException(nameof(legs)));
LastQuote = initialQuote ?? throw new ArgumentNullException(nameof(initialQuote));
ComboDirection = comboDirection;
PricingMode = pricingMode;
CurrentNetPrice = initialNetPrice;
AttemptNumber = 1;
StartTime = DateTime.UtcNow;
if (ComboTickets.Count == 0)
throw new ArgumentException("Combo tickets cannot be empty", nameof(comboTickets));
if (Legs.Count == 0)
throw new ArgumentException("Legs cannot be empty", nameof(legs));
}
/// <summary>
/// Updates the net price and quote information for the next pricing attempt
/// </summary>
/// <param name="newNetPrice">New net limit price</param>
/// <param name="newQuote">Updated market quote</param>
public void UpdateNetPrice(decimal newNetPrice, ComboQuote newQuote)
{
CurrentNetPrice = newNetPrice;
LastQuote = newQuote ?? throw new ArgumentNullException(nameof(newQuote));
AttemptNumber++;
}
/// <summary>
/// Records a partial fill event for tracking purposes
/// </summary>
/// <param name="orderEvent">Order event representing the partial fill</param>
public void UpdatePartialFill(OrderEvent orderEvent)
{
// For combo orders, we mainly track this for logging
// The actual fill logic is handled by QuantConnect's combo order system
// We could enhance this later to track per-leg fill status if needed
}
/// <summary>
/// Gets summary information about this combo order for logging
/// </summary>
/// <returns>Formatted string with combo order details</returns>
public string GetSummary()
{
var status = IsCompletelyFilled ? "FILLED" :
HasPartialFills ? "PARTIAL" :
IsActive ? "ACTIVE" : "INACTIVE";
var runtime = DateTime.UtcNow - StartTime;
return $"Combo Order {PrimaryOrderId}: {Legs.Count} legs, " +
$"NetPrice=${CurrentNetPrice:F2}, Attempt={AttemptNumber}, " +
$"Status={status}, Runtime={runtime.TotalSeconds:F0}s, " +
$"Mode={PricingMode}";
}
/// <summary>
/// Gets detailed status of all leg tickets
/// </summary>
/// <returns>String with status of each leg</returns>
public string GetLegStatus()
{
var legStatuses = ComboTickets.Select((ticket, index) =>
$"Leg{index + 1}[{ticket.OrderId}]: {ticket.Status} " +
$"({ticket.QuantityFilled}/{ticket.Quantity})");
return string.Join(", ", legStatuses);
}
/// <summary>
/// Calculates total runtime since combo order was placed
/// </summary>
/// <returns>Time elapsed since order placement</returns>
public TimeSpan GetRuntime()
{
return DateTime.UtcNow - StartTime;
}
/// <summary>
/// Determines if this combo order should continue with progressive pricing
/// </summary>
/// <param name="maxAttempts">Maximum number of attempts allowed</param>
/// <param name="maxRuntime">Maximum runtime before giving up</param>
/// <returns>True if progressive pricing should continue</returns>
public bool ShouldContinuePricing(int maxAttempts, TimeSpan maxRuntime)
{
if (!IsActive)
return false;
if (AttemptNumber >= maxAttempts)
return false;
if (GetRuntime() >= maxRuntime)
return false;
return true;
}
}
}using System;
using System.Collections.Generic;
using QuantConnect.Orders;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// Pricing engine for combo orders that calculates net limit prices and progressive pricing
/// for multi-leg options strategies. Applies smart pricing logic to the entire combo as a unit.
/// </summary>
public class ComboPricingEngine
{
private readonly SmartPricingMode _mode;
private readonly decimal _maxNetSpreadWidth;
/// <summary>
/// Creates a new combo pricing engine with the specified mode and constraints
/// </summary>
/// <param name="mode">Smart pricing mode (Normal, Fast, Patient)</param>
/// <param name="maxNetSpreadWidth">Maximum acceptable net spread width</param>
public ComboPricingEngine(SmartPricingMode mode, decimal maxNetSpreadWidth = 5.0m)
{
_mode = mode;
_maxNetSpreadWidth = maxNetSpreadWidth;
}
/// <summary>
/// Calculates the initial net limit price for a combo order based on current market conditions
/// </summary>
/// <param name="legs">List of legs in the combo order</param>
/// <param name="comboQuote">Current market quote for the combo</param>
/// <param name="orderDirection">Overall direction of the combo (Buy = paying net debit, Sell = receiving net credit)</param>
/// <returns>Initial net limit price, or null if combo should not be priced intelligently</returns>
public decimal? CalculateInitialComboPrice(List<Leg> legs, ComboQuote comboQuote, OrderDirection orderDirection)
{
if (legs == null || legs.Count == 0 || comboQuote == null || !comboQuote.IsValid)
return null;
// Skip smart pricing if net spread is too wide
if (comboQuote.NetSpread > _maxNetSpreadWidth)
return null;
// Start at net mid-price for both buy and sell combos
// This gives us the best initial price while still being realistic
return comboQuote.NetMid;
}
/// <summary>
/// Calculates the next progressive price step for an existing combo order
/// </summary>
/// <param name="currentNetPrice">Current net limit price of the combo order</param>
/// <param name="comboQuote">Current market quote for the combo</param>
/// <param name="orderDirection">Overall direction of the combo</param>
/// <param name="attemptNumber">Current attempt number (1-based)</param>
/// <returns>Next progressive net price, or null if no more steps available</returns>
public decimal? CalculateNextComboPrice(decimal currentNetPrice, ComboQuote comboQuote,
OrderDirection orderDirection, int attemptNumber)
{
if (comboQuote == null || !comboQuote.IsValid)
return null;
var maxAttempts = GetMaxAttempts();
if (attemptNumber >= maxAttempts)
return null;
// Calculate how far to progress toward the target price
var progressionRatio = CalculateProgressionRatio(attemptNumber, maxAttempts);
decimal targetPrice;
decimal startPrice = comboQuote.NetMid;
if (orderDirection == OrderDirection.Buy)
{
// For buy combos (net debit): progress from mid toward ask
// We're willing to pay more to get filled
targetPrice = comboQuote.NetAsk;
}
else
{
// For sell combos (net credit): progress from mid toward bid
// We're willing to accept less to get filled
targetPrice = comboQuote.NetBid;
}
// Calculate next price using linear interpolation
var nextPrice = startPrice + (targetPrice - startPrice) * progressionRatio;
// Ensure we don't go backwards or exceed target
if (orderDirection == OrderDirection.Buy)
{
nextPrice = Math.Max(nextPrice, currentNetPrice);
nextPrice = Math.Min(nextPrice, targetPrice);
}
else
{
nextPrice = Math.Min(nextPrice, currentNetPrice);
nextPrice = Math.Max(nextPrice, targetPrice);
}
// Round to nearest cent for practical execution
return Math.Round(nextPrice, 2);
}
/// <summary>
/// Gets the pricing interval between attempts based on the current mode
/// </summary>
/// <returns>Time interval between pricing updates</returns>
public TimeSpan GetPricingInterval()
{
return _mode switch
{
SmartPricingMode.Fast => TimeSpan.FromSeconds(5), // 3 steps over 15 seconds
SmartPricingMode.Normal => TimeSpan.FromSeconds(10), // 4 steps over 40 seconds
SmartPricingMode.Patient => TimeSpan.FromSeconds(20), // 5 steps over 100 seconds
_ => TimeSpan.FromSeconds(10)
};
}
/// <summary>
/// Gets the maximum number of pricing attempts for the current mode
/// </summary>
/// <returns>Maximum number of attempts</returns>
public int GetMaxAttempts()
{
return _mode switch
{
SmartPricingMode.Fast => 3,
SmartPricingMode.Normal => 4,
SmartPricingMode.Patient => 5,
_ => 4
};
}
/// <summary>
/// Validates if the combo pricing engine should attempt to improve the price
/// </summary>
/// <param name="comboQuote">Current combo market quote</param>
/// <param name="orderDirection">Overall combo direction</param>
/// <returns>True if smart combo pricing should be attempted</returns>
public bool ShouldAttemptComboPricing(ComboQuote comboQuote, OrderDirection orderDirection)
{
if (comboQuote == null || !comboQuote.IsValid)
return false;
// Skip if net spread is too wide (slippage would be excessive)
if (comboQuote.NetSpread > _maxNetSpreadWidth)
return false;
// Skip if net spread is too narrow (little room for improvement)
if (comboQuote.NetSpread < 0.10m)
return false;
return true;
}
/// <summary>
/// Determines the overall direction of a combo order based on its legs
/// </summary>
/// <param name="legs">List of legs in the combo</param>
/// <returns>Buy if net debit expected, Sell if net credit expected</returns>
public static OrderDirection DetermineComboDirection(List<Leg> legs)
{
if (legs == null || legs.Count == 0)
return OrderDirection.Buy; // Default
// Simple heuristic: if we have more short legs than long legs, it's likely a credit spread
// This works for most common strategies (Iron Condor, Credit Spreads, etc.)
int longLegs = 0;
int shortLegs = 0;
foreach (var leg in legs)
{
if (leg.Quantity > 0)
longLegs += Math.Abs(leg.Quantity);
else if (leg.Quantity < 0)
shortLegs += Math.Abs(leg.Quantity);
}
// If more short than long, assume it's a credit spread (we receive money)
return shortLegs > longLegs ? OrderDirection.Sell : OrderDirection.Buy;
}
/// <summary>
/// Calculates the progression ratio for moving from mid-price toward target
/// </summary>
/// <param name="attemptNumber">Current attempt (1-based)</param>
/// <param name="maxAttempts">Maximum number of attempts</param>
/// <returns>Ratio from 0.0 (start) to 1.0 (target)</returns>
private decimal CalculateProgressionRatio(int attemptNumber, int maxAttempts)
{
if (attemptNumber <= 0 || maxAttempts <= 1)
return 0m;
// Linear progression: attempt 1 = 25%, attempt 2 = 50%, etc.
var ratio = (decimal)attemptNumber / maxAttempts;
// Cap at 90% to avoid hitting exact bid/ask (leave room for market movement)
return Math.Min(ratio, 0.90m);
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Orders;
using QuantConnect.Securities;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// Represents net bid/ask pricing for a combo order based on individual leg quotes.
/// Used for calculating intelligent limit prices for multi-leg options strategies.
/// </summary>
public class ComboQuote
{
/// <summary>
/// Net bid price for the combo order (sum of leg bids adjusted for direction)
/// </summary>
public decimal NetBid { get; }
/// <summary>
/// Net ask price for the combo order (sum of leg asks adjusted for direction)
/// </summary>
public decimal NetAsk { get; }
/// <summary>
/// Mid-point between net bid and net ask
/// </summary>
public decimal NetMid => (NetBid + NetAsk) / 2;
/// <summary>
/// Net spread width (ask - bid)
/// </summary>
public decimal NetSpread => NetAsk - NetBid;
/// <summary>
/// Individual leg quotes used to calculate net pricing
/// </summary>
public Dictionary<Symbol, Quote> LegQuotes { get; }
/// <summary>
/// Timestamp when this quote was calculated
/// </summary>
public DateTime Timestamp { get; }
/// <summary>
/// Whether all legs have valid quotes (bid > 0 and ask > bid)
/// </summary>
public bool IsValid => LegQuotes.Values.All(q => q.Bid > 0 && q.Ask > q.Bid) && NetSpread >= 0;
/// <summary>
/// Creates a new ComboQuote from individual leg quotes and their quantities
/// </summary>
/// <param name="legs">List of legs with symbols and quantities</param>
/// <param name="legQuotes">Quotes for each leg symbol</param>
/// <param name="timestamp">When this quote was calculated</param>
public ComboQuote(List<Leg> legs, Dictionary<Symbol, Quote> legQuotes, DateTime timestamp)
{
if (legs == null || legs.Count == 0)
throw new ArgumentException("Legs cannot be null or empty", nameof(legs));
if (legQuotes == null)
throw new ArgumentNullException(nameof(legQuotes));
LegQuotes = new Dictionary<Symbol, Quote>(legQuotes);
Timestamp = timestamp;
// Calculate net bid and ask based on leg direction
decimal netBid = 0;
decimal netAsk = 0;
foreach (var leg in legs)
{
if (!legQuotes.TryGetValue(leg.Symbol, out var quote))
{
throw new ArgumentException($"Missing quote for leg symbol {leg.Symbol}", nameof(legQuotes));
}
// For buy legs (positive quantity): we pay the ask and receive the bid
// For sell legs (negative quantity): we receive the bid and pay the ask
if (leg.Quantity > 0)
{
// Buying this leg: pay ask price, receive bid price
netAsk += quote.Ask * Math.Abs(leg.Quantity);
netBid += quote.Bid * Math.Abs(leg.Quantity);
}
else if (leg.Quantity < 0)
{
// Selling this leg: receive bid price, pay ask price
netBid -= quote.Ask * Math.Abs(leg.Quantity); // We receive when selling (positive for us)
netAsk -= quote.Bid * Math.Abs(leg.Quantity); // We pay when selling (less negative = better)
}
}
NetBid = netBid;
NetAsk = netAsk;
}
/// <summary>
/// Creates a ComboQuote from a list of legs and securities (fetches current quotes)
/// </summary>
/// <param name="legs">List of legs with symbols and quantities</param>
/// <param name="securities">Securities collection to get current quotes from</param>
/// <returns>ComboQuote with current market data, or null if quotes unavailable</returns>
public static ComboQuote FromSecurities(List<Leg> legs, SecurityManager securities)
{
if (legs == null || legs.Count == 0)
return null;
try
{
var legQuotes = new Dictionary<Symbol, Quote>();
var timestamp = DateTime.UtcNow;
foreach (var leg in legs)
{
var security = securities[leg.Symbol];
var quote = GetCurrentQuote(security);
if (quote == null)
return null; // Missing quote for any leg invalidates the entire combo quote
legQuotes[leg.Symbol] = quote;
}
return new ComboQuote(legs, legQuotes, timestamp);
}
catch
{
return null;
}
}
/// <summary>
/// Gets the current market quote for a security (similar to SmartPricingExecutionModel logic)
/// </summary>
private static Quote GetCurrentQuote(Security security)
{
try
{
// Try to get the most recent quote using Cache.GetData for QuoteBar
var quoteBar = security.Cache.GetData<QuantConnect.Data.Market.QuoteBar>();
if (quoteBar != null && quoteBar.Bid.Close > 0 && quoteBar.Ask.Close > 0)
{
return new Quote(quoteBar.Bid.Close, quoteBar.Ask.Close);
}
// Fall back to using last price if quote is not available
var price = security.Price;
if (price > 0)
{
// Estimate spread as 0.5% of price (conservative estimate for options)
var estimatedSpread = price * 0.005m;
return new Quote(price - estimatedSpread / 2, price + estimatedSpread / 2);
}
return null;
}
catch
{
return null;
}
}
/// <summary>
/// Returns a string representation of the combo quote
/// </summary>
public override string ToString()
{
return $"ComboQuote: NetBid={NetBid:F2}, NetAsk={NetAsk:F2}, NetMid={NetMid:F2}, NetSpread={NetSpread:F2}, Valid={IsValid}";
}
/// <summary>
/// Determines if this combo quote is stale based on age threshold
/// </summary>
/// <param name="maxAge">Maximum age before considering stale</param>
/// <returns>True if the quote is older than maxAge</returns>
public bool IsStale(TimeSpan maxAge)
{
return DateTime.UtcNow - Timestamp > maxAge;
}
}
}using System;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// Fast SmartPricing strategy: 3 steps over 15 seconds (5-second intervals)
///
/// This aggressive approach is suitable for high-volume trading or when quick fills
/// are more important than optimal pricing. Moves quickly toward market prices.
///
/// Timing: Step every 5 seconds for up to 15 seconds total
/// Progression: Mid → 50% → 80% → Ask/Bid
/// </summary>
public class FastPricingStrategy : PricingStrategy
{
/// <summary>
/// Fast pricing mode identifier
/// </summary>
public override SmartPricingMode Mode => SmartPricingMode.Fast;
/// <summary>
/// 3 progressive pricing steps (fewer steps for faster execution)
/// </summary>
protected override int StepCount => 3;
/// <summary>
/// 5-second intervals between steps (faster progression)
/// </summary>
protected override TimeSpan StepInterval => TimeSpan.FromSeconds(5);
/// <summary>
/// Aggressive pricing calculation for Fast mode with rapid progression
/// </summary>
public override decimal? CalculateNextPrice(decimal currentPrice, Quote quote, OrderDirection orderDirection, int attemptNumber)
{
if (quote == null)
throw new ArgumentNullException(nameof(quote));
if (attemptNumber > StepCount)
return null;
// Fast progression: aggressive moves toward market
decimal targetPrice;
var halfSpread = quote.Spread / 2;
switch (attemptNumber)
{
case 1:
// Start at mid-spread (already set in initial order)
return null;
case 2:
// Move 50% toward market price (more aggressive than Normal)
targetPrice = orderDirection == OrderDirection.Buy
? quote.Price + (halfSpread * 0.50m)
: quote.Price - (halfSpread * 0.50m);
break;
case 3:
// Move 80% toward market price
targetPrice = orderDirection == OrderDirection.Buy
? quote.Price + (halfSpread * 0.80m)
: quote.Price - (halfSpread * 0.80m);
break;
default:
// Final attempt: go to market (ask/bid)
targetPrice = orderDirection == OrderDirection.Buy ? quote.Ask : quote.Bid;
break;
}
// Ensure we don't exceed market boundaries
if (orderDirection == OrderDirection.Buy)
{
targetPrice = Math.Min(targetPrice, quote.Ask);
}
else
{
targetPrice = Math.Max(targetPrice, quote.Bid);
}
// Fast mode uses smaller minimum change threshold for quicker updates
var priceChange = Math.Abs(targetPrice - currentPrice);
var minChange = quote.Spread * 0.03m; // Minimum 3% of spread movement
return priceChange >= minChange ? targetPrice : null;
}
/// <summary>
/// Fast mode should be more selective about when to use SmartPricing
/// since it moves quickly toward market prices
/// </summary>
public override bool ShouldAttemptPricing(Quote quote, OrderDirection orderDirection)
{
if (!base.ShouldAttemptPricing(quote, orderDirection))
return false;
// Fast mode works best with reasonable spreads where quick progression makes sense
// Minimum spread of $0.15 for options (higher than Normal)
if (quote.Spread < 0.15m)
return false;
// Maximum spread of 3% of mid-price (more restrictive than Normal)
if (quote.Spread > quote.Price * 0.03m)
return false;
// Don't use fast mode for very low-priced options (under $1)
if (quote.Price < 1.0m)
return false;
return true;
}
}
}using System;
using System.Collections.Generic;
using QuantConnect.Orders;
using QuantConnect.Data.Market;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// Interface for SmartPricing engine that provides progressive pricing capabilities
/// for improving options spread fill rates using QuantConnect's native execution framework.
/// </summary>
public interface ISmartPricingEngine
{
/// <summary>
/// Gets the current pricing mode configuration
/// </summary>
SmartPricingMode Mode { get; }
/// <summary>
/// Calculates the initial limit price for a new order based on current market conditions
/// </summary>
/// <param name="quote">Current bid/ask quote for the security</param>
/// <param name="orderDirection">Direction of the order (Buy/Sell)</param>
/// <returns>Initial limit price starting at mid-spread</returns>
decimal CalculateInitialPrice(Quote quote, OrderDirection orderDirection);
/// <summary>
/// Calculates the next progressive price step for an existing order
/// </summary>
/// <param name="currentPrice">Current limit price of the order</param>
/// <param name="quote">Current bid/ask quote for the security</param>
/// <param name="orderDirection">Direction of the order (Buy/Sell)</param>
/// <param name="attemptNumber">Current attempt number (1-based)</param>
/// <returns>Next progressive price, or null if no more steps available</returns>
decimal? CalculateNextPrice(decimal currentPrice, Quote quote, OrderDirection orderDirection, int attemptNumber);
/// <summary>
/// Gets the time interval between pricing attempts for the current mode
/// </summary>
/// <returns>Time interval in seconds</returns>
TimeSpan GetPricingInterval();
/// <summary>
/// Gets the maximum number of pricing attempts for the current mode
/// </summary>
/// <returns>Maximum number of attempts</returns>
int GetMaxAttempts();
/// <summary>
/// Validates if the pricing engine should attempt to improve the price
/// </summary>
/// <param name="quote">Current market quote</param>
/// <param name="orderDirection">Order direction</param>
/// <returns>True if pricing improvement should be attempted</returns>
bool ShouldAttemptPricing(Quote quote, OrderDirection orderDirection);
/// <summary>
/// Calculates the initial net limit price for a combo order based on current market conditions
/// </summary>
/// <param name="legs">List of legs in the combo order</param>
/// <param name="comboQuote">Current market quote for the combo</param>
/// <param name="orderDirection">Overall direction of the combo (Buy = paying net debit, Sell = receiving net credit)</param>
/// <returns>Initial net limit price, or null if combo should not be priced intelligently</returns>
decimal? CalculateInitialComboPrice(List<Leg> legs, ComboQuote comboQuote, OrderDirection orderDirection);
/// <summary>
/// Calculates the next progressive price step for an existing combo order
/// </summary>
/// <param name="currentNetPrice">Current net limit price of the combo order</param>
/// <param name="comboQuote">Current market quote for the combo</param>
/// <param name="orderDirection">Overall direction of the combo</param>
/// <param name="attemptNumber">Current attempt number (1-based)</param>
/// <returns>Next progressive net price, or null if no more steps available</returns>
decimal? CalculateNextComboPrice(decimal currentNetPrice, ComboQuote comboQuote, OrderDirection orderDirection, int attemptNumber);
/// <summary>
/// Validates if the combo pricing engine should attempt to improve the price
/// </summary>
/// <param name="comboQuote">Current combo market quote</param>
/// <param name="orderDirection">Overall combo direction</param>
/// <returns>True if smart combo pricing should be attempted</returns>
bool ShouldAttemptComboPricing(ComboQuote comboQuote, OrderDirection orderDirection);
}
/// <summary>
/// Order direction for SmartPricing calculations
/// </summary>
public enum OrderDirection
{
/// <summary>
/// Buy order (long position)
/// </summary>
Buy,
/// <summary>
/// Sell order (short position)
/// </summary>
Sell
}
/// <summary>
/// Smart pricing modes with different aggressiveness levels
/// </summary>
public enum SmartPricingMode
{
/// <summary>
/// SmartPricing disabled - use standard execution
/// </summary>
Off,
/// <summary>
/// Normal mode: 4 steps over 40 seconds (10s intervals)
/// </summary>
Normal,
/// <summary>
/// Fast mode: 3 steps over 15 seconds (5s intervals)
/// </summary>
Fast,
/// <summary>
/// Patient mode: 5 steps over 100 seconds (20s intervals)
/// </summary>
Patient
}
}using System;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// Normal SmartPricing strategy: 4 steps over 40 seconds (10-second intervals)
///
/// This is the default balanced approach that provides good execution improvement
/// without being too aggressive or too patient.
///
/// Timing: Step every 10 seconds for up to 40 seconds total
/// Progression: Mid → 25% → 50% → 75% → Ask/Bid
/// </summary>
public class NormalPricingStrategy : PricingStrategy
{
/// <summary>
/// Normal pricing mode identifier
/// </summary>
public override SmartPricingMode Mode => SmartPricingMode.Normal;
/// <summary>
/// 4 progressive pricing steps
/// </summary>
protected override int StepCount => 4;
/// <summary>
/// 10-second intervals between steps
/// </summary>
protected override TimeSpan StepInterval => TimeSpan.FromSeconds(10);
/// <summary>
/// Enhanced pricing calculation for Normal mode with optimized progression
/// </summary>
public override decimal? CalculateNextPrice(decimal currentPrice, Quote quote, OrderDirection orderDirection, int attemptNumber)
{
if (quote == null)
throw new ArgumentNullException(nameof(quote));
if (attemptNumber > StepCount)
return null;
// Normal progression: more conservative steps toward market
decimal targetPrice;
var halfSpread = quote.Spread / 2;
switch (attemptNumber)
{
case 1:
// Start at mid-spread (already set in initial order)
return null;
case 2:
// Move 25% toward market price
targetPrice = orderDirection == OrderDirection.Buy
? quote.Price + (halfSpread * 0.25m)
: quote.Price - (halfSpread * 0.25m);
break;
case 3:
// Move 50% toward market price
targetPrice = orderDirection == OrderDirection.Buy
? quote.Price + (halfSpread * 0.50m)
: quote.Price - (halfSpread * 0.50m);
break;
case 4:
// Move 75% toward market price
targetPrice = orderDirection == OrderDirection.Buy
? quote.Price + (halfSpread * 0.75m)
: quote.Price - (halfSpread * 0.75m);
break;
default:
// Final attempt: go to market (ask/bid)
targetPrice = orderDirection == OrderDirection.Buy ? quote.Ask : quote.Bid;
break;
}
// Ensure we don't exceed market boundaries
if (orderDirection == OrderDirection.Buy)
{
targetPrice = Math.Min(targetPrice, quote.Ask);
}
else
{
targetPrice = Math.Max(targetPrice, quote.Bid);
}
// Only update if meaningful price change
var priceChange = Math.Abs(targetPrice - currentPrice);
var minChange = quote.Spread * 0.05m; // Minimum 5% of spread movement
return priceChange >= minChange ? targetPrice : null;
}
/// <summary>
/// Normal mode should attempt pricing for most reasonable market conditions
/// </summary>
public override bool ShouldAttemptPricing(Quote quote, OrderDirection orderDirection)
{
if (!base.ShouldAttemptPricing(quote, orderDirection))
return false;
// Normal mode is suitable for most options with reasonable spreads
// Minimum spread of $0.10 for options
if (quote.Spread < 0.10m)
return false;
// Maximum spread of 5% of mid-price
if (quote.Spread > quote.Price * 0.05m)
return false;
return true;
}
}
}using System;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// Patient SmartPricing strategy: 5 steps over 100 seconds (20-second intervals)
///
/// This conservative approach is suitable for less liquid options or when getting
/// the best possible price is more important than speed. Takes time to find optimal fills.
///
/// Timing: Step every 20 seconds for up to 100 seconds total
/// Progression: Mid → 15% → 30% → 50% → 70% → Ask/Bid
/// </summary>
public class PatientPricingStrategy : PricingStrategy
{
/// <summary>
/// Patient pricing mode identifier
/// </summary>
public override SmartPricingMode Mode => SmartPricingMode.Patient;
/// <summary>
/// 5 progressive pricing steps (more steps for patient execution)
/// </summary>
protected override int StepCount => 5;
/// <summary>
/// 20-second intervals between steps (slower progression)
/// </summary>
protected override TimeSpan StepInterval => TimeSpan.FromSeconds(20);
/// <summary>
/// Conservative pricing calculation for Patient mode with gradual progression
/// </summary>
public override decimal? CalculateNextPrice(decimal currentPrice, Quote quote, OrderDirection orderDirection, int attemptNumber)
{
if (quote == null)
throw new ArgumentNullException(nameof(quote));
if (attemptNumber > StepCount)
return null;
// Patient progression: gradual moves toward market
decimal targetPrice;
var halfSpread = quote.Spread / 2;
switch (attemptNumber)
{
case 1:
// Start at mid-spread (already set in initial order)
return null;
case 2:
// Move 15% toward market price (very conservative)
targetPrice = orderDirection == OrderDirection.Buy
? quote.Price + (halfSpread * 0.15m)
: quote.Price - (halfSpread * 0.15m);
break;
case 3:
// Move 30% toward market price
targetPrice = orderDirection == OrderDirection.Buy
? quote.Price + (halfSpread * 0.30m)
: quote.Price - (halfSpread * 0.30m);
break;
case 4:
// Move 50% toward market price
targetPrice = orderDirection == OrderDirection.Buy
? quote.Price + (halfSpread * 0.50m)
: quote.Price - (halfSpread * 0.50m);
break;
case 5:
// Move 70% toward market price
targetPrice = orderDirection == OrderDirection.Buy
? quote.Price + (halfSpread * 0.70m)
: quote.Price - (halfSpread * 0.70m);
break;
default:
// Final attempt: go to market (ask/bid)
targetPrice = orderDirection == OrderDirection.Buy ? quote.Ask : quote.Bid;
break;
}
// Ensure we don't exceed market boundaries
if (orderDirection == OrderDirection.Buy)
{
targetPrice = Math.Min(targetPrice, quote.Ask);
}
else
{
targetPrice = Math.Max(targetPrice, quote.Bid);
}
// Patient mode uses higher minimum change threshold for more meaningful updates
var priceChange = Math.Abs(targetPrice - currentPrice);
var minChange = quote.Spread * 0.08m; // Minimum 8% of spread movement
return priceChange >= minChange ? targetPrice : null;
}
/// <summary>
/// Patient mode should work with wider spreads and less liquid options
/// </summary>
public override bool ShouldAttemptPricing(Quote quote, OrderDirection orderDirection)
{
if (!base.ShouldAttemptPricing(quote, orderDirection))
return false;
// Patient mode is designed for wider spreads where gradual progression helps
// Minimum spread of $0.20 for options
if (quote.Spread < 0.20m)
return false;
// Maximum spread of 8% of mid-price (more tolerant than other modes)
if (quote.Spread > quote.Price * 0.08m)
return false;
// Patient mode works well for higher-priced options where small improvements matter
// No minimum price restriction (unlike Fast mode)
return true;
}
}
}using System;
using System.Collections.Generic;
using QuantConnect.Orders;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// Base class for SmartPricing strategies that implement progressive pricing logic
/// </summary>
public abstract class PricingStrategy : ISmartPricingEngine
{
/// <summary>
/// Gets the pricing mode for this strategy
/// </summary>
public abstract SmartPricingMode Mode { get; }
/// <summary>
/// Gets the number of pricing steps for this strategy
/// </summary>
protected abstract int StepCount { get; }
/// <summary>
/// Gets the time interval between pricing steps
/// </summary>
protected abstract TimeSpan StepInterval { get; }
/// <summary>
/// Calculates the initial limit price at mid-spread
/// </summary>
public virtual decimal CalculateInitialPrice(Quote quote, OrderDirection orderDirection)
{
if (quote == null)
throw new ArgumentNullException(nameof(quote));
// Start at mid-spread for better execution probability
return quote.Price;
}
/// <summary>
/// Calculates the next progressive price step moving toward ask/bid
/// </summary>
public virtual decimal? CalculateNextPrice(decimal currentPrice, Quote quote, OrderDirection orderDirection, int attemptNumber)
{
if (quote == null)
throw new ArgumentNullException(nameof(quote));
if (attemptNumber > StepCount)
return null; // No more steps available
// Calculate progression percentage based on attempt number
var progressionPct = (decimal)attemptNumber / StepCount;
// Move from mid-spread toward ask (buy) or bid (sell)
decimal targetPrice;
if (orderDirection == OrderDirection.Buy)
{
// Progress from mid toward ask
targetPrice = quote.Price + (quote.Spread / 2 * progressionPct);
// Don't exceed ask price
targetPrice = Math.Min(targetPrice, quote.Ask);
}
else
{
// Progress from mid toward bid
targetPrice = quote.Price - (quote.Spread / 2 * progressionPct);
// Don't go below bid price
targetPrice = Math.Max(targetPrice, quote.Bid);
}
// Only update if price has meaningfully changed
var priceChange = Math.Abs(targetPrice - currentPrice);
var minChange = quote.Spread * 0.1m; // Minimum 10% of spread movement
return priceChange >= minChange ? targetPrice : null;
}
/// <summary>
/// Gets the time interval between pricing attempts
/// </summary>
public virtual TimeSpan GetPricingInterval()
{
return StepInterval;
}
/// <summary>
/// Gets the maximum number of pricing attempts
/// </summary>
public virtual int GetMaxAttempts()
{
return StepCount;
}
/// <summary>
/// Validates if pricing should be attempted based on market conditions
/// </summary>
public virtual bool ShouldAttemptPricing(Quote quote, OrderDirection orderDirection)
{
if (quote == null)
return false;
// Don't attempt pricing if spread is too narrow (less than $0.05)
if (quote.Spread < 0.05m)
return false;
// Don't attempt pricing if spread is too wide (more than 10% of mid-price)
if (quote.Spread > quote.Price * 0.10m)
return false;
// Valid for pricing
return true;
}
/// <summary>
/// Calculates the initial net limit price for a combo order based on current market conditions
/// </summary>
/// <param name="legs">List of legs in the combo order</param>
/// <param name="comboQuote">Current market quote for the combo</param>
/// <param name="orderDirection">Overall direction of the combo (Buy = paying net debit, Sell = receiving net credit)</param>
/// <returns>Initial net limit price, or null if combo should not be priced intelligently</returns>
public virtual decimal? CalculateInitialComboPrice(List<Leg> legs, ComboQuote comboQuote, OrderDirection orderDirection)
{
if (legs == null || legs.Count == 0 || comboQuote == null || !comboQuote.IsValid)
return null;
// Use the embedded ComboPricingEngine logic for consistency
var comboPricingEngine = new ComboPricingEngine(Mode, 5.0m); // Default max spread width
return comboPricingEngine.CalculateInitialComboPrice(legs, comboQuote, orderDirection);
}
/// <summary>
/// Calculates the next progressive price step for an existing combo order
/// </summary>
/// <param name="currentNetPrice">Current net limit price of the combo order</param>
/// <param name="comboQuote">Current market quote for the combo</param>
/// <param name="orderDirection">Overall direction of the combo</param>
/// <param name="attemptNumber">Current attempt number (1-based)</param>
/// <returns>Next progressive net price, or null if no more steps available</returns>
public virtual decimal? CalculateNextComboPrice(decimal currentNetPrice, ComboQuote comboQuote, OrderDirection orderDirection, int attemptNumber)
{
if (comboQuote == null || !comboQuote.IsValid)
return null;
// Use the embedded ComboPricingEngine logic for consistency
var comboPricingEngine = new ComboPricingEngine(Mode, 5.0m); // Default max spread width
return comboPricingEngine.CalculateNextComboPrice(currentNetPrice, comboQuote, orderDirection, attemptNumber);
}
/// <summary>
/// Validates if the combo pricing engine should attempt to improve the price
/// </summary>
/// <param name="comboQuote">Current combo market quote</param>
/// <param name="orderDirection">Overall combo direction</param>
/// <returns>True if smart combo pricing should be attempted</returns>
public virtual bool ShouldAttemptComboPricing(ComboQuote comboQuote, OrderDirection orderDirection)
{
if (comboQuote == null || !comboQuote.IsValid)
return false;
// Use the embedded ComboPricingEngine logic for consistency
var comboPricingEngine = new ComboPricingEngine(Mode, 5.0m); // Default max spread width
return comboPricingEngine.ShouldAttemptComboPricing(comboQuote, orderDirection);
}
/// <summary>
/// Calculates the price improvement benefit for a given attempt
/// </summary>
protected decimal CalculatePriceImprovement(Quote quote, OrderDirection orderDirection, int attemptNumber)
{
var aggressivePrice = orderDirection == OrderDirection.Buy ? quote.Ask : quote.Bid;
var midPrice = quote.Price;
// Calculate how much better mid-price is compared to aggressive price
var maxImprovement = Math.Abs(aggressivePrice - midPrice);
// Progressive improvement - less improvement with each step
var progressionPct = (decimal)(attemptNumber - 1) / StepCount;
var currentImprovement = maxImprovement * (1 - progressionPct);
return currentImprovement;
}
}
}using System;
using QuantConnect.Orders;
using QuantConnect.Scheduling;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// Tracks the state of an order undergoing SmartPricing progression
/// </summary>
public class SmartOrderTracker
{
/// <summary>
/// The QuantConnect OrderTicket being tracked
/// </summary>
public OrderTicket OrderTicket { get; }
/// <summary>
/// Direction of the order (Buy/Sell)
/// </summary>
public OrderDirection OrderDirection { get; }
/// <summary>
/// SmartPricing mode used for this order
/// </summary>
public SmartPricingMode PricingMode { get; }
/// <summary>
/// Current limit price of the order
/// </summary>
public decimal CurrentPrice { get; set; }
/// <summary>
/// Current attempt number (1-based)
/// </summary>
public int AttemptNumber { get; set; }
/// <summary>
/// Time when the order was first placed
/// </summary>
public DateTime StartTime { get; }
/// <summary>
/// Initial market quote when order was placed
/// </summary>
public Quote InitialQuote { get; }
/// <summary>
/// Most recent market quote
/// </summary>
public Quote CurrentQuote { get; private set; }
/// <summary>
/// Scheduled event for the next price update
/// </summary>
public ScheduledEvent ScheduledEvent { get; set; }
/// <summary>
/// Total quantity that has been filled
/// </summary>
public decimal FilledQuantity { get; private set; }
/// <summary>
/// Remaining quantity to be filled
/// </summary>
public decimal RemainingQuantity => Math.Abs(OrderTicket.Quantity) - FilledQuantity;
/// <summary>
/// Whether the order has been partially filled
/// </summary>
public bool IsPartiallyFilled => FilledQuantity > 0 && FilledQuantity < Math.Abs(OrderTicket.Quantity);
/// <summary>
/// History of price attempts for analysis
/// </summary>
public PricingAttempt[] PriceHistory { get; private set; }
public SmartOrderTracker(OrderTicket orderTicket, Quote initialQuote, OrderDirection direction, SmartPricingMode mode, decimal initialPrice)
{
OrderTicket = orderTicket ?? throw new ArgumentNullException(nameof(orderTicket));
InitialQuote = initialQuote ?? throw new ArgumentNullException(nameof(initialQuote));
OrderDirection = direction;
PricingMode = mode;
CurrentPrice = initialPrice;
CurrentQuote = initialQuote;
AttemptNumber = 1;
StartTime = DateTime.UtcNow;
FilledQuantity = 0;
// Initialize price history
PriceHistory = new PricingAttempt[GetMaxAttempts(mode)];
PriceHistory[0] = new PricingAttempt(1, CurrentPrice, initialQuote, StartTime);
}
/// <summary>
/// Updates the order price and tracking information
/// </summary>
public void UpdatePrice(decimal newPrice, Quote newQuote)
{
CurrentPrice = newPrice;
CurrentQuote = newQuote;
AttemptNumber++;
// Record this attempt in history
if (AttemptNumber <= PriceHistory.Length)
{
PriceHistory[AttemptNumber - 1] = new PricingAttempt(AttemptNumber, newPrice, newQuote, DateTime.UtcNow);
}
}
/// <summary>
/// Updates tracking information when a partial fill occurs
/// </summary>
public void UpdatePartialFill(OrderEvent orderEvent)
{
if (orderEvent.Status == OrderStatus.PartiallyFilled)
{
FilledQuantity += Math.Abs(orderEvent.FillQuantity);
}
}
/// <summary>
/// Gets performance metrics for this order's pricing progression
/// </summary>
public SmartPricingMetrics GetMetrics()
{
var elapsed = DateTime.UtcNow - StartTime;
var initialSpread = InitialQuote.Spread;
var currentSpread = CurrentQuote?.Spread ?? initialSpread;
// Calculate price improvement relative to initial aggressive price
var aggressivePrice = OrderDirection == OrderDirection.Buy ? InitialQuote.Ask : InitialQuote.Bid;
var priceImprovement = OrderDirection == OrderDirection.Buy ?
aggressivePrice - CurrentPrice : CurrentPrice - aggressivePrice;
return new SmartPricingMetrics
{
OrderId = OrderTicket.OrderId,
Symbol = OrderTicket.Symbol.Value,
Direction = OrderDirection,
Mode = PricingMode,
AttemptNumber = AttemptNumber,
TotalAttempts = GetMaxAttempts(PricingMode),
ElapsedTime = elapsed,
InitialPrice = PriceHistory[0].Price,
CurrentPrice = CurrentPrice,
PriceImprovement = priceImprovement,
InitialSpread = initialSpread,
CurrentSpread = currentSpread,
FilledQuantity = FilledQuantity,
RemainingQuantity = RemainingQuantity,
IsPartiallyFilled = IsPartiallyFilled,
IsCompleted = OrderTicket.Status == OrderStatus.Filled || OrderTicket.Status == OrderStatus.Canceled
};
}
/// <summary>
/// Gets the limit price from an order ticket
/// </summary>
private static decimal GetLimitPrice(OrderTicket orderTicket)
{
// For now, assume the initial price was set correctly
// In practice, this will be set properly by the execution model
return 0; // Will be overridden by actual limit price from the order
}
/// <summary>
/// Gets the maximum attempts for a pricing mode
/// </summary>
private static int GetMaxAttempts(SmartPricingMode mode)
{
return mode switch
{
SmartPricingMode.Fast => 3,
SmartPricingMode.Normal => 4,
SmartPricingMode.Patient => 5,
_ => 1
};
}
}
/// <summary>
/// Represents a single pricing attempt in the progression
/// </summary>
public class PricingAttempt
{
public int AttemptNumber { get; }
public decimal Price { get; }
public Quote MarketQuote { get; }
public DateTime Timestamp { get; }
public PricingAttempt(int attemptNumber, decimal price, Quote marketQuote, DateTime timestamp)
{
AttemptNumber = attemptNumber;
Price = price;
MarketQuote = marketQuote;
Timestamp = timestamp;
}
}
/// <summary>
/// Performance metrics for SmartPricing orders
/// </summary>
public class SmartPricingMetrics
{
public int OrderId { get; set; }
public string Symbol { get; set; }
public OrderDirection Direction { get; set; }
public SmartPricingMode Mode { get; set; }
public int AttemptNumber { get; set; }
public int TotalAttempts { get; set; }
public TimeSpan ElapsedTime { get; set; }
public decimal InitialPrice { get; set; }
public decimal CurrentPrice { get; set; }
public decimal PriceImprovement { get; set; }
public decimal InitialSpread { get; set; }
public decimal CurrentSpread { get; set; }
public decimal FilledQuantity { get; set; }
public decimal RemainingQuantity { get; set; }
public bool IsPartiallyFilled { get; set; }
public bool IsCompleted { get; set; }
}
}using System;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// Factory for creating SmartPricing engines based on configuration
/// </summary>
public static class SmartPricingEngineFactory
{
/// <summary>
/// Creates a SmartPricing engine for the specified mode
/// </summary>
/// <param name="mode">The pricing mode to use</param>
/// <returns>Configured pricing engine instance</returns>
public static ISmartPricingEngine Create(SmartPricingMode mode)
{
return mode switch
{
SmartPricingMode.Fast => new FastPricingStrategy(),
SmartPricingMode.Normal => new NormalPricingStrategy(),
SmartPricingMode.Patient => new PatientPricingStrategy(),
SmartPricingMode.Off => throw new InvalidOperationException("Cannot create engine for SmartPricingMode.Off"),
_ => throw new ArgumentException($"Unknown SmartPricing mode: {mode}", nameof(mode))
};
}
/// <summary>
/// Creates a SmartPricing engine from a string configuration value
/// </summary>
/// <param name="modeString">String representation of the mode ("Normal", "Fast", "Patient", "Off")</param>
/// <returns>Configured pricing engine instance</returns>
public static ISmartPricingEngine Create(string modeString)
{
if (string.IsNullOrWhiteSpace(modeString))
return Create(SmartPricingMode.Normal); // Default mode
var mode = ParseMode(modeString);
return Create(mode);
}
/// <summary>
/// Parses a string into a SmartPricingMode enum value
/// </summary>
/// <param name="modeString">String to parse</param>
/// <returns>Parsed SmartPricingMode</returns>
public static SmartPricingMode ParseMode(string modeString)
{
if (string.IsNullOrWhiteSpace(modeString))
return SmartPricingMode.Normal;
return modeString.ToUpperInvariant() switch
{
"FAST" => SmartPricingMode.Fast,
"NORMAL" => SmartPricingMode.Normal,
"PATIENT" => SmartPricingMode.Patient,
"OFF" => SmartPricingMode.Off,
"DISABLED" => SmartPricingMode.Off,
"FALSE" => SmartPricingMode.Off,
_ => SmartPricingMode.Normal // Default fallback
};
}
/// <summary>
/// Gets a descriptive summary of a pricing mode
/// </summary>
/// <param name="mode">The pricing mode</param>
/// <returns>Human-readable description</returns>
public static string GetModeDescription(SmartPricingMode mode)
{
return mode switch
{
SmartPricingMode.Off => "Disabled - uses standard execution",
SmartPricingMode.Fast => "Fast - 3 steps over 15 seconds (aggressive)",
SmartPricingMode.Normal => "Normal - 4 steps over 40 seconds (balanced)",
SmartPricingMode.Patient => "Patient - 5 steps over 100 seconds (conservative)",
_ => "Unknown mode"
};
}
/// <summary>
/// Validates if a mode string is supported
/// </summary>
/// <param name="modeString">String to validate</param>
/// <returns>True if the mode is supported</returns>
public static bool IsValidMode(string modeString)
{
if (string.IsNullOrWhiteSpace(modeString))
return true; // Default is valid
var upper = modeString.ToUpperInvariant();
return upper == "FAST" || upper == "NORMAL" || upper == "PATIENT" ||
upper == "OFF" || upper == "DISABLED" || upper == "FALSE";
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Orders;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.Core.Execution
{
/// <summary>
/// QC-First SmartPricing Execution Model that extends QuantConnect's IExecutionModel
/// to improve fill rates on options spreads through intelligent limit order progression.
///
/// This execution model starts orders at mid-spread and progressively moves toward
/// ask (for buys) or bid (for sells) over time to improve fill rates while maintaining
/// good execution prices.
/// </summary>
public class SmartPricingExecutionModel : ExecutionModel
{
private readonly IAlgorithmContext _context;
private readonly ISmartPricingEngine _pricingEngine;
private readonly Dictionary<int, SmartOrderTracker> _activeOrders;
private readonly HashSet<ScheduledEvent> _scheduledEvents;
/// <summary>
/// Initializes a new instance of the SmartPricingExecutionModel
/// </summary>
/// <param name="context">Algorithm context for logging and market data access</param>
/// <param name="pricingEngine">Pricing engine for progressive pricing logic</param>
public SmartPricingExecutionModel(IAlgorithmContext context, ISmartPricingEngine pricingEngine)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_pricingEngine = pricingEngine ?? throw new ArgumentNullException(nameof(pricingEngine));
_activeOrders = new Dictionary<int, SmartOrderTracker>();
_scheduledEvents = new HashSet<ScheduledEvent>();
}
/// <summary>
/// Executes market orders immediately and starts progressive pricing for limit orders
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="targets">Portfolio targets to execute</param>
public override void Execute(QCAlgorithm algorithm, IPortfolioTarget[] targets)
{
try
{
// Check if SmartPricing is enabled
if (_pricingEngine.Mode == SmartPricingMode.Off)
{
// Use standard execution model when SmartPricing is disabled
base.Execute(algorithm, targets);
return;
}
((dynamic)_context.Logger).Debug($"SmartPricing: Processing {targets.Length} portfolio targets in {_pricingEngine.Mode} mode");
foreach (var target in targets)
{
var security = algorithm.Securities[target.Symbol];
var quantity = target.Quantity - security.Holdings.Quantity;
if (quantity == 0)
continue;
// Get current market quote
var quote = GetCurrentQuote(security);
if (quote == null || !_pricingEngine.ShouldAttemptPricing(quote, quantity > 0 ? OrderDirection.Buy : OrderDirection.Sell))
{
// Fall back to market order if we can't get quote or shouldn't use smart pricing
algorithm.MarketOrder(target.Symbol, quantity, tag: "SmartPricing:Fallback", asynchronous: true);
continue;
}
// Calculate initial smart price
var orderDirection = quantity > 0 ? OrderDirection.Buy : OrderDirection.Sell;
var initialPrice = _pricingEngine.CalculateInitialPrice(quote, orderDirection);
// Place initial limit order
var orderTicket = algorithm.LimitOrder(target.Symbol, quantity, initialPrice,
tag: $"SmartPricing:{_pricingEngine.Mode}:Initial", asynchronous: true);
if (orderTicket != null)
{
// Track this order for progressive pricing
var tracker = new SmartOrderTracker(orderTicket, quote, orderDirection, _pricingEngine.Mode, initialPrice);
_activeOrders[orderTicket.OrderId] = tracker;
// Schedule first pricing update
ScheduleNextPricingUpdate(algorithm, tracker);
((dynamic)_context.Logger).Debug($"SmartPricing: Started {orderDirection} order {orderTicket.OrderId} at ${initialPrice:F2} " +
$"(Mid: ${quote.Price:F2}, Spread: ${quote.Ask - quote.Bid:F2})");
}
}
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"SmartPricing execution error: {ex.Message}");
// Fall back to standard execution on error
base.Execute(algorithm, targets);
}
}
/// <summary>
/// Handles order events to track fills and update order state
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="orderEvent">The order event</param>
public override void OnOrderEvent(QCAlgorithm algorithm, OrderEvent orderEvent)
{
if (!_activeOrders.TryGetValue(orderEvent.OrderId, out var tracker))
return;
switch (orderEvent.Status)
{
case OrderStatus.Filled:
((dynamic)_context.Logger).Debug($"SmartPricing: Order {orderEvent.OrderId} filled at ${orderEvent.FillPrice:F2} " +
$"after {tracker.AttemptNumber} attempts");
CleanupOrder(tracker);
break;
case OrderStatus.PartiallyFilled:
((dynamic)_context.Logger).Debug($"SmartPricing: Order {orderEvent.OrderId} partially filled " +
$"({orderEvent.FillQuantity}/{tracker.OrderTicket.Quantity})");
tracker.UpdatePartialFill(orderEvent);
break;
case OrderStatus.Canceled:
case OrderStatus.Invalid:
((dynamic)_context.Logger).Warning($"SmartPricing: Order {orderEvent.OrderId} {orderEvent.Status}");
CleanupOrder(tracker);
break;
}
}
/// <summary>
/// Schedules the next pricing update for an order
/// </summary>
private void ScheduleNextPricingUpdate(QCAlgorithm algorithm, SmartOrderTracker tracker)
{
var interval = _pricingEngine.GetPricingInterval();
var updateTime = algorithm.Time.Add(interval);
var scheduledEvent = algorithm.Schedule.On(algorithm.DateRules.On(updateTime.Date),
algorithm.TimeRules.At(updateTime.Hour, updateTime.Minute, updateTime.Second),
() => UpdateOrderPrice(algorithm, tracker));
_scheduledEvents.Add(scheduledEvent);
tracker.ScheduledEvent = scheduledEvent;
}
/// <summary>
/// Updates the price of an active order using progressive pricing
/// </summary>
private void UpdateOrderPrice(QCAlgorithm algorithm, SmartOrderTracker tracker)
{
try
{
// Remove the scheduled event
if (tracker.ScheduledEvent != null)
{
_scheduledEvents.Remove(tracker.ScheduledEvent);
tracker.ScheduledEvent = null;
}
// Check if order is still active
if (!_activeOrders.ContainsKey(tracker.OrderTicket.OrderId) ||
tracker.OrderTicket.Status == OrderStatus.Filled)
{
return;
}
// Get current market quote
var security = algorithm.Securities[tracker.OrderTicket.Symbol];
var currentQuote = GetCurrentQuote(security);
if (currentQuote == null)
{
((dynamic)_context.Logger).Warning($"SmartPricing: No quote available for {tracker.OrderTicket.Symbol}, canceling order {tracker.OrderTicket.OrderId}");
tracker.OrderTicket.Cancel("No quote available");
CleanupOrder(tracker);
return;
}
// Calculate next price
var nextPrice = _pricingEngine.CalculateNextPrice(
tracker.CurrentPrice, currentQuote, tracker.OrderDirection, tracker.AttemptNumber + 1);
if (nextPrice.HasValue)
{
// Update order price
var updateResult = tracker.OrderTicket.Update(new UpdateOrderFields { LimitPrice = nextPrice.Value });
if (updateResult.IsSuccess)
{
tracker.UpdatePrice(nextPrice.Value, currentQuote);
((dynamic)_context.Logger).Debug($"SmartPricing: Updated order {tracker.OrderTicket.OrderId} " +
$"to ${nextPrice.Value:F2} (attempt {tracker.AttemptNumber})");
// Schedule next update if we haven't reached max attempts
if (tracker.AttemptNumber < _pricingEngine.GetMaxAttempts())
{
ScheduleNextPricingUpdate(algorithm, tracker);
}
else
{
((dynamic)_context.Logger).Debug($"SmartPricing: Order {tracker.OrderTicket.OrderId} reached max attempts, keeping final price");
}
}
else
{
((dynamic)_context.Logger).Warning($"SmartPricing: Failed to update order {tracker.OrderTicket.OrderId}: {updateResult.ErrorMessage}");
}
}
else
{
// No more price improvements available, keep current order
((dynamic)_context.Logger).Debug($"SmartPricing: No more price improvements for order {tracker.OrderTicket.OrderId}");
}
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"SmartPricing: Error updating order {tracker.OrderTicket.OrderId}: {ex.Message}");
}
}
/// <summary>
/// Gets the current market quote for a security
/// </summary>
private Quote GetCurrentQuote(Security security)
{
try
{
// Try to get the most recent quote - use Cache.GetData for QuoteBar
var quoteBar = security.Cache.GetData<QuoteBar>();
if (quoteBar != null && quoteBar.Bid.Close > 0 && quoteBar.Ask.Close > 0)
{
return new Quote(quoteBar.Bid.Close, quoteBar.Ask.Close);
}
// Fall back to using last price if quote is not available
var price = security.Price;
if (price > 0)
{
// Estimate spread as 0.5% of price (conservative estimate)
var estimatedSpread = price * 0.005m;
return new Quote(price - estimatedSpread / 2, price + estimatedSpread / 2);
}
return null;
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"SmartPricing: Error getting quote for {security.Symbol}: {ex.Message}");
return null;
}
}
/// <summary>
/// Cleans up resources for a completed order
/// </summary>
private void CleanupOrder(SmartOrderTracker tracker)
{
_activeOrders.Remove(tracker.OrderTicket.OrderId);
if (tracker.ScheduledEvent != null)
{
_scheduledEvents.Remove(tracker.ScheduledEvent);
}
}
}
/// <summary>
/// Represents a market quote with bid and ask prices
/// </summary>
public class Quote
{
public decimal Bid { get; }
public decimal Ask { get; }
public decimal Price => (Bid + Ask) / 2;
public decimal Spread => Ask - Bid;
public Quote(decimal bid, decimal ask)
{
Bid = bid;
Ask = ask;
}
}
}using System;
using System.Collections.Generic;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Orders;
using QuantConnect.Securities;
namespace CoreAlgo.Architecture.Core.Helpers
{
/// <summary>
/// Static utility class for handling option assignments using QuantConnect's native Portfolio system.
/// Provides immediate liquidation of assigned shares to prevent margin crises.
/// </summary>
public static class AssignmentHandler
{
// Track processed assignments to prevent duplicate liquidations
private static readonly HashSet<string> _processedAssignments = new HashSet<string>();
/// <summary>
/// Handle option assignment by immediately liquidating assigned underlying shares.
/// Uses assignment event quantity and contract multiplier to compute shares (QC-native approach).
/// Special handling for cash-settled index options (SPX).
/// </summary>
/// <param name="algorithm">The QC algorithm instance</param>
/// <param name="assignmentEvent">The assignment order event from QC</param>
public static void HandleAssignment(QCAlgorithm algorithm, OrderEvent assignmentEvent)
{
try
{
algorithm.Log($"ASSIGNMENT DETECTED: {assignmentEvent.Symbol} at {algorithm.Time}");
// Idempotency guard: prevent duplicate processing of same assignment
var assignmentKey = $"{assignmentEvent.Symbol}_{assignmentEvent.OrderId}_{algorithm.Time}";
if (_processedAssignments.Contains(assignmentKey))
{
algorithm.Log($"ASSIGNMENT ALREADY PROCESSED: {assignmentEvent.Symbol} (skipping duplicate)");
return;
}
_processedAssignments.Add(assignmentKey);
// Get the assigned option symbol
var assignedSymbol = assignmentEvent.Symbol;
// Check if this is a cash-settled index option (SPX)
if (IsCashSettledIndexOption(assignedSymbol))
{
algorithm.Log($"INDEX OPTION ASSIGNMENT: {assignedSymbol} (Cash-settled - no equity position created)");
HandleCashSettledAssignment(algorithm, assignmentEvent);
return;
}
// For option assignments, get the underlying symbol
var underlyingSymbol = GetUnderlyingSymbol(assignedSymbol);
if (underlyingSymbol == null)
{
algorithm.Error($"Could not determine underlying symbol for assignment: {assignedSymbol}");
return;
}
// QC-NATIVE APPROACH: Compute shares from assignment event directly
// This avoids relying on Portfolio holdings which may not be populated at midnight (assignment time)
if (!algorithm.Securities.ContainsKey(assignedSymbol))
{
algorithm.Error($"Option contract not in securities: {assignedSymbol}");
return;
}
var optionSec = algorithm.Securities[assignedSymbol];
var contractMultiplier = optionSec.SymbolProperties.ContractMultiplier;
var contracts = Math.Abs(assignmentEvent.FillQuantity);
// CRITICAL FIX: Correct sign logic based on option right
// Short CALL assignment creates SHORT stock position → BUY to flatten
// Short PUT assignment creates LONG stock position → SELL to flatten
int sharesToFlatten;
// Try to use actual portfolio holdings if available (most accurate)
if (algorithm.Portfolio.ContainsKey(underlyingSymbol) &&
algorithm.Portfolio[underlyingSymbol].Quantity != 0)
{
// Flatten existing position by negating current holdings
sharesToFlatten = -(int)algorithm.Portfolio[underlyingSymbol].Quantity;
algorithm.Log($"Using portfolio holdings: {underlyingSymbol.Value} has {algorithm.Portfolio[underlyingSymbol].Quantity} shares");
}
else
{
// Fallback: compute from option right
var optionRight = assignedSymbol.ID.OptionRight;
if (optionRight == OptionRight.Call)
{
// Short call assignment → we delivered shares → now short stock → BUY to cover
sharesToFlatten = (int)(contracts * contractMultiplier);
}
else // OptionRight.Put
{
// Short put assignment → we bought shares → now long stock → SELL to flatten
sharesToFlatten = -(int)(contracts * contractMultiplier);
}
algorithm.Log($"Using computed position for {optionRight}: {contracts} contracts → {sharesToFlatten} shares");
}
algorithm.Log($"Assignment Details: {underlyingSymbol.Value} - Contracts: {contracts}, " +
$"Multiplier: {contractMultiplier}, Shares to flatten: {sharesToFlatten}");
// Execute immediate liquidation using computed quantity
var orderTicket = algorithm.MarketOrder(underlyingSymbol, sharesToFlatten, tag: "Assignment Auto-Liquidation", asynchronous: true);
algorithm.Log($"LIQUIDATING ASSIGNMENT: {underlyingSymbol.Value} - " +
$"Quantity: {sharesToFlatten}, Order ID: {orderTicket.OrderId}");
// Log assignment impact for tracking
LogAssignmentImpact(algorithm, assignmentEvent, underlyingSymbol, contractMultiplier);
}
catch (Exception ex)
{
algorithm.Error($"Error handling assignment: {ex.Message}");
algorithm.Error($"Stack trace: {ex.StackTrace}");
}
}
/// <summary>
/// Check if the symbol represents a cash-settled index option (SPX, SPXW).
/// These options settle in cash and don't create underlying equity positions.
/// </summary>
private static bool IsCashSettledIndexOption(Symbol symbol)
{
if (symbol?.SecurityType == SecurityType.IndexOption)
{
return true;
}
// SPXW weekly options are cash-settled but might not have IndexOption type
var symbolStr = symbol?.Value?.ToUpperInvariant() ?? "";
if (symbolStr.StartsWith("SPXW") || symbolStr.StartsWith("SPX"))
return true;
// Other known cash-settled index options
if (symbolStr.StartsWith("VIX") || symbolStr.StartsWith("NDX") || symbolStr.StartsWith("RUT"))
return true;
return false;
}
/// <summary>
/// Get the underlying symbol from an option symbol.
/// Handles both equity options (SPY) and index options (SPXW -> SPX).
/// </summary>
private static Symbol GetUnderlyingSymbol(Symbol optionSymbol)
{
if (optionSymbol.SecurityType == SecurityType.Option || optionSymbol.SecurityType == SecurityType.IndexOption)
{
return optionSymbol.Underlying;
}
// If it's already an equity symbol (direct assignment), return as-is
if (optionSymbol.SecurityType == SecurityType.Equity || optionSymbol.SecurityType == SecurityType.Index)
{
return optionSymbol;
}
return null;
}
/// <summary>
/// Log assignment impact for performance tracking and analysis.
/// Uses event-based calculation when holdings aren't available (e.g., at midnight assignment time).
/// </summary>
private static void LogAssignmentImpact(QCAlgorithm algorithm, OrderEvent assignmentEvent,
Symbol underlyingSymbol, decimal contractMultiplier)
{
try
{
var holding = algorithm.Portfolio[underlyingSymbol];
decimal assignmentValue;
// If holdings are populated, use them; otherwise compute from event
if (holding.Invested && holding.HoldingsValue != 0)
{
assignmentValue = Math.Abs(holding.HoldingsValue);
}
else
{
// Compute from assignment event: contracts × multiplier × fill price
var contracts = Math.Abs(assignmentEvent.FillQuantity);
var underlyingPrice = algorithm.Securities.ContainsKey(underlyingSymbol)
? algorithm.Securities[underlyingSymbol].Price
: assignmentEvent.FillPrice;
assignmentValue = contracts * contractMultiplier * underlyingPrice;
}
var portfolioImpact = algorithm.Portfolio.TotalPortfolioValue > 0
? assignmentValue / algorithm.Portfolio.TotalPortfolioValue * 100
: 0;
algorithm.Log($"ASSIGNMENT IMPACT: {underlyingSymbol.Value} - " +
$"Value: ${assignmentValue:F0}, Portfolio Impact: {portfolioImpact:F1}%, " +
$"Free Margin: ${algorithm.Portfolio.MarginRemaining:F0}");
}
catch (Exception ex)
{
algorithm.Debug($"Error logging assignment impact: {ex.Message}");
}
}
/// <summary>
/// Handle cash-settled index option assignments (SPX).
/// These options settle in cash and don't create underlying positions.
/// </summary>
/// <param name="algorithm">The QC algorithm instance</param>
/// <param name="assignmentEvent">The assignment order event from QC</param>
private static void HandleCashSettledAssignment(QCAlgorithm algorithm, OrderEvent assignmentEvent)
{
try
{
// For cash-settled options, calculate the settlement value
var optionSymbol = assignmentEvent.Symbol;
var underlyingSymbol = GetUnderlyingSymbol(optionSymbol);
if (underlyingSymbol == null)
{
algorithm.Error($"Could not determine underlying symbol for cash-settled assignment: {optionSymbol}");
return;
}
// Get current underlying price for settlement calculation
var underlyingPrice = algorithm.Securities.ContainsKey(underlyingSymbol)
? algorithm.Securities[underlyingSymbol].Price
: 0m;
// Log the cash settlement details
algorithm.Log($"CASH SETTLEMENT: {optionSymbol} based on {underlyingSymbol.Value} price: ${underlyingPrice:F2}");
algorithm.Log($"Settlement Value: ${assignmentEvent.FillPrice:F2} per contract");
algorithm.Log($"Total Settlement: ${assignmentEvent.FillPrice * Math.Abs(assignmentEvent.FillQuantity):F2}");
// Log assignment impact for cash-settled options
LogCashSettledAssignmentImpact(algorithm, assignmentEvent, underlyingSymbol, underlyingPrice);
}
catch (Exception ex)
{
algorithm.Error($"Error handling cash-settled assignment: {ex.Message}");
}
}
/// <summary>
/// Log assignment impact for cash-settled index options.
/// </summary>
private static void LogCashSettledAssignmentImpact(QCAlgorithm algorithm, OrderEvent assignmentEvent,
Symbol underlyingSymbol, decimal underlyingPrice)
{
try
{
var settlementValue = Math.Abs(assignmentEvent.FillPrice * assignmentEvent.FillQuantity);
var portfolioImpact = algorithm.Portfolio.TotalPortfolioValue > 0
? settlementValue / algorithm.Portfolio.TotalPortfolioValue * 100
: 0;
algorithm.Log($"ASSIGNMENT IMPACT: {underlyingSymbol.Value} - " +
$"Value: ${settlementValue:F0}, Portfolio Impact: {portfolioImpact:F1}%, " +
$"Free Margin: ${algorithm.Portfolio.MarginRemaining:F0}");
}
catch (Exception ex)
{
algorithm.Debug($"Error logging cash-settled assignment impact: {ex.Message}");
}
}
/// <summary>
/// Check for unexpected equity positions that might indicate unhandled assignments.
/// This can be called periodically to monitor for assignment handling issues.
/// </summary>
public static void MonitorUnexpectedEquityPositions(QCAlgorithm algorithm)
{
try
{
var unexpectedEquities = 0;
// Use QC's Portfolio.Values to check all positions
foreach (var holding in algorithm.Portfolio.Values)
{
if (holding.Invested &&
holding.Symbol.SecurityType == SecurityType.Equity &&
holding.Quantity != 0)
{
unexpectedEquities++;
algorithm.Log($"WARNING: UNEXPECTED EQUITY POSITION: {holding.Symbol.Value} - " +
$"Qty: {holding.Quantity}, Value: ${holding.HoldingsValue:F0}");
}
}
if (unexpectedEquities > 0)
{
algorithm.Log($"ALERT: Found {unexpectedEquities} unexpected equity positions - " +
$"Check assignment handling logic");
}
}
catch (Exception ex)
{
algorithm.Debug($"Error monitoring equity positions: {ex.Message}");
}
}
}
}using System;
namespace CoreAlgo.Architecture.Core.Helpers
{
/// <summary>
/// Helper methods for volatility calculations including forward volatility.
/// </summary>
public static class VolatilityMath
{
/// <summary>
/// Computes forward volatility between two maturities using the forward variance identity.
///
/// Formula:
/// T1 = nearDte / 365, T2 = farDte / 365
/// TV1 = nearIv^2 * T1, TV2 = farIv^2 * T2
/// ForwardVar = (TV2 - TV1) / (T2 - T1)
/// ForwardVol = sqrt(ForwardVar)
///
/// Returns false if inputs are invalid (T2 <= T1, negative forward variance, etc.)
/// </summary>
/// <param name="nearIv">Near maturity implied volatility (as decimal, e.g., 0.45 for 45%)</param>
/// <param name="nearDte">Near maturity days to expiration</param>
/// <param name="farIv">Far maturity implied volatility (as decimal, e.g., 0.35 for 35%)</param>
/// <param name="farDte">Far maturity days to expiration</param>
/// <param name="forwardVol">Output: computed forward volatility (annualized, as decimal)</param>
/// <returns>True if computation succeeded, false if inputs are invalid</returns>
public static bool TryComputeForwardVolatility(
decimal nearIv,
int nearDte,
decimal farIv,
int farDte,
out decimal forwardVol)
{
forwardVol = 0m;
// Convert to years
var T1 = nearDte / 365m;
var T2 = farDte / 365m;
// Validate: T2 must be > T1
if (T2 <= T1)
return false;
// Compute total variances
var tv1 = nearIv * nearIv * T1;
var tv2 = farIv * farIv * T2;
// Compute forward variance
var fwdVar = (tv2 - tv1) / (T2 - T1);
// Check for negative or zero forward variance
if (fwdVar <= 0m)
return false;
// Compute forward volatility
forwardVol = (decimal)Math.Sqrt((double)fwdVar);
return true;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Validates sufficient collateral for short option positions
/// Prevents overselling calls against limited shares or puts against limited cash
/// </summary>
public class CollateralValidationRule : IPositionOverlapRule
{
private readonly IAlgorithmContext _context;
private readonly object _logger;
public string RuleName => "CollateralValidationRule";
public string Description => "Ensures sufficient collateral for short option positions";
public bool IsEnabled { get; set; } = true;
public CollateralValidationRule(IAlgorithmContext context)
{
_context = context;
_logger = context.Logger;
}
public ValidationResult Validate(
Symbol proposedSymbol,
decimal quantity,
IEnumerable<KeyValuePair<Symbol, SecurityHolding>> existingPositions,
string strategyTag = "")
{
if (!IsEnabled)
return ValidationResult.Success();
try
{
// Only validate option positions
if (proposedSymbol.SecurityType != SecurityType.Option)
return ValidationResult.Success();
// Only validate short positions (negative quantity)
if (quantity >= 0)
return ValidationResult.Success();
// Skip validation for multi-leg strategies (spreads, Iron Condors, etc.)
// These strategies manage their own risk through spread construction
if (IsMultiLegStrategy(strategyTag))
{
((dynamic)_logger).Debug($"[{RuleName}] Skipping collateral validation for multi-leg strategy: {strategyTag}");
return ValidationResult.Success();
}
var underlying = proposedSymbol.Underlying;
var optionRight = proposedSymbol.ID.OptionRight;
var contractMultiplier = 100; // Standard option contract size
if (optionRight == OptionRight.Call)
{
return ValidateCallCollateral(underlying, quantity, existingPositions, contractMultiplier);
}
else if (optionRight == OptionRight.Put)
{
return ValidatePutCollateral(proposedSymbol, quantity, existingPositions, contractMultiplier);
}
return ValidationResult.Success();
}
catch (System.Exception ex)
{
((dynamic)_logger).Error($"[{RuleName}] Error validating collateral: {ex.Message}");
return ValidationResult.Error($"Collateral validation error: {ex.Message}");
}
}
/// <summary>
/// Validates sufficient shares for covered call positions
/// </summary>
private ValidationResult ValidateCallCollateral(
Symbol underlying,
decimal proposedQuantity,
IEnumerable<KeyValuePair<Symbol, SecurityHolding>> existingPositions,
int contractMultiplier)
{
try
{
// Get current stock holdings
var stockHolding = _context.Algorithm.Portfolio[underlying];
var availableShares = stockHolding?.Quantity ?? 0;
// Calculate total short calls (existing + proposed)
var existingShortCalls = existingPositions
.Where(p => p.Key.SecurityType == SecurityType.Option &&
p.Key.Underlying == underlying &&
p.Key.ID.OptionRight == OptionRight.Call &&
p.Value.Quantity < 0)
.Sum(p => -p.Value.Quantity); // Convert to positive for counting
var totalShortCalls = existingShortCalls + (-proposedQuantity);
var sharesRequired = totalShortCalls * contractMultiplier;
((dynamic)_logger).Debug($"[{RuleName}] Call validation: " +
$"Available shares: {availableShares}, " +
$"Required: {sharesRequired}, " +
$"Existing short calls: {existingShortCalls}, " +
$"Proposed: {-proposedQuantity}");
if (sharesRequired > availableShares)
{
return ValidationResult.Blocked(
$"Insufficient shares for covered call: " +
$"Need {sharesRequired} shares, have {availableShares}. " +
$"Total short calls would be {totalShortCalls}.");
}
return ValidationResult.Success();
}
catch (System.Exception ex)
{
((dynamic)_logger).Warning($"[{RuleName}] Error in call validation: {ex.Message}");
// Allow the trade if we can't validate (conservative but functional)
return ValidationResult.Success();
}
}
/// <summary>
/// Validates sufficient cash for cash-secured put positions
/// </summary>
private ValidationResult ValidatePutCollateral(
Symbol proposedSymbol,
decimal proposedQuantity,
IEnumerable<KeyValuePair<Symbol, SecurityHolding>> existingPositions,
int contractMultiplier)
{
try
{
var underlying = proposedSymbol.Underlying;
var strike = proposedSymbol.ID.StrikePrice;
// Get available cash
var availableCash = _context.Algorithm.Portfolio.Cash;
// Calculate cash required for this put
var cashRequiredForNewPut = Math.Abs(proposedQuantity) * strike * contractMultiplier;
// Calculate cash already committed to existing cash-secured puts
var existingCashCommitment = existingPositions
.Where(p => p.Key.SecurityType == SecurityType.Option &&
p.Key.Underlying == underlying &&
p.Key.ID.OptionRight == OptionRight.Put &&
p.Value.Quantity < 0)
.Sum(p => Math.Abs(p.Value.Quantity) * p.Key.ID.StrikePrice * contractMultiplier);
var totalCashRequired = existingCashCommitment + cashRequiredForNewPut;
((dynamic)_logger).Debug($"[{RuleName}] Put validation: " +
$"Available cash: ${availableCash:F2}, " +
$"Total required: ${totalCashRequired:F2}, " +
$"Existing commitment: ${existingCashCommitment:F2}, " +
$"New requirement: ${cashRequiredForNewPut:F2}");
if (totalCashRequired > availableCash)
{
return ValidationResult.Blocked(
$"Insufficient cash for cash-secured put: " +
$"Need ${totalCashRequired:F2}, have ${availableCash:F2}. " +
$"Put strike: ${strike}, quantity: {Math.Abs(proposedQuantity)}");
}
return ValidationResult.Success();
}
catch (System.Exception ex)
{
((dynamic)_logger).Warning($"[{RuleName}] Error in put validation: {ex.Message}");
// Allow the trade if we can't validate
return ValidationResult.Success();
}
}
/// <summary>
/// Gets total buying power reduction for all positions on an underlying
/// </summary>
private decimal GetBuyingPowerReduction(Symbol underlying,
IEnumerable<KeyValuePair<Symbol, SecurityHolding>> positions)
{
try
{
return positions
.Where(p => GetPositionUnderlying(p.Key) == underlying)
.Sum(p => Math.Abs(p.Value.HoldingsValue));
}
catch
{
return 0m;
}
}
/// <summary>
/// Helper to get underlying symbol from any security type
/// </summary>
private Symbol GetPositionUnderlying(Symbol symbol)
{
return symbol.SecurityType == SecurityType.Option ? symbol.Underlying : symbol;
}
/// <summary>
/// Determines if this is a multi-leg strategy based on current validation context
/// Multi-leg strategies manage their own risk through spread construction
/// </summary>
private bool IsMultiLegStrategy(string strategyTag)
{
// Check for common multi-leg indicators in tag (keep generic terms only)
if (!string.IsNullOrEmpty(strategyTag))
{
var tag = strategyTag.ToUpperInvariant();
if (tag.Contains("COMBO") || tag.Contains("MULTI") || tag.Contains("SPREAD"))
return true;
}
// Try to determine from strategy configuration if available
try
{
var algorithm = _context.Algorithm;
if (algorithm != null)
{
var comboLegCount = algorithm.GetParameter("ComboOrderLegCount");
if (int.TryParse(comboLegCount, out var legCount) && legCount > 1)
{
return true;
}
}
}
catch (Exception ex)
{
((dynamic)_logger).Debug($"[MULTI-LEG DETECTION] Could not read ComboOrderLegCount: {ex.Message}");
}
// Default to false for single-leg strategies
return false;
}
}
}namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Log levels for centralized logging with level checking
/// Matches CentralAlgorithm pattern: ERROR=0, WARNING=1, INFO=2, DEBUG=3
/// </summary>
public enum LogLevel
{
/// <summary>
/// Error messages - highest priority, always logged
/// </summary>
Error = 0,
/// <summary>
/// Warning messages - important issues that don't stop execution
/// </summary>
Warning = 1,
/// <summary>
/// Information messages - general algorithm flow and status
/// </summary>
Info = 2,
/// <summary>
/// Debug messages - detailed execution information, lowest priority
/// </summary>
Debug = 3
}
}#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Globalization;
using System.Drawing;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Text;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Selection;
using QuantConnect.Api;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Commands;
using QuantConnect.Configuration;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.Shortable;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.OptionExercise;
using QuantConnect.Orders.Slippage;
using QuantConnect.Orders.TimeInForces;
using QuantConnect.Python;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Positions;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using Calendar = QuantConnect.Data.Consolidators.Calendar;
using CoreAlgo.Architecture.Core.Interfaces;
#endregion
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Trade highlight event types for logging important trade actions
/// </summary>
public enum HighlightKind
{
Entry,
TakeProfit,
StopLoss,
TrailingStop,
PartialFill,
Cancel
}
/// <summary>
/// Non-generic base class to hold shared static collections for all logger instances
/// </summary>
public static class SmartLoggerStore
{
// Shared static storage for ALL logger instances (thread-safe)
public static readonly ConcurrentDictionary<string, List<SmartLogMessage>> DailyMessages =
new ConcurrentDictionary<string, List<SmartLogMessage>>();
public static readonly ConcurrentDictionary<string, MessageGroup> MessageGroups =
new ConcurrentDictionary<string, MessageGroup>();
public static DateTime? LastProcessedDay = null;
public static readonly object ProcessLock = new object();
// Trade highlight metrics (ephemeral scope-based context)
public static readonly ConcurrentDictionary<string, Dictionary<string, string>> HighlightContexts =
new ConcurrentDictionary<string, Dictionary<string, string>>();
public static readonly ConcurrentQueue<SmartLogMessage> Highlights = new ConcurrentQueue<SmartLogMessage>();
// Limits for highlight metrics
private const int MaxMetricsPerScope = 12;
private const int MaxMetricChars = 80;
// Throttling for high-frequency messages
private static readonly ConcurrentDictionary<string, (DateTime lastSeen, int count)> MessageThrottle =
new ConcurrentDictionary<string, (DateTime, int)>();
private static readonly TimeSpan ThrottleWindow = TimeSpan.FromMinutes(1);
private const int MaxMessagesPerWindow = 50;
/// <summary>
/// Get current collection counts for debugging
/// </summary>
public static (int dailyMessages, int messageGroups) GetCollectionCounts()
{
return (DailyMessages.Count, MessageGroups.Count);
}
/// <summary>
/// Check if message should be throttled due to high frequency
/// </summary>
public static bool ShouldThrottleMessage(string baseHash, DateTime currentTime)
{
var throttleKey = $"throttle_{baseHash}";
var info = MessageThrottle.GetOrAdd(throttleKey, _ => (currentTime, 0));
// Reset count if window has expired
if (currentTime - info.lastSeen > ThrottleWindow)
{
MessageThrottle.TryUpdate(throttleKey, (currentTime, 1), info);
return false;
}
// Increment count
var newCount = info.count + 1;
MessageThrottle.TryUpdate(throttleKey, (currentTime, newCount), info);
// Throttle if exceeded limit
return newCount > MaxMessagesPerWindow;
}
/// <summary>
/// Clean old throttle entries to prevent memory leaks
/// </summary>
private static void CleanOldThrottleEntries(DateTime currentDay)
{
var cutoff = currentDay.AddDays(-1); // Keep entries from yesterday and today only
var keysToRemove = new List<string>();
foreach (var kvp in MessageThrottle)
{
if (kvp.Value.lastSeen.Date < cutoff.Date)
{
keysToRemove.Add(kvp.Key);
}
}
foreach (var key in keysToRemove)
{
MessageThrottle.TryRemove(key, out _);
}
}
/// <summary>
/// Merge metrics into a scope's context (thread-safe)
/// </summary>
public static void MergeMetrics(string scopeKey, Dictionary<string, string> metrics)
{
if (string.IsNullOrEmpty(scopeKey) || metrics == null || metrics.Count == 0)
return;
var context = HighlightContexts.GetOrAdd(scopeKey, _ => new Dictionary<string, string>());
lock (context)
{
foreach (var kvp in metrics)
{
if (context.Count >= MaxMetricsPerScope)
break;
var value = kvp.Value;
if (value.Length > MaxMetricChars)
value = value.Substring(0, MaxMetricChars - 3) + "...";
context[kvp.Key] = value;
}
}
}
/// <summary>
/// Try to get and clear metrics for a scope (thread-safe)
/// </summary>
public static bool TryGetAndClearMetrics(string scopeKey, out Dictionary<string, string> metrics)
{
metrics = null;
if (string.IsNullOrEmpty(scopeKey))
return false;
if (HighlightContexts.TryRemove(scopeKey, out var context))
{
lock (context)
{
metrics = new Dictionary<string, string>(context);
}
return metrics.Count > 0;
}
return false;
}
/// <summary>
/// Clear metrics for a specific scope
/// </summary>
public static void ClearHighlightMetrics(string scopeKey)
{
if (!string.IsNullOrEmpty(scopeKey))
{
HighlightContexts.TryRemove(scopeKey, out _);
}
}
/// <summary>
/// Add a highlight message for EOD processing
/// </summary>
public static void AddHighlight(SmartLogMessage message)
{
Highlights.Enqueue(message);
}
/// <summary>
/// Try to dequeue all highlights for processing
/// </summary>
public static List<SmartLogMessage> TryDequeueAllHighlights()
{
var result = new List<SmartLogMessage>();
while (Highlights.TryDequeue(out var msg))
{
result.Add(msg);
}
return result;
}
/// <summary>
/// Process and output accumulated daily logs with smart summarization
/// </summary>
public static void ProcessDailyLogs(QCAlgorithm algorithm, int maxHighlightsPerDay = 2000)
{
// Skip daily log processing entirely in live mode
if (algorithm.LiveMode)
return;
lock (ProcessLock)
{
var currentDay = algorithm.Time.Date;
// FIXED: Match Python centralalgorithm logic exactly
// Always skip if already processed today (no conditions about message counts)
if (LastProcessedDay.HasValue && LastProcessedDay.Value == currentDay)
{
return; // First OnEndOfDay() call processes, subsequent calls are ignored
}
algorithm.Log($"=== DEBUG: Collection counts - Daily: {DailyMessages.Count}, Groups: {MessageGroups.Count}, Highlights: {Highlights.Count} ===");
algorithm.Log("---------------------------------");
algorithm.Log($"Daily Log Summary - {currentDay:yyyy-MM-dd}");
algorithm.Log("---------------------------------");
// Print highlights FIRST (most important trade events)
var highlights = TryDequeueAllHighlights();
if (highlights.Any())
{
algorithm.Log("");
algorithm.Log($"==== IMPORTANT TRADE EVENTS ({currentDay:yyyy-MM-dd}) ====");
var sortedHighlights = highlights.OrderBy(h => h.Timestamp).ToList();
var displayCount = Math.Min(sortedHighlights.Count, maxHighlightsPerDay);
for (int i = 0; i < displayCount; i++)
{
var h = sortedHighlights[i];
algorithm.Log(h.Message);
}
if (sortedHighlights.Count > maxHighlightsPerDay)
{
algorithm.Log($"... and {sortedHighlights.Count - maxHighlightsPerDay} more");
}
// Count by type (Entry, TP, SL, TS based on message prefixes)
var entryCount = highlights.Count(h => h.Message.Contains("==> ENTRY"));
var tpCount = highlights.Count(h => h.Message.Contains("<== TP"));
var slCount = highlights.Count(h => h.Message.Contains("!! SL"));
var tsCount = highlights.Count(h => h.Message.Contains("~~> TS"));
algorithm.Log($"Counts: Entry={entryCount}, TP={tpCount}, SL={slCount}, TS={tsCount}");
algorithm.Log("");
}
if (!DailyMessages.Any() && !MessageGroups.Any())
{
algorithm.Log("*** NO SMART MESSAGES WERE COLLECTED ***");
}
else
{
algorithm.Log($"Found {DailyMessages.Count} daily message groups and {MessageGroups.Count} statistical groups to process");
}
// Process regular messages from DailyMessages
foreach (var kvp in DailyMessages)
{
var messages = kvp.Value;
if (messages.Count == 0) continue;
lock (messages)
{
var firstMsg = messages[0];
if (messages.Count > 1)
{
algorithm.Log($"{firstMsg.Timestamp:HH:mm:ss} {firstMsg.Level} -> " +
$"{firstMsg.ClassName}.{firstMsg.FunctionName}: {firstMsg.Message} " +
$"(repeated {messages.Count} times)");
}
else
{
algorithm.Log($"{firstMsg.Timestamp:HH:mm:ss} {firstMsg.Level} -> " +
$"{firstMsg.ClassName}.{firstMsg.FunctionName}: {firstMsg.Message}");
}
}
}
// Process grouped messages with statistical analysis
foreach (var kvp in MessageGroups)
{
var group = kvp.Value;
algorithm.Log(group.GetSummary());
}
algorithm.Log("");
// Clear processed messages
DailyMessages.Clear();
MessageGroups.Clear();
// Clear old throttle entries and highlight contexts to prevent memory leaks
CleanOldThrottleEntries(currentDay);
HighlightContexts.Clear();
LastProcessedDay = currentDay;
}
}
}
/// <summary>
/// Represents a log message with smart hashing and pattern recognition capabilities
/// </summary>
public class SmartLogMessage
{
public string Level { get; set; }
public string ClassName { get; set; }
public string FunctionName { get; set; }
public string Message { get; set; }
public DateTime Timestamp { get; set; }
private string _hash;
private string _baseHash;
public SmartLogMessage(string level, string className, string functionName, string message, DateTime timestamp)
{
Level = level;
ClassName = className;
FunctionName = functionName;
Message = message;
Timestamp = timestamp;
}
/// <summary>
/// Exact hash for identical messages
/// </summary>
public string Hash
{
get
{
if (_hash == null)
{
var content = $"{Level}|{ClassName}|{FunctionName}|{Message}";
_hash = ComputeHash(content);
}
return _hash;
}
}
/// <summary>
/// Pattern-based hash for messages with similar structure but different values
/// </summary>
public string BaseHash
{
get
{
if (_baseHash == null)
{
try
{
// Normalize the message by replacing numeric values and common variables
var template = Regex.Replace(Message, @"[-+]?[0-9,]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?", "NUM");
template = Regex.Replace(template, @"\s+", " ");
template = template.ToLowerInvariant();
// Normalize specific patterns like symbols and states
template = Regex.Replace(template, @"(onendofday called for symbol\s+)(.*)", "$1SYMBOL_PLACEHOLDER");
template = Regex.Replace(template, @"(strategy state:\s+)(.*)", "$1STATE_PLACEHOLDER");
template = Regex.Replace(template, @"(strategy name:\s+)(.*)", "$1NAME_PLACEHOLDER");
template = Regex.Replace(template, @"(symbol\s+)([A-Z]{2,5})", "$1SYMBOL_PLACEHOLDER");
template = Regex.Replace(template, @"[:\[\]{}()]", "");
var content = $"{Level.ToLowerInvariant()}|{ClassName.ToLowerInvariant()}|{FunctionName.ToLowerInvariant()}|{template}";
_baseHash = ComputeHash(content);
}
catch
{
// Fallback to simpler hash if normalization fails
var content = $"{Level}|{ClassName}|{FunctionName}";
_baseHash = ComputeHash(content);
}
}
return _baseHash;
}
}
/// <summary>
/// Extract numeric value from message for statistical analysis
/// </summary>
public double? ExtractValue()
{
try
{
var matches = Regex.Matches(Message, @"[-+]?[0-9,]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?");
if (matches.Count > 0)
{
var lastMatch = matches[matches.Count - 1].Value.Replace(",", "");
if (double.TryParse(lastMatch, out double value))
return value;
}
}
catch
{
// Ignore extraction errors
}
return null;
}
private static string ComputeHash(string input)
{
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
}
/// <summary>
/// Groups messages with similar patterns for statistical analysis
/// </summary>
public class MessageGroup
{
public string BaseHash { get; private set; }
public List<SmartLogMessage> Messages { get; private set; }
public DateTime FirstTime { get; private set; }
public DateTime LastTime { get; private set; }
public string Level { get; private set; }
public string ClassName { get; private set; }
public string FunctionName { get; private set; }
public string BaseMessage { get; private set; }
public MessageGroup(SmartLogMessage firstMessage)
{
BaseHash = firstMessage.BaseHash;
Messages = new List<SmartLogMessage> { firstMessage };
FirstTime = firstMessage.Timestamp;
LastTime = firstMessage.Timestamp;
Level = firstMessage.Level;
ClassName = firstMessage.ClassName;
FunctionName = firstMessage.FunctionName;
BaseMessage = firstMessage.Message;
}
public bool TryAddMessage(SmartLogMessage message)
{
if (message.BaseHash != BaseHash)
return false;
Messages.Add(message);
LastTime = message.Timestamp;
return true;
}
/// <summary>
/// Generate statistical summary of grouped messages
/// </summary>
public string GetSummary()
{
if (Messages.Count == 1)
return FormatMessage(Messages[0]);
// Extract values and timestamps from sorted messages
var sortedMessages = Messages.OrderBy(m => m.Timestamp).ToList();
var values = new List<double>();
var timestamps = new List<DateTime>();
foreach (var msg in sortedMessages)
{
var value = msg.ExtractValue();
if (value.HasValue)
{
values.Add(value.Value);
timestamps.Add(msg.Timestamp);
}
}
if (!values.Any())
return FormatMessage(Messages[0]);
// Calculate statistics
var mean = values.Average();
var min = values.Min();
var max = values.Max();
var count = values.Count;
// Format time range
var timeRange = $"{FirstTime:HH:mm:ss}-{LastTime:HH:mm:ss}";
// Build summary with clear sections like Python version
var sections = new List<string>
{
$"{timeRange} {Level} -> {ClassName}.{FunctionName}: {BaseMessage}",
$" Stats: mean={mean:F2}, min={min:F2}, max={max:F2}"
};
// Add trend analysis if we have enough data points
if (count >= 2)
{
var trend = GetTrendAnalysis(values, timestamps);
if (!string.IsNullOrEmpty(trend))
sections.Add($" Trend:{trend}");
}
// Add distribution analysis if we have enough data points
if (count >= 3)
{
var distribution = GetDistributionAnalysis(values);
if (!string.IsNullOrEmpty(distribution))
sections.Add($" {distribution}");
}
// Sample count
sections.Add($" Samples: {count}");
return string.Join("\n", sections);
}
private string GetTrendAnalysis(List<double> values, List<DateTime> timestamps)
{
if (values.Count < 2) return "";
try
{
// Find key changes (local maxima and minima) like Python version
var keyChanges = new List<(DateTime time, double value)>();
for (int i = 1; i < values.Count - 1; i++)
{
// Look for local maxima and minima
if ((values[i] > values[i-1] && values[i] > values[i+1]) ||
(values[i] < values[i-1] && values[i] < values[i+1]))
{
keyChanges.Add((timestamps[i], values[i]));
}
}
// Limit to most significant changes
if (keyChanges.Count > 4)
{
// Sort by absolute change magnitude from first value
keyChanges = keyChanges.OrderByDescending(x => Math.Abs(x.value - values[0])).Take(4).ToList();
// Resort by time
keyChanges = keyChanges.OrderBy(x => x.time).ToList();
}
if (!keyChanges.Any()) return "";
// Format changes with timestamps like Python
var changes = keyChanges.Select(x =>
$"{(x.value > values[0] ? "↑" : "↓")}{x.value:F2}({x.time:HH:mm})").ToList();
return " " + string.Join(" ", changes);
}
catch
{
return "";
}
}
private string GetDistributionAnalysis(List<double> values)
{
if (values.Count < 3) return "";
try
{
var minVal = values.Min();
var maxVal = values.Max();
// If all values are the same, return special format
if (Math.Abs(minVal - maxVal) < 0.001)
return $"Distribution: [constant={minVal:F2}]";
// Create 3 bins like Python version
var binSize = (maxVal - minVal) / 3;
var bins = new int[3];
foreach (var value in values)
{
var binIdx = (int)((value - minVal) / binSize);
if (binIdx == 3) binIdx = 2; // Handle edge case for max value
bins[binIdx]++;
}
// Format distribution
var distParts = new List<string> { "Distribution:" };
for (int i = 0; i < 3; i++)
{
if (bins[i] > 0)
{
var binStart = minVal + i * binSize;
var binEnd = minVal + (i + 1) * binSize;
distParts.Add($"[{binStart:F1}-{binEnd:F1}: {bins[i]}]");
}
}
return string.Join(" ", distParts);
}
catch
{
return "";
}
}
private static string FormatMessage(SmartLogMessage msg)
{
return $"{msg.Timestamp:HH:mm:ss} {msg.Level} -> {msg.ClassName}.{msg.FunctionName}: {msg.Message}";
}
}
/// <summary>
/// QuantConnect-specific logger implementation with smart deduplication
/// </summary>
public class QCLogger<T> : ILogger<T>
{
private readonly QCAlgorithm _algorithm;
private readonly string _categoryName;
private readonly int _currentLogLevel;
private readonly bool _verboseMode;
public QCLogger(QCAlgorithm algorithm, int logLevel = 3, bool verboseMode = false)
{
_algorithm = algorithm ?? throw new ArgumentNullException(nameof(algorithm));
_categoryName = typeof(T).Name;
_currentLogLevel = logLevel;
_verboseMode = algorithm.LiveMode || verboseMode;
}
public void LogInformation(string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
if (_verboseMode)
{
var realTimeMessage = FormatRealTimeMessage(formattedMessage, _categoryName, GetCallingFunctionName(), LogLevel.Info);
_algorithm.Log(realTimeMessage);
return;
}
StoreSmartMessage("INFO", formattedMessage);
}
public void LogDebug(string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
if (_verboseMode)
{
var realTimeMessage = FormatRealTimeMessage(formattedMessage, _categoryName, GetCallingFunctionName(), LogLevel.Debug);
_algorithm.Log(realTimeMessage);
return;
}
StoreSmartMessage("DEBUG", formattedMessage);
}
public void LogWarning(string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
if (_verboseMode)
{
var realTimeMessage = FormatRealTimeMessage(formattedMessage, _categoryName, GetCallingFunctionName(), LogLevel.Warning);
_algorithm.Log(realTimeMessage);
return;
}
StoreSmartMessage("WARN", formattedMessage);
}
public void LogError(string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
if (_verboseMode)
{
var realTimeMessage = FormatRealTimeMessage(formattedMessage, _categoryName, GetCallingFunctionName(), LogLevel.Error);
_algorithm.Log(realTimeMessage);
return;
}
StoreSmartMessage("ERROR", formattedMessage);
}
public void LogError(Exception exception, string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
var fullMessage = $"{formattedMessage} - Exception: {exception.Message}";
if (_verboseMode)
{
var realTimeMessage = FormatRealTimeMessage(fullMessage, _categoryName, GetCallingFunctionName(), LogLevel.Error);
_algorithm.Log(realTimeMessage);
var stackTraceMessage = FormatRealTimeMessage($"StackTrace: {exception.StackTrace}", _categoryName, GetCallingFunctionName(), LogLevel.Error);
_algorithm.Log(stackTraceMessage);
return;
}
StoreSmartMessage("ERROR", fullMessage);
StoreSmartMessage("ERROR", $"StackTrace: {exception.StackTrace}");
}
/// <summary>
/// Central logging method with level checking - implements context pattern like CentralAlgorithm
/// </summary>
/// <param name="message">Message to log</param>
/// <param name="level">Log level for filtering</param>
public void LogMessage(string message, LogLevel level = LogLevel.Debug)
{
// Level checking: only log if level is at or below current threshold
if ((int)level > _currentLogLevel)
return;
// In live mode, output immediately for all levels
if (_verboseMode)
{
var realTimeMessage = FormatRealTimeMessage(message, _categoryName, GetCallingFunctionName(), level);
_algorithm.Log(realTimeMessage);
return;
}
// Route to appropriate logging method based on level
switch (level)
{
case LogLevel.Error:
LogError(message);
break;
case LogLevel.Warning:
LogWarning(message);
break;
case LogLevel.Info:
LogInformation(message);
break;
case LogLevel.Debug:
LogDebug(message);
break;
default:
LogDebug(message);
break;
}
}
/// <summary>
/// Convenience method for error logging
/// </summary>
public void Error(string message) => LogMessage(message, LogLevel.Error);
/// <summary>
/// Convenience method for warning logging
/// </summary>
public void Warning(string message) => LogMessage(message, LogLevel.Warning);
/// <summary>
/// Convenience method for information logging
/// </summary>
public void Info(string message) => LogMessage(message, LogLevel.Info);
/// <summary>
/// Convenience method for debug logging
/// </summary>
public void Debug(string message) => LogMessage(message, LogLevel.Debug);
/// <summary>
/// Build a stable scope key for highlight metrics (Symbol:TradeId)
/// </summary>
public string BuildScope(Symbol symbol, int? tradeId = null)
{
return $"{symbol.Value}:{tradeId?.ToString() ?? "_"}";
}
/// <summary>
/// Add a single metric to a highlight scope
/// </summary>
public void AddHighlightMetric(string scopeKey, string key, object value, int? decimalPlaces = null)
{
if (string.IsNullOrEmpty(scopeKey) || string.IsNullOrEmpty(key))
return;
var formattedValue = FormatMetricValue(value, decimalPlaces);
var metrics = new Dictionary<string, string> { { key, formattedValue } };
SmartLoggerStore.MergeMetrics(scopeKey, metrics);
}
/// <summary>
/// Add multiple metrics to a highlight scope
/// </summary>
public void AddHighlightMetrics(string scopeKey, IDictionary<string, object> metrics, int? decimalPlaces = null)
{
if (string.IsNullOrEmpty(scopeKey) || metrics == null || metrics.Count == 0)
return;
var formatted = new Dictionary<string, string>();
foreach (var kvp in metrics)
{
formatted[kvp.Key] = FormatMetricValue(kvp.Value, decimalPlaces);
}
SmartLoggerStore.MergeMetrics(scopeKey, formatted);
}
/// <summary>
/// Clear metrics for a specific scope
/// </summary>
public void ClearHighlightMetrics(string scopeKey)
{
SmartLoggerStore.ClearHighlightMetrics(scopeKey);
}
/// <summary>
/// Log a trade highlight event with optional metrics
/// </summary>
public void HighlightTradeEvent(string className, string methodName, HighlightKind kind, string oneLine, string scopeKey = null)
{
// Check if highlights are enabled
var highlightEnabled = bool.Parse(_algorithm.GetParameter("HighlightTrades", "true"));
if (!highlightEnabled)
return;
// Get immediate print settings
var immediateEnabled = bool.Parse(_algorithm.GetParameter("HighlightImmediate", "true"));
var includeMetrics = bool.Parse(_algorithm.GetParameter("HighlightIncludeStrategyMetrics", "true"));
// Build final message with metrics suffix if available
var finalLine = oneLine;
if (includeMetrics && !string.IsNullOrEmpty(scopeKey) &&
SmartLoggerStore.TryGetAndClearMetrics(scopeKey, out var metrics))
{
var suffix = CoreAlgo.Architecture.Core.Services.TradeHighlightFormatter.FormatMetricsSuffix(metrics);
if (!string.IsNullOrEmpty(suffix))
{
finalLine = $"{oneLine} {suffix}";
}
}
// Immediate print (live mode only - backtests rely on EOD summary)
if (_verboseMode && immediateEnabled)
{
_algorithm.Log(finalLine);
}
// Store for EOD processing in backtests (bypasses throttle for highlights)
if (!_verboseMode)
{
var msg = new SmartLogMessage("INFO", className, methodName, finalLine, _algorithm.Time);
SmartLoggerStore.AddHighlight(msg);
}
}
/// <summary>
/// Format a metric value for display (handles decimals, strings, booleans)
/// </summary>
private string FormatMetricValue(object value, int? decimalPlaces)
{
if (value == null)
return "null";
if (value is decimal d)
return d.ToString(decimalPlaces.HasValue ? $"F{decimalPlaces.Value}" : "F2");
if (value is double dbl)
return dbl.ToString(decimalPlaces.HasValue ? $"F{decimalPlaces.Value}" : "F2");
if (value is float f)
return f.ToString(decimalPlaces.HasValue ? $"F{decimalPlaces.Value}" : "F2");
if (value is int || value is long || value is short)
return value.ToString();
if (value is bool b)
return b ? "T" : "F";
// String or other - trim and sanitize
var str = value.ToString();
if (str.Length > 20)
str = str.Substring(0, 17) + "...";
return str;
}
/// <summary>
/// Logs a message with explicit context information for better traceability
/// Uses LogLevel-based output control: ERROR/WARNING immediate, INFO conditional, DEBUG batched
/// </summary>
public void LogWithContext(string message, string className, string methodName, LogLevel level = LogLevel.Info)
{
// Level checking: only log if level is at or below current threshold
if ((int)level > _currentLogLevel)
return;
// Try dynamic context detection first (like Python logger)
if (string.IsNullOrEmpty(className) || className == "SimpleBaseStrategy")
{
var (dynClass, dynMethod) = GetCallerContextDynamic();
if (dynClass != "Unknown")
{
className = dynClass;
methodName = dynMethod;
}
}
// In live mode, output immediately for all levels and skip batching
if (_verboseMode)
{
var formattedMessage = FormatRealTimeMessage(message, className, methodName, level);
_algorithm.Log(formattedMessage);
return;
}
// LogLevel-based output decision
bool shouldOutputImmediate = level <= LogLevel.Warning || // ERROR/WARNING always immediate
(_verboseMode && level == LogLevel.Info);
if (shouldOutputImmediate)
{
var formattedMessage = FormatRealTimeMessage(message, className, methodName, level);
_algorithm.Log(formattedMessage);
}
// Still batch DEBUG and non-verbose INFO for end-of-day summary
if (!shouldOutputImmediate || level == LogLevel.Debug)
{
StoreSmartMessageWithContext(level.ToString().ToUpper(), message, className, methodName);
}
}
/// <summary>
/// Store message for smart processing with grouping for statistical analysis
/// </summary>
private void StoreSmartMessage(string level, string message)
{
// Skip storing in live mode to avoid memory accumulation
if (_verboseMode)
return;
var functionName = GetCallingFunctionName();
var smartMessage = new SmartLogMessage(level, _categoryName, functionName, message, _algorithm.Time);
// Apply throttling for high-frequency messages (especially DEBUG level)
if (level == "DEBUG" && SmartLoggerStore.ShouldThrottleMessage(smartMessage.BaseHash, _algorithm.Time))
{
return; // Skip this message due to throttling
}
// Try to add to existing message group for messages with numeric values
var numericValue = smartMessage.ExtractValue();
if (numericValue.HasValue)
{
var existingGroup = SmartLoggerStore.MessageGroups.GetOrAdd(smartMessage.BaseHash,
_ => new MessageGroup(smartMessage));
if (existingGroup.BaseHash == smartMessage.BaseHash)
{
if (existingGroup.TryAddMessage(smartMessage))
{
return; // Successfully added to group
}
}
}
// Store as regular message if no numeric value or grouping failed
var messageList = SmartLoggerStore.DailyMessages.GetOrAdd(smartMessage.Hash, _ => new List<SmartLogMessage>());
lock (messageList)
{
messageList.Add(smartMessage);
}
}
/// <summary>
/// Store message for smart processing with explicit context
/// </summary>
private void StoreSmartMessageWithContext(string level, string message, string className, string methodName)
{
// Skip storing in live mode to avoid memory accumulation
if (_verboseMode)
return;
var smartMessage = new SmartLogMessage(level, className, methodName, message, _algorithm.Time);
// Apply throttling for high-frequency messages (especially DEBUG level)
if (level == "DEBUG" && SmartLoggerStore.ShouldThrottleMessage(smartMessage.BaseHash, _algorithm.Time))
{
return; // Skip this message due to throttling
}
// Try to add to existing message group for messages with numeric values
var numericValue = smartMessage.ExtractValue();
if (numericValue.HasValue)
{
var existingGroup = SmartLoggerStore.MessageGroups.GetOrAdd(smartMessage.BaseHash,
_ => new MessageGroup(smartMessage));
if (existingGroup.BaseHash == smartMessage.BaseHash)
{
if (existingGroup.TryAddMessage(smartMessage))
{
return; // Successfully added to group
}
}
}
// Store as regular message if no numeric value or grouping failed
var messageList = SmartLoggerStore.DailyMessages.GetOrAdd(smartMessage.Hash, _ => new List<SmartLogMessage>());
lock (messageList)
{
messageList.Add(smartMessage);
}
}
/// <summary>
/// Format message for real-time output
/// </summary>
private string FormatRealTimeMessage(string message, string className, string methodName, LogLevel level)
{
var timestamp = _algorithm.Time.ToString("HH:mm:ss");
var realTime = DateTime.Now.ToString("HH:mm:ss.fff");
// Format: "09:31:00 (15:24:33.123) [ORBTemplate.OnInitialize] INFO: Message"
if (_verboseMode)
{
return $"{timestamp} [{className}.{methodName}] {level.ToString().ToUpper()}: {message}";
}
else
{
// Include real time in backtest for debugging
return $"{timestamp} (Real: {realTime}) [{className}.{methodName}] {level.ToString().ToUpper()}: {message}";
}
}
private static string GetCallingFunctionName()
{
try
{
var stackTrace = new System.Diagnostics.StackTrace();
// Skip current method, StoreSmartMessage, and LogXXX method
var frame = stackTrace.GetFrame(3);
return frame?.GetMethod()?.Name ?? "Unknown";
}
catch
{
return "Unknown";
}
}
/// <summary>
/// Better stack frame detection for caller context
/// </summary>
private static (string className, string methodName) GetCallerContext()
{
try
{
var stackTrace = new System.Diagnostics.StackTrace();
for (int i = 1; i < Math.Min(stackTrace.FrameCount, 10); i++)
{
var frame = stackTrace.GetFrame(i);
var method = frame?.GetMethod();
if (method == null) continue;
var declaringType = method.DeclaringType;
if (declaringType == null) continue;
var typeName = declaringType.Name;
var methodName = method.Name;
// Skip system and wrapper methods
if (typeName.Contains("Logger") ||
typeName.Contains("QCLogger") ||
typeName.Contains("<") || // Skip compiler-generated
methodName.StartsWith("Smart") || // Skip SmartLog wrappers
methodName == "StoreSmartMessage" ||
methodName == "StoreSmartMessageWithContext" ||
methodName == "LogMessage" ||
methodName == "LogWithContext")
{
continue;
}
// If it's SimpleBaseStrategy, try to get the actual derived class by looking deeper
if (typeName == "SimpleBaseStrategy")
{
continue;
}
return (typeName, methodName);
}
return ("Unknown", "Unknown");
}
catch
{
return ("Unknown", "Unknown");
}
}
/// <summary>
/// Enhanced dynamic context detection inspired by Python logger
/// Uses deeper stack inspection to find the actual calling strategy class
/// </summary>
private static (string className, string methodName) GetCallerContextDynamic()
{
try
{
var stackTrace = new System.Diagnostics.StackTrace(true);
for (int i = 2; i < Math.Min(stackTrace.FrameCount, 15); i++)
{
var frame = stackTrace.GetFrame(i);
var method = frame?.GetMethod();
if (method == null) continue;
var declaringType = method.DeclaringType;
if (declaringType == null) continue;
var typeName = declaringType.Name;
var methodName = method.Name;
// Skip wrapper/system methods
if (IsWrapperMethod(typeName, methodName))
continue;
// For templates, get the actual derived class (highest priority)
if (typeName.EndsWith("Template") && !typeName.Contains("<"))
return (typeName, methodName);
// Skip SimpleBaseStrategy to find the actual strategy
if (typeName != "SimpleBaseStrategy" &&
!typeName.Contains("Logger") &&
!typeName.Contains("Algorithm") &&
!typeName.Contains("System"))
{
return (typeName, methodName);
}
}
return ("Unknown", "Unknown");
}
catch
{
return ("Unknown", "Unknown");
}
}
/// <summary>
/// Check if a method should be skipped during stack trace inspection
/// </summary>
private static bool IsWrapperMethod(string typeName, string methodName)
{
return typeName.Contains("Logger") ||
typeName.Contains("QCLogger") ||
typeName.Contains("<") || // Skip compiler-generated
methodName.StartsWith("Smart") || // Skip SmartLog wrappers
methodName == "StoreSmartMessage" ||
methodName == "StoreSmartMessageWithContext" ||
methodName == "LogMessage" ||
methodName == "LogWithContext" ||
methodName == "GetCallerContext" ||
methodName == "GetCallerContextDynamic" ||
typeName == "RuntimeMethodHandle" ||
typeName == "RuntimeType";
}
private string FormatMessage(string message, object[] args)
{
string formatted;
try
{
// Replace placeholders with argument indices for string.Format compatibility
if (args != null && args.Length > 0)
{
var formattedMessage = message;
for (int i = 0; i < args.Length; i++)
{
formattedMessage = formattedMessage.Replace($"{{{args[i]?.GetType()?.Name ?? "arg"}}}", $"{{{i}}}");
}
formatted = string.Format(formattedMessage, args);
}
else
{
formatted = message;
}
}
catch
{
// Fallback to simple concatenation if formatting fails
formatted = message;
if (args != null && args.Length > 0)
{
formatted += " [" + string.Join(", ", args) + "]";
}
}
return formatted;
}
}
}using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.Core.Services;
using CoreAlgo.Architecture.Core.Helpers;
using CoreAlgo.Architecture.Core.Configuration;
using CoreAlgo.Architecture.QC.Helpers;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Helper methods for SimpleBaseStrategy
/// This partial class contains logging, configuration, option setup, and utility methods
/// </summary>
public partial class SimpleBaseStrategy
{
// ============================================================================
// BASIC LOGGING AND UTILITY HELPERS
// ============================================================================
// QC Helper Methods - delegate to Algorithm instance
protected void Log(string message) => ((dynamic)Logger).Info(message);
protected void Error(string message) => ((dynamic)Logger).Error(message);
protected Security AddEquity(string ticker, Resolution resolution = Resolution.Minute) => Algorithm.AddEquity(ticker, resolution);
protected Security AddOption(string underlying, Resolution resolution = Resolution.Minute) => Algorithm.AddOption(underlying, resolution);
protected void SetStartDate(DateTime date) => Algorithm.SetStartDate(date);
protected void SetEndDate(DateTime date) => Algorithm.SetEndDate(date);
protected void SetCash(decimal cash) => Algorithm.SetCash(cash);
/// <summary>
/// Helper method for debug logging using context pattern
/// </summary>
protected void Debug(string message) => ((dynamic)Logger).Debug(message);
/// <summary>
/// Get typed configuration parameter with QC fallback
/// </summary>
protected T GetConfigParameter<T>(string key, T defaultValue = default(T))
{
try
{
var stringValue = Algorithm.GetParameter(key);
if (string.IsNullOrEmpty(stringValue))
return defaultValue;
return (T)Convert.ChangeType(stringValue, typeof(T));
}
catch
{
return defaultValue;
}
}
// ============================================================================
// SMART LOGGING WITH CONTEXT TRACKING
// ============================================================================
/// <summary>
/// Helper method for information logging with improved context tracking
/// </summary>
protected void SmartLog(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "")
{
// Extract actual class name from file path (QuantConnect compatible)
var className = ExtractClassNameFromPath(sourceFilePath);
if (string.IsNullOrEmpty(className))
className = this.GetType().Name;
((dynamic)Logger).LogWithContext(message, className, memberName, LogLevel.Info);
}
/// <summary>
/// Helper method for warning logging with improved context tracking
/// </summary>
protected void SmartWarn(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "")
{
var className = ExtractClassNameFromPath(sourceFilePath);
if (string.IsNullOrEmpty(className))
className = this.GetType().Name;
((dynamic)Logger).LogWithContext(message, className, memberName, LogLevel.Warning);
}
/// <summary>
/// Helper method for debug logging with improved context tracking
/// </summary>
protected void SmartDebug(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "")
{
var className = ExtractClassNameFromPath(sourceFilePath);
if (string.IsNullOrEmpty(className))
className = this.GetType().Name;
((dynamic)Logger).LogWithContext(message, className, memberName, LogLevel.Debug);
}
/// <summary>
/// Helper method for error logging with improved context tracking
/// </summary>
protected void SmartError(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "")
{
var className = ExtractClassNameFromPath(sourceFilePath);
if (string.IsNullOrEmpty(className))
className = this.GetType().Name;
((dynamic)Logger).LogWithContext(message, className, memberName, LogLevel.Error);
}
/// <summary>
/// Extract class name from file path using QuantConnect-compatible approach
/// </summary>
private string ExtractClassNameFromPath(string filePath)
{
if (string.IsNullOrEmpty(filePath))
return "";
try
{
// Extract filename from path manually (QuantConnect compatible)
var lastSlash = Math.Max(filePath.LastIndexOf('/'), filePath.LastIndexOf('\\'));
var fileName = lastSlash >= 0 ? filePath.Substring(lastSlash + 1) : filePath;
// Remove .cs extension
if (fileName.EndsWith(".cs"))
fileName = fileName.Substring(0, fileName.Length - 3);
return fileName;
}
catch
{
// Fallback to runtime type name
return this.GetType().Name;
}
}
// ============================================================================
// COMMON OPTION SETUP HELPERS (Eliminates duplication across templates)
// ============================================================================
/// <summary>
/// Set up underlying and options using AssetManager for consistent asset handling
/// </summary>
protected (Symbol underlying, Symbol options) SetupAssetWithOptions(string symbol, Resolution resolution = Resolution.Minute)
{
var security = AssetManager.AddAsset(this, symbol, resolution);
var optionsSymbol = AssetManager.AddOptionsChain(this, security, resolution);
return (security.Symbol, optionsSymbol);
}
/// <summary>
/// Set up options filter with common parameters used across strategies
/// </summary>
protected void SetupStandardOptionFilter(Symbol optionsSymbol, int strikeRange, int minDTE, int maxDTE,
bool callsOnly = false, bool putsOnly = false)
{
SmartLog($"[DEBUG] SETTING UP OPTION FILTER for {optionsSymbol}");
SmartLog($" Filter Parameters:");
SmartLog($" - strikeRange: {strikeRange} (±{strikeRange} strikes from ATM)");
SmartLog($" - minDTE: {minDTE} days");
SmartLog($" - maxDTE: {maxDTE} days");
SmartLog($" - callsOnly: {callsOnly}");
SmartLog($" - putsOnly: {putsOnly}");
// Verify the options symbol exists in Securities
if (!Algorithm.Securities.ContainsKey(optionsSymbol))
{
SmartLog($"[ERROR] CRITICAL: optionsSymbol {optionsSymbol} NOT found in Securities collection!");
SmartLog($" Available securities count: {Algorithm.Securities.Count}");
SmartLog($" Sample securities: {string.Join(", ", Algorithm.Securities.Keys.Take(5))}");
return;
}
var option = Algorithm.Securities[optionsSymbol] as Option;
if (option == null)
{
SmartLog($"[ERROR] CRITICAL: Security {optionsSymbol} is not an Option type!");
SmartLog($" Actual type: {Algorithm.Securities[optionsSymbol].Type}");
SmartLog($" Security details: {Algorithm.Securities[optionsSymbol]}");
return;
}
SmartLog($"[SUCCESS] Option security found: {option}");
SmartLog($" Option type: {option.Type}");
SmartLog($" Option resolution: {option.Subscriptions.GetHighestResolution()}");
try
{
var distinctResolutions = option.Subscriptions
.Select(s => s.Resolution)
.Distinct()
.OrderBy(r => r)
.ToList();
SmartLog($" All subscription resolutions: {string.Join(", ", distinctResolutions)}");
// Clarify option resolution diagnostics to avoid GetHighestResolution confusion
SmartWarn($"IMPORTANT: For canonical option universes, GetHighestResolution() and subscription lists may show 'Daily' due to the OptionUniverse subscription.");
SmartWarn($"Intraday readiness should be determined by actual presence of contracts in slice.OptionChains during runtime, not the canonical security's subscription summary.");
}
catch { }
SmartLog($" Option exchange: {option.Exchange}");
try
{
if (callsOnly)
{
SmartLog($"[TARGET] Applying CALLS ONLY filter");
option.SetFilter(filter => filter
.CallsOnly()
.Strikes(-strikeRange, strikeRange)
.Expiration(minDTE, maxDTE)
.IncludeWeeklys());
}
else if (putsOnly)
{
SmartLog($"[TARGET] Applying PUTS ONLY filter");
option.SetFilter(filter => filter
.PutsOnly()
.Strikes(-strikeRange, strikeRange)
.Expiration(minDTE, maxDTE)
.IncludeWeeklys());
}
else
{
SmartLog($"[TARGET] Applying BOTH CALLS AND PUTS filter");
option.SetFilter(filter => filter
.Strikes(-strikeRange, strikeRange)
.Expiration(minDTE, maxDTE)
.IncludeWeeklys());
}
SmartLog($"[SUCCESS] Option filter applied successfully for {optionsSymbol}");
SmartLog($" Filter should include strikes: {-strikeRange} to +{strikeRange} from ATM");
SmartLog($" Filter should include DTE: {minDTE} to {maxDTE} days");
SmartLog($" Option is now ready to receive data in slice.OptionChains");
}
catch (Exception ex)
{
SmartLog($"[ERROR] EXCEPTION applying option filter: {ex.Message}");
SmartLog($" Exception type: {ex.GetType().Name}");
SmartLog($" Stack trace: {ex.StackTrace}");
}
}
/// <summary>
/// Setup underlying and options with standard filter configuration
/// Combines asset setup and filtering into one call to reduce template code
/// </summary>
protected (Symbol underlying, Symbol options) SetupOptionsForSymbol(string symbol, int strikeRange = 10,
int minDTE = 3, int maxDTE = 30, bool callsOnly = false, bool putsOnly = false, Resolution resolution = Resolution.Minute)
{
var (underlying, options) = SetupAssetWithOptions(symbol, resolution);
SetupStandardOptionFilter(options, strikeRange, minDTE, maxDTE, callsOnly, putsOnly);
return (underlying, options);
}
private void OnError(Exception exception, ErrorSeverity severity, bool canContinue, string context)
{
ErrorOccurred?.Invoke(this, new StrategyErrorEventArgs(exception, severity, canContinue, context));
}
/// <summary>
/// Override for custom pre-execution logic that should always run.
/// </summary>
protected virtual void OnPreExecuteAlways(Slice slice)
{
// Intentionally left blank. Derived strategies can override to prepare non-order state
// such as computing time windows, refreshing mapped symbols, or resetting daily tracking.
}
// ============================================================================
// UNIVERSE OPTIMIZATION HELPERS (For Large Universe Strategies)
// ============================================================================
/// <summary>
/// Sets up optimized universe settings for large universe strategies.
/// Enables async selection and configures performance optimizations.
/// </summary>
/// <param name="resolution">Universe data resolution</param>
/// <param name="enableAsync">Enable asynchronous universe selection (recommended for large universes)</param>
/// <param name="extendedHours">Enable extended market hours</param>
protected void SetupAsyncUniverse(Resolution resolution = Resolution.Minute,
bool enableAsync = true, bool extendedHours = false)
{
UniverseOptimizer.SetupOptimizedUniverse(Algorithm, resolution, enableAsync, extendedHours);
}
/// <summary>
/// Batch fetch historical data for multiple symbols efficiently.
/// Processes symbols in chunks to avoid memory issues and timeouts.
/// </summary>
/// <param name="symbols">Symbols to fetch data for</param>
/// <param name="days">Number of days of history</param>
/// <param name="resolution">Data resolution</param>
/// <param name="batchSize">Batch size for processing (default: 500)</param>
/// <returns>Dictionary mapping Symbol to list of volumes</returns>
protected Dictionary<Symbol, List<decimal>> BatchFetchHistory(IEnumerable<Symbol> symbols,
int days, Resolution resolution = Resolution.Daily, int batchSize = 500)
{
return UniverseOptimizer.BatchFetchHistory(Algorithm, symbols, days, resolution, batchSize);
}
/// <summary>
/// Calculate Average Daily Volume (ADV) for multiple symbols efficiently.
/// Uses batch processing to handle large universes.
/// </summary>
/// <param name="symbols">Symbols to calculate ADV for</param>
/// <param name="days">Number of days for ADV calculation</param>
/// <returns>Dictionary mapping Symbol to ADV</returns>
protected Dictionary<Symbol, decimal> CalculateBatchedADV(IEnumerable<Symbol> symbols, int days = 21)
{
return UniverseOptimizer.CalculateBatchedADV(Algorithm, symbols, days);
}
/// <summary>
/// Calculate volume shock ratios for universe symbols efficiently.
/// Compares current intraday volume to historical average.
/// </summary>
/// <param name="intradayVolumes">Current intraday volume data</param>
/// <param name="symbols">Universe symbols to process</param>
/// <param name="advDays">Days for ADV calculation</param>
/// <returns>Dictionary mapping Symbol to shock ratio</returns>
protected Dictionary<Symbol, decimal> CalculateVolumeShock(ConcurrentDictionary<Symbol, long> intradayVolumes,
IEnumerable<Symbol> symbols, int advDays = 21)
{
return UniverseOptimizer.CalculateVolumeShock(Algorithm, intradayVolumes, symbols, advDays);
}
/// <summary>
/// Clean up removed securities from tracking structures to prevent memory leaks.
/// Call this from OnSecuritiesChanged for universe strategies.
/// </summary>
/// <param name="changes">Security changes from OnSecuritiesChanged</param>
/// <param name="trackingDictionaries">Collection of dictionaries to clean up</param>
protected void CleanupRemovedSecurities(SecurityChanges changes, params object[] trackingDictionaries)
{
UniverseOptimizer.CleanupRemovedSecurities(Algorithm, changes, trackingDictionaries);
}
/// <summary>
/// Process large universe data in parallel for improved performance.
/// Useful for computationally intensive selection logic.
/// </summary>
/// <typeparam name="TInput">Input data type</typeparam>
/// <typeparam name="TResult">Result data type</typeparam>
/// <param name="data">Input data collection</param>
/// <param name="processor">Function to process each item</param>
/// <param name="batchSize">Batch size for processing</param>
/// <returns>Collection of results</returns>
protected IEnumerable<TResult> ProcessUniverseInParallel<TInput, TResult>(IEnumerable<TInput> data,
Func<TInput, TResult> processor, int batchSize = 500)
{
return UniverseOptimizer.ProcessInParallel(data, processor, batchSize);
}
}
}using System;
using System.Linq;
using QuantConnect;
using QuantConnect.Orders;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.QC.Helpers;
using CoreAlgo.Architecture.Core.Configuration;
using CoreAlgo.Architecture.Core.Helpers;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Trade tracking and assignment handling methods for SimpleBaseStrategy
/// This partial class contains all trade tracking functionality similar to Python Position.py
/// </summary>
public partial class SimpleBaseStrategy
{
// ============================================================================
// TRADE TRACKING HELPERS (Simple trade data collection like Python Position.py)
// ============================================================================
/// <summary>
/// Track a new working order (order submitted but not filled)
/// Captures SecurityIdentifier for symbol reconstruction
/// </summary>
protected void TrackWorkingOrder(OrderTicket ticket, string strategy = "")
{
if (ticket?.OrderId != null)
{
var orderId = ticket.OrderId.ToString();
var symbol = ticket.Symbol?.Value ?? "Unknown";
var strategyName = !string.IsNullOrEmpty(strategy) ? strategy : Name;
var trade = new TradeRecord(orderId, symbol, strategyName, ticket.Tag);
// Capture SecurityIdentifier for persistence
if (ticket.Symbol != null)
{
trade.SymbolIds.Add(ticket.Symbol.ID.ToString());
}
TradeTracker.AllTrades.Add(trade);
TradeTracker.WorkingTrades.Add(trade);
Debug($"Tracking working order: {orderId} for {symbol}");
// Persist to ObjectStore
TradePersistence.SaveTrades(TradeTracker);
}
}
/// <summary>
/// Mark an order as filled (moved from working to open)
/// Resolves trade by any order ID (entry or child), enhances with symbol info, and marks open only if it's an entry order
/// </summary>
public void TrackOrderFilled(OrderEvent orderEvent)
{
if (orderEvent?.OrderId != null)
{
var orderId = orderEvent.OrderId;
var orderIdStr = orderId.ToString();
// Resolve trade by any order ID (entry or child)
var trade = TradeTracker.FindTradeByAnyOrderId(orderId);
// Enhance trade record with symbol info if found
if (trade != null && orderEvent.Symbol != null)
{
// Enhance with Symbol ID if not already present
var symbolId = orderEvent.Symbol.ID.ToString();
if (!trade.SymbolIds.Contains(symbolId))
{
trade.SymbolIds.Add(symbolId);
}
if (string.IsNullOrEmpty(trade.Symbol) || trade.Symbol == "Unknown")
{
trade.Symbol = orderEvent.Symbol.Value;
}
}
// Only mark as open if this is an entry order (already tracked as Working)
// Child orders (SL/TP) should not create new trades or transition to Open
TradeTracker.MarkTradeAsOpen(orderIdStr, orderEvent.FillPrice, (int)orderEvent.FillQuantity, Algorithm.UtcTime);
Debug($"Order filled: {orderId} at {orderEvent.FillPrice}");
OnOrderFilled(orderEvent);
// Persist to ObjectStore on fill
TradePersistence.SaveTrades(TradeTracker);
}
}
/// <summary>
/// Register a child order (SL/TP) with its parent entry trade
/// Adds child order ID to trade's OrderIds list for lookup via FindTradeByAnyOrderId
/// </summary>
protected void RegisterChildOrder(int parentEntryOrderId, int childOrderId)
{
var trade = TradeTracker.FindTradeByAnyOrderId(parentEntryOrderId);
if (trade != null && !trade.OrderIds.Contains(childOrderId))
{
trade.OrderIds.Add(childOrderId);
}
}
/// <summary>
/// Hook for derived strategies to handle order fill events immediately (event-time logic).
/// Default implementation is no-op.
/// </summary>
/// <param name="orderEvent">The order event that was filled</param>
protected virtual void OnOrderFilled(OrderEvent orderEvent) { }
/// <summary>
/// Hook for derived strategies to receive ALL order events (fills, cancels, partials, submitted).
/// Called after tracking system processes the event. Use for event-time logic like swap-to-TP.
/// Default implementation is no-op.
/// </summary>
/// <param name="orderEvent">Any order event from QuantConnect</param>
public virtual void OnOrderEventRouted(OrderEvent orderEvent) { }
/// <summary>
/// Mark a position as closed with P&L
/// </summary>
protected void TrackPositionClosed(string orderId, decimal closePrice, decimal pnl)
{
TradeTracker.MarkTradeAsClosed(orderId, closePrice, pnl);
Debug($"Position closed: {orderId}, P&L: {pnl}");
TradePersistence.SaveTrades(TradeTracker);
}
/// <summary>
/// Cancel a working order
/// </summary>
public void TrackOrderCancelled(string orderId)
{
TradeTracker.CancelWorkingTrade(orderId);
Debug($"Order cancelled: {orderId}");
TradePersistence.SaveTrades(TradeTracker);
}
/// <summary>
/// Handle option assignments using QuantConnect's native assignment detection.
/// Automatically liquidates assigned underlying shares to prevent margin crises.
/// </summary>
/// <param name="assignmentEvent">Assignment order event from QuantConnect</param>
public virtual void OnAssignmentOrderEvent(OrderEvent assignmentEvent)
{
try
{
SmartLog($"Assignment event received: {assignmentEvent.Symbol} at {Algorithm.Time}");
// Use our QC-native assignment handler
AssignmentHandler.HandleAssignment(Algorithm, assignmentEvent);
// Allow derived strategies to add custom assignment logic
OnAssignmentHandled(assignmentEvent);
}
catch (Exception ex)
{
SmartError($"Error in assignment handling: {ex.Message}");
OnError(ex, ErrorSeverity.Error, true, "Assignment handling error");
}
}
/// <summary>
/// Called after assignment handling is complete. Override in derived strategies for custom logic.
/// </summary>
/// <param name="assignmentEvent">The processed assignment event</param>
protected virtual void OnAssignmentHandled(OrderEvent assignmentEvent)
{
// Default implementation - derived strategies can override
SmartLog($"Assignment handling completed for {assignmentEvent.Symbol}");
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Orders;
using QuantConnect.Securities;
using CoreAlgo.Architecture.QC.Helpers;
using CoreAlgo.Architecture.Core.Configuration;
using CoreAlgo.Architecture.Core.Services;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.Core.Execution;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Trading-related methods for SimpleBaseStrategy
/// This partial class contains all order management and smart trading functionality
/// </summary>
public partial class SimpleBaseStrategy
{
// Smart order methods that use SmartOrderManager when available and enabled
// All orders use asynchronous: true for faster live trading (Lean v17282+)
protected OrderTicket MarketOrder(Symbol symbol, decimal quantity, string tag = "")
{
if (ShouldUseSmartOrderManager())
{
return SmartOrderManager.SmartMarketOrder(symbol, quantity, tag);
}
return Algorithm.MarketOrder(symbol, quantity, tag: tag, asynchronous: true);
}
protected OrderTicket LimitOrder(Symbol symbol, decimal quantity, decimal limitPrice, string tag = "")
{
var rounded = PriceRounding.RoundLimitPrice(Algorithm.Securities, symbol, quantity, limitPrice);
return Algorithm.LimitOrder(symbol, quantity, rounded, tag: tag, asynchronous: true);
}
protected OrderTicket StopMarketOrder(Symbol symbol, decimal quantity, decimal stopPrice, string tag = "")
{
var rounded = PriceRounding.RoundStopPrice(Algorithm.Securities, symbol, quantity, stopPrice);
return Algorithm.StopMarketOrder(symbol, quantity, rounded, tag: tag, asynchronous: true);
}
protected OrderTicket StopLimitOrder(Symbol symbol, decimal quantity, decimal stopPrice, decimal limitPrice, string tag = "")
{
try
{
var (roundedStop, roundedLimit) = PriceRounding.RoundStopLimitPrices(Algorithm.Securities, symbol, quantity, stopPrice, limitPrice);
// Pre-validation: ensure relationship is valid after rounding
if (quantity < 0) // Sell stop-limit
{
if (roundedLimit >= roundedStop)
{
SmartWarn($"[STOP-LIMIT INVALID] Sell: limit {roundedLimit} >= stop {roundedStop}. Falling back to StopMarketOrder.");
return Algorithm.StopMarketOrder(symbol, quantity, roundedStop, tag: tag, asynchronous: true);
}
}
else // Buy stop-limit
{
if (roundedLimit <= roundedStop)
{
SmartWarn($"[STOP-LIMIT INVALID] Buy: limit {roundedLimit} <= stop {roundedStop}. Falling back to StopMarketOrder.");
return Algorithm.StopMarketOrder(symbol, quantity, roundedStop, tag: tag, asynchronous: true);
}
}
return Algorithm.StopLimitOrder(symbol, quantity, roundedStop, roundedLimit, tag: tag, asynchronous: true);
}
catch (ArgumentException ex)
{
SmartError($"[STOP-LIMIT ERROR] {ex.Message}. Falling back to StopMarketOrder at {stopPrice:F4}.");
return Algorithm.StopMarketOrder(symbol, quantity, stopPrice, tag: tag, asynchronous: true);
}
}
protected OrderTicket TrailingStopOrder(Symbol symbol, decimal quantity, decimal trailingAmount, bool isDollarTrailing, string tag = "")
{
var rounded = PriceRounding.RoundTrailingStopPrice(Algorithm.Securities, symbol, quantity, trailingAmount, isDollarTrailing);
return Algorithm.TrailingStopOrder(symbol, quantity, rounded, isDollarTrailing, tag: tag, asynchronous: true);
}
protected List<OrderTicket> ComboMarketOrder(List<Leg> legs, int quantity, string tag = "")
{
if (ShouldUseSmartOrderManager())
{
return SmartOrderManager.SmartComboMarketOrder(legs, quantity, tag);
}
return Algorithm.ComboMarketOrder(legs, quantity, tag: tag, asynchronous: true);
}
protected OrderTicket MarketOnOpenOrder(Symbol symbol, int quantity, string tag = "")
{
return Algorithm.MarketOnOpenOrder(symbol, quantity, tag: tag, asynchronous: true);
}
protected OrderTicket MarketOnCloseOrder(Symbol symbol, int quantity, string tag = "")
{
return Algorithm.MarketOnCloseOrder(symbol, quantity, tag: tag, asynchronous: true);
}
protected List<OrderTicket> ComboLimitOrder(List<Leg> legs, int quantity, decimal limitPrice, string tag = "")
{
// Note: ComboLimitOrder may not support asynchronous parameter in C# (per QC forum discussion)
// Attempt to use it, but fall back if not available
try
{
return Algorithm.ComboLimitOrder(legs, quantity, limitPrice, tag: tag, asynchronous: true);
}
catch (Exception)
{
// Fallback to synchronous if async not supported
return Algorithm.ComboLimitOrder(legs, quantity, limitPrice, tag: tag);
}
}
protected OrderTicket Liquidate(Symbol symbol, string tag = "")
{
// Liquidate with async returns List<OrderTicket> - extract first ticket
dynamic result = Algorithm.Liquidate(symbol: symbol, tag: tag, asynchronous: true);
if (result is List<OrderTicket> tickets && tickets.Count > 0)
{
return tickets[0];
}
// Fallback: try to return as OrderTicket
return result as OrderTicket ?? null;
}
protected void LiquidateAll(string tag = "")
{
Algorithm.Liquidate(asynchronous: true);
}
protected void SetHoldingsTargets(List<PortfolioTarget> targets, bool liquidateExistingHoldings = false)
{
Algorithm.SetHoldings(targets, liquidateExistingHoldings: liquidateExistingHoldings, asynchronous: true);
}
// Helper method to determine if SmartOrderManager should be used
private bool ShouldUseSmartOrderManager()
{
// Always route orders through SmartOrderManager when available.
return SmartOrderManager != null;
}
// Helper method to determine if SmartPricing should be used
private bool ShouldUseSmartPricing()
{
if (SmartOrderManager == null) return false;
// Check if SmartPricingMode is enabled in config
if (Config is StrategyConfig strategyConfig)
{
var smartPricingMode = strategyConfig.GetParameterValue("SmartPricingMode", "Off");
return !string.Equals(smartPricingMode?.ToString(), "Off", StringComparison.OrdinalIgnoreCase);
}
return false;
}
// Helper method to check if overlap prevention is enabled
private bool IsOverlapPreventionEnabled()
{
if (Config is StrategyConfig strategyConfig)
{
return strategyConfig.EnableOverlapPrevention;
}
return false;
}
// Setup SmartPricing engine based on configuration
private void SetupSmartPricing()
{
if (SmartOrderManager == null)
{
((dynamic)Logger).Warning("SetupSmartPricing called but SmartOrderManager is null");
return;
}
var smartPricingMode = "Off";
// Try to get from Config first (if available)
if (Config is StrategyConfig strategyConfig)
{
smartPricingMode = strategyConfig.GetParameterValue("SmartPricingMode", "Off")?.ToString() ?? "Off";
((dynamic)Logger).Info($"SmartPricing mode from Config: {smartPricingMode}");
}
else
{
// Fallback to direct parameter reading if Config not yet loaded
smartPricingMode = Algorithm.GetParameter("SmartPricingMode", "Off");
((dynamic)Logger).Info($"SmartPricing mode from Algorithm.GetParameter: {smartPricingMode}");
}
if (!string.Equals(smartPricingMode, "Off", StringComparison.OrdinalIgnoreCase))
{
try
{
((dynamic)Logger).Info($"Creating SmartPricing engine for mode: {smartPricingMode}");
var pricingEngine = SmartPricingEngineFactory.Create(smartPricingMode);
SmartOrderManager.SetPricingEngine(pricingEngine);
((dynamic)Logger).Info($"SmartPricing enabled with mode: {smartPricingMode}");
}
catch (Exception ex)
{
((dynamic)Logger).Error($"Failed to setup SmartPricing: {ex.Message}");
}
}
else
{
((dynamic)Logger).Info("SmartPricing disabled (mode is Off)");
}
}
/// <summary>
/// Helper method for creating option combo orders with better error handling
/// </summary>
protected OrderTicket SubmitComboOrder(List<Leg> legs, string tag = "")
{
try
{
// Validate margin before placing combo order
var underlyingSymbol = legs.FirstOrDefault()?.Symbol.Underlying.Value ?? "";
if (!string.IsNullOrEmpty(underlyingSymbol) &&
Algorithm.Portfolio.MarginRemaining < 0)
{
SmartWarn($"Insufficient margin for combo order on {underlyingSymbol}");
return null;
}
var tickets = ComboMarketOrder(legs, 1, tag: tag);
return tickets?.FirstOrDefault();
}
catch (Exception ex)
{
Error($"Failed to submit combo order: {ex.Message}");
return null;
}
}
/// <summary>
/// Helper method for progressive pricing (simple retry logic)
/// </summary>
protected OrderTicket SubmitOrderWithRetry(Symbol symbol, decimal quantity, decimal? limitPrice = null, int maxRetries = 3)
{
// Check margin utilization before attempting order
// Check if approaching margin call using QC's built-in calculations
var marginUtilization = Algorithm.Portfolio.TotalMarginUsed / Algorithm.Portfolio.TotalPortfolioValue;
if (marginUtilization > 0.7m)
{
SmartWarn($"Margin utilization too high, skipping order for {symbol}");
return null;
}
for (int i = 0; i < maxRetries; i++)
{
try
{
var ticket = limitPrice.HasValue ?
LimitOrder(symbol, quantity, limitPrice.Value) :
MarketOrder(symbol, quantity);
if (ticket != null)
return ticket;
}
catch (Exception ex)
{
Debug($"Order attempt {i + 1} failed: {ex.Message}");
if (i == maxRetries - 1)
throw;
}
}
return null;
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities.Option;
using CoreAlgo.Architecture.Core.Interfaces;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.Core.Configuration;
using CoreAlgo.Architecture.Core.Helpers;
using CoreAlgo.Architecture.Core.Services;
using CoreAlgo.Architecture.Core.Execution;
using CoreAlgo.Architecture.QC.Helpers;
using System.Collections.Concurrent;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Simplified base strategy that leverages QC's strengths while adding configuration and light extensions
/// Implements context pattern for centralized logging and algorithm access
/// </summary>
public abstract partial class SimpleBaseStrategy : IStrategy, IAlgorithmContext
{
public StrategyConfig Config { get; private set; }
protected EntryRestrictions EntryRestrictions { get; private set; }
protected ExitRestrictions ExitRestrictions { get; private set; }
protected StrikeRangeCalculator StrikeRangeCalculator { get; private set; }
private StrategyState _state = StrategyState.NotInitialized;
private Dictionary<string, object> _parameters = new Dictionary<string, object>();
// Context pattern implementation - provides centralized access to algorithm and logger
public QCAlgorithm Algorithm { get; private set; }
public object Logger { get; private set; }
// SmartPricing integration
public SmartOrderManager SmartOrderManager { get; private set; }
// Position overlap management
protected PositionOverlapManager OverlapManager { get; private set; }
// Trade tracking system (like Python setupbasestructure.py arrays)
public TradeTracker TradeTracker { get; private set; } = new TradeTracker();
// Trade persistence service for ObjectStore integration
public TradePersistenceService TradePersistence { get; private set; }
/// <inheritdoc/>
public abstract string Name { get; }
/// <inheritdoc/>
public abstract string Description { get; }
/// <inheritdoc/>
public virtual string Version => "1.0.0";
/// <inheritdoc/>
public StrategyState State
{
get => _state;
private set
{
if (_state != value)
{
var previousState = _state;
_state = value;
StateChanged?.Invoke(this, new StrategyStateChangedEventArgs(previousState, value));
}
}
}
/// <inheritdoc/>
public Dictionary<string, object> Parameters => _parameters;
/// <inheritdoc/>
public event EventHandler<StrategyStateChangedEventArgs> StateChanged;
/// <inheritdoc/>
public event EventHandler<StrategyErrorEventArgs> ErrorOccurred;
/// <inheritdoc/>
public virtual void Initialize(QCAlgorithm algorithm)
{
Algorithm = algorithm;
// Logger will be injected via context pattern - no initialization here
// Logger comes from Main.cs via SetContext method
// Initialize calculators
StrikeRangeCalculator = new StrikeRangeCalculator(algorithm);
// Initialize SmartOrderManager if logger is available
if (Logger != null)
{
// Create algorithm context adapter
var context = new SimpleAlgorithmContext(algorithm, Logger);
SmartOrderManager = new SmartOrderManager(algorithm, context);
// Initialize trade persistence service
TradePersistence = new TradePersistenceService(context);
// Clear positions if backtest (LiveMode protection built in)
TradePersistence.ClearPositionsIfBacktest();
// Try to load previous trade state from ObjectStore
var loadedTracker = TradePersistence.LoadTrades();
if (loadedTracker != null)
{
TradeTracker = loadedTracker;
((dynamic)Logger).Info($"[{Name}] Restored {TradeTracker.AllTrades.Count} trades from ObjectStore");
}
// Note: OverlapManager will be initialized later in OnConfigured() after Config is loaded
// Set up pricing engine based on configuration
SetupSmartPricing();
}
State = StrategyState.Initializing;
OnInitialize();
State = StrategyState.Ready;
}
/// <inheritdoc/>
public virtual void Initialize(QCAlgorithm algorithm, Dictionary<string, object> parameters)
{
Algorithm = algorithm;
_parameters = parameters ?? new Dictionary<string, object>();
// Logger will be injected via context pattern - no initialization here
// Logger comes from Main.cs via SetContext method
// Initialize calculators
StrikeRangeCalculator = new StrikeRangeCalculator(algorithm);
State = StrategyState.Initializing;
OnInitialize();
State = StrategyState.Ready;
}
/// <summary>
/// Set the context logger (injected from Main.cs)
/// This implements the true context pattern where logger is created once in Main.cs
/// </summary>
public void SetContext(object logger)
{
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Configure the strategy with typed configuration
/// </summary>
public virtual void Configure<T>() where T : StrategyConfig, new()
{
Config = new T();
Config.LoadFromParameters(this); // Pass context instead of separate parameters
// Higher-level validation before proceeding
var validationErrors = Config.Validate();
if (validationErrors.Length > 0)
{
var errorMessage = $"Configuration validation failed:\n{string.Join("\n", validationErrors)}";
((dynamic)Logger).Error(errorMessage);
throw new InvalidOperationException(errorMessage);
}
((dynamic)Logger).Debug($"Configuration validation passed for {typeof(T).Name}");
// Initialize entry/exit restrictions with the loaded config
EntryRestrictions = new EntryRestrictions(Config, Algorithm);
ExitRestrictions = new ExitRestrictions(Config, Algorithm);
OnConfigured();
}
/// <summary>
/// Called during strategy initialization - override to set up securities, indicators, etc.
/// </summary>
public abstract void OnInitialize();
// Core methods moved to partial class files:
// - Trading methods: SimpleBaseStrategy.Trading.cs
// - Tracking methods: SimpleBaseStrategy.Tracking.cs
// - Helper methods: SimpleBaseStrategy.Helpers.cs
protected SecurityPortfolioManager Portfolio => Algorithm.Portfolio;
protected SecurityManager Securities => Algorithm.Securities;
protected DateTime Time => Algorithm.Time;
/// <summary>
/// Called after configuration is loaded
/// </summary>
protected virtual void OnConfigured()
{
// Initialize position overlap manager now that Config is available
if (Config != null && Config.EnableOverlapPrevention && SmartOrderManager != null && Logger != null)
{
var context = new SimpleAlgorithmContext(Algorithm, Logger);
OverlapManager = new PositionOverlapManager(context);
SmartOrderManager.SetOverlapManager(OverlapManager);
((dynamic)Logger).Info($"[{Name}] Position overlap prevention enabled (Mode: {Config.OverlapPreventionMode})");
}
}
/// <summary>
/// Ensures SmartPricing is initialized if needed (called from Main.cs after full initialization)
/// </summary>
public void EnsureSmartPricingInitialized()
{
((dynamic)Logger).Info($"EnsureSmartPricingInitialized called. SmartOrderManager null: {SmartOrderManager == null}, Logger null: {Logger == null}, Algorithm null: {Algorithm == null}");
if (SmartOrderManager == null && Logger != null && Algorithm != null)
{
((dynamic)Logger).Info("Creating SmartOrderManager...");
// Create algorithm context adapter
var context = new SimpleAlgorithmContext(Algorithm, Logger);
SmartOrderManager = new SmartOrderManager(Algorithm, context);
// Set up pricing engine based on configuration
SetupSmartPricing();
((dynamic)Logger).Info("SmartOrderManager initialized after strategy setup");
}
else
{
((dynamic)Logger).Warning($"SmartOrderManager initialization skipped - already exists: {SmartOrderManager != null}");
}
}
/// <summary>
/// Centralized validation to determine if the strategy should execute trades.
/// Checks common conditions like trading hours, position limits, margin utilization.
/// Templates can override OnShouldExecuteTrade for strategy-specific validations.
/// </summary>
/// <param name="slice">Current market data slice</param>
/// <param name="blockReason">Reason why trading is blocked (if returning false)</param>
/// <returns>True if strategy should proceed with trade execution, false otherwise</returns>
protected virtual bool ShouldExecuteTrade(Slice slice, out string blockReason)
{
blockReason = "";
// Skip validation if config not loaded yet
if (Config == null) return true;
// 1. Trading Hours Check (if configured)
// Use TimeSpan.Zero to disable this check
if (Config.TradingStartTime != TimeSpan.Zero || Config.TradingEndTime != TimeSpan.Zero)
{
var timeOfDay = slice.Time.TimeOfDay;
if (timeOfDay < Config.TradingStartTime || timeOfDay > Config.TradingEndTime)
{
blockReason = $"Outside trading hours ({Config.TradingStartTime:hh\\:mm}-{Config.TradingEndTime:hh\\:mm})";
return false;
}
}
// 2. Position Limit Check (if MaxPositions > 0)
// Use 0 or negative values to disable this check
if (Config.MaxPositions > 0)
{
var activePositions = Portfolio.Where(p => p.Value.Invested).Count();
if (activePositions >= Config.MaxPositions)
{
blockReason = $"Max positions reached ({activePositions}/{Config.MaxPositions})";
return false;
}
}
// 3. Margin Utilization Check (configurable threshold)
var marginThreshold = Config.GetParameterValue("MaxMarginUtilization", 0.7m) as decimal? ?? 0.7m;
if (marginThreshold > 0 && Portfolio.TotalPortfolioValue > 0)
{
var marginUtilization = Portfolio.TotalMarginUsed / Portfolio.TotalPortfolioValue;
if (marginUtilization > marginThreshold)
{
blockReason = $"Margin utilization too high ({marginUtilization:P0} > {marginThreshold:P0})";
return false;
}
}
// 4. Central Entry Window Check (optional)
if (Config.UseEntryTimeWindow)
{
var tod = slice.Time.TimeOfDay;
if (tod < Config.EntryWindowStart || tod > Config.EntryWindowEnd)
{
blockReason = $"Outside entry window ({Config.EntryWindowStart:hh\\:mm}-{Config.EntryWindowEnd:hh\\:mm})";
return false;
}
}
// 4. Available Cash Check (ensure minimum cash reserves)
var minCashReserveRatio = Config.GetParameterValue("MinCashReserveRatio", 0.05m) as decimal? ?? 0.05m;
if (minCashReserveRatio > 0 && Portfolio.TotalPortfolioValue > 0)
{
var cashRatio = Portfolio.Cash / Portfolio.TotalPortfolioValue;
if (cashRatio < minCashReserveRatio)
{
blockReason = $"Insufficient cash reserves ({cashRatio:P1} < {minCashReserveRatio:P1})";
return false;
}
}
// 5. Call strategy-specific validation hook
if (!OnShouldExecuteTrade(slice, out var customReason))
{
blockReason = customReason;
return false;
}
return true;
}
/// <summary>
/// Strategy-specific validation hook. Override in templates for custom validations
/// like daily trade limits, loss limits, or other strategy-specific conditions.
/// </summary>
/// <param name="slice">Current market data slice</param>
/// <param name="blockReason">Reason why trading should be blocked</param>
/// <returns>True if strategy-specific conditions allow trading</returns>
protected virtual bool OnShouldExecuteTrade(Slice slice, out string blockReason)
{
blockReason = "";
return true;
}
/// <summary>
/// Check exit conditions for all current positions using ExitRestrictions.
/// This is called even when new trades are blocked to ensure proper exits.
/// </summary>
/// <param name="slice">Current market data slice</param>
protected virtual void CheckExitConditions(Slice slice)
{
if (ExitRestrictions == null) return;
// Check all invested positions for exit conditions
var positionsToCheck = Portfolio.Where(p => p.Value.Invested).ToList();
foreach (var position in positionsToCheck)
{
if (ExitRestrictions.ShouldExitPosition(position.Key, slice, out var reason))
{
SmartLog($"[EXIT SIGNAL] {position.Key}: {reason}");
// Call exit signal handler - templates can override for custom exit logic
OnExitSignal(position.Key, reason);
}
}
}
/// <summary>
/// Handle exit signal for a position. Override in templates for custom exit logic.
/// Default implementation liquidates the position immediately.
/// </summary>
/// <param name="symbol">Symbol to exit</param>
/// <param name="reason">Reason for the exit</param>
protected virtual void OnExitSignal(Symbol symbol, string reason)
{
try
{
// Default behavior - liquidate immediately
var ticket = Liquidate(symbol, reason);
if (ticket != null)
{
SmartLog($"[EXIT ORDER] Liquidating {symbol}: {reason}");
}
else
{
SmartError($"[EXIT ERROR] Failed to liquidate {symbol}: {reason}");
}
}
catch (Exception ex)
{
SmartError($"[EXIT ERROR] Exception liquidating {symbol}: {ex.Message}");
}
}
/// <inheritdoc/>
public virtual void Execute(Slice slice)
{
if (State == StrategyState.Ready)
State = StrategyState.Running;
if (State != StrategyState.Running)
{
return;
}
try
{
// Allow strategies to prepare required state before gating (no order placement here)
OnPreExecuteAlways(slice);
// Update positions using QC's native Portfolio
// QC's Portfolio updates automatically
// 1. Always check exit conditions first (even if new trades are blocked)
CheckExitConditions(slice);
// 2. Check if new trades should be executed
if (!ShouldExecuteTrade(slice, out var blockReason))
{
// Log blocking reason periodically to avoid spam
// Only log on the first minute of each hour to reduce noise
if (slice.Time.Minute == 0 && slice.Time.Second == 0)
{
SmartLog($"[TRADING BLOCKED] {blockReason}");
}
return;
}
// 3. Execute strategy-specific logic if validation passes
OnExecute(slice);
}
catch (Exception ex)
{
((dynamic)Logger).Error($"Error during strategy execution: {ex.Message}");
((dynamic)Logger).Error($"Stack trace: {ex.StackTrace}");
OnError(ex, ErrorSeverity.Error, true, "Error during strategy execution");
throw;
}
}
/// <inheritdoc/>
public virtual void Shutdown()
{
if (State == StrategyState.Shutdown || State == StrategyState.ShuttingDown)
return;
try
{
State = StrategyState.ShuttingDown;
OnShutdown();
State = StrategyState.Shutdown;
}
catch (Exception ex)
{
OnError(ex, ErrorSeverity.Error, false, "Error during strategy shutdown");
State = StrategyState.Error;
throw;
}
}
/// <inheritdoc/>
public virtual bool Validate()
{
try
{
return OnValidate();
}
catch (Exception ex)
{
OnError(ex, ErrorSeverity.Warning, true, "Error during strategy validation");
return false;
}
}
/// <inheritdoc/>
public virtual Dictionary<string, double> GetPerformanceMetrics()
{
var metrics = new Dictionary<string, double>();
try
{
// Use QC's native portfolio metrics
metrics["TotalPortfolioValue"] = (double)Portfolio.TotalPortfolioValue;
metrics["Cash"] = (double)Portfolio.Cash;
metrics["TotalHoldingsValue"] = (double)Portfolio.TotalHoldingsValue;
metrics["UnrealizedProfit"] = (double)Portfolio.TotalUnrealizedProfit;
metrics["TotalProfit"] = (double)Portfolio.TotalProfit;
// Add custom metrics from derived strategies
OnGetPerformanceMetrics(metrics);
}
catch (Exception ex)
{
OnError(ex, ErrorSeverity.Warning, true, "Error getting performance metrics");
}
return metrics;
}
/// <inheritdoc/>
public virtual void Reset()
{
if (State == StrategyState.Running)
throw new InvalidOperationException("Cannot reset strategy while running");
try
{
OnReset();
State = StrategyState.Ready;
}
catch (Exception ex)
{
OnError(ex, ErrorSeverity.Error, false, "Error during strategy reset");
throw;
}
}
/// <summary>
/// Strategy-specific execution logic
/// </summary>
protected abstract void OnExecute(Slice slice);
/// <summary>
/// Override for custom shutdown logic
/// </summary>
protected virtual void OnShutdown()
{
// Export trade tracking data to logs (like Python main.py export)
try
{
SmartLog("Exporting trade tracking data...");
TradeTracker.ExportToLogs(message => SmartLog(message));
SmartLog(TradeTracker.GetSummary());
}
catch (Exception ex)
{
SmartError($"Failed to export trade data: {ex.Message}");
}
}
/// <summary>
/// Override for custom validation logic
/// </summary>
protected virtual bool OnValidate() => true;
/// <summary>
/// Override to add custom performance metrics
/// </summary>
protected virtual void OnGetPerformanceMetrics(Dictionary<string, double> metrics) { }
/// <summary>
/// Override for custom reset logic
/// </summary>
protected virtual void OnReset() { }
/// <summary>
/// Handle security changes from universe selection.
/// Override in templates that use dynamic universe selection.
/// </summary>
/// <param name="changes">Security changes from QuantConnect</param>
public virtual void OnSecuritiesChanged(SecurityChanges changes)
{
// Default implementation - do nothing
// Strategies using universe selection can override
}
// Additional helper methods moved to SimpleBaseStrategy.Helpers.cs
// Trade tracking methods moved to SimpleBaseStrategy.Tracking.cs
// Option setup helpers moved to SimpleBaseStrategy.Helpers.cs
// Universe optimization helpers moved to SimpleBaseStrategy.Helpers.cs
}
}#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Selection;
using QuantConnect.Api;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Commands;
using QuantConnect.Configuration;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.Shortable;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.OptionExercise;
using QuantConnect.Orders.Slippage;
using QuantConnect.Orders.TimeInForces;
using QuantConnect.Python;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Positions;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using Calendar = QuantConnect.Data.Consolidators.Calendar;
#endregion
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Event arguments for strategy state changes
/// </summary>
public class StrategyStateChangedEventArgs : EventArgs
{
/// <summary>
/// Gets the previous state
/// </summary>
public StrategyState PreviousState { get; }
/// <summary>
/// Gets the new state
/// </summary>
public StrategyState NewState { get; }
/// <summary>
/// Gets the timestamp of the state change
/// </summary>
public DateTime Timestamp { get; }
/// <summary>
/// Gets any additional message about the state change
/// </summary>
public string Message { get; }
/// <summary>
/// Creates a new instance of StrategyStateChangedEventArgs
/// </summary>
public StrategyStateChangedEventArgs(StrategyState previousState, StrategyState newState, string message = null)
{
PreviousState = previousState;
NewState = newState;
Timestamp = DateTime.UtcNow;
Message = message;
}
}
/// <summary>
/// Event arguments for strategy errors
/// </summary>
public class StrategyErrorEventArgs : EventArgs
{
/// <summary>
/// Gets the error that occurred
/// </summary>
public Exception Error { get; }
/// <summary>
/// Gets the error severity
/// </summary>
public ErrorSeverity Severity { get; }
/// <summary>
/// Gets the timestamp of the error
/// </summary>
public DateTime Timestamp { get; }
/// <summary>
/// Gets whether the strategy can continue
/// </summary>
public bool CanContinue { get; }
/// <summary>
/// Gets additional context about the error
/// </summary>
public string Context { get; }
/// <summary>
/// Creates a new instance of StrategyErrorEventArgs
/// </summary>
public StrategyErrorEventArgs(Exception error, ErrorSeverity severity, bool canContinue, string context = null)
{
Error = error;
Severity = severity;
CanContinue = canContinue;
Context = context;
Timestamp = DateTime.UtcNow;
}
}
/// <summary>
/// Error severity levels
/// </summary>
public enum ErrorSeverity
{
/// <summary>
/// Informational message
/// </summary>
Info,
/// <summary>
/// Warning that doesn't affect execution
/// </summary>
Warning,
/// <summary>
/// Error that may affect execution
/// </summary>
Error,
/// <summary>
/// Critical error that stops execution
/// </summary>
Critical
}
}#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Selection;
using QuantConnect.Api;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Commands;
using QuantConnect.Configuration;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.Shortable;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.OptionExercise;
using QuantConnect.Orders.Slippage;
using QuantConnect.Orders.TimeInForces;
using QuantConnect.Python;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Positions;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using Calendar = QuantConnect.Data.Consolidators.Calendar;
#endregion
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Represents the state of a strategy
/// </summary>
public enum StrategyState
{
/// <summary>
/// Strategy has not been initialized
/// </summary>
NotInitialized,
/// <summary>
/// Strategy is being initialized
/// </summary>
Initializing,
/// <summary>
/// Strategy is initialized and ready
/// </summary>
Ready,
/// <summary>
/// Strategy is actively running
/// </summary>
Running,
/// <summary>
/// Strategy is paused
/// </summary>
Paused,
/// <summary>
/// Strategy is being shut down
/// </summary>
ShuttingDown,
/// <summary>
/// Strategy has been shut down
/// </summary>
Shutdown,
/// <summary>
/// Strategy is in error state
/// </summary>
Error,
/// <summary>
/// Strategy is in maintenance mode
/// </summary>
Maintenance
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Prevents overlapping strike prices that could create margin model conflicts
/// Especially important for Iron Condors and complex multi-leg strategies
/// </summary>
public class StrikeOverlapRule : IPositionOverlapRule
{
private readonly IAlgorithmContext _context;
private readonly object _logger;
private readonly decimal _minimumStrikeDistance = 5m; // Minimum distance between strikes
public string RuleName => "StrikeOverlapRule";
public string Description => "Prevents overlapping strikes that cause margin conflicts";
public bool IsEnabled { get; set; } = true;
public StrikeOverlapRule(IAlgorithmContext context, decimal minimumStrikeDistance = 5m)
{
_context = context;
_logger = context.Logger;
_minimumStrikeDistance = minimumStrikeDistance;
}
public ValidationResult Validate(
Symbol proposedSymbol,
decimal quantity,
IEnumerable<KeyValuePair<Symbol, SecurityHolding>> existingPositions,
string strategyTag = "")
{
if (!IsEnabled)
return ValidationResult.Success();
try
{
// Only validate option positions
if (proposedSymbol.SecurityType != SecurityType.Option)
return ValidationResult.Success();
var underlying = proposedSymbol.Underlying;
var proposedStrike = proposedSymbol.ID.StrikePrice;
var proposedExpiry = proposedSymbol.ID.Date;
var proposedRight = proposedSymbol.ID.OptionRight;
// Get existing option positions on same underlying
var existingOptions = existingPositions
.Where(p => p.Value.Invested &&
p.Key.SecurityType == SecurityType.Option &&
p.Key.Underlying == underlying)
.ToList();
if (!existingOptions.Any())
return ValidationResult.Success();
// Only check for exact duplicates - allow all other combinations
return ValidateExactDuplicates(
proposedStrike,
proposedExpiry,
proposedRight,
quantity,
existingOptions);
}
catch (System.Exception ex)
{
((dynamic)_logger).Error($"[{RuleName}] Error validating strike overlaps: {ex.Message}");
return ValidationResult.Error($"Strike validation error: {ex.Message}");
}
}
/// <summary>
/// Validates for exact duplicate positions only
/// </summary>
private ValidationResult ValidateExactDuplicates(
decimal proposedStrike,
System.DateTime proposedExpiry,
OptionRight proposedRight,
decimal quantity,
List<KeyValuePair<Symbol, SecurityHolding>> existingOptions)
{
// Check for exact strike/expiry/right matches
var exactMatch = existingOptions.FirstOrDefault(p =>
p.Key.ID.StrikePrice == proposedStrike &&
p.Key.ID.Date == proposedExpiry &&
p.Key.ID.OptionRight == proposedRight);
if (exactMatch.Key != null)
{
// Allow position modifications (different quantities)
if (IsPositionModification(quantity, exactMatch.Value.Quantity))
{
((dynamic)_logger).Debug($"[{RuleName}] Allowing position modification for {proposedStrike} {proposedRight} {proposedExpiry:yyyy-MM-dd}");
return ValidationResult.Success();
}
// Block exact duplicates (same quantity, same position)
if (Math.Sign(quantity) == Math.Sign(exactMatch.Value.Quantity))
{
return ValidationResult.Blocked(
$"Exact duplicate position: {proposedStrike} {proposedRight} {proposedExpiry:yyyy-MM-dd} " +
$"already exists with quantity {exactMatch.Value.Quantity}");
}
}
return ValidationResult.Success();
}
/// <summary>
/// Determines if this is a position modification rather than a new position
/// </summary>
private bool IsPositionModification(decimal proposedQuantity, decimal existingQuantity)
{
// Allow all position modifications:
// - Same sign = position increase (e.g., double the position)
// - Different sign = position reduction/close (e.g., sell 50%)
// - Different quantity = any modification is allowed
return Math.Abs(proposedQuantity) != Math.Abs(existingQuantity);
}
}
}using System;
using System.Collections.Generic;
using System.Globalization;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Simple trade record for tracking individual trades through their lifecycle
/// Supports both single-leg and multi-leg (combo) orders
/// </summary>
public class TradeRecord
{
public string OrderId { get; set; } = string.Empty;
public string Symbol { get; set; } = string.Empty;
public string Strategy { get; set; } = string.Empty;
public DateTime OpenTime { get; set; }
public DateTime? CloseTime { get; set; }
public decimal OpenPrice { get; set; }
public decimal? ClosePrice { get; set; }
public int Quantity { get; set; }
public string Status { get; set; } = "Working"; // Working, Open, Closed, Cancelled, PartialFill
public decimal PnL { get; set; }
public string OrderTag { get; set; } = string.Empty;
/// <summary>
/// List of order IDs for multi-leg trades (combo orders)
/// Single-leg trades will have just one entry
/// </summary>
public List<int> OrderIds { get; set; } = new List<int>();
/// <summary>
/// List of SecurityIdentifier strings for all symbols in this trade
/// QC Symbol.ID.ToString() provides a stable, serializable identifier
/// Can be used to reconstruct Symbol via SecurityIdentifier.Parse()
/// </summary>
public List<string> SymbolIds { get; set; } = new List<string>();
/// <summary>
/// Filled quantity (for partial fills tracking)
/// </summary>
public int FilledQuantity { get; set; }
/// <summary>
/// Last update timestamp
/// </summary>
public DateTime LastUpdateUtc { get; set; }
/// <summary>
/// Strategy-specific details bag for dynamic tracking
/// Allows any strategy to store custom data for later use (adjustments, analysis, etc.)
/// </summary>
public Dictionary<string, string> StrategyDetails { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Creates a new trade record
/// </summary>
public TradeRecord(string orderId, string symbol, string strategy = "", string orderTag = "")
{
OrderId = orderId;
Symbol = symbol;
Strategy = strategy;
OrderTag = orderTag;
OpenTime = DateTime.UtcNow;
LastUpdateUtc = DateTime.UtcNow;
// Parse order ID and add to list
if (int.TryParse(orderId, out var parsedOrderId))
{
OrderIds.Add(parsedOrderId);
}
}
/// <summary>
/// Parameterless constructor for serialization
/// </summary>
public TradeRecord()
{
OpenTime = DateTime.UtcNow;
LastUpdateUtc = DateTime.UtcNow;
}
/// <summary>
/// Marks trade as open with fill details
/// </summary>
public void MarkAsOpen(decimal fillPrice, int fillQuantity)
{
MarkAsOpen(fillPrice, fillQuantity, DateTime.UtcNow);
}
/// <summary>
/// Marks trade as open with fill details and explicit UTC time (for backtest time accuracy)
/// </summary>
public void MarkAsOpen(decimal fillPrice, int fillQuantity, DateTime openTimeUtc)
{
Status = "Open";
OpenPrice = fillPrice;
Quantity = fillQuantity;
FilledQuantity = fillQuantity;
OpenTime = openTimeUtc;
LastUpdateUtc = openTimeUtc;
}
/// <summary>
/// Updates partial fill status
/// </summary>
public void MarkAsPartialFill(int filledQuantity, decimal averagePrice)
{
Status = "PartialFill";
FilledQuantity = filledQuantity;
OpenPrice = averagePrice;
LastUpdateUtc = DateTime.UtcNow;
}
/// <summary>
/// Marks trade as closed with close details
/// </summary>
public void MarkAsClosed(decimal closePrice, decimal pnl)
{
Status = "Closed";
CloseTime = DateTime.UtcNow;
ClosePrice = closePrice;
PnL = pnl;
LastUpdateUtc = DateTime.UtcNow;
}
/// <summary>
/// Marks trade as cancelled
/// </summary>
public void MarkAsCancelled()
{
Status = "Cancelled";
LastUpdateUtc = DateTime.UtcNow;
}
/// <summary>
/// Set a string detail
/// </summary>
public void SetDetail(string key, string value)
{
StrategyDetails[key] = value;
LastUpdateUtc = DateTime.UtcNow;
}
/// <summary>
/// Set a decimal detail
/// </summary>
public void SetDetail(string key, decimal value)
{
StrategyDetails[key] = value.ToString(CultureInfo.InvariantCulture);
LastUpdateUtc = DateTime.UtcNow;
}
/// <summary>
/// Set an int detail
/// </summary>
public void SetDetail(string key, int value)
{
StrategyDetails[key] = value.ToString(CultureInfo.InvariantCulture);
LastUpdateUtc = DateTime.UtcNow;
}
/// <summary>
/// Set a bool detail
/// </summary>
public void SetDetail(string key, bool value)
{
StrategyDetails[key] = value ? "true" : "false";
LastUpdateUtc = DateTime.UtcNow;
}
/// <summary>
/// Set a DateTime detail (stored in UTC)
/// </summary>
public void SetDetail(string key, DateTime valueUtc)
{
StrategyDetails[key] = valueUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
LastUpdateUtc = DateTime.UtcNow;
}
/// <summary>
/// Try to get a string detail
/// </summary>
public bool TryGetDetail(string key, out string value) => StrategyDetails.TryGetValue(key, out value);
/// <summary>
/// Try to get a decimal detail
/// </summary>
public bool TryGetDecimal(string key, out decimal value)
{
value = 0m;
return StrategyDetails.TryGetValue(key, out var s)
&& decimal.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out value);
}
/// <summary>
/// Try to get an int detail
/// </summary>
public bool TryGetInt(string key, out int value)
{
value = 0;
return StrategyDetails.TryGetValue(key, out var s)
&& int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
}
/// <summary>
/// Try to get a bool detail
/// </summary>
public bool TryGetBool(string key, out bool value)
{
value = false;
return StrategyDetails.TryGetValue(key, out var s)
&& bool.TryParse(s, out value);
}
/// <summary>
/// Try to get a DateTime detail (returns UTC)
/// </summary>
public bool TryGetDateTime(string key, out DateTime valueUtc)
{
valueUtc = default;
return StrategyDetails.TryGetValue(key, out var s)
&& DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out valueUtc);
}
/// <summary>
/// Returns CSV header
/// </summary>
public static string CsvHeader()
{
return "OrderId,Symbol,Strategy,OrderTag,Status,OpenTime,CloseTime,OpenPrice,ClosePrice,Quantity,PnL";
}
/// <summary>
/// Returns trade data as CSV row
/// </summary>
public string ToCsv()
{
return $"{OrderId},{Symbol},{Strategy},{OrderTag},{Status},{OpenTime:yyyy-MM-dd HH:mm:ss}," +
$"{CloseTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""}," +
$"{OpenPrice},{ClosePrice?.ToString() ?? ""},{Quantity},{PnL}";
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Trade tracking system that maintains lists of trades in different states
/// Similar to the Python setupbasestructure.py arrays for position management
/// Supports both single-leg and multi-leg (combo) orders with ObjectStore persistence
/// </summary>
public class TradeTracker
{
/// <summary>
/// All trades ever created (complete history)
/// </summary>
public List<TradeRecord> AllTrades { get; set; } = new List<TradeRecord>();
/// <summary>
/// Currently open trades (filled and active)
/// </summary>
public List<TradeRecord> OpenTrades { get; set; } = new List<TradeRecord>();
/// <summary>
/// Working trades (submitted but not yet filled)
/// </summary>
public List<TradeRecord> WorkingTrades { get; set; } = new List<TradeRecord>();
/// <summary>
/// Closed trades (completed positions)
/// </summary>
public List<TradeRecord> ClosedTrades { get; set; } = new List<TradeRecord>();
/// <summary>
/// Adds a new working trade
/// </summary>
public void AddWorkingTrade(string orderId, string symbol, string strategy = "", string orderTag = "")
{
var trade = new TradeRecord(orderId, symbol, strategy, orderTag);
AllTrades.Add(trade);
WorkingTrades.Add(trade);
}
/// <summary>
/// Finds a trade by any order ID (primary OrderId or in OrderIds list)
/// Used to resolve parent trade from child order IDs (SL/TP)
/// </summary>
public TradeRecord FindTradeByAnyOrderId(int orderId)
{
// First check primary OrderId
var trade = AllTrades.FirstOrDefault(t => t.OrderId == orderId.ToString());
if (trade != null)
{
return trade;
}
// Then check OrderIds list (for child orders)
return AllTrades.FirstOrDefault(t => t.OrderIds.Contains(orderId));
}
/// <summary>
/// Moves a trade from working to open when it gets filled
/// Only transitions trades that are already in WorkingTrades (no fallback Unknown creation)
/// </summary>
public void MarkTradeAsOpen(string orderId, decimal fillPrice, int fillQuantity, DateTime fillTimeUtc)
{
var trade = WorkingTrades.FirstOrDefault(t => t.OrderId == orderId);
if (trade != null)
{
trade.MarkAsOpen(fillPrice, fillQuantity, fillTimeUtc);
WorkingTrades.Remove(trade);
OpenTrades.Add(trade);
return;
}
// Check if trade exists in AllTrades but not in WorkingTrades (already moved or race condition)
trade = AllTrades.FirstOrDefault(t => t.OrderId == orderId);
if (trade != null && trade.Status != "Open")
{
trade.MarkAsOpen(fillPrice, fillQuantity, fillTimeUtc);
if (!OpenTrades.Contains(trade))
{
OpenTrades.Add(trade);
}
return;
}
// Do NOT create fallback Unknown trade - child order fills should not create new trades
// If trade is not found, it means this is a child order (SL/TP) fill, which should be handled
// by the strategy's OnOrderFilled method, not by creating a new trade record
}
/// <summary>
/// Moves a trade from open to closed when position is closed
/// </summary>
public void MarkTradeAsClosed(string orderId, decimal closePrice, decimal pnl)
{
var trade = OpenTrades.FirstOrDefault(t => t.OrderId == orderId);
if (trade != null)
{
trade.MarkAsClosed(closePrice, pnl);
OpenTrades.Remove(trade);
ClosedTrades.Add(trade);
}
}
/// <summary>
/// Removes a working trade (cancelled order)
/// </summary>
public void CancelWorkingTrade(string orderId)
{
var trade = WorkingTrades.FirstOrDefault(t => t.OrderId == orderId);
if (trade != null)
{
WorkingTrades.Remove(trade);
// Keep in AllTrades for audit trail but mark as cancelled
trade.Status = "Cancelled";
}
}
/// <summary>
/// Exports all trades to QC logs (CSV format in logs instead of file)
/// QuantConnect doesn't allow file operations, so we use logging instead
/// </summary>
public void ExportToLogs(Action<string> logAction)
{
try
{
// Log header
logAction?.Invoke($"=== TRADE EXPORT === {TradeRecord.CsvHeader()}");
// Log all trades
foreach (var trade in AllTrades.OrderBy(t => t.OpenTime))
{
logAction?.Invoke($"TRADE_DATA: {trade.ToCsv()}");
}
logAction?.Invoke("=== END TRADE EXPORT ===");
}
catch (Exception ex)
{
// Fail silently - don't break algorithm execution
logAction?.Invoke($"Failed to export trades to logs: {ex.Message}");
}
}
/// <summary>
/// Gets summary statistics for logging
/// </summary>
public string GetSummary()
{
var totalTrades = AllTrades.Count;
var workingCount = WorkingTrades.Count;
var openCount = OpenTrades.Count;
var closedCount = ClosedTrades.Count;
var totalPnL = ClosedTrades.Sum(t => t.PnL);
return $"Trade Summary: Total={totalTrades}, Working={workingCount}, Open={openCount}, Closed={closedCount}, PnL=${totalPnL:F2}";
}
/// <summary>
/// Clears all trade data (for testing)
/// </summary>
public void Clear()
{
AllTrades.Clear();
WorkingTrades.Clear();
OpenTrades.Clear();
ClosedTrades.Clear();
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Prevents cross-strategy conflicts on the same underlying asset
/// Example: Blocks Iron Condor + Covered Call on SPY simultaneously
/// </summary>
public class UnderlyingConflictRule : IPositionOverlapRule
{
private readonly IAlgorithmContext _context;
private readonly object _logger;
public string RuleName => "UnderlyingConflictRule";
public string Description => "Prevents multiple complex strategies on same underlying";
public bool IsEnabled { get; set; } = true;
public UnderlyingConflictRule(IAlgorithmContext context)
{
_context = context;
_logger = context.Logger;
}
public ValidationResult Validate(
Symbol proposedSymbol,
decimal quantity,
IEnumerable<KeyValuePair<Symbol, SecurityHolding>> existingPositions,
string strategyTag = "")
{
if (!IsEnabled)
return ValidationResult.Success();
try
{
// Determine the underlying asset for the proposed position
var proposedUnderlying = GetUnderlying(proposedSymbol);
if (proposedUnderlying == null)
return ValidationResult.Success();
// Check for existing positions on the same underlying
var conflictingPositions = existingPositions
.Where(p => p.Value.Invested)
.Where(p => GetUnderlying(p.Key) == proposedUnderlying)
.ToList();
if (!conflictingPositions.Any())
return ValidationResult.Success();
// Count different types of positions on this underlying
var optionPositions = conflictingPositions.Count(p => p.Key.SecurityType == SecurityType.Option);
var stockPositions = conflictingPositions.Count(p => p.Key.SecurityType == SecurityType.Equity);
// Only check for truly conflicting positions (e.g., opposite directions on same asset)
// Allow multiple strategies on same underlying - they can complement each other
// For now, allow all combinations - let individual strategies manage their own limits
// This rule focuses on preventing true conflicts, not limiting strategy diversity
((dynamic)_logger).Debug($"[{RuleName}] Allowed: {proposedUnderlying.Value} has {optionPositions} options, {stockPositions} stock positions");
return ValidationResult.Success();
}
catch (System.Exception ex)
{
((dynamic)_logger).Error($"[{RuleName}] Error validating underlying conflict: {ex.Message}");
return ValidationResult.Error($"Validation error: {ex.Message}");
}
}
/// <summary>
/// Gets the underlying symbol for any security type
/// </summary>
private Symbol GetUnderlying(Symbol symbol)
{
if (symbol.SecurityType == SecurityType.Option)
return symbol.Underlying;
if (symbol.SecurityType == SecurityType.Equity)
return symbol;
return null; // Unsupported security type
}
}
}#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Selection;
using QuantConnect.Api;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Commands;
using QuantConnect.Configuration;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.Shortable;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.OptionExercise;
using QuantConnect.Orders.Slippage;
using QuantConnect.Orders.TimeInForces;
using QuantConnect.Python;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Positions;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using Calendar = QuantConnect.Data.Consolidators.Calendar;
#endregion
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Represents the result of a validation operation
/// </summary>
public class ValidationResult
{
/// <summary>
/// Gets whether the validation passed
/// </summary>
public bool IsValid { get; private set; }
/// <summary>
/// Gets the validation errors
/// </summary>
public List<ValidationError> Errors { get; private set; }
/// <summary>
/// Gets the validation warnings
/// </summary>
public List<ValidationWarning> Warnings { get; private set; }
/// <summary>
/// Gets the first error or warning message
/// </summary>
public string Message
{
get
{
if (Errors?.Any() == true)
return Errors.First().Message;
if (Warnings?.Any() == true)
return Warnings.First().Message;
return string.Empty;
}
}
/// <summary>
/// Creates a successful validation result
/// </summary>
public static ValidationResult Success()
{
return new ValidationResult
{
IsValid = true,
Errors = new List<ValidationError>(),
Warnings = new List<ValidationWarning>()
};
}
/// <summary>
/// Creates a failed validation result with errors
/// </summary>
public static ValidationResult Failure(params ValidationError[] errors)
{
return new ValidationResult
{
IsValid = false,
Errors = new List<ValidationError>(errors),
Warnings = new List<ValidationWarning>()
};
}
/// <summary>
/// Creates a blocked validation result with message
/// </summary>
public static ValidationResult Blocked(string message)
{
var result = new ValidationResult
{
IsValid = false,
Errors = new List<ValidationError>(),
Warnings = new List<ValidationWarning>()
};
result.AddError("BLOCKED", message);
return result;
}
/// <summary>
/// Creates an error validation result with message
/// </summary>
public static ValidationResult Error(string message)
{
var result = new ValidationResult
{
IsValid = false,
Errors = new List<ValidationError>(),
Warnings = new List<ValidationWarning>()
};
result.AddError("ERROR", message);
return result;
}
/// <summary>
/// Creates a warning validation result with message
/// </summary>
public static ValidationResult Warning(string message)
{
var result = new ValidationResult
{
IsValid = true,
Errors = new List<ValidationError>(),
Warnings = new List<ValidationWarning>()
};
result.AddWarning("WARNING", message);
return result;
}
/// <summary>
/// Adds an error to the validation result
/// </summary>
public void AddError(string code, string message, string field = null)
{
IsValid = false;
Errors.Add(new ValidationError { Code = code, Message = message, Field = field });
}
/// <summary>
/// Adds a warning to the validation result
/// </summary>
public void AddWarning(string code, string message, string field = null)
{
Warnings.Add(new ValidationWarning { Code = code, Message = message, Field = field });
}
/// <summary>
/// Merges another validation result into this one
/// </summary>
public void Merge(ValidationResult other)
{
if (!other.IsValid)
{
IsValid = false;
}
Errors.AddRange(other.Errors);
Warnings.AddRange(other.Warnings);
}
/// <summary>
/// Creates a new instance of ValidationResult
/// </summary>
public ValidationResult()
{
Errors = new List<ValidationError>();
Warnings = new List<ValidationWarning>();
IsValid = true;
}
}
/// <summary>
/// Represents a validation error
/// </summary>
public class ValidationError
{
/// <summary>
/// Gets or sets the error code
/// </summary>
public string Code { get; set; }
/// <summary>
/// Gets or sets the error message
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets the field that caused the error
/// </summary>
public string Field { get; set; }
}
/// <summary>
/// Represents a validation warning
/// </summary>
public class ValidationWarning
{
/// <summary>
/// Gets or sets the warning code
/// </summary>
public string Code { get; set; }
/// <summary>
/// Gets or sets the warning message
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets the field that caused the warning
/// </summary>
public string Field { get; set; }
}
}using QuantConnect.Algorithm;
namespace CoreAlgo.Architecture.Core.Interfaces
{
/// <summary>
/// Context interface that provides access to algorithm instance and centralized logger
/// Implements the context pattern like CentralAlgorithm where classes access context.logger
/// </summary>
public interface IAlgorithmContext
{
/// <summary>
/// The QuantConnect algorithm instance - provides access to all QC functionality
/// </summary>
QCAlgorithm Algorithm { get; }
/// <summary>
/// Centralized smart logger with level checking and advanced features
/// Access via context.Logger.Debug(), context.Logger.Info(), etc.
/// Uses object type to avoid circular dependency with Main.cs CoreAlgo class
/// </summary>
object Logger { get; }
}
}#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Selection;
using QuantConnect.Api;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Commands;
using QuantConnect.Configuration;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.Shortable;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.OptionExercise;
using QuantConnect.Orders.Slippage;
using QuantConnect.Orders.TimeInForces;
using QuantConnect.Python;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Positions;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using Calendar = QuantConnect.Data.Consolidators.Calendar;
using CoreAlgo.Architecture.Core.Implementations;
#endregion
namespace CoreAlgo.Architecture.Core.Interfaces
{
/// <summary>
/// Simple logger interface for the core infrastructure
/// </summary>
public interface ILogger<T>
{
/// <summary>
/// Logs an informational message
/// </summary>
void LogInformation(string message, params object[] args);
/// <summary>
/// Logs a debug message
/// </summary>
void LogDebug(string message, params object[] args);
/// <summary>
/// Logs a warning message
/// </summary>
void LogWarning(string message, params object[] args);
/// <summary>
/// Logs an error message
/// </summary>
void LogError(string message, params object[] args);
/// <summary>
/// Logs an error message with exception
/// </summary>
void LogError(Exception exception, string message, params object[] args);
/// <summary>
/// Logs a message with explicit context information for better traceability
/// </summary>
void LogWithContext(string message, string className, string methodName, LogLevel level = LogLevel.Info);
/// <summary>
/// Central logging method with level checking
/// </summary>
void LogMessage(string message, LogLevel level = LogLevel.Debug);
}
}using System.Collections.Generic;
using QuantConnect;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Implementations;
namespace CoreAlgo.Architecture.Core.Interfaces
{
/// <summary>
/// Interface for position overlap validation rules
/// Enables extensible rule system for different overlap scenarios
/// </summary>
public interface IPositionOverlapRule
{
/// <summary>
/// Validates whether a new position can be opened without creating dangerous overlaps
/// </summary>
/// <param name="proposedSymbol">Symbol for the new position</param>
/// <param name="quantity">Proposed quantity (positive for long, negative for short)</param>
/// <param name="existingPositions">Current portfolio positions from QC's Portfolio API</param>
/// <param name="strategyTag">Strategy identifier for context</param>
/// <returns>ValidationResult indicating if position is allowed</returns>
ValidationResult Validate(
Symbol proposedSymbol,
decimal quantity,
IEnumerable<KeyValuePair<Symbol, SecurityHolding>> existingPositions,
string strategyTag = "");
/// <summary>
/// Gets the name of this rule for logging and configuration
/// </summary>
string RuleName { get; }
/// <summary>
/// Gets description of what this rule validates
/// </summary>
string Description { get; }
/// <summary>
/// Whether this rule is enabled (allows dynamic rule toggling)
/// </summary>
bool IsEnabled { get; set; }
}
}#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Selection;
using QuantConnect.Api;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Commands;
using QuantConnect.Configuration;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.Shortable;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.OptionExercise;
using QuantConnect.Orders.Slippage;
using QuantConnect.Orders.TimeInForces;
using QuantConnect.Python;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Positions;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using Calendar = QuantConnect.Data.Consolidators.Calendar;
using CoreAlgo.Architecture.Core;
using CoreAlgo.Architecture.Core.Implementations;
#endregion
namespace CoreAlgo.Architecture.Core.Interfaces
{
/// <summary>
/// Base interface for all trading strategies
/// </summary>
public interface IStrategy
{
/// <summary>
/// Gets the unique name of the strategy
/// </summary>
string Name { get; }
/// <summary>
/// Gets the description of the strategy
/// </summary>
string Description { get; }
/// <summary>
/// Gets the version of the strategy
/// </summary>
string Version { get; }
/// <summary>
/// Gets the current state of the strategy
/// </summary>
StrategyState State { get; }
/// <summary>
/// Gets the parameters for this strategy
/// </summary>
Dictionary<string, object> Parameters { get; }
/// <summary>
/// Initializes the strategy with the given algorithm instance
/// </summary>
/// <param name="algorithm">The QCAlgorithm instance</param>
void Initialize(QCAlgorithm algorithm);
/// <summary>
/// Initializes the strategy with parameters
/// </summary>
/// <param name="algorithm">The QCAlgorithm instance</param>
/// <param name="parameters">Strategy-specific parameters</param>
void Initialize(QCAlgorithm algorithm, Dictionary<string, object> parameters);
/// <summary>
/// Executes the strategy logic for the given data slice
/// </summary>
/// <param name="slice">The current data slice</param>
void Execute(Slice slice);
/// <summary>
/// Called when the strategy is being shut down
/// </summary>
void Shutdown();
/// <summary>
/// Validates the strategy configuration
/// </summary>
/// <returns>True if the strategy is valid</returns>
bool Validate();
/// <summary>
/// Gets performance metrics for the strategy
/// </summary>
/// <returns>Dictionary of performance metrics</returns>
Dictionary<string, double> GetPerformanceMetrics();
/// <summary>
/// Resets the strategy to its initial state
/// </summary>
void Reset();
/// <summary>
/// Event raised when the strategy state changes
/// </summary>
event EventHandler<StrategyStateChangedEventArgs> StateChanged;
/// <summary>
/// Event raised when the strategy encounters an error
/// </summary>
event EventHandler<StrategyErrorEventArgs> ErrorOccurred;
}
}using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using QuantConnect;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Configuration for Adaptive Scalper strategy
/// Micro-scalps equities using ATR-driven adaptive stops/targets and spread quality gates
/// </summary>
public class AdaptiveScalperConfig : StrategyConfig
{
/// <summary>
/// Default constructor - sets AMD as default underlying symbol for scalping
/// </summary>
public AdaptiveScalperConfig()
{
UnderlyingSymbol = "AMD";
Symbols = new[] { "AMD" };
StartDate = new DateTime(2025, 8, 1);
EndDate = new DateTime(2025, 9, 1);
TradingStartTime = new TimeSpan(9, 0, 0); // 09:00 AM
TradingEndTime = new TimeSpan(15, 0, 0); // 15:00 PM
MaxPositions = 0; // Disable base MaxPositions check - use MaxConcurrentTrades instead
EnableOverlapPrevention = false; // Allow multiple entries for same underlying
ProfitTarget = 0;
StopLoss = 0;
AccountSize = 1000000;
DebugMode = false;
LogEntryRestrictions = false;
UnderlyingResolution = "Minute";
}
// ============================================================================
// INDICATOR PARAMETERS
// ============================================================================
[StrategyParameter("AtrPeriod", 14)]
[Description("ATR indicator lookback period (default: 14)")]
public int AtrPeriod { get; set; } = 14;
[StrategyParameter("AtrResolution", "Minute")]
[Description("Resolution for ATR updates: Tick, Second, Minute (default: Minute)")]
public string AtrResolution { get; set; } = "Minute";
[StrategyParameter("VolatilityBaselineWindow", 50)]
[Description("Rolling window size for ATR baseline calculation (default: 50)")]
public int VolatilityBaselineWindow { get; set; } = 50;
// ============================================================================
// SPREAD QUALITY GATES
// ============================================================================
[StrategyParameter("MinSpread", 0.02)]
[Description("Minimum acceptable spread in dollars (default: $0.02)")]
public decimal MinSpread { get; set; } = 0.02m;
[StrategyParameter("MaxSpread", 0.08)]
[Description("Maximum acceptable spread in dollars (default: $0.08)")]
public decimal MaxSpread { get; set; } = 0.08m;
// ============================================================================
// RISK, TARGETS, AND POSITION SIZING
// ============================================================================
[StrategyParameter("TargetDollarRisk", 120)]
[Description("Target dollar risk per trade for position sizing (default: $120)")]
public decimal TargetDollarRisk { get; set; } = 120m;
[StrategyParameter("MinStopLoss", 0.015)]
[Description("Minimum stop loss in dollars (default: $0.015)")]
public decimal MinStopLoss { get; set; } = 0.015m;
[StrategyParameter("StopLossAtrMultiple", 0.5)]
[Description("Stop loss as multiple of ATR (default: 0.5x ATR)")]
public decimal StopLossAtrMultiple { get; set; } = 0.5m;
[StrategyParameter("TakeProfitAtrMultiple", 0.4)]
[Description("Take profit as multiple of ATR (default: 0.4x ATR)")]
public decimal TakeProfitAtrMultiple { get; set; } = 0.4m;
[StrategyParameter("TrailingStartFraction", 0.5)]
[Description("Fraction of take profit at which trailing stop activates (default: 0.5 = 50%)")]
public decimal TrailingStartFraction { get; set; } = 0.5m;
[StrategyParameter("DebugMode", false)]
[Description("Enable verbose diagnostic logging for AdaptiveScalper (default: false)")]
public bool DebugMode { get; set; }
[StrategyParameter("LogEntryRestrictions", false)]
[Description("Log detailed entry restriction checks (default: false)")]
public override bool LogEntryRestrictions { get; set; }
[StrategyParameter("EnableTradeTrackingLogs", true)]
[Description("Enable detailed trade tracking logs for debugging position management (default: true)")]
public bool EnableTradeTrackingLogs { get; set; } = true;
[StrategyParameter("TradeTrackingLogFrequencySeconds", 60)]
[Description("Minimum seconds between trade tracking log dumps per trade (default: 60)")]
public int TradeTrackingLogFrequencySeconds { get; set; } = 60;
[StrategyParameter("LogQuotesInTracking", true)]
[Description("Include bid/ask quotes in trade tracking logs (default: true)")]
public bool LogQuotesInTracking { get; set; } = true;
[StrategyParameter("OHVolatilityMultiplier", 2.0)]
[Description("Multiplier for stop/target during opening hour (9:30-10:30) to account for higher volatility (default: 2.0)")]
public decimal OHVolatilityMultiplier { get; set; } = 2.0m;
// ============================================================================
// THROTTLE AND KILL SWITCH
// ============================================================================
[StrategyParameter("DailyKillSwitch", -50000)]
[Description("Daily loss limit - halt trading when reached (default: -$50,000)")]
public decimal DailyKillSwitch { get; set; } = -50000m;
[StrategyParameter("ThrottleDecay", 0.25)]
[Description("Rate at which throttle reduces position size as losses build (default: 0.25)")]
public decimal ThrottleDecay { get; set; } = 0.25m;
[StrategyParameter("MinThrottle", 0.10)]
[Description("Minimum throttle level - trading halts below this (default: 0.10 = 10%)")]
public decimal MinThrottle { get; set; } = 0.10m;
// ============================================================================
// HFT MULTI-TRADE PARAMETERS
// ============================================================================
[StrategyParameter("MaxConcurrentTrades", 50)]
[Description("Maximum number of concurrent independent trades (default: 50)")]
public int MaxConcurrentTrades { get; set; } = 50;
[StrategyParameter("MaxEntriesPerDay", 0)]
[Description("Maximum number of entry orders per day (default: 0)")]
public int MaxEntriesPerDay { get; set; } = 0;
[StrategyParameter("EntryCooldownMs", 180)]
[Description("Minimum milliseconds between entry orders to prevent bursts (default: 150ms)")]
public int EntryCooldownMs { get; set; } = 180;
[StrategyParameter("MaxOrdersPerMinute", 300)]
[Description("Maximum orders per minute rate limit (default: 300)")]
public int MaxOrdersPerMinute { get; set; } = 300;
[StrategyParameter("RequireQuotesForEntry", false)]
[Description("If true, require valid bid/ask quotes before entering trades (default: false - use fallback spread=0 for Minute bars)")]
public bool RequireQuotesForEntry { get; set; } = true;
[StrategyParameter("EntryLimitOffsetTicks", 0)]
[Description("Offset in ticks from mid-price for entry limit orders (default: 0 = at mid)")]
public int EntryLimitOffsetTicks { get; set; } = 0;
[StrategyParameter("StopLimitBufferTicks", 2)]
[Description("Buffer in ticks between stop price and limit price for stop-limit orders (default: 2)")]
public int StopLimitBufferTicks { get; set; } = 2;
[StrategyParameter("StopLimitChaseStartMinutes", 1)]
[Description("Minutes after stop trigger to start chasing limit price (default: 1, 0 = immediate)")]
public int StopLimitChaseStartMinutes { get; set; } = 1;
[StrategyParameter("StopLimitChaseBufferTicks", 3)]
[Description("Tick buffer from best quote when chasing stop-limit limit price (default: 3)")]
public int StopLimitChaseBufferTicks { get; set; } = 3;
[StrategyParameter("MinQuantity", 300)]
[Description("Minimum order quantity (default: 300)")]
public int MinQuantity { get; set; } = 300;
[StrategyParameter("MaxQuantity", 500)]
[Description("Maximum order quantity (default: 500)")]
public int MaxQuantity { get; set; } = 500;
[StrategyParameter("PauseTrading", false)]
[Description("When true, stops new entries and cancels all outstanding orders for manual liquidation (default: false)")]
public bool PauseTrading { get; set; } = false;
// ============================================================================
// EXIT SAFEGUARDS
// ============================================================================
[StrategyParameter("FlattenBeforeCloseMinutes", 15)]
[Description("Minutes before market close to force-market-close open positions (default: 15, 0 disables)")]
public int FlattenBeforeCloseMinutes { get; set; } = 15;
[StrategyParameter("MaxHoldingMinutes", 0)]
[Description("Maximum minutes a trade may remain open before force-closing (default: 0 = disabled)")]
public int MaxHoldingMinutes { get; set; } = 0;
// ============================================================================
// DATA RESOLUTION
// ============================================================================
/// <summary>
/// Get the Resolution enum value for ATR updates
/// </summary>
public Resolution GetAtrResolution()
{
return AtrResolution?.ToUpperInvariant() switch
{
"TICK" => Resolution.Tick,
"SECOND" => Resolution.Second,
"MINUTE" => Resolution.Minute,
"HOUR" => Resolution.Hour,
"DAILY" => Resolution.Daily,
_ => Resolution.Minute // Default to Minute
};
}
/// <summary>
/// Key parameters for AdaptiveScalper optimization
/// </summary>
public override string[] OptimizationParameters => base.OptimizationParameters.Concat(new[]
{
"AtrPeriod",
"VolatilityBaselineWindow",
"MinSpread",
"MaxSpread",
"TargetDollarRisk",
"MinStopLoss",
"StopLossAtrMultiple",
"TakeProfitAtrMultiple",
"TrailingStartFraction",
"DailyKillSwitch",
"ThrottleDecay",
"OHVolatilityMultiplier",
"MaxConcurrentTrades",
"MaxEntriesPerDay",
"EntryCooldownMs",
"MaxOrdersPerMinute",
"RequireQuotesForEntry",
"EntryLimitOffsetTicks",
"StopLimitBufferTicks",
"StopLimitChaseStartMinutes",
"StopLimitChaseBufferTicks",
"MinQuantity",
"MaxQuantity",
"PauseTrading",
"FlattenBeforeCloseMinutes",
"MaxHoldingMinutes"
}).ToArray();
/// <summary>
/// Override base validation to allow MaxPositions = 0 (we use MaxConcurrentTrades instead)
/// </summary>
public override string[] Validate()
{
var errors = new List<string>();
// Skip base MaxPositions validation - we use MaxConcurrentTrades instead
// Validate our own concurrency parameter
if (MaxConcurrentTrades <= 0)
errors.Add($"MaxConcurrentTrades must be greater than 0 (current: {MaxConcurrentTrades})");
// Entry window validation
if (UseEntryTimeWindow)
{
if (EntryWindowStart > EntryWindowEnd)
{
errors.Add($"EntryWindowStart {EntryWindowStart:hh\\:mm} must be <= EntryWindowEnd {EntryWindowEnd:hh\\:mm}");
}
}
// HFT-specific validations
if (EntryCooldownMs < 0)
errors.Add($"EntryCooldownMs must be >= 0 (current: {EntryCooldownMs})");
if (MaxOrdersPerMinute <= 0)
errors.Add($"MaxOrdersPerMinute must be greater than 0 (current: {MaxOrdersPerMinute})");
// Quantity bounds validation
if (MinQuantity < 1)
errors.Add($"MinQuantity must be >= 1 (current: {MinQuantity})");
if (MaxQuantity < MinQuantity)
errors.Add($"MaxQuantity must be >= MinQuantity (current: MaxQuantity={MaxQuantity}, MinQuantity={MinQuantity})");
// Exit safeguard validations
if (FlattenBeforeCloseMinutes < 0)
errors.Add($"FlattenBeforeCloseMinutes must be >= 0 (current: {FlattenBeforeCloseMinutes})");
if (MaxHoldingMinutes < 0)
errors.Add($"MaxHoldingMinutes must be >= 0 (current: {MaxHoldingMinutes})");
return errors.ToArray();
}
public override string ToString()
{
var eodStatus = FlattenBeforeCloseMinutes > 0 ? $"EOD:{FlattenBeforeCloseMinutes}m" : "EOD:OFF";
var timeoutStatus = MaxHoldingMinutes > 0 ? $"MaxHold:{MaxHoldingMinutes}m" : "MaxHold:OFF";
return $"AdaptiveScalper[{UnderlyingSymbol}] ATR:{AtrPeriod} Spread:{MinSpread:F3}-{MaxSpread:F3} " +
$"Risk:${TargetDollarRisk} SL:{StopLossAtrMultiple}xATR TP:{TakeProfitAtrMultiple}xATR " +
$"Trail@{TrailingStartFraction:P0} Kill:${DailyKillSwitch} MaxConcurrent:{MaxConcurrentTrades} " +
$"EntryOffset:{EntryLimitOffsetTicks}t SLBuffer:{StopLimitBufferTicks}t Chase:{StopLimitChaseStartMinutes}m/{StopLimitChaseBufferTicks}t Qty:{MinQuantity}-{MaxQuantity} " +
$"{eodStatus} {timeoutStatus} Pause:{PauseTrading} Resolution:{UnderlyingResolution}";
}
}
}
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Basic strategy configuration for simple strategies like Iron Condor, Covered Call, etc.
/// Provides concrete implementation of StrategyConfig for strategies that don't need special configuration.
/// </summary>
public class BasicStrategyConfig : StrategyConfig
{
// This class inherits all [StrategyParameter] attributes from StrategyConfig
// and provides a concrete implementation that can be instantiated
// No additional properties needed - base class has all the standard parameters:
// - Symbols, UnderlyingSymbol
// - EntryDeltaMin/Max, ExitDelta
// - AllocationPerPosition, MaxPositions
// - TradingStartTime/EndTime
// - ProfitTarget, StopLoss, MaxDaysInTrade
// - MinImpliedVolatility
// - UseUniverseSelection
/// <summary>
/// Basic strategies use the core optimization parameters from base class
/// No strategy-specific parameters need to be added for optimization
/// </summary>
public override string[] OptimizationParameters => base.OptimizationParameters;
}
}using System;
using System.Linq;
using QuantConnect;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Base configuration class for futures trading strategies.
/// Provides common parameters for futures contract management, rollover handling,
/// and continuous contract settings.
/// </summary>
public abstract class FuturesConfig : StrategyConfig
{
/// <summary>
/// The futures symbol to trade (e.g., "ES", "GC", "CL")
/// </summary>
[StrategyParameter("Futures symbol", "ES")]
public string FutureSymbol { get; set; } = "ES";
/// <summary>
/// Number of days before contract expiration to initiate rollover
/// Only relevant for manual contract management (not continuous contracts)
/// </summary>
[StrategyParameter("Rollover days to expiry", 5)]
public int ContractRolloverDays { get; set; } = 5;
/// <summary>
/// Whether to use continuous contracts (recommended for backtesting)
/// When true, QuantConnect automatically handles contract rollovers
/// </summary>
[StrategyParameter("Use continuous contracts", true)]
public bool UseContinuousContract { get; set; } = true;
/// <summary>
/// Contract depth offset (0 = front month, 1 = next month, etc.)
/// Used when adding the futures contract
/// </summary>
[StrategyParameter("Contract depth offset", 0)]
public int ContractDepthOffset { get; set; } = 0;
/// <summary>
/// Position sizing multiplier relative to account equity
/// For futures, this is combined with contract multiplier
/// </summary>
[StrategyParameter("Position size multiplier", 1.0)]
public decimal PositionSizeMultiplier { get; set; } = 1.0m;
/// <summary>
/// Maximum number of contracts to trade (risk management)
/// </summary>
[StrategyParameter("Maximum contracts per trade", 5)]
public int MaxContractsPerTrade { get; set; } = 5;
/// <summary>
/// Data resolution for futures data subscription
/// </summary>
[StrategyParameter("Data resolution", "Minute")]
public string DataResolution { get; set; } = "Minute";
/// <summary>
/// Parse the data resolution string to Resolution enum
/// </summary>
/// <returns>Resolution enum value</returns>
public Resolution GetDataResolution()
{
if (Enum.TryParse<Resolution>(DataResolution, true, out var resolution))
{
return resolution;
}
return Resolution.Minute; // Default fallback
}
/// <summary>
/// Get the futures category based on the symbol
/// Useful for strategy-specific logic
/// </summary>
/// <returns>Futures category string</returns>
public string GetFuturesCategory()
{
var symbol = FutureSymbol?.ToUpperInvariant();
// Equity Index Futures
if (new[] { "ES", "NQ", "YM", "RTY", "EMD", "NKD" }.Contains(symbol))
return "equity";
// Energy Futures
if (new[] { "CL", "NG", "RB", "HO", "BZ" }.Contains(symbol))
return "energy";
// Metal Futures
if (new[] { "GC", "SI", "HG", "PA", "PL" }.Contains(symbol))
return "metals";
// Agricultural Futures
if (new[] { "ZC", "ZS", "ZW", "ZM", "ZL", "KC", "CT", "SB", "CC", "OJ" }.Contains(symbol))
return "agricultural";
// Bond/Interest Rate Futures
if (new[] { "ZB", "ZN", "ZF", "TU", "UB", "ED", "SR1", "SR3" }.Contains(symbol))
return "bonds";
// Currency Futures
if (new[] { "6E", "6J", "6B", "6S", "6C", "6A", "6N", "6M", "E7", "J7" }.Contains(symbol))
return "currency";
// Volatility Futures
if (new[] { "VX" }.Contains(symbol))
return "volatility";
// Crypto Futures
if (new[] { "BTC", "ETH" }.Contains(symbol))
return "crypto";
return "unknown";
}
/// <summary>
/// Optimization parameters for futures trading strategies.
/// Focuses on core futures parameters and contract management
/// </summary>
public override string[] OptimizationParameters => base.OptimizationParameters.Concat(new[]
{
"FutureSymbol", // Asset selection for futures
"PositionSizeMultiplier", // Position sizing relative to equity
"MaxContractsPerTrade", // Risk management - contract limit
"ContractRolloverDays", // Contract management timing
"UseContinuousContract", // Contract type selection
"ContractDepthOffset", // Contract selection depth
"DataResolution" // Data granularity for strategy
}).ToArray();
}
}using System;
using System.Linq;
using QuantConnect.Algorithm;
using CoreAlgo.Architecture.Core.Attributes;
using CoreAlgo.Architecture.QC.Helpers;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Enhanced strategy configuration for multi-asset options trading.
/// Extends StrategyConfig with asset-specific parameter adaptation and validation.
/// </summary>
public class MultiAssetConfig : StrategyConfig
{
/// <summary>
/// Enable automatic asset-specific parameter adaptation
/// When true, delta ranges, position limits, and strike widths are automatically
/// adjusted based on the underlying asset characteristics
/// </summary>
[StrategyParameter("EnableAssetAdaptation", true)]
public bool EnableAssetAdaptation { get; set; } = true;
/// <summary>
/// Maximum number of different assets to trade simultaneously
/// Helps control correlation risk when trading multiple underlyings
/// </summary>
[StrategyParameter("MaxAssets", 3)]
public int MaxAssets { get; set; } = 3;
/// <summary>
/// Strike width multiplier for option selection
/// Base value that gets adjusted per asset (e.g., SPX gets wider, AAPL gets narrower)
/// </summary>
[StrategyParameter("BaseStrikeWidth", 0.05)]
public decimal BaseStrikeWidth { get; set; } = 0.05m; // 5% base strike width
/// <summary>
/// Whether to use equal allocation across all assets or asset-specific allocation
/// </summary>
[StrategyParameter("UseEqualAllocation", false)]
public bool UseEqualAllocation { get; set; } = false;
/// <summary>
/// Maximum correlation threshold between assets (0.0 to 1.0)
/// Prevents trading highly correlated assets simultaneously
/// </summary>
[StrategyParameter("MaxCorrelation", 0.7)]
public decimal MaxCorrelation { get; set; } = 0.7m;
/// <summary>
/// Minimum asset price for inclusion (helps filter penny stocks)
/// </summary>
[StrategyParameter("MinAssetPrice", 10.0)]
public decimal MinAssetPrice { get; set; } = 10.0m;
/// <summary>
/// Load parameters and apply asset-specific adaptations
/// </summary>
public override void LoadFromParameters(IAlgorithmContext context)
{
// Load base parameters first
base.LoadFromParameters(context);
// Apply asset-specific adaptations if enabled
if (EnableAssetAdaptation && Symbols != null && Symbols.Length > 0)
{
ApplyAssetAdaptations(context);
}
// Validate multi-asset specific constraints
ValidateMultiAssetConstraints(context);
}
/// <summary>
/// Apply asset-specific parameter adaptations based on the selected symbols
/// </summary>
private void ApplyAssetAdaptations(IAlgorithmContext context)
{
((dynamic)context.Logger).Info("MultiAssetConfig: Applying asset-specific adaptations");
// For multi-asset strategies, we'll adapt based on the first (primary) symbol
// or calculate weighted averages for portfolio-level parameters
var primarySymbol = Symbols[0];
// Get asset-specific delta targets
var (adaptedDeltaMin, adaptedDeltaMax) = MultiAssetHelper.GetAssetDeltaTargets(
primarySymbol, EntryDeltaMin, EntryDeltaMax);
EntryDeltaMin = adaptedDeltaMin;
EntryDeltaMax = adaptedDeltaMax;
// Get asset-specific position limits
var (minPositions, maxPositions, recommendedAllocation) = MultiAssetHelper.GetAssetPositionLimits(
primarySymbol, StartingCash);
// Update max positions if not using equal allocation
if (!UseEqualAllocation)
{
MaxPositions = Math.Min(MaxPositions, maxPositions);
AllocationPerPosition = recommendedAllocation;
}
((dynamic)context.Logger).Info($"MultiAssetConfig: Adapted for {primarySymbol} - " +
$"Delta: {EntryDeltaMin:F2}-{EntryDeltaMax:F2}, " +
$"MaxPos: {MaxPositions}, " +
$"Allocation: {AllocationPerPosition:P1}");
// Log asset profiles for all symbols
foreach (var symbol in Symbols)
{
var profile = MultiAssetHelper.GetAssetProfile(symbol);
if (profile != null)
{
((dynamic)context.Logger).Info($"MultiAssetConfig: {symbol} profile - {profile}");
}
else
{
((dynamic)context.Logger).Info($"MultiAssetConfig: {symbol} - using default profile (unknown asset)");
}
}
}
/// <summary>
/// Validate multi-asset specific constraints
/// </summary>
private void ValidateMultiAssetConstraints(IAlgorithmContext context)
{
// Check asset count limit
if (Symbols.Length > MaxAssets)
{
((dynamic)context.Logger).Error($"MultiAssetConfig: Too many assets ({Symbols.Length}), maximum allowed is {MaxAssets}");
}
// Check for valid options support
var unsupportedSymbols = Symbols.Where(symbol => !MultiAssetHelper.HasLiquidOptions(symbol)).ToList();
if (unsupportedSymbols.Any())
{
((dynamic)context.Logger).Warning($"MultiAssetConfig: Warning - symbols may have limited options liquidity: {string.Join(", ", unsupportedSymbols)}");
}
// Log configuration summary
var summaryMessages = new[]
{
$"MultiAssetConfig: Trading {Symbols.Length} assets: {string.Join(", ", Symbols)}",
$"MultiAssetConfig: Asset adaptation: {(EnableAssetAdaptation ? "Enabled" : "Disabled")}",
$"MultiAssetConfig: Equal allocation: {(UseEqualAllocation ? "Enabled" : "Disabled")}"
};
foreach (var message in summaryMessages)
{
((dynamic)context.Logger).Info(message);
}
}
/// <summary>
/// Get asset-specific strike width for a symbol
/// </summary>
public decimal GetStrikeWidthForAsset(string symbol)
{
return MultiAssetHelper.GetAssetStrikeWidth(symbol, BaseStrikeWidth);
}
/// <summary>
/// Get position limits for a specific asset
/// </summary>
public (int MinPositions, int MaxPositions, decimal RecommendedAllocation) GetPositionLimitsForAsset(string symbol)
{
return MultiAssetHelper.GetAssetPositionLimits(symbol, StartingCash);
}
/// <summary>
/// Get delta targets for a specific asset
/// </summary>
public (decimal DeltaMin, decimal DeltaMax) GetDeltaTargetsForAsset(string symbol)
{
return MultiAssetHelper.GetAssetDeltaTargets(symbol, EntryDeltaMin, EntryDeltaMax);
}
/// <summary>
/// Validate the multi-asset configuration
/// </summary>
public override string[] Validate()
{
var errors = base.Validate().ToList();
// Multi-asset specific validations
if (Symbols == null || Symbols.Length == 0)
{
errors.Add("Symbols array cannot be null or empty");
}
if (MaxAssets < 1)
{
errors.Add("MaxAssets must be at least 1");
}
if (BaseStrikeWidth <= 0)
{
errors.Add("BaseStrikeWidth must be positive");
}
if (MaxCorrelation < 0 || MaxCorrelation > 1)
{
errors.Add("MaxCorrelation must be between 0.0 and 1.0");
}
if (MinAssetPrice <= 0)
{
errors.Add("MinAssetPrice must be positive");
}
// Asset-specific validation against account size and margin requirements
if (Symbols != null && Symbols.Length > 0)
{
foreach (var symbol in Symbols)
{
var profile = MultiAssetHelper.GetAssetProfile(symbol);
if (profile != null)
{
// Check if account size meets minimum requirements for this asset
if (AccountSize < profile.MinAccountSize)
{
errors.Add($"Account size ${AccountSize:F0} below minimum ${profile.MinAccountSize:F0} required for {symbol}");
}
// Check if allocation is reasonable for this asset type
if (AssetManager.IsIndex(symbol) && AllocationPerPosition > 0.1m)
{
errors.Add($"Allocation {AllocationPerPosition:P0} too high for index option {symbol} (recommended max 10%)");
}
}
else
{
// Warning for unknown assets
errors.Add($"Unknown asset {symbol} - option liquidity not verified");
}
}
}
return errors.ToArray();
}
/// <summary>
/// Key parameters for Multi-Asset strategy optimization
/// Focuses on asset selection, adaptation, and allocation management
/// </summary>
public override string[] OptimizationParameters => base.OptimizationParameters.Concat(new[]
{
"EnableAssetAdaptation", // Asset-specific adaptation toggle
"MaxAssets", // Portfolio diversification limit
"BaseStrikeWidth", // Strike selection base width
"UseEqualAllocation", // Allocation strategy choice
"MaxCorrelation", // Risk management - correlation limit
"MinAssetPrice" // Asset filtering - minimum price
}).ToArray();
}
}using System.Linq;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Configuration for multi-asset Iron Condor strategy focused on Equity options (AAPL, MSFT, GOOGL)
/// Demonstrates asset-specific parameter defaults optimized for individual stock options
/// </summary>
public class MultiAssetEquityConfig : MultiAssetConfig
{
/// <summary>
/// Default to major tech stocks with liquid options
/// </summary>
[StrategyParameter("Symbols", "AAPL,MSFT,GOOGL")]
public new string[] Symbols { get; set; } = new[] { "AAPL", "MSFT", "GOOGL" };
/// <summary>
/// Enable asset adaptation for equity options
/// </summary>
[StrategyParameter("EnableAssetAdaptation", true)]
public new bool EnableAssetAdaptation { get; set; } = true;
/// <summary>
/// Allow more equity assets for diversification
/// </summary>
[StrategyParameter("MaxAssets", 5)]
public new int MaxAssets { get; set; } = 5;
/// <summary>
/// Narrower strike width for equity options (smaller premium)
/// </summary>
[StrategyParameter("BaseStrikeWidth", 0.04)]
public new decimal BaseStrikeWidth { get; set; } = 0.04m; // 4% for equities
/// <summary>
/// Use equal allocation across equity positions
/// </summary>
[StrategyParameter("UseEqualAllocation", true)]
public new bool UseEqualAllocation { get; set; } = true;
/// <summary>
/// Lower minimum price for equity options
/// </summary>
[StrategyParameter("MinAssetPrice", 50.0)]
public new decimal MinAssetPrice { get; set; } = 50.0m;
/// <summary>
/// Wider delta range for equities (lower gamma risk)
/// </summary>
[StrategyParameter("EntryDeltaMin", 0.15)]
public new decimal EntryDeltaMin { get; set; } = 0.15m;
[StrategyParameter("EntryDeltaMax", 0.35)]
public new decimal EntryDeltaMax { get; set; } = 0.35m;
/// <summary>
/// Lower allocation per position for equities (more but smaller trades)
/// </summary>
[StrategyParameter("AllocationPerPosition", 0.08)]
public new decimal AllocationPerPosition { get; set; } = 0.08m;
/// <summary>
/// More max positions for equity strategies
/// </summary>
[StrategyParameter("MaxPositions", 8)]
public new int MaxPositions { get; set; } = 8;
/// <summary>
/// Lower profit target for equity options (faster premium decay)
/// </summary>
[StrategyParameter("ProfitTarget", 0.40)]
public new decimal ProfitTarget { get; set; } = 0.40m;
/// <summary>
/// Wider stop loss for equity options (lower volatility)
/// </summary>
[StrategyParameter("StopLoss", -0.60)]
public new decimal StopLoss { get; set; } = -0.60m;
/// <summary>
/// Shorter hold time for equity options (faster moves)
/// </summary>
[StrategyParameter("MaxDaysInTrade", 30)]
public new int MaxDaysInTrade { get; set; } = 30;
/// <summary>
/// Higher volatility requirement for equity options
/// </summary>
[StrategyParameter("MinImpliedVolatility", 0.25)]
public new decimal MinImpliedVolatility { get; set; } = 0.25m;
/// <summary>
/// Optimization parameters for multi-asset equity strategy.
/// Focuses on equity-specific adaptations and portfolio management
/// </summary>
public override string[] OptimizationParameters => base.OptimizationParameters.Concat(new[]
{
"BaseStrikeWidth", // Strike positioning for equity options
"EntryDeltaMin", // Delta targeting - minimum
"EntryDeltaMax", // Delta targeting - maximum
"AllocationPerPosition", // Position sizing per equity
"MaxPositions", // Portfolio diversification limit
"ProfitTarget", // Exit target for equity volatility
"StopLoss", // Risk management
"MaxDaysInTrade", // Holding period for equity moves
"MinImpliedVolatility", // Volatility filter for equity options
"MaxAssets" // Asset diversification control
}).ToArray();
}
}using System;
using System.Linq;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Configuration for multi-asset Iron Condor strategy focused on Index options (SPX, NDX)
/// Demonstrates asset-specific parameter defaults optimized for high-value index options
/// </summary>
public class MultiAssetSPXConfig : MultiAssetConfig
{
/// <summary>
/// Default to SPX and SPY for viable margin strategy (SPX index + SPY equity)
/// </summary>
[StrategyParameter("Symbols", "SPX,SPY")]
public new string[] Symbols { get; set; } = new[] { "SPX", "SPY" };
/// <summary>
/// Enable asset adaptation for index options
/// </summary>
[StrategyParameter("EnableAssetAdaptation", true)]
public new bool EnableAssetAdaptation { get; set; } = true;
/// <summary>
/// Limit to 2 index assets to control correlation risk
/// </summary>
[StrategyParameter("MaxAssets", 2)]
public new int MaxAssets { get; set; } = 2;
/// <summary>
/// Wider strike width for index options (higher premium)
/// </summary>
[StrategyParameter("BaseStrikeWidth", 0.06)]
public new decimal BaseStrikeWidth { get; set; } = 0.06m; // 6% for indices
/// <summary>
/// Use asset-specific allocation (indices need less diversification)
/// </summary>
[StrategyParameter("UseEqualAllocation", false)]
public new bool UseEqualAllocation { get; set; } = false;
/// <summary>
/// Higher minimum price for index options
/// </summary>
[StrategyParameter("MinAssetPrice", 3000.0)]
public new decimal MinAssetPrice { get; set; } = 3000.0m;
/// <summary>
/// Tighter delta range for indices (higher gamma risk)
/// </summary>
[StrategyParameter("EntryDeltaMin", 0.20)]
public new decimal EntryDeltaMin { get; set; } = 0.20m;
[StrategyParameter("EntryDeltaMax", 0.30)]
public new decimal EntryDeltaMax { get; set; } = 0.30m;
/// <summary>
/// Conservative allocation per position for margin-intensive strategies
/// </summary>
[StrategyParameter("AllocationPerPosition", 0.05)]
public new decimal AllocationPerPosition { get; set; } = 0.05m;
/// <summary>
/// Fewer max positions for index strategies
/// </summary>
[StrategyParameter("MaxPositions", 3)]
public new int MaxPositions { get; set; } = 3;
/// <summary>
/// Higher profit target for index options (better premium decay)
/// </summary>
[StrategyParameter("ProfitTarget", 0.60)]
public new decimal ProfitTarget { get; set; } = 0.60m;
/// <summary>
/// Tighter stop loss for index options (higher volatility)
/// </summary>
[StrategyParameter("StopLoss", -0.80)]
public new decimal StopLoss { get; set; } = -0.80m;
/// <summary>
/// Longer hold time for index options (better time decay)
/// </summary>
[StrategyParameter("MaxDaysInTrade", 45)]
public new int MaxDaysInTrade { get; set; } = 45;
/// <summary>
/// Higher account size for index options (required for SPX margin requirements)
/// </summary>
[StrategyParameter("AccountSize", 300000)]
public override decimal AccountSize { get; set; } = 300000m;
/// <summary>
/// Shorter test period for performance testing (1 month instead of 1 year)
/// </summary>
[StrategyParameter("StartDate", "2024-01-01")]
public new DateTime StartDate { get; set; } = new DateTime(2024, 1, 1);
[StrategyParameter("EndDate", "2024-01-31")]
public new DateTime EndDate { get; set; } = new DateTime(2024, 1, 31);
/// <summary>
/// Key parameters for Multi-Asset SPX strategy optimization
/// Focuses on index-specific parameters and high-value option characteristics
/// </summary>
public override string[] OptimizationParameters => base.OptimizationParameters.Concat(new[]
{
"BaseStrikeWidth", // Strike width for index options
"EntryDeltaMin", // Delta targeting - minimum
"EntryDeltaMax", // Delta targeting - maximum
"AllocationPerPosition", // Position sizing for high-margin strategies
"MaxPositions", // Portfolio concentration for indices
"ProfitTarget", // Exit strategy - profit taking
"StopLoss", // Exit strategy - loss cutting
"MaxDaysInTrade" // Hold period for index options
}).ToArray();
}
}using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using QuantConnect;
using QuantConnect.Algorithm;
using CoreAlgo.Architecture.Core.Attributes;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Base class for all strategy configurations with QC GetParameter() integration
/// </summary>
public abstract class StrategyConfig
{
/// <summary>
/// Underlying symbol to trade (e.g., "SPY", "QQQ", "AAPL")
/// </summary>
public string UnderlyingSymbol { get; set; } = "SPY";
/// <summary>
/// Strategy start date
/// Configurable via StartDate parameter in config.json
/// </summary>
[StrategyParameter("StartDate", "2023-01-01")]
public DateTime StartDate { get; set; } = new DateTime(2023, 1, 1);
/// <summary>
/// Strategy end date
/// Configurable via EndDate parameter in config.json
/// </summary>
[StrategyParameter("EndDate", "2023-12-31")]
public DateTime EndDate { get; set; } = new DateTime(2023, 12, 31);
/// <summary>
/// Starting cash amount
/// </summary>
public decimal StartingCash { get; set; } = 100000m;
/// <summary>
/// Account size for position sizing and margin calculations (configurable via parameters)
/// </summary>
[StrategyParameter("AccountSize", 100000)]
public virtual decimal AccountSize { get; set; } = 100000m;
// ============================================================================
// POSITION OVERLAP PREVENTION CONFIGURATION (Task 19)
// ============================================================================
/// <summary>
/// Enable position overlap prevention system
/// </summary>
[StrategyParameter("EnableOverlapPrevention", true)]
public bool EnableOverlapPrevention { get; set; } = true;
/// <summary>
/// Position overlap prevention mode: Block, Warn, or Log
/// Block: Prevents orders that would create overlaps
/// Warn: Allows orders but logs warnings
/// Log: Only logs overlap detection for analysis
/// </summary>
[StrategyParameter("OverlapPreventionMode", "Block")]
public string OverlapPreventionMode { get; set; } = "Block";
/// <summary>
/// Allow multiple strategies on same underlying (advanced users only)
/// </summary>
[StrategyParameter("AllowMultiStrategyUnderlying", false)]
public bool AllowSameUnderlyingMultiStrategy { get; set; } = false;
/// <summary>
/// Maximum number of active positions per underlying asset
/// </summary>
[StrategyParameter("MaxPositionsPerUnderlying", 1)]
public int MaxPositionsPerUnderlying { get; set; } = 1;
/// <summary>
/// Maximum number of active combo orders per underlying asset
/// Combo orders are multi-leg strategies like Iron Condors, Spreads, etc.
/// </summary>
[StrategyParameter("MaxPositionsPerCombo", 2)]
public int MaxPositionsPerCombo { get; set; } = 2;
/// <summary>
/// Expected leg count for this strategy's combo orders
/// Used for structural analysis: 2=Spread, 4=Iron Condor, etc.
/// Set to 0 for single-leg strategies or variable leg counts
/// </summary>
[StrategyParameter("ComboOrderLegCount", 0)]
public int ComboOrderLegCount { get; set; } = 0;
/// <summary>
/// Minimum strike distance for options overlap detection (in dollars)
/// </summary>
[StrategyParameter("MinimumStrikeDistance", 5.0)]
public decimal MinimumStrikeDistance { get; set; } = 5.0m;
// ============================================================================
// ENHANCED STRATEGY PARAMETERS (Added for Task 2)
// ============================================================================
/// <summary>
/// Array of symbols to trade (supports multi-asset strategies)
/// Default to single SPY for backward compatibility
/// </summary>
[StrategyParameter("Symbols", "SPY")]
public string[] Symbols { get; set; } = new[] { "SPY" };
/// <summary>
/// Enable universe selection instead of manual symbol list
/// </summary>
[StrategyParameter("UseUniverseSelection", false)]
public bool UseUniverseSelection { get; set; } = false;
/// <summary>
/// Minimum delta for option entry (for options strategies)
/// </summary>
[StrategyParameter("EntryDeltaMin", 0.25)]
public decimal EntryDeltaMin { get; set; } = 0.25m;
/// <summary>
/// Maximum delta for option entry (for options strategies)
/// </summary>
[StrategyParameter("EntryDeltaMax", 0.35)]
public decimal EntryDeltaMax { get; set; } = 0.35m;
/// <summary>
/// Delta threshold for option exit (for options strategies)
/// </summary>
[StrategyParameter("ExitDelta", 0.10)]
public decimal ExitDelta { get; set; } = 0.10m;
/// <summary>
/// Allocation percentage per position (e.g., 0.1 = 10% of portfolio per position)
/// </summary>
[StrategyParameter("AllocationPerPosition", 0.1)]
public decimal AllocationPerPosition { get; set; } = 0.1m;
/// <summary>
/// Maximum number of concurrent positions
/// </summary>
[StrategyParameter("MaxPositions", 5)]
public int MaxPositions { get; set; } = 5;
/// <summary>
/// Trading start time (market hours restriction)
/// </summary>
[StrategyParameter("TradingStartTime", "09:30:00")]
public TimeSpan TradingStartTime { get; set; } = new TimeSpan(9, 30, 0);
/// <summary>
/// Trading end time (market hours restriction)
/// </summary>
[StrategyParameter("TradingEndTime", "15:30:00")]
public TimeSpan TradingEndTime { get; set; } = new TimeSpan(15, 30, 0);
/// <summary>
/// Enable a central entry time window. When enabled, entries are only allowed between EntryWindowStart and EntryWindowEnd
/// </summary>
[StrategyParameter("UseEntryTimeWindow", false)]
public bool UseEntryTimeWindow { get; set; } = false;
/// <summary>
/// Earliest time-of-day to allow new entries (ET). Ignored when UseEntryTimeWindow is false
/// </summary>
[StrategyParameter("EntryWindowStart", "00:00:00")]
public TimeSpan EntryWindowStart { get; set; } = TimeSpan.Zero;
/// <summary>
/// Latest time-of-day to allow new entries (ET). Ignored when UseEntryTimeWindow is false
/// </summary>
[StrategyParameter("EntryWindowEnd", "23:59:59")]
public TimeSpan EntryWindowEnd { get; set; } = new TimeSpan(23, 59, 59);
/// <summary>
/// Profit target as percentage (e.g., 0.5 = 50% profit target)
/// Set to 0 to disable profit target checks (for strategies using custom exits)
/// </summary>
[StrategyParameter("ProfitTarget", 0.5)]
public decimal ProfitTarget { get; set; } = 0.5m;
/// <summary>
/// Stop loss as percentage (e.g., -0.5 = 50% stop loss)
/// Set to 0 to disable stop loss checks (for strategies using custom exits)
/// </summary>
[StrategyParameter("StopLoss", -0.5)]
public decimal StopLoss { get; set; } = -0.5m;
/// <summary>
/// Maximum days to hold a position
/// </summary>
[StrategyParameter("MaxDaysInTrade", 30)]
public int MaxDaysInTrade { get; set; } = 30;
/// <summary>
/// Enable detailed entry restriction logging
/// </summary>
[StrategyParameter("LogEntryRestrictions", false)]
public virtual bool LogEntryRestrictions { get; set; }
/// <summary>
/// Minimum implied volatility for entry (options strategies)
/// </summary>
[StrategyParameter("MinImpliedVolatility", 0.15)]
public decimal MinImpliedVolatility { get; set; } = 0.15m;
// ============================================================================
// SMARTPRICING EXECUTION PARAMETERS (Added for Task 12)
// ============================================================================
/// <summary>
/// SmartPricing execution mode for improved fill rates on options spreads
/// Supported values: "Normal", "Fast", "Patient", "Off"
/// </summary>
[StrategyParameter("SmartPricingMode", "Normal")]
public string SmartPricingMode { get; set; } = "Normal";
/// <summary>
/// Maximum acceptable net spread width for combo orders before using market orders
/// Prevents smart pricing on combos with excessive spread costs
/// </summary>
[StrategyParameter("ComboMaxNetSpreadWidth", 5.0)]
public decimal ComboMaxNetSpreadWidth { get; set; } = 5.0m;
/// <summary>
/// SmartPricing mode specifically for combo orders (can be different from single-leg)
/// If not specified, uses the main SmartPricingMode. Supported values: "Normal", "Fast", "Patient", "Off"
/// </summary>
[StrategyParameter("ComboSmartPricingMode", "")]
public string ComboSmartPricingMode { get; set; } = "";
/// <summary>
/// Enable smart pricing for combo orders (multi-leg strategies)
/// When false, combo orders use basic market execution regardless of SmartPricingMode
/// </summary>
[StrategyParameter("EnableComboSmartPricing", true)]
public bool EnableComboSmartPricing { get; set; } = true;
// ============================================================================
// OPTIMIZATION PARAMETERS CONFIGURATION
// ============================================================================
/// <summary>
/// Parameters to include in config.json for QuantConnect optimization
/// These are the key parameters that should be exposed for backtesting optimization
/// All other StrategyParameter attributes remain available for internal configuration
/// </summary>
/// <summary>
/// Preferred underlying data resolution for strategies that need to downsample equities
/// Defaults to Minute to preserve existing behaviour
/// </summary>
[StrategyParameter("DataResolution", "Minute")]
public string UnderlyingResolution { get; set; } = "Minute";
/// <summary>
/// Parse the UnderlyingResolution string into Resolution enum (fallback Minute)
/// </summary>
public virtual Resolution GetUnderlyingResolution()
{
return UnderlyingResolution?.ToUpperInvariant() switch
{
"SECOND" => Resolution.Second,
"MINUTE" => Resolution.Minute,
"HOUR" => Resolution.Hour,
"DAILY" => Resolution.Daily,
_ => Resolution.Minute
};
}
public virtual string[] OptimizationParameters => new[]
{
"Strategy", // Always required for strategy selection
"StartDate", // Backtest period start
"EndDate", // Backtest period end
"AccountSize", // Position sizing base
"MaxPositions", // Risk management - position count
"ProfitTarget", // Exit strategy - profit taking
"StopLoss", // Exit strategy - loss cutting
"AllocationPerPosition" // Position sizing per trade
};
/// <summary>
/// Load parameters from QC's GetParameter() method using context pattern
/// </summary>
public virtual void LoadFromParameters(IAlgorithmContext context)
{
var properties = GetType().GetProperties();
var loadedParams = new System.Collections.Generic.List<string>();
foreach (var property in properties)
{
var attribute = property.GetCustomAttribute<StrategyParameterAttribute>();
if (attribute != null)
{
var parameterName = attribute.Name;
var defaultValue = property.GetValue(this)?.ToString() ?? attribute.DefaultValue?.ToString() ?? "";
var parameterValue = context.Algorithm.GetParameter(parameterName, defaultValue);
// Convert the parameter value to the property type
var convertedValue = ConvertParameterValue(parameterValue, property.PropertyType);
property.SetValue(this, convertedValue);
// Format parameter for logging
var formattedValue = convertedValue is string[] arrayValue
? $"[{string.Join(", ", arrayValue)}]"
: $"{convertedValue}";
loadedParams.Add($"{parameterName}={formattedValue}");
}
}
// Single log output for all parameters
if (loadedParams.Count > 0)
{
((dynamic)context.Logger).Debug($"Loaded parameters: {string.Join(", ", loadedParams)}");
}
}
/// <summary>
/// Convert parameter value to the target type
/// </summary>
private object ConvertParameterValue(string value, Type targetType)
{
if (targetType == typeof(string))
return value;
if (targetType == typeof(int))
return int.Parse(value);
if (targetType == typeof(decimal))
return decimal.Parse(value);
if (targetType == typeof(bool))
return bool.Parse(value);
if (targetType == typeof(DateTime))
return DateTime.Parse(value);
if (targetType == typeof(TimeSpan))
return TimeSpan.Parse(value);
// Handle string arrays (for Symbols parameter)
if (targetType == typeof(string[]))
{
// Handle various input formats that QuantConnect might send
if (string.IsNullOrWhiteSpace(value))
{
return new string[] { "SPY" }; // Default fallback
}
// Handle already-parsed arrays (QuantConnect might pass this way)
if (value.StartsWith("System.String[]"))
{
// QC passes arrays as "System.String[]" string - extract from property if available
return new string[] { "SPY" }; // Fallback, will be overridden by template logic
}
// Handle comma-separated string (our expected format)
var result = value.Split(',').Select(s => s.Trim()).Where(s => !string.IsNullOrEmpty(s)).ToArray();
return result.Length > 0 ? result : new string[] { "SPY" }; // Ensure we always have at least one symbol
}
// For other types, try generic conversion
return Convert.ChangeType(value, targetType);
}
/// <summary>
/// Validate the configuration
/// </summary>
public virtual string[] Validate()
{
var errors = new List<string>();
// Generic validations that apply to all strategies
if (AccountSize < 10000m)
errors.Add($"Account size ${AccountSize:F0} too small (minimum $10,000)");
if (AllocationPerPosition > 0.5m)
errors.Add($"Allocation {AllocationPerPosition:P0} too high (maximum 50%)");
if (MaxPositions <= 0)
errors.Add($"MaxPositions must be greater than 0 (current: {MaxPositions})");
// Skip validation if profit target is disabled (0 means disabled)
if (ProfitTarget != 0 && ProfitTarget <= 0)
errors.Add($"ProfitTarget must be positive (current: {ProfitTarget:P1}) or 0 to disable");
// Skip validation if stop loss is disabled (0 means disabled)
if (StopLoss != 0 && StopLoss >= 0)
errors.Add($"StopLoss must be negative (current: {StopLoss:P1}) or 0 to disable");
// Validate SmartPricing mode
if (!CoreAlgo.Architecture.Core.Execution.SmartPricingEngineFactory.IsValidMode(SmartPricingMode))
errors.Add($"Invalid SmartPricingMode '{SmartPricingMode}' (valid: Normal, Fast, Patient, Off)");
// Entry window validation
if (UseEntryTimeWindow)
{
if (EntryWindowStart > EntryWindowEnd)
{
errors.Add($"EntryWindowStart {EntryWindowStart:hh\\:mm} must be <= EntryWindowEnd {EntryWindowEnd:hh\\:mm}");
}
}
return errors.ToArray();
}
/// <summary>
/// Gets a parameter value by name with a default fallback
/// </summary>
public object GetParameterValue(string parameterName, object defaultValue)
{
var property = GetType().GetProperty(parameterName);
if (property != null)
{
return property.GetValue(this);
}
return defaultValue;
}
/// <summary>
/// Creates a SmartPricing engine based on the current configuration
/// </summary>
/// <returns>Configured SmartPricing engine or null if disabled</returns>
public CoreAlgo.Architecture.Core.Execution.ISmartPricingEngine CreateSmartPricingEngine()
{
var mode = CoreAlgo.Architecture.Core.Execution.SmartPricingEngineFactory.ParseMode(SmartPricingMode);
if (mode == CoreAlgo.Architecture.Core.Execution.SmartPricingMode.Off)
return null;
return CoreAlgo.Architecture.Core.Execution.SmartPricingEngineFactory.Create(mode);
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.Core.Services
{
/// <summary>
/// Cross-asset risk management system that extends QuantConnect's native risk capabilities.
/// Provides portfolio-level risk aggregation, concentration limits, and multi-asset risk monitoring.
///
/// Design Philosophy: Extend QC's excellent risk framework rather than replace it.
/// Integrates with QC's Portfolio, margin calculations, and position tracking.
/// </summary>
public class CoreAlgoRiskManager
{
private readonly IAlgorithmContext _context;
// Risk thresholds - configurable via strategy parameters
private readonly decimal _maxPortfolioMarginUtilization;
private readonly decimal _maxAssetConcentration;
private readonly decimal _maxCorrelatedAssetsAllocation;
public CoreAlgoRiskManager(IAlgorithmContext context,
decimal maxMarginUtilization = 0.70m,
decimal maxAssetConcentration = 0.30m,
decimal maxCorrelatedAllocation = 0.50m)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_maxPortfolioMarginUtilization = maxMarginUtilization;
_maxAssetConcentration = maxAssetConcentration;
_maxCorrelatedAssetsAllocation = maxCorrelatedAllocation;
}
/// <summary>
/// Validates if a new position can be safely added to the portfolio.
/// Integrates with QC's native margin and position tracking.
/// </summary>
/// <param name="symbol">Symbol to validate</param>
/// <param name="quantity">Proposed quantity</param>
/// <param name="estimatedPrice">Estimated fill price</param>
/// <returns>True if position is within risk limits</returns>
public bool ValidateNewPosition(string symbol, decimal quantity, decimal estimatedPrice)
{
try
{
var algorithm = _context.Algorithm;
var portfolio = algorithm.Portfolio;
// 1. Check portfolio-level margin utilization (extends QC's native margin tracking)
var currentMarginUtilization = GetPortfolioMarginUtilization();
if (currentMarginUtilization > _maxPortfolioMarginUtilization)
{
((dynamic)_context.Logger).Warning($"Risk: Portfolio margin utilization too high: {currentMarginUtilization:P1} > {_maxPortfolioMarginUtilization:P1}");
return false;
}
// 2. Check asset concentration limits
var proposedConcentration = CalculateAssetConcentrationAfterTrade(symbol, quantity, estimatedPrice);
if (proposedConcentration > _maxAssetConcentration)
{
((dynamic)_context.Logger).Warning($"Risk: Asset concentration would exceed limit: {proposedConcentration:P1} > {_maxAssetConcentration:P1} for {symbol}");
return false;
}
// 3. Check if we have too many positions (basic diversification)
var totalOptionPositions = portfolio.Values.Count(x => x.Invested && x.Symbol.SecurityType == SecurityType.Option);
if (totalOptionPositions >= 20) // Reasonable upper limit for option strategies
{
((dynamic)_context.Logger).Warning($"Risk: Too many option positions: {totalOptionPositions} >= 20");
return false;
}
return true;
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"Risk validation error for {symbol}: {ex.Message}");
return false; // Fail safe - reject if we can't validate
}
}
/// <summary>
/// Gets comprehensive portfolio risk metrics using QC's native data.
/// </summary>
public PortfolioRiskMetrics GetPortfolioRisk()
{
var algorithm = _context.Algorithm;
var portfolio = algorithm.Portfolio;
return new PortfolioRiskMetrics
{
// QC Native metrics
TotalPortfolioValue = portfolio.TotalPortfolioValue,
TotalMarginUsed = portfolio.TotalMarginUsed,
MarginRemaining = portfolio.MarginRemaining,
Cash = portfolio.Cash,
TotalUnrealizedProfit = portfolio.TotalUnrealizedProfit,
// CoreAlgo calculated metrics
MarginUtilization = GetPortfolioMarginUtilization(),
AssetConcentrations = GetAssetConcentrations(),
OptionPositionCount = portfolio.Values.Count(x => x.Invested && x.Symbol.SecurityType == SecurityType.Option),
LargestAssetExposure = GetLargestAssetExposure(),
// Risk alerts
RiskAlerts = GenerateRiskAlerts()
};
}
/// <summary>
/// Calculates portfolio margin utilization using QC's native margin tracking.
/// </summary>
private decimal GetPortfolioMarginUtilization()
{
var portfolio = _context.Algorithm.Portfolio;
if (portfolio.TotalPortfolioValue <= 0)
return 0;
return portfolio.TotalMarginUsed / portfolio.TotalPortfolioValue;
}
/// <summary>
/// Calculates concentration by underlying asset across all positions.
/// </summary>
private Dictionary<string, decimal> GetAssetConcentrations()
{
var portfolio = _context.Algorithm.Portfolio;
var concentrations = new Dictionary<string, decimal>();
var totalValue = portfolio.TotalPortfolioValue;
if (totalValue <= 0) return concentrations;
// Group by underlying asset for options
var assetGroups = portfolio.Values
.Where(x => x.Invested)
.GroupBy(x => x.Symbol.SecurityType == SecurityType.Option ?
x.Symbol.Underlying.Value : x.Symbol.Value);
foreach (var group in assetGroups)
{
var assetValue = group.Sum(x => Math.Abs(x.HoldingsValue));
var concentration = assetValue / totalValue;
concentrations[group.Key] = concentration;
}
return concentrations;
}
/// <summary>
/// Calculates what asset concentration would be after a proposed trade.
/// </summary>
private decimal CalculateAssetConcentrationAfterTrade(string symbol, decimal quantity, decimal price)
{
var concentrations = GetAssetConcentrations();
var portfolio = _context.Algorithm.Portfolio;
var totalValue = portfolio.TotalPortfolioValue;
// Calculate additional value from proposed trade
var additionalValue = Math.Abs(quantity * price);
var newTotalValue = totalValue + additionalValue;
// Get underlying symbol for options
var underlyingSymbol = symbol; // Assume equity by default
// TODO: Extract underlying symbol for options when we add option support
var currentAssetValue = concentrations.ContainsKey(underlyingSymbol)
? concentrations[underlyingSymbol] * totalValue
: 0;
var newAssetValue = currentAssetValue + additionalValue;
return newTotalValue > 0 ? newAssetValue / newTotalValue : 0;
}
/// <summary>
/// Gets the largest single asset exposure as percentage of portfolio.
/// </summary>
private decimal GetLargestAssetExposure()
{
var concentrations = GetAssetConcentrations();
return concentrations.Values.DefaultIfEmpty(0).Max();
}
/// <summary>
/// Generates active risk alerts based on current portfolio state.
/// </summary>
private List<RiskAlert> GenerateRiskAlerts()
{
var alerts = new List<RiskAlert>();
// Margin utilization alert
var marginUtil = GetPortfolioMarginUtilization();
if (marginUtil > _maxPortfolioMarginUtilization)
{
alerts.Add(new RiskAlert
{
Type = RiskAlertType.MarginUtilization,
Severity = marginUtil > 0.85m ? RiskSeverity.High : RiskSeverity.Medium,
Message = $"Margin utilization: {marginUtil:P1} exceeds limit: {_maxPortfolioMarginUtilization:P1}",
Value = marginUtil
});
}
// Asset concentration alerts
var concentrations = GetAssetConcentrations();
foreach (var kvp in concentrations.Where(x => x.Value > _maxAssetConcentration))
{
alerts.Add(new RiskAlert
{
Type = RiskAlertType.AssetConcentration,
Severity = kvp.Value > 0.50m ? RiskSeverity.High : RiskSeverity.Medium,
Message = $"Asset {kvp.Key} concentration: {kvp.Value:P1} exceeds limit: {_maxAssetConcentration:P1}",
Symbol = kvp.Key,
Value = kvp.Value
});
}
return alerts;
}
}
/// <summary>
/// Portfolio risk metrics combining QC native data with CoreAlgo calculations.
/// </summary>
public class PortfolioRiskMetrics
{
// QC Native Portfolio metrics
public decimal TotalPortfolioValue { get; set; }
public decimal TotalMarginUsed { get; set; }
public decimal MarginRemaining { get; set; }
public decimal Cash { get; set; }
public decimal TotalUnrealizedProfit { get; set; }
// CoreAlgo calculated risk metrics
public decimal MarginUtilization { get; set; }
public Dictionary<string, decimal> AssetConcentrations { get; set; } = new Dictionary<string, decimal>();
public int OptionPositionCount { get; set; }
public decimal LargestAssetExposure { get; set; }
// Risk monitoring
public List<RiskAlert> RiskAlerts { get; set; } = new List<RiskAlert>();
}
/// <summary>
/// Risk alert for portfolio monitoring.
/// </summary>
public class RiskAlert
{
public RiskAlertType Type { get; set; }
public RiskSeverity Severity { get; set; }
public string Message { get; set; }
public string Symbol { get; set; }
public decimal Value { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
public enum RiskAlertType
{
MarginUtilization,
AssetConcentration,
PositionCount,
CorrelationRisk
}
public enum RiskSeverity
{
Low,
Medium,
High,
Critical
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Interfaces;
using CoreAlgo.Architecture.Core.Implementations;
namespace CoreAlgo.Architecture.Core.Services
{
/// <summary>
/// TODO: INTEGRATE LATER - Advanced portfolio reporting for multi-asset strategies.
///
/// CURRENT STATUS: Built but not integrated with templates yet.
/// REASON: Avoiding complexity in templates while building foundation for future reporting needs.
///
/// FUTURE INTEGRATION PLAN:
/// 1. Add periodic portfolio reporting to Main.cs OnEndOfDay
/// 2. Integrate with risk monitoring dashboard
/// 3. Add performance analytics and attribution reporting
/// 4. Connect with correlation analysis for risk attribution
///
/// This component extends QuantConnect's native portfolio metrics with multi-asset
/// specific reporting, risk attribution, and performance analytics.
/// </summary>
public class PortfolioReporter
{
private readonly IAlgorithmContext _context;
private readonly CoreAlgoRiskManager _riskManager;
// TODO: FUTURE USE - Add correlation calculator when ready for integration
// private readonly CorrelationCalculator _correlationCalculator;
public PortfolioReporter(IAlgorithmContext context, CoreAlgoRiskManager riskManager)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_riskManager = riskManager ?? throw new ArgumentNullException(nameof(riskManager));
}
/// <summary>
/// TODO: FUTURE USE - Generates comprehensive portfolio risk report.
/// Combines QC native metrics with CoreAlgo risk analysis.
/// </summary>
public PortfolioRiskReport GenerateRiskReport()
{
try
{
var portfolio = _context.Algorithm.Portfolio;
var riskMetrics = _riskManager.GetPortfolioRisk();
var report = new PortfolioRiskReport
{
Timestamp = _context.Algorithm.Time,
// Portfolio overview
PortfolioSummary = new PortfolioSummary
{
TotalValue = portfolio.TotalPortfolioValue,
Cash = portfolio.Cash,
InvestedCapital = portfolio.TotalHoldingsValue,
UnrealizedPnL = portfolio.TotalUnrealizedProfit,
RealizedPnL = portfolio.TotalProfit - portfolio.TotalUnrealizedProfit,
MarginUsed = portfolio.TotalMarginUsed,
MarginUtilization = riskMetrics.MarginUtilization
},
// Asset breakdown
AssetBreakdown = GenerateAssetBreakdown(),
// Risk metrics
RiskMetrics = riskMetrics,
// Position summary
PositionSummary = GeneratePositionSummary(),
// Performance metrics (TODO: Add more sophisticated metrics)
PerformanceMetrics = GeneratePerformanceMetrics()
};
((dynamic)_context.Logger).Debug($"Portfolio risk report generated: {report.AssetBreakdown.Count} assets, {report.PositionSummary.TotalPositions} positions");
return report;
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"Error generating portfolio risk report: {ex.Message}");
return new PortfolioRiskReport { Timestamp = _context.Algorithm.Time };
}
}
/// <summary>
/// TODO: FUTURE USE - Gets key portfolio metrics for dashboard display.
/// Designed for periodic monitoring and alert systems.
/// </summary>
public Dictionary<string, object> GetDashboardMetrics()
{
var portfolio = _context.Algorithm.Portfolio;
var riskMetrics = _riskManager.GetPortfolioRisk();
return new Dictionary<string, object>
{
// Key performance indicators
["TotalValue"] = portfolio.TotalPortfolioValue,
["DailyPnL"] = portfolio.TotalUnrealizedProfit, // TODO: Calculate actual daily P&L
["MarginUtilization"] = riskMetrics.MarginUtilization,
["CashBalance"] = portfolio.Cash,
// Risk indicators
["ActivePositions"] = riskMetrics.OptionPositionCount,
["LargestExposure"] = riskMetrics.LargestAssetExposure,
["RiskAlerts"] = riskMetrics.RiskAlerts.Count,
// Asset diversification
["UniqueAssets"] = riskMetrics.AssetConcentrations.Count,
["TopAssetConcentration"] = riskMetrics.AssetConcentrations.Values.DefaultIfEmpty(0).Max(),
// Status indicators
["LastUpdate"] = _context.Algorithm.Time,
["PortfolioHealth"] = CalculatePortfolioHealthScore(riskMetrics)
};
}
/// <summary>
/// TODO: FUTURE USE - Generates asset-level breakdown of portfolio.
/// Groups positions by underlying asset for multi-asset analysis.
/// </summary>
private List<AssetBreakdown> GenerateAssetBreakdown()
{
var portfolio = _context.Algorithm.Portfolio;
var breakdown = new List<AssetBreakdown>();
// Group positions by underlying asset
var assetGroups = portfolio.Values
.Where(x => x.Invested)
.GroupBy(x => GetUnderlyingSymbol(x.Symbol));
foreach (var group in assetGroups)
{
var positions = group.ToList();
var totalValue = positions.Sum(x => x.HoldingsValue);
var totalUnrealized = positions.Sum(x => x.UnrealizedProfit);
breakdown.Add(new AssetBreakdown
{
UnderlyingSymbol = group.Key,
PositionCount = positions.Count,
TotalValue = totalValue,
UnrealizedPnL = totalUnrealized,
Concentration = portfolio.TotalPortfolioValue > 0 ?
Math.Abs(totalValue) / portfolio.TotalPortfolioValue : 0,
// Position details
Positions = positions.Select(x => new PositionDetail
{
Symbol = x.Symbol.Value,
Quantity = x.Quantity,
AveragePrice = x.AveragePrice,
MarketPrice = x.Price,
HoldingsValue = x.HoldingsValue,
UnrealizedPnL = x.UnrealizedProfit,
SecurityType = x.Symbol.SecurityType
}).ToList()
});
}
return breakdown.OrderByDescending(x => Math.Abs(x.TotalValue)).ToList();
}
/// <summary>
/// TODO: FUTURE USE - Generates summary of all positions.
/// </summary>
private PositionSummary GeneratePositionSummary()
{
var portfolio = _context.Algorithm.Portfolio;
var investedPositions = portfolio.Values.Where(x => x.Invested).ToList();
return new PositionSummary
{
TotalPositions = investedPositions.Count,
OptionPositions = investedPositions.Count(x => x.Symbol.SecurityType == SecurityType.Option),
EquityPositions = investedPositions.Count(x => x.Symbol.SecurityType == SecurityType.Equity),
ProfitablePositions = investedPositions.Count(x => x.UnrealizedProfit > 0),
LosingPositions = investedPositions.Count(x => x.UnrealizedProfit < 0),
LargestPosition = investedPositions.DefaultIfEmpty()
.OrderByDescending(x => Math.Abs(x?.HoldingsValue ?? 0))
.FirstOrDefault()?.Symbol.Value ?? "None",
LargestPositionValue = investedPositions.DefaultIfEmpty()
.Max(x => Math.Abs(x?.HoldingsValue ?? 0))
};
}
/// <summary>
/// TODO: FUTURE USE - Generates performance metrics.
/// Placeholder for more sophisticated performance analytics.
/// </summary>
private PerformanceMetrics GeneratePerformanceMetrics()
{
var portfolio = _context.Algorithm.Portfolio;
// TODO: Implement proper performance calculations
// - Sharpe ratio, Sortino ratio, maximum drawdown
// - Risk-adjusted returns, alpha, beta
// - Asset-specific performance attribution
return new PerformanceMetrics
{
TotalReturn = portfolio.TotalProfit,
UnrealizedReturn = portfolio.TotalUnrealizedProfit,
RealizedReturn = portfolio.TotalProfit - portfolio.TotalUnrealizedProfit,
// Placeholder metrics - TODO: Implement proper calculations
WinRate = CalculateWinRate(),
AverageWin = 0, // TODO: Calculate from closed positions
AverageLoss = 0, // TODO: Calculate from closed positions
// TODO: Add time-weighted returns, risk metrics, benchmarking
};
}
/// <summary>
/// Calculates a simple portfolio health score (0-100).
/// TODO: FUTURE USE - Enhance with more sophisticated scoring.
/// </summary>
private int CalculatePortfolioHealthScore(PortfolioRiskMetrics riskMetrics)
{
var score = 100;
// Penalize high margin utilization
if (riskMetrics.MarginUtilization > 0.8m) score -= 30;
else if (riskMetrics.MarginUtilization > 0.6m) score -= 15;
// Penalize high concentration
if (riskMetrics.LargestAssetExposure > 0.5m) score -= 25;
else if (riskMetrics.LargestAssetExposure > 0.3m) score -= 10;
// Penalize too many risk alerts
score -= riskMetrics.RiskAlerts.Count * 5;
return Math.Max(0, Math.Min(100, score));
}
/// <summary>
/// Gets the underlying symbol for an asset (handles options).
/// </summary>
private string GetUnderlyingSymbol(Symbol symbol)
{
return symbol.SecurityType == SecurityType.Option ?
symbol.Underlying.Value : symbol.Value;
}
/// <summary>
/// Calculates win rate from current unrealized positions.
/// TODO: Enhance to use actual trade history.
/// </summary>
private decimal CalculateWinRate()
{
var portfolio = _context.Algorithm.Portfolio;
var investedPositions = portfolio.Values.Where(x => x.Invested).ToList();
if (investedPositions.Count == 0) return 0;
var winners = investedPositions.Count(x => x.UnrealizedProfit > 0);
return (decimal)winners / investedPositions.Count;
}
/// <summary>
/// Logs daily order position summary using the trade persistence service
/// Simple report showing counts by state and sample positions
/// </summary>
public void LogDailyOrderPositions(TradePersistenceService persistenceService, int maxPositionsToLog = 10)
{
if (persistenceService == null) return;
try
{
var summary = persistenceService.GetTradeSummary(null); // Will be called with tracker from Main.cs
((dynamic)_context.Logger).Info("=== DAILY ORDER POSITION REPORT ===");
((dynamic)_context.Logger).Info($"Total Trades: {summary.TotalTrades}");
((dynamic)_context.Logger).Info($" Working (Pending): {summary.WorkingCount}");
((dynamic)_context.Logger).Info($" Partial Fills: {summary.PartialFillCount}");
((dynamic)_context.Logger).Info($" Open (Filled): {summary.OpenCount}");
((dynamic)_context.Logger).Info($" Closed: {summary.ClosedCount}");
((dynamic)_context.Logger).Info($" Cancelled: {summary.CancelledCount}");
((dynamic)_context.Logger).Info($"Total P&L (Closed): ${summary.TotalPnL:F2}");
((dynamic)_context.Logger).Info($"Report Time: {summary.AsOfUtc:yyyy-MM-dd HH:mm:ss} UTC");
((dynamic)_context.Logger).Info("=================================");
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"Error generating daily order position report: {ex.Message}");
}
}
/// <summary>
/// Logs daily order position summary with detailed position list
/// </summary>
public void LogDailyOrderPositionsDetailed(TradeTracker tracker, int maxPositionsToLog = 10)
{
if (tracker == null) return;
try
{
((dynamic)_context.Logger).Info("=== DAILY ORDER POSITION REPORT (DETAILED) ===");
((dynamic)_context.Logger).Info($"Total Trades: {tracker.AllTrades.Count}");
((dynamic)_context.Logger).Info($" Working (Pending): {tracker.WorkingTrades.Count}");
((dynamic)_context.Logger).Info($" Open (Filled): {tracker.OpenTrades.Count}");
((dynamic)_context.Logger).Info($" Closed: {tracker.ClosedTrades.Count}");
var partialFillCount = tracker.AllTrades.Count(t => t.Status == "PartialFill");
var cancelledCount = tracker.AllTrades.Count(t => t.Status == "Cancelled");
((dynamic)_context.Logger).Info($" Partial Fills: {partialFillCount}");
((dynamic)_context.Logger).Info($" Cancelled: {cancelledCount}");
var totalPnL = tracker.ClosedTrades.Sum(t => t.PnL);
((dynamic)_context.Logger).Info($"Total P&L (Closed): ${totalPnL:F2}");
// Log sample of open positions
if (tracker.OpenTrades.Count > 0)
{
((dynamic)_context.Logger).Info($"\nOpen Positions (showing up to {maxPositionsToLog}):");
foreach (var trade in tracker.OpenTrades.Take(maxPositionsToLog))
{
var symbolsDisplay = trade.SymbolIds.Count > 0
? string.Join(", ", trade.SymbolIds.Take(2)) + (trade.SymbolIds.Count > 2 ? "..." : "")
: trade.Symbol;
((dynamic)_context.Logger).Info($" [{trade.Strategy}] {trade.OrderTag} | " +
$"Orders: {string.Join(",", trade.OrderIds)} | " +
$"Qty: {trade.FilledQuantity}/{trade.Quantity} | " +
$"Symbols: {symbolsDisplay}");
}
}
// Log sample of working positions
if (tracker.WorkingTrades.Count > 0)
{
((dynamic)_context.Logger).Info($"\nWorking Orders (showing up to {Math.Min(5, maxPositionsToLog)}):");
foreach (var trade in tracker.WorkingTrades.Take(Math.Min(5, maxPositionsToLog)))
{
((dynamic)_context.Logger).Info($" [{trade.Strategy}] {trade.OrderTag} | Order: {trade.OrderId} | Status: {trade.Status}");
}
}
((dynamic)_context.Logger).Info($"Report Time: {_context.Algorithm.Time:yyyy-MM-dd HH:mm:ss}");
((dynamic)_context.Logger).Info("=============================================");
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"Error generating detailed order position report: {ex.Message}");
}
}
}
#region Report Data Models
/// <summary>
/// TODO: FUTURE USE - Comprehensive portfolio risk report.
/// </summary>
public class PortfolioRiskReport
{
public DateTime Timestamp { get; set; }
public PortfolioSummary PortfolioSummary { get; set; }
public List<AssetBreakdown> AssetBreakdown { get; set; } = new List<AssetBreakdown>();
public PortfolioRiskMetrics RiskMetrics { get; set; }
public PositionSummary PositionSummary { get; set; }
public PerformanceMetrics PerformanceMetrics { get; set; }
}
public class PortfolioSummary
{
public decimal TotalValue { get; set; }
public decimal Cash { get; set; }
public decimal InvestedCapital { get; set; }
public decimal UnrealizedPnL { get; set; }
public decimal RealizedPnL { get; set; }
public decimal MarginUsed { get; set; }
public decimal MarginUtilization { get; set; }
}
public class AssetBreakdown
{
public string UnderlyingSymbol { get; set; }
public int PositionCount { get; set; }
public decimal TotalValue { get; set; }
public decimal UnrealizedPnL { get; set; }
public decimal Concentration { get; set; }
public List<PositionDetail> Positions { get; set; } = new List<PositionDetail>();
}
public class PositionDetail
{
public string Symbol { get; set; }
public decimal Quantity { get; set; }
public decimal AveragePrice { get; set; }
public decimal MarketPrice { get; set; }
public decimal HoldingsValue { get; set; }
public decimal UnrealizedPnL { get; set; }
public SecurityType SecurityType { get; set; }
}
public class PositionSummary
{
public int TotalPositions { get; set; }
public int OptionPositions { get; set; }
public int EquityPositions { get; set; }
public int ProfitablePositions { get; set; }
public int LosingPositions { get; set; }
public string LargestPosition { get; set; }
public decimal LargestPositionValue { get; set; }
}
public class PerformanceMetrics
{
public decimal TotalReturn { get; set; }
public decimal UnrealizedReturn { get; set; }
public decimal RealizedReturn { get; set; }
public decimal WinRate { get; set; }
public decimal AverageWin { get; set; }
public decimal AverageLoss { get; set; }
}
#endregion
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Interfaces;
using CoreAlgo.Architecture.Core.Implementations;
namespace CoreAlgo.Architecture.Core.Services
{
/// <summary>
/// Manages position overlap detection and prevention across all strategies
/// Leverages QuantConnect's native Portfolio for real-time position tracking
/// </summary>
public class PositionOverlapManager
{
private readonly IAlgorithmContext _context;
private readonly List<IPositionOverlapRule> _rules;
private readonly object _logger;
public PositionOverlapManager(IAlgorithmContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = context.Logger;
_rules = new List<IPositionOverlapRule>();
// Initialize with built-in rules
InitializeBuiltInRules();
}
/// <summary>
/// Validates whether a new combo order (multi-leg strategy) can be opened without creating dangerous overlaps
/// </summary>
/// <param name="legs">List of legs in the combo order</param>
/// <param name="quantity">Combo order quantity</param>
/// <param name="strategyTag">Strategy identifier for validation</param>
/// <returns>ValidationResult indicating if combo order is allowed</returns>
public ValidationResult ValidateComboOrder(List<QuantConnect.Orders.Leg> legs, int quantity, string strategyTag = "")
{
try
{
// For combo orders, validate the strategy as a whole unit rather than individual legs
// This preserves ComboMarketOrder atomicity and prevents partial execution issues
((dynamic)_logger).Debug($"[COMBO VALIDATION] Validating combo order: {legs.Count} legs, qty:{quantity}, strategy:{strategyTag}");
// Get current portfolio positions using QC's native Portfolio API
var existingPositions = _context.Algorithm.Portfolio
.Where(p => p.Value.Invested)
.ToList();
// Apply combo-specific validation logic based on strategy type
return ValidateComboStrategy(legs, quantity, existingPositions, strategyTag);
}
catch (Exception ex)
{
var errorMsg = $"Error in combo order validation: {ex.Message}";
((dynamic)_logger).Error(errorMsg);
return ValidationResult.Error(errorMsg);
}
}
/// <summary>
/// Validates whether a new position can be opened without creating dangerous overlaps
/// </summary>
/// <param name="symbol">Symbol to validate</param>
/// <param name="quantity">Proposed quantity</param>
/// <param name="strategyTag">Strategy identifier for logging</param>
/// <returns>ValidationResult indicating if position is allowed</returns>
public ValidationResult ValidateNewPosition(Symbol symbol, decimal quantity, string strategyTag = "")
{
try
{
// Get current portfolio positions using QC's native Portfolio API
var existingPositions = _context.Algorithm.Portfolio
.Where(p => p.Value.Invested)
.ToList();
((dynamic)_logger).Debug($"[OVERLAP CHECK] Validating {symbol} qty:{quantity} strategy:{strategyTag}");
((dynamic)_logger).Debug($"[OVERLAP CHECK] Existing positions: {existingPositions.Count}");
// Apply all registered rules
foreach (var rule in _rules)
{
var result = rule.Validate(symbol, quantity, existingPositions, strategyTag);
if (!result.IsValid)
{
((dynamic)_logger).Warning($"[OVERLAP BLOCKED] {rule.GetType().Name}: {result.Message}");
return result;
}
}
((dynamic)_logger).Debug($"[OVERLAP ALLOWED] Position validated successfully");
return ValidationResult.Success();
}
catch (Exception ex)
{
var errorMsg = $"Error in position overlap validation: {ex.Message}";
((dynamic)_logger).Error(errorMsg);
return ValidationResult.Error(errorMsg);
}
}
/// <summary>
/// Validates a combo strategy as an atomic unit using structural analysis (QC-First approach)
/// </summary>
private ValidationResult ValidateComboStrategy(List<QuantConnect.Orders.Leg> legs, int quantity,
List<KeyValuePair<Symbol, SecurityHolding>> existingPositions, string strategyTag)
{
// Analyze order structure instead of relying on strategy names
var orderStructure = AnalyzeOrderStructure(legs);
((dynamic)_logger).Debug($"[COMBO STRATEGY] Analyzed structure: {orderStructure.LegCount} legs, {orderStructure.PutCount} puts, {orderStructure.CallCount} calls");
// Use structural analysis for validation
if (orderStructure.LegCount == 4 && orderStructure.PutCount == 2 && orderStructure.CallCount == 2)
{
// 4-leg combo with 2 puts + 2 calls = Iron Condor pattern
return ValidateFourLegCombo(legs, quantity, existingPositions, orderStructure);
}
else if (orderStructure.LegCount == 2)
{
// 2-leg combo = Spread pattern
return ValidateTwoLegCombo(legs, quantity, existingPositions, orderStructure);
}
else
{
// Other combo patterns - use conservative validation
return ValidateGenericCombo(legs, quantity, existingPositions, orderStructure);
}
}
/// <summary>
/// Validates 4-leg combo orders (Iron Condor pattern) using configuration-driven limits
/// </summary>
private ValidationResult ValidateFourLegCombo(List<QuantConnect.Orders.Leg> legs, int quantity,
List<KeyValuePair<Symbol, SecurityHolding>> existingPositions, OrderStructureAnalysis structure)
{
// Get the underlying from the first leg
var underlying = legs.First().Symbol.Underlying;
var expiry = legs.First().Symbol.ID.Date;
// Count existing 4-leg combos on same underlying and expiry
var activeFourLegCombos = existingPositions
.Where(h => h.Key.SecurityType == SecurityType.Option &&
h.Key.Underlying == underlying &&
h.Key.ID.Date == expiry)
.GroupBy(h => h.Key.ID.Date)
.Where(g => g.Count() == 4) // 4-leg combo pattern
.Count();
// Get limit from strategy config (default to 1 for 4-leg combos if not specified)
var maxComboPositions = GetComboPositionLimit(4); // 4-leg combos default to strict limit
if (activeFourLegCombos >= maxComboPositions)
{
return ValidationResult.Blocked(
$"4-leg combo limit exceeded: {activeFourLegCombos} active positions on {underlying.Value} {expiry:yyyy-MM-dd} (max: {maxComboPositions})");
}
((dynamic)_logger).Debug($"[4-LEG COMBO] Validation passed: {activeFourLegCombos}/{maxComboPositions} positions");
return ValidationResult.Success();
}
/// <summary>
/// Validates 2-leg combo orders (Spread pattern) using configuration-driven limits
/// </summary>
private ValidationResult ValidateTwoLegCombo(List<QuantConnect.Orders.Leg> legs, int quantity,
List<KeyValuePair<Symbol, SecurityHolding>> existingPositions, OrderStructureAnalysis structure)
{
var underlying = legs.First().Symbol.Underlying;
// Count existing 2-leg combos on same underlying
var activeTwoLegCombos = existingPositions
.Where(h => h.Key.SecurityType == SecurityType.Option &&
h.Key.Underlying == underlying)
.GroupBy(h => h.Key.ID.Date)
.Where(g => g.Count() == 2) // 2-leg combo pattern
.Count();
// Get limit from strategy config (more permissive for 2-leg combos)
var maxComboPositions = GetComboPositionLimit(2);
if (activeTwoLegCombos >= maxComboPositions)
{
return ValidationResult.Blocked(
$"2-leg combo limit exceeded: {activeTwoLegCombos} active positions on {underlying.Value} (max: {maxComboPositions})");
}
((dynamic)_logger).Debug($"[2-LEG COMBO] Validation passed: {activeTwoLegCombos}/{maxComboPositions} positions");
return ValidationResult.Success();
}
/// <summary>
/// Validates generic combo orders with conservative portfolio-level limits
/// </summary>
private ValidationResult ValidateGenericCombo(List<QuantConnect.Orders.Leg> legs, int quantity,
List<KeyValuePair<Symbol, SecurityHolding>> existingPositions, OrderStructureAnalysis structure)
{
// Conservative validation for unknown combo patterns
// Focus on portfolio-level risk management
var underlying = legs.First().Symbol.Underlying;
var activeComboPositions = existingPositions
.Where(h => h.Key.SecurityType == SecurityType.Option &&
h.Key.Underlying == underlying)
.GroupBy(h => h.Key.ID.Date)
.Where(g => g.Count() >= structure.LegCount)
.Count();
// Use configuration-driven limit with conservative fallback
var maxComboPositions = GetComboPositionLimit(structure.LegCount);
if (activeComboPositions >= maxComboPositions)
{
return ValidationResult.Blocked(
$"{structure.LegCount}-leg combo limit exceeded: {activeComboPositions} active positions on {underlying.Value} (max: {maxComboPositions})");
}
((dynamic)_logger).Debug($"[GENERIC COMBO] Validation passed: {activeComboPositions}/{maxComboPositions} positions");
return ValidationResult.Success();
}
/// <summary>
/// Analyzes combo order structure without relying on strategy names
/// </summary>
private OrderStructureAnalysis AnalyzeOrderStructure(List<QuantConnect.Orders.Leg> legs)
{
var analysis = new OrderStructureAnalysis
{
LegCount = legs.Count,
PutCount = legs.Count(l => l.Symbol.SecurityType == SecurityType.Option && l.Symbol.ID.OptionRight == OptionRight.Put),
CallCount = legs.Count(l => l.Symbol.SecurityType == SecurityType.Option && l.Symbol.ID.OptionRight == OptionRight.Call),
EquityCount = legs.Count(l => l.Symbol.SecurityType == SecurityType.Equity),
HasOptions = legs.Any(l => l.Symbol.SecurityType == SecurityType.Option),
HasEquity = legs.Any(l => l.Symbol.SecurityType == SecurityType.Equity)
};
// Analyze strike relationships for pattern detection
if (analysis.HasOptions)
{
var strikes = legs.Where(l => l.Symbol.SecurityType == SecurityType.Option)
.Select(l => l.Symbol.ID.StrikePrice)
.OrderBy(s => s)
.ToList();
analysis.UniqueStrikes = strikes.Distinct().Count();
analysis.StrikeSpread = strikes.Count > 1 ? strikes.Max() - strikes.Min() : 0;
}
return analysis;
}
/// <summary>
/// Gets combo position limit from strategy configuration
/// </summary>
private int GetComboPositionLimit(int legCount)
{
try
{
// Try to get MaxPositionsPerCombo from current strategy configuration
var algorithm = _context.Algorithm;
if (algorithm != null)
{
// Access strategy config through algorithm if available
var maxComboPositions = algorithm.GetParameter("MaxPositionsPerCombo");
if (int.TryParse(maxComboPositions, out var configLimit))
{
return configLimit;
}
}
}
catch (Exception ex)
{
((dynamic)_logger).Debug($"[CONFIG] Could not read MaxPositionsPerCombo: {ex.Message}");
}
// Fallback limits based on leg count complexity
return legCount switch
{
4 => 1, // 4-leg combos (Iron Condor pattern) - most restrictive
3 => 2, // 3-leg combos - moderate
2 => 3, // 2-leg combos (Spreads) - more permissive
_ => 2 // Other patterns - moderate default
};
}
/// <summary>
/// Structure analysis result for combo orders
/// </summary>
private class OrderStructureAnalysis
{
public int LegCount { get; set; }
public int PutCount { get; set; }
public int CallCount { get; set; }
public int EquityCount { get; set; }
public bool HasOptions { get; set; }
public bool HasEquity { get; set; }
public int UniqueStrikes { get; set; }
public decimal StrikeSpread { get; set; }
}
/// <summary>
/// Adds a custom overlap rule to the validation pipeline
/// </summary>
public void AddRule(IPositionOverlapRule rule)
{
if (rule == null) throw new ArgumentNullException(nameof(rule));
_rules.Add(rule);
((dynamic)_logger).Debug($"[OVERLAP MANAGER] Added rule: {rule.GetType().Name}");
}
/// <summary>
/// Removes a rule from the validation pipeline
/// </summary>
public void RemoveRule<T>() where T : IPositionOverlapRule
{
var removed = _rules.RemoveAll(r => r is T);
if (removed > 0)
{
((dynamic)_logger).Debug($"[OVERLAP MANAGER] Removed {removed} rule(s) of type: {typeof(T).Name}");
}
}
/// <summary>
/// Gets summary of current overlap prevention configuration
/// </summary>
public string GetConfigurationSummary()
{
var summary = $"Position Overlap Manager - {_rules.Count} active rules:\n";
foreach (var rule in _rules)
{
summary += $" - {rule.GetType().Name}\n";
}
return summary;
}
/// <summary>
/// Initialize built-in overlap prevention rules
/// </summary>
private void InitializeBuiltInRules()
{
// Add core overlap prevention rules
AddRule(new UnderlyingConflictRule(_context));
AddRule(new CollateralValidationRule(_context));
AddRule(new StrikeOverlapRule(_context));
((dynamic)_logger).Info($"[OVERLAP MANAGER] Initialized with {_rules.Count} built-in rules");
}
/// <summary>
/// Gets all positions for a specific underlying symbol
/// </summary>
public List<SecurityHolding> GetPositionsForUnderlying(Symbol underlying)
{
return _context.Algorithm.Portfolio.Values
.Where(h => h.Invested &&
(h.Symbol == underlying ||
(h.Symbol.SecurityType == SecurityType.Option && h.Symbol.Underlying == underlying)))
.ToList();
}
/// <summary>
/// Checks if there are any active positions for a specific underlying
/// </summary>
public bool HasActivePositions(Symbol underlying)
{
return GetPositionsForUnderlying(underlying).Any();
}
/// <summary>
/// Gets count of active option positions for an underlying
/// </summary>
public int GetActiveOptionPositionCount(Symbol underlying)
{
return _context.Algorithm.Portfolio.Values
.Count(h => h.Invested &&
h.Symbol.SecurityType == SecurityType.Option &&
h.Symbol.Underlying == underlying);
}
/// <summary>
/// Calculates total margin requirement for all positions on an underlying
/// </summary>
public decimal GetTotalMarginRequirement(Symbol underlying)
{
try
{
var positions = GetPositionsForUnderlying(underlying);
return positions.Sum(p => Math.Abs(p.HoldingsValue * 0.2m)); // Simplified margin calculation
}
catch (Exception ex)
{
((dynamic)_logger).Warning($"Error calculating margin requirement: {ex.Message}");
return 0m;
}
}
}
}using System;
using QuantConnect;
using QuantConnect.Algorithm;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.Core.Services
{
/// <summary>
/// Simple implementation of IAlgorithmContext for SmartOrderManager
/// </summary>
public class SimpleAlgorithmContext : IAlgorithmContext
{
public QCAlgorithm Algorithm { get; }
public object Logger { get; }
public SimpleAlgorithmContext(QCAlgorithm algorithm, object logger)
{
Algorithm = algorithm ?? throw new ArgumentNullException(nameof(algorithm));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Orders;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Execution;
using CoreAlgo.Architecture.Core.Interfaces;
using CoreAlgo.Architecture.QC.Helpers;
namespace CoreAlgo.Architecture.Core.Services
{
/// <summary>
/// Manages smart orders with progressive pricing without requiring Algorithm Framework
/// Intercepts order placement and applies progressive limit pricing to improve fill rates
/// </summary>
public class SmartOrderManager
{
private readonly QCAlgorithm _algorithm;
private readonly IAlgorithmContext _context;
private readonly Dictionary<int, SmartOrderTracker> _activeOrders;
private readonly Dictionary<int, ComboOrderTracker> _activeComboOrders;
private readonly HashSet<ScheduledEvent> _scheduledEvents;
private ISmartPricingEngine _pricingEngine;
private PositionOverlapManager _overlapManager;
public SmartOrderManager(QCAlgorithm algorithm, IAlgorithmContext context)
{
_algorithm = algorithm ?? throw new ArgumentNullException(nameof(algorithm));
_context = context ?? throw new ArgumentNullException(nameof(context));
_activeOrders = new Dictionary<int, SmartOrderTracker>();
_activeComboOrders = new Dictionary<int, ComboOrderTracker>();
_scheduledEvents = new HashSet<ScheduledEvent>();
}
/// <summary>
/// Sets the pricing engine for smart order execution
/// </summary>
public void SetPricingEngine(ISmartPricingEngine pricingEngine)
{
_pricingEngine = pricingEngine;
}
/// <summary>
/// Sets the position overlap manager for order validation
/// </summary>
public void SetOverlapManager(PositionOverlapManager overlapManager)
{
_overlapManager = overlapManager;
}
/// <summary>
/// Places a smart market order that starts as a limit order at mid-spread
/// and progressively moves toward market price
/// </summary>
public OrderTicket SmartMarketOrder(Symbol symbol, decimal quantity, string tag = "")
{
// Validate position overlap before placing order
if (_overlapManager != null)
{
var validation = _overlapManager.ValidateNewPosition(symbol, quantity, tag);
if (!validation.IsValid)
{
((dynamic)_context.Logger).Warning($"[OVERLAP PREVENTION] Order blocked: {validation.Message}");
// Return null ticket to indicate order was blocked
// Strategies should check for null returns and handle appropriately
return null;
}
}
if (_pricingEngine == null)
{
// Fall back to regular market order if no pricing engine
return _algorithm.MarketOrder(symbol, quantity, tag: tag, asynchronous: true);
}
try
{
var security = _algorithm.Securities[symbol];
var quote = GetCurrentQuote(security);
if (quote == null || quote.Spread > 10m) // Skip if spread too wide
{
((dynamic)_context.Logger).Debug($"SmartOrder: Using market order for {symbol} due to wide spread or no quote");
return _algorithm.MarketOrder(symbol, quantity, tag: tag, asynchronous: true);
}
// Calculate initial limit price at mid-spread
var direction = quantity > 0 ? CoreAlgo.Architecture.Core.Execution.OrderDirection.Buy : CoreAlgo.Architecture.Core.Execution.OrderDirection.Sell;
var initialPrice = _pricingEngine.CalculateInitialPrice(quote, direction);
var roundedInitialPrice = PriceRounding.RoundLimitPrice(_algorithm.Securities, symbol, quantity, initialPrice);
// Place initial limit order
var ticket = _algorithm.LimitOrder(symbol, quantity, roundedInitialPrice, tag: tag + " [Smart]", asynchronous: true);
if (ticket == null || ticket.Status == OrderStatus.Invalid)
{
((dynamic)_context.Logger).Error($"SmartOrder: Failed to place initial limit order for {symbol}");
return _algorithm.MarketOrder(symbol, quantity, tag: tag, asynchronous: true);
}
// Create tracker for progressive pricing
var tracker = new SmartOrderTracker(ticket, quote, direction, SmartPricingMode.Normal, roundedInitialPrice);
_activeOrders[ticket.OrderId] = tracker;
((dynamic)_context.Logger).Info($"SmartOrder: Placed initial limit order for {symbol} at ${initialPrice:F2} " +
$"(Mid: ${quote.Price:F2}, Spread: ${quote.Spread:F2})");
// Schedule first price update
ScheduleNextPricingUpdate(tracker);
return ticket;
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"SmartOrder error: {ex.Message}");
// Fall back to market order on any error
return _algorithm.MarketOrder(symbol, quantity, tag: tag, asynchronous: true);
}
}
/// <summary>
/// Places a smart combo limit order for multi-leg options with progressive net pricing
/// Uses QC's native ComboLimitOrder with intelligent net price calculation and updates
/// </summary>
public List<OrderTicket> SmartComboMarketOrder(List<Leg> legs, int quantity, string tag = "")
{
// Validate combo order as atomic unit (QC-First approach)
if (_overlapManager != null)
{
var validation = _overlapManager.ValidateComboOrder(legs, quantity, tag);
if (!validation.IsValid)
{
((dynamic)_context.Logger).Warning($"[COMBO ORDER BLOCKED] {validation.Message}");
return new List<OrderTicket>(); // Return empty list to indicate order was blocked
}
((dynamic)_context.Logger).Debug($"[COMBO ORDER APPROVED] {legs.Count}-leg order validated for {tag}");
}
// If no pricing engine, fall back to basic combo market order
if (_pricingEngine == null || _pricingEngine.Mode == SmartPricingMode.Off)
{
((dynamic)_context.Logger).Debug($"SmartCombo: Using basic combo market order (no smart pricing)");
return _algorithm.ComboMarketOrder(legs, quantity, tag: tag, asynchronous: true);
}
try
{
// Get current market quotes for the combo
var comboQuote = ComboQuote.FromSecurities(legs, _algorithm.Securities);
if (comboQuote == null)
{
((dynamic)_context.Logger).Warning($"SmartCombo: No valid quotes available for combo, using market order");
return _algorithm.ComboMarketOrder(legs, quantity, tag: tag, asynchronous: true);
}
// Determine combo direction (buy = net debit, sell = net credit)
var comboDirection = ComboPricingEngine.DetermineComboDirection(legs);
// Check if we should attempt smart pricing
if (!_pricingEngine.ShouldAttemptComboPricing(comboQuote, comboDirection))
{
((dynamic)_context.Logger).Debug($"SmartCombo: Conditions not suitable for smart pricing, using market order");
return _algorithm.ComboMarketOrder(legs, quantity, tag: tag, asynchronous: true);
}
// Calculate initial net limit price
var initialNetPrice = _pricingEngine.CalculateInitialComboPrice(legs, comboQuote, comboDirection);
if (!initialNetPrice.HasValue)
{
((dynamic)_context.Logger).Debug($"SmartCombo: Could not calculate initial price, using market order");
return _algorithm.ComboMarketOrder(legs, quantity, tag: tag, asynchronous: true);
}
// Place initial combo limit order with calculated net price
// Note: ComboLimitOrder may not support asynchronous parameter in C#
var comboTickets = _algorithm.ComboLimitOrder(legs, quantity, initialNetPrice.Value, tag: tag + " [SmartCombo]", asynchronous: true);
if (comboTickets == null || comboTickets.Count == 0)
{
((dynamic)_context.Logger).Error($"SmartCombo: Failed to place combo limit order, trying market order fallback");
return _algorithm.ComboMarketOrder(legs, quantity, tag: tag, asynchronous: true);
}
// Create combo tracker for progressive pricing
var roundedInitialNetPrice = PriceRounding.RoundComboNetPrice(
_algorithm.Securities,
legs,
initialNetPrice.Value,
comboDirection == CoreAlgo.Architecture.Core.Execution.OrderDirection.Buy);
// Update initial ticket to rounded net price if it differs
if (Math.Abs(roundedInitialNetPrice - initialNetPrice.Value) > 0m)
{
foreach (var comboTicket in comboTickets)
{
comboTicket.Update(new UpdateOrderFields { LimitPrice = roundedInitialNetPrice });
}
}
var comboTracker = new ComboOrderTracker(comboTickets, legs, comboQuote,
comboDirection, _pricingEngine.Mode, roundedInitialNetPrice);
// Track using primary order ID
_activeComboOrders[comboTracker.PrimaryOrderId] = comboTracker;
((dynamic)_context.Logger).Info($"SmartCombo: Placed {legs.Count}-leg combo limit order " +
$"at net price ${roundedInitialNetPrice:F2} " +
$"(NetBid: ${comboQuote.NetBid:F2}, NetAsk: ${comboQuote.NetAsk:F2}, " +
$"NetMid: ${comboQuote.NetMid:F2}, Direction: {comboDirection})");
// Schedule first pricing update
ScheduleNextComboPricingUpdate(comboTracker);
return comboTickets;
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"SmartCombo error: {ex.Message}");
// Fall back to basic combo market order on any error
return _algorithm.ComboMarketOrder(legs, quantity, tag: tag, asynchronous: true);
}
}
/// <summary>
/// Handles order events to track fills and update order state
/// </summary>
public void OnOrderEvent(OrderEvent orderEvent)
{
// Handle single-leg orders
if (_activeOrders.TryGetValue(orderEvent.OrderId, out var tracker))
{
switch (orderEvent.Status)
{
case OrderStatus.Filled:
((dynamic)_context.Logger).Info($"SmartOrder: Order {orderEvent.OrderId} filled at ${orderEvent.FillPrice:F2} " +
$"after {tracker.AttemptNumber} attempts");
CleanupOrder(tracker);
break;
case OrderStatus.PartiallyFilled:
((dynamic)_context.Logger).Debug($"SmartOrder: Order {orderEvent.OrderId} partially filled " +
$"({orderEvent.FillQuantity}/{tracker.OrderTicket.Quantity})");
tracker.UpdatePartialFill(orderEvent);
break;
case OrderStatus.Canceled:
case OrderStatus.Invalid:
((dynamic)_context.Logger).Warning($"SmartOrder: Order {orderEvent.OrderId} {orderEvent.Status}");
CleanupOrder(tracker);
break;
}
return;
}
// Handle combo orders - check if this order event belongs to any tracked combo
foreach (var comboTracker in _activeComboOrders.Values)
{
var matchingTicket = comboTracker.ComboTickets.FirstOrDefault(t => t.OrderId == orderEvent.OrderId);
if (matchingTicket != null)
{
HandleComboOrderEvent(comboTracker, orderEvent, matchingTicket);
return;
}
}
}
/// <summary>
/// Handles order events for combo orders
/// </summary>
private void HandleComboOrderEvent(ComboOrderTracker comboTracker, OrderEvent orderEvent, OrderTicket matchingTicket)
{
switch (orderEvent.Status)
{
case OrderStatus.Filled:
((dynamic)_context.Logger).Debug($"SmartCombo: Leg {orderEvent.OrderId} of combo {comboTracker.PrimaryOrderId} " +
$"filled at ${orderEvent.FillPrice:F2}");
// Check if entire combo is now filled
if (comboTracker.IsCompletelyFilled)
{
((dynamic)_context.Logger).Info($"SmartCombo: Combo order {comboTracker.PrimaryOrderId} completely filled " +
$"after {comboTracker.AttemptNumber} pricing attempts");
CleanupComboOrder(comboTracker);
}
break;
case OrderStatus.PartiallyFilled:
((dynamic)_context.Logger).Debug($"SmartCombo: Leg {orderEvent.OrderId} of combo {comboTracker.PrimaryOrderId} " +
$"partially filled ({orderEvent.FillQuantity}/{matchingTicket.Quantity})");
comboTracker.UpdatePartialFill(orderEvent);
break;
case OrderStatus.Canceled:
case OrderStatus.Invalid:
((dynamic)_context.Logger).Warning($"SmartCombo: Leg {orderEvent.OrderId} of combo {comboTracker.PrimaryOrderId} {orderEvent.Status}");
// If any leg fails, the entire combo fails
((dynamic)_context.Logger).Warning($"SmartCombo: Combo order {comboTracker.PrimaryOrderId} failed due to leg {orderEvent.OrderId}");
CleanupComboOrder(comboTracker);
break;
}
}
/// <summary>
/// Updates the price of an active order using progressive pricing
/// </summary>
private void UpdateOrderPrice(SmartOrderTracker tracker)
{
try
{
// Remove the scheduled event
if (tracker.ScheduledEvent != null)
{
_scheduledEvents.Remove(tracker.ScheduledEvent);
tracker.ScheduledEvent = null;
}
// Check if order is still active
if (!_activeOrders.ContainsKey(tracker.OrderTicket.OrderId) ||
tracker.OrderTicket.Status == OrderStatus.Filled)
{
return;
}
// Check if we've exceeded max attempts
if (tracker.AttemptNumber >= _pricingEngine.GetMaxAttempts())
{
((dynamic)_context.Logger).Info($"SmartOrder: Max attempts reached for order {tracker.OrderTicket.OrderId}, " +
"converting to market order");
// Cancel limit order and place market order
tracker.OrderTicket.Cancel("Max pricing attempts reached");
// Place market order for remaining quantity
var remainingQty = tracker.OrderTicket.Quantity - tracker.OrderTicket.QuantityFilled;
if (remainingQty != 0)
{
_algorithm.MarketOrder(tracker.OrderTicket.Symbol, remainingQty,
tag: tracker.OrderTicket.Tag + " [Smart-Market]", asynchronous: true);
}
CleanupOrder(tracker);
return;
}
// Get current market quote
var security = _algorithm.Securities[tracker.OrderTicket.Symbol];
var currentQuote = GetCurrentQuote(security);
if (currentQuote == null)
{
((dynamic)_context.Logger).Warning($"SmartOrder: No quote available for {tracker.OrderTicket.Symbol}");
// Keep the order but don't update - try again next time
ScheduleNextPricingUpdate(tracker);
return;
}
// Calculate next price
tracker.AttemptNumber++;
var nextPrice = _pricingEngine.CalculateNextPrice(
tracker.CurrentPrice, currentQuote, tracker.OrderDirection, tracker.AttemptNumber);
if (nextPrice.HasValue && Math.Abs(nextPrice.Value - tracker.CurrentPrice) > 0.01m)
{
var roundedNextPrice = PriceRounding.RoundLimitPrice(
_algorithm.Securities,
tracker.OrderTicket.Symbol,
tracker.OrderTicket.Quantity,
nextPrice.Value);
// Update order price
var updateFields = new UpdateOrderFields { LimitPrice = roundedNextPrice };
var response = tracker.OrderTicket.Update(updateFields);
if (response.IsSuccess)
{
tracker.CurrentPrice = roundedNextPrice;
((dynamic)_context.Logger).Debug($"SmartOrder: Updated order {tracker.OrderTicket.OrderId} " +
$"price to ${roundedNextPrice:F2} (attempt {tracker.AttemptNumber})");
}
else
{
((dynamic)_context.Logger).Warning($"SmartOrder: Failed to update order price: {response.ErrorMessage}");
}
}
// Schedule next update
ScheduleNextPricingUpdate(tracker);
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"SmartOrder update error: {ex.Message}");
CleanupOrder(tracker);
}
}
/// <summary>
/// Schedules the next pricing update for an order
/// </summary>
private void ScheduleNextPricingUpdate(SmartOrderTracker tracker)
{
var interval = _pricingEngine.GetPricingInterval();
var updateTime = _algorithm.Time.Add(interval);
var scheduledEvent = _algorithm.Schedule.On(
_algorithm.DateRules.On(updateTime.Date),
_algorithm.TimeRules.At(updateTime.Hour, updateTime.Minute, updateTime.Second),
() => UpdateOrderPrice(tracker));
_scheduledEvents.Add(scheduledEvent);
tracker.ScheduledEvent = scheduledEvent;
}
/// <summary>
/// Cleans up an order and removes associated scheduled events
/// </summary>
private void CleanupOrder(SmartOrderTracker tracker)
{
_activeOrders.Remove(tracker.OrderTicket.OrderId);
if (tracker.ScheduledEvent != null)
{
_algorithm.Schedule.Remove(tracker.ScheduledEvent);
_scheduledEvents.Remove(tracker.ScheduledEvent);
}
}
/// <summary>
/// Gets the current quote for a security
/// </summary>
private Quote GetCurrentQuote(Security security)
{
if (security.BidPrice == 0 || security.AskPrice == 0)
return null;
return new Quote(security.BidPrice, security.AskPrice);
}
/// <summary>
/// Schedules the next pricing update for a combo order
/// </summary>
private void ScheduleNextComboPricingUpdate(ComboOrderTracker tracker)
{
var interval = _pricingEngine.GetPricingInterval();
var updateTime = _algorithm.Time.Add(interval);
var scheduledEvent = _algorithm.Schedule.On(_algorithm.DateRules.On(updateTime.Date),
_algorithm.TimeRules.At(updateTime.Hour, updateTime.Minute, updateTime.Second),
() => UpdateComboOrderPrice(tracker));
_scheduledEvents.Add(scheduledEvent);
tracker.ScheduledEvent = scheduledEvent;
}
/// <summary>
/// Updates the net price of an active combo order using progressive pricing
/// </summary>
private void UpdateComboOrderPrice(ComboOrderTracker tracker)
{
try
{
// Remove the scheduled event
if (tracker.ScheduledEvent != null)
{
_scheduledEvents.Remove(tracker.ScheduledEvent);
tracker.ScheduledEvent = null;
}
// Check if combo is still active
if (!_activeComboOrders.ContainsKey(tracker.PrimaryOrderId) || tracker.IsCompletelyFilled)
{
return;
}
// Check if we should continue pricing
var maxAttempts = _pricingEngine.GetMaxAttempts();
var maxRuntime = TimeSpan.FromMinutes(5); // Max 5 minutes for combo orders
if (!tracker.ShouldContinuePricing(maxAttempts, maxRuntime))
{
((dynamic)_context.Logger).Debug($"SmartCombo: Stopping progressive pricing for combo {tracker.PrimaryOrderId} " +
$"(attempts: {tracker.AttemptNumber}/{maxAttempts}, runtime: {tracker.GetRuntime().TotalSeconds:F0}s)");
CleanupComboOrder(tracker);
return;
}
// Get current combo quote
var currentComboQuote = ComboQuote.FromSecurities(tracker.Legs, _algorithm.Securities);
if (currentComboQuote == null)
{
((dynamic)_context.Logger).Warning($"SmartCombo: No quotes available for combo {tracker.PrimaryOrderId}, stopping updates");
CleanupComboOrder(tracker);
return;
}
// Calculate next net price
var nextNetPrice = _pricingEngine.CalculateNextComboPrice(
tracker.CurrentNetPrice, currentComboQuote, tracker.ComboDirection, tracker.AttemptNumber + 1);
if (nextNetPrice.HasValue)
{
// Update all combo order tickets with the new net price
// QC handles the individual leg price distribution automatically
var updateSuccess = true;
foreach (var ticket in tracker.ComboTickets)
{
if (ticket.Status == OrderStatus.Submitted || ticket.Status == OrderStatus.PartiallyFilled)
{
// Note: For combo orders, we update the primary ticket's limit price
// QC automatically adjusts the other legs proportionally
var result = ticket.Update(new UpdateOrderFields { LimitPrice = nextNetPrice.Value });
if (!result.IsSuccess)
{
((dynamic)_context.Logger).Warning($"SmartCombo: Failed to update combo ticket {ticket.OrderId}: {result.ErrorMessage}");
updateSuccess = false;
}
}
}
if (updateSuccess)
{
var isDebit = tracker.ComboDirection == CoreAlgo.Architecture.Core.Execution.OrderDirection.Buy;
var roundedNetPrice = PriceRounding.RoundComboNetPrice(_algorithm.Securities, tracker.Legs, nextNetPrice.Value, isDebit);
if (Math.Abs(roundedNetPrice - nextNetPrice.Value) > 0m)
{
foreach (var ticket in tracker.ComboTickets)
{
if (ticket.Status == OrderStatus.Submitted || ticket.Status == OrderStatus.PartiallyFilled)
{
ticket.Update(new UpdateOrderFields { LimitPrice = roundedNetPrice });
}
}
}
tracker.UpdateNetPrice(roundedNetPrice, currentComboQuote);
((dynamic)_context.Logger).Debug($"SmartCombo: Updated combo {tracker.PrimaryOrderId} " +
$"to net price ${nextNetPrice.Value:F2} (attempt {tracker.AttemptNumber}) " +
$"NetMid: ${currentComboQuote.NetMid:F2}");
// Schedule next update if we haven't reached max attempts
if (tracker.AttemptNumber < maxAttempts)
{
ScheduleNextComboPricingUpdate(tracker);
}
else
{
((dynamic)_context.Logger).Debug($"SmartCombo: Reached max attempts for combo {tracker.PrimaryOrderId}");
}
}
else
{
((dynamic)_context.Logger).Warning($"SmartCombo: Failed to update combo order, stopping progressive pricing");
CleanupComboOrder(tracker);
}
}
else
{
((dynamic)_context.Logger).Debug($"SmartCombo: No more price improvements for combo {tracker.PrimaryOrderId}");
CleanupComboOrder(tracker);
}
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"SmartCombo update error for combo {tracker.PrimaryOrderId}: {ex.Message}");
CleanupComboOrder(tracker);
}
}
/// <summary>
/// Cleans up a combo order and removes associated scheduled events
/// </summary>
private void CleanupComboOrder(ComboOrderTracker tracker)
{
_activeComboOrders.Remove(tracker.PrimaryOrderId);
if (tracker.ScheduledEvent != null)
{
_algorithm.Schedule.Remove(tracker.ScheduledEvent);
_scheduledEvents.Remove(tracker.ScheduledEvent);
}
}
/// <summary>
/// Gets the mode for the pricing engine (for logging)
/// </summary>
public string GetPricingMode()
{
return _pricingEngine?.GetType().Name.Replace("PricingStrategy", "") ?? "Off";
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using CoreAlgo.Architecture.Core.Interfaces;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Architecture.Core.Services
{
/// <summary>
/// Utility class for discovering strategy implementations through reflection
/// Eliminates hardcoded switch statements by dynamically finding Templates
/// </summary>
public static class StrategyDiscovery
{
private static readonly Dictionary<string, Type> _strategyTypes = new Dictionary<string, Type>();
private static readonly Dictionary<string, Type> _configTypes = new Dictionary<string, Type>();
private static readonly object _lock = new object();
private static bool _initialized = false;
/// <summary>
/// Initialize discovery cache by scanning Templates namespace
/// </summary>
private static void EnsureInitialized()
{
if (_initialized) return;
lock (_lock)
{
if (_initialized) return;
// Discover all IStrategy implementations in Templates namespace
var assembly = Assembly.GetExecutingAssembly();
var strategyTypes = assembly.GetTypes()
.Where(t => typeof(IStrategy).IsAssignableFrom(t) &&
!t.IsInterface &&
!t.IsAbstract &&
t.Namespace == "CoreAlgo.Architecture.Core.Templates")
.ToList();
// Build strategy name mappings
foreach (var type in strategyTypes)
{
var strategyName = GetStrategyNameFromType(type);
_strategyTypes[strategyName.ToUpperInvariant()] = type;
// Find corresponding config type
var configType = FindConfigType(strategyName);
if (configType != null)
{
_configTypes[strategyName.ToUpperInvariant()] = configType;
}
}
_initialized = true;
}
}
/// <summary>
/// Extract strategy name from template class name
/// IronCondorTemplate -> IronCondor
/// </summary>
private static string GetStrategyNameFromType(Type type)
{
var name = type.Name;
return name.EndsWith("Template") ? name.Substring(0, name.Length - 8) : name;
}
/// <summary>
/// Find config type by naming convention
/// IronCondor -> IronCondorConfig
/// </summary>
private static Type FindConfigType(string strategyName)
{
var configTypeName = $"{strategyName}Config";
var assembly = Assembly.GetExecutingAssembly();
return assembly.GetTypes()
.FirstOrDefault(t => t.Name == configTypeName &&
typeof(StrategyConfig).IsAssignableFrom(t));
}
/// <summary>
/// Get all discovered strategy names
/// </summary>
public static IEnumerable<string> GetAllStrategyNames()
{
EnsureInitialized();
return _strategyTypes.Keys.Select(k => k.ToLowerInvariant());
}
/// <summary>
/// Get strategy type by name
/// </summary>
public static Type GetStrategyType(string strategyName)
{
EnsureInitialized();
_strategyTypes.TryGetValue(strategyName.ToUpperInvariant(), out var type);
return type;
}
/// <summary>
/// Get config type by strategy name
/// </summary>
public static Type GetConfigType(string strategyName)
{
EnsureInitialized();
_configTypes.TryGetValue(strategyName.ToUpperInvariant(), out var type);
return type;
}
/// <summary>
/// Create strategy instance by name
/// </summary>
public static IStrategy CreateStrategy(string strategyName)
{
var strategyType = GetStrategyType(strategyName);
if (strategyType == null)
throw new ArgumentException($"Unknown strategy '{strategyName}'. Available strategies: {string.Join(", ", GetAllStrategyNames())}");
return (IStrategy)Activator.CreateInstance(strategyType);
}
/// <summary>
/// Check if strategy exists
/// </summary>
public static bool StrategyExists(string strategyName)
{
return GetStrategyType(strategyName) != null;
}
}
}#region imports
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Implementations;
#endregion
namespace CoreAlgo.Architecture.Core.Services
{
/// <summary>
/// Formatter for trade highlight log messages with ASCII-only single-line outputs
/// Provides Entry/Exit formatting with metric suffix support
/// </summary>
public static class TradeHighlightFormatter
{
/// <summary>
/// Build entry highlight message
/// Format: ==> ENTRY AMD 2025-10-14 10:25 @219.95 x153 | SL 219.17 | TP 220.57 | O:1
/// </summary>
public static string BuildEntry(
string symbol,
DateTime time,
decimal entryPrice,
int quantity,
decimal? stopLoss = null,
decimal? takeProfit = null,
int? orderId = null)
{
var parts = new List<string>
{
$"==> ENTRY {symbol} {time:yyyy-MM-dd HH:mm} @{entryPrice:F2} x{quantity}"
};
if (stopLoss.HasValue)
parts.Add($"SL {stopLoss.Value:F2}");
if (takeProfit.HasValue)
parts.Add($"TP {takeProfit.Value:F2}");
if (orderId.HasValue)
parts.Add($"O:{orderId.Value}");
return string.Join(" | ", parts);
}
/// <summary>
/// Build exit highlight message (TP/SL/TS)
/// TP Format: <== TP AMD 2025-10-14 10:29 @220.58 | PnL +96.39 | O:2
/// SL Format: !! SL AMD 2025-10-14 10:30 @221.04 | PnL -133.45 | O:3
/// TS Format: ~~> TS AMD 2025-10-14 10:30 @221.04 | PnL -42.10 | O:4
/// </summary>
public static string BuildExit(
string symbol,
DateTime time,
decimal exitPrice,
decimal pnl,
ExitType exitType,
int? orderId = null)
{
var prefix = exitType switch
{
ExitType.TakeProfit => "<== TP",
ExitType.StopLoss => "!! SL",
ExitType.TrailingStop => "~~> TS",
ExitType.Cancel => "X CANCEL",
_ => "<==" // fallback
};
var pnlSign = pnl >= 0 ? "+" : "";
var parts = new List<string>
{
$"{prefix} {symbol} {time:yyyy-MM-dd HH:mm} @{exitPrice:F2}",
$"PnL {pnlSign}{pnl:F2}"
};
if (orderId.HasValue)
parts.Add($"O:{orderId.Value}");
return string.Join(" | ", parts);
}
/// <summary>
/// Format metrics dictionary into bracketed suffix: [k=v, k=v]
/// </summary>
public static string FormatMetricsSuffix(IDictionary<string, string> metrics)
{
if (metrics == null || metrics.Count == 0)
return string.Empty;
// Sort keys alphabetically for consistent output
var sorted = metrics.OrderBy(kvp => kvp.Key).ToList();
// Build comma-separated key=value pairs
var pairs = sorted.Select(kvp => $"{kvp.Key}={kvp.Value}");
var content = string.Join(", ", pairs);
// Truncate if too long (cap at 120 chars total)
if (content.Length > 120)
content = content.Substring(0, 117) + "...";
return $"[{content}]";
}
/// <summary>
/// Format per-bar trade tracking summary (single line, flexible detail level)
/// Format: [TT] E123 @10:25:00 | Price:219.95 | Entry:219.50@10:20 | High:220.10 | UnrealPnL:+45.00 | Trail:ARMED | Stop:219.50 | SL:O456 Stop:219.20 Limit:219.15 [TRIGGERED] | Bid:219.94 Ask:220.00
/// </summary>
public static string FormatTradeTracking(
int entryId,
DateTime time,
decimal currentPrice,
TradeRecord trade,
decimal highestPriceSinceEntry,
bool trailingArmed,
decimal currentStopPrice,
OrderTicket slTicket = null,
bool isLongPosition = true,
bool includeQuotes = false,
Security security = null)
{
var parts = new List<string>
{
$"[TT] E{entryId} @{time:HH:mm:ss}",
$"Price:{currentPrice:F2}",
$"Entry:{trade.OpenPrice:F2}@{trade.OpenTime:HH:mm:ss}",
$"High:{highestPriceSinceEntry:F2}"
};
var unrealizedPnL = (currentPrice - trade.OpenPrice) * trade.Quantity;
var pnlSign = unrealizedPnL >= 0 ? "+" : "";
parts.Add($"UnrealPnL:{pnlSign}{unrealizedPnL:F2}");
parts.Add($"Trail:{(trailingArmed ? "ARMED" : "DISARMED")}");
if (currentStopPrice > 0)
{
parts.Add($"Stop:{currentStopPrice:F2}");
}
// Add SL ticket context if available
if (slTicket != null)
{
var stopPrice = slTicket.Get(OrderField.StopPrice);
var limitPrice = slTicket.Get(OrderField.LimitPrice);
var ticketParts = new List<string> { $"SL:O{slTicket.OrderId}" };
if (stopPrice != 0)
{
ticketParts.Add($"Stop:{stopPrice:F2}");
// Predict if stop is triggered
bool stopTriggered = isLongPosition
? currentPrice <= stopPrice
: currentPrice >= stopPrice;
if (stopTriggered && slTicket.Status == OrderStatus.Submitted)
{
ticketParts.Add("[TRIGGERED]");
}
}
if (limitPrice != 0)
{
ticketParts.Add($"Limit:{limitPrice:F2}");
if (stopPrice != 0)
{
var distanceToLimit = Math.Abs(limitPrice - currentPrice);
ticketParts.Add($"Dist:{distanceToLimit:F4}");
}
}
ticketParts.Add($"Status:{slTicket.Status}");
parts.Add(string.Join(" ", ticketParts));
}
// Add quotes if requested and available
if (includeQuotes && security != null)
{
var bid = security.BidPrice;
var ask = security.AskPrice;
if (bid > 0 && ask > 0)
{
var spread = ask - bid;
parts.Add($"Bid:{bid:F2} Ask:{ask:F2} Spread:{spread:F4}");
}
}
return string.Join(" | ", parts);
}
/// <summary>
/// Format entry fill details (single line)
/// Format: [TT ENTRY FILL] E123 @10:25:00 | Fill:219.95 Qty:153 | StopDist:0.45 TpDist:0.40 ATR:0.50 Adapt:1.2 | SL:219.50 Limit:219.45 Buffer:0.05 Tick:0.01 | OpenTrades:5 | Bid:219.94 Ask:220.00
/// </summary>
public static string FormatEntryFill(
int entryId,
DateTime time,
decimal fillPrice,
int quantity,
decimal stopDist,
decimal tpDist,
decimal atrAtEntry,
decimal adaptFactor,
decimal slPrice,
decimal slLimitPrice,
decimal stopLimitBuffer,
decimal tick,
int openTradesBefore,
bool includeQuotes = false,
Security security = null)
{
var parts = new List<string>
{
$"[TT ENTRY FILL] E{entryId} @{time:HH:mm:ss}",
$"Fill:{fillPrice:F2} Qty:{quantity}",
$"StopDist:{stopDist:F4} TpDist:{tpDist:F4} ATR:{atrAtEntry:F4} Adapt:{adaptFactor:F4}",
$"SL:{slPrice:F2} Limit:{slLimitPrice:F2} Buffer:{stopLimitBuffer:F4} Tick:{tick:F4}",
$"OpenTrades:{openTradesBefore}"
};
if (includeQuotes && security != null)
{
var bid = security.BidPrice;
var ask = security.AskPrice;
if (bid > 0 && ask > 0)
{
parts.Add($"Bid:{bid:F2} Ask:{ask:F2}");
}
}
return string.Join(" | ", parts);
}
/// <summary>
/// Format exit fill details (single line)
/// Format: [TT EXIT FILL] E123 @10:30:00 Reason:TrailHit | Entry:219.50@10:20 Exit:220.10 PnL:+60.00 | Trail:ARMED High:220.15 Stop:219.50 | OpenTrades:4→3 | SL:O456 Stop:219.50 Limit:219.45 | Bid:220.09 Ask:220.11
/// </summary>
public static string FormatExitFill(
int entryId,
DateTime time,
string exitReason,
decimal entryPrice,
DateTime entryTime,
decimal exitPrice,
decimal pnl,
bool trailingArmed,
decimal highestPriceSinceEntry,
decimal currentStopPrice,
int openTradesBefore,
OrderTicket slTicket = null,
bool isLongPosition = true,
bool includeQuotes = false,
Security security = null)
{
var pnlSign = pnl >= 0 ? "+" : "";
var parts = new List<string>
{
$"[TT EXIT FILL] E{entryId} @{time:HH:mm:ss} Reason:{exitReason}",
$"Entry:{entryPrice:F2}@{entryTime:HH:mm:ss} Exit:{exitPrice:F2} PnL:{pnlSign}{pnl:F2}",
$"Trail:{(trailingArmed ? "ARMED" : "DISARMED")} High:{highestPriceSinceEntry:F2} Stop:{currentStopPrice:F2}",
$"OpenTrades:{openTradesBefore}→{openTradesBefore - 1}"
};
if (slTicket != null)
{
var stopPrice = slTicket.Get(OrderField.StopPrice);
var limitPrice = slTicket.Get(OrderField.LimitPrice);
var ticketParts = new List<string> { $"SL:O{slTicket.OrderId}" };
if (stopPrice != 0) ticketParts.Add($"Stop:{stopPrice:F2}");
if (limitPrice != 0) ticketParts.Add($"Limit:{limitPrice:F2}");
ticketParts.Add($"Status:{slTicket.Status}");
parts.Add(string.Join(" ", ticketParts));
}
if (includeQuotes && security != null)
{
var bid = security.BidPrice;
var ask = security.AskPrice;
if (bid > 0 && ask > 0)
{
parts.Add($"Bid:{bid:F2} Ask:{ask:F2}");
}
}
return string.Join(" | ", parts);
}
/// <summary>
/// Format EOD flatten trade snapshot (single line per trade)
/// Format: [TT EOD PRE-FLATTEN] E123 @15:45:00 | Entry:219.50@10:20 Current:220.10 UnrealPnL:+60.00 | Trail:ARMED High:220.15 Stop:219.50 | SL:O456 Stop:219.50 Limit:219.45 [TRIGGERED] | Bid:220.09 Ask:220.11
/// </summary>
public static string FormatEodFlattenTrade(
int entryId,
DateTime time,
TradeRecord trade,
decimal currentPrice,
decimal unrealizedPnL,
bool trailingArmed,
decimal highestPriceSinceEntry,
decimal currentStopPrice,
OrderTicket slTicket = null,
bool isLongPosition = true,
bool includeQuotes = false,
Security security = null)
{
var pnlSign = unrealizedPnL >= 0 ? "+" : "";
var parts = new List<string>
{
$"[TT EOD PRE-FLATTEN] E{entryId} @{time:HH:mm:ss}",
$"Entry:{trade.OpenPrice:F2}@{trade.OpenTime:HH:mm:ss} Current:{currentPrice:F2} UnrealPnL:{pnlSign}{unrealizedPnL:F2}",
$"Trail:{(trailingArmed ? "ARMED" : "DISARMED")} High:{highestPriceSinceEntry:F2} Stop:{currentStopPrice:F2}"
};
if (slTicket != null)
{
var stopPrice = slTicket.Get(OrderField.StopPrice);
var limitPrice = slTicket.Get(OrderField.LimitPrice);
var ticketParts = new List<string> { $"SL:O{slTicket.OrderId}" };
if (stopPrice != 0)
{
ticketParts.Add($"Stop:{stopPrice:F2}");
bool stopTriggered = isLongPosition
? currentPrice <= stopPrice
: currentPrice >= stopPrice;
if (stopTriggered)
{
ticketParts.Add("[TRIGGERED]");
}
}
if (limitPrice != 0) ticketParts.Add($"Limit:{limitPrice:F2}");
ticketParts.Add($"Status:{slTicket.Status}");
parts.Add(string.Join(" ", ticketParts));
}
if (includeQuotes && security != null)
{
var bid = security.BidPrice;
var ask = security.AskPrice;
if (bid > 0 && ask > 0)
{
parts.Add($"Bid:{bid:F2} Ask:{ask:F2}");
}
}
return string.Join(" | ", parts);
}
/// <summary>
/// Format SL order created event (single line)
/// Format: [TT SL CREATED] E123 | SL OrderId:456 Status:Submitted | Stop:219.50 Limit:219.45 | Fill:219.95
/// </summary>
public static string FormatSlCreated(
int entryId,
int slOrderId,
OrderStatus status,
decimal stopPrice,
decimal limitPrice,
decimal fillPrice,
bool isLongPosition)
{
var parts = new List<string>
{
$"[TT SL CREATED] E{entryId}",
$"SL OrderId:{slOrderId} Status:{status}",
$"Stop:{stopPrice:F2} Limit:{limitPrice:F2}",
$"Fill:{fillPrice:F2}"
};
return string.Join(" | ", parts);
}
}
/// <summary>
/// Exit type for trade highlights
/// </summary>
public enum ExitType
{
TakeProfit,
StopLoss,
TrailingStop,
Cancel
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using CoreAlgo.Architecture.Core.Interfaces;
using CoreAlgo.Architecture.Core.Implementations;
namespace CoreAlgo.Architecture.Core.Services
{
/// <summary>
/// Handles persistence of trade records to QuantConnect ObjectStore
/// Provides simple load/save functionality for resuming algorithm state
/// </summary>
public class TradePersistenceService
{
private readonly IAlgorithmContext _context;
private const string CurrentPositionsKey = "positions/current.json";
private const string SnapshotKeyPrefix = "positions/snapshots/";
private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public TradePersistenceService(IAlgorithmContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
/// <summary>
/// Saves current trade state to ObjectStore
/// Only persists open and working trades (excludes closed/cancelled to minimize storage)
/// </summary>
public void SaveTrades(TradeTracker tracker)
{
if (tracker == null) return;
try
{
// Only persist open and working trades to minimize ObjectStore usage
var openAndWorking = tracker.WorkingTrades.Concat(tracker.OpenTrades).ToList();
var book = new TradeBook
{
Version = "1.0",
AsOfUtc = _context.Algorithm.UtcTime,
AllTrades = openAndWorking,
WorkingTrades = tracker.WorkingTrades.Select(t => t.OrderId).ToList(),
OpenTrades = tracker.OpenTrades.Select(t => t.OrderId).ToList(),
ClosedTrades = new List<string>() // Don't persist closed trades
};
var json = JsonSerializer.Serialize(book, SerializerOptions);
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
_context.Algorithm.ObjectStore.SaveBytes(CurrentPositionsKey, bytes);
((dynamic)_context.Logger).Debug($"TradePersistence: Saved {openAndWorking.Count} open/working trades to ObjectStore (excluded {tracker.ClosedTrades.Count} closed)");
}
catch (Exception ex)
{
// Never break trading due to persistence errors
((dynamic)_context.Logger).Error($"TradePersistence: Error saving trades: {ex.Message}");
}
}
/// <summary>
/// Clears current positions from ObjectStore if running in backtest mode
/// Live mode is protected - positions are preserved for warm restarts
/// </summary>
public void ClearPositionsIfBacktest()
{
if (_context.Algorithm.LiveMode)
{
return;
}
if (_context.Algorithm.ObjectStore.ContainsKey(CurrentPositionsKey))
{
_context.Algorithm.ObjectStore.Delete(CurrentPositionsKey);
((dynamic)_context.Logger).Info("TradePersistence: Cleared current positions for backtest");
}
}
/// <summary>
/// Clears all daily snapshots from ObjectStore if running in backtest mode
/// This reclaims space from previous backtest runs without adding retention complexity
/// Live mode is protected - snapshots are preserved for analysis
/// </summary>
public void ClearSnapshotsIfBacktest()
{
if (_context.Algorithm.LiveMode)
{
return;
}
try
{
var deletedCount = 0;
var keysToDelete = new List<string>();
// Enumerate all keys in ObjectStore and find snapshot keys
foreach (var key in _context.Algorithm.ObjectStore.Keys)
{
if (key.StartsWith(SnapshotKeyPrefix))
{
keysToDelete.Add(key);
}
}
// Delete all snapshot keys
foreach (var key in keysToDelete)
{
_context.Algorithm.ObjectStore.Delete(key);
deletedCount++;
}
if (deletedCount > 0)
{
((dynamic)_context.Logger).Info($"TradePersistence: Cleared {deletedCount} daily snapshots for backtest");
}
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"TradePersistence: Error clearing snapshots: {ex.Message}");
}
}
/// <summary>
/// Loads trade state from ObjectStore
/// Returns null if no saved state exists
/// </summary>
public TradeTracker LoadTrades()
{
try
{
if (!_context.Algorithm.ObjectStore.ContainsKey(CurrentPositionsKey))
{
((dynamic)_context.Logger).Info("TradePersistence: No saved trades found, starting fresh");
return null;
}
var bytes = _context.Algorithm.ObjectStore.ReadBytes(CurrentPositionsKey);
var json = System.Text.Encoding.UTF8.GetString(bytes);
var book = JsonSerializer.Deserialize<TradeBook>(json, SerializerOptions);
if (book == null || book.AllTrades == null)
{
((dynamic)_context.Logger).Warning("TradePersistence: Invalid saved data, starting fresh");
return null;
}
var tracker = new TradeTracker
{
AllTrades = book.AllTrades
};
// Rebuild categorized lists
foreach (var trade in book.AllTrades)
{
if (book.WorkingTrades.Contains(trade.OrderId))
{
tracker.WorkingTrades.Add(trade);
}
else if (book.OpenTrades.Contains(trade.OrderId))
{
tracker.OpenTrades.Add(trade);
}
else if (book.ClosedTrades.Contains(trade.OrderId))
{
tracker.ClosedTrades.Add(trade);
}
}
((dynamic)_context.Logger).Info($"TradePersistence: Loaded {tracker.AllTrades.Count} trades " +
$"(Working: {tracker.WorkingTrades.Count}, Open: {tracker.OpenTrades.Count}, " +
$"Closed: {tracker.ClosedTrades.Count})");
return tracker;
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"TradePersistence: Error loading trades: {ex.Message}");
return null;
}
}
/// <summary>
/// Saves a daily snapshot of trades
/// Only persists open and working trades (excludes closed/cancelled to minimize storage)
/// </summary>
public void SaveDailySnapshot(TradeTracker tracker)
{
if (tracker == null) return;
try
{
var date = _context.Algorithm.Time.ToString("yyyy-MM-dd");
var snapshotKey = $"{SnapshotKeyPrefix}{date}.json";
// Only persist open and working trades to minimize ObjectStore usage
var openAndWorking = tracker.WorkingTrades.Concat(tracker.OpenTrades).ToList();
var book = new TradeBook
{
Version = "1.0",
AsOfUtc = _context.Algorithm.UtcTime,
AllTrades = openAndWorking,
WorkingTrades = tracker.WorkingTrades.Select(t => t.OrderId).ToList(),
OpenTrades = tracker.OpenTrades.Select(t => t.OrderId).ToList(),
ClosedTrades = new List<string>() // Don't persist closed trades
};
var json = JsonSerializer.Serialize(book, SerializerOptions);
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
_context.Algorithm.ObjectStore.SaveBytes(snapshotKey, bytes);
((dynamic)_context.Logger).Debug($"TradePersistence: Saved daily snapshot for {date} ({openAndWorking.Count} open/working trades)");
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"TradePersistence: Error saving daily snapshot: {ex.Message}");
}
}
/// <summary>
/// Gets summary of persisted trade data for reporting
/// </summary>
public TradeSummary GetTradeSummary(TradeTracker tracker)
{
if (tracker == null)
{
return new TradeSummary();
}
return new TradeSummary
{
TotalTrades = tracker.AllTrades.Count,
WorkingCount = tracker.WorkingTrades.Count,
OpenCount = tracker.OpenTrades.Count,
ClosedCount = tracker.ClosedTrades.Count,
PartialFillCount = tracker.AllTrades.Count(t => t.Status == "PartialFill"),
CancelledCount = tracker.AllTrades.Count(t => t.Status == "Cancelled"),
TotalPnL = tracker.ClosedTrades.Sum(t => t.PnL),
AsOfUtc = DateTime.UtcNow
};
}
}
/// <summary>
/// Container for serialized trade data
/// </summary>
public class TradeBook
{
public string Version { get; set; }
public DateTime AsOfUtc { get; set; }
public List<TradeRecord> AllTrades { get; set; } = new List<TradeRecord>();
public List<string> WorkingTrades { get; set; } = new List<string>();
public List<string> OpenTrades { get; set; } = new List<string>();
public List<string> ClosedTrades { get; set; } = new List<string>();
}
/// <summary>
/// Summary statistics for trade reporting
/// </summary>
public class TradeSummary
{
public int TotalTrades { get; set; }
public int WorkingCount { get; set; }
public int OpenCount { get; set; }
public int ClosedCount { get; set; }
public int PartialFillCount { get; set; }
public int CancelledCount { get; set; }
public decimal TotalPnL { get; set; }
public DateTime AsOfUtc { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;
using QuantConnect.Orders;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Implementations;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.Core.Services;
using CoreAlgo.Architecture.QC.Helpers;
namespace CoreAlgo.Architecture.Core.Templates
{
/// <summary>
/// Adaptive Scalper strategy template - HFT Multi-Trade Edition
/// Micro-scalps equities using ATR-driven adaptive stops/targets, spread quality gates, and progressive daily throttle
/// Supports multiple concurrent trades with independent TP/SL per trade using Lean OCO pattern
/// QC-First approach - leverages QuantConnect's native indicators, bid/ask quotes, and OrderTickets
/// Uses TradeTracker/TradeRecord for durable state with lightweight runtime state for trailing
/// </summary>
public class AdaptiveScalperTemplate : SimpleBaseStrategy
{
/// <summary>
/// Spread quality classification
/// </summary>
private enum SpreadState
{
Tight,
Normal,
Wide
}
/// <summary>
/// Spread evaluation result
/// </summary>
private struct SpreadEvaluation
{
public SpreadState State;
public decimal SpreadValue;
public decimal Mid;
public bool HasValidQuotes;
}
/// <summary>
/// Lightweight runtime state for fast-moving fields (trailing)
/// Not persisted - recreated from TradeRecord on algorithm restart
/// </summary>
private class RuntimeState
{
public decimal HighestPriceSinceEntry;
public bool TrailingArmed;
public decimal CurrentStopPrice;
public int? SlOrderId; // Child SL order ID
}
// Order tag constants
private const string TAG_ENTRY = "ENTRY";
private const string TAG_SL_PREFIX = "SL_E";
// TradeRecord StrategyDetails keys
private const string KEY_STOP_DIST = "StopDist";
private const string KEY_TP_DIST = "TpDist"; // Kept for metric-only (no order placement)
private const string KEY_ATR_AT_ENTRY = "AtrAtEntry";
private const string KEY_ADAPT_FACTOR = "AdaptFactor";
private const string KEY_SPREAD_STATE = "SpreadState";
private const string KEY_SL_ORDER_ID = "SlOrderId";
private const string KEY_SL_CHASE_MARKED_AT = "SLChaseMarkedAt";
private const string KEY_SL_CHASE_ACTIVE = "SLChaseActive";
private const string KEY_SL_CHASE_LAST_AT = "SLChaseLastAt";
private const string KEY_SL_CHASE_UPDATES = "SLChaseUpdates";
private AdaptiveScalperConfig _config;
private Symbol _underlying;
private AverageTrueRange _atr;
private RollingWindow<decimal> _atrBaseline;
// Multi-trade state management using TradeTracker + runtime maps
private readonly Dictionary<int, RuntimeState> _runtimeByEntryId = new Dictionary<int, RuntimeState>();
// Trade tracking log frequency control
private readonly Dictionary<int, DateTime> _lastTrackingLogByEntryId = new Dictionary<int, DateTime>();
// Rate limiting
private DateTime _lastEntryTime = DateTime.MinValue;
private Queue<DateTime> _orderTimestamps = new Queue<DateTime>();
// Daily tracking
private decimal _dailyPnL = 0m;
private decimal _tradeThrottle = 1.0m;
private bool _haltedForDay = false;
private DateTime _lastTradingDay = DateTime.MinValue;
// Performance tracking
private int _totalEntriesToday = 0;
private int _totalExitsToday = 0;
// Pause trading control
private bool _wasPausedLastCheck = false;
public override string Name => "Adaptive Scalper HFT";
public override string Description =>
"High-frequency micro-scalping strategy with multi-trade execution, ATR-driven adaptive stops/targets, spread quality gates, and progressive daily throttle";
// ============================================================================
// HELPER METHODS (In-place simplification)
// ============================================================================
/// <summary>
/// Evaluate spread quality and compute mid-price
/// </summary>
private SpreadEvaluation EvaluateSpread(decimal bid, decimal ask, decimal currentPrice, decimal dynMin, decimal dynMax, bool requireQuotes)
{
bool hasValidQuotes = bid > 0 && ask > 0;
decimal spread;
decimal mid;
if (!hasValidQuotes)
{
if (requireQuotes)
{
// Strict mode: no quotes = cannot evaluate
return new SpreadEvaluation
{
State = SpreadState.Wide,
SpreadValue = 0,
Mid = currentPrice,
HasValidQuotes = false
};
}
// Fallback mode: use spread=0 and current price as mid
spread = 0m;
mid = currentPrice;
}
else
{
spread = ask - bid;
mid = (ask + bid) / 2m;
}
// Classify spread quality
SpreadState state;
if (!hasValidQuotes)
{
state = SpreadState.Normal; // Treat no-quotes as NORMAL when fallback is enabled
}
else if (spread <= dynMin * 1.1m)
{
state = SpreadState.Tight;
}
else if (spread <= dynMax * 0.9m)
{
state = SpreadState.Normal;
}
else
{
state = SpreadState.Wide;
}
return new SpreadEvaluation
{
State = state,
SpreadValue = spread,
Mid = mid,
HasValidQuotes = hasValidQuotes
};
}
/// <summary>
/// Compute ATR adaptation factor (protects against zero-divide)
/// </summary>
private decimal GetAdaptFactor()
{
if (!_atr.IsReady || !_atrBaseline.IsReady) return 1.0m;
var atrValue = (decimal)_atr.Current.Value;
var baseline = _atrBaseline.Average();
return baseline != 0 ? atrValue / baseline : 1.0m;
}
/// <summary>
/// Single entry gate - encapsulates all entry checks
/// </summary>
private bool ShouldEnter(Slice slice, SpreadEvaluation spreadEval, int openCount, out string reason)
{
// Check concurrency
if (openCount >= _config.MaxConcurrentTrades)
{
reason = $"MaxConcurrent ({openCount}/{_config.MaxConcurrentTrades})";
return false;
}
// Check throttle
if (_tradeThrottle <= _config.MinThrottle)
{
reason = $"Throttle too low ({_tradeThrottle:P0} <= {_config.MinThrottle:P0})";
return false;
}
// Check spread quality
if (spreadEval.State != SpreadState.Tight && spreadEval.State != SpreadState.Normal)
{
reason = $"Spread state: {spreadEval.State}";
return false;
}
// Check rate limits
if (!CanEnterNow(slice.Time))
{
reason = "Rate limiting";
return false;
}
// Check entry restrictions (time window, capital)
if (!EntryRestrictions.CanEnterPosition(_underlying, slice, out reason))
{
return false;
}
// Check daily cap (if configured)
if (_config.MaxEntriesPerDay > 0 && _totalEntriesToday >= _config.MaxEntriesPerDay)
{
reason = $"Daily entry cap reached ({_totalEntriesToday}/{_config.MaxEntriesPerDay})";
return false;
}
reason = "";
return true;
}
/// <summary>
/// Get or create runtime state for a trade (centralized hydration)
/// </summary>
private RuntimeState GetOrCreateRuntime(TradeRecord trade)
{
var entryId = int.Parse(trade.OrderId);
if (!_runtimeByEntryId.TryGetValue(entryId, out var runtime))
{
runtime = new RuntimeState
{
HighestPriceSinceEntry = trade.OpenPrice,
TrailingArmed = false,
CurrentStopPrice = 0
};
// Load child order IDs from TradeRecord if available
if (trade.TryGetInt(KEY_SL_ORDER_ID, out var slId) && slId > 0)
{
runtime.SlOrderId = slId;
}
_runtimeByEntryId[entryId] = runtime;
}
return runtime;
}
/// <summary>
/// Try to update a stop order (DRY helper for ticket fetch + update)
/// </summary>
private bool TryUpdateStop(int slOrderId, decimal newStopPrice, decimal stopLimitBuffer)
{
var slTicket = Algorithm.Transactions.GetOrderTicket(slOrderId);
if (slTicket == null || slTicket.Status != OrderStatus.Submitted)
{
return false;
}
var updateFields = new UpdateOrderFields
{
StopPrice = newStopPrice,
LimitPrice = newStopPrice - stopLimitBuffer
};
var response = slTicket.Update(updateFields);
return response.IsSuccess;
}
/// <summary>
/// Compute trailing stop (returns new stop price or null if no update needed)
/// </summary>
private decimal? ComputeTrailingStop(RuntimeState runtime, decimal tpDist, decimal tick)
{
if (!runtime.TrailingArmed || tpDist <= 0) return null;
// Calculate trailing amount - smaller than initial stop to allow profit capture
var trailingAmount = Math.Max(tick, tpDist * (1m - _config.TrailingStartFraction));
// Calculate new stop based on highest price
var candidateStop = runtime.HighestPriceSinceEntry - trailingAmount;
var newStopPrice = PriceRounding.FloorToTick(candidateStop, tick);
// Only update if stop moves up by at least 1 tick
if (newStopPrice > runtime.CurrentStopPrice + tick)
{
return newStopPrice;
}
return null;
}
/// <summary>
/// Unified entry ID resolution from tag
/// Supports: ENTRY, ENTRY_E{id}, SL_E{id}
/// </summary>
private bool TryExtractEntryIdFromTagOrMap(string tag, int orderId, out int entryId, out bool isEntry)
{
entryId = 0;
isEntry = false;
// Check if this is an entry order (ENTRY or ENTRY_E{id})
if (!string.IsNullOrEmpty(tag) && tag.StartsWith(TAG_ENTRY))
{
isEntry = true;
// Try to parse ENTRY_E{id} format
if (tag.Length > TAG_ENTRY.Length && tag[TAG_ENTRY.Length] == '_')
{
var idPart = tag.Substring(TAG_ENTRY.Length + 1);
if (idPart.StartsWith("E") && int.TryParse(idPart.Substring(1), out entryId))
{
return true;
}
}
// Fallback: use orderId as entryId for plain "ENTRY" tag
entryId = orderId;
return true;
}
// Try to extract from child tag (SL_E123)
if (!string.IsNullOrEmpty(tag))
{
var parts = tag.Split('_');
if (parts.Length >= 2 && parts[1].StartsWith("E"))
{
if (int.TryParse(parts[1].Substring(1), out entryId))
{
return true;
}
}
}
return false;
}
/// <summary>
/// Cancel all exit orders for open trades (used by kill switch and pause trading)
/// </summary>
private void CancelAllExitOrdersForOpenTrades()
{
var openTrades = GetOpenTradesForSymbol();
foreach (var trade in openTrades)
{
var entryId = int.Parse(trade.OrderId);
if (_runtimeByEntryId.TryGetValue(entryId, out var runtime))
{
if (runtime.SlOrderId.HasValue)
{
var slTicket = Algorithm.Transactions.GetOrderTicket(runtime.SlOrderId.Value);
if (slTicket != null && slTicket.Status == OrderStatus.Submitted)
{
slTicket.Cancel("Kill switch/Pause");
}
}
}
}
}
/// <summary>
/// Force-market-close all open positions N minutes before market close
/// Cancels outstanding stop-limit orders and submits market orders to ensure exit
/// </summary>
private void FlattenBeforeClose()
{
var openTrades = GetOpenTradesForSymbol();
if (openTrades.Count == 0) return;
SmartLog($"[EOD] Flattening {openTrades.Count} open position(s) {_config.FlattenBeforeCloseMinutes} minutes before close");
// Trade tracking log: Pre-flatten snapshot
if (_config.EnableTradeTrackingLogs)
{
var security = Algorithm.Securities[_underlying];
var currentPrice = security.Price;
SmartLog($"[TT EOD FLATTEN START] @ {Algorithm.Time:HH:mm:ss} | Open trades: {openTrades.Count}");
foreach (var trade in openTrades)
{
var entryId = int.Parse(trade.OrderId);
var runtime = GetOrCreateRuntime(trade);
var slTicket = runtime.SlOrderId.HasValue
? Algorithm.Transactions.GetOrderTicket(runtime.SlOrderId.Value)
: null;
var isLongPosition = trade.Quantity > 0;
var unrealizedPnL = (currentPrice - trade.OpenPrice) * trade.Quantity;
var flattenLine = TradeHighlightFormatter.FormatEodFlattenTrade(
entryId,
Algorithm.Time,
trade,
currentPrice,
unrealizedPnL,
runtime.TrailingArmed,
runtime.HighestPriceSinceEntry,
runtime.CurrentStopPrice,
slTicket,
isLongPosition,
_config.LogQuotesInTracking,
security
);
SmartLog(flattenLine);
}
}
foreach (var trade in openTrades)
{
var entryId = int.Parse(trade.OrderId);
// Cancel any working stop-limit orders
if (_runtimeByEntryId.TryGetValue(entryId, out var runtime))
{
if (runtime.SlOrderId.HasValue)
{
var slTicket = Algorithm.Transactions.GetOrderTicket(runtime.SlOrderId.Value);
if (slTicket != null && slTicket.Status == OrderStatus.Submitted)
{
if (_config.EnableTradeTrackingLogs)
{
SmartLog($"[TT EOD CANCEL SL] E{entryId} | SL OrderId:{slTicket.OrderId} Status:{slTicket.Status}");
}
slTicket.Cancel("EOD flatten");
}
}
}
else
{
// Fallback: try to get SL order ID from TradeRecord
if (trade.TryGetInt(KEY_SL_ORDER_ID, out var slId) && slId > 0)
{
var slTicket = Algorithm.Transactions.GetOrderTicket(slId);
if (slTicket != null && slTicket.Status == OrderStatus.Submitted)
{
if (_config.EnableTradeTrackingLogs)
{
SmartLog($"[TT EOD CANCEL SL] E{entryId} | SL OrderId:{slId} Status:{slTicket.Status} (fallback)");
}
slTicket.Cancel("EOD flatten");
}
}
}
// Submit market order to close position
var marketTicket = MarketOrder(_underlying, -trade.Quantity, tag: $"{TAG_SL_PREFIX}{entryId}_EOD");
if (marketTicket != null)
{
RegisterChildOrder(entryId, marketTicket.OrderId);
if (_config.EnableTradeTrackingLogs)
{
SmartLog($"[TT EOD MARKET ORDER] E{entryId} | OrderId:{marketTicket.OrderId} Qty:{-trade.Quantity} Status:{marketTicket.Status}");
}
}
else
{
SmartWarn($"[EOD] Failed to create market order for E{entryId}");
if (_config.EnableTradeTrackingLogs)
{
SmartWarn($"[TT EOD MARKET ORDER FAILED] E{entryId} | MarketOrder returned null");
}
}
}
// Trade tracking log: Post-flatten snapshot
if (_config.EnableTradeTrackingLogs)
{
var openTradesAfter = GetOpenTradesForSymbol().Count;
SmartLog($"[TT EOD FLATTEN END] @ {Algorithm.Time:HH:mm:ss} | Open trades after: {openTradesAfter}");
}
}
// ============================================================================
// STRATEGY LIFECYCLE
// ============================================================================
public override void OnInitialize()
{
// Configure with AdaptiveScalper-specific settings
try
{
Configure<AdaptiveScalperConfig>();
_config = (AdaptiveScalperConfig)Config;
}
catch (Exception ex)
{
SmartError($"Failed to load configuration: {ex.Message}");
throw;
}
// Setup underlying equity with appropriate resolution for quote data
var dataResolution = _config.GetUnderlyingResolution();
var security = AddEquity(_config.UnderlyingSymbol, dataResolution);
_underlying = security.Symbol;
// Initialize ATR indicator for volatility-based stop/target calculation
var atrResolution = _config.GetAtrResolution();
_atr = new AverageTrueRange(_config.AtrPeriod);
// Register for automatic updates and warm up with historical data
Algorithm.RegisterIndicator(_underlying, _atr, atrResolution);
Algorithm.WarmUpIndicator(_underlying, _atr, atrResolution);
// Initialize ATR baseline rolling window for adaptive factor calculation
_atrBaseline = new RollingWindow<decimal>(_config.VolatilityBaselineWindow);
// Schedule daily PnL reset at market open
Algorithm.Schedule.On(
Algorithm.DateRules.EveryDay(_underlying),
Algorithm.TimeRules.AfterMarketOpen(_underlying, 1),
ResetDailyTracking
);
// Schedule EOD flatten if enabled
if (_config.FlattenBeforeCloseMinutes > 0)
{
Algorithm.Schedule.On(
Algorithm.DateRules.EveryDay(_underlying),
Algorithm.TimeRules.BeforeMarketClose(_underlying, _config.FlattenBeforeCloseMinutes),
FlattenBeforeClose
);
}
// Guardrail: warn if stop/TP ratios make trailing difficult
if (_config.StopLossAtrMultiple >= _config.TakeProfitAtrMultiple)
{
SmartWarn(
$"[GUARDRAIL] StopLoss ({_config.StopLossAtrMultiple}x ATR) >= TakeProfit ({_config.TakeProfitAtrMultiple}x ATR). " +
$"Trailing may rarely reach breakeven before TP is hit. Consider reducing StopLoss or increasing TakeProfit multiples."
);
}
// Compact initialization summary
SmartLog($"Adaptive Scalper HFT initialized: {_underlying} | ATR({_config.AtrPeriod}) Ready:{_atr.IsReady} | MaxConcurrent:{_config.MaxConcurrentTrades} | KillSwitch:${_config.DailyKillSwitch:N0}");
}
protected override void OnPreExecuteAlways(Slice slice)
{
// Daily reset guard (scheduled reset is primary, this is backup)
if (slice.Time.Date > _lastTradingDay)
{
ResetDailyTracking();
_lastTradingDay = slice.Time.Date;
}
}
protected override void OnExecute(Slice slice)
{
// Early exit for halted day
if (_haltedForDay) return;
// Pause trading control - check for state transition
if (_config.PauseTrading)
{
// On transition to paused, cancel all outstanding orders
if (!_wasPausedLastCheck)
{
SmartLog("[PAUSE] Trading paused - canceling all outstanding orders");
CancelAllExitOrdersForOpenTrades();
// Also cancel any working entry orders
var workingOrders = Algorithm.Transactions.GetOpenOrders(_underlying);
foreach (var order in workingOrders)
{
var ticket = Algorithm.Transactions.GetOrderTicket(order.Id);
if (ticket != null && ticket.Status == OrderStatus.Submitted)
{
ticket.Cancel("Pause trading");
}
}
}
_wasPausedLastCheck = true;
// Still manage trailing stops for existing positions, but don't enter new trades
ManageOpenTrades(slice, Securities[_underlying].Price);
return;
}
else
{
_wasPausedLastCheck = false;
}
// Update ATR baseline
if (_atr.IsReady)
{
_atrBaseline.Add((decimal)_atr.Current.Value);
}
// Wait for indicators to be ready
if (!_atr.IsReady || !_atrBaseline.IsReady) return;
// Get current market data
var security = Securities[_underlying];
var currentPrice = security.Price;
var atrValue = (decimal)_atr.Current.Value;
var adaptFactor = GetAdaptFactor();
// Evaluate spread quality
var dynMin = _config.MinSpread * adaptFactor;
var dynMax = _config.MaxSpread * adaptFactor;
var spreadEval = EvaluateSpread(security.BidPrice, security.AskPrice, currentPrice, dynMin, dynMax, _config.RequireQuotesForEntry);
// Early exit if spread evaluation failed (strict quote mode)
if (_config.RequireQuotesForEntry && spreadEval.State == SpreadState.Wide && !spreadEval.HasValidQuotes)
{
return;
}
// Calculate time-of-day volatility multiplier (higher during opening hour)
var hour = slice.Time.Hour + slice.Time.Minute / 60m;
var volMultiplier = (hour >= 9.5m && hour <= 10.5m) ? _config.OHVolatilityMultiplier : 1.0m;
// Calculate dynamic stop/target with tick rounding
var tick = PriceRounding.GetMinPriceVariation(Algorithm.Securities, _underlying);
var rawStopLoss = Math.Max(_config.MinStopLoss, atrValue * _config.StopLossAtrMultiple * volMultiplier);
var rawTakeProfit = atrValue * _config.TakeProfitAtrMultiple * volMultiplier;
var stopLoss = PriceRounding.CeilToTick(rawStopLoss, tick);
var takeProfit = PriceRounding.CeilToTick(rawTakeProfit, tick);
// Calculate risk-based position size with quantity bounds
var baseQty = (int)(_config.TargetDollarRisk / stopLoss);
var tradeQty = Math.Max(1, (int)(baseQty * _tradeThrottle));
// Clamp quantity within configured min/max bounds
tradeQty = Math.Clamp(tradeQty, _config.MinQuantity, _config.MaxQuantity);
// === ENTRY LOGIC ===
var openTrades = GetOpenTradesForSymbol();
var openCount = openTrades.Count;
// Single entry gate check
if (ShouldEnter(slice, spreadEval, openCount, out var reason))
{
// Place entry limit order at mid-price (or offset by config ticks)
var entryPrice = spreadEval.Mid + (_config.EntryLimitOffsetTicks * tick);
var entryTicket = LimitOrder(_underlying, tradeQty, entryPrice, tag: TAG_ENTRY);
if (entryTicket == null)
{
return;
}
// Track and persist entry order
TrackWorkingOrder(entryTicket, Name);
var trade = TradeTracker.AllTrades.FirstOrDefault(t => t.OrderId == entryTicket.OrderId.ToString());
if (trade != null)
{
trade.SetDetail(KEY_STOP_DIST, stopLoss);
trade.SetDetail(KEY_TP_DIST, takeProfit);
trade.SetDetail(KEY_ATR_AT_ENTRY, atrValue);
trade.SetDetail(KEY_ADAPT_FACTOR, adaptFactor);
trade.SetDetail(KEY_SPREAD_STATE, spreadEval.State.ToString());
trade.Quantity = tradeQty;
TradePersistence.SaveTrades(TradeTracker);
}
_lastEntryTime = slice.Time;
_totalEntriesToday++;
}
// === EXIT AND TRADE MANAGEMENT ===
ManageOpenTrades(slice, currentPrice);
}
/// <summary>
/// Manage all open trades - update trailing stops when profit threshold reached
/// </summary>
private void ManageOpenTrades(Slice slice, decimal currentPrice)
{
var tick = PriceRounding.GetMinPriceVariation(Algorithm.Securities, _underlying);
var stopLimitBuffer = _config.StopLimitBufferTicks * tick;
var openTrades = GetOpenTradesForSymbol();
var security = Algorithm.Securities[_underlying];
foreach (var trade in openTrades)
{
var entryId = int.Parse(trade.OrderId);
var runtime = GetOrCreateRuntime(trade);
// Trade tracking log: Per-bar dump (respecting frequency)
if (_config.EnableTradeTrackingLogs)
{
var shouldLog = !_lastTrackingLogByEntryId.TryGetValue(entryId, out var lastLogTime) ||
(slice.Time - lastLogTime).TotalSeconds >= _config.TradeTrackingLogFrequencySeconds;
if (shouldLog)
{
var slTicket = runtime.SlOrderId.HasValue
? Algorithm.Transactions.GetOrderTicket(runtime.SlOrderId.Value)
: null;
var isLongPosition = trade.Quantity > 0;
var trackingLine = TradeHighlightFormatter.FormatTradeTracking(
entryId,
slice.Time,
currentPrice,
trade,
runtime.HighestPriceSinceEntry,
runtime.TrailingArmed,
runtime.CurrentStopPrice,
slTicket,
isLongPosition,
_config.LogQuotesInTracking,
security
);
SmartLog(trackingLine);
_lastTrackingLogByEntryId[entryId] = slice.Time;
}
}
// === MAX HOLDING TIME ENFORCEMENT ===
if (_config.MaxHoldingMinutes > 0)
{
var holdingMinutes = (slice.Time - trade.OpenTime).TotalMinutes;
if (holdingMinutes >= _config.MaxHoldingMinutes)
{
// Cancel any working stop-limit orders
if (runtime.SlOrderId.HasValue)
{
var slTicket = Algorithm.Transactions.GetOrderTicket(runtime.SlOrderId.Value);
if (slTicket != null && slTicket.Status == OrderStatus.Submitted)
{
slTicket.Cancel("Max holding time");
}
}
else
{
// Fallback: try to get SL order ID from TradeRecord
if (trade.TryGetInt(KEY_SL_ORDER_ID, out var slId) && slId > 0)
{
var slTicket = Algorithm.Transactions.GetOrderTicket(slId);
if (slTicket != null && slTicket.Status == OrderStatus.Submitted)
{
slTicket.Cancel("Max holding time");
}
}
}
// Submit market order to force-close
var marketTicket = MarketOrder(_underlying, -trade.Quantity, tag: $"{TAG_SL_PREFIX}{entryId}_TIMEOUT");
if (marketTicket != null)
{
RegisterChildOrder(entryId, marketTicket.OrderId);
SmartLog($"[TIMEOUT] Force-closing E{entryId} after {holdingMinutes:F1} min (limit: {_config.MaxHoldingMinutes} min)");
}
else
{
SmartWarn($"[TIMEOUT] Failed to create market order for E{entryId}");
}
continue; // Skip trailing logic for this trade
}
}
// Update highest price tracker
if (currentPrice > runtime.HighestPriceSinceEntry)
{
runtime.HighestPriceSinceEntry = currentPrice;
}
// Get static details from TradeRecord
trade.TryGetDecimal(KEY_TP_DIST, out var tpDist);
trade.TryGetDecimal(KEY_STOP_DIST, out var stopDist);
var unrealizedPnL = currentPrice - trade.OpenPrice;
// === TRAILING STOP LOGIC ===
// Arm trailing when price reaches threshold
if (!runtime.TrailingArmed && tpDist > 0 && unrealizedPnL >= tpDist * _config.TrailingStartFraction && runtime.SlOrderId.HasValue)
{
var newStopPrice = PriceRounding.FloorToTick(trade.OpenPrice, tick);
if (TryUpdateStop(runtime.SlOrderId.Value, newStopPrice, stopLimitBuffer))
{
runtime.TrailingArmed = true;
runtime.CurrentStopPrice = newStopPrice;
// Trade tracking log: Trailing armed
if (_config.EnableTradeTrackingLogs)
{
var oldStopPrice = trade.OpenPrice - stopDist;
SmartLog($"[TT TRAIL ARMED] E{entryId} @ {slice.Time:HH:mm:ss} | Price:{currentPrice:F2} UnrealPnL:{unrealizedPnL:F2} Threshold:{tpDist * _config.TrailingStartFraction:F2} | Stop:{oldStopPrice:F2}→{newStopPrice:F2}");
}
}
}
// Update trailing stop if armed
if (runtime.TrailingArmed && runtime.SlOrderId.HasValue)
{
var newStop = ComputeTrailingStop(runtime, tpDist, tick);
if (newStop.HasValue && TryUpdateStop(runtime.SlOrderId.Value, newStop.Value, stopLimitBuffer))
{
var oldStop = runtime.CurrentStopPrice;
runtime.CurrentStopPrice = newStop.Value;
// Trade tracking log: Trailing updated
if (_config.EnableTradeTrackingLogs)
{
SmartLog($"[TT TRAIL UPDATE] E{entryId} @ {slice.Time:HH:mm:ss} | Stop:{oldStop:F2}→{newStop.Value:F2} High:{runtime.HighestPriceSinceEntry:F2} Current:{currentPrice:F2}");
}
}
}
// === STOP-LIMIT CHASE LOGIC ===
if (runtime.SlOrderId.HasValue)
{
var slTicket = Algorithm.Transactions.GetOrderTicket(runtime.SlOrderId.Value);
if (slTicket != null && (slTicket.Status == OrderStatus.Submitted || slTicket.Status == OrderStatus.PartiallyFilled))
{
// Get current stop price from order
var currentStopPrice = slTicket.Get(OrderField.StopPrice);
var currentLimitPrice = slTicket.Get(OrderField.LimitPrice);
var isLongPosition = trade.Quantity > 0; // Long = sell stop-limit, Short = buy stop-limit
// Check if price has breached stop threshold (mark for chase)
bool priceBreachedStop = isLongPosition
? currentPrice <= currentStopPrice
: currentPrice >= currentStopPrice;
if (priceBreachedStop)
{
// Mark chase start time if not already marked
if (!trade.TryGetDateTime(KEY_SL_CHASE_MARKED_AT, out var markedAt))
{
trade.SetDetail(KEY_SL_CHASE_MARKED_AT, slice.Time);
trade.SetDetail(KEY_SL_CHASE_ACTIVE, false);
TradePersistence.SaveTrades(TradeTracker);
}
}
// Check if chase should be active
if (trade.TryGetDateTime(KEY_SL_CHASE_MARKED_AT, out var chaseMarkedAt))
{
var minutesSinceMark = (slice.Time - chaseMarkedAt).TotalMinutes;
var shouldChase = minutesSinceMark >= _config.StopLimitChaseStartMinutes;
if (shouldChase)
{
// Activate chase if not already active
if (!trade.TryGetBool(KEY_SL_CHASE_ACTIVE, out var chaseActive) || !chaseActive)
{
trade.SetDetail(KEY_SL_CHASE_ACTIVE, true);
TradePersistence.SaveTrades(TradeTracker);
}
// Update limit price each minute (throttle to once per minute)
bool shouldUpdateNow = true;
if (trade.TryGetDateTime(KEY_SL_CHASE_LAST_AT, out var lastChaseAt))
{
var minutesSinceLastUpdate = (slice.Time - lastChaseAt).TotalMinutes;
shouldUpdateNow = minutesSinceLastUpdate >= 1.0;
}
if (shouldUpdateNow)
{
// Get current market quotes
var marketSecurity = Algorithm.Securities[_underlying];
var bid = marketSecurity.BidPrice;
var ask = marketSecurity.AskPrice;
var hasValidQuotes = bid > 0 && ask > 0;
if (hasValidQuotes)
{
var chaseBuffer = _config.StopLimitChaseBufferTicks * tick;
decimal newLimitPrice;
if (isLongPosition)
{
// Sell stop-limit: limit should be below stop, chase bid down
var targetLimit = bid - chaseBuffer;
// Ensure limit < stop (for sell stop-limit)
newLimitPrice = Math.Min(targetLimit, currentStopPrice - tick);
}
else
{
// Buy stop-limit: limit should be above stop, chase ask up
var targetLimit = ask + chaseBuffer;
// Ensure limit > stop (for buy stop-limit)
newLimitPrice = Math.Max(targetLimit, currentStopPrice + tick);
}
// Round to tick
newLimitPrice = isLongPosition
? PriceRounding.FloorToTick(newLimitPrice, tick)
: PriceRounding.CeilToTick(newLimitPrice, tick);
// Only update if price changed meaningfully
if (Math.Abs(newLimitPrice - currentLimitPrice) >= tick)
{
var updateFields = new UpdateOrderFields
{
LimitPrice = newLimitPrice
};
var response = slTicket.Update(updateFields);
if (response.IsSuccess)
{
// Update tracking
trade.SetDetail(KEY_SL_CHASE_LAST_AT, slice.Time);
var updateCount = trade.TryGetInt(KEY_SL_CHASE_UPDATES, out var count) ? count : 0;
trade.SetDetail(KEY_SL_CHASE_UPDATES, updateCount + 1);
TradePersistence.SaveTrades(TradeTracker);
}
}
}
}
}
}
}
}
}
}
/// <summary>
/// Handle order fills - place child orders on entry fill, cancel siblings on exit fill
/// Called by Main.cs via TrackOrderFilled when any order is filled
/// </summary>
protected override void OnOrderFilled(OrderEvent orderEvent)
{
var orderId = orderEvent.OrderId;
var ticket = Algorithm.Transactions.GetOrderTicket(orderId);
var tag = ticket?.Tag ?? "";
// Resolve trade via order ID mapping (no tag parsing dependency)
var trade = TradeTracker.FindTradeByAnyOrderId(orderEvent.OrderId);
if (trade == null)
{
return; // Not our order
}
var isEntry = trade.OrderId == orderEvent.OrderId.ToString();
var entryId = int.Parse(trade.OrderId);
// Entry order filled - place SL (trailing stop only, no TP)
if (isEntry && trade.Status == "Open")
{
var openTradesBefore = GetOpenTradesForSymbol().Count;
trade.TryGetDecimal(KEY_STOP_DIST, out var stopDist);
trade.TryGetDecimal(KEY_TP_DIST, out var tpDist);
trade.TryGetDecimal(KEY_ATR_AT_ENTRY, out var atrAtEntry);
trade.TryGetDecimal(KEY_ADAPT_FACTOR, out var adaptFactor);
var tpPrice = orderEvent.FillPrice + tpDist; // Metric-only for highlights
var slPrice = orderEvent.FillPrice - stopDist;
var tick = PriceRounding.GetMinPriceVariation(Algorithm.Securities, _underlying);
var stopLimitBuffer = _config.StopLimitBufferTicks * tick;
// Trade tracking log: Entry fill details
if (_config.EnableTradeTrackingLogs)
{
var security = Algorithm.Securities[_underlying];
var entryFillLine = TradeHighlightFormatter.FormatEntryFill(
entryId,
Algorithm.Time,
orderEvent.FillPrice,
trade.Quantity,
stopDist,
tpDist,
atrAtEntry,
adaptFactor,
slPrice,
slPrice - stopLimitBuffer,
stopLimitBuffer,
tick,
openTradesBefore,
_config.LogQuotesInTracking,
security
);
SmartLog(entryFillLine);
}
// Place initial protective stop-limit order
var slTicket = StopLimitOrder(_underlying, -trade.Quantity, slPrice, slPrice - stopLimitBuffer, tag: $"{TAG_SL_PREFIX}{entryId}");
if (slTicket != null)
{
trade.SetDetail(KEY_SL_ORDER_ID, slTicket.OrderId);
RegisterChildOrder(entryId, slTicket.OrderId);
// Initialize runtime state
_runtimeByEntryId[entryId] = new RuntimeState
{
HighestPriceSinceEntry = orderEvent.FillPrice,
TrailingArmed = false,
CurrentStopPrice = slPrice,
SlOrderId = slTicket.OrderId
};
// Initialize tracking log timestamp
_lastTrackingLogByEntryId[entryId] = Algorithm.Time;
TradePersistence.SaveTrades(TradeTracker);
// Update entry order tag to include order id for CSV traceability
var entryTicket = Algorithm.Transactions.GetOrderTicket(entryId);
if (entryTicket != null)
{
entryTicket.Update(new UpdateOrderFields { Tag = $"{TAG_ENTRY}_E{entryId}" });
}
// Trade tracking log: SL order created
if (_config.EnableTradeTrackingLogs)
{
var slCreatedLine = TradeHighlightFormatter.FormatSlCreated(
entryId,
slTicket.OrderId,
slTicket.Status,
slPrice,
slPrice - stopLimitBuffer,
orderEvent.FillPrice,
trade.Quantity > 0
);
SmartLog(slCreatedLine);
}
}
else
{
SmartError($"[ENTRY SL E{entryId}] StopLimitOrder returned null - no protective stop placed!");
}
// Trade highlight
var logger = (dynamic)Logger;
var scope = logger.BuildScope(_underlying, entryId);
logger.AddHighlightMetrics(scope, new Dictionary<string, object>
{
{"ATR", trade.TryGetDecimal(KEY_ATR_AT_ENTRY, out var atr) ? atr : 0},
{"SL", slPrice},
{"TP", tpPrice}
});
logger.HighlightTradeEvent(
nameof(AdaptiveScalperTemplate),
nameof(OnOrderFilled),
HighlightKind.Entry,
TradeHighlightFormatter.BuildEntry(_underlying.Value, Algorithm.Time, orderEvent.FillPrice, trade.Quantity, slPrice, tpPrice, orderId),
scope
);
return;
}
// Get runtime state
var hasRuntime = _runtimeByEntryId.TryGetValue(entryId, out var runtime);
// Any child fill (SL/EOD/TIMEOUT) - close trade
if (!isEntry && trade.Status == "Open")
{
var openTradesBefore = GetOpenTradesForSymbol().Count;
var pnl = (orderEvent.FillPrice - trade.OpenPrice) * trade.Quantity;
_dailyPnL += pnl;
_totalExitsToday++;
// Determine exit reason from tag
string exitType;
HighlightKind highlightKind;
ExitType exitTypeEnum;
if (tag.Contains("_EOD"))
{
exitType = "EOD";
highlightKind = HighlightKind.StopLoss; // Use StopLoss for EOD
exitTypeEnum = ExitType.StopLoss;
}
else if (tag.Contains("_TIMEOUT"))
{
exitType = "TimeCap";
highlightKind = HighlightKind.StopLoss; // Use StopLoss for timeout
exitTypeEnum = ExitType.StopLoss;
}
else
{
// Standard exit logic
var trailingArmed = hasRuntime && runtime.TrailingArmed;
exitType = trailingArmed ? "TrailHit" : "StopHit";
highlightKind = trailingArmed ? HighlightKind.TrailingStop : HighlightKind.StopLoss;
exitTypeEnum = trailingArmed ? ExitType.TrailingStop : ExitType.StopLoss;
}
// Trade tracking log: Exit fill details
if (_config.EnableTradeTrackingLogs)
{
var security = Algorithm.Securities[_underlying];
var slTicket = runtime?.SlOrderId.HasValue == true
? Algorithm.Transactions.GetOrderTicket(runtime.SlOrderId.Value)
: null;
var exitFillLine = TradeHighlightFormatter.FormatExitFill(
entryId,
Algorithm.Time,
exitType,
trade.OpenPrice,
trade.OpenTime,
orderEvent.FillPrice,
pnl,
hasRuntime && runtime.TrailingArmed,
hasRuntime ? runtime.HighestPriceSinceEntry : trade.OpenPrice,
hasRuntime ? runtime.CurrentStopPrice : 0,
openTradesBefore,
slTicket,
trade.Quantity > 0,
_config.LogQuotesInTracking,
security
);
SmartLog(exitFillLine);
}
// Trade highlight
var logger = (dynamic)Logger;
var scope = logger.BuildScope(_underlying, entryId);
logger.AddHighlightMetrics(scope, new Dictionary<string, object> { {"Reason", exitType} });
logger.HighlightTradeEvent(nameof(AdaptiveScalperTemplate), nameof(OnOrderFilled),
highlightKind,
TradeHighlightFormatter.BuildExit(_underlying.Value, Algorithm.Time, orderEvent.FillPrice, pnl,
exitTypeEnum, orderId), scope);
TrackPositionClosed(entryId.ToString(), orderEvent.FillPrice, pnl);
if (IsTradeClosed(entryId, trade)) ClearTradeMappings(entryId, runtime);
// Clean up tracking log timestamp
_lastTrackingLogByEntryId.Remove(entryId);
AdjustThrottle();
return;
}
// Handle cancelled orders
if (orderEvent.Status == OrderStatus.Canceled)
{
if (IsTradeClosed(entryId, trade)) ClearTradeMappings(entryId, runtime);
}
}
/// <summary>
/// Receive ALL order events (fills, cancels, partials) for event-time logic
/// No-op in trailing-only mode (no swap-to-TP logic)
/// </summary>
public override void OnOrderEventRouted(OrderEvent orderEvent)
{
// No event-time logic needed for trailing-only mode
}
/// <summary>
/// Get open trades for this strategy and symbol from TradeTracker
/// </summary>
private List<TradeRecord> GetOpenTradesForSymbol()
{
return TradeTracker.OpenTrades
.Where(t => t.Strategy == Name && t.Symbol == _underlying.Value)
.ToList();
}
/// <summary>
/// Check if SL order is in terminal state
/// </summary>
private bool IsTradeClosed(int entryId, TradeRecord trade)
{
// Get runtime state for order ID
if (!_runtimeByEntryId.TryGetValue(entryId, out var runtime))
{
// No runtime state - check TradeRecord details
trade.TryGetInt(KEY_SL_ORDER_ID, out var slId);
return slId == 0 || IsOrderTerminal(slId);
}
return !runtime.SlOrderId.HasValue || IsOrderTerminal(runtime.SlOrderId.Value);
}
/// <summary>
/// Check if order is in terminal state
/// </summary>
private bool IsOrderTerminal(int orderId)
{
var ticket = Algorithm.Transactions.GetOrderTicket(orderId);
if (ticket == null) return true;
return ticket.Status == OrderStatus.Filled ||
ticket.Status == OrderStatus.Canceled ||
ticket.Status == OrderStatus.Invalid;
}
/// <summary>
/// Clear runtime mappings for a trade
/// </summary>
private void ClearTradeMappings(int entryId, RuntimeState runtime)
{
_runtimeByEntryId.Remove(entryId);
}
/// <summary>
/// Check if we can enter a new trade now based on rate limits
/// </summary>
private bool CanEnterNow(DateTime currentTime)
{
// Entry cooldown check
var msSinceLastEntry = (currentTime - _lastEntryTime).TotalMilliseconds;
if (msSinceLastEntry < _config.EntryCooldownMs)
{
return false;
}
// Orders per minute rate limit
var oneMinuteAgo = currentTime.AddMinutes(-1);
while (_orderTimestamps.Count > 0 && _orderTimestamps.Peek() < oneMinuteAgo)
{
_orderTimestamps.Dequeue();
}
if (_orderTimestamps.Count >= _config.MaxOrdersPerMinute)
{
return false;
}
_orderTimestamps.Enqueue(currentTime);
return true;
}
/// <summary>
/// Adjust throttle based on daily PnL
/// </summary>
private void AdjustThrottle()
{
// Progressive throttle adjustment as losses build
if (_dailyPnL <= _config.DailyKillSwitch * 0.5m)
{
var lossRatio = Math.Abs(_dailyPnL / _config.DailyKillSwitch);
_tradeThrottle = Math.Max(_config.MinThrottle, 1m - lossRatio * _config.ThrottleDecay);
}
// Kill switch - halt trading for the day
if (_dailyPnL <= _config.DailyKillSwitch)
{
SmartLog($"[KILL SWITCH] Loss limit ${_dailyPnL:F2} - halting");
CancelAllExitOrdersForOpenTrades();
Algorithm.Liquidate(_underlying);
_runtimeByEntryId.Clear();
_haltedForDay = true;
}
}
private void ResetDailyTracking()
{
SmartLog($"=== Daily reset {Algorithm.Time.Date:yyyy-MM-dd} | Entries: {_totalEntriesToday}, Exits: {_totalExitsToday}, PnL: ${_dailyPnL:F2} ===");
// Cross-check with TradeTracker
if (TradePersistence != null)
{
var summary = TradePersistence.GetTradeSummary(TradeTracker);
var openCount = GetOpenTradesForSymbol().Count;
if (_config.DebugMode)
{
SmartLog($"Tracker: Working={summary.WorkingCount}, Open={summary.OpenCount}, Closed={summary.ClosedCount}");
}
if (openCount != summary.OpenCount)
{
SmartWarn($"Trade divergence: Local={openCount}, Tracker={summary.OpenCount}");
}
}
// Reset daily counters
_dailyPnL = 0m;
_tradeThrottle = 1.0m;
_haltedForDay = false;
_totalEntriesToday = 0;
_totalExitsToday = 0;
_orderTimestamps.Clear();
// Rehydrate runtime state from TradeTracker instead of clearing
// This prevents ensure storms at day open and reduces divergence
_runtimeByEntryId.Clear();
var openTrades = GetOpenTradesForSymbol();
foreach (var trade in openTrades)
{
// GetOrCreateRuntime will rebuild state from TradeRecord
GetOrCreateRuntime(trade);
}
if (_config.DebugMode && openTrades.Count > 0)
{
SmartLog($"[RESET] Rehydrated runtime for {openTrades.Count} open trades");
}
}
/// <summary>
/// Override base exit signal handler to prevent unintended liquidations.
/// AdaptiveScalper manages exits strictly via event-driven SL/TP only.
/// </summary>
protected override void OnExitSignal(Symbol symbol, string reason)
{
// Ignore global exit signals to avoid unintended liquidations
if (_config.DebugMode)
{
SmartDebug($"[EXIT IGNORED] {symbol}: {reason} (event-driven SL/TP only)");
}
}
protected override void OnGetPerformanceMetrics(Dictionary<string, double> metrics)
{
// Core metrics for monitoring and dashboard
metrics["DailyPnL"] = (double)_dailyPnL;
metrics["TradeThrottle"] = (double)_tradeThrottle;
metrics["HaltedForDay"] = _haltedForDay ? 1.0 : 0.0;
metrics["ATRValue"] = _atr.IsReady ? (double)_atr.Current.Value : 0.0;
metrics["OpenTrades"] = GetOpenTradesForSymbol().Count;
metrics["TotalEntriesToday"] = _totalEntriesToday;
metrics["TotalExitsToday"] = _totalExitsToday;
if (_atrBaseline.IsReady)
{
metrics["ATRBaseline"] = (double)_atrBaseline.Average();
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Securities;
using QuantConnect.Securities.Future;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.QC.Helpers
{
/// <summary>
/// Simple static helper for asset type detection and appropriate QC method calls.
/// Handles the complexity of different asset types (equity, index, future) so strategies
/// can switch between SPY/SPX/QQQ/ES with just a configuration change.
/// </summary>
public static class AssetManager
{
/// <summary>
/// Cash indices that require AddIndex() instead of AddEquity()
/// These are the QuantConnect supported cash indices
/// </summary>
private static readonly HashSet<string> CashIndices = new HashSet<string>
{
"SPX", "VIX", "NDX", "RUT", "DJX"
};
/// <summary>
/// Future symbols that require AddFuture() instead of AddEquity()
/// Expanded to include major futures across all asset classes
/// </summary>
private static readonly HashSet<string> Futures = new HashSet<string>
{
// Equity Index Futures
"ES", "NQ", "YM", "RTY", "EMD", "NKD",
// Energy Futures
"CL", "NG", "RB", "HO", "BZ",
// Metal Futures
"GC", "SI", "HG", "PA", "PL",
// Agricultural Futures
"ZC", "ZS", "ZW", "ZM", "ZL", "KC", "CT", "SB", "CC", "OJ",
// Interest Rate & Bond Futures
"ZB", "ZN", "ZF", "TU", "UB", "ED", "SR1", "SR3",
// Currency Futures
"6E", "6J", "6B", "6S", "6C", "6A", "6N", "6M", "E7", "J7",
// Volatility Futures
"VX",
// Crypto Futures (if supported)
"BTC", "ETH"
};
/// <summary>
/// Add an asset to the algorithm using the appropriate QC method based on asset type.
/// Automatically detects whether to use AddEquity(), AddIndex(), or AddFuture().
/// For futures, supports continuous contracts with proper data normalization.
/// </summary>
/// <param name="context">The algorithm context providing access to algorithm and logger</param>
/// <param name="symbol">The symbol to add (e.g., "SPY", "SPX", "ES")</param>
/// <param name="resolution">Data resolution (default: Minute)</param>
/// <param name="useContinuousContract">For futures: use continuous contract (default: true)</param>
/// <param name="contractDepthOffset">For futures: contract depth (0=front month, 1=next month, default: 0)</param>
/// <returns>The Security object for the added asset</returns>
/// <exception cref="ArgumentNullException">If context or symbol is null</exception>
/// <exception cref="ArgumentException">If symbol is empty or whitespace</exception>
public static Security AddAsset(IAlgorithmContext context, string symbol, Resolution resolution = Resolution.Minute,
bool useContinuousContract = true, int contractDepthOffset = 0, bool extendedMarketHours = false)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (string.IsNullOrWhiteSpace(symbol))
throw new ArgumentException("Symbol cannot be null or empty", nameof(symbol));
var upperSymbol = symbol.ToUpperInvariant();
try
{
Security addedSecurity;
// Detect asset type and call appropriate QC method
if (CashIndices.Contains(upperSymbol))
{
((dynamic)context.Logger).Info($"AssetManager: Adding index {upperSymbol} using AddIndex()");
addedSecurity = context.Algorithm.AddIndex(upperSymbol, resolution);
}
else if (Futures.Contains(upperSymbol))
{
((dynamic)context.Logger).Info($"AssetManager: Adding future {upperSymbol} using AddFuture() with continuous contract: {useContinuousContract}, extendedMarketHours: {extendedMarketHours}");
if (useContinuousContract)
{
// Add future with continuous contract support
addedSecurity = context.Algorithm.AddFuture(upperSymbol, resolution,
extendedMarketHours: extendedMarketHours,
dataMappingMode: DataMappingMode.OpenInterest,
dataNormalizationMode: DataNormalizationMode.BackwardsRatio,
contractDepthOffset: contractDepthOffset);
((dynamic)context.Logger).Info($"AssetManager: Continuous contract configured - DataMapping: OpenInterest, DataNormalization: BackwardsRatio, Depth: {contractDepthOffset}");
}
else
{
// Simple future addition without continuous contract
addedSecurity = context.Algorithm.AddFuture(upperSymbol, resolution, extendedMarketHours: extendedMarketHours);
}
}
else
{
((dynamic)context.Logger).Info($"AssetManager: Adding equity {upperSymbol} using AddEquity()");
addedSecurity = context.Algorithm.AddEquity(upperSymbol, resolution);
}
// Verify the security was successfully added and log data availability
if (addedSecurity != null)
{
var isInSecurities = context.Algorithm.Securities.ContainsKey(addedSecurity.Symbol);
((dynamic)context.Logger).Info($"AssetManager: [SUCCESS] {upperSymbol} successfully added to Securities collection: {isInSecurities}");
((dynamic)context.Logger).Info($"AssetManager: Security details - Type: {addedSecurity.Type}, Resolution: {addedSecurity.Subscriptions.GetHighestResolution()}, Exchange: {addedSecurity.Exchange}");
// Check if security has current price data (indicates data subscription is working)
var hasPrice = addedSecurity.Price > 0;
var priceStatus = hasPrice ? $"${addedSecurity.Price:F2}" : "No price data yet";
((dynamic)context.Logger).Info($"AssetManager: Price data status: {priceStatus}");
if (!hasPrice)
{
((dynamic)context.Logger).Info($"AssetManager: [WARNING] No price data yet for {upperSymbol} - this is normal during initialization, data should arrive during backtest");
}
}
else
{
((dynamic)context.Logger).Error($"AssetManager: [FAILED] Failed to add {upperSymbol} - returned null security");
}
return addedSecurity;
}
catch (Exception ex)
{
((dynamic)context.Logger).Error($"AssetManager: Failed to add asset {upperSymbol}: {ex.Message}");
throw;
}
}
/// <summary>
/// Add an options chain for the underlying asset using the appropriate QC method.
/// Handles special cases like SPX->SPXW mapping automatically.
/// </summary>
/// <param name="context">The algorithm context providing access to algorithm and logger</param>
/// <param name="underlying">The underlying security</param>
/// <param name="resolution">Data resolution (default: Minute)</param>
/// <returns>The Symbol for the options chain</returns>
/// <exception cref="ArgumentNullException">If context or underlying is null</exception>
public static Symbol AddOptionsChain(IAlgorithmContext context, Security underlying, Resolution resolution = Resolution.Minute)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (underlying == null)
throw new ArgumentNullException(nameof(underlying));
var symbol = underlying.Symbol.Value.ToUpperInvariant();
try
{
Symbol optionSymbol;
if (underlying.Type == SecurityType.Index)
{
// Special case for SPX: use SPXW (weekly) options
if (symbol == "SPX")
{
((dynamic)context.Logger).Info($"AssetManager: Adding SPX index options using SPXW with {resolution} resolution");
// Pre-add diagnostics for universe and requested resolution
((dynamic)context.Logger).Info($"AssetManager: [UNIVERSE] UniverseSettings.Resolution={context.Algorithm.UniverseSettings.Resolution}, FillForward={context.Algorithm.UniverseSettings.FillForward}, ExtendedMktHours={context.Algorithm.UniverseSettings.ExtendedMarketHours}");
// Force minute resolution explicitly for intraday option trading
var requestedResolution = resolution == Resolution.Daily ? Resolution.Minute : resolution;
if (requestedResolution != resolution)
{
((dynamic)context.Logger).Info($"AssetManager: [RESOLUTION OVERRIDE] Changing from {resolution} to {requestedResolution} for intraday option data");
}
optionSymbol = context.Algorithm.AddIndexOption(underlying.Symbol, "SPXW", requestedResolution).Symbol;
((dynamic)context.Logger).Info($"AssetManager: [RESOLUTION VERIFICATION] Requested: {requestedResolution}");
}
else
{
// Other indices use standard index options
((dynamic)context.Logger).Info($"AssetManager: Adding index options for {symbol} with {resolution} resolution");
// Pre-add diagnostics for universe and requested resolution
((dynamic)context.Logger).Info($"AssetManager: [UNIVERSE] UniverseSettings.Resolution={context.Algorithm.UniverseSettings.Resolution}, FillForward={context.Algorithm.UniverseSettings.FillForward}, ExtendedMktHours={context.Algorithm.UniverseSettings.ExtendedMarketHours}");
// Force minute resolution explicitly for intraday option trading
var requestedResolution = resolution == Resolution.Daily ? Resolution.Minute : resolution;
if (requestedResolution != resolution)
{
((dynamic)context.Logger).Info($"AssetManager: [RESOLUTION OVERRIDE] Changing from {resolution} to {requestedResolution} for intraday option data");
}
context.Algorithm.AddIndexOption(underlying.Symbol, requestedResolution);
optionSymbol = Symbol.CreateCanonicalOption(underlying.Symbol);
((dynamic)context.Logger).Info($"AssetManager: [RESOLUTION VERIFICATION] Requested: {requestedResolution}");
}
}
else if (underlying.Type == SecurityType.Future)
{
((dynamic)context.Logger).Info($"AssetManager: Adding future options for {symbol}");
context.Algorithm.AddFutureOption(underlying.Symbol);
optionSymbol = Symbol.CreateCanonicalOption(underlying.Symbol);
}
else
{
// Equity options
((dynamic)context.Logger).Info($"AssetManager: Adding equity options for {symbol}");
optionSymbol = context.Algorithm.AddOption(symbol, resolution).Symbol;
}
// [DEBUG] COMPREHENSIVE OPTIONS CHAIN VERIFICATION
if (optionSymbol != null)
{
((dynamic)context.Logger).Info($"AssetManager: [SUCCESS] Options chain created for {symbol}");
((dynamic)context.Logger).Info($"AssetManager: [DEBUG] DETAILED Option Symbol Analysis:");
((dynamic)context.Logger).Info($" optionSymbol: {optionSymbol}");
((dynamic)context.Logger).Info($" optionSymbol.Value: {optionSymbol.Value}");
((dynamic)context.Logger).Info($" optionSymbol.SecurityType: {optionSymbol.SecurityType}");
((dynamic)context.Logger).Info($" optionSymbol.ID: {optionSymbol.ID}");
((dynamic)context.Logger).Info($" optionSymbol.HasCanonical: {optionSymbol.HasCanonical()}");
if (optionSymbol.HasCanonical())
{
((dynamic)context.Logger).Info($" optionSymbol.Canonical: {optionSymbol.Canonical}");
}
// Check if option symbol is in Securities collection
var isInSecurities = context.Algorithm.Securities.ContainsKey(optionSymbol);
((dynamic)context.Logger).Info($"AssetManager: Option chain in Securities collection: {isInSecurities}");
// [DEBUG] VERIFY OPTION SECURITY DETAILS
if (isInSecurities)
{
var optionSecurity = context.Algorithm.Securities[optionSymbol];
// Enumerate and log all subscriptions with detail
var subscriptionResolutions = new System.Collections.Generic.List<Resolution>();
foreach (var sub in optionSecurity.Subscriptions)
{
subscriptionResolutions.Add(sub.Resolution);
((dynamic)context.Logger).Info($"AssetManager: [SUB] DataType={sub.Type?.Name}, Resolution={sub.Resolution}, TickType={sub.TickType}");
}
var distinctRes = subscriptionResolutions.Distinct().OrderBy(x => x).ToList();
((dynamic)context.Logger).Info($"AssetManager: [DEBUG] Option Security Details:");
((dynamic)context.Logger).Info($" Type: {optionSecurity.Type}");
((dynamic)context.Logger).Info($" Subscriptions: {string.Join(", ", distinctRes)}");
((dynamic)context.Logger).Info($" Exchange: {optionSecurity.Exchange}");
((dynamic)context.Logger).Info($" IsMarketOpen: {optionSecurity.Exchange.ExchangeOpen}");
// Check if we have minute-level subscriptions for intraday trading
var hasMinuteData = distinctRes.Contains(Resolution.Minute) ||
distinctRes.Contains(Resolution.Second);
if (!hasMinuteData && distinctRes.All(r => r == Resolution.Daily))
{
((dynamic)context.Logger).Warning($"AssetManager: [RESOLUTION WARNING] Only Daily resolution subscriptions found");
((dynamic)context.Logger).Warning($"AssetManager: [RESOLUTION WARNING] Intraday option chains may not be available");
((dynamic)context.Logger).Warning($"AssetManager: [HINT] If Minute was requested, ensure UniverseSettings.Resolution=Minute before adding options.");
}
else
{
((dynamic)context.Logger).Info($"AssetManager: [RESOLUTION SUCCESS] Intraday subscriptions available");
}
}
// [DEBUG] LOG ALL SECURITIES THAT CONTAIN OPTION-RELATED SYMBOLS
var optionRelatedSecurities = new List<Symbol>();
foreach (var sec in context.Algorithm.Securities.Keys)
{
if (sec.SecurityType == SecurityType.Option ||
sec.Value.Contains(symbol) ||
sec.Value.Contains("?"))
{
optionRelatedSecurities.Add(sec);
if (optionRelatedSecurities.Count >= 10) break;
}
}
if (optionRelatedSecurities.Any())
{
((dynamic)context.Logger).Info($"AssetManager: [DEBUG] Option-related securities in collection ({optionRelatedSecurities.Count}):");
foreach (var sec in optionRelatedSecurities)
{
((dynamic)context.Logger).Info($" {sec} (Type: {sec.SecurityType})");
}
}
else
{
((dynamic)context.Logger).Info($"AssetManager: [ERROR] No option-related securities found in collection");
}
// Additional verification for option chain subscription
((dynamic)context.Logger).Info($"AssetManager: Option chain ready for filtering and data feed");
// Note: The following line previously suggested LOCAL data; remove misleading data-source implication
// ((dynamic)context.Logger).Info($"AssetManager: [DEBUG] Data Source: LOCAL - Using generated option data for testing");
((dynamic)context.Logger).Info($"AssetManager: [TARGET] Symbol to use for slice.OptionChains access: {optionSymbol}");
}
else
{
((dynamic)context.Logger).Error($"AssetManager: [FAILED] Failed to create options chain for {symbol} - returned null symbol");
}
return optionSymbol;
}
catch (Exception ex)
{
((dynamic)context.Logger).Error($"AssetManager: Failed to add options chain for {symbol}: {ex.Message}");
throw;
}
}
/// <summary>
/// Check if a symbol is a supported cash index
/// </summary>
/// <param name="symbol">The symbol to check</param>
/// <returns>True if the symbol is a cash index</returns>
public static bool IsIndex(string symbol)
{
if (string.IsNullOrWhiteSpace(symbol))
return false;
return CashIndices.Contains(symbol.ToUpperInvariant());
}
/// <summary>
/// Check if a symbol is a supported future
/// </summary>
/// <param name="symbol">The symbol to check</param>
/// <returns>True if the symbol is a future</returns>
public static bool IsFuture(string symbol)
{
if (string.IsNullOrWhiteSpace(symbol))
return false;
return Futures.Contains(symbol.ToUpperInvariant());
}
/// <summary>
/// Check if a symbol is treated as an equity (default case)
/// </summary>
/// <param name="symbol">The symbol to check</param>
/// <returns>True if the symbol is treated as an equity</returns>
public static bool IsEquity(string symbol)
{
if (string.IsNullOrWhiteSpace(symbol))
return false;
return !IsIndex(symbol) && !IsFuture(symbol);
}
/// <summary>
/// Get the asset type for a symbol
/// </summary>
/// <param name="symbol">The symbol to check</param>
/// <returns>Asset type as string: "INDEX", "FUTURE", or "EQUITY"</returns>
public static string GetAssetType(string symbol)
{
if (IsIndex(symbol)) return "INDEX";
if (IsFuture(symbol)) return "FUTURE";
return "EQUITY";
}
/// <summary>
/// Get a list of all supported cash indices
/// </summary>
/// <returns>Array of supported cash index symbols</returns>
public static string[] GetSupportedIndices()
{
return new string[CashIndices.Count];
}
/// <summary>
/// Get a list of all supported futures
/// </summary>
/// <returns>Array of supported future symbols</returns>
public static string[] GetSupportedFutures()
{
return Futures.ToArray();
}
/// <summary>
/// Get futures by category for easier strategy configuration
/// </summary>
/// <param name="category">Category: "equity", "energy", "metals", "agricultural", "bonds", "currency", "volatility", "crypto"</param>
/// <returns>Array of futures symbols in the specified category</returns>
public static string[] GetFuturesByCategory(string category)
{
switch (category?.ToLowerInvariant())
{
case "equity":
case "index":
return new[] { "ES", "NQ", "YM", "RTY", "EMD", "NKD" };
case "energy":
return new[] { "CL", "NG", "RB", "HO", "BZ" };
case "metals":
case "metal":
return new[] { "GC", "SI", "HG", "PA", "PL" };
case "agricultural":
case "agri":
case "grains":
return new[] { "ZC", "ZS", "ZW", "ZM", "ZL", "KC", "CT", "SB", "CC", "OJ" };
case "bonds":
case "rates":
case "interest":
return new[] { "ZB", "ZN", "ZF", "TU", "UB", "ED", "SR1", "SR3" };
case "currency":
case "fx":
return new[] { "6E", "6J", "6B", "6S", "6C", "6A", "6N", "6M", "E7", "J7" };
case "volatility":
case "vol":
return new[] { "VX" };
case "crypto":
return new[] { "BTC", "ETH" };
default:
return new string[0];
}
}
/// <summary>
/// Check if a futures contract rollover is approaching based on days to expiration
/// </summary>
/// <param name="context">Algorithm context</param>
/// <param name="futureSymbol">Future symbol to check</param>
/// <param name="rolloverDays">Number of days before expiration to trigger rollover (default: 5)</param>
/// <returns>True if rollover is needed</returns>
public static bool ShouldRolloverContract(IAlgorithmContext context, Symbol futureSymbol, int rolloverDays = 5)
{
if (context?.Algorithm?.Securities == null || !context.Algorithm.Securities.ContainsKey(futureSymbol))
return false;
var security = context.Algorithm.Securities[futureSymbol];
if (security?.Type != SecurityType.Future)
return false;
try
{
// For continuous contracts, QC handles rollover automatically
// This method is for manual contract management if needed
var daysToExpiry = (security.Symbol.ID.Date - context.Algorithm.Time).Days;
return daysToExpiry <= rolloverDays;
}
catch
{
return false;
}
}
}
}using System;
using System.Collections.Generic;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.QC.Helpers
{
/// <summary>
/// Static helper for multi-asset specific parameters and calculations.
/// Provides asset-specific defaults for strike ranges, position sizing, and option filtering
/// to enable easy switching between SPX, QQQ, AAPL, etc. with optimal parameters for each.
/// </summary>
public static class MultiAssetHelper
{
/// <summary>
/// Asset-specific volatility characteristics for position sizing and strike selection
/// </summary>
private static readonly Dictionary<string, AssetProfile> AssetProfiles = new Dictionary<string, AssetProfile>
{
// Major Indices - Higher volatility, wider strikes, high margin requirements
["SPX"] = new AssetProfile { TypicalVolatility = 0.20m, StrikeWidthMultiplier = 1.5m, MinPosition = 1, MaxPosition = 3, EstimatedMarginMultiplier = 1.7m, MinAccountSize = 120000m },
["NDX"] = new AssetProfile { TypicalVolatility = 0.25m, StrikeWidthMultiplier = 1.5m, MinPosition = 1, MaxPosition = 3, EstimatedMarginMultiplier = 2.2m, MinAccountSize = 200000m },
["RUT"] = new AssetProfile { TypicalVolatility = 0.30m, StrikeWidthMultiplier = 1.5m, MinPosition = 1, MaxPosition = 3, EstimatedMarginMultiplier = 1.5m, MinAccountSize = 100000m },
["VIX"] = new AssetProfile { TypicalVolatility = 0.80m, StrikeWidthMultiplier = 2.0m, MinPosition = 1, MaxPosition = 2, EstimatedMarginMultiplier = 1.0m, MinAccountSize = 50000m },
// ETFs - Moderate volatility, lower margin requirements
["SPY"] = new AssetProfile { TypicalVolatility = 0.18m, StrikeWidthMultiplier = 1.0m, MinPosition = 1, MaxPosition = 5, EstimatedMarginMultiplier = 0.3m, MinAccountSize = 25000m },
["QQQ"] = new AssetProfile { TypicalVolatility = 0.22m, StrikeWidthMultiplier = 1.0m, MinPosition = 1, MaxPosition = 5, EstimatedMarginMultiplier = 0.4m, MinAccountSize = 30000m },
["IWM"] = new AssetProfile { TypicalVolatility = 0.28m, StrikeWidthMultiplier = 1.2m, MinPosition = 1, MaxPosition = 4, EstimatedMarginMultiplier = 0.5m, MinAccountSize = 40000m },
// Individual Stocks - Variable volatility, moderate margin requirements
["AAPL"] = new AssetProfile { TypicalVolatility = 0.35m, StrikeWidthMultiplier = 1.0m, MinPosition = 1, MaxPosition = 10, EstimatedMarginMultiplier = 0.4m, MinAccountSize = 20000m },
["TSLA"] = new AssetProfile { TypicalVolatility = 0.60m, StrikeWidthMultiplier = 1.5m, MinPosition = 1, MaxPosition = 5, EstimatedMarginMultiplier = 0.6m, MinAccountSize = 50000m },
["AMZN"] = new AssetProfile { TypicalVolatility = 0.40m, StrikeWidthMultiplier = 1.2m, MinPosition = 1, MaxPosition = 8, EstimatedMarginMultiplier = 0.5m, MinAccountSize = 30000m },
["GOOGL"] = new AssetProfile { TypicalVolatility = 0.35m, StrikeWidthMultiplier = 1.1m, MinPosition = 1, MaxPosition = 8, EstimatedMarginMultiplier = 0.5m, MinAccountSize = 30000m },
["MSFT"] = new AssetProfile { TypicalVolatility = 0.30m, StrikeWidthMultiplier = 1.0m, MinPosition = 1, MaxPosition = 10, EstimatedMarginMultiplier = 0.4m, MinAccountSize = 25000m },
// Futures - High volatility, fewer positions, high margin requirements
["ES"] = new AssetProfile { TypicalVolatility = 0.22m, StrikeWidthMultiplier = 1.3m, MinPosition = 1, MaxPosition = 3, EstimatedMarginMultiplier = 1.2m, MinAccountSize = 75000m },
["NQ"] = new AssetProfile { TypicalVolatility = 0.28m, StrikeWidthMultiplier = 1.4m, MinPosition = 1, MaxPosition = 3, EstimatedMarginMultiplier = 1.4m, MinAccountSize = 100000m },
["YM"] = new AssetProfile { TypicalVolatility = 0.20m, StrikeWidthMultiplier = 1.2m, MinPosition = 1, MaxPosition = 3, EstimatedMarginMultiplier = 1.1m, MinAccountSize = 60000m },
};
/// <summary>
/// Add multiple assets with options chains to the algorithm.
/// Uses AssetManager for each symbol and returns configured securities.
/// </summary>
/// <param name="context">The algorithm context providing access to algorithm and logger</param>
/// <param name="symbols">Array of symbols to add</param>
/// <param name="resolution">Data resolution</param>
/// <returns>Dictionary mapping symbols to their Security and options Symbol</returns>
public static Dictionary<string, (Security Security, Symbol OptionsSymbol)> AddMultiAssetOptions(
IAlgorithmContext context,
string[] symbols,
Resolution resolution = Resolution.Minute)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (symbols == null || symbols.Length == 0)
throw new ArgumentException("Symbols array cannot be null or empty", nameof(symbols));
var result = new Dictionary<string, (Security, Symbol)>();
foreach (var symbol in symbols)
{
try
{
// Add the underlying asset
var security = AssetManager.AddAsset(context, symbol, resolution);
// Add options chain
var optionsSymbol = AssetManager.AddOptionsChain(context, security, resolution);
result[symbol.ToUpperInvariant()] = (security, optionsSymbol);
((dynamic)context.Logger).Debug($"MultiAssetHelper: Successfully added {symbol} with options chain");
}
catch (Exception ex)
{
((dynamic)context.Logger).Error($"MultiAssetHelper: Failed to add {symbol}: {ex.Message}");
throw;
}
}
return result;
}
/// <summary>
/// Get asset-specific strike width for option selection.
/// Returns wider ranges for higher volatility assets.
/// </summary>
/// <param name="symbol">The underlying symbol</param>
/// <param name="baseStrikeWidth">Base strike width (in dollar terms or percentage)</param>
/// <returns>Adjusted strike width for the asset</returns>
public static decimal GetAssetStrikeWidth(string symbol, decimal baseStrikeWidth)
{
if (string.IsNullOrWhiteSpace(symbol))
return baseStrikeWidth;
var upperSymbol = symbol.ToUpperInvariant();
if (AssetProfiles.TryGetValue(upperSymbol, out var profile))
{
return baseStrikeWidth * profile.StrikeWidthMultiplier;
}
// Default for unknown symbols
return baseStrikeWidth;
}
/// <summary>
/// Get asset-specific position sizing limits.
/// Different assets have different optimal position counts based on liquidity and volatility.
/// </summary>
/// <param name="symbol">The underlying symbol</param>
/// <param name="totalPortfolioValue">Total portfolio value for percentage-based sizing</param>
/// <returns>Recommended min and max position sizes</returns>
public static (int MinPositions, int MaxPositions, decimal RecommendedAllocation) GetAssetPositionLimits(
string symbol,
decimal totalPortfolioValue)
{
if (string.IsNullOrWhiteSpace(symbol))
return (1, 5, 0.1m); // Default
var upperSymbol = symbol.ToUpperInvariant();
if (AssetProfiles.TryGetValue(upperSymbol, out var profile))
{
// Calculate recommended allocation based on volatility
// Higher volatility = smaller allocation per position
var recommendedAllocation = Math.Max(0.05m, Math.Min(0.2m, 0.15m / profile.TypicalVolatility));
return (profile.MinPosition, profile.MaxPosition, recommendedAllocation);
}
// Default for unknown symbols
return (1, 5, 0.1m);
}
/// <summary>
/// Get asset-specific delta targets for option selection.
/// Adjusts delta ranges based on typical volatility characteristics.
/// </summary>
/// <param name="symbol">The underlying symbol</param>
/// <param name="baseDeltaMin">Base minimum delta</param>
/// <param name="baseDeltaMax">Base maximum delta</param>
/// <returns>Adjusted delta range for the asset</returns>
public static (decimal DeltaMin, decimal DeltaMax) GetAssetDeltaTargets(
string symbol,
decimal baseDeltaMin,
decimal baseDeltaMax)
{
if (string.IsNullOrWhiteSpace(symbol))
return (baseDeltaMin, baseDeltaMax);
var upperSymbol = symbol.ToUpperInvariant();
if (AssetProfiles.TryGetValue(upperSymbol, out var profile))
{
// For higher volatility assets, use slightly tighter delta ranges
if (profile.TypicalVolatility > 0.4m)
{
// High vol assets: tighter delta range
var adjustment = 0.05m;
return (baseDeltaMin + adjustment, baseDeltaMax - adjustment);
}
else if (profile.TypicalVolatility < 0.2m)
{
// Low vol assets: wider delta range
var adjustment = 0.03m;
return (Math.Max(0.05m, baseDeltaMin - adjustment), Math.Min(0.45m, baseDeltaMax + adjustment));
}
}
// Default unchanged
return (baseDeltaMin, baseDeltaMax);
}
/// <summary>
/// Check if symbol has options available and is suitable for options strategies
/// </summary>
/// <param name="symbol">The symbol to check</param>
/// <returns>True if symbol is known to have liquid options</returns>
public static bool HasLiquidOptions(string symbol)
{
if (string.IsNullOrWhiteSpace(symbol))
return false;
var upperSymbol = symbol.ToUpperInvariant();
return AssetProfiles.ContainsKey(upperSymbol);
}
/// <summary>
/// Get all supported symbols for multi-asset strategies
/// </summary>
/// <returns>Array of all supported symbols</returns>
public static string[] GetSupportedSymbols()
{
var result = new string[AssetProfiles.Count];
AssetProfiles.Keys.CopyTo(result, 0);
return result;
}
/// <summary>
/// Get asset profile information for debugging/logging
/// </summary>
/// <param name="symbol">The symbol to look up</param>
/// <returns>Asset profile or null if not found</returns>
public static AssetProfile GetAssetProfile(string symbol)
{
if (string.IsNullOrWhiteSpace(symbol))
return null;
var upperSymbol = symbol.ToUpperInvariant();
return AssetProfiles.TryGetValue(upperSymbol, out var profile) ? profile : null;
}
/// <summary>
/// Get asset-specific strike increment for rounding option strikes.
/// Different assets have different strike intervals (SPX=5, SPY=1, etc.)
/// </summary>
/// <param name="symbol">The underlying symbol</param>
/// <returns>Strike increment for rounding</returns>
public static decimal GetStrikeIncrement(string symbol)
{
if (string.IsNullOrWhiteSpace(symbol))
return 1m;
var upperSymbol = symbol.ToUpperInvariant();
// Asset-specific strike increments
switch (upperSymbol)
{
case "SPX":
case "NDX":
case "RUT":
case "VIX":
return 5m; // Index options typically use $5 increments
case "SPY":
case "QQQ":
case "IWM":
return 1m; // ETF options typically use $1 increments
case "AAPL":
case "MSFT":
case "GOOGL":
case "AMZN":
case "TSLA":
return 2.5m; // High-value stocks often use $2.50 increments
case "ES":
case "NQ":
case "YM":
return 5m; // Futures options typically use $5 increments
default:
return 1m; // Default for unknown symbols
}
}
}
/// <summary>
/// Asset-specific profile containing volatility and position characteristics
/// </summary>
public class AssetProfile
{
/// <summary>
/// Typical implied volatility for the asset (used for position sizing)
/// </summary>
public decimal TypicalVolatility { get; set; }
/// <summary>
/// Multiplier for strike width selection (1.0 = default, >1.0 = wider strikes)
/// </summary>
public decimal StrikeWidthMultiplier { get; set; }
/// <summary>
/// Minimum recommended number of positions for this asset
/// </summary>
public int MinPosition { get; set; }
/// <summary>
/// Maximum recommended number of positions for this asset
/// </summary>
public int MaxPosition { get; set; }
/// <summary>
/// Estimated margin requirement as multiplier of underlying price (e.g., 1.7 = 170% of underlying)
/// </summary>
public decimal EstimatedMarginMultiplier { get; set; } = 0.3m;
/// <summary>
/// Minimum account size recommended for trading this asset's options
/// </summary>
public decimal MinAccountSize { get; set; } = 10000m;
public override string ToString()
{
return $"Vol: {TypicalVolatility:P1}, StrikeMultiplier: {StrikeWidthMultiplier:F1}x, Positions: {MinPosition}-{MaxPosition}, MinAccount: ${MinAccountSize:F0}";
}
}
}using System;
using System.Threading;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Orders;
namespace CoreAlgo.Architecture.QC.Helpers
{
/// <summary>
/// Simple extension methods for order retry logic and enhanced order management.
/// Provides minimal retry functionality without complex order management overhead.
/// </summary>
public static class OrderExtensions
{
/// <summary>
/// Submit an order with automatic retry logic on failures.
/// Retries failed orders with configurable attempts and delays.
/// </summary>
/// <param name="algorithm">The QC algorithm instance</param>
/// <param name="request">The order request to submit</param>
/// <param name="maxRetries">Maximum number of retry attempts (default: 3)</param>
/// <param name="retryDelay">Delay between retry attempts (default: 1 second)</param>
/// <returns>The OrderTicket from the successful submission, or null if all retries failed</returns>
public static OrderTicket SubmitWithRetry(this QCAlgorithm algorithm, SubmitOrderRequest request,
int maxRetries = 3, TimeSpan retryDelay = default)
{
if (algorithm == null)
throw new ArgumentNullException(nameof(algorithm));
if (request == null)
throw new ArgumentNullException(nameof(request));
if (retryDelay == default)
retryDelay = TimeSpan.FromSeconds(1);
OrderTicket ticket = null;
Exception lastException = null;
for (int attempt = 1; attempt <= maxRetries + 1; attempt++)
{
try
{
algorithm.Debug($"OrderExtensions: Submitting order attempt {attempt}/{maxRetries + 1} for {request.Symbol}");
// Submit the order using QC's standard method
if (request.OrderType == OrderType.Market)
{
ticket = algorithm.MarketOrder(request.Symbol, request.Quantity, tag: request.Tag, asynchronous: true);
}
else if (request.OrderType == OrderType.Limit)
{
ticket = algorithm.LimitOrder(request.Symbol, request.Quantity, request.LimitPrice, tag: request.Tag, asynchronous: true);
}
else
{
// For other order types, try the basic Submit method
ticket = algorithm.MarketOrder(request.Symbol, request.Quantity, tag: request.Tag, asynchronous: true);
}
if (ticket != null)
{
if (attempt > 1)
{
algorithm.Debug($"OrderExtensions: Order submitted successfully on attempt {attempt}");
}
return ticket;
}
else
{
algorithm.Debug($"OrderExtensions: Order submission returned null ticket on attempt {attempt}");
}
}
catch (Exception ex)
{
lastException = ex;
algorithm.Debug($"OrderExtensions: Order submission failed on attempt {attempt}: {ex.Message}");
// Don't retry on the last attempt
if (attempt <= maxRetries)
{
algorithm.Debug($"OrderExtensions: Waiting {retryDelay.TotalSeconds} seconds before retry");
Thread.Sleep(retryDelay);
}
}
}
// All attempts failed
algorithm.Error($"OrderExtensions: Failed to submit order after {maxRetries + 1} attempts. Last error: {lastException?.Message}");
return null;
}
/// <summary>
/// Submit a market order with retry logic.
/// Convenience method for the most common order type.
/// </summary>
/// <param name="algorithm">The QC algorithm instance</param>
/// <param name="symbol">The symbol to trade</param>
/// <param name="quantity">The quantity to trade</param>
/// <param name="maxRetries">Maximum number of retry attempts (default: 3)</param>
/// <param name="retryDelay">Delay between retry attempts (default: 1 second)</param>
/// <returns>The OrderTicket from successful submission, or null if failed</returns>
public static OrderTicket MarketOrderWithRetry(this QCAlgorithm algorithm, Symbol symbol, int quantity,
int maxRetries = 3, TimeSpan retryDelay = default)
{
var request = new SubmitOrderRequest(OrderType.Market, algorithm.Securities[symbol].Type,
symbol, quantity, 0, 0, algorithm.UtcTime, $"Market order with retry");
return algorithm.SubmitWithRetry(request, maxRetries, retryDelay);
}
/// <summary>
/// Submit a limit order with retry logic.
/// Convenience method for limit orders.
/// </summary>
/// <param name="algorithm">The QC algorithm instance</param>
/// <param name="symbol">The symbol to trade</param>
/// <param name="quantity">The quantity to trade</param>
/// <param name="limitPrice">The limit price</param>
/// <param name="maxRetries">Maximum number of retry attempts (default: 3)</param>
/// <param name="retryDelay">Delay between retry attempts (default: 1 second)</param>
/// <returns>The OrderTicket from successful submission, or null if failed</returns>
public static OrderTicket LimitOrderWithRetry(this QCAlgorithm algorithm, Symbol symbol, int quantity,
decimal limitPrice, int maxRetries = 3, TimeSpan retryDelay = default)
{
var request = new SubmitOrderRequest(OrderType.Limit, algorithm.Securities[symbol].Type,
symbol, quantity, 0, limitPrice, algorithm.UtcTime, $"Limit order with retry");
return algorithm.SubmitWithRetry(request, maxRetries, retryDelay);
}
/// <summary>
/// Check if an order ticket represents a successful order.
/// Provides simple success/failure checking.
/// </summary>
/// <param name="ticket">The order ticket to check</param>
/// <returns>True if the order was successful, false otherwise</returns>
public static bool WasSuccessful(this OrderTicket ticket)
{
if (ticket == null)
return false;
// Check order status for success indicators
var status = ticket.Status;
return status == OrderStatus.Filled ||
status == OrderStatus.PartiallyFilled ||
status == OrderStatus.Submitted;
}
/// <summary>
/// Check if an order ticket represents a failed order.
/// Provides simple failure checking.
/// </summary>
/// <param name="ticket">The order ticket to check</param>
/// <returns>True if the order failed, false otherwise</returns>
public static bool HasFailed(this OrderTicket ticket)
{
if (ticket == null)
return true;
var status = ticket.Status;
return status == OrderStatus.Invalid ||
status == OrderStatus.Canceled ||
status == OrderStatus.CancelPending;
}
/// <summary>
/// Get a human-readable description of the order status.
/// Useful for logging and debugging.
/// </summary>
/// <param name="ticket">The order ticket to describe</param>
/// <returns>A descriptive string of the order status</returns>
public static string GetStatusDescription(this OrderTicket ticket)
{
if (ticket == null)
return "Null ticket";
var status = ticket.Status;
var filled = ticket.QuantityFilled;
var remaining = ticket.Quantity - filled;
return status switch
{
OrderStatus.New => "Order created but not yet submitted",
OrderStatus.Submitted => $"Order submitted, waiting for fill",
OrderStatus.PartiallyFilled => $"Partially filled: {filled}/{ticket.Quantity}, {remaining} remaining",
OrderStatus.Filled => $"Completely filled: {filled} shares",
OrderStatus.Canceled => "Order was canceled",
OrderStatus.None => "Order status unknown",
OrderStatus.Invalid => "Order is invalid",
OrderStatus.CancelPending => "Cancel request pending",
OrderStatus.UpdateSubmitted => "Order update submitted",
_ => $"Unknown status: {status}"
};
}
/// <summary>
/// Submit a Market-On-Close order with basic error handling and logging.
/// </summary>
public static OrderTicket MarketOnCloseWithRetry(this QCAlgorithm algorithm, Symbol symbol, int quantity,
int maxRetries = 1, TimeSpan retryDelay = default, string tag = "")
{
if (retryDelay == default) retryDelay = TimeSpan.FromSeconds(1);
OrderTicket ticket = null;
for (int attempt = 1; attempt <= Math.Max(1, maxRetries); attempt++)
{
try
{
algorithm.Debug($"OrderExtensions: Submitting MOC attempt {attempt} for {symbol}");
ticket = algorithm.MarketOnCloseOrder(symbol, quantity, tag: tag, asynchronous: true);
if (ticket != null) return ticket;
}
catch (Exception ex)
{
algorithm.Debug($"OrderExtensions: MOC submission failed attempt {attempt}: {ex.Message}");
if (attempt < maxRetries) Thread.Sleep(retryDelay);
}
}
algorithm.Error($"OrderExtensions: Failed to submit MOC for {symbol}");
return ticket;
}
/// <summary>
/// Submit a Market-On-Open order with basic error handling and logging.
/// </summary>
public static OrderTicket MarketOnOpenWithRetry(this QCAlgorithm algorithm, Symbol symbol, int quantity,
int maxRetries = 1, TimeSpan retryDelay = default, string tag = "")
{
if (retryDelay == default) retryDelay = TimeSpan.FromSeconds(1);
OrderTicket ticket = null;
for (int attempt = 1; attempt <= Math.Max(1, maxRetries); attempt++)
{
try
{
algorithm.Debug($"OrderExtensions: Submitting MOO attempt {attempt} for {symbol}");
ticket = algorithm.MarketOnOpenOrder(symbol, quantity, tag: tag, asynchronous: true);
if (ticket != null) return ticket;
}
catch (Exception ex)
{
algorithm.Debug($"OrderExtensions: MOO submission failed attempt {attempt}: {ex.Message}");
if (attempt < maxRetries) Thread.Sleep(retryDelay);
}
}
algorithm.Error($"OrderExtensions: Failed to submit MOO for {symbol}");
return ticket;
}
}
}using System;
using QuantConnect.Securities;
namespace CoreAlgo.Architecture.QC.Helpers
{
/// <summary>
/// Simple static helper for position size calculations.
/// Provides percentage-based and fixed sizing methods for different asset types.
/// Handles the complexity of options contract multipliers and different security types.
/// </summary>
public static class PositionSizer
{
/// <summary>
/// Standard options contract multiplier (100 shares per contract)
/// </summary>
public const int StandardOptionsMultiplier = 100;
/// <summary>
/// Calculate position quantity based on percentage allocation of portfolio.
/// Works for stocks, futures, and other direct securities.
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="allocationPercent">Percentage of portfolio to allocate (e.g., 0.1 = 10%)</param>
/// <param name="price">Current price of the security</param>
/// <returns>Quantity to purchase (number of shares/contracts)</returns>
/// <exception cref="ArgumentNullException">If portfolio is null</exception>
/// <exception cref="ArgumentException">If allocation or price is invalid</exception>
public static int CalculateQuantity(SecurityPortfolioManager portfolio, decimal allocationPercent, decimal price)
{
if (portfolio == null)
throw new ArgumentNullException(nameof(portfolio));
if (allocationPercent <= 0 || allocationPercent > 1)
throw new ArgumentException("Allocation percent must be between 0 and 1", nameof(allocationPercent));
if (price <= 0)
throw new ArgumentException("Price must be greater than 0", nameof(price));
// Calculate allocation amount from total portfolio value
var totalValue = portfolio.TotalPortfolioValue;
var allocationAmount = totalValue * allocationPercent;
// Calculate quantity based on price
var quantity = (int)Math.Floor(allocationAmount / price);
return Math.Max(0, quantity);
}
/// <summary>
/// Calculate position quantity for options contracts.
/// Accounts for the contract multiplier (typically 100 shares per contract).
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="allocationPercent">Percentage of portfolio to allocate (e.g., 0.1 = 10%)</param>
/// <param name="premium">Premium price per contract</param>
/// <param name="multiplier">Contract multiplier (default: 100 for standard options)</param>
/// <returns>Number of options contracts to purchase</returns>
/// <exception cref="ArgumentNullException">If portfolio is null</exception>
/// <exception cref="ArgumentException">If allocation, premium, or multiplier is invalid</exception>
public static int CalculateOptionsQuantity(SecurityPortfolioManager portfolio, decimal allocationPercent,
decimal premium, int multiplier = StandardOptionsMultiplier)
{
if (portfolio == null)
throw new ArgumentNullException(nameof(portfolio));
if (allocationPercent <= 0 || allocationPercent > 1)
throw new ArgumentException("Allocation percent must be between 0 and 1", nameof(allocationPercent));
if (premium <= 0)
throw new ArgumentException("Premium must be greater than 0", nameof(premium));
if (multiplier <= 0)
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
// Calculate allocation amount from total portfolio value
var totalValue = portfolio.TotalPortfolioValue;
var allocationAmount = totalValue * allocationPercent;
// Calculate cost per contract (premium * multiplier)
var costPerContract = premium * multiplier;
// Calculate number of contracts
var contracts = (int)Math.Floor(allocationAmount / costPerContract);
return Math.Max(0, contracts);
}
/// <summary>
/// Calculate position quantity using a fixed dollar amount.
/// Alternative to percentage-based sizing.
/// </summary>
/// <param name="dollarAmount">Fixed dollar amount to invest</param>
/// <param name="price">Current price of the security</param>
/// <returns>Quantity to purchase</returns>
/// <exception cref="ArgumentException">If dollar amount or price is invalid</exception>
public static int CalculateFixedDollarQuantity(decimal dollarAmount, decimal price)
{
if (dollarAmount <= 0)
throw new ArgumentException("Dollar amount must be greater than 0", nameof(dollarAmount));
if (price <= 0)
throw new ArgumentException("Price must be greater than 0", nameof(price));
var quantity = (int)Math.Floor(dollarAmount / price);
return Math.Max(0, quantity);
}
/// <summary>
/// Calculate options quantity using a fixed dollar amount.
/// </summary>
/// <param name="dollarAmount">Fixed dollar amount to invest</param>
/// <param name="premium">Premium price per contract</param>
/// <param name="multiplier">Contract multiplier (default: 100 for standard options)</param>
/// <returns>Number of options contracts to purchase</returns>
/// <exception cref="ArgumentException">If parameters are invalid</exception>
public static int CalculateFixedDollarOptionsQuantity(decimal dollarAmount, decimal premium,
int multiplier = StandardOptionsMultiplier)
{
if (dollarAmount <= 0)
throw new ArgumentException("Dollar amount must be greater than 0", nameof(dollarAmount));
if (premium <= 0)
throw new ArgumentException("Premium must be greater than 0", nameof(premium));
if (multiplier <= 0)
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
// Calculate cost per contract
var costPerContract = premium * multiplier;
// Calculate number of contracts
var contracts = (int)Math.Floor(dollarAmount / costPerContract);
return Math.Max(0, contracts);
}
/// <summary>
/// Calculate the maximum safe position size to avoid overcommitting portfolio.
/// Includes a safety buffer to account for price movements and fees.
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="allocationPercent">Desired percentage allocation</param>
/// <param name="price">Current price of the security</param>
/// <param name="safetyBuffer">Safety buffer as percentage (e.g., 0.05 = 5% buffer)</param>
/// <returns>Safe quantity to purchase</returns>
public static int CalculateSafeQuantity(SecurityPortfolioManager portfolio, decimal allocationPercent,
decimal price, decimal safetyBuffer = 0.05m)
{
if (safetyBuffer < 0 || safetyBuffer > 0.5m)
throw new ArgumentException("Safety buffer must be between 0 and 0.5", nameof(safetyBuffer));
// Adjust allocation for safety buffer
var adjustedAllocation = allocationPercent * (1 - safetyBuffer);
return CalculateQuantity(portfolio, adjustedAllocation, price);
}
/// <summary>
/// Calculate safe options quantity with buffer.
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="allocationPercent">Desired percentage allocation</param>
/// <param name="premium">Premium price per contract</param>
/// <param name="safetyBuffer">Safety buffer as percentage (default: 5%)</param>
/// <param name="multiplier">Contract multiplier (default: 100)</param>
/// <returns>Safe number of options contracts to purchase</returns>
public static int CalculateSafeOptionsQuantity(SecurityPortfolioManager portfolio, decimal allocationPercent,
decimal premium, decimal safetyBuffer = 0.05m, int multiplier = StandardOptionsMultiplier)
{
if (safetyBuffer < 0 || safetyBuffer > 0.5m)
throw new ArgumentException("Safety buffer must be between 0 and 0.5", nameof(safetyBuffer));
// Adjust allocation for safety buffer
var adjustedAllocation = allocationPercent * (1 - safetyBuffer);
return CalculateOptionsQuantity(portfolio, adjustedAllocation, premium, multiplier);
}
/// <summary>
/// Get the effective buying power for position sizing.
/// Accounts for existing positions and available cash.
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <returns>Available buying power for new positions</returns>
public static decimal GetAvailableBuyingPower(SecurityPortfolioManager portfolio)
{
if (portfolio == null)
throw new ArgumentNullException(nameof(portfolio));
// Use QC's available cash as buying power
return portfolio.Cash;
}
/// <summary>
/// Calculate position value for risk management.
/// Useful for tracking total exposure.
/// </summary>
/// <param name="quantity">Number of shares/contracts</param>
/// <param name="price">Current price</param>
/// <param name="multiplier">Contract multiplier (1 for stocks, 100 for options)</param>
/// <returns>Total position value</returns>
public static decimal CalculatePositionValue(int quantity, decimal price, int multiplier = 1)
{
if (quantity < 0)
throw new ArgumentException("Quantity cannot be negative", nameof(quantity));
if (price < 0)
throw new ArgumentException("Price cannot be negative", nameof(price));
if (multiplier <= 0)
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
return quantity * price * multiplier;
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Orders;
using QuantConnect.Securities;
namespace CoreAlgo.Architecture.QC.Helpers
{
/// <summary>
/// Utility helpers for aligning order prices to brokerage tick size requirements.
/// Centralizes interaction with QuantConnect price variation models so strategies,
/// wrappers, and services share the same rounding semantics.
/// </summary>
public static class PriceRounding
{
/// <summary>
/// Resolve the minimum price variation (tick size) for a security using QC-native
/// price variation models, falling back to symbol properties and pragmatic defaults.
/// </summary>
public static decimal GetMinPriceVariation(Security security)
{
if (security == null)
{
return 0.01m;
}
try
{
var tick = security.PriceVariationModel?.GetMinimumPriceVariation(new GetMinimumPriceVariationParameters(security, security.Price)) ?? 0m;
if (tick <= 0m)
{
tick = security.SymbolProperties?.MinimumPriceVariation ?? 0m;
}
if (tick > 0m)
{
return tick;
}
if (security.Type == SecurityType.Equity)
{
return security.Price < 1m ? 0.0001m : 0.01m;
}
}
catch
{
// Swallow and fall back to defaults below.
}
return 0.01m;
}
/// <summary>
/// Convenience overload using SecuritiesManager for easy access within strategies.
/// </summary>
public static decimal GetMinPriceVariation(SecurityManager securities, Symbol symbol)
{
if (securities == null || symbol == null)
{
return 0.01m;
}
return securities.TryGetValue(symbol, out var security)
? GetMinPriceVariation(security)
: 0.01m;
}
public static decimal FloorToTick(decimal price, decimal tick)
{
return tick > 0m ? Math.Floor(price / tick) * tick : price;
}
public static decimal CeilToTick(decimal price, decimal tick)
{
return tick > 0m ? Math.Ceiling(price / tick) * tick : price;
}
public static decimal RoundToNearestTick(decimal price, decimal tick)
{
return tick > 0m ? Math.Round(price / tick, MidpointRounding.AwayFromZero) * tick : price;
}
/// <summary>
/// Direction-aware rounding for limit prices (buy=floor, sell=ceil).
/// </summary>
public static decimal RoundLimitPrice(SecurityManager securities, Symbol symbol, decimal quantity, decimal limitPrice)
{
var tick = GetMinPriceVariation(securities, symbol);
return quantity >= 0 ? FloorToTick(limitPrice, tick) : CeilToTick(limitPrice, tick);
}
/// <summary>
/// Direction-aware rounding for stop prices (buy=ceil, sell=floor).
/// </summary>
public static decimal RoundStopPrice(SecurityManager securities, Symbol symbol, decimal quantity, decimal stopPrice)
{
var tick = GetMinPriceVariation(securities, symbol);
return quantity >= 0 ? CeilToTick(stopPrice, tick) : FloorToTick(stopPrice, tick);
}
/// <summary>
/// Round trailing stop amount to ensure dynamically calculated stop prices align with tick size.
/// For dollar trailing, we use ceiling to ensure sufficient protection distance.
/// </summary>
public static decimal RoundTrailingStopPrice(SecurityManager securities, Symbol symbol, decimal quantity, decimal trailingAmount, bool isDollarTrailing)
{
var tick = GetMinPriceVariation(securities, symbol);
// Always ceil trailing amounts to ensure adequate stop distance and tick alignment
return CeilToTick(trailingAmount, tick);
}
/// <summary>
/// Direction-aware rounding for stop-limit orders combining stop and limit semantics.
/// Enforces valid stop/limit relationships post-rounding to prevent order rejection:
/// - Buy stop-limit: limit >= stop + tick (limit must be at or above trigger)
/// - Sell stop-limit: limit <= stop - tick (limit must be at or below trigger)
/// Hardened to handle edge cases: tick<=0, negative prices, and insufficient buffers
/// </summary>
public static (decimal RoundedStop, decimal RoundedLimit) RoundStopLimitPrices(
SecurityManager securities,
Symbol symbol,
decimal quantity,
decimal stopPrice,
decimal limitPrice)
{
var tick = GetMinPriceVariation(securities, symbol);
// Harden: ensure tick is always valid (fallback to 0.01 if tick resolution fails)
if (tick <= 0m)
{
tick = 0.01m;
}
// Guard: reject negative or zero prices
if (stopPrice <= 0m || limitPrice <= 0m)
{
throw new ArgumentException($"Invalid stop-limit prices: stop={stopPrice}, limit={limitPrice} must be > 0");
}
if (quantity >= 0)
{
// Buy stop-limit: stop rounds up, limit rounds down
var roundedStop = CeilToTick(stopPrice, tick);
var roundedLimit = FloorToTick(limitPrice, tick);
// Enforce limit >= stop + tick for buy stop-limits
if (roundedLimit < roundedStop + tick)
{
roundedLimit = roundedStop + tick;
}
// Final safety: if still invalid, nudge limit up by one more tick
if (roundedLimit < roundedStop)
{
roundedLimit = roundedStop + tick;
}
return (roundedStop, roundedLimit);
}
else
{
// Sell stop-limit: stop rounds down, limit rounds up
var roundedStop = FloorToTick(stopPrice, tick);
var roundedLimit = CeilToTick(limitPrice, tick);
// Enforce limit <= stop - tick for sell stop-limits
// Critical: For sell stop-limits, the limit MUST be below the stop
if (roundedLimit > roundedStop - tick)
{
roundedLimit = roundedStop - tick;
}
// Additional safety check: if they're still equal or inverted after clamping,
// force a valid separation (this can happen with very coarse ticks)
if (roundedLimit >= roundedStop)
{
roundedLimit = roundedStop - tick;
}
// Final safety: ensure at least 1 tick separation for sells
if (roundedLimit >= roundedStop - tick)
{
roundedLimit = roundedStop - tick;
}
// Guard: if limit is now <= 0 after clamping, this order is invalid
if (roundedLimit <= 0m)
{
throw new ArgumentException($"Stop-limit prices invalid after rounding: stop={roundedStop}, limit={roundedLimit}, tick={tick}. Buffer too small for tick size.");
}
return (roundedStop, roundedLimit);
}
}
/// <summary>
/// Calculate a combo order net-price tick size based on member legs.
/// Uses the smallest leg tick as pragmatic baseline.
/// </summary>
public static decimal GetComboNetTick(SecurityManager securities, IReadOnlyList<Leg> legs)
{
if (legs == null || legs.Count == 0)
{
return 0.01m;
}
var ticks = legs
.Select(leg => securities != null && securities.TryGetValue(leg.Symbol, out var security)
? GetMinPriceVariation(security)
: 0.01m)
.Where(t => t > 0m);
var minTick = ticks.DefaultIfEmpty(0.01m).Min();
return minTick > 0m ? minTick : 0.01m;
}
/// <summary>
/// Round combo net price (debit=floor, credit=ceil) based on aggregate tick size.
/// </summary>
public static decimal RoundComboNetPrice(
SecurityManager securities,
IReadOnlyList<Leg> legs,
decimal netPrice,
bool isDebit)
{
var netTick = GetComboNetTick(securities, legs);
return isDebit ? FloorToTick(netPrice, netTick) : CeilToTick(netPrice, netTick);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using CoreAlgo.Architecture.Core.Models;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data.Market;
using QuantConnect.Securities.Option;
namespace CoreAlgo.Architecture.QC.Helpers
{
/// <summary>
/// Calculates optimal strike ranges based on delta targets and market conditions
/// </summary>
public class StrikeRangeCalculator
{
private readonly QCAlgorithm _algorithm;
private readonly decimal _highVolThreshold;
private readonly decimal _volAdjustment;
public StrikeRangeCalculator(QCAlgorithm algorithm,
decimal highVolThreshold = 0.30m, decimal volAdjustment = 0.05m)
{
_algorithm = algorithm;
_highVolThreshold = highVolThreshold;
_volAdjustment = volAdjustment;
}
/// <summary>
/// Represents delta targets for option strategies
/// </summary>
public class DeltaTargets
{
public decimal ShortPut { get; set; }
public decimal LongPut { get; set; }
public decimal ShortCall { get; set; }
public decimal LongCall { get; set; }
}
/// <summary>
/// Represents selected strikes for a strategy
/// </summary>
public class StrikeRange
{
public decimal ShortPutStrike { get; set; }
public decimal LongPutStrike { get; set; }
public decimal ShortCallStrike { get; set; }
public decimal LongCallStrike { get; set; }
public decimal ATMStrike { get; set; }
}
/// <summary>
/// Gets asset-specific delta targets adjusted for current volatility
/// </summary>
public DeltaTargets GetAssetSpecificDeltas(string symbol, decimal currentVolatility)
{
// Get base delta targets from MultiAssetHelper
var (deltaMin, deltaMax) = MultiAssetHelper.GetAssetDeltaTargets(symbol, 0.15m, 0.25m);
// Adjust deltas based on volatility regime
var adjustment = currentVolatility > _highVolThreshold ? _volAdjustment : 0m;
return new DeltaTargets
{
// In high volatility, move strikes further OTM
ShortPut = Math.Max(0.05m, deltaMin - adjustment),
LongPut = Math.Max(0.01m, deltaMin - adjustment - 0.05m),
ShortCall = Math.Max(0.05m, deltaMin + adjustment),
LongCall = Math.Max(0.01m, deltaMin + adjustment + 0.05m)
};
}
/// <summary>
/// Calculates optimal strike range for the given option chain
/// </summary>
public StrikeRange CalculateStrikeRange(OptionChain chain, DeltaTargets targets)
{
if (chain == null || !chain.Any())
{
throw new InvalidOperationException("Option chain is empty");
}
var underlying = chain.Underlying;
var atmStrike = GetATMStrike(chain);
var strikes = new StrikeRange { ATMStrike = atmStrike };
// Separate puts and calls
var puts = chain.Where(x => x.Right == OptionRight.Put)
.OrderBy(x => x.Strike)
.ToList();
var calls = chain.Where(x => x.Right == OptionRight.Call)
.OrderBy(x => x.Strike)
.ToList();
// Find strikes closest to target deltas
strikes.ShortPutStrike = FindStrikeByDelta(puts, targets.ShortPut, atmStrike);
strikes.LongPutStrike = FindStrikeByDelta(puts, targets.LongPut, atmStrike);
strikes.ShortCallStrike = FindStrikeByDelta(calls, -targets.ShortCall, atmStrike); // Call deltas are negative
strikes.LongCallStrike = FindStrikeByDelta(calls, -targets.LongCall, atmStrike);
// Validate and adjust strikes
return ValidateAndAdjustStrikes(strikes, chain.Symbol.Underlying.Value);
}
/// <summary>
/// Gets the at-the-money strike price
/// </summary>
public decimal GetATMStrike(OptionChain chain)
{
var underlyingPrice = chain.Underlying.Price;
// Find the strike closest to the underlying price
var atmStrike = chain
.Select(x => x.Strike)
.Distinct()
.OrderBy(strike => Math.Abs(strike - underlyingPrice))
.FirstOrDefault();
return atmStrike > 0 ? atmStrike : underlyingPrice;
}
/// <summary>
/// Finds the strike price closest to the target delta
/// </summary>
private decimal FindStrikeByDelta(List<OptionContract> contracts, decimal targetDelta, decimal atmStrike)
{
if (!contracts.Any()) return atmStrike;
OptionContract bestContract = null;
decimal bestDeltaDiff = decimal.MaxValue;
foreach (var contract in contracts)
{
// Skip if Greeks aren't available
if (contract.Greeks?.Delta == null) continue;
var deltaDiff = Math.Abs(contract.Greeks.Delta - targetDelta);
if (deltaDiff < bestDeltaDiff)
{
bestDeltaDiff = deltaDiff;
bestContract = contract;
}
}
// If no contract with Greeks found, use strike selection based on distance from ATM
if (bestContract == null)
{
return EstimateStrikeByDelta(contracts, targetDelta, atmStrike);
}
return bestContract.Strike;
}
/// <summary>
/// Estimates strike when Greeks aren't available
/// </summary>
private decimal EstimateStrikeByDelta(List<OptionContract> contracts, decimal targetDelta, decimal atmStrike)
{
var isPut = contracts.FirstOrDefault()?.Right == OptionRight.Put;
var absTargetDelta = Math.Abs(targetDelta);
// Rough approximation: 0.50 delta at ATM, decreases as we move OTM
decimal targetDistance;
if (absTargetDelta >= 0.40m) targetDistance = 0.02m; // 2% OTM
else if (absTargetDelta >= 0.30m) targetDistance = 0.04m; // 4% OTM
else if (absTargetDelta >= 0.20m) targetDistance = 0.06m; // 6% OTM
else if (absTargetDelta >= 0.10m) targetDistance = 0.10m; // 10% OTM
else targetDistance = 0.15m; // 15% OTM
var targetStrike = isPut
? atmStrike * (1 - targetDistance)
: atmStrike * (1 + targetDistance);
// Find closest available strike
return contracts
.Select(x => x.Strike)
.OrderBy(strike => Math.Abs(strike - targetStrike))
.FirstOrDefault();
}
/// <summary>
/// Validates and adjusts strikes to ensure proper spread structure
/// </summary>
private StrikeRange ValidateAndAdjustStrikes(StrikeRange strikes, string symbol)
{
var increment = MultiAssetHelper.GetStrikeIncrement(symbol);
var minSpreadWidth = increment * 2; // Minimum 2 strikes apart
// Ensure put spreads are valid
if (strikes.ShortPutStrike - strikes.LongPutStrike < minSpreadWidth)
{
strikes.LongPutStrike = strikes.ShortPutStrike - minSpreadWidth;
}
// Ensure call spreads are valid
if (strikes.LongCallStrike - strikes.ShortCallStrike < minSpreadWidth)
{
strikes.LongCallStrike = strikes.ShortCallStrike + minSpreadWidth;
}
// Round strikes to proper increments
strikes.ShortPutStrike = RoundToIncrement(strikes.ShortPutStrike, increment);
strikes.LongPutStrike = RoundToIncrement(strikes.LongPutStrike, increment);
strikes.ShortCallStrike = RoundToIncrement(strikes.ShortCallStrike, increment);
strikes.LongCallStrike = RoundToIncrement(strikes.LongCallStrike, increment);
return strikes;
}
/// <summary>
/// Rounds a strike price to the nearest valid increment
/// </summary>
private decimal RoundToIncrement(decimal strike, decimal increment)
{
return Math.Round(strike / increment) * increment;
}
/// <summary>
/// Validates strike spacing using structural analysis instead of strategy names
/// </summary>
public bool ValidateStrikeSpacing(StrikeRange strikes)
{
// Detect strategy pattern from strike structure
var hasLongStrikes = strikes.LongPutStrike > 0 || strikes.LongCallStrike > 0;
var hasShortStrikes = strikes.ShortPutStrike > 0 || strikes.ShortCallStrike > 0;
var hasBothPutAndCall = strikes.ShortPutStrike > 0 && strikes.ShortCallStrike > 0;
// 4-strike pattern (Iron Condor/Butterfly structure)
if (hasLongStrikes && hasShortStrikes && hasBothPutAndCall &&
strikes.LongPutStrike > 0 && strikes.LongCallStrike > 0)
{
// Validate 4-leg structure: Long Put < Short Put < ATM < Short Call < Long Call
return strikes.LongPutStrike < strikes.ShortPutStrike &&
strikes.ShortPutStrike < strikes.ATMStrike &&
strikes.ATMStrike < strikes.ShortCallStrike &&
strikes.ShortCallStrike < strikes.LongCallStrike;
}
// 2-strike pattern with both puts and calls (Strangle/Straddle structure)
if (hasShortStrikes && hasBothPutAndCall && !hasLongStrikes)
{
// Validate short strikes are on opposite sides of ATM
return strikes.ShortPutStrike < strikes.ATMStrike &&
strikes.ShortCallStrike > strikes.ATMStrike;
}
// Single-leg or spread patterns - basic validation
return ValidateBasicStrikeOrder(strikes);
}
/// <summary>
/// Validates basic strike ordering for any strategy pattern
/// </summary>
private bool ValidateBasicStrikeOrder(StrikeRange strikes)
{
// Ensure put strikes are below ATM and call strikes are above ATM
bool validPutSide = strikes.ShortPutStrike <= 0 || strikes.ShortPutStrike < strikes.ATMStrike;
bool validCallSide = strikes.ShortCallStrike <= 0 || strikes.ShortCallStrike > strikes.ATMStrike;
// Ensure long strikes are outside short strikes if both exist
bool validLongPut = strikes.LongPutStrike <= 0 || strikes.ShortPutStrike <= 0 ||
strikes.LongPutStrike < strikes.ShortPutStrike;
bool validLongCall = strikes.LongCallStrike <= 0 || strikes.ShortCallStrike <= 0 ||
strikes.LongCallStrike > strikes.ShortCallStrike;
return validPutSide && validCallSide && validLongPut && validLongCall;
}
/// <summary>
/// Gets strike selection statistics for reporting
/// </summary>
public Dictionary<string, object> GetStrikeStats(StrikeRange strikes)
{
return new Dictionary<string, object>
{
["ATMStrike"] = strikes.ATMStrike,
["PutSpreadWidth"] = strikes.ShortPutStrike - strikes.LongPutStrike,
["CallSpreadWidth"] = strikes.LongCallStrike - strikes.ShortCallStrike,
["TotalWidth"] = strikes.LongCallStrike - strikes.LongPutStrike,
["PutDistance"] = (strikes.ATMStrike - strikes.ShortPutStrike) / strikes.ATMStrike,
["CallDistance"] = (strikes.ShortCallStrike - strikes.ATMStrike) / strikes.ATMStrike
};
}
}
}using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;
namespace CoreAlgo.Architecture.QC.Helpers
{
/// <summary>
/// Helper class for optimizing large universe strategies.
/// Provides efficient batch processing, memory management, and QC-native patterns.
/// </summary>
public static class UniverseOptimizer
{
/// <summary>
/// Fetches historical data in batches to avoid memory issues with large universes.
/// Processes symbols in chunks to prevent timeouts and optimize performance.
/// </summary>
/// <param name="algorithm">The QCAlgorithm instance</param>
/// <param name="symbols">List of symbols to fetch data for</param>
/// <param name="days">Number of days of history</param>
/// <param name="resolution">Data resolution</param>
/// <param name="batchSize">Number of symbols to process per batch (default: 500)</param>
/// <returns>Dictionary mapping Symbol to list of TradeBar volumes</returns>
public static Dictionary<Symbol, List<decimal>> BatchFetchHistory(
QCAlgorithm algorithm,
IEnumerable<Symbol> symbols,
int days,
Resolution resolution = Resolution.Daily,
int batchSize = 500)
{
var results = new ConcurrentDictionary<Symbol, List<decimal>>();
var symbolList = symbols.ToList();
// Process in batches to avoid memory issues and timeouts
for (int i = 0; i < symbolList.Count; i += batchSize)
{
var batch = symbolList.Skip(i).Take(batchSize).ToList();
try
{
var history = algorithm.History<TradeBar>(batch, days, resolution);
foreach (var dateGroup in history)
{
foreach (var symbolData in dateGroup)
{
var symbol = symbolData.Key;
var bar = symbolData.Value;
if (!results.ContainsKey(symbol))
{
results[symbol] = new List<decimal>();
}
results[symbol].Add(bar.Volume);
}
}
}
catch (Exception ex)
{
algorithm.Log($"Error fetching history for batch starting at index {i}: {ex.Message}");
}
}
return results.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
/// <summary>
/// Calculates Average Daily Volume (ADV) for multiple symbols efficiently.
/// Uses batch processing to handle large universes without memory issues.
/// </summary>
/// <param name="algorithm">The QCAlgorithm instance</param>
/// <param name="symbols">Symbols to calculate ADV for</param>
/// <param name="days">Number of days for ADV calculation</param>
/// <returns>Dictionary mapping Symbol to ADV value</returns>
public static Dictionary<Symbol, decimal> CalculateBatchedADV(
QCAlgorithm algorithm,
IEnumerable<Symbol> symbols,
int days = 21)
{
var historyData = BatchFetchHistory(algorithm, symbols, days, Resolution.Daily);
var advResults = new Dictionary<Symbol, decimal>();
foreach (var kvp in historyData)
{
var symbol = kvp.Key;
var volumes = kvp.Value;
if (volumes.Any())
{
advResults[symbol] = volumes.Average();
}
else
{
advResults[symbol] = 0;
}
}
return advResults;
}
/// <summary>
/// Optimized volume shock calculation for large universes.
/// Compares current intraday volume to historical average efficiently.
/// </summary>
/// <param name="algorithm">The QCAlgorithm instance</param>
/// <param name="intradayVolumes">Current intraday volume data</param>
/// <param name="symbols">Universe symbols to process</param>
/// <param name="advDays">Days for ADV calculation</param>
/// <returns>Dictionary mapping Symbol to volume shock ratio</returns>
public static Dictionary<Symbol, decimal> CalculateVolumeShock(
QCAlgorithm algorithm,
ConcurrentDictionary<Symbol, long> intradayVolumes,
IEnumerable<Symbol> symbols,
int advDays = 21)
{
// Get ADV for all symbols in batch
var advData = CalculateBatchedADV(algorithm, symbols, advDays);
var shockRatios = new Dictionary<Symbol, decimal>();
foreach (var symbol in symbols)
{
var intradayVol = intradayVolumes.GetValueOrDefault(symbol, 0L);
var adv = advData.GetValueOrDefault(symbol, 0m);
var ratio = adv > 0 ? (decimal)intradayVol / adv : 0m;
shockRatios[symbol] = ratio;
}
return shockRatios;
}
/// <summary>
/// Efficiently cleans up removed securities from tracking structures.
/// Prevents memory leaks in large universe strategies.
/// </summary>
/// <param name="algorithm">The QCAlgorithm instance</param>
/// <param name="changes">Security changes from OnSecuritiesChanged</param>
/// <param name="trackingDictionaries">Collection of dictionaries to clean up</param>
public static void CleanupRemovedSecurities(
QCAlgorithm algorithm,
SecurityChanges changes,
params object[] trackingDictionaries)
{
foreach (var removed in changes.RemovedSecurities)
{
// Liquidate any positions in removed securities
if (algorithm.Portfolio[removed.Symbol].Invested)
{
algorithm.Log($"Liquidating position in removed security: {removed.Symbol}");
algorithm.Liquidate(symbol: removed.Symbol, tag: "Security removed from universe", asynchronous: true);
}
// Clean up tracking dictionaries
foreach (var dict in trackingDictionaries)
{
switch (dict)
{
case ConcurrentDictionary<Symbol, long> longDict:
longDict.TryRemove(removed.Symbol, out _);
break;
case ConcurrentDictionary<Symbol, decimal> decimalDict:
decimalDict.TryRemove(removed.Symbol, out _);
break;
case Dictionary<Symbol, long> simpleLongDict:
simpleLongDict.Remove(removed.Symbol);
break;
case Dictionary<Symbol, decimal> simpleDecimalDict:
simpleDecimalDict.Remove(removed.Symbol);
break;
case HashSet<Symbol> symbolSet:
symbolSet.Remove(removed.Symbol);
break;
}
}
}
}
/// <summary>
/// Sets up optimized universe settings for large universe strategies.
/// Configures async selection and other performance optimizations.
/// </summary>
/// <param name="algorithm">The QCAlgorithm instance</param>
/// <param name="resolution">Universe data resolution</param>
/// <param name="enableAsync">Enable asynchronous universe selection</param>
/// <param name="extendedHours">Enable extended market hours</param>
public static void SetupOptimizedUniverse(
QCAlgorithm algorithm,
Resolution resolution = Resolution.Minute,
bool enableAsync = true,
bool extendedHours = false)
{
algorithm.UniverseSettings.Resolution = resolution;
algorithm.UniverseSettings.Asynchronous = enableAsync;
algorithm.UniverseSettings.ExtendedMarketHours = extendedHours;
algorithm.Log($"Universe configured: Resolution={resolution}, Async={enableAsync}, ExtendedHours={extendedHours}");
}
/// <summary>
/// Processes a large universe in parallel for improved performance.
/// Useful for computationally intensive universe selection logic.
/// </summary>
/// <typeparam name="TInput">Input data type</typeparam>
/// <typeparam name="TResult">Result data type</typeparam>
/// <param name="data">Input data collection</param>
/// <param name="processor">Function to process each item</param>
/// <param name="batchSize">Batch size for processing</param>
/// <returns>Collection of results</returns>
public static IEnumerable<TResult> ProcessInParallel<TInput, TResult>(
IEnumerable<TInput> data,
Func<TInput, TResult> processor,
int batchSize = 500)
{
var results = new ConcurrentBag<TResult>();
var batches = data.Batch(batchSize);
System.Threading.Tasks.Parallel.ForEach(batches, batch =>
{
foreach (var item in batch)
{
try
{
var result = processor(item);
results.Add(result);
}
catch
{
// Log error but continue processing
}
}
});
return results;
}
}
/// <summary>
/// Extension methods for universe optimization
/// </summary>
public static class UniverseExtensions
{
/// <summary>
/// Batches an enumerable into chunks of specified size
/// </summary>
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int batchSize)
{
var batch = new List<T>(batchSize);
foreach (var item in source)
{
batch.Add(item);
if (batch.Count == batchSize)
{
yield return batch;
batch = new List<T>(batchSize);
}
}
if (batch.Count > 0)
yield return batch;
}
}
}#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Selection;
using QuantConnect.Api;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Commands;
using QuantConnect.Configuration;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.Shortable;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.OptionExercise;
using QuantConnect.Orders.Slippage;
using QuantConnect.Orders.TimeInForces;
using QuantConnect.Python;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Positions;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Securities.Volatility;
using QuantConnect.Storage;
using QuantConnect.Statistics;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
using Calendar = QuantConnect.Data.Consolidators.Calendar;
using CoreAlgo.Architecture.Core.Interfaces;
using CoreAlgo.Architecture.Core.Implementations;
using CoreAlgo.Architecture.Core.Templates;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.Core.Services;
#endregion
namespace QuantConnect.Algorithm.CSharp
{
public class CoreAlgo : QCAlgorithm
{
private IStrategy _strategy;
private int _debugCallCount = 0;
public QCLogger<CoreAlgo> Logger { get; private set; }
private bool _postWarmupSliceLogged = false;
private bool _warmupCompleteLogged = false;
private bool _dailySnapshotSaved = false;
// Static constructor to verify class loading
static CoreAlgo()
{
// This should execute when the class is first loaded
System.Diagnostics.Debug.WriteLine("=== STATIC CONSTRUCTOR: CoreAlgo class loaded ===");
}
// Constructor to verify instance creation
public CoreAlgo()
{
// This should execute when instance is created
System.Diagnostics.Debug.WriteLine("=== CONSTRUCTOR: CoreAlgo instance created ===");
}
public override void Initialize()
{
try
{
// Apply basic algorithm configuration using GetParameter with defaults
var environment = GetParameter("Environment", "development");
var startDate = GetParameter("StartDate", "2023-12-25");
var endDate = GetParameter("EndDate", "2024-12-25");
var accountSize = GetParameter("AccountSize", 100000m);
SetStartDate(DateTime.Parse(startDate));
SetEndDate(DateTime.Parse(endDate));
SetCash(accountSize);
SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
// CRITICAL FIX: Disable LEAN's broken position group buying power model
// LEAN doesn't recognize combo orders as spreads and calculates margin incorrectly
Portfolio.SetPositions(SecurityPositionGroupModel.Null);
// Global security initializer - disables buying power for all securities (matches Python)
SetSecurityInitializer(CompleteSecurityInitializer);
// Ensure universe-added securities (including option chain universes) are intraday
UniverseSettings.Resolution = Resolution.Minute;
UniverseSettings.ExtendedMarketHours = false;
UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw;
Log($"UNIVERSE SETTINGS -> Resolution: {UniverseSettings.Resolution}, FillForward: {UniverseSettings.FillForward}, ExtendedMktHours: {UniverseSettings.ExtendedMarketHours}, DataNormalizationMode: {UniverseSettings.DataNormalizationMode}");
Logger?.Info($"UniverseSettings configured. Resolution={UniverseSettings.Resolution}, FillForward={UniverseSettings.FillForward}, ExtendedMarketHours={UniverseSettings.ExtendedMarketHours}, DataNormalizationMode={UniverseSettings.DataNormalizationMode}");
Log("BASIC SETUP COMPLETED");
// Initialize smart logger with debug logging
Log("=== DEBUG: About to create QCLogger ===");
var logLevel = GetParameter("LogLevel", 3); // Default to Info level (2 = Info, 3 = Debug)
var verboseMode = bool.Parse(GetParameter("VerboseMode", "true")); // Allow forcing verbose output in backtest
if (verboseMode)
{
Log("=== VERBOSE MODE ENABLED - All INFO logs will output immediately ===");
Log("=== Use VerboseMode only for debugging - it may exceed cloud log limits ===");
}
Logger = new QCLogger<CoreAlgo>(this, logLevel, verboseMode);
Log($"=== DEBUG: Smart logger created: {Logger != null} ===");
Log($"=== DEBUG: LogLevel: {logLevel}, VerboseMode: {verboseMode}, LiveMode: {LiveMode} ===");
// Test smart logger immediately
Logger.Info($"Algorithm initialized for environment: {environment}");
Log("ABOUT TO INITIALIZE STRATEGY");
}
catch (Exception ex)
{
Error($"INITIALIZE FAILED: {ex.Message}");
Error($"STACK TRACE: {ex.StackTrace}");
throw;
}
// Strategy Selection - dynamically discovered from Templates folder
var strategyType = GetParameter("Strategy", "IronCondor");
Log($"Initializing strategy: {strategyType}");
Logger.Info($"Initializing strategy: {strategyType}");
try
{
_strategy = StrategyDiscovery.CreateStrategy(strategyType);
}
catch (ArgumentException ex)
{
Log($"Unknown strategy '{strategyType}', defaulting to IronCondor. {ex.Message}");
Logger.Warning($"Unknown strategy '{strategyType}', defaulting to IronCondor. {ex.Message}");
_strategy = StrategyDiscovery.CreateStrategy("IronCondor");
}
// Initialize the strategy with this algorithm instance
try
{
// Inject the centralized logger into the strategy BEFORE initialization (context pattern)
if (_strategy is SimpleBaseStrategy baseStrategy)
{
baseStrategy.SetContext(Logger);
Logger.Debug("Logger injected into strategy via context pattern");
}
// Allow strategy configs to override UniverseSettings prior to initialization
if (_strategy is SimpleBaseStrategy simpleStrategy)
{
var desiredResolution = simpleStrategy?.Config?.GetUnderlyingResolution() ?? Resolution.Minute;
UniverseSettings.Resolution = desiredResolution;
Logger.Debug($"Strategy requested UniverseSettings resolution: {desiredResolution}");
}
_strategy.Initialize(this);
Log($"Strategy '{_strategy.Name}' initialized successfully");
Logger.Info($"Strategy '{_strategy.Name}' initialized successfully");
Log($"Strategy state: {_strategy.State}");
Logger.Info($"Strategy state: {_strategy.State}");
Log($"Description: {_strategy.Description}");
Logger.Info($"Description: {_strategy.Description}");
Log("Ready for options trading!");
Logger.Info("Ready for options trading!");
// Initialize SmartPricing if configured
if (_strategy is SimpleBaseStrategy baseStrategy2)
{
Logger.Info("Calling EnsureSmartPricingInitialized...");
baseStrategy2.EnsureSmartPricingInitialized();
Logger.Info("EnsureSmartPricingInitialized completed");
// Ensure ObjectStore positions are cleared for backtests (guarantee clean state)
baseStrategy2.TradePersistence?.ClearPositionsIfBacktest();
baseStrategy2.TradePersistence?.ClearSnapshotsIfBacktest();
Logger.Info("ObjectStore backtest cleanup completed");
}
else
{
Logger.Warning($"Strategy is not SimpleBaseStrategy: {_strategy?.GetType().Name}");
}
}
catch (Exception ex)
{
Error($"Failed to initialize strategy: {ex.Message}");
Logger.LogError(ex, "Failed to initialize strategy");
_strategy = null; // Prevent strategy execution
Error("Strategy initialization failed - algorithm cannot trade");
Logger.Error("Strategy initialization failed - algorithm cannot trade");
}
}
/// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
/// Slice object keyed by symbol containing the stock data
public override void OnData(Slice data)
{
// SMART DEBUG: Track execution flow with automatic deduplication
_debugCallCount++;
// Skip strategy execution during warmup period
if (IsWarmingUp)
{
return;
}
// One-time warmup completion diagnostics with option chain visibility
if (!_warmupCompleteLogged)
{
var chainsCount = data.OptionChains.Count;
var keys = chainsCount > 0 ? string.Join(", ", data.OptionChains.Keys) : "<none>";
Logger?.Info($"WARMUP COMPLETED at {Time:yyyy-MM-dd HH:mm:ss} - LiveMode: {LiveMode}, Resolution: {UniverseSettings.Resolution}");
Logger?.Info($"Option chain snapshot - Count: {chainsCount}, Keys: [{keys}]");
_warmupCompleteLogged = true;
}
// Keep existing post-warmup slice logging for additional diagnostics
if (!_postWarmupSliceLogged)
{
var chainsCount = data.OptionChains.Count;
var keys = chainsCount > 0 ? string.Join(", ", data.OptionChains.Keys) : "<none>";
Logger?.Info($"POST-WARMUP SLICE: OptionChains.Count={chainsCount}; Keys=[{keys}] at {Time:yyyy-MM-dd HH:mm:ss}");
_postWarmupSliceLogged = true;
}
// Only log option chain activity when chains are available (demoted to Debug to reduce noise)
if (data.OptionChains.Count > 0)
{
Logger.Debug($"Options chains available: {data.OptionChains.Count}");
}
// Execute strategy if properly initialized
if (_strategy != null)
{
try
{
_strategy.Execute(data);
}
catch (Exception ex)
{
Logger.LogError(ex, "Strategy execution error");
}
}
else
{
// CRITICAL: Strategy failed to initialize - algorithm cannot trade
if (_debugCallCount <= 5)
{
Logger.Error("CRITICAL: No strategy initialized - algorithm cannot trade");
Logger.Error("Check strategy initialization logs above for errors");
}
}
}
/// <summary>
/// Handle security changes from universe selection and forward to strategy
/// </summary>
public override void OnSecuritiesChanged(SecurityChanges changes)
{
base.OnSecuritiesChanged(changes);
// Forward to strategy if it implements universe handling
if (_strategy is SimpleBaseStrategy baseStrategy)
{
try
{
baseStrategy.OnSecuritiesChanged(changes);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error in strategy OnSecuritiesChanged");
}
}
}
/// <summary>
/// Called when order events occur (fills, cancellations, etc.)
/// Routes order events to SmartOrderManager for progressive pricing and trade tracking
/// </summary>
public override void OnOrderEvent(OrderEvent orderEvent)
{
// Let the base class handle the event first
base.OnOrderEvent(orderEvent);
// Single cast - exit early if not a SimpleBaseStrategy
if (!(_strategy is SimpleBaseStrategy s)) return;
// Route to SmartOrderManager if available
s.SmartOrderManager?.OnOrderEvent(orderEvent);
// Track order events for trade tracking system (like Python position tracking)
var orderId = orderEvent.OrderId.ToString();
switch (orderEvent.Status)
{
case OrderStatus.Submitted:
// Working orders are tracked at order placement via TrackWorkingOrder
// Just persist state change here
s.TradePersistence.SaveTrades(s.TradeTracker);
break;
case OrderStatus.PartiallyFilled:
var trade = s.TradeTracker.AllTrades.FirstOrDefault(t => t.OrderId == orderId);
if (trade != null)
{
trade.MarkAsPartialFill((int)orderEvent.FillQuantity, orderEvent.FillPrice);
Logger.Debug($"Partial fill: {orderId} - {orderEvent.FillQuantity} @ {orderEvent.FillPrice}");
s.TradePersistence.SaveTrades(s.TradeTracker);
}
break;
case OrderStatus.Filled:
s.TrackOrderFilled(orderEvent);
break;
case OrderStatus.Canceled:
s.TrackOrderCancelled(orderId);
break;
}
// Route ALL events to strategy for event-time logic (swap-to-TP, etc.)
s.OnOrderEventRouted(orderEvent);
}
/// <summary>
/// Called at the end of each trading day for each symbol
/// Always process batched logs (DEBUG and non-verbose INFO)
/// Even in LiveMode we want daily summaries of DEBUG messages
/// </summary>
public override void OnEndOfDay(Symbol symbol)
{
// Debug message to verify OnEndOfDay is called
Logger?.Debug($"OnEndOfDay called for symbol {symbol}");
// Save daily snapshot of trades (only once per day, not per symbol)
if (_strategy is SimpleBaseStrategy strategy)
{
if (strategy.TradePersistence != null && !_dailySnapshotSaved)
{
try
{
strategy.TradePersistence.SaveDailySnapshot(strategy.TradeTracker);
_dailySnapshotSaved = true;
// Log daily trade report
var summary = strategy.TradePersistence.GetTradeSummary(strategy.TradeTracker);
Logger?.Info($"=== EOD Trade Summary {Time:yyyy-MM-dd} ===\n" +
$"Total: {summary.TotalTrades}, Working: {summary.WorkingCount}, \n" +
$"Open: {summary.OpenCount}, Closed: {summary.ClosedCount}, \n" +
$"P&L: ${summary.TotalPnL:F2}");
}
catch (Exception ex)
{
Logger?.Error($"Error in EOD trade snapshot: {ex.Message}");
Log($"ERROR in EOD trade snapshot: {ex.Message}");
}
}
else if (strategy.TradePersistence == null)
{
// Debug why persistence is null
Log($"DEBUG: TradePersistence is null in OnEndOfDay for {strategy.Name}");
}
}
// Always process batched logs for daily summary
// This includes DEBUG logs (always batched) and INFO logs (when not verbose)
// Even in LiveMode, we want the DEBUG daily summary
SmartLoggerStore.ProcessDailyLogs(this);
// Reset daily snapshot flag for next day
_dailySnapshotSaved = false;
}
public override void OnEndOfAlgorithm()
{
// DEBUG: Immediate verification that OnEndOfAlgorithm() is called
Log("=== DEBUG: OnEndOfAlgorithm() CALLED - IMMEDIATE CONFIRMATION ===");
// Save final trade state to ObjectStore
if (_strategy is SimpleBaseStrategy strategy)
{
if (strategy.TradePersistence != null)
{
try
{
strategy.TradePersistence.SaveTrades(strategy.TradeTracker);
var summary = strategy.TradePersistence.GetTradeSummary(strategy.TradeTracker);
Logger?.Info($"=== Final Trade Summary ===\n" +
$"Total Trades: {summary.TotalTrades}\n" +
$" Working: {summary.WorkingCount}, Open: {summary.OpenCount}, Closed: {summary.ClosedCount}\n" +
$" Total P&L: ${summary.TotalPnL:F2}");
Logger?.Info("=== Final Trade Summary saved to ObjectStore ===");
}
catch (Exception ex)
{
Logger?.Error($"=== ERROR saving final trade state: {ex.Message} ===");
}
}
else
{
Logger?.Error("=== DEBUG: TradePersistence is null in OnEndOfAlgorithm ===");
}
}
// Check smart logger state
Log($"=== DEBUG: Smart logger null check: {Logger == null} ===");
// Check static collection state before processing
var (dailyCount, groupCount) = SmartLoggerStore.GetCollectionCounts();
Log($"=== DEBUG: Collection counts - Daily: {dailyCount}, Groups: {groupCount} ===");
// Process any remaining accumulated smart logs (fallback for symbols that didn't trigger OnEndOfDay)
Log("=== DEBUG: About to call ProcessDailyLogs ===");
SmartLoggerStore.ProcessDailyLogs(this);
Log("=== DEBUG: ProcessDailyLogs completed ===");
// Test smart logger one more time
if (Logger != null)
{
Logger.Info($"Algorithm finished. Final portfolio value: ${Portfolio.TotalPortfolioValue:N2}");
Logger.Info($"Total trades: {Transactions.GetOrderTickets().Count()}");
Log("=== DEBUG: Smart logger calls in OnEndOfAlgorithm completed ===");
}
else
{
Log("=== DEBUG: Smart logger is NULL in OnEndOfAlgorithm ===");
}
// Final confirmation
Log("=== DEBUG: OnEndOfAlgorithm() FINISHED ===");
}
/// <summary>
/// Forward assignment events to the strategy for handling
/// </summary>
public override void OnAssignmentOrderEvent(OrderEvent assignmentEvent)
{
Log($"Assignment event forwarded to strategy: {assignmentEvent.Symbol}");
// Forward to strategy if it supports assignment handling
if (_strategy is SimpleBaseStrategy strategy)
{
strategy.OnAssignmentOrderEvent(assignmentEvent);
}
else
{
Log($"Strategy does not support assignment handling: {_strategy?.GetType().Name ?? "null"}");
}
}
/// <summary>
/// Global security initializer that matches Python setupbasestructure.py CompleteSecurityInitializer
/// Disables buying power for most securities and applies backtesting configurations
/// Special handling for SPX index options (cash-settled)
/// </summary>
private void CompleteSecurityInitializer(Security security)
{
Logger?.Debug($"CompleteSecurityInitializer: {security.Symbol} ({security.Type})");
// Special handling for SPX index options (cash-settled European-style options)
if (security.Type == SecurityType.IndexOption)
{
Logger?.Info($"Configuring SPX index option: {security.Symbol}");
// Use null buying power model for index options (no margin requirements)
security.SetBuyingPowerModel(BuyingPowerModel.Null);
// CRITICAL FIX: Disable option assignment for cash-settled index options
// SPX options are cash-settled and should not simulate physical assignment
if (security is IndexOption indexOption)
{
indexOption.SetOptionAssignmentModel(new NullOptionAssignmentModel());
Logger?.Debug($"Applied NullOptionAssignmentModel to {security.Symbol}");
}
}
else
{
// CRITICAL: Disable buying power on other securities (matches Python)
security.SetBuyingPowerModel(BuyingPowerModel.Null);
}
// Apply dynamic slippage model that adapts to volume and price
// VolumeShareSlippageModel automatically handles different security types
// Default parameters: volumeLimit=0.025 (2.5%), priceImpact=0.1
security.SetSlippageModel(new VolumeShareSlippageModel());
// Skip backtesting-specific configurations in live mode (matches Python pattern)
if (LiveMode)
return;
// Backtesting configurations - set RAW normalization via subscription configs
security.Subscriptions.SetDataNormalizationMode(DataNormalizationMode.Raw);
var mode = security.Subscriptions.FirstOrDefault()?.DataNormalizationMode;
Logger?.Debug($"Initializer RAW set: {security.Symbol} => {mode}");
var lastPrices = GetLastKnownPrices(security);
if (lastPrices != null && lastPrices.Any())
{
security.SetMarketPrice(lastPrices.First());
}
}
}
}