| Overall Statistics |
|
Total Orders 156 Average Win 0% Average Loss 0% Compounding Annual Return 0% Drawdown 0% Expectancy 0 Start Equity 1000000.0 End Equity 984152.36 Net Profit 0% Sharpe Ratio 0 Sortino Ratio 0 Probabilistic Sharpe Ratio 0% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0 Annual Variance 0 Information Ratio 0 Tracking Error 0 Treynor Ratio 0 Total Fees $256.52 Estimated Strategy Capacity $1600000.00 Lowest Capacity Asset AMD R735QTJ8XC9X Portfolio Turnover 566.79% Drawdown Recovery 0 |
#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");
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");
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");
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>
/// 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();
// 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>
/// Process and output accumulated daily logs with smart summarization
/// </summary>
public static void ProcessDailyLogs(QCAlgorithm algorithm)
{
// 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} ===");
algorithm.Log("---------------------------------");
algorithm.Log($"Daily Log Summary - {currentDay:yyyy-MM-dd}");
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 to prevent memory leaks
CleanOldThrottleEntries(currentDay);
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 _isLiveMode;
private readonly bool _verboseMode;
public QCLogger(QCAlgorithm algorithm, int logLevel = 3)
{
_algorithm = algorithm ?? throw new ArgumentNullException(nameof(algorithm));
_categoryName = typeof(T).Name;
_currentLogLevel = logLevel;
_isLiveMode = algorithm.LiveMode;
_verboseMode = bool.Parse(algorithm.GetParameter("VerboseMode", "false"));
}
public void LogInformation(string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
if (_isLiveMode)
{
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 (_isLiveMode)
{
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 (_isLiveMode)
{
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 (_isLiveMode)
{
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 (_isLiveMode)
{
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 (_isLiveMode)
{
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>
/// 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 (_isLiveMode)
{
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) || // INFO if verbose
(_isLiveMode && level == LogLevel.Info); // INFO if live
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 (_isLiveMode)
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 (_isLiveMode)
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 (_isLiveMode)
{
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)
/// Enhances TradeRecord with symbol info from orderEvent if available
/// </summary>
public void TrackOrderFilled(OrderEvent orderEvent)
{
if (orderEvent?.OrderId != null)
{
var orderId = orderEvent.OrderId.ToString();
// Find or create the trade record
var trade = TradeTracker.AllTrades.FirstOrDefault(t => t.OrderId == orderId);
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;
}
}
TradeTracker.MarkTradeAsOpen(orderId, orderEvent.FillPrice, (int)orderEvent.FillQuantity);
Debug($"Order filled: {orderId} at {orderEvent.FillPrice}");
OnOrderFilled(orderEvent);
// Persist to ObjectStore on fill
TradePersistence.SaveTrades(TradeTracker);
}
}
/// <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>
/// 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.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
protected OrderTicket MarketOrder(Symbol symbol, decimal quantity, string tag = "")
{
if (ShouldUseSmartOrderManager())
{
return SmartOrderManager.SmartMarketOrder(symbol, quantity, tag);
}
return Algorithm.MarketOrder(symbol, quantity, tag: tag);
}
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);
}
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);
}
protected OrderTicket StopLimitOrder(Symbol symbol, decimal quantity, decimal stopPrice, decimal limitPrice, string tag = "")
{
var (roundedStop, roundedLimit) = PriceRounding.RoundStopLimitPrices(Algorithm.Securities, symbol, quantity, stopPrice, limitPrice);
return Algorithm.StopLimitOrder(symbol, quantity, roundedStop, roundedLimit, tag: tag);
}
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);
}
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);
}
// 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);
// 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 = Algorithm.Liquidate(symbol: symbol, tag: 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;
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>
/// 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)
{
Status = "Open";
OpenPrice = fillPrice;
Quantity = fillQuantity;
FilledQuantity = fillQuantity;
LastUpdateUtc = DateTime.UtcNow;
}
/// <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>
/// 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>
/// Moves a trade from working to open when it gets filled
/// If trade doesn't exist in Working (race condition), looks in AllTrades or creates new one
/// </summary>
public void MarkTradeAsOpen(string orderId, decimal fillPrice, int fillQuantity)
{
var trade = WorkingTrades.FirstOrDefault(t => t.OrderId == orderId);
if (trade != null)
{
trade.MarkAsOpen(fillPrice, fillQuantity);
WorkingTrades.Remove(trade);
OpenTrades.Add(trade);
return;
}
// Fallback: Check if trade exists in AllTrades but not in WorkingTrades (already moved)
trade = AllTrades.FirstOrDefault(t => t.OrderId == orderId);
if (trade != null && trade.Status != "Open")
{
trade.MarkAsOpen(fillPrice, fillQuantity);
if (!OpenTrades.Contains(trade))
{
OpenTrades.Add(trade);
}
return;
}
// Last resort: Create trade on-the-fly (handles race condition where fill arrives before tracking)
if (trade == null)
{
trade = new TradeRecord(orderId, "Unknown", "Unknown", "")
{
Quantity = fillQuantity,
FilledQuantity = fillQuantity,
OpenPrice = fillPrice,
Status = "Open"
};
AllTrades.Add(trade);
OpenTrades.Add(trade);
}
}
/// <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, 10, 14);
EndDate = new DateTime(2025, 10, 15);
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;
EntryEvalLogEveryMinutes = 10;
LogEntryRestrictions = false;
}
// ============================================================================
// 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("EntryEvalLogEveryMinutes", 10)]
[Description("Frequency in minutes for entry evaluation logs (default: 10 minutes)")]
public int EntryEvalLogEveryMinutes { get; set; } = 10;
[StrategyParameter("LogEntryRestrictions", false)]
[Description("Log detailed entry restriction checks (default: false)")]
public override bool LogEntryRestrictions { get; set; }
[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("EntryCooldownMs", 150)]
[Description("Minimum milliseconds between entry orders to prevent bursts (default: 150ms)")]
public int EntryCooldownMs { get; set; } = 150;
[StrategyParameter("MaxOrdersPerMinute", 300)]
[Description("Maximum orders per minute rate limit (default: 300)")]
public int MaxOrdersPerMinute { get; set; } = 300;
[StrategyParameter("ExitMode", "LeanOCO")]
[Description("Exit order mode: LeanOCO (manual OCO via OrderTickets) or BrokerOCO (broker-native brackets)")]
public string ExitMode { get; set; } = "LeanOCO";
[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; } = false;
// ============================================================================
// 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",
"EntryCooldownMs",
"MaxOrdersPerMinute",
"ExitMode",
"RequireQuotesForEntry"
}).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})");
return errors.ToArray();
}
public override string ToString()
{
return $"AdaptiveScalper[{UnderlyingSymbol}] ATR:{AtrPeriod} Spread:{MinSpread:F3}-{MaxSpread:F3} " +
$"Risk:${TargetDollarRisk} SL:{StopLossAtrMultiple}xATR TP:{TakeProfitAtrMultiple}xATR " +
$"Kill:${DailyKillSwitch} MaxConcurrent:{MaxConcurrentTrades} " +
$"Mode:{ExitMode} 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();
foreach (var property in properties)
{
var attribute = property.GetCustomAttribute<StrategyParameterAttribute>();
if (attribute != null)
{
try
{
var parameterName = attribute.Name;
var defaultValue = property.GetValue(this)?.ToString() ?? attribute.DefaultValue?.ToString() ?? "";
var parameterValue = context.Algorithm.GetParameter(parameterName, defaultValue);
// Use context logger for debugging
// ((dynamic)context.Logger).Debug($"Raw parameter {parameterName} = '{parameterValue}' (Type: {parameterValue?.GetType()?.Name ?? "null"})");
// Convert the parameter value to the property type
var convertedValue = ConvertParameterValue(parameterValue, property.PropertyType);
property.SetValue(this, convertedValue);
// Enhanced logging for arrays
if (convertedValue is string[] arrayValue)
{
((dynamic)context.Logger).Debug($"Loaded parameter {parameterName} = [{string.Join(", ", arrayValue)}] (Array Length: {arrayValue.Length})");
}
else
{
((dynamic)context.Logger).Debug($"Loaded parameter {parameterName} = {convertedValue} (Converted Type: {convertedValue?.GetType()?.Name ?? "null"})");
}
}
catch (Exception ex)
{
((dynamic)context.Logger).Error($"Failed to load parameter {attribute.Name}: {ex.Message}");
}
}
}
}
/// <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);
}
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);
}
// 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]");
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);
}
// 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);
}
}
/// <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);
}
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);
}
// 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);
}
// 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);
}
// Place initial combo limit order with calculated net price
var comboTickets = _algorithm.ComboLimitOrder(legs, quantity, initialNetPrice.Value, tag: tag + " [SmartCombo]");
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);
}
// 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);
}
}
/// <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]");
}
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;
}
}
}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
/// </summary>
public void SaveTrades(TradeTracker tracker)
{
if (tracker == null) return;
try
{
var book = new TradeBook
{
Version = "1.0",
AsOfUtc = DateTime.UtcNow,
AllTrades = tracker.AllTrades,
WorkingTrades = tracker.WorkingTrades.Select(t => t.OrderId).ToList(),
OpenTrades = tracker.OpenTrades.Select(t => t.OrderId).ToList(),
ClosedTrades = tracker.ClosedTrades.Select(t => t.OrderId).ToList()
};
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 {tracker.AllTrades.Count} trades to ObjectStore");
}
catch (Exception ex)
{
// Never break trading due to persistence errors
((dynamic)_context.Logger).Error($"TradePersistence: Error saving trades: {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
/// </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";
var book = new TradeBook
{
Version = "1.0",
AsOfUtc = DateTime.UtcNow,
AllTrades = tracker.AllTrades,
WorkingTrades = tracker.WorkingTrades.Select(t => t.OrderId).ToList(),
OpenTrades = tracker.OpenTrades.Select(t => t.OrderId).ToList(),
ClosedTrades = tracker.ClosedTrades.Select(t => t.OrderId).ToList()
};
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}");
}
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.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
/// </summary>
public class AdaptiveScalperTemplate : SimpleBaseStrategy
{
/// <summary>
/// Lightweight trade state for concurrent lot tracking
/// Inline nested class - keeps everything in one file per QC-First simplicity principles
/// </summary>
private class Trade
{
public long Id;
public int EntryOrderId; // Link to TradeTracker record
public decimal Entry;
public decimal StopDist;
public decimal TpDist;
public OrderTicket Tp;
public OrderTicket Sl;
public int Qty;
public DateTime Open;
public bool Filled;
}
private AdaptiveScalperConfig _config;
private Symbol _underlying;
private AverageTrueRange _atr;
private RollingWindow<decimal> _atrBaseline;
// Multi-trade state management
private Dictionary<long, Trade> _openTrades = new Dictionary<long, Trade>();
private readonly Dictionary<int, long> _orderIdToTradeId = new Dictionary<int, long>();
private readonly HashSet<string> _warnedMissingChildren = new HashSet<string>();
private DateTime _lastEntryEvalLogTime = DateTime.MinValue;
private long _nextTradeId = 1;
// 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;
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";
public override void OnInitialize()
{
SmartLog("AdaptiveScalperTemplate.OnInitialize() starting (HFT Multi-Trade Edition)...");
// Configure with AdaptiveScalper-specific settings
try
{
Configure<AdaptiveScalperConfig>();
_config = (AdaptiveScalperConfig)Config;
SmartLog("Configuration loaded successfully");
}
catch (Exception ex)
{
SmartError($"Failed to load configuration: {ex.Message}");
throw;
}
// Setup underlying equity with appropriate resolution for quote data
SmartLog($"Setting up {_config.UnderlyingSymbol} for scalping");
var dataResolution = _config.GetUnderlyingResolution();
SmartLog($"Using data resolution: {dataResolution} (from config: {_config.UnderlyingResolution})");
var security = AddEquity(_config.UnderlyingSymbol, dataResolution);
_underlying = security.Symbol;
SmartLog($"Successfully added {_underlying} at {dataResolution} resolution");
// Initialize ATR indicator for volatility-based stop/target calculation
var atrResolution = _config.GetAtrResolution();
SmartLog($"Creating ATR({_config.AtrPeriod}) at {atrResolution} resolution");
_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);
SmartLog($"ATR registered and warmed up - IsReady: {_atr.IsReady}, Current: {(_atr.IsReady ? _atr.Current.Value.ToString("F4") : "N/A")}");
// Initialize ATR baseline rolling window for adaptive factor calculation
_atrBaseline = new RollingWindow<decimal>(_config.VolatilityBaselineWindow);
SmartLog($"ATR baseline window created with size {_config.VolatilityBaselineWindow}");
// Schedule daily PnL reset at market open
Algorithm.Schedule.On(
Algorithm.DateRules.EveryDay(_underlying),
Algorithm.TimeRules.AfterMarketOpen(_underlying, 1),
ResetDailyTracking
);
// Format trading window status
var tradingWindowStatus = (_config.TradingStartTime == TimeSpan.Zero && _config.TradingEndTime == TimeSpan.Zero)
? "DISABLED (00:00-00:00) - trades allowed all day"
: $"{_config.TradingStartTime:hh\\:mm} - {_config.TradingEndTime:hh\\:mm} (active)";
SmartLog(
$"Adaptive Scalper HFT initialized:\n" +
$" Underlying: {_underlying}\n" +
$" Data Resolution: {dataResolution}\n" +
$" ATR Period: {_config.AtrPeriod} at {atrResolution}\n" +
$" Baseline Window: {_config.VolatilityBaselineWindow}\n" +
$" Spread Range: ${_config.MinSpread:F3} - ${_config.MaxSpread:F3}\n" +
$" Target Risk: ${_config.TargetDollarRisk}\n" +
$" Stop Loss: max(${_config.MinStopLoss:F3}, {_config.StopLossAtrMultiple}x ATR)\n" +
$" Take Profit: {_config.TakeProfitAtrMultiple}x ATR\n" +
$" Trailing Start: {_config.TrailingStartFraction:P0} of TP\n" +
$" Daily Kill Switch: ${_config.DailyKillSwitch:N0}\n" +
$" Throttle Decay: {_config.ThrottleDecay}\n" +
$" Min Throttle: {_config.MinThrottle:P0}\n" +
$" Max Concurrent Trades: {_config.MaxConcurrentTrades}\n" +
$" Entry Cooldown: {_config.EntryCooldownMs}ms\n" +
$" Max Orders/Min: {_config.MaxOrdersPerMinute}\n" +
$" Exit Mode: {_config.ExitMode}\n" +
$" Trading Window: {tradingWindowStatus}\n" +
$" ATR Ready: {_atr.IsReady}\n" +
$" Live Mode: {Algorithm.LiveMode}\n" +
$" Algorithm Status: {Algorithm.Status}"
);
}
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)
{
var shouldLogEntryEval = ShouldLogEntryEvaluation(slice.Time);
if (shouldLogEntryEval)
{
SmartLog($"[DEBUG OnExecute] Called at {slice.Time} | Halted: {_haltedForDay} | ATR Ready: {_atr.IsReady} | Baseline Ready: {_atrBaseline.IsReady}");
}
// Early exits for halted or warming conditions
if (_haltedForDay)
{
if (shouldLogEntryEval)
{
SmartLog($"[DEBUG OnExecute] Exiting - day is halted");
}
return;
}
// Update ATR baseline if we have ATR
if (_atr.IsReady)
{
var currentAtr = (decimal)_atr.Current.Value;
_atrBaseline.Add(currentAtr);
}
if (!_atr.IsReady || !_atrBaseline.IsReady)
{
if (shouldLogEntryEval)
{
SmartLog($"[DEBUG OnExecute] Exiting - indicators not ready | ATR: {_atr.IsReady} | Baseline: {_atrBaseline.IsReady}");
}
return;
}
// Get current security and quote data
var security = Securities[_underlying];
var bid = security.BidPrice;
var ask = security.AskPrice;
var currentPrice = security.Price;
// Handle missing quotes gracefully (common with Minute bars on equities)
// If RequireQuotesForEntry is true, strictly require quotes; otherwise use fallback
decimal spread;
bool hasValidQuotes = bid > 0 && ask > 0;
if (!hasValidQuotes)
{
if (_config.RequireQuotesForEntry)
{
// Strict mode: require quotes before entering
return;
}
// Fallback mode: use spread=0 and proceed with mid-price logic
spread = 0m;
// Set bid/ask to current price for mid calculation
bid = currentPrice;
ask = currentPrice;
}
else
{
spread = ask - bid;
}
// Get ATR for calculations
var atrValue = (decimal)_atr.Current.Value;
var atrBaseline = _atrBaseline.Average();
var adaptFactor = atrBaseline != 0 ? atrValue / atrBaseline : 1.0m;
// Dynamic spread bounds adjusted by volatility
var dynMin = _config.MinSpread * adaptFactor;
var dynMax = _config.MaxSpread * adaptFactor;
// Classify spread quality
string spreadState;
if (!hasValidQuotes)
{
// No quotes available - treat as NORMAL spread for entry purposes
spreadState = "NORMAL";
}
else if (spread <= dynMin * 1.1m)
{
spreadState = "TIGHT";
}
else if (spread <= dynMax * 0.9m)
{
spreadState = "NORMAL";
}
else
{
spreadState = "WIDE";
}
// 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 loss and take profit
var rawStopLoss = Math.Max(_config.MinStopLoss, atrValue * _config.StopLossAtrMultiple * volMultiplier);
var rawTakeProfit = atrValue * _config.TakeProfitAtrMultiple * volMultiplier;
// Round to tick size to ensure trailing stop calculations align with brokerage precision
var tick = PriceRounding.GetMinPriceVariation(Algorithm.Securities, _underlying);
var stopLoss = PriceRounding.CeilToTick(rawStopLoss, tick); // Ceil for adequate protection
var takeProfit = PriceRounding.CeilToTick(rawTakeProfit, tick); // Ceil to ensure profit capture
// Calculate risk-based position size
var baseQty = (int)(_config.TargetDollarRisk / stopLoss);
var tradeQty = Math.Max(1, (int)(baseQty * _tradeThrottle));
// === ENTRY LOGIC ===
if (shouldLogEntryEval)
{
SmartLog($"[DEBUG ENTRY EVAL] Time: {slice.Time} | Price: ${currentPrice:F2} | Spread: {spreadState} (${spread:F3})");
SmartLog($" Open Trades: {_openTrades.Count}/{_config.MaxConcurrentTrades} | Throttle: {_tradeThrottle:P0} (Min: {_config.MinThrottle:P0})");
SmartLog($" Spread OK: {spreadState == "TIGHT" || spreadState == "NORMAL"} | HasQuotes: {hasValidQuotes}");
_lastEntryEvalLogTime = slice.Time;
}
// Check concurrency cap, throttle, and spread quality
if (_openTrades.Count < _config.MaxConcurrentTrades &&
_tradeThrottle > _config.MinThrottle &&
(spreadState == "TIGHT" || spreadState == "NORMAL"))
{
// DEBUG: Passed initial checks
if (shouldLogEntryEval)
{
SmartLog($"[DEBUG ENTRY] Passed concurrency/throttle/spread checks, checking rate limits...");
}
// Rate limiting checks
if (!CanEnterNow(slice.Time))
{
if (shouldLogEntryEval)
{
SmartLog($"[DEBUG ENTRY] Blocked by rate limiting");
}
return;
}
if (shouldLogEntryEval)
{
SmartLog($"[DEBUG ENTRY] Passed rate limits, checking entry restrictions...");
}
// Check entry restrictions (time window, capital)
if (!CanEnterWithLogging(slice, shouldLogEntryEval, out var reason))
{
return;
}
if (shouldLogEntryEval)
{
SmartLog($"[DEBUG ENTRY] ALL CHECKS PASSED - placing entry order");
}
var mid = (ask + bid) / 2m;
var tradeId = _nextTradeId++;
// Create trade state
var trade = new Trade
{
Id = tradeId,
Qty = tradeQty,
Entry = 0, // Will be set on fill
StopDist = stopLoss,
TpDist = takeProfit,
Open = slice.Time,
Filled = false
};
_openTrades[tradeId] = trade;
// Place entry market order with trade ID tag
var entryTicket = MarketOrder(_underlying, tradeQty, tag: $"ENTRY_T{tradeId}");
if (entryTicket == null)
{
_openTrades.Remove(tradeId);
SmartWarn($"[WARN ENTRY] MarketOrder returned null for trade {tradeId}");
return;
}
TrackOrderMapping(entryTicket, tradeId);
// Record in centralized TradeTracker for persistence and reporting
trade.EntryOrderId = entryTicket.OrderId;
TrackWorkingOrder(entryTicket, Name);
_lastEntryTime = slice.Time;
_totalEntriesToday++;
// Throttled logging (only log every 10th entry to reduce noise)
if ((_totalEntriesToday % 10 == 1 || _openTrades.Count == 1) && _config.DebugMode)
{
var quoteMode = hasValidQuotes ? "" : " [NO QUOTES - FALLBACK]";
SmartLog(
$"[ENTRY T{tradeId}] {spreadState} Spread{quoteMode} | Price: ${currentPrice:F2} (Mid: ${mid:F2})\n" +
$" ATR: {atrValue:F4} | Baseline: {atrBaseline:F4} | AdaptFactor: {adaptFactor:F2}\n" +
$" Vol Multiplier: {volMultiplier:F1}x (hour: {hour:F2})\n" +
$" Stop: ${stopLoss:F4} | TP: ${takeProfit:F4} | Qty: {tradeQty}\n" +
$" Throttle: {_tradeThrottle:P0} | Open Trades: {_openTrades.Count}/{_config.MaxConcurrentTrades}\n" +
$" Spread: ${spread:F3} (Min: ${dynMin:F3}, Max: ${dynMax:F3})"
);
}
}
else
{
if (shouldLogEntryEval)
{
var failReasons = new List<string>();
if (_openTrades.Count >= _config.MaxConcurrentTrades)
failReasons.Add($"MaxConcurrent ({_openTrades.Count}/{_config.MaxConcurrentTrades})");
if (_tradeThrottle <= _config.MinThrottle)
failReasons.Add($"Throttle too low ({_tradeThrottle:P0} <= {_config.MinThrottle:P0})");
if (spreadState != "TIGHT" && spreadState != "NORMAL")
failReasons.Add($"Spread state: {spreadState}");
SmartLog($"[DEBUG ENTRY] NOT attempting entry - Failed: {string.Join(", ", failReasons)}");
}
}
// === EXIT AND TRADE MANAGEMENT ===
ManageOpenTrades(slice, currentPrice);
}
/// <summary>
/// Manage all open trades - ensure child orders exist
/// </summary>
private void ManageOpenTrades(Slice slice, decimal currentPrice)
{
_ = currentPrice;
foreach (var kvp in _openTrades)
{
var trade = kvp.Value;
// Skip if entry not yet filled
if (!trade.Filled)
{
continue;
}
// Warn about missing child orders (TP/SL) every 15 minutes
if ((trade.Tp == null || trade.Sl == null) && slice.Time.Minute % 15 == 0)
{
var key = $"{trade.Id}:{slice.Time:HHmm}";
if (_warnedMissingChildren.Add(key))
{
SmartWarn($"[WARN T{trade.Id}] Missing child orders - TP: {trade.Tp != null}, SL: {trade.Sl != null}");
}
}
}
}
/// <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;
var tradeId = ResolveTradeId(orderId, tag);
if (tradeId == 0)
{
SmartWarn($"[WARN ORD] Unable to resolve trade for order {orderId} (Tag='{tag ?? "<null>"}')");
return;
}
if (!_openTrades.TryGetValue(tradeId, out var trade))
{
SmartWarn($"[WARN ORD] Trade {tradeId} not found for order {orderId} (Tag='{tag ?? "<null>"}')");
_orderIdToTradeId.Remove(orderId);
return;
}
// Entry order filled - place child TP/trailing orders
if (tag != null && tag.StartsWith("ENTRY_T") && !trade.Filled)
{
trade.Filled = true;
trade.Entry = orderEvent.FillPrice;
// Calculate TP and SL prices
var tpPrice = trade.Entry + trade.TpDist;
var trailingAmount = trade.StopDist;
// Place take profit limit order
trade.Tp = LimitOrder(_underlying, -trade.Qty, tpPrice, tag: $"TP_T{tradeId}");
TrackOrderMapping(trade.Tp, tradeId);
// Place stop loss stop market order
trade.Sl = TrailingStopOrder(_underlying, -trade.Qty, trailingAmount, false, tag: $"TS_T{tradeId}");
TrackOrderMapping(trade.Sl, tradeId);
_orderIdToTradeId.Remove(orderId);
// Throttled logging
if ((_config.DebugMode && tradeId % 10 == 1) || _openTrades.Count <= 3)
{
var tpOrderId = trade.Tp?.OrderId.ToString() ?? "<null>";
var tsOrderId = trade.Sl?.OrderId.ToString() ?? "<null>";
SmartLog(
$"[FILL T{tradeId}] Entry ${trade.Entry:F2} | TP ${tpPrice:F2} (Order {tpOrderId}) | TS Δ${trailingAmount:F4} (Order {tsOrderId}) | Qty: {trade.Qty}"
);
}
return;
}
// TP order filled - cancel SL and close trade
if (tag != null && tag.StartsWith("TP_T"))
{
var pnl = (orderEvent.FillPrice - trade.Entry) * trade.Qty;
_dailyPnL += pnl;
_totalExitsToday++;
// Cancel the SL order (don't change tag - preserve mapping)
if (trade.Sl != null && trade.Sl.Status == OrderStatus.Submitted)
{
trade.Sl.Cancel();
}
// Throttled logging
if ((_config.DebugMode && tradeId % 10 == 1) || _openTrades.Count <= 3)
{
SmartLog(
$"[TP EXIT T{tradeId}] Entry: ${trade.Entry:F2} | Exit: ${orderEvent.FillPrice:F2}\n" +
$" PnL: ${pnl:F2} | Daily PnL: ${_dailyPnL:F2} | Qty: {trade.Qty}"
);
}
// Mark position closed in TradeTracker for persistence and reporting
TrackPositionClosed(trade.EntryOrderId.ToString(), orderEvent.FillPrice, pnl);
_orderIdToTradeId.Remove(orderId);
// Check if both orders are done before removing trade
if (IsTradeClosed(trade))
{
ClearTradeMappings(trade);
_openTrades.Remove(tradeId);
}
AdjustThrottle();
return;
}
// Trailing stop filled - cancel TP and close trade
if (tag != null && tag.StartsWith("TS_T"))
{
var pnl = (orderEvent.FillPrice - trade.Entry) * trade.Qty;
_dailyPnL += pnl;
_totalExitsToday++;
// Cancel the TP order (don't change tag - preserve mapping)
if (trade.Tp != null && trade.Tp.Status == OrderStatus.Submitted)
{
trade.Tp.Cancel();
}
// Throttled logging
if ((_config.DebugMode && tradeId % 10 == 1) || _openTrades.Count <= 3)
{
SmartLog(
$"[TS EXIT T{tradeId}] Entry: ${trade.Entry:F2} | Exit: ${orderEvent.FillPrice:F2}\n" +
$" PnL: ${pnl:F2} | Daily PnL: ${_dailyPnL:F2} | Qty: {trade.Qty}"
);
}
// Mark position closed in TradeTracker for persistence and reporting
TrackPositionClosed(trade.EntryOrderId.ToString(), orderEvent.FillPrice, pnl);
_orderIdToTradeId.Remove(orderId);
// Check if both orders are done before removing trade
if (IsTradeClosed(trade))
{
ClearTradeMappings(trade);
_openTrades.Remove(tradeId);
}
AdjustThrottle();
return;
}
// Handle cancelled orders (sibling was filled)
if (orderEvent.Status == OrderStatus.Canceled)
{
_orderIdToTradeId.Remove(orderId);
// Check if trade should be cleaned up now
if (IsTradeClosed(trade))
{
ClearTradeMappings(trade);
_openTrades.Remove(tradeId);
}
return;
}
}
/// <summary>
/// Check if both TP and SL orders are in terminal states
/// </summary>
private bool IsTradeClosed(Trade trade)
{
var tpDone = trade.Tp == null ||
trade.Tp.Status == OrderStatus.Filled ||
trade.Tp.Status == OrderStatus.Canceled ||
trade.Tp.Status == OrderStatus.Invalid;
var slDone = trade.Sl == null ||
trade.Sl.Status == OrderStatus.Filled ||
trade.Sl.Status == OrderStatus.Canceled ||
trade.Sl.Status == OrderStatus.Invalid;
return tpDone && slDone;
}
/// <summary>
/// Parse trade ID from order tag
/// </summary>
private long ParseTradeId(string tag)
{
if (string.IsNullOrEmpty(tag))
{
return 0;
}
// Tag format: ENTRY_T123, TP_T123, TS_T123
var parts = tag.Split('_');
if (parts.Length < 2)
{
return 0;
}
var idPart = parts[1];
if (idPart.StartsWith("T") && long.TryParse(idPart.Substring(1), out var tradeId))
{
return tradeId;
}
return 0;
}
private long ResolveTradeId(int orderId, string tag)
{
if (_orderIdToTradeId.TryGetValue(orderId, out var tradeId))
{
return tradeId;
}
var parsed = ParseTradeId(tag);
if (parsed != 0)
{
_orderIdToTradeId[orderId] = parsed;
}
return parsed;
}
private void TrackOrderMapping(OrderTicket ticket, long tradeId)
{
if (ticket == null)
{
return;
}
_orderIdToTradeId[ticket.OrderId] = tradeId;
}
private void ClearTradeMappings(Trade trade)
{
if (trade == null)
{
return;
}
if (trade.Tp != null)
{
_orderIdToTradeId.Remove(trade.Tp.OrderId);
}
if (trade.Sl != null)
{
_orderIdToTradeId.Remove(trade.Sl.OrderId);
}
var prefix = $"{trade.Id}:";
_warnedMissingChildren.RemoveWhere(key => key.StartsWith(prefix, StringComparison.Ordinal));
}
/// <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);
// Throttled logging
if (_totalExitsToday % 20 == 0)
{
SmartLog($"[THROTTLE] Adjusted to {_tradeThrottle:P0} (Daily PnL: ${_dailyPnL:F2})");
}
}
// Kill switch - halt trading for the day
if (_dailyPnL <= _config.DailyKillSwitch)
{
SmartLog($"[KILL SWITCH] Daily loss limit reached (${_dailyPnL:F2} <= ${_config.DailyKillSwitch:F2}) - halting trading");
// Liquidate all open trades
foreach (var trade in _openTrades.Values.ToList())
{
if (trade.Tp != null && trade.Tp.Status == OrderStatus.Submitted)
{
trade.Tp.Cancel("Kill switch");
}
if (trade.Sl != null && trade.Sl.Status == OrderStatus.Submitted)
{
trade.Sl.Cancel("Kill switch");
}
ClearTradeMappings(trade);
}
Algorithm.Liquidate(_underlying);
_openTrades.Clear();
_haltedForDay = true;
}
}
private void ResetDailyTracking()
{
SmartLog($"=== Resetting daily tracking for {Algorithm.Time.Date:yyyy-MM-dd} ===");
SmartLog($"Previous day stats: Entries: {_totalEntriesToday}, Exits: {_totalExitsToday}, PnL: ${_dailyPnL:F2}");
// Log TradeTracker summary for cross-validation
if (TradePersistence != null)
{
var summary = TradePersistence.GetTradeSummary(TradeTracker);
SmartLog($"TradeTracker state: Working={summary.WorkingCount}, Open={summary.OpenCount}, " +
$"Closed={summary.ClosedCount}, Total PnL=${summary.TotalPnL:F2}");
// Cross-check local vs tracker (detect drift)
if (_openTrades.Count != summary.OpenCount)
{
SmartWarn($"Trade count divergence detected: Local={_openTrades.Count}, Tracker={summary.OpenCount}");
}
}
_dailyPnL = 0m;
_tradeThrottle = 1.0m;
_haltedForDay = false;
_totalEntriesToday = 0;
_totalExitsToday = 0;
_orderTimestamps.Clear();
_warnedMissingChildren.Clear();
_lastEntryEvalLogTime = DateTime.MinValue;
SmartLog($"Daily PnL reset to $0, Throttle reset to 100%, Halted: false");
}
private bool CanEnterWithLogging(Slice slice, bool shouldLogEntryEval, out string reason)
{
var result = EntryRestrictions.CanEnterPosition(_underlying, slice, out reason);
if (!result)
{
if (shouldLogEntryEval || _config.LogEntryRestrictions)
{
SmartLog($"[DEBUG ENTRY] Blocked by entry restriction: {reason}");
}
return false;
}
if (_config.LogEntryRestrictions && shouldLogEntryEval)
{
SmartLog("[DEBUG ENTRY] Entry restrictions passed");
}
return true;
}
private bool ShouldLogEntryEvaluation(DateTime time)
{
if (_config.DebugMode)
{
return true;
}
if (_config.EntryEvalLogEveryMinutes <= 0)
{
return false;
}
if (_lastEntryEvalLogTime == DateTime.MinValue)
{
return true;
}
var elapsedMinutes = (time - _lastEntryEvalLogTime).TotalMinutes;
return elapsedMinutes >= _config.EntryEvalLogEveryMinutes;
}
protected override void OnGetPerformanceMetrics(Dictionary<string, double> metrics)
{
// Add AdaptiveScalper specific metrics
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"] = _openTrades.Count;
metrics["TotalEntriesToday"] = _totalEntriesToday;
metrics["TotalExitsToday"] = _totalExitsToday;
if (_atrBaseline.IsReady)
{
metrics["ATRBaseline"] = (double)_atrBaseline.Average();
metrics["ATRAdaptFactor"] = _atrBaseline.Average() != 0 ? (double)((decimal)_atr.Current.Value / _atrBaseline.Average()) : 1.0;
}
// Aggregate PnL of all open trades
if (_openTrades.Count > 0)
{
var currentPrice = Securities[_underlying].Price;
var totalUnrealizedPnL = _openTrades.Values
.Where(t => t.Filled)
.Sum(t => (currentPrice - t.Entry) * t.Qty);
metrics["UnrealizedPnL"] = (double)totalUnrealizedPnL;
}
}
}
}
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);
}
else if (request.OrderType == OrderType.Limit)
{
ticket = algorithm.LimitOrder(request.Symbol, request.Quantity, request.LimitPrice, tag: request.Tag);
}
else
{
// For other order types, try the basic Submit method
ticket = algorithm.MarketOrder(request.Symbol, request.Quantity, tag: request.Tag);
}
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);
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);
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.
/// </summary>
public static (decimal RoundedStop, decimal RoundedLimit) RoundStopLimitPrices(
SecurityManager securities,
Symbol symbol,
decimal quantity,
decimal stopPrice,
decimal limitPrice)
{
var tick = GetMinPriceVariation(securities, symbol);
if (quantity >= 0)
{
return (CeilToTick(stopPrice, tick), FloorToTick(limitPrice, tick));
}
return (FloorToTick(stopPrice, tick), CeilToTick(limitPrice, tick));
}
/// <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");
}
// 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", "false")); // 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);
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");
}
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);
// Route to strategy's SmartOrderManager if available
if (_strategy is SimpleBaseStrategy baseStrategy && baseStrategy.SmartOrderManager != null)
{
baseStrategy.SmartOrderManager.OnOrderEvent(orderEvent);
}
// Track order events for trade tracking system (like Python position tracking)
if (_strategy is SimpleBaseStrategy strategy)
{
var orderId = orderEvent.OrderId.ToString();
// Track submitted orders (initial pending state)
if (orderEvent.Status == OrderStatus.Submitted)
{
// Working orders are tracked at order placement via TrackWorkingOrder
// Just persist state change here
strategy.TradePersistence.SaveTrades(strategy.TradeTracker);
}
// Track partial fills
else if (orderEvent.Status == OrderStatus.PartiallyFilled)
{
var trade = strategy.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}");
strategy.TradePersistence.SaveTrades(strategy.TradeTracker);
}
}
// Track filled orders
else if (orderEvent.Status == OrderStatus.Filled)
{
strategy.TrackOrderFilled(orderEvent);
}
// Track cancelled orders
else if (orderEvent.Status == OrderStatus.Canceled)
{
strategy.TrackOrderCancelled(orderId);
}
}
}
/// <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());
}
}
}
}