| Overall Statistics |
|
Total Orders 5886 Average Win 0.44% Average Loss -0.52% Compounding Annual Return 11.193% Drawdown 10.500% Expectancy 0.083 Start Equity 100000 End Equity 225541.21 Net Profit 125.541% Sharpe Ratio 0.477 Sortino Ratio 0.764 Probabilistic Sharpe Ratio 15.399% Loss Rate 41% Win Rate 59% Profit-Loss Ratio 0.84 Alpha 0.021 Beta 0.413 Annual Standard Deviation 0.113 Annual Variance 0.013 Information Ratio -0.201 Tracking Error 0.132 Treynor Ratio 0.13 Total Fees $3026.00 Estimated Strategy Capacity $0 Lowest Capacity Asset SPXW YUWDA1RAURU6|SPX 31 Portfolio Turnover 0.33% Drawdown Recovery 312 |
#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;
// 1. Trading Hours Check
if (!IsWithinTradingHours(slice.Time))
{
reason = $"Outside trading hours ({_config.TradingStartTime}-{_config.TradingEndTime})";
return false;
}
// 2. Max Positions Check
if (!HasCapacityForNewPosition())
{
reason = $"Max positions reached ({_config.MaxPositions})";
return false;
}
// 3. Existing Position Check
if (HasExistingPosition(symbol))
{
reason = "Already have position in this symbol";
return false;
}
// 4. Available Capital Check
if (!HasSufficientCapital())
{
reason = "Insufficient capital for new position";
return false;
}
// 5. Volatility Check (if configured)
if (_config.MinImpliedVolatility > 0 && !MeetsVolatilityRequirement(symbol, slice))
{
reason = $"Implied volatility below minimum ({_config.MinImpliedVolatility:P0})";
return false;
}
// 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.
/// </summary>
private bool IsWithinTradingHours(DateTime currentTime)
{
var timeOfDay = currentTime.TimeOfDay;
return timeOfDay >= _config.TradingStartTime && timeOfDay <= _config.TradingEndTime;
}
/// <summary>
/// Check if we have capacity for a new position based on MaxPositions.
/// </summary>
private bool HasCapacityForNewPosition()
{
var currentPositions = _algorithm.Portfolio
.Where(kvp => kvp.Value.Invested)
.Count();
return currentPositions < _config.MaxPositions;
}
/// <summary>
/// Check if we already have a position in this symbol.
/// </summary>
private bool HasExistingPosition(Symbol symbol)
{
// Check direct position
if (_algorithm.Portfolio[symbol].Invested)
return true;
// For options, also check if we have positions in the underlying
if (symbol.SecurityType == SecurityType.Option)
{
var underlying = symbol.Underlying;
return _algorithm.Portfolio.Any(kvp =>
kvp.Key.Underlying == underlying && kvp.Value.Invested);
}
return false;
}
/// <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 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 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
{
/// <summary>
/// Handle option assignment by immediately liquidating assigned underlying shares.
/// Uses QC's Portfolio system for position tracking and MarketOrder for liquidation.
/// </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}");
// Get the assigned position from QC's Portfolio system
var assignedSymbol = assignmentEvent.Symbol;
// For option assignments, we need to check the underlying symbol
var underlyingSymbol = GetUnderlyingSymbol(assignedSymbol);
if (underlyingSymbol == null)
{
algorithm.Error($"Could not determine underlying symbol for assignment: {assignedSymbol}");
return;
}
// Check if we have an equity position from the assignment using QC's Portfolio
var holding = algorithm.Portfolio[underlyingSymbol];
if (holding.Invested && holding.Quantity != 0)
{
algorithm.Log($"Assignment Details: {underlyingSymbol.Value} - Quantity: {holding.Quantity}, " +
$"Value: ${holding.HoldingsValue:F0}");
// Execute immediate liquidation (simplified approach)
ExecuteLiquidation(algorithm, underlyingSymbol, holding);
}
else
{
algorithm.Log($"WARNING: No equity position found for assignment: {underlyingSymbol.Value}");
}
// Log assignment impact for tracking
LogAssignmentImpact(algorithm, assignmentEvent, underlyingSymbol);
}
catch (Exception ex)
{
algorithm.Error($"Error handling assignment: {ex.Message}");
algorithm.Error($"Stack trace: {ex.StackTrace}");
}
}
/// <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>
/// Execute immediate liquidation of assigned shares.
/// </summary>
private static void ExecuteLiquidation(QCAlgorithm algorithm, Symbol underlyingSymbol, SecurityHolding holding)
{
// Use QC's MarketOrder for immediate liquidation
var liquidationQuantity = -(int)holding.Quantity; // Opposite sign to close position, convert to int
var orderTicket = algorithm.MarketOrder(underlyingSymbol, liquidationQuantity, tag: "Assignment Auto-Liquidation");
algorithm.Log($"LIQUIDATING ASSIGNMENT: {underlyingSymbol.Value} - " +
$"Quantity: {liquidationQuantity}, Order ID: {orderTicket.OrderId}");
}
/// <summary>
/// Log assignment impact for performance tracking and analysis.
/// </summary>
private static void LogAssignmentImpact(QCAlgorithm algorithm, OrderEvent assignmentEvent, Symbol underlyingSymbol)
{
try
{
var holding = algorithm.Portfolio[underlyingSymbol];
var assignmentValue = Math.Abs(holding.HoldingsValue);
var portfolioImpact = assignmentValue / algorithm.Portfolio.TotalPortfolioValue * 100;
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>
/// 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;
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 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 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)
/// </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;
TradeTracker.AddWorkingTrade(orderId, symbol, strategyName, ticket.Tag);
Debug($"Tracking working order: {orderId} for {symbol}");
}
}
/// <summary>
/// Mark an order as filled (moved from working to open)
/// </summary>
public void TrackOrderFilled(OrderEvent orderEvent)
{
if (orderEvent?.OrderId != null)
{
var orderId = orderEvent.OrderId.ToString();
TradeTracker.MarkTradeAsOpen(orderId, orderEvent.FillPrice, (int)orderEvent.FillQuantity);
Debug($"Order filled: {orderId} at {orderEvent.FillPrice}");
OnOrderFilled(orderEvent);
}
}
/// <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}");
}
/// <summary>
/// Cancel a working order
/// </summary>
public void TrackOrderCancelled(string orderId)
{
TradeTracker.CancelWorkingTrade(orderId);
Debug($"Order cancelled: {orderId}");
}
/// <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.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 = "") => Algorithm.LimitOrder(symbol, quantity, limitPrice, tag);
protected OrderTicket StopMarketOrder(Symbol symbol, decimal quantity, decimal stopPrice, string tag = "")
=> Algorithm.StopMarketOrder(symbol, quantity, stopPrice, tag);
protected OrderTicket StopLimitOrder(Symbol symbol, decimal quantity, decimal stopPrice, decimal limitPrice, string tag = "")
=> Algorithm.StopLimitOrder(symbol, quantity, stopPrice, limitPrice, 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
{
protected 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)
protected SimpleTradeTracker TradeTracker { get; private set; } = new SimpleTradeTracker();
/// <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);
// 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
}
}using System;
using System.Collections.Generic;
using System.Linq;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Simple trade tracking system that maintains lists of trades in different states
/// Similar to the Python setupbasestructure.py arrays for position management
/// </summary>
public class SimpleTradeTracker
{
/// <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
/// </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);
}
}
/// <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();
}
}
}#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;
namespace CoreAlgo.Architecture.Core.Implementations
{
/// <summary>
/// Simple trade record for tracking individual trades through their lifecycle
/// </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
public decimal PnL { get; set; }
public string OrderTag { get; set; } = string.Empty;
/// <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;
}
/// <summary>
/// Marks trade as open with fill details
/// </summary>
public void MarkAsOpen(decimal fillPrice, int fillQuantity)
{
Status = "Open";
OpenPrice = fillPrice;
Quantity = fillQuantity;
}
/// <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;
}
/// <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;
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 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.ComponentModel;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Configuration for 1-DTE SPX Call Debit Spread strategy
/// Enters once per day at a specific time and holds to expiration (no proactive exits)
/// </summary>
public class SPXCallDebitConfig : StrategyConfig
{
public SPXCallDebitConfig()
{
// Defaults aligned with plan (SPX weekly options)
UnderlyingSymbol = "SPX";
Symbols = new[] { "SPX" };
// Disable regular-hours gating (we enter at night)
TradingStartTime = TimeSpan.Zero;
TradingEndTime = TimeSpan.Zero;
StartDate = new DateTime(2018, 1, 1);
EndDate = new DateTime(2025, 8, 30);
// Set high base cap; enforce spread-level caps in template
MaxPositions = 10;
// Disable generic profit/stop logic (hold-to-expiration behavior)
ProfitTarget = 0m;
StopLoss = 0m;
}
[StrategyParameter("EntryTime", "15:57")]
[Description("Exact daily entry time (ET) in HH:mm; default 15:57 (near market close)")]
public string EntryTime { get; set; } = "15:57";
[StrategyParameter("TargetDte", 1)]
[Description("Target Days-To-Expiry (1 = next trading day)")]
public int TargetDte { get; set; } = 1;
[StrategyParameter("LongCallDelta", 0.30)]
[Description("Target delta for long call (lower strike; ~0.30 by default)")]
public decimal LongCallDelta { get; set; } = 0.30m;
[StrategyParameter("ShortCallDelta", 0.10)]
[Description("Target delta for short call (higher strike; ~0.10 by default)")]
public decimal ShortCallDelta { get; set; } = 0.10m;
[StrategyParameter("ContractSize", 1)]
[Description("Number of debit spread contracts per entry")]
public int ContractSize { get; set; } = 1;
[StrategyParameter("MaxActiveSpreads", 2)]
[Description("Maximum number of active SPX debit spreads (spread-aware cap)")]
public int MaxActiveSpreads { get; set; } = 2;
// Simplified: no additional distance-from-close filter
public override string[] OptimizationParameters => new[]
{
"Strategy",
"StartDate",
"EndDate",
"AccountSize",
"AllocationPerPosition",
"MaxPositions",
"EntryTime",
"TargetDte",
"LongCallDelta",
"ShortCallDelta",
"ContractSize"
};
public override string ToString()
{
return $"SPXCallDebit[{UnderlyingSymbol}] Entry:{EntryTime} DTE:{TargetDte} LongΔ:{LongCallDelta:F2} ShortΔ:{ShortCallDelta:F2} x{ContractSize}";
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
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>
/// 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>
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.Algorithm;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using CoreAlgo.Architecture.Core.Interfaces;
namespace CoreAlgo.Architecture.Core.Services
{
/// <summary>
/// TODO: INTEGRATE LATER - Infrastructure component for asset correlation analysis.
///
/// CURRENT STATUS: Built but not integrated with templates yet.
/// REASON: Keeping main system simple while building foundation for future enhancements.
///
/// FUTURE INTEGRATION PLAN:
/// 1. Add to CoreAlgoRiskManager when main system is stable
/// 2. Integrate with MultiAssetIronCondorTemplate for correlation-based position sizing
/// 3. Add correlation-based asset selection to strategy templates
///
/// This component leverages QuantConnect's historical data APIs to calculate asset correlations
/// and provide correlation-based risk management capabilities.
/// </summary>
public class CorrelationCalculator
{
private readonly IAlgorithmContext _context;
private readonly Dictionary<string, List<decimal>> _priceHistory;
private readonly Dictionary<(string, string), decimal> _correlationCache;
private readonly int _defaultLookbackDays;
public CorrelationCalculator(IAlgorithmContext context, int defaultLookbackDays = 30)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_priceHistory = new Dictionary<string, List<decimal>>();
_correlationCache = new Dictionary<(string, string), decimal>();
_defaultLookbackDays = defaultLookbackDays;
}
/// <summary>
/// TODO: FUTURE USE - Calculates correlation between two assets using historical price data.
///
/// Uses QuantConnect's historical data API to fetch price series and calculate Pearson correlation.
/// Caches results to avoid repeated calculations for performance.
/// </summary>
/// <param name="symbol1">First asset symbol</param>
/// <param name="symbol2">Second asset symbol</param>
/// <param name="lookbackDays">Number of days for correlation calculation</param>
/// <returns>Correlation coefficient (-1 to 1)</returns>
public decimal CalculateCorrelation(string symbol1, string symbol2, int? lookbackDays = null)
{
try
{
var days = lookbackDays ?? _defaultLookbackDays;
var cacheKey = (symbol1, symbol2);
// Check cache first (TODO: Add cache expiration)
if (_correlationCache.ContainsKey(cacheKey))
{
return _correlationCache[cacheKey];
}
// Get historical price data using QC's API
var prices1 = GetHistoricalPrices(symbol1, days);
var prices2 = GetHistoricalPrices(symbol2, days);
if (prices1.Count < 10 || prices2.Count < 10)
{
((ILogger<CorrelationCalculator>)_context.Logger).LogWarning($"Insufficient price data for correlation: {symbol1} ({prices1.Count}), {symbol2} ({prices2.Count})");
return 0; // No correlation if insufficient data
}
// Calculate Pearson correlation coefficient
var correlation = CalculatePearsonCorrelation(prices1, prices2);
// Cache result
_correlationCache[cacheKey] = correlation;
_correlationCache[(symbol2, symbol1)] = correlation; // Symmetric
((ILogger<CorrelationCalculator>)_context.Logger).LogDebug($"Correlation calculated: {symbol1} vs {symbol2} = {correlation:F3} ({days} days)");
return correlation;
}
catch (Exception ex)
{
((ILogger<CorrelationCalculator>)_context.Logger).LogError(ex, $"Error calculating correlation between {symbol1} and {symbol2}: {ex.Message}");
return 0; // Default to no correlation on error
}
}
/// <summary>
/// TODO: FUTURE USE - Gets correlation matrix for a set of symbols.
/// Useful for portfolio construction and risk analysis.
/// </summary>
public Dictionary<(string, string), decimal> GetCorrelationMatrix(string[] symbols, int? lookbackDays = null)
{
var matrix = new Dictionary<(string, string), decimal>();
for (int i = 0; i < symbols.Length; i++)
{
for (int j = i + 1; j < symbols.Length; j++)
{
var correlation = CalculateCorrelation(symbols[i], symbols[j], lookbackDays);
matrix[(symbols[i], symbols[j])] = correlation;
}
}
return matrix;
}
/// <summary>
/// TODO: FUTURE USE - Checks if a new asset would create high correlation with existing positions.
/// Prevents adding highly correlated assets to reduce concentration risk.
/// </summary>
public bool ShouldPreventTrade(string newSymbol, string[] existingSymbols, decimal maxCorrelation = 0.7m)
{
foreach (var existingSymbol in existingSymbols)
{
var correlation = Math.Abs(CalculateCorrelation(newSymbol, existingSymbol));
if (correlation > maxCorrelation)
{
((ILogger<CorrelationCalculator>)_context.Logger).LogWarning($"High correlation detected: {newSymbol} vs {existingSymbol} = {correlation:F3} > {maxCorrelation:F3}");
return true; // Prevent trade
}
}
return false; // Allow trade
}
/// <summary>
/// TODO: FUTURE USE - Gets highly correlated assets for a given symbol.
/// Useful for risk monitoring and position sizing adjustments.
/// </summary>
public List<CorrelatedAsset> GetHighlyCorrelatedAssets(string symbol, string[] candidateSymbols, decimal threshold = 0.6m)
{
var correlatedAssets = new List<CorrelatedAsset>();
foreach (var candidate in candidateSymbols)
{
if (candidate == symbol) continue;
var correlation = CalculateCorrelation(symbol, candidate);
if (Math.Abs(correlation) > threshold)
{
correlatedAssets.Add(new CorrelatedAsset
{
Symbol = candidate,
Correlation = correlation,
IsPositivelyCorrelated = correlation > 0
});
}
}
return correlatedAssets.OrderByDescending(x => Math.Abs(x.Correlation)).ToList();
}
/// <summary>
/// Gets historical daily closing prices using QuantConnect's History API.
/// TODO: Consider using returns instead of prices for better correlation calculation.
/// </summary>
private List<decimal> GetHistoricalPrices(string symbol, int days)
{
try
{
// TODO: Implement actual QC History API call
// var history = _context.Algorithm.History<TradeBar>(symbol, days, Resolution.Daily);
// return history.Select(x => x.Close).ToList();
// PLACEHOLDER: Return mock data for now
// This will be replaced with actual QC History API call during integration
var random = new Random(symbol.GetHashCode());
var prices = new List<decimal>();
var basePrice = 100m;
for (int i = 0; i < days; i++)
{
basePrice *= (decimal)(1 + (random.NextDouble() - 0.5) * 0.02); // ±1% daily moves
prices.Add(basePrice);
}
return prices;
}
catch (Exception ex)
{
((ILogger<CorrelationCalculator>)_context.Logger).LogError(ex, $"Error fetching historical prices for {symbol}: {ex.Message}");
return new List<decimal>();
}
}
/// <summary>
/// Calculates Pearson correlation coefficient between two price series.
/// </summary>
private decimal CalculatePearsonCorrelation(List<decimal> prices1, List<decimal> prices2)
{
var count = Math.Min(prices1.Count, prices2.Count);
if (count < 2) return 0;
// Calculate returns instead of using raw prices
var returns1 = new List<decimal>();
var returns2 = new List<decimal>();
for (int i = 1; i < count; i++)
{
if (prices1[i - 1] != 0 && prices2[i - 1] != 0)
{
returns1.Add((prices1[i] - prices1[i - 1]) / prices1[i - 1]);
returns2.Add((prices2[i] - prices2[i - 1]) / prices2[i - 1]);
}
}
if (returns1.Count < 2) return 0;
var mean1 = returns1.Average();
var mean2 = returns2.Average();
var numerator = returns1.Zip(returns2, (x, y) => (x - mean1) * (y - mean2)).Sum();
var denominator1 = Math.Sqrt((double)returns1.Sum(x => (x - mean1) * (x - mean1)));
var denominator2 = Math.Sqrt((double)returns2.Sum(x => (x - mean2) * (x - mean2)));
if (denominator1 == 0 || denominator2 == 0) return 0;
return (decimal)(numerator / (decimal)(denominator1 * denominator2));
}
}
/// <summary>
/// Represents an asset that is correlated with another asset.
/// TODO: FUTURE USE - Used for correlation-based risk analysis.
/// </summary>
public class CorrelatedAsset
{
public string Symbol { get; set; }
public decimal Correlation { get; set; }
public bool IsPositivelyCorrelated { get; set; }
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Interfaces;
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;
}
}
#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;
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);
// Place initial limit order
var ticket = _algorithm.LimitOrder(symbol, quantity, initialPrice, 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, initialPrice);
_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 comboTracker = new ComboOrderTracker(comboTickets, legs, comboQuote,
comboDirection, _pricingEngine.Mode, initialNetPrice.Value);
// Track using primary order ID
_activeComboOrders[comboTracker.PrimaryOrderId] = comboTracker;
((dynamic)_context.Logger).Info($"SmartCombo: Placed {legs.Count}-leg combo limit order " +
$"at net price ${initialNetPrice.Value: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)
{
// Update order price
var updateFields = new UpdateOrderFields { LimitPrice = nextPrice.Value };
var response = tracker.OrderTicket.Update(updateFields);
if (response.IsSuccess)
{
tracker.CurrentPrice = nextPrice.Value;
((dynamic)_context.Logger).Debug($"SmartOrder: Updated order {tracker.OrderTicket.OrderId} " +
$"price to ${nextPrice.Value: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)
{
tracker.UpdateNetPrice(nextNetPrice.Value, 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 QuantConnect;
using QuantConnect.Data;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using QuantConnect.Orders;
using CoreAlgo.Architecture.Core.Implementations;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Architecture.Core.Templates
{
/// <summary>
/// 1-DTE SPX Call Debit Spread
/// - Enters once per day at configured time (default 15:57 ET)
/// - Long call ~0.30Δ (lower strike), short call ~0.10Δ (higher strike)
/// - Holds to expiration (no proactive exit), single active position until expiry
/// </summary>
public class SPXCallDebitTemplate : SimpleBaseStrategy
{
private SPXCallDebitConfig _config;
private Symbol _underlying;
private Symbol _optionSymbol;
public override string Name => "SPX Call Debit (1-DTE)";
public override string Description =>
"Buys ~0.30Δ call and sells ~0.10Δ call for next-day expiry at 22:57 ET; holds to expiration with overlap prevention.";
public override void OnInitialize()
{
SmartLog("SPXCallDebitTemplate.OnInitialize() starting...");
try
{
Configure<SPXCallDebitConfig>();
_config = (SPXCallDebitConfig)Config;
SmartLog($"Configuration loaded: {_config}");
}
catch (Exception ex)
{
SmartError($"Failed to load configuration: {ex.Message}");
throw;
}
// Add SPX underlying + options; filter for next-day expiries and calls
// Using 1-2 DTE window to ensure visibility of next trading day chain
var resolution = Resolution.Minute;
(_underlying, _optionSymbol) = SetupOptionsForSymbol(_config.UnderlyingSymbol,
strikeRange: 50, minDTE: Math.Max(1, _config.TargetDte), maxDTE: Math.Max(2, _config.TargetDte + 1),
callsOnly: true, putsOnly: false, resolution: resolution);
SmartLog($"Initialized for {_underlying} with options {_optionSymbol}; Entry {_config.EntryTime} ET");
}
protected override bool OnShouldExecuteTrade(Slice slice, out string blockReason)
{
blockReason = "";
if (_underlying == default || _optionSymbol == default)
{
blockReason = "Underlying or options not initialized";
return false;
}
// Only attempt at the exact configured minute
if (!TryParseTime(_config.EntryTime, slice.Time.Date, out var entryTime))
{
blockReason = $"Invalid EntryTime '{_config.EntryTime}'";
return false;
}
if (!(slice.Time.Hour == entryTime.Hour && slice.Time.Minute == entryTime.Minute))
{
blockReason = "Outside entry minute";
return false;
}
// Spread-aware cap: count distinct SPX option expiries with >=2 call legs open
if (_config.MaxActiveSpreads > 0)
{
var openCallLegs = Algorithm.Portfolio.Values
.Where(h => h.Invested &&
h.Symbol.SecurityType == SecurityType.Option &&
h.Symbol.Underlying == _underlying &&
h.Symbol.ID.OptionRight == OptionRight.Call)
.ToList();
var activeSpreads = openCallLegs
.GroupBy(h => h.Symbol.ID.Date)
.Count(g => g.Select(x => x.Symbol.ID.StrikePrice).Distinct().Count() >= 2);
if (activeSpreads >= _config.MaxActiveSpreads)
{
blockReason = $"Max spreads reached ({activeSpreads}/{_config.MaxActiveSpreads})";
return false;
}
}
return true;
}
protected override void OnExecute(Slice slice)
{
// Safety: ensure option chain exists (with fallback keys)
if (!slice.OptionChains.TryGetValue(_optionSymbol, out var chain) || !chain.Any())
{
var usedFallback = false;
// Try underlying key
if (slice.OptionChains.TryGetValue(_underlying, out var chainByUnderlying) && chainByUnderlying.Any())
{
chain = chainByUnderlying;
usedFallback = true;
SmartLog($"Using fallback option chain keyed by underlying {_underlying}");
}
else
{
// Try canonical symbol
try
{
var canonical = Symbol.CreateCanonicalOption(_underlying);
if (slice.OptionChains.TryGetValue(canonical, out var canonicalChain) && canonicalChain.Any())
{
chain = canonicalChain;
usedFallback = true;
SmartLog($"Using fallback canonical option chain {canonical}");
}
}
catch { }
}
if (!usedFallback)
{
SmartLog($"No option chain available for {_optionSymbol} at {slice.Time:yyyy-MM-dd HH:mm}");
if (slice.OptionChains.Count > 0)
{
SmartLog($"Available option chains in slice: {string.Join(", ", slice.OptionChains.Keys)}");
}
else
{
SmartLog("No option chains available in slice at all");
}
return;
}
}
// Pick next trading day's expiry (>= target DTE; prefer nearest > today)
var today = slice.Time.Date;
var expiries = chain.Select(c => c.Expiry.Date).Distinct().OrderBy(d => d).ToList();
var targetExpiry = expiries.FirstOrDefault(d => d > today);
if (targetExpiry == default)
{
SmartLog("No future expiries found (cannot enter)");
return;
}
var calls = chain
.Where(c => c.Right == OptionRight.Call && c.Expiry.Date == targetExpiry)
.Where(c => c.BidPrice > 0 && c.AskPrice > 0)
.OrderBy(c => c.Strike)
.ToList();
if (calls.Count < 2)
{
SmartLog($"Not enough calls for {targetExpiry:yyyy-MM-dd}; found {calls.Count}");
return;
}
// Select long (~0.30Δ) and short (~0.10Δ) calls
var longCall = calls.FirstOrDefault();
var shortCall = calls.FirstOrDefault();
// Prefer contracts with Greeks data; fallback to ATM distance if missing
var candidatesWithGreeks = calls.Where(c => c.Greeks != null).ToList();
if (candidatesWithGreeks.Any())
{
longCall = candidatesWithGreeks
.OrderBy(c => Math.Abs(Math.Abs((double)c.Greeks.Delta) - (double)_config.LongCallDelta))
.FirstOrDefault();
if (longCall != null)
{
shortCall = candidatesWithGreeks
.Where(c => c.Strike > longCall.Strike)
.OrderBy(c => Math.Abs(Math.Abs((double)c.Greeks.Delta) - (double)_config.ShortCallDelta))
.FirstOrDefault();
}
}
if (longCall == null || shortCall == null)
{
// Fallback: approximate by distance from ATM
var underlyingPrice = Securities[_underlying].Price;
var atm = calls.Select(x => x.Strike).OrderBy(s => Math.Abs(s - underlyingPrice)).FirstOrDefault();
longCall ??= calls
.OrderBy(c => Math.Abs((double)(c.Strike - atm))) // closest to ATM
.FirstOrDefault();
if (longCall != null)
{
shortCall ??= calls.FirstOrDefault(c => c.Strike > longCall.Strike);
}
}
if (longCall == null || shortCall == null)
{
SmartLog("Unable to select suitable strikes for debit spread");
return;
}
// Simplified: no additional distance-from-close filter
// Build debit spread legs: Long lower strike (+1), Short higher strike (-1)
var legs = new List<Leg>
{
Leg.Create(longCall.Symbol, 1),
Leg.Create(shortCall.Symbol, -1)
};
var qty = Math.Max(1, _config.ContractSize);
var tickets = ComboMarketOrder(legs, qty, tag: "SPX Call Debit 1DTE");
SmartLog(
$"DEBIT SPREAD OPENED:\n" +
$" Expiry: {targetExpiry:yyyy-MM-dd}\n" +
$" Long: {longCall.Strike}C @ ${longCall.AskPrice:F2}\n" +
$" Short: {shortCall.Strike}C @ ${shortCall.BidPrice:F2}\n" +
$" Contracts: {qty}"
);
if (tickets == null || !tickets.Any())
{
SmartWarn("Combo order returned no tickets (may be blocked by overlap or pricing)");
}
}
private bool TryParseTime(string hhmm, DateTime date, out DateTime resolved)
{
resolved = default;
if (string.IsNullOrWhiteSpace(hhmm)) return false;
var parts = hhmm.Split(':');
if (parts.Length < 2) return false;
if (!int.TryParse(parts[0], out var h)) return false;
if (!int.TryParse(parts[1], out var m)) return false;
if (h < 0 || h > 23 || m < 0 || m > 59) return false;
resolved = date.AddHours(h).AddMinutes(m);
return true;
}
}
}
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 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;
// 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);
// 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;
Log($"UNIVERSE SETTINGS -> Resolution: {UniverseSettings.Resolution}, FillForward: {UniverseSettings.FillForward}, ExtendedMktHours: {UniverseSettings.ExtendedMarketHours}");
Logger?.Info($"UniverseSettings configured. Resolution={UniverseSettings.Resolution}, FillForward={UniverseSettings.FillForward}, ExtendedMarketHours={UniverseSettings.ExtendedMarketHours}");
Log("BASIC SETUP COMPLETED");
// Initialize smart logger with debug logging
Log("=== DEBUG: About to create QCLogger ===");
var logLevel = GetParameter("LogLevel", 2); // 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");
}
_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
if (data.OptionChains.Count > 0)
{
Logger.Info($"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)
{
try
{
baseStrategy.SmartOrderManager.OnOrderEvent(orderEvent);
}
catch (Exception ex)
{
Logger.LogError(ex, $"Error processing order event {orderEvent.OrderId}");
}
}
// Track order events for trade tracking system (like Python position tracking)
if (_strategy is SimpleBaseStrategy strategy)
{
try
{
// Track filled orders
if (orderEvent.Status == OrderStatus.Filled)
{
strategy.TrackOrderFilled(orderEvent);
}
// Track cancelled orders
else if (orderEvent.Status == OrderStatus.Canceled)
{
strategy.TrackOrderCancelled(orderEvent.OrderId.ToString());
}
}
catch (Exception ex)
{
Logger.LogError(ex, $"Error tracking order event {orderEvent.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}");
// 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);
}
public override void OnEndOfAlgorithm()
{
// DEBUG: Immediate verification that OnEndOfAlgorithm() is called
Log("=== DEBUG: OnEndOfAlgorithm() CALLED - IMMEDIATE CONFIRMATION ===");
Error("=== DEBUG: OnEndOfAlgorithm() CALLED - VIA ERROR FOR VISIBILITY ===");
// 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 all securities and applies backtesting configurations
/// </summary>
private void CompleteSecurityInitializer(Security security)
{
Logger?.Debug($"CompleteSecurityInitializer: {security.Symbol} ({security.Type})");
// CRITICAL: Disable buying power on ALL 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
#pragma warning disable CS0618 // Type or member is obsolete
security.SetDataNormalizationMode(DataNormalizationMode.Raw);
#pragma warning restore CS0618
var lastPrices = GetLastKnownPrices(security);
if (lastPrices != null && lastPrices.Any())
{
security.SetMarketPrice(lastPrices.First());
}
// Type-specific configurations for index options
// Note: Option assignment model is handled at the strategy level
}
}
}