| Overall Statistics |
|
Total Orders 506 Average Win 18.94% Average Loss -9.16% Compounding Annual Return 11.579% Drawdown 62.000% Expectancy 0.339 Start Equity 100000 End Equity 205717.07 Net Profit 105.717% Sharpe Ratio 0.342 Sortino Ratio 0.129 Probabilistic Sharpe Ratio 4.438% Loss Rate 56% Win Rate 44% Profit-Loss Ratio 2.07 Alpha 0.075 Beta 0.014 Annual Standard Deviation 0.222 Annual Variance 0.049 Information Ratio -0.089 Tracking Error 0.277 Treynor Ratio 5.473 Total Fees $1625.70 Estimated Strategy Capacity $530000.00 Lowest Capacity Asset SPXW 32TBR94UTM6B2|SPX 31 Portfolio Turnover 1.05% Drawdown Recovery 883 |
#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.
/// </summary>
private bool IsProfitTargetReached(SecurityHolding holding)
{
return holding.UnrealizedProfitPercent >= _config.ProfitTarget;
}
/// <summary>
/// Check if stop loss has been triggered.
/// </summary>
private bool IsStopLossTriggered(SecurityHolding holding)
{
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 (holding.UnrealizedProfitPercent <= _config.StopLoss)
{
urgency = 1.0;
}
// Profit target is high priority
else if (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;
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 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>
/// 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 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 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}");
}
}
}
}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>
Information = 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)
{
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;
public QCLogger(QCAlgorithm algorithm, int logLevel = 3)
{
_algorithm = algorithm ?? throw new ArgumentNullException(nameof(algorithm));
_categoryName = typeof(T).Name;
_currentLogLevel = logLevel;
}
public void LogInformation(string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
StoreSmartMessage("INFO", formattedMessage);
}
public void LogDebug(string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
StoreSmartMessage("DEBUG", formattedMessage);
}
public void LogWarning(string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
StoreSmartMessage("WARN", formattedMessage);
}
public void LogError(string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
StoreSmartMessage("ERROR", formattedMessage);
}
public void LogError(Exception exception, string message, params object[] args)
{
var formattedMessage = FormatMessage(message, args);
var fullMessage = $"{formattedMessage} - Exception: {exception.Message}";
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;
// Route to appropriate logging method based on level
switch (level)
{
case LogLevel.Error:
LogError(message);
break;
case LogLevel.Warning:
LogWarning(message);
break;
case LogLevel.Information:
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.Information);
/// <summary>
/// Convenience method for debug logging
/// </summary>
public void Debug(string message) => LogMessage(message, LogLevel.Debug);
/// <summary>
/// Store message for smart processing with grouping for statistical analysis
/// </summary>
private void StoreSmartMessage(string level, string message)
{
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);
}
}
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";
}
}
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.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
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;
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 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; }
// 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);
// 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();
// 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);
// Smart order methods that use SmartOrderManager when available and enabled
protected OrderTicket MarketOrder(Symbol symbol, decimal quantity, string tag = "")
{
if (ShouldUseSmartPricing())
{
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 List<OrderTicket> ComboMarketOrder(List<Leg> legs, int quantity, string tag = "")
{
if (ShouldUseSmartPricing())
{
return SmartOrderManager.SmartComboMarketOrder(legs, quantity, tag);
}
return Algorithm.ComboMarketOrder(legs, quantity, tag: tag);
}
// 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;
}
// 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)");
}
}
protected void SetStartDate(DateTime date) => Algorithm.SetStartDate(date);
protected void SetEndDate(DateTime date) => Algorithm.SetEndDate(date);
protected void SetCash(decimal cash) => Algorithm.SetCash(cash);
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() { }
/// <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}");
}
}
/// <inheritdoc/>
public virtual void Execute(Slice slice)
{
((dynamic)Logger).Debug($"SimpleBaseStrategy.Execute called, State: {State}");
if (State == StrategyState.Ready)
State = StrategyState.Running;
if (State != StrategyState.Running)
{
((dynamic)Logger).Debug($"Strategy not running, state is {State}");
return;
}
try
{
// Update positions using QC's native Portfolio
// QC's Portfolio updates automatically
// Execute strategy logic
((dynamic)Logger).Debug("Calling OnExecute...");
OnExecute(slice);
((dynamic)Logger).Debug("OnExecute completed");
}
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(SmartLog);
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>
/// 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;
}
/// <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;
}
}
/// <summary>
/// Helper method for debug logging using context pattern
/// </summary>
protected void Debug(string message) => ((dynamic)Logger).Debug(message);
/// <summary>
/// Helper method for information logging using context pattern
/// </summary>
protected void SmartLog(string message) => ((dynamic)Logger).Info(message);
/// <summary>
/// Helper method for warning logging using context pattern
/// </summary>
protected void SmartWarn(string message) => ((dynamic)Logger).Warning(message);
/// <summary>
/// Helper method for error logging using context pattern
/// </summary>
protected void SmartError(string message) => ((dynamic)Logger).Error(message);
// ============================================================================
// 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}");
}
}
/// <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}");
}
// ============================================================================
// 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()}");
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));
}
else if (putsOnly)
{
SmartLog($"[TARGET] Applying PUTS ONLY filter");
option.SetFilter(filter => filter
.PutsOnly()
.Strikes(-strikeRange, strikeRange)
.Expiration(minDTE, maxDTE));
}
else
{
SmartLog($"[TARGET] Applying BOTH CALLS AND PUTS filter");
option.SetFilter(filter => filter
.Strikes(-strikeRange, strikeRange)
.Expiration(minDTE, maxDTE));
}
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));
}
}
}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 System.IO, 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;
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}";
}
}
}#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>
/// 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>
/// 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;
#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);
}
}#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
}
}using System;
using System.ComponentModel;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Configuration for Cash Secured Put options strategy
/// QC-First approach - minimal wrapper for maximum flexibility
/// </summary>
public class CashSecuredPutConfig : StrategyConfig
{
[StrategyParameter("PutStrikeOffset", 0.02)]
[Description("Offset below current price for put strike (e.g., 0.02 = 2% OTM)")]
public decimal PutStrikeOffset { get; set; } = 0.02m; // 2% OTM for more opportunities
[StrategyParameter("MinDaysToExpiration", 1)]
[Description("Minimum days to expiration for put options")]
public int MinDaysToExpiration { get; set; } = 1; // More aggressive - allow shorter expiry
[StrategyParameter("MaxDaysToExpiration", 60)]
[Description("Maximum days to expiration for put options")]
public int MaxDaysToExpiration { get; set; } = 60; // Wider range for more options
[StrategyParameter("MinimumPremium", 0.25)]
[Description("Minimum premium to collect per contract")]
public decimal MinimumPremium { get; set; } = 0.25m; // $25 per contract minimum - much more aggressive
[StrategyParameter("CashPerContract", 0)]
[Description("Cash required per contract (0 = auto-calculate from strike)")]
public decimal CashPerContract { get; set; } = 0m; // Auto-calculate: strike * 100
[StrategyParameter("MaxActivePositions", 2)]
[Description("Maximum number of cash secured put positions")]
public int MaxActivePositions { get; set; } = 2; // Conservative position limit
[StrategyParameter("UseMarginReduction", false)]
[Description("Use margin reduction strategies if available")]
public bool UseMarginReduction { get; set; } = false; // Cash secured only by default
[StrategyParameter("AcceptAssignment", true)]
[Description("Accept assignment and buy shares if ITM at expiration")]
public bool AcceptAssignment { get; set; } = true; // Willing to own the stock
public override string ToString()
{
return $"CashSecuredPut[{UnderlyingSymbol}] Strike:{PutStrikeOffset:P1} DTE:{MinDaysToExpiration}-{MaxDaysToExpiration} MinPrem:${MinimumPremium}";
}
public override string[] Validate()
{
var errors = new System.Collections.Generic.List<string>();
if (PutStrikeOffset <= 0 || PutStrikeOffset > 0.20m)
errors.Add("PutStrikeOffset must be between 0% and 20%");
if (MinDaysToExpiration < 1 || MinDaysToExpiration > MaxDaysToExpiration)
errors.Add("Invalid DTE range");
if (MinimumPremium < 0)
errors.Add("MinimumPremium cannot be negative");
if (CashPerContract < 0)
errors.Add("CashPerContract cannot be negative");
if (MaxActivePositions < 1)
errors.Add("MaxActivePositions must be at least 1");
return errors.ToArray();
}
}
}using System;
using System.ComponentModel;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Configuration for Covered Call options strategy
/// QC-First approach - minimal wrapper for maximum flexibility
/// </summary>
public class CoveredCallConfig : StrategyConfig
{
[StrategyParameter("CallStrikeOffset", 0.02)]
[Description("Offset above current stock price for call strike (e.g., 0.02 = 2% OTM)")]
public decimal CallStrikeOffset { get; set; } = 0.02m; // 2% OTM for reasonable premium
[StrategyParameter("MinDaysToExpiration", 7)]
[Description("Minimum days to expiration for call options")]
public int MinDaysToExpiration { get; set; } = 7; // Weekly options minimum
[StrategyParameter("MaxDaysToExpiration", 45)]
[Description("Maximum days to expiration for call options")]
public int MaxDaysToExpiration { get; set; } = 45; // Monthly options maximum
[StrategyParameter("MinimumPremium", 0.50)]
[Description("Minimum premium to collect per call contract")]
public decimal MinimumPremium { get; set; } = 0.50m; // $50 per contract minimum
[StrategyParameter("SharesPerContract", 100)]
[Description("Number of shares per covered call (typically 100)")]
public int SharesPerContract { get; set; } = 100; // Standard option contract size
[StrategyParameter("MaxActivePositions", 3)]
[Description("Maximum number of covered call positions")]
public int MaxActivePositions { get; set; } = 3; // Multiple positions allowed
[StrategyParameter("BuySharesIfNeeded", true)]
[Description("Automatically buy shares if not owned")]
public bool BuySharesIfNeeded { get; set; } = true; // Auto-buy underlying
public override string ToString()
{
return $"CoveredCall[{UnderlyingSymbol}] Strike:{CallStrikeOffset:P1} DTE:{MinDaysToExpiration}-{MaxDaysToExpiration} MinPrem:${MinimumPremium}";
}
public override string[] Validate()
{
var errors = new System.Collections.Generic.List<string>();
if (CallStrikeOffset <= 0 || CallStrikeOffset > 0.20m)
errors.Add("CallStrikeOffset must be between 0% and 20%");
if (MinDaysToExpiration < 1 || MinDaysToExpiration > MaxDaysToExpiration)
errors.Add("Invalid DTE range");
if (MinimumPremium < 0)
errors.Add("MinimumPremium cannot be negative");
if (SharesPerContract != 100)
errors.Add("SharesPerContract must be 100 for standard options");
return errors.ToArray();
}
}
}using System;
using System.ComponentModel;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Configuration for Iron Condor options strategy
/// </summary>
public class IronCondorConfig : StrategyConfig
{
[StrategyParameter("PutShortStrikeOffset", 0.03)]
[Description("Offset below current price for short put strike (e.g., 0.03 = 3% below)")]
public decimal PutShortStrikeOffset { get; set; } = 0.03m; // More aggressive - closer to current price
[StrategyParameter("CallShortStrikeOffset", 0.03)]
[Description("Offset above current price for short call strike (e.g., 0.03 = 3% above)")]
public decimal CallShortStrikeOffset { get; set; } = 0.03m; // More aggressive - closer to current price
[StrategyParameter("PutStrikeWidth", 5)]
[Description("Distance between short and long put strikes")]
public decimal PutStrikeWidth { get; set; } = 5m; // Smaller spreads for easier fills
[StrategyParameter("CallStrikeWidth", 5)]
[Description("Distance between short and long call strikes")]
public decimal CallStrikeWidth { get; set; } = 5m; // Smaller spreads for easier fills
[StrategyParameter("MinDaysToExpiration", 3)]
[Description("Minimum days to expiration for options")]
public int MinDaysToExpiration { get; set; } = 3; // Much shorter for more active trading
[StrategyParameter("MaxDaysToExpiration", 21)]
[Description("Maximum days to expiration for options")]
public int MaxDaysToExpiration { get; set; } = 21; // Shorter expiration for more options available
[StrategyParameter("MinimumPremium", 0.25)]
[Description("Minimum premium to collect per contract")]
public decimal MinimumPremium { get; set; } = 0.25m; // Much lower minimum for easier entry
[StrategyParameter("MaxActivePositions", 5)]
[Description("Maximum number of Iron Condor positions")]
public int MaxActivePositions { get; set; } = 5; // Allow more positions
/// <summary>
/// Shorter test period for performance testing (1 month instead of 1 year)
/// </summary>
[StrategyParameter("StartDate", "2023-01-01")]
public new DateTime StartDate { get; set; } = new DateTime(2023, 1, 1);
[StrategyParameter("EndDate", "2023-01-31")]
public new DateTime EndDate { get; set; } = new DateTime(2023, 1, 31);
public override string ToString()
{
return $"IronCondor[{UnderlyingSymbol}] Put:{PutShortStrikeOffset:P1}±{PutStrikeWidth} Call:{CallShortStrikeOffset:P1}±{CallStrikeWidth} DTE:{MinDaysToExpiration}-{MaxDaysToExpiration}";
}
}
}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();
}
}
}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;
}
}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>
/// Multi-Asset Iron Condor strategy configuration with ComboOrder support.
/// Extends MultiAssetConfig with Iron Condor specific parameters and multi-asset adaptations.
/// </summary>
public class MultiAssetIronCondorConfig : MultiAssetConfig
{
/// <summary>
/// Use ComboOrders for atomic execution of Iron Condor legs
/// When true, uses QuantConnect's ComboMarketOrder for simultaneous execution
/// When false, places individual orders for each leg
/// </summary>
[StrategyParameter("UseComboOrders", true)]
public bool UseComboOrders { get; set; } = true;
/// <summary>
/// Minimum wing width for Iron Condor spreads (in strikes)
/// Prevents creating spreads that are too tight
/// </summary>
[StrategyParameter("MinWingWidth", 5)]
public int MinWingWidth { get; set; } = 5;
/// <summary>
/// Maximum wing width for Iron Condor spreads (in strikes)
/// Prevents creating spreads that are too wide
/// </summary>
[StrategyParameter("MaxWingWidth", 20)]
public int MaxWingWidth { get; set; } = 20;
/// <summary>
/// Target delta for short put leg (closest to money)
/// </summary>
[StrategyParameter("ShortPutDelta", -0.15)]
public decimal ShortPutDelta { get; set; } = -0.15m;
/// <summary>
/// Target delta for short call leg (closest to money)
/// </summary>
[StrategyParameter("ShortCallDelta", 0.15)]
public decimal ShortCallDelta { get; set; } = 0.15m;
/// <summary>
/// Enable iron condor specific asset adaptations
/// Adjusts strike selection and wing widths based on asset volatility
/// </summary>
[StrategyParameter("EnableIronCondorAdaptation", true)]
public bool EnableIronCondorAdaptation { get; set; } = true;
/// <summary>
/// Load parameters and apply Iron Condor and multi-asset specific adaptations
/// </summary>
public override void LoadFromParameters(IAlgorithmContext context)
{
// Load base multi-asset parameters first
base.LoadFromParameters(context);
// Apply Iron Condor specific adaptations
if (EnableIronCondorAdaptation && Symbols != null && Symbols.Length > 0)
{
ApplyIronCondorAdaptations(context);
}
// Validate Iron Condor specific constraints
ValidateIronCondorConstraints(context);
}
/// <summary>
/// Apply Iron Condor specific parameter adaptations based on the selected assets
/// </summary>
private void ApplyIronCondorAdaptations(IAlgorithmContext context)
{
((dynamic)context.Logger).Info("MultiAssetIronCondorConfig: Applying Iron Condor asset-specific adaptations");
// Adapt wing widths based on asset characteristics
foreach (var symbol in Symbols)
{
var profile = MultiAssetHelper.GetAssetProfile(symbol);
if (profile != null)
{
// Adjust wing widths based on asset type and volatility
var adaptedWingWidth = GetAdaptedWingWidth(symbol, profile);
((dynamic)context.Logger).Info($"MultiAssetIronCondorConfig: {symbol} adapted wing width: {adaptedWingWidth}");
}
}
// Ensure ComboOrders are used for better multi-asset coordination
if (UseComboOrders)
{
((dynamic)context.Logger).Info("MultiAssetIronCondorConfig: ComboOrders enabled for atomic Iron Condor execution");
}
else
{
((dynamic)context.Logger).Warning("MultiAssetIronCondorConfig: ComboOrders disabled - using individual leg orders");
}
}
/// <summary>
/// Get adapted wing width for a specific asset
/// </summary>
private int GetAdaptedWingWidth(string symbol, dynamic profile)
{
// Base wing width from configuration
var baseWidth = (MinWingWidth + MaxWingWidth) / 2;
// Adjust based on asset characteristics
if (AssetManager.IsIndex(symbol))
{
// Index options typically have tighter spreads, can use narrower wings
return Math.Max(MinWingWidth, baseWidth - 2);
}
else if (AssetManager.IsEquity(symbol))
{
// ETFs generally have good liquidity, use standard wing width
return baseWidth;
}
else
{
// Individual stocks may need wider wings for liquidity
return Math.Min(MaxWingWidth, baseWidth + 3);
}
}
/// <summary>
/// Validate Iron Condor specific constraints
/// </summary>
private void ValidateIronCondorConstraints(IAlgorithmContext context)
{
// Check wing width constraints
if (MinWingWidth <= 0)
{
((dynamic)context.Logger).Error("MultiAssetIronCondorConfig: MinWingWidth must be positive");
}
if (MaxWingWidth <= MinWingWidth)
{
((dynamic)context.Logger).Error("MultiAssetIronCondorConfig: MaxWingWidth must be greater than MinWingWidth");
}
// Check delta targets
if (ShortPutDelta >= 0)
{
((dynamic)context.Logger).Error("MultiAssetIronCondorConfig: ShortPutDelta must be negative");
}
if (ShortCallDelta <= 0)
{
((dynamic)context.Logger).Error("MultiAssetIronCondorConfig: ShortCallDelta must be positive");
}
// Log Iron Condor specific configuration
var summaryMessages = new[]
{
$"MultiAssetIronCondorConfig: Wing width range: {MinWingWidth}-{MaxWingWidth} strikes",
$"MultiAssetIronCondorConfig: Delta targets - Put: {ShortPutDelta:F2}, Call: {ShortCallDelta:F2}",
$"MultiAssetIronCondorConfig: ComboOrders: {(UseComboOrders ? "Enabled" : "Disabled")}",
$"MultiAssetIronCondorConfig: Iron Condor adaptation: {(EnableIronCondorAdaptation ? "Enabled" : "Disabled")}"
};
foreach (var message in summaryMessages)
{
((dynamic)context.Logger).Info(message);
}
}
/// <summary>
/// Get wing width for a specific asset with adaptations
/// </summary>
public int GetWingWidthForAsset(string symbol)
{
var profile = MultiAssetHelper.GetAssetProfile(symbol);
return profile != null ? GetAdaptedWingWidth(symbol, profile) : (MinWingWidth + MaxWingWidth) / 2;
}
/// <summary>
/// Validate the multi-asset Iron Condor configuration
/// </summary>
public override string[] Validate()
{
var errors = base.Validate().ToList();
// Iron Condor specific validations
if (MinWingWidth <= 0)
{
errors.Add("MinWingWidth must be positive");
}
if (MaxWingWidth <= MinWingWidth)
{
errors.Add("MaxWingWidth must be greater than MinWingWidth");
}
if (ShortPutDelta >= 0)
{
errors.Add("ShortPutDelta must be negative for Iron Condor");
}
if (ShortCallDelta <= 0)
{
errors.Add("ShortCallDelta must be positive for Iron Condor");
}
if (Math.Abs(ShortPutDelta) > 0.5m || Math.Abs(ShortCallDelta) > 0.5m)
{
errors.Add("Delta targets should typically be between -0.5 and 0.5 for Iron Condor");
}
// Multi-asset Iron Condor specific checks
if (Symbols != null && Symbols.Length > 0)
{
foreach (var symbol in Symbols)
{
if (!MultiAssetHelper.HasLiquidOptions(symbol))
{
errors.Add($"Iron Condor requires liquid options for {symbol}");
}
}
}
return errors.ToArray();
}
}
}using System;
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);
}
}using System;
using System.Collections.Generic;
using System.ComponentModel;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Configuration for Opening Range Breakout (ORB) strategy
/// Monitors opening range and opens 0DTE credit spreads at 12:00 PM on full trading days
/// </summary>
public class ORBConfig : StrategyConfig
{
/// <summary>
/// Default constructor - sets SPX as default underlying symbol for ORB
/// </summary>
public ORBConfig()
{
UnderlyingSymbol = "SPX";
Symbols = new[] { "SPX" };
StartDate = new DateTime(2019, 1, 1);
EndDate = new DateTime(2025, 7, 30);
}
[StrategyParameter("RangePeriodMinutes", 60)]
[Description("Duration in minutes to establish the opening range (default: 60)")]
public int RangePeriodMinutes { get; set; } = 60;
[StrategyParameter("MinRangeWidthPercent", 0.2)]
[Description("Minimum required range width as percentage of opening price (default: 0.2%)")]
public decimal MinRangeWidthPercent { get; set; } = 0.2m;
[StrategyParameter("SpreadWidth", 15)]
[Description("Width between short and long strikes in dollars (default: $15)")]
public decimal SpreadWidth { get; set; } = 15m;
[StrategyParameter("EntryHour", 12)]
[Description("Hour to evaluate and enter trades (default: 12 = 12:00 PM)")]
public int EntryHour { get; set; } = 12;
[StrategyParameter("EntryMinute", 0)]
[Description("Minute to evaluate and enter trades (default: 0)")]
public int EntryMinute { get; set; } = 0;
[StrategyParameter("MaxPositionsPerDay", 1)]
[Description("Maximum number of positions to open per day (default: 1)")]
public int MaxPositionsPerDay { get; set; } = 1;
[StrategyParameter("ContractSize", 10)]
[Description("Number of contracts per trade (default: 10)")]
public int ContractSize { get; set; } = 10;
[StrategyParameter("MinStrikeOffset", 0.01)]
[Description("Minimum offset from underlying price for short strikes (default: $0.01)")]
public decimal MinStrikeOffset { get; set; } = 0.01m;
[StrategyParameter("UseSmaTwentyFilter", true)]
[Description("Use SMA(20) filter - only enter call spreads when price below SMA (default: true)")]
public bool UseSmaTwentyFilter { get; set; } = true;
[StrategyParameter("CapitalAllocation", 100000)]
[Description("Maximum capital allocation for ORB strategy (default: $100,000)")]
public decimal CapitalAllocation { get; set; } = 100000m;
[StrategyParameter("SkipFomcDays", true)]
[Description("Skip trading on FOMC meeting days (default: true)")]
public bool SkipFomcDays { get; set; } = true;
[StrategyParameter("SlippageAmount", 0.10)]
[Description("Slippage amount for order execution (default: 0.10)")]
public decimal SlippageAmount { get; set; } = 0.10m;
/// <summary>
/// FOMC meeting dates for 2024-2025
/// </summary>
public static readonly HashSet<DateTime> FomcDates2024_2025 = new HashSet<DateTime>
{
// 2024 FOMC dates
new DateTime(2024, 1, 31),
new DateTime(2024, 3, 20),
new DateTime(2024, 5, 1),
new DateTime(2024, 6, 12),
new DateTime(2024, 7, 31),
new DateTime(2024, 9, 18),
new DateTime(2024, 11, 7),
new DateTime(2024, 12, 18),
// 2025 FOMC dates (projected)
new DateTime(2025, 1, 29),
new DateTime(2025, 3, 19),
new DateTime(2025, 4, 30),
new DateTime(2025, 6, 11),
new DateTime(2025, 7, 30),
new DateTime(2025, 9, 17),
new DateTime(2025, 11, 6),
new DateTime(2025, 12, 17)
};
public override string ToString()
{
return $"ORB[{UnderlyingSymbol}] Range:{RangePeriodMinutes}min MinWidth:{MinRangeWidthPercent:P1} SpreadWidth:${SpreadWidth} Entry:{EntryHour}:{EntryMinute:D2} Contracts:{ContractSize} SMA:{UseSmaTwentyFilter} Capital:${CapitalAllocation:N0}";
}
}
}using System;
using System.Collections.Generic;
using QuantConnect;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Configuration for the Restricted Trading strategy demonstrating Entry/Exit Restriction Framework.
/// Shows how to configure trading restrictions for controlled position management.
/// </summary>
public class RestrictedTradingConfig : StrategyConfig
{
// === Underlying Configuration ===
[StrategyParameter("Underlying Symbol", "SPY")]
public new string UnderlyingSymbol { get; set; } = "SPY";
[StrategyParameter("Trade Options", "false")]
public bool TradeOptions { get; set; } = false;
// === Entry Restrictions ===
[StrategyParameter("Trading Start Time", "09:30:00")]
public new TimeSpan TradingStartTime { get; set; } = new TimeSpan(9, 30, 0);
[StrategyParameter("Trading End Time", "15:30:00")]
public new TimeSpan TradingEndTime { get; set; } = new TimeSpan(15, 30, 0);
[StrategyParameter("Max Positions", "3")]
public new int MaxPositions { get; set; } = 3;
[StrategyParameter("Allocation Per Position", "0.30")]
public new decimal AllocationPerPosition { get; set; } = 0.30m; // 30% per position
[StrategyParameter("Min Implied Volatility", "0.15")]
public new decimal MinImpliedVolatility { get; set; } = 0.15m; // 15% minimum IV
// === Exit Restrictions ===
[StrategyParameter("Profit Target", "0.10")]
public new decimal ProfitTarget { get; set; } = 0.10m; // 10% profit target
[StrategyParameter("Stop Loss", "-0.05")]
public new decimal StopLoss { get; set; } = -0.05m; // 5% stop loss
[StrategyParameter("Max Days in Trade", "30")]
public new int MaxDaysInTrade { get; set; } = 30; // Exit after 30 days
// === Options-Specific Parameters ===
[StrategyParameter("Entry Delta Min", "0.25")]
public new decimal EntryDeltaMin { get; set; } = 0.25m;
[StrategyParameter("Entry Delta Max", "0.40")]
public new decimal EntryDeltaMax { get; set; } = 0.40m;
[StrategyParameter("Exit Delta", "0.15")]
public new decimal ExitDelta { get; set; } = 0.15m; // Exit when delta drops below this
/// <summary>
/// Validate configuration parameters.
/// </summary>
public override string[] Validate()
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(UnderlyingSymbol))
errors.Add("UnderlyingSymbol cannot be empty");
if (TradingStartTime >= TradingEndTime)
errors.Add("TradingStartTime must be before TradingEndTime");
if (MaxPositions <= 0 || MaxPositions > 10)
errors.Add("MaxPositions must be between 1 and 10");
if (AllocationPerPosition <= 0 || AllocationPerPosition > 1)
errors.Add("AllocationPerPosition must be between 0 and 1");
if (ProfitTarget <= 0)
errors.Add("ProfitTarget must be positive");
if (StopLoss >= 0)
errors.Add("StopLoss must be negative");
if (MaxDaysInTrade <= 0)
errors.Add("MaxDaysInTrade must be positive");
if (TradeOptions)
{
if (EntryDeltaMin <= 0 || EntryDeltaMin >= 1)
errors.Add("EntryDeltaMin must be between 0 and 1");
if (EntryDeltaMax <= EntryDeltaMin || EntryDeltaMax >= 1)
errors.Add("EntryDeltaMax must be greater than EntryDeltaMin and less than 1");
if (ExitDelta <= 0 || ExitDelta >= EntryDeltaMin)
errors.Add("ExitDelta must be positive and less than EntryDeltaMin");
if (MinImpliedVolatility < 0 || MinImpliedVolatility > 2)
errors.Add("MinImpliedVolatility must be between 0 and 2");
}
return errors.ToArray();
}
/// <summary>
/// Get a friendly description of the configuration.
/// </summary>
public override string ToString()
{
return $"RestrictedTradingConfig: {UnderlyingSymbol}, " +
$"Trading Hours: {TradingStartTime:hh\\:mm}-{TradingEndTime:hh\\:mm}, " +
$"Max Positions: {MaxPositions}, " +
$"Allocation: {AllocationPerPosition:P0}, " +
$"Targets: +{ProfitTarget:P0}/{StopLoss:P0}, " +
$"Max Days: {MaxDaysInTrade}" +
(TradeOptions ? $", Options Delta: {EntryDeltaMin:F2}-{EntryDeltaMax:F2}" : "");
}
}
}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;
// ============================================================================
// 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>
/// Profit target as percentage (e.g., 0.5 = 50% profit target)
/// </summary>
[StrategyParameter("ProfitTarget", 0.5)]
public decimal ProfitTarget { get; set; } = 0.5m;
/// <summary>
/// Stop loss as percentage (e.g., -0.5 = 50% stop loss)
/// </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>
/// 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 = attribute.DefaultValue?.ToString() ?? property.GetValue(this)?.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})");
if (ProfitTarget <= 0)
errors.Add($"ProfitTarget must be positive (current: {ProfitTarget:P1})");
if (StopLoss >= 0)
errors.Add($"StopLoss must be negative (current: {StopLoss:P1})");
// Validate SmartPricing mode
if (!CoreAlgo.Architecture.Core.Execution.SmartPricingEngineFactory.IsValidMode(SmartPricingMode))
errors.Add($"Invalid SmartPricingMode '{SmartPricingMode}' (valid: Normal, Fast, Patient, Off)");
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.ComponentModel;
using CoreAlgo.Architecture.Core.Attributes;
namespace CoreAlgo.Architecture.Core.Models
{
/// <summary>
/// Configuration for Universe Selection strategy that demonstrates enhanced StrategyConfig capabilities.
/// Shows how to use multi-asset support, fundamental filters, and all new parameters.
/// </summary>
public class UniverseSelectionConfig : StrategyConfig
{
// ============================================================================
// UNIVERSE SELECTION PARAMETERS
// ============================================================================
[StrategyParameter("MinMarketCap", 1000000000)]
[Description("Minimum market capitalization for universe selection (default: $1B)")]
public decimal MinMarketCap { get; set; } = 1_000_000_000m;
[StrategyParameter("MinDollarVolume", 10000000)]
[Description("Minimum daily dollar volume for universe selection (default: $10M)")]
public decimal MinDollarVolume { get; set; } = 10_000_000m;
[StrategyParameter("MinPERatio", 5)]
[Description("Minimum P/E ratio for fundamental filter")]
public decimal MinPERatio { get; set; } = 5m;
[StrategyParameter("MaxPERatio", 30)]
[Description("Maximum P/E ratio for fundamental filter")]
public decimal MaxPERatio { get; set; } = 30m;
[StrategyParameter("TradeOptions", false)]
[Description("Enable options trading for selected securities")]
public bool TradeOptions { get; set; } = false;
[StrategyParameter("MinutesBetwenTrades", 60)]
[Description("Minimum minutes between trades for the same symbol")]
public int MinutesBetwenTrades { get; set; } = 60;
// ============================================================================
// CONSTRUCTOR WITH STRATEGY-SPECIFIC DEFAULTS
// ============================================================================
public UniverseSelectionConfig()
{
// Override base class defaults for this strategy
UseUniverseSelection = true; // This strategy is all about universe selection
// Multi-asset capability - manual symbols for when universe selection is disabled
Symbols = new[] { "SPY", "QQQ", "TLT", "GLD", "SPX" }; // Mix of equity and index
// Position sizing optimized for universe selection
AllocationPerPosition = 0.15m; // 15% per position
MaxPositions = 6; // Allow more positions for diversification
// More conservative risk management for dynamic universe
ProfitTarget = 0.20m; // 20% profit target
StopLoss = -0.10m; // 10% stop loss
MaxDaysInTrade = 21; // 3 weeks max
// Options parameters for when TradeOptions is enabled
EntryDeltaMin = 0.20m;
EntryDeltaMax = 0.40m;
ExitDelta = 0.05m;
MinImpliedVolatility = 0.20m; // Higher IV threshold for universe selection
// Trading hours - avoid first and last 30 minutes
TradingStartTime = new TimeSpan(10, 0, 0); // 10:00 AM
TradingEndTime = new TimeSpan(15, 0, 0); // 3:00 PM
}
public override string[] Validate()
{
var errors = new List<string>();
// Add base validation
var baseErrors = base.Validate();
errors.AddRange(baseErrors);
// Strategy-specific validation
if (MinMarketCap <= 0)
errors.Add("MinMarketCap must be greater than 0");
if (MinDollarVolume <= 0)
errors.Add("MinDollarVolume must be greater than 0");
if (MinPERatio <= 0 || MinPERatio >= MaxPERatio)
errors.Add("MinPERatio must be positive and less than MaxPERatio");
if (MaxPERatio <= MinPERatio)
errors.Add("MaxPERatio must be greater than MinPERatio");
if (MinutesBetwenTrades < 0)
errors.Add("MinutesBetwenTrades cannot be negative");
// Validate allocation doesn't exceed 100%
var totalAllocation = AllocationPerPosition * MaxPositions;
if (totalAllocation > 1.0m)
errors.Add($"Total allocation ({totalAllocation:P0}) exceeds 100% (MaxPositions * AllocationPerPosition)");
return errors.ToArray();
}
public override string ToString()
{
return $"UniverseSelection[{(UseUniverseSelection ? "Dynamic" : "Manual")}] " +
$"Cap>=${MinMarketCap/1000000:N0}M Vol>=${MinDollarVolume/1000000:N0}M " +
$"PE:{MinPERatio}-{MaxPERatio} Pos:{MaxPositions}x{AllocationPerPosition:P0} " +
$"Options:{TradeOptions}";
}
}
}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)
{
((dynamic)_context.Logger).Warning($"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
((dynamic)_context.Logger).Debug($"Correlation calculated: {symbol1} vs {symbol2} = {correlation:F3} ({days} days)");
return correlation;
}
catch (Exception ex)
{
((dynamic)_context.Logger).Error($"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)
{
((dynamic)_context.Logger).Warning($"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)
{
((dynamic)_context.Logger).Error($"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 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 HashSet<ScheduledEvent> _scheduledEvents;
private ISmartPricingEngine _pricingEngine;
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>();
_scheduledEvents = new HashSet<ScheduledEvent>();
}
/// <summary>
/// Sets the pricing engine for smart order execution
/// </summary>
public void SetPricingEngine(ISmartPricingEngine pricingEngine)
{
_pricingEngine = pricingEngine;
}
/// <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 = "")
{
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 market order for multi-leg options
/// </summary>
public List<OrderTicket> SmartComboMarketOrder(List<Leg> legs, int quantity, string tag = "")
{
// For now, pass through to regular combo order
// TODO: Implement smart pricing for combos
return _algorithm.ComboMarketOrder(legs, quantity, tag: tag);
}
/// <summary>
/// Handles order events to track fills and update order state
/// </summary>
public void OnOrderEvent(OrderEvent orderEvent)
{
if (!_activeOrders.TryGetValue(orderEvent.OrderId, out var tracker))
return;
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;
}
}
/// <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>
/// 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.Linq;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using CoreAlgo.Architecture.Core.Implementations;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Architecture.Core.Templates
{
/// <summary>
/// Cash Secured Put options strategy template
/// QC-First approach - leverages QuantConnect's native capabilities
/// </summary>
public class CashSecuredPutTemplate : SimpleBaseStrategy
{
private CashSecuredPutConfig _config;
private Symbol _underlying;
private Symbol _optionSymbol;
public override string Name => "Cash Secured Put";
public override string Description =>
"Income strategy that generates premium by selling put options backed by cash";
public override void OnInitialize()
{
SmartLog("CashSecuredPutTemplate.OnInitialize() starting...");
// Configure with CashSecuredPut-specific settings
try
{
Configure<CashSecuredPutConfig>();
_config = (CashSecuredPutConfig)Config;
SmartLog("Configuration loaded successfully");
}
catch (Exception ex)
{
SmartError($"Failed to load configuration: {ex.Message}");
throw;
}
// Configuration already loaded automatically by Configure<CashSecuredPutConfig>()
// Setup underlying and put options using common helper
(_underlying, _optionSymbol) = SetupOptionsForSymbol(_config.UnderlyingSymbol,
strikeRange: 5, _config.MinDaysToExpiration, _config.MaxDaysToExpiration, putsOnly: true);
SmartLog($"Cash Secured Put initialized for {_config.UnderlyingSymbol}");
SmartLog($"Configuration:");
SmartLog($" Put offset: {_config.PutStrikeOffset:P1} below current price");
SmartLog($" DTE range: {_config.MinDaysToExpiration}-{_config.MaxDaysToExpiration} days");
SmartLog($" Min premium: ${_config.MinimumPremium:F2}");
SmartLog($" Max positions: {_config.MaxActivePositions}");
SmartLog($" Accept assignment: {_config.AcceptAssignment}");
SmartLog($"Ready for cash secured put trading!");
}
protected override void OnExecute(Slice slice)
{
Debug($"CashSecuredPut OnExecute called at {slice.Time}");
// Check if we have option data
if (!slice.OptionChains.TryGetValue(_optionSymbol, out var chain))
{
Debug("No option chain data available");
return;
}
// Get current underlying price
var underlyingPrice = Securities[_underlying].Price;
if (underlyingPrice <= 0)
{
Debug("Invalid underlying price");
return;
}
// Check our existing put positions
var putPositions = Portfolio.Securities
.Where(kvp => kvp.Value.Type == SecurityType.Option &&
kvp.Value.Symbol.Underlying == _underlying &&
kvp.Value.Symbol.ID.OptionRight == OptionRight.Put &&
kvp.Value.Holdings.Quantity < 0) // Short puts
.Count();
if (putPositions >= _config.MaxActivePositions)
{
Debug($"Max positions reached: {putPositions}/{_config.MaxActivePositions}");
return;
}
// Find suitable put options to sell
var targetStrike = underlyingPrice * (1 - _config.PutStrikeOffset);
var candidatePuts = chain
.Where(contract => contract.Right == OptionRight.Put)
.Where(contract => contract.Strike <= targetStrike)
.Where(contract => contract.BidPrice >= _config.MinimumPremium)
.OrderByDescending(contract => contract.Strike) // Prefer higher strikes (less OTM)
.ThenBy(contract => contract.Expiry)
.Take(3); // Top 3 candidates
var selectedPut = candidatePuts.FirstOrDefault();
if (selectedPut == null)
{
Debug($"No suitable puts found with premium >= ${_config.MinimumPremium}");
return;
}
// Calculate cash required
var cashRequired = selectedPut.Strike * 100; // 100 shares per contract
var availableCash = Portfolio.Cash;
if (availableCash < cashRequired)
{
Debug($"Insufficient cash: ${availableCash:F2} < ${cashRequired:F2} required");
return;
}
// Sell the put option
var quantity = -1; // Negative for selling
var putOrder = MarketOrder(selectedPut.Symbol, quantity);
SmartLog($"CASH SECURED PUT EXECUTED:");
SmartLog($" Underlying: {_underlying.Value} @ ${underlyingPrice:F2}");
SmartLog($" Sold Put: Strike ${selectedPut.Strike:F2}, Expiry {selectedPut.Expiry:yyyy-MM-dd}");
SmartLog($" Premium: ${selectedPut.BidPrice:F2} x 100 = ${selectedPut.BidPrice * 100:F2}");
SmartLog($" Cash Secured: ${cashRequired:F2}");
SmartLog($" DTE: {(selectedPut.Expiry - Time).TotalDays:F0} days");
SmartLog($" Break-even: ${selectedPut.Strike - selectedPut.BidPrice:F2}");
}
protected void LogDailySummary()
{
// Log daily summary - called from OnExecute when needed
var underlyingPrice = Algorithm.Securities[_underlying].Price;
var putPositions = Algorithm.Portfolio.Securities
.Where(kvp => kvp.Value.Type == SecurityType.Option &&
kvp.Value.Symbol.Underlying == _underlying &&
kvp.Value.Symbol.ID.OptionRight == OptionRight.Put)
.Select(kvp => kvp.Value);
SmartLog($"=== Daily Summary for {_underlying.Value} ===");
SmartLog($"Underlying Price: ${underlyingPrice:F2}");
SmartLog($"Available Cash: ${Algorithm.Portfolio.Cash:N2}");
foreach (var option in putPositions)
{
if (option.Holdings.Quantity < 0) // Short puts
{
var strike = option.Symbol.ID.StrikePrice;
var expiry = option.Symbol.ID.Date;
var dte = (expiry - Algorithm.Time).TotalDays;
var moneyness = underlyingPrice > strike ? "OTM" : "ITM";
SmartLog($"Short Put: {strike} {expiry:yyyy-MM-dd} x{Math.Abs(option.Holdings.Quantity)} ({moneyness}, {dte:F0} DTE)");
}
}
SmartLog($"Portfolio Value: ${Algorithm.Portfolio.TotalPortfolioValue:N2}");
}
}
}using System;
using System.Linq;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using CoreAlgo.Architecture.Core.Implementations;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Architecture.Core.Templates
{
/// <summary>
/// Covered Call options strategy template
/// QC-First approach - leverages QuantConnect's native capabilities
/// </summary>
public class CoveredCallTemplate : SimpleBaseStrategy
{
private CoveredCallConfig _config;
private Symbol _underlying;
private Symbol _optionSymbol;
public override string Name => "Covered Call";
public override string Description =>
"Income strategy that generates premium by selling call options against owned stock";
public override void OnInitialize()
{
SmartLog("CoveredCallTemplate.OnInitialize() starting...");
// Configure with CoveredCall-specific settings
try
{
Configure<CoveredCallConfig>();
_config = (CoveredCallConfig)Config;
SmartLog("Configuration loaded successfully");
}
catch (Exception ex)
{
SmartError($"Failed to load configuration: {ex.Message}");
throw;
}
// Configuration already loaded automatically by Configure<CoveredCallConfig>()
// Setup underlying and call options using common helper
(_underlying, _optionSymbol) = SetupOptionsForSymbol(_config.UnderlyingSymbol,
strikeRange: 5, _config.MinDaysToExpiration, _config.MaxDaysToExpiration, callsOnly: true);
SmartLog($"Covered Call initialized for {_config.UnderlyingSymbol}");
SmartLog($"Configuration:");
SmartLog($" Call offset: {_config.CallStrikeOffset:P1} above current price");
SmartLog($" DTE range: {_config.MinDaysToExpiration}-{_config.MaxDaysToExpiration} days");
SmartLog($" Min premium: ${_config.MinimumPremium:F2}");
SmartLog($" Max positions: {_config.MaxActivePositions}");
SmartLog($" Auto-buy shares: {_config.BuySharesIfNeeded}");
SmartLog($"Ready for covered call trading!");
}
protected override void OnExecute(Slice slice)
{
Debug($"CoveredCall OnExecute called at {slice.Time}");
// Check if we have option data
if (!slice.OptionChains.TryGetValue(_optionSymbol, out var chain))
{
Debug("No option chain data available");
return;
}
// Get current underlying price
var underlyingPrice = Securities[_underlying].Price;
if (underlyingPrice <= 0)
{
Debug("Invalid underlying price");
return;
}
// Check our stock position
var stockHolding = Portfolio[_underlying].Quantity;
var sharesNeeded = _config.SharesPerContract - stockHolding;
// If we need shares and auto-buy is enabled
if (sharesNeeded > 0 && _config.BuySharesIfNeeded)
{
SmartLog($"Need {sharesNeeded} shares for covered call position");
var buyOrder = MarketOrder(_underlying, sharesNeeded);
SmartLog($"Bought {sharesNeeded} shares of {_underlying.Value} at market");
return; // Wait for next bar to sell calls
}
// Check if we have enough shares to sell a covered call
if (stockHolding < _config.SharesPerContract)
{
Debug($"Insufficient shares: {stockHolding} < {_config.SharesPerContract}");
return;
}
// Check if we already have call positions
var callPositions = Portfolio.Securities
.Where(kvp => kvp.Value.Type == SecurityType.Option &&
kvp.Value.Symbol.Underlying == _underlying &&
kvp.Value.Holdings.Quantity < 0) // Short calls
.Count();
if (callPositions >= _config.MaxActivePositions)
{
Debug($"Max positions reached: {callPositions}/{_config.MaxActivePositions}");
return;
}
// Find suitable call options to sell
var targetStrike = underlyingPrice * (1 + _config.CallStrikeOffset);
var candidateCalls = chain
.Where(contract => contract.Right == OptionRight.Call)
.Where(contract => contract.Strike >= targetStrike)
.Where(contract => contract.BidPrice >= _config.MinimumPremium)
.OrderBy(contract => Math.Abs(contract.Strike - targetStrike))
.ThenBy(contract => contract.Expiry)
.Take(3); // Top 3 candidates
var selectedCall = candidateCalls.FirstOrDefault();
if (selectedCall == null)
{
Debug($"No suitable calls found with premium >= ${_config.MinimumPremium}");
return;
}
// Sell the call option
var quantity = -1; // Negative for selling
var callOrder = MarketOrder(selectedCall.Symbol, quantity);
SmartLog($"COVERED CALL EXECUTED:");
SmartLog($" Underlying: {_underlying.Value} @ ${underlyingPrice:F2}");
SmartLog($" Sold Call: Strike ${selectedCall.Strike:F2}, Expiry {selectedCall.Expiry:yyyy-MM-dd}");
SmartLog($" Premium: ${selectedCall.BidPrice:F2} x 100 = ${selectedCall.BidPrice * 100:F2}");
SmartLog($" DTE: {(selectedCall.Expiry - Time).TotalDays:F0} days");
}
protected void LogDailySummary()
{
// Log daily summary - called from OnExecute when needed
var stockPosition = Algorithm.Portfolio[_underlying];
var optionPositions = Algorithm.Portfolio.Securities
.Where(kvp => kvp.Value.Type == SecurityType.Option &&
kvp.Value.Symbol.Underlying == _underlying)
.Select(kvp => kvp.Value);
SmartLog($"=== Daily Summary for {_underlying.Value} ===");
SmartLog($"Stock Position: {stockPosition.Quantity} shares @ ${stockPosition.AveragePrice:F2}");
foreach (var option in optionPositions)
{
if (option.Holdings.Quantity < 0) // Short calls
{
SmartLog($"Short Call: {option.Symbol.ID.StrikePrice} {option.Symbol.ID.Date:yyyy-MM-dd} x{Math.Abs(option.Holdings.Quantity)}");
}
}
SmartLog($"Portfolio Value: ${Algorithm.Portfolio.TotalPortfolioValue:N2}");
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using CoreAlgo.Architecture.Core.Implementations;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Architecture.Core.Templates
{
/// <summary>
/// Iron Condor options strategy template
/// QC-First approach - leverages QuantConnect's native capabilities
/// </summary>
public class IronCondorTemplate : SimpleBaseStrategy
{
private IronCondorConfig _config;
private Symbol _underlying;
private Symbol _optionSymbol;
public override string Name => "Iron Condor";
public override string Description =>
"Neutral options strategy that profits from low volatility by selling both a call spread and put spread";
public override void OnInitialize()
{
SmartLog("IronCondorTemplate.OnInitialize() starting...");
// Configure with IronCondor-specific settings
try
{
Configure<IronCondorConfig>();
_config = (IronCondorConfig)Config;
SmartLog("Configuration loaded successfully");
}
catch (Exception ex)
{
SmartError($"Failed to load configuration: {ex.Message}");
throw;
}
// Configuration already loaded automatically by Configure<IronCondorConfig>()
// Don't apply algorithm settings from config - Main.cs handles this
// SetStartDate(_config.StartDate);
// SetEndDate(_config.EndDate);
// SetCash(_config.StartingCash);
// Add underlying and options using AssetManager
SmartLog($"Setting up {_config.UnderlyingSymbol} for Iron Condor trading");
SmartLog($" Asset type - Index: {CoreAlgo.Architecture.QC.Helpers.AssetManager.IsIndex(_config.UnderlyingSymbol)}, " +
$"Future: {CoreAlgo.Architecture.QC.Helpers.AssetManager.IsFuture(_config.UnderlyingSymbol)}, " +
$"Equity: {CoreAlgo.Architecture.QC.Helpers.AssetManager.IsEquity(_config.UnderlyingSymbol)}");
// Setup underlying and options with VERY WIDE strike range for testing (Phase 5)
SmartLog($"[PHASE5] Using VERY WIDE option filters to rule out restrictive filtering");
SmartLog($" Original DTE range: {_config.MinDaysToExpiration}-{_config.MaxDaysToExpiration}");
SmartLog($" Test DTE range: 1-60 (very wide)");
SmartLog($" Original strike range: ±10");
SmartLog($" Test strike range: ±50 (very wide)");
(_underlying, _optionSymbol) = SetupOptionsForSymbol(_config.UnderlyingSymbol,
strikeRange: 10, _config.MinDaysToExpiration, _config.MaxDaysToExpiration, resolution: Resolution.Minute);
SmartLog($" Successfully added {_underlying} with options chain {_optionSymbol}");
SmartLog($"Iron Condor initialized for {_config.UnderlyingSymbol}");
SmartLog($"Configuration:");
SmartLog($" Put offset: {_config.PutShortStrikeOffset:P1} below, Call offset: {_config.CallShortStrikeOffset:P1} above");
SmartLog($" Strike widths: Put ${_config.PutStrikeWidth}, Call ${_config.CallStrikeWidth}");
SmartLog($" DTE range: {_config.MinDaysToExpiration}-{_config.MaxDaysToExpiration} days");
SmartLog($" Min premium: ${_config.MinimumPremium:F2}");
SmartLog($" Max positions: {_config.MaxActivePositions}");
SmartLog($"Ready for aggressive Iron Condor trading!");
}
protected override void OnExecute(Slice slice)
{
// [DEBUG] COMPREHENSIVE SYMBOL ACCESS DEBUGGING
SmartLog($"[START] IronCondor OnExecute ENTRY at {slice.Time}");
SmartLog($"[DATA] Initial Slice Analysis:");
SmartLog($" slice.OptionChains.Count: {slice.OptionChains.Count}");
SmartLog($" slice.Bars.Count: {slice.Bars.Count}");
SmartLog($" slice.QuoteBars.Count: {slice.QuoteBars.Count}");
SmartLog($" slice.Ticks.Count: {slice.Ticks.Count}");
SmartLog($" slice.Keys.Count: {slice.Keys.Count}");
// [DEBUG] DETAILED SYMBOL DEBUGGING
SmartLog($"[TARGET] Symbol Access Analysis:");
SmartLog($" _underlying symbol: {_underlying}");
SmartLog($" _optionSymbol we're looking for: {_optionSymbol}");
SmartLog($" _optionSymbol.Value: {_optionSymbol.Value}");
SmartLog($" _optionSymbol.SecurityType: {_optionSymbol.SecurityType}");
// [DEBUG] LOG ALL AVAILABLE SLICE KEYS
if (slice.Keys.Any())
{
SmartLog($"[LIST] ALL Available Slice Keys ({slice.Keys.Count} total):");
foreach (var key in slice.Keys.Take(20)) // Limit to first 20 to avoid spam
{
SmartLog($" Key: {key} (Type: {key.SecurityType})");
}
if (slice.Keys.Count > 20)
{
SmartLog($" ... and {slice.Keys.Count - 20} more keys");
}
}
else
{
SmartLog($"[ERROR] NO Keys found in slice at all!");
}
// [DEBUG] LOG ALL AVAILABLE OPTION CHAIN KEYS
if (slice.OptionChains.Keys.Any())
{
SmartLog($"[LIGHTNING] Available OptionChains Keys ({slice.OptionChains.Keys.Count} total):");
foreach (var optKey in slice.OptionChains.Keys)
{
SmartLog($" OptionChain Key: {optKey} (Type: {optKey.SecurityType}, Value: {optKey.Value})");
}
}
else
{
SmartLog($"[ERROR] NO OptionChains keys found in slice!");
}
// [DEBUG] COMPREHENSIVE DATA INSPECTION (PHASE 1)
SmartLog($"=== COMPREHENSIVE DATA SLICE INSPECTION at {slice.Time} ===");
SmartLog($"Algorithm time: {Time}");
SmartLog($"Slice time: {slice.Time}");
SmartLog($"Day of week: {slice.Time.DayOfWeek}");
SmartLog($"Market open: {Securities[_underlying].Exchange.DateTimeIsOpen(slice.Time)}");
// Securities collection inspection
SmartLog($"=== SECURITIES COLLECTION INSPECTION ===");
SmartLog($"Securities collection has {Securities.Keys.Count} securities");
foreach (var sec in Securities.Keys.Take(10))
{
var security = Securities[sec];
SmartLog($" Security: {sec} Type: {security.Type} Resolution: {security.Subscriptions.GetHighestResolution()} HasData: {security.HasData}");
}
// Available symbols in slice
SmartLog($"=== SLICE SYMBOLS INSPECTION ===");
SmartLog($"Available symbols in slice: {string.Join(", ", slice.Keys.Take(15))}");
if (slice.Keys.Count > 15)
{
SmartLog($"... and {slice.Keys.Count - 15} more symbols");
}
// [DEBUG] TEST DIFFERENT SYMBOL ACCESS PATTERNS (PHASE 2)
SmartLog($"=== SYMBOL ACCESS PATTERN TESTING ===");
// Test 1: Our current approach
var foundWithOptionSymbol = slice.OptionChains.TryGetValue(_optionSymbol, out var chain1);
SmartLog($" Test 1 - _optionSymbol ({_optionSymbol}): {foundWithOptionSymbol}");
if (foundWithOptionSymbol) SmartLog($" Chain1 has {chain1.Count} contracts");
// Test 2: Try underlying symbol
var foundWithUnderlying = slice.OptionChains.TryGetValue(_underlying, out var chain2);
SmartLog($" Test 2 - _underlying ({_underlying}): {foundWithUnderlying}");
if (foundWithUnderlying) SmartLog($" Chain2 has {chain2.Count} contracts");
// Test 3: Try canonical symbol
try
{
var canonical = Symbol.CreateCanonicalOption(_underlying);
var foundWithCanonical = slice.OptionChains.TryGetValue(canonical, out var chain3);
SmartLog($" Test 3 - Canonical ({canonical}): {foundWithCanonical}");
if (foundWithCanonical) SmartLog($" Chain3 has {chain3.Count} contracts");
}
catch (Exception ex)
{
SmartLog($" Test 3 - Canonical creation failed: {ex.Message}");
}
// Test 4: Try exact string match with available keys
Symbol matchingKey = null;
foreach (var key in slice.OptionChains.Keys)
{
if (key.Value == _optionSymbol.Value || key.Value == _underlying.Value)
{
matchingKey = key;
break;
}
}
if (matchingKey != null)
{
var foundWithMatching = slice.OptionChains.TryGetValue(matchingKey, out var chain4);
SmartLog($" Test 4 - Matching key ({matchingKey}): {foundWithMatching}");
if (foundWithMatching) SmartLog($" Chain4 has {chain4.Count} contracts");
}
else
{
SmartLog($" Test 4 - No matching keys found");
}
// Test 5: Try all available option chain keys
if (slice.OptionChains.Any())
{
SmartLog($" Test 5 - Trying all available option chain keys:");
foreach (var kvp in slice.OptionChains.Take(3))
{
SmartLog($" Available key: {kvp.Key} has {kvp.Value.Count} contracts");
if (kvp.Value.Any())
{
var sample = kvp.Value.First();
SmartLog($" Sample contract: {sample.Strike} {sample.Right} {sample.Expiry:MM/dd/yyyy}");
}
}
}
// DEBUG: Log execution entry (keep existing for compatibility)
Debug($"IronCondor OnExecute called at {slice.Time} with {slice.OptionChains.Count} option chains");
// More aggressive Iron Condor logic to ensure trades happen
if (slice.OptionChains.Count == 0)
{
SmartLog($"[ERROR] EARLY EXIT: No option chains available at {slice.Time}");
SmartLog($" This means our symbol access pattern is not finding any option data");
SmartLog($" Either: 1) Wrong symbol used, 2) No data available, 3) Filter too restrictive");
Debug("No option chains available, returning");
return;
}
SmartLog($"[SUCCESS] PROCEEDING: Found {slice.OptionChains.Count} option chains");
// Check if we've reached position limit
var activePositions = Portfolio.Count(x => x.Value.Invested);
if (activePositions >= _config.MaxActivePositions)
return;
// QC-First: Prevent overlapping Iron Condor positions using native Portfolio
var activeIronCondors = Portfolio.Values
.Where(h => h.Invested && h.Symbol.SecurityType == SecurityType.Option)
.GroupBy(h => new { h.Symbol.Underlying, h.Symbol.ID.Date })
.Where(g => g.Count() == 4) // Iron Condor = 4 legs
.Count();
if (activeIronCondors >= 1) // Limit to 1 active Iron Condor to prevent margin crashes
{
SmartLog("[POSITION LIMIT] Blocking new Iron Condor - existing position detected");
SmartLog($" Active Iron Condors: {activeIronCondors}");
SmartLog($" This prevents QuantConnect margin model overlapping position crashes");
return;
}
// Trade on any weekday to maximize trading opportunities
if (slice.Time.DayOfWeek == DayOfWeek.Saturday || slice.Time.DayOfWeek == DayOfWeek.Sunday)
return;
// Get option chain for underlying
if (slice.OptionChains.TryGetValue(_optionSymbol, out var chain))
{
var underlyingPrice = chain.Underlying.Price;
SmartLog($"[DEBUG] PRE-FILTER OPTION CHAIN ANALYSIS for {_optionSymbol.Value}");
SmartLog($"[DATA] RAW Option Universe: {chain.Count} total contracts at underlying price ${underlyingPrice:F2}");
// Show complete raw option universe BEFORE any filtering
SmartLog($"[CALENDAR] ALL Available Expiration Dates ({chain.Select(x => x.Expiry.Date).Distinct().Count()} unique):");
var allExpirations = chain.Select(x => x.Expiry.Date).Distinct().OrderBy(x => x).ToList();
foreach (var exp in allExpirations)
{
var contractsForExp = chain.Where(x => x.Expiry.Date == exp).Count();
var daysToExp = (exp - slice.Time.Date).Days;
SmartLog($" {exp:yyyy-MM-dd} ({daysToExp} DTE): {contractsForExp} contracts");
}
SmartLog($"[LIGHTNING] ALL Available Strikes ({chain.Select(x => x.Strike).Distinct().Count()} unique):");
var allStrikes = chain.Select(x => x.Strike).Distinct().OrderBy(x => x).ToList();
SmartLog($" Range: ${allStrikes.FirstOrDefault():F2} to ${allStrikes.LastOrDefault():F2}");
SmartLog($" Strikes around current price (+/-10): {string.Join(", ", allStrikes.Where(s => Math.Abs(s - underlyingPrice) <= 10).Select(s => $"${s:F0}"))}");
SmartLog($"[TARGET] Target Strike Analysis:");
SmartLog($" Current Price: ${underlyingPrice:F2}");
SmartLog($" Target Put Strike: ${underlyingPrice * (1 - _config.PutShortStrikeOffset):F2} ({_config.PutShortStrikeOffset:P1} below)");
SmartLog($" Target Call Strike: ${underlyingPrice * (1 + _config.CallShortStrikeOffset):F2} ({_config.CallShortStrikeOffset:P1} above)");
// Show sample of raw option data with bid/ask prices
SmartLog($"[LIST] Sample Raw Options (first 10 contracts):");
var sampleOptions = chain.Take(10).ToList();
foreach (var opt in sampleOptions)
{
var dte = (opt.Expiry.Date - slice.Time.Date).Days;
SmartLog($" {opt.Symbol.ID.StrikePrice:F0} {opt.Right} {opt.Expiry:MM/dd} ({dte}DTE) - Bid:${opt.BidPrice:F2} Ask:${opt.AskPrice:F2} Vol:{opt.Volume}");
}
// Show options by type before filtering
var rawPuts = chain.Where(x => x.Right == OptionRight.Put).ToList();
var rawCalls = chain.Where(x => x.Right == OptionRight.Call).ToList();
SmartLog($"[DATA] Raw Option Types: {rawPuts.Count} Puts, {rawCalls.Count} Calls");
Debug($"Processing {chain.Count} options contracts at underlying price ${underlyingPrice:F2}");
// DEBUG: Show available expirations and strikes (keep existing debug for compatibility)
var expirations = chain.Select(x => x.Expiry.Date).Distinct().OrderBy(x => x).ToList();
Debug($"Available expiration dates: {string.Join(", ", expirations.Select(x => x.ToString("yyyy-MM-dd")))}");
var strikes = chain.Select(x => x.Strike).Distinct().OrderBy(x => x).ToList();
Debug($"Available strikes: Min=${strikes.FirstOrDefault():F2}, Max=${strikes.LastOrDefault():F2}, Count={strikes.Count}");
Debug($"Strike range needed: Put=${underlyingPrice * (1 - _config.PutShortStrikeOffset):F2}, Call=${underlyingPrice * (1 + _config.CallShortStrikeOffset):F2}");
// STEP-BY-STEP FILTERING WITH DETAILED DIAGNOSTICS
SmartLog($"[DEBUG] FILTERING PROCESS - Step by Step Analysis:");
// Step 1: DTE Filter
var dteFiltered = chain.Where(x =>
(x.Expiry.Date - slice.Time.Date).Days >= _config.MinDaysToExpiration &&
(x.Expiry.Date - slice.Time.Date).Days <= _config.MaxDaysToExpiration).ToList();
SmartLog($"[CALENDAR] Step 1 - DTE Filter ({_config.MinDaysToExpiration}-{_config.MaxDaysToExpiration} days): {chain.Count} -> {dteFiltered.Count} options ({chain.Count - dteFiltered.Count} removed)");
if (dteFiltered.Count == 0)
{
SmartLog($"[ERROR] No options match DTE criteria - showing why:");
var expirationsOutOfRange = chain.GroupBy(x => x.Expiry.Date)
.Select(g => new { Date = g.Key, Count = g.Count(), DTE = (g.Key - slice.Time.Date).Days })
.OrderBy(x => x.DTE).Take(5).ToList();
foreach (var exp in expirationsOutOfRange)
{
var status = exp.DTE < _config.MinDaysToExpiration ? "too soon" : "too far";
SmartLog($" {exp.Date:yyyy-MM-dd} ({exp.DTE} DTE): {exp.Count} contracts - {status}");
}
}
// Step 2: Liquidity Filter (Bid/Ask > 0)
var liquidOptions = dteFiltered.Where(x => x.BidPrice > 0 && x.AskPrice > 0).ToList();
SmartLog($"[MONEY] Step 2 - Liquidity Filter (bid/ask > 0): {dteFiltered.Count} -> {liquidOptions.Count} options ({dteFiltered.Count - liquidOptions.Count} removed)");
if (liquidOptions.Count < dteFiltered.Count)
{
var illiquidCount = dteFiltered.Count - liquidOptions.Count;
var illiquidSample = dteFiltered.Where(x => x.BidPrice <= 0 || x.AskPrice <= 0).Take(3).ToList();
SmartLog($"[WARNING] {illiquidCount} options removed for liquidity issues. Sample:");
foreach (var opt in illiquidSample)
{
SmartLog($" {opt.Strike:F0} {opt.Right} {opt.Expiry:MM/dd} - Bid:${opt.BidPrice:F2} Ask:${opt.AskPrice:F2}");
}
}
// Final result
var allOptions = liquidOptions;
SmartLog($"[SUCCESS] FINAL FILTERED RESULT: {allOptions.Count} options available for Iron Condor");
// Show data quality metrics
if (allOptions.Count > 0)
{
var avgBidAskSpread = allOptions.Average(x => x.AskPrice - x.BidPrice);
var avgVolume = allOptions.Average(x => x.Volume);
SmartLog($"[DATA] Data Quality Metrics:");
SmartLog($" Average Bid-Ask Spread: ${avgBidAskSpread:F3}");
SmartLog($" Average Volume: {avgVolume:F0}");
SmartLog($" Price Range: ${allOptions.Min(x => x.BidPrice):F2} - ${allOptions.Max(x => x.AskPrice):F2}");
}
// DEBUG: Show filtering results (keep existing debug for compatibility)
Debug($"After DTE filter ({_config.MinDaysToExpiration}-{_config.MaxDaysToExpiration} days): {dteFiltered.Count} options");
Debug($"After liquidity filter (bid/ask > 0): {liquidOptions.Count} options");
if (allOptions.Count < 4)
{
Debug($"Not enough liquid options found: {allOptions.Count}, need at least 4");
// DEBUG: Show why options are being filtered out
var sample = chain.Take(5).Select(x =>
$" {x.Symbol.ID.StrikePrice:F0} {x.Right} exp:{x.Expiry:MM/dd} DTE:{(x.Expiry.Date - slice.Time.Date).Days} Bid:{x.BidPrice:F2} Ask:{x.AskPrice:F2}");
Debug($"Sample options:\n{string.Join("\n", sample)}");
return;
}
// Separate puts and calls
var puts = allOptions.Where(x => x.Right == OptionRight.Put).OrderByDescending(x => x.Strike).ToList();
var calls = allOptions.Where(x => x.Right == OptionRight.Call).OrderBy(x => x.Strike).ToList();
Debug($"Found {puts.Count} puts and {calls.Count} calls with good liquidity");
if (puts.Count < 2 || calls.Count < 2)
{
Debug("Need at least 2 puts and 2 calls for Iron Condor");
return;
}
// More flexible strike selection - find any reasonable strikes
var shortPutStrike = underlyingPrice * (1 - _config.PutShortStrikeOffset);
var shortCallStrike = underlyingPrice * (1 + _config.CallShortStrikeOffset);
// Find closest available strikes to our targets
var shortPut = puts.Where(p => p.Strike <= shortPutStrike).OrderByDescending(p => p.Strike).FirstOrDefault();
var shortCall = calls.Where(c => c.Strike >= shortCallStrike).OrderBy(c => c.Strike).FirstOrDefault();
// If we can't find exact strikes, try any reasonable ones
if (shortPut == null)
shortPut = puts.Where(p => p.Strike < underlyingPrice).OrderByDescending(p => p.Strike).FirstOrDefault();
if (shortCall == null)
shortCall = calls.Where(c => c.Strike > underlyingPrice).OrderBy(c => c.Strike).FirstOrDefault();
if (shortPut == null || shortCall == null)
{
Debug("Could not find suitable short strikes");
return;
}
// Find long strikes (wider spreads for better liquidity)
var longPut = puts.Where(p => p.Strike <= shortPut.Strike - _config.PutStrikeWidth).OrderByDescending(p => p.Strike).FirstOrDefault();
var longCall = calls.Where(c => c.Strike >= shortCall.Strike + _config.CallStrikeWidth).OrderBy(c => c.Strike).FirstOrDefault();
if (longPut == null || longCall == null)
{
Debug("Could not find suitable long strikes for protection");
return;
}
// Calculate total premium (be more lenient)
var totalPremium = shortPut.BidPrice + shortCall.BidPrice - longPut.AskPrice - longCall.AskPrice;
SmartLog($"Iron Condor Analysis:");
SmartLog($" Short Put: {shortPut.Strike} @ ${shortPut.BidPrice:F2}");
SmartLog($" Long Put: {longPut.Strike} @ ${longPut.AskPrice:F2}");
SmartLog($" Short Call: {shortCall.Strike} @ ${shortCall.BidPrice:F2}");
SmartLog($" Long Call: {longCall.Strike} @ ${longCall.AskPrice:F2}");
SmartLog($" Total Premium: ${totalPremium:F2} (Min required: ${_config.MinimumPremium:F2})");
if (totalPremium >= _config.MinimumPremium)
{
// QC-First: Execute Iron Condor using ComboMarketOrder (fixes margin calculation crashes)
SmartLog($"[COMBO ORDER] Executing Iron Condor as atomic multi-leg order");
SmartLog($" This prevents QuantConnect margin model 'Sequence contains no matching element' errors");
var legs = new List<Leg>
{
Leg.Create(shortPut.Symbol, -1), // Short Put
Leg.Create(longPut.Symbol, 1), // Long Put
Leg.Create(shortCall.Symbol, -1), // Short Call
Leg.Create(longCall.Symbol, 1) // Long Call
};
var tickets = ComboMarketOrder(legs, 1, tag: "Iron Condor");
SmartLog($"IRON CONDOR OPENED! Premium: ${totalPremium:F2}");
SmartLog($" Put Spread: {longPut.Strike:F0}/{shortPut.Strike:F0}");
SmartLog($" Call Spread: {shortCall.Strike:F0}/{longCall.Strike:F0}");
SmartLog($" Expiration: {shortPut.Expiry:MM/dd/yyyy}");
// Log combo order results
if (tickets != null && tickets.Any())
{
SmartLog($"[SUCCESS] Combo Order Status: {tickets.Count} tickets created");
foreach (var ticket in tickets.Take(4))
{
SmartLog($" Ticket {ticket.OrderId}: {ticket.Symbol.Value} x{ticket.Quantity} - {ticket.Status}");
}
}
else
{
SmartLog($"[ERROR] Combo order failed - no tickets returned");
}
}
else
{
Debug($"Premium too low: ${totalPremium:F2} < ${_config.MinimumPremium:F2}");
}
}
}
protected override void OnGetPerformanceMetrics(System.Collections.Generic.Dictionary<string, double> metrics)
{
// Add Iron Condor specific metrics
metrics["ActiveIronCondors"] = Portfolio.Count(x => x.Value.Invested && x.Key.SecurityType == SecurityType.Option) / 4.0; // 4 legs per IC
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using QuantConnect.Data.Market;
using CoreAlgo.Architecture.Core.Implementations;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.Core.Services;
using CoreAlgo.Architecture.QC.Helpers;
namespace CoreAlgo.Architecture.Core.Templates
{
/// <summary>
/// Multi-Asset Iron Condor strategy template demonstrating asset-specific parameter adaptation.
/// Based on proven IronCondorTemplate pattern but with multi-asset configuration support.
/// </summary>
public class MultiAssetIronCondorTemplate : SimpleBaseStrategy
{
private MultiAssetIronCondorConfig _config;
private Dictionary<string, Symbol> _underlyings = new Dictionary<string, Symbol>();
private Dictionary<string, Symbol> _optionSymbols = new Dictionary<string, Symbol>();
private Dictionary<string, DateTime> _lastTradeTime = new Dictionary<string, DateTime>();
private CoreAlgoRiskManager _riskManager;
// Strike range caching for performance optimization
private Dictionary<string, (StrikeRangeCalculator.StrikeRange strikes, DateTime calculated, decimal underlyingPrice)> _strikeCache =
new Dictionary<string, (StrikeRangeCalculator.StrikeRange, DateTime, decimal)>();
private readonly TimeSpan _cacheTimeout = TimeSpan.FromHours(1); // Cache strikes for 1 hour
private readonly decimal _priceChangeThreshold = 0.02m; // Invalidate cache if price moves >2%
/// <inheritdoc/>
public override string Name => "Multi-Asset Iron Condor";
/// <inheritdoc/>
public override string Description => "Iron Condor strategy with asset-specific parameter adaptation for SPX, QQQ, AAPL, etc.";
/// <summary>
/// Initialize the multi-asset Iron Condor strategy
/// </summary>
public override void OnInitialize()
{
SmartLog("MultiAssetIronCondor.OnInitialize() starting...");
// Determine configuration type based on strategy parameter
var strategyType = Algorithm.GetParameter("Strategy", "MULTIASSET").ToUpper();
// Configure the strategy with appropriate multi-asset configuration
try
{
// CRITICAL: Create config but don't load parameters yet
if (strategyType.Contains("SPX") || strategyType.Contains("INDEX"))
{
_config = new MultiAssetIronCondorConfig();
SmartLog("MultiAsset: Created SPX/Index configuration");
}
else if (strategyType.Contains("EQUITY") || strategyType.Contains("STOCK"))
{
_config = new MultiAssetIronCondorConfig();
SmartLog("MultiAsset: Created Equity/Stock configuration");
}
else
{
_config = new MultiAssetIronCondorConfig();
SmartLog("MultiAsset: Created default configuration");
}
// Fix symbols BEFORE any parameter loading
FixSymbolsFromParameters();
// NOW load parameters with correct symbols
_config.LoadFromParameters(this);
SmartLog("MultiAsset configuration loaded with corrected symbols");
}
catch (Exception ex)
{
SmartError($"Failed to load MultiAsset configuration: {ex.Message}");
throw;
}
// Initialize risk manager (optional - can be disabled via config)
try
{
_riskManager = new CoreAlgoRiskManager(this);
SmartLog("MultiAsset: Risk manager initialized successfully");
}
catch (Exception ex)
{
SmartError($"Warning: Could not initialize risk manager: {ex.Message}");
// Continue without risk manager - template remains functional
_riskManager = null;
}
// Add ALL underlying assets from the symbols array
try
{
SmartLog($"MultiAsset: Adding {_config.Symbols.Length} assets: {string.Join(", ", _config.Symbols)}");
foreach (var symbol in _config.Symbols)
{
try
{
var security = AssetManager.AddAsset(this, symbol, Resolution.Minute);
_underlyings[symbol] = security.Symbol;
// Add options chain for this asset
var optionSymbol = AssetManager.AddOptionsChain(this, security, Resolution.Minute);
_optionSymbols[symbol] = optionSymbol;
SmartLog($"MultiAsset: Added {symbol} (Type: {security.Type}) with options");
// Set up option chain filter for better performance
// Optimized: Reduced from (-10, 10, 20-60 days) to (-5, 5, 30-45 days)
// This significantly reduces data volume while maintaining strategy effectiveness
var option = Algorithm.Securities[optionSymbol] as Option;
if (option != null)
{
option.SetFilter(-5, 5, TimeSpan.FromDays(30), TimeSpan.FromDays(45));
SmartLog($"MultiAsset: Optimized option chain filter configured for {symbol} (strikes: ±5, DTE: 30-45)");
}
// Log asset-specific parameters
var strikeWidth = _config.GetStrikeWidthForAsset(symbol);
var (deltaMin, deltaMax) = _config.GetDeltaTargetsForAsset(symbol);
var (minPos, maxPos, allocation) = _config.GetPositionLimitsForAsset(symbol);
SmartLog($"MultiAsset: {symbol} parameters - " +
$"StrikeWidth: {strikeWidth:P1}, " +
$"Delta: {deltaMin:F2}-{deltaMax:F2}, " +
$"MaxPos: {maxPos}, Allocation: {allocation:P1}");
}
catch (Exception ex)
{
SmartError($"Failed to add asset {symbol}: {ex.Message}");
// Continue with other symbols instead of failing completely
}
}
SmartLog($"MultiAsset: Successfully added {_underlyings.Count} out of {_config.Symbols.Length} requested assets");
}
catch (Exception ex)
{
SmartError($"Failed to add multi-asset symbols: {ex.Message}");
throw;
}
SmartLog("MultiAssetIronCondor initialization completed successfully");
}
/// <summary>
/// Handle data updates - main execution logic for multi-asset trading
/// </summary>
protected override void OnExecute(Slice data)
{
// Debug logging for troubleshooting
SmartLog($"OnExecute called at {Algorithm.Time} with {data.OptionChains.Count} option chains");
// Process each asset independently
foreach (var kvp in _optionSymbols)
{
var assetName = kvp.Key;
var optionSymbol = kvp.Value;
var underlyingSymbol = _underlyings[assetName];
try
{
// Only process if we have options data for this asset
if (!data.OptionChains.ContainsKey(optionSymbol))
{
SmartLog($"No option chain data for {assetName} ({optionSymbol})");
continue;
}
var chain = data.OptionChains[optionSymbol];
if (chain.Count == 0)
{
SmartLog($"Option chain for {assetName} is empty");
continue;
}
// Get current underlying price
var underlyingPrice = Algorithm.Securities[underlyingSymbol].Price;
if (underlyingPrice <= 0)
{
SmartLog($"Invalid underlying price for {assetName}: {underlyingPrice}");
continue;
}
// Price tracking handled by QC's portfolio system
SmartLog($"{assetName} price: ${underlyingPrice:F2}, option chain: {chain.Count} contracts");
// Check if we should enter new positions for this asset
if (ShouldEnterNewPosition(assetName))
{
TryEnterIronCondor(assetName, chain, underlyingPrice);
}
}
catch (Exception ex)
{
SmartError($"Error processing {assetName}: {ex.Message}");
}
}
// Check existing positions for exit conditions (all assets)
CheckExitConditions();
}
/// <summary>
/// Check if we should enter a new Iron Condor position for a specific asset
/// </summary>
private bool ShouldEnterNewPosition(string assetName)
{
// Count positions for this specific asset
var assetPositions = Algorithm.Portfolio.Values
.Where(x => x.Invested &&
x.Symbol.SecurityType == SecurityType.Option &&
x.Symbol.Underlying.Value == assetName)
.Count();
var totalPositions = Algorithm.Portfolio.Values.Where(x => x.Invested && x.Symbol.SecurityType == SecurityType.Option).Count();
var currentTime = Algorithm.Time.TimeOfDay;
// Get asset-specific limits
var (minPos, maxPos, allocation) = _config.GetPositionLimitsForAsset(assetName);
// Check minimum time between trades (prevent excessive trading)
// Increased from 24h to 48h to reduce trade frequency and improve performance
var minTimeBetweenTrades = TimeSpan.FromHours(48); // Only trade every 2 days per asset
if (_lastTradeTime.ContainsKey(assetName))
{
var timeSinceLastTrade = Algorithm.Time - _lastTradeTime[assetName];
if (timeSinceLastTrade < minTimeBetweenTrades)
{
SmartLog($"{assetName}: Too soon since last trade ({timeSinceLastTrade.TotalHours:F1}h ago, min: {minTimeBetweenTrades.TotalHours}h)");
return false;
}
}
var shouldEnter = assetPositions == 0 &&
totalPositions < _config.MaxPositions &&
currentTime >= _config.TradingStartTime &&
currentTime <= _config.TradingEndTime;
// Debug logging to understand why we're not trading
if (assetPositions > 0)
SmartLog($"{assetName}: Already have {assetPositions} positions for this asset");
if (totalPositions >= _config.MaxPositions)
SmartLog($"{assetName}: At max total positions ({totalPositions}/{_config.MaxPositions})");
if (currentTime < _config.TradingStartTime || currentTime > _config.TradingEndTime)
SmartLog($"{assetName}: Outside trading hours: {currentTime} (window: {_config.TradingStartTime}-{_config.TradingEndTime})");
if (shouldEnter)
SmartLog($"{assetName}: Ready to enter new position");
return shouldEnter;
}
/// <summary>
/// Try to enter an Iron Condor position for a specific asset using proven QC patterns
/// </summary>
private void TryEnterIronCondor(string assetName, OptionChain chain, decimal underlyingPrice)
{
try
{
// Filter expiration: 30-45 days (matching our optimized option filter)
var validExpirations = chain.Select(x => x.Expiry).Distinct()
.Where(exp => (exp - Algorithm.Time.Date).TotalDays >= 30 &&
(exp - Algorithm.Time.Date).TotalDays <= 45)
.OrderBy(exp => exp);
if (!validExpirations.Any())
{
SmartLog("No valid expirations found");
return;
}
var targetExpiry = validExpirations.First();
var contractsForExpiry = chain.Where(x => x.Expiry == targetExpiry).ToList();
// Use cached strikes if available and still valid, otherwise calculate new ones
StrikeRangeCalculator.StrikeRange strikes;
if (_strikeCache.ContainsKey(assetName))
{
var cached = _strikeCache[assetName];
var timeSinceCalculation = Algorithm.Time - cached.calculated;
var priceChange = Math.Abs(underlyingPrice - cached.underlyingPrice) / cached.underlyingPrice;
// Use cache if it's fresh and price hasn't moved significantly
if (timeSinceCalculation < _cacheTimeout && priceChange < _priceChangeThreshold)
{
strikes = cached.strikes;
SmartLog($"{assetName}: Using cached strikes (age: {timeSinceCalculation.TotalMinutes:F1}m, price change: {priceChange:P1})");
}
else
{
SmartLog($"{assetName}: Cache invalidated - age: {timeSinceCalculation.TotalMinutes:F1}m, price change: {priceChange:P1}");
strikes = CalculateAndCacheStrikes(assetName, chain, underlyingPrice);
}
}
else
{
SmartLog($"{assetName}: No cached strikes, calculating new ones");
strikes = CalculateAndCacheStrikes(assetName, chain, underlyingPrice);
}
SmartLog($"{assetName}: Calculated strikes: Put@{strikes.ShortPutStrike}, Call@{strikes.ShortCallStrike} (ATM: {strikes.ATMStrike})");
// Find the option contracts using calculated strikes
var shortPut = contractsForExpiry.FirstOrDefault(x =>
x.Right == OptionRight.Put && x.Strike == strikes.ShortPutStrike);
var shortCall = contractsForExpiry.FirstOrDefault(x =>
x.Right == OptionRight.Call && x.Strike == strikes.ShortCallStrike);
if (shortPut == null || shortCall == null)
{
SmartLog($"{assetName}: Could not find contracts at calculated strikes");
return;
}
SmartLog($"{assetName}: Found contracts: Put@{shortPut.Strike}, Call@{shortCall.Strike}");
// Use PositionAdapter for dynamic position sizing
var (minPos, maxPos, configAllocation) = _config.GetPositionLimitsForAsset(assetName);
var allocation = GetAllocationForAsset(assetName); // Use template-specific allocation
// Calculate base position size from configuration
var baseSize = Math.Max(1, (int)(Algorithm.Portfolio.TotalPortfolioValue * allocation / (underlyingPrice * 100)));
// Use simple position sizing based on asset profile
var assetProfile2 = MultiAssetHelper.GetAssetProfile(assetName);
var maxPosition = assetProfile2?.MaxPosition ?? 5;
var adaptedSize = Math.Min(baseSize, maxPosition);
int contracts = (int)Math.Max(minPos, Math.Min(adaptedSize, maxPos));
// Asset-specific validation
var profile = MultiAssetHelper.GetAssetProfile(assetName);
var accountSize = _config.AccountSize; // Use configurable account size
if (profile != null && accountSize < profile.MinAccountSize)
{
SmartLog($"{assetName}: Account ${accountSize:F0} below minimum ${profile.MinAccountSize:F0} for this asset - skipping");
contracts = 0; // Skip trading for insufficient account size
}
else if (AssetManager.IsIndex(assetName))
{
// Always cap at 1 contract for indices
contracts = Math.Min(contracts, 1);
SmartLog($"{assetName}: Index position sizing - Adapted size: {adaptedSize}, Final: {contracts} contracts (max 1 for indices)");
}
else
{
SmartLog($"{assetName}: Equity position sizing - Base: {baseSize}, Adapted: {adaptedSize}, Final: {contracts} contracts");
}
// Pre-flight risk validation (optional - continues if risk manager not available)
var passesRiskValidation = true;
if (_riskManager != null && contracts > 0)
{
passesRiskValidation = _riskManager.ValidateNewPosition(assetName, contracts, underlyingPrice);
if (!passesRiskValidation)
{
SmartLog($"MultiAsset: {assetName} position blocked by risk manager");
}
}
// Pre-flight margin check before placing orders
if (contracts > 0 && passesRiskValidation && ValidateMarginRequirements(assetName, contracts, underlyingPrice))
{
// Enter the Iron Condor using ComboMarketOrder for atomic execution
var legs = new List<Leg>
{
Leg.Create(shortPut.Symbol, -contracts), // Short Put
Leg.Create(shortCall.Symbol, -contracts) // Short Call
};
var tickets = Algorithm.ComboMarketOrder(legs, 1, tag: $"MultiAsset Iron Condor {assetName}");
// Record trade time to prevent excessive trading
_lastTradeTime[assetName] = Algorithm.Time;
SmartLog($"MultiAsset: Entered Iron Condor (ComboOrder) for {assetName} - " +
$"Put: ${shortPut.Strike}, Call: ${shortCall.Strike}, " +
$"Contracts: {contracts}, Price: ${underlyingPrice:F2}");
// Log combo order results
if (tickets != null && tickets.Any())
{
SmartLog($" Combo Order Status: {tickets.Count} tickets created");
foreach (var ticket in tickets.Take(2))
{
SmartLog($" Ticket {ticket.OrderId}: {ticket.Symbol.Value} x{ticket.Quantity} - {ticket.Status}");
}
}
else
{
SmartLog($" [ERROR] Combo order failed - no tickets returned");
}
}
else
{
var reason = contracts == 0 ? "zero contracts calculated" :
!passesRiskValidation ? "failed risk validation" :
"failed margin validation";
SmartLog($"MultiAsset: Skipped {assetName} - {reason}");
}
}
catch (Exception ex)
{
SmartError($"Error entering Iron Condor for {assetName}: {ex.Message}");
}
}
/// <summary>
/// Fix symbols array from QuantConnect parameters before config adaptation
/// This is critical because QC passes arrays as "System.String[]" string
/// </summary>
private void FixSymbolsFromParameters()
{
try
{
// Read parameters directly from QuantConnect
var symbolOverride = Algorithm.GetParameter("Symbol", "");
var underlyingSymbolLegacy = Algorithm.GetParameter("UnderlyingSymbol", "");
var symbolsParameter = Algorithm.GetParameter("Symbols", "");
SmartLog($"MultiAsset: Raw Parameters - Symbol='{symbolOverride}', UnderlyingSymbol='{underlyingSymbolLegacy}', Symbols='{symbolsParameter}'");
string[] actualSymbols = null;
string primarySymbol = "";
// Priority order: Symbol > Symbols > UnderlyingSymbol
if (!string.IsNullOrEmpty(symbolOverride))
{
primarySymbol = symbolOverride;
actualSymbols = new[] { symbolOverride };
SmartLog($"MultiAsset: Using Symbol override: {primarySymbol}");
}
else if (!string.IsNullOrEmpty(symbolsParameter) && symbolsParameter != "System.String[]")
{
// Parse Symbols parameter directly (comma-separated)
actualSymbols = symbolsParameter.Split(',').Select(s => s.Trim()).Where(s => !string.IsNullOrEmpty(s)).ToArray();
if (actualSymbols.Length > 0)
{
primarySymbol = actualSymbols[0];
SmartLog($"MultiAsset: Parsed Symbols parameter: [{string.Join(", ", actualSymbols)}] (Primary: {primarySymbol})");
}
}
else if (!string.IsNullOrEmpty(underlyingSymbolLegacy) && underlyingSymbolLegacy != "SPY")
{
primarySymbol = underlyingSymbolLegacy;
actualSymbols = new[] { underlyingSymbolLegacy };
SmartLog($"MultiAsset: Using UnderlyingSymbol (legacy): {primarySymbol}");
}
// Update config with correct symbols if we found any
if (actualSymbols != null && actualSymbols.Length > 0)
{
// Update the symbol arrays BEFORE config loads parameters
_config.Symbols = actualSymbols;
if (_config.Symbols.Length > 0)
{
_config.UnderlyingSymbol = actualSymbols[0]; // Set primary symbol too
}
SmartLog($"MultiAsset: Pre-configured with {actualSymbols.Length} symbols: [{string.Join(", ", actualSymbols)}]");
}
else
{
SmartLog($"MultiAsset: No valid symbols found in parameters, keeping defaults: [{string.Join(", ", _config.Symbols)}]");
}
}
catch (Exception ex)
{
SmartError($"Error fixing symbols from parameters: {ex.Message}");
}
}
/// <summary>
/// Validate margin requirements before placing orders
/// </summary>
private bool ValidateMarginRequirements(string assetName, int contracts, decimal underlyingPrice)
{
try
{
// Create a list of legs for the Iron Condor strategy
var legs = new List<Leg>();
// For margin calculation purposes, we need to estimate the position
// Since we're doing a simplified Iron Condor (just short put and call)
// we'll validate based on strategy type
// Use QC's built-in margin validation
var isValid = Algorithm.Portfolio.MarginRemaining > 0;
// Also check current margin utilization
var marginUtilization = Algorithm.Portfolio.TotalMarginUsed / Algorithm.Portfolio.TotalPortfolioValue;
var isApproachingLimit = marginUtilization > 0.6m; // 60% threshold
if (isApproachingLimit)
{
SmartLog($"{assetName}: Margin utilization high ({marginUtilization:P1}) - skipping trade");
return false;
}
// Get margin statistics for logging
// Use QC's built-in portfolio metrics
SmartLog($"{assetName}: Margin validation - " +
$"Used: ${Algorithm.Portfolio.TotalMarginUsed:F0}, " +
$"Remaining: ${Algorithm.Portfolio.MarginRemaining:F0}, " +
$"Utilization: {marginUtilization:P1}, " +
$"Result: {(isValid ? "PASS" : "FAIL")}");
return isValid;
}
catch (Exception ex)
{
SmartError($"Error validating margin for {assetName}: {ex.Message}");
return false; // Fail safe - don't trade if validation fails
}
}
/// <summary>
/// Get template-specific allocation for each asset
/// </summary>
private decimal GetAllocationForAsset(string assetName)
{
return assetName switch
{
"SPX" => 0.03m, // 3% for SPX (high margin, lower allocation)
"SPY" => 0.15m, // 15% for SPY (lower margin, higher allocation)
"QQQ" => 0.12m, // 12% for QQQ
"AAPL" => 0.10m, // 10% for individual stocks
"TSLA" => 0.08m, // 8% for high volatility stocks
_ => _config.AllocationPerPosition // Use config default for unknown assets
};
}
/// <summary>
/// Calculate strikes using StrikeRangeCalculator and cache the result for performance
/// </summary>
private StrikeRangeCalculator.StrikeRange CalculateAndCacheStrikes(string assetName, OptionChain chain, decimal underlyingPrice)
{
try
{
// Use asset profile volatility from MultiAssetHelper
var assetProfile = MultiAssetHelper.GetAssetProfile(assetName);
var currentVolatility = assetProfile?.TypicalVolatility ?? 0.20m; // Default 20% volatility
var deltaTargets = StrikeRangeCalculator.GetAssetSpecificDeltas(assetName, currentVolatility);
// For simplified Iron Condor, we only need short strikes
var simplifiedTargets = new StrikeRangeCalculator.DeltaTargets
{
ShortPut = deltaTargets.ShortPut,
ShortCall = deltaTargets.ShortCall,
LongPut = 0, // Not used in simplified version
LongCall = 0 // Not used in simplified version
};
// Calculate optimal strikes using the calculator
var strikes = StrikeRangeCalculator.CalculateStrikeRange(chain, simplifiedTargets);
// Cache the result with current time and price
_strikeCache[assetName] = (strikes, Algorithm.Time, underlyingPrice);
SmartLog($"{assetName}: Calculated and cached new strikes at price ${underlyingPrice:F2}");
return strikes;
}
catch (Exception ex)
{
SmartError($"Error calculating strikes for {assetName}: {ex.Message}");
// Return a simple fallback strike range if calculation fails
var atmStrike = underlyingPrice;
return new StrikeRangeCalculator.StrikeRange
{
ATMStrike = atmStrike,
ShortPutStrike = atmStrike * 0.95m,
ShortCallStrike = atmStrike * 1.05m,
LongPutStrike = atmStrike * 0.90m,
LongCallStrike = atmStrike * 1.10m
};
}
}
/// <summary>
/// Check exit conditions for existing positions across all assets
/// </summary>
private void CheckExitConditions()
{
try
{
var optionPositions = Algorithm.Portfolio.Values
.Where(x => x.Invested && x.Symbol.SecurityType == SecurityType.Option)
.ToList();
SmartLog($"MultiAsset: Checking exit conditions for {optionPositions.Count} option positions");
foreach (var position in optionPositions)
{
// Determine which asset this position belongs to
var underlyingSymbol = position.Symbol.Underlying.Value;
var assetName = _underlyings.FirstOrDefault(kvp => kvp.Value.Value == underlyingSymbol).Key;
if (string.IsNullOrEmpty(assetName))
{
SmartLog($"MultiAsset: Could not determine asset for position {position.Symbol}");
assetName = underlyingSymbol; // Fallback to underlying symbol
}
var currentValue = position.HoldingsValue;
var entryValue = position.HoldingsCost;
if (entryValue != 0)
{
var pnlPercent = (currentValue - entryValue) / Math.Abs(entryValue);
// Exit at profit target or stop loss from config
if (pnlPercent >= _config.ProfitTarget || pnlPercent <= _config.StopLoss)
{
Algorithm.MarketOrder(position.Symbol, -position.Quantity);
SmartLog($"MultiAsset: Exited {assetName} position {position.Symbol} at {pnlPercent:P1} P&L (Value: ${currentValue:F0}, Cost: ${entryValue:F0})");
}
else
{
SmartLog($"MultiAsset: {assetName} position {position.Symbol} P&L: {pnlPercent:P1} (holding, target: {_config.ProfitTarget:P1}, stop: {_config.StopLoss:P1})");
}
}
else
{
SmartLog($"MultiAsset: {assetName} position {position.Symbol} has zero entry value, skipping exit check");
}
}
}
catch (Exception ex)
{
SmartError($"Error checking exit conditions: {ex.Message}");
}
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Orders;
using QuantConnect.Orders.Slippage;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using QuantConnect.Indicators;
using CoreAlgo.Architecture.Core.Implementations;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Architecture.Core.Templates
{
/// <summary>
/// Opening Range Breakout (ORB) strategy template
/// Monitors opening range and opens 0DTE credit spreads at 12:00 PM based on price action
/// Includes SMA(20) filter, FOMC day skipping, and capital allocation limits
/// </summary>
public class ORBTemplate : SimpleBaseStrategy
{
private ORBConfig _config;
private Symbol _underlying;
private Symbol _optionSymbol;
private SimpleMovingAverage _sma20;
// Opening range tracking
private decimal _orbHigh = decimal.MinValue;
private decimal _orbLow = decimal.MaxValue;
private decimal _openingPrice = 0;
private DateTime _rangeEndTime;
private bool _rangeEstablished = false;
private bool _validRangeWidth = false;
// Day-long price monitoring
private bool _dayHighExceededORB = false;
private bool _dayLowBrokeORB = false;
private decimal _dayHigh = decimal.MinValue;
private decimal _dayLow = decimal.MaxValue;
// Position tracking
private bool _positionOpenedToday = false;
private DateTime _lastTradingDay = DateTime.MinValue;
public override string Name => "Opening Range Breakout";
public override string Description =>
"Monitors opening range and opens 0DTE credit spreads at 12:00 PM on full trading days based on breakout conditions with SMA(20) filter and capital allocation limits";
public override void OnInitialize()
{
SmartLog("ORBTemplate.OnInitialize() starting...");
// Configure with ORB-specific settings
try
{
Configure<ORBConfig>();
_config = (ORBConfig)Config;
SmartLog("Configuration loaded successfully");
}
catch (Exception ex)
{
SmartError($"Failed to load configuration: {ex.Message}");
throw;
}
// Setup underlying and options
SmartLog($"Setting up {_config.UnderlyingSymbol} for ORB trading");
// For 0DTE we need options expiring today, so use 0-1 day range
(_underlying, _optionSymbol) = SetupOptionsForSymbol(_config.UnderlyingSymbol,
strikeRange: 50, minDTE: 0, maxDTE: 1);
SmartLog($"Successfully added {_underlying} with options chain {_optionSymbol}");
// Initialize SMA(20) indicator for trend filter
if (_config.UseSmaTwentyFilter)
{
_sma20 = new SimpleMovingAverage(20);
SmartLog("SMA(20) indicator initialized for trend filtering");
}
// Slippage is now handled globally in Main.cs CompleteSecurityInitializer using VolumeShareSlippageModel
// Strategy will execute during OnExecute when conditions are met
SmartLog($"Strategy will check for entry conditions at {_config.EntryHour}:{_config.EntryMinute:D2} during regular OnExecute calls");
SmartLog($"ORB Strategy initialized:");
SmartLog($" Range Period: {_config.RangePeriodMinutes} minutes");
SmartLog($" Min Range Width: {_config.MinRangeWidthPercent:P1}");
SmartLog($" Spread Width: ${_config.SpreadWidth}");
SmartLog($" Entry Time: {_config.EntryHour}:{_config.EntryMinute:D2} (on full trading days only)");
SmartLog($" Contract Size: {_config.ContractSize}");
SmartLog($" SMA(20) Filter: {(_config.UseSmaTwentyFilter ? "Enabled" : "Disabled")}");
SmartLog($" Capital Allocation: ${_config.CapitalAllocation:N0}");
SmartLog($" Skip FOMC Days: {(_config.SkipFomcDays ? "Yes" : "No")}");
SmartLog($" Options Slippage: ${_config.SlippageAmount}");
}
protected override void OnExecute(Slice slice)
{
// Reset daily tracking at market open
if (slice.Time.Date > _lastTradingDay)
{
ResetDailyTracking(slice);
_lastTradingDay = slice.Time.Date;
}
// Get current price
var currentPrice = Securities[_underlying].Price;
if (currentPrice <= 0) return;
var currentTime = slice.Time;
var marketOpen = currentTime.Date.AddHours(9).AddMinutes(30);
// Track opening range (first X minutes)
if (!_rangeEstablished && currentTime >= marketOpen && currentTime <= _rangeEndTime)
{
UpdateOpeningRange(currentPrice, currentTime);
}
// Track day-long price movement after range is established
if (_rangeEstablished)
{
UpdateDayTracking(currentPrice);
}
// Check for entry conditions at or after entry time
var entryTime = currentTime.Date.AddHours(_config.EntryHour).AddMinutes(_config.EntryMinute);
if (currentTime >= entryTime && _rangeEstablished && _validRangeWidth && !_positionOpenedToday)
{
// Try to execute ORB strategy with available option chain data
TryExecuteORBWithSlice(slice);
}
}
private void ResetDailyTracking(Slice slice)
{
SmartLog($"=== Resetting daily tracking for {slice.Time.Date:yyyy-MM-dd} ===");
_orbHigh = decimal.MinValue;
_orbLow = decimal.MaxValue;
_openingPrice = 0;
_rangeEstablished = false;
_validRangeWidth = false;
_dayHighExceededORB = false;
_dayLowBrokeORB = false;
_dayHigh = decimal.MinValue;
_dayLow = decimal.MaxValue;
_positionOpenedToday = false;
// Set range end time
_rangeEndTime = slice.Time.Date.AddHours(9).AddMinutes(30).AddMinutes(_config.RangePeriodMinutes);
}
private void UpdateOpeningRange(decimal price, DateTime time)
{
// Capture opening price
if (_openingPrice == 0)
{
_openingPrice = price;
SmartLog($"DIAGNOSTIC: Opening price captured: ${_openingPrice:F2} at {time:HH:mm:ss}");
}
// Update range high/low
if (price > _orbHigh || _orbHigh == decimal.MinValue)
{
_orbHigh = price;
}
if (price < _orbLow || _orbLow == decimal.MaxValue)
{
_orbLow = price;
}
// Check if range period is complete
if (time >= _rangeEndTime && !_rangeEstablished)
{
_rangeEstablished = true;
var rangeWidth = _orbHigh - _orbLow;
var rangeWidthPercent = rangeWidth / _openingPrice * 100;
_validRangeWidth = rangeWidthPercent >= _config.MinRangeWidthPercent;
SmartLog($"DIAGNOSTIC: Opening Range Established:");
SmartLog($" High: ${_orbHigh:F2}");
SmartLog($" Low: ${_orbLow:F2}");
SmartLog($" Width: ${rangeWidth:F2} ({rangeWidthPercent:F2}%)");
SmartLog($" Valid: {(_validRangeWidth ? "YES" : "NO - Too narrow")}");
SmartLog($" Required Width: {_config.MinRangeWidthPercent:F2}%");
// Initialize day tracking with opening range values
_dayHigh = _orbHigh;
_dayLow = _orbLow;
}
}
private void UpdateDayTracking(decimal price)
{
// Update day high/low
if (price > _dayHigh)
{
_dayHigh = price;
// Check if exceeded ORB high
if (price > _orbHigh && !_dayHighExceededORB)
{
_dayHighExceededORB = true;
SmartLog($"DIAGNOSTIC: Day high EXCEEDED ORB high at ${price:F2} > ${_orbHigh:F2}");
}
}
if (price < _dayLow)
{
_dayLow = price;
// Check if broke below ORB low
if (price < _orbLow && !_dayLowBrokeORB)
{
_dayLowBrokeORB = true;
SmartLog($"DIAGNOSTIC: Day low BROKE below ORB low at ${price:F2} < ${_orbLow:F2}");
}
}
}
private void TryExecuteORBWithSlice(Slice slice)
{
SmartLog($"DIAGNOSTIC: === ORB Strategy Execution at {slice.Time:HH:mm:ss} ===");
SmartLog($"DIAGNOSTIC: Range established: {_rangeEstablished}, Valid width: {_validRangeWidth}");
// Check FOMC day skip condition
if (_config.SkipFomcDays && ORBConfig.FomcDates2024_2025.Contains(slice.Time.Date))
{
SmartLog($"DIAGNOSTIC: Skipping trading on FOMC day: {slice.Time.Date:yyyy-MM-dd}");
return;
}
SmartLog($"DIAGNOSTIC: Not an FOMC day - proceeding with strategy evaluation");
// Check capital allocation limits
var availableCapital = GetAvailableCapital();
if (availableCapital <= 0)
{
SmartLog($"Capital allocation limit reached. Available: ${availableCapital:F2}");
return;
}
// Check if this is a full trading day by verifying market close time
var todayMarketHours = Algorithm.Securities[_underlying].Exchange.Hours.GetMarketHours(slice.Time.Date);
// Check if market is open for full day by verifying it closes at normal time or later
if (todayMarketHours.IsClosedAllDay)
{
SmartLog($"Market closed all day, skipping ORB strategy");
return;
}
// For simplicity, assume if market is open and we have data, it's a full trading day
SmartLog($"Full trading day assumed - ORB strategy proceeding");
// Determine base entry conditions
var callSpreadBaseCondition = _dayLowBrokeORB && !_dayHighExceededORB;
var putSpreadBaseCondition = _dayHighExceededORB && !_dayLowBrokeORB;
// Get current price and update SMA
var currentPrice = Algorithm.Securities[_underlying].Price;
// Update SMA manually if enabled
if (_config.UseSmaTwentyFilter && _sma20 != null)
{
_sma20.Update(slice.Time, currentPrice);
}
// SMA filter for call spreads only
var smaFilterPassed = !_config.UseSmaTwentyFilter || _sma20 == null || !_sma20.IsReady || currentPrice < _sma20.Current.Value;
// Apply SMA filter to call spread condition
var shouldOpenCallSpread = callSpreadBaseCondition && smaFilterPassed;
var shouldOpenPutSpread = putSpreadBaseCondition; // No SMA filter for put spreads
SmartLog($"DIAGNOSTIC: Entry Analysis:");
SmartLog($" Day Low Broke ORB: {_dayLowBrokeORB}");
SmartLog($" Day High Exceeded ORB: {_dayHighExceededORB}");
SmartLog($" Current Price: ${currentPrice:F2}");
if (_config.UseSmaTwentyFilter && _sma20?.IsReady == true)
{
SmartLog($" SMA(20): ${_sma20.Current.Value:F2}");
SmartLog($" Price < SMA(20): {(currentPrice < _sma20.Current.Value ? "YES" : "NO")}");
}
SmartLog($" SMA Filter Passed: {smaFilterPassed}");
SmartLog($" Available Capital: ${availableCapital:F2}");
SmartLog($" Call Spread Base Condition: {callSpreadBaseCondition}");
SmartLog($" Put Spread Base Condition: {putSpreadBaseCondition}");
SmartLog($" Should Open Call Spread (with SMA filter): {shouldOpenCallSpread}");
SmartLog($" Should Open Put Spread (no SMA filter): {shouldOpenPutSpread}");
// Check if any spread conditions are met
if (!shouldOpenCallSpread && !shouldOpenPutSpread)
{
SmartLog("DIAGNOSTIC: No entry conditions met - neither call nor put spread criteria satisfied");
return;
}
// Get option chain from slice (QC-standard approach)
if (!slice.OptionChains.TryGetValue(_optionSymbol, out var optionChain))
{
SmartLog("No option chain available in slice");
return;
}
if (!optionChain.Any())
{
SmartLog("Option chain is empty");
return;
}
SmartLog($"Found option chain with {optionChain.Count()} contracts");
// Log available expiration dates for debugging
var expirations = optionChain.Select(x => x.Expiry.Date).Distinct().OrderBy(x => x).ToList();
SmartLog($"Available expirations: {string.Join(", ", expirations.Select(x => x.ToString("yyyy-MM-dd")))}");
var currentDate = Algorithm.Time.Date;
SmartLog($"Looking for 0DTE options expiring on: {currentDate:yyyy-MM-dd}");
var todayOptions = optionChain.Where(x => x.Expiry.Date == currentDate).ToList();
SmartLog($"Options expiring today ({currentDate:yyyy-MM-dd}): {todayOptions.Count}");
if (todayOptions.Count == 0)
{
SmartLog("No options expiring today - trying nearest expiration");
var nearestExpiry = expirations.FirstOrDefault(x => x >= currentDate);
if (nearestExpiry != default)
{
SmartLog($"Using nearest expiration: {nearestExpiry:yyyy-MM-dd}");
todayOptions = optionChain.Where(x => x.Expiry.Date == nearestExpiry).ToList();
SmartLog($"Options with nearest expiration: {todayOptions.Count}");
}
}
// Execute spreads independently - call spread first (with SMA filter)
if (shouldOpenCallSpread)
{
SmartLog($"DIAGNOSTIC: Attempting to execute CALL SPREAD (with SMA filter)");
ExecuteCallSpread(optionChain, availableCapital);
_positionOpenedToday = true;
SmartLog($"DIAGNOSTIC: Call spread executed, position opened flag set to true");
return;
}
// Execute put spread (no SMA filter)
if (shouldOpenPutSpread)
{
SmartLog($"DIAGNOSTIC: Attempting to execute PUT SPREAD (no SMA filter)");
ExecutePutSpread(optionChain, availableCapital);
_positionOpenedToday = true;
SmartLog($"DIAGNOSTIC: Put spread executed, position opened flag set to true");
return;
}
SmartLog($"DIAGNOSTIC: This should not happen - no spread executed despite conditions being met");
}
private decimal GetAvailableCapital()
{
// Calculate current SPX position value using Algorithm.Portfolio
var currentPositionValue = Algorithm.Portfolio
.Where(kvp => kvp.Key.Underlying?.Value == "SPX")
.Sum(kvp => Math.Abs(kvp.Value.HoldingsValue));
return Math.Max(0, _config.CapitalAllocation - currentPositionValue);
}
private int CalculateContractSize(decimal availableCapital)
{
// Calculate max risk per contract (spread width * 100)
var maxRiskPerContract = _config.SpreadWidth * 100;
// Calculate max contracts based on available capital
var maxContractsByCapital = (int)Math.Floor(availableCapital / maxRiskPerContract);
// Use the minimum of configured size and capital-limited size
return Math.Min(_config.ContractSize, maxContractsByCapital);
}
private void ExecuteCallSpread(OptionChain chain, decimal availableCapital)
{
SmartLog("Executing CALL SPREAD (bearish position)...");
var underlyingPrice = chain.Underlying.Price;
var currentDate = Algorithm.Time.Date;
// Get the best expiration (preferably 0DTE, otherwise nearest)
var expirations = chain.Select(x => x.Expiry.Date).Distinct().OrderBy(x => x).ToList();
var targetExpiry = expirations.FirstOrDefault(x => x == currentDate);
if (targetExpiry == default)
{
targetExpiry = expirations.FirstOrDefault(x => x >= currentDate);
SmartLog($"No 0DTE options available, using nearest expiration: {targetExpiry:yyyy-MM-dd}");
}
else
{
SmartLog($"Using 0DTE options expiring: {targetExpiry:yyyy-MM-dd}");
}
// Filter for calls with target expiration
var calls = chain.Where(x =>
x.Right == OptionRight.Call &&
x.Expiry.Date == targetExpiry &&
x.BidPrice > 0 && x.AskPrice > 0)
.OrderBy(x => x.Strike)
.ToList();
if (calls.Count < 2)
{
SmartLog($"Not enough calls available for {targetExpiry:yyyy-MM-dd}: {calls.Count}");
return;
}
// Find short call: $0.01+ above underlying
var shortCallStrike = underlyingPrice + _config.MinStrikeOffset;
var shortCall = calls.FirstOrDefault(c => c.Strike >= shortCallStrike);
if (shortCall == null)
{
SmartLog("Could not find suitable short call strike");
return;
}
// Find long call: exactly $15 above short
var longCallStrike = shortCall.Strike + _config.SpreadWidth;
var longCall = calls.FirstOrDefault(c => Math.Abs(c.Strike - longCallStrike) < 0.01m);
if (longCall == null)
{
SmartLog($"Could not find long call at exact strike ${longCallStrike:F2}");
return;
}
// Calculate contract size based on available capital
var contractSize = CalculateContractSize(availableCapital);
if (contractSize <= 0)
{
SmartLog("Insufficient capital for call spread - skipping");
return;
}
// Execute spread using ComboMarketOrder for atomic execution
// QuantConnect expects: leg quantities as ratios (-1, +1) and global quantity as total contracts
var legs = new List<Leg>
{
Leg.Create(shortCall.Symbol, -1), // Short Call (ratio)
Leg.Create(longCall.Symbol, 1) // Long Call (ratio)
};
var tickets = ComboMarketOrder(legs, contractSize, tag: "ORB Call Spread");
SmartLog($"CALL SPREAD OPENED (ComboOrder):");
SmartLog($" Contracts: {contractSize}");
SmartLog($" Short Call: {shortCall.Strike} @ ${shortCall.BidPrice:F2}");
SmartLog($" Long Call: {longCall.Strike} @ ${longCall.AskPrice:F2}");
SmartLog($" Credit: ${(shortCall.BidPrice - longCall.AskPrice) * 100 * contractSize:F2}");
SmartLog($" Max Risk: ${_config.SpreadWidth * 100 * contractSize:F2}");
// Log combo order results
if (tickets != null && tickets.Any())
{
SmartLog($" Combo Order Status: {tickets.Count} tickets created");
foreach (var ticket in tickets.Take(2))
{
SmartLog($" Ticket {ticket.OrderId}: {ticket.Symbol.Value} x{ticket.Quantity} - {ticket.Status}");
}
}
else
{
SmartLog($" [ERROR] Combo order failed - no tickets returned");
}
_positionOpenedToday = true;
}
private void ExecutePutSpread(OptionChain chain, decimal availableCapital)
{
SmartLog("Executing PUT SPREAD (bullish position)...");
var underlyingPrice = chain.Underlying.Price;
var currentDate = Algorithm.Time.Date;
// Get the best expiration (preferably 0DTE, otherwise nearest)
var expirations = chain.Select(x => x.Expiry.Date).Distinct().OrderBy(x => x).ToList();
var targetExpiry = expirations.FirstOrDefault(x => x == currentDate);
if (targetExpiry == default)
{
targetExpiry = expirations.FirstOrDefault(x => x >= currentDate);
SmartLog($"No 0DTE options available, using nearest expiration: {targetExpiry:yyyy-MM-dd}");
}
else
{
SmartLog($"Using 0DTE options expiring: {targetExpiry:yyyy-MM-dd}");
}
// Filter for puts with target expiration
var puts = chain.Where(x =>
x.Right == OptionRight.Put &&
x.Expiry.Date == targetExpiry &&
x.BidPrice > 0 && x.AskPrice > 0)
.OrderByDescending(x => x.Strike)
.ToList();
if (puts.Count < 2)
{
SmartLog($"Not enough puts available for {targetExpiry:yyyy-MM-dd}: {puts.Count}");
return;
}
// Find short put: $0.01+ below underlying
var shortPutStrike = underlyingPrice - _config.MinStrikeOffset;
var shortPut = puts.FirstOrDefault(p => p.Strike <= shortPutStrike);
if (shortPut == null)
{
SmartLog("Could not find suitable short put strike");
return;
}
// Find long put: exactly $15 below short
var longPutStrike = shortPut.Strike - _config.SpreadWidth;
var longPut = puts.FirstOrDefault(p => Math.Abs(p.Strike - longPutStrike) < 0.01m);
if (longPut == null)
{
SmartLog($"Could not find long put at exact strike ${longPutStrike:F2}");
return;
}
// Calculate contract size based on available capital
var contractSize = CalculateContractSize(availableCapital);
if (contractSize <= 0)
{
SmartLog("Insufficient capital for put spread - skipping");
return;
}
// Execute spread using ComboMarketOrder for atomic execution
// QuantConnect expects: leg quantities as ratios (-1, +1) and global quantity as total contracts
var legs = new List<Leg>
{
Leg.Create(shortPut.Symbol, -1), // Short Put (ratio)
Leg.Create(longPut.Symbol, 1) // Long Put (ratio)
};
var tickets = ComboMarketOrder(legs, contractSize, tag: "ORB Put Spread");
SmartLog($"PUT SPREAD OPENED (ComboOrder):");
SmartLog($" Contracts: {contractSize}");
SmartLog($" Short Put: {shortPut.Strike} @ ${shortPut.BidPrice:F2}");
SmartLog($" Long Put: {longPut.Strike} @ ${longPut.AskPrice:F2}");
SmartLog($" Credit: ${(shortPut.BidPrice - longPut.AskPrice) * 100 * contractSize:F2}");
SmartLog($" Max Risk: ${_config.SpreadWidth * 100 * contractSize:F2}");
// Log combo order results
if (tickets != null && tickets.Any())
{
SmartLog($" Combo Order Status: {tickets.Count} tickets created");
foreach (var ticket in tickets.Take(2))
{
SmartLog($" Ticket {ticket.OrderId}: {ticket.Symbol.Value} x{ticket.Quantity} - {ticket.Status}");
}
}
else
{
SmartLog($" [ERROR] Combo order failed - no tickets returned");
}
_positionOpenedToday = true;
}
protected override void OnGetPerformanceMetrics(System.Collections.Generic.Dictionary<string, double> metrics)
{
// Add ORB specific metrics
metrics["ORBRangeValid"] = _validRangeWidth ? 1.0 : 0.0;
metrics["DayHighExceededORB"] = _dayHighExceededORB ? 1.0 : 0.0;
metrics["DayLowBrokeORB"] = _dayLowBrokeORB ? 1.0 : 0.0;
metrics["PositionOpenedToday"] = _positionOpenedToday ? 1.0 : 0.0;
metrics["AvailableCapital"] = (double)GetAvailableCapital();
if (_config.UseSmaTwentyFilter && _sma20?.IsReady == true)
{
metrics["SMA20Value"] = (double)_sma20.Current.Value;
metrics["PriceBelowSMA20"] = Algorithm.Securities[_underlying].Price < _sma20.Current.Value ? 1.0 : 0.0;
}
}
}
}using System;
using System.Linq;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using CoreAlgo.Architecture.Core.Implementations;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.Core.Configuration;
using CoreAlgo.Architecture.QC.Helpers;
namespace CoreAlgo.Architecture.Core.Templates
{
/// <summary>
/// Template demonstrating the Entry/Exit Restriction Framework.
/// Shows how to use EntryRestrictions and ExitRestrictions for controlled trading.
/// This example trades both stocks and options with full restriction checks.
/// </summary>
public class RestrictedTradingTemplate : SimpleBaseStrategy
{
private RestrictedTradingConfig _config;
private Symbol _underlying;
private Symbol _optionSymbol;
public override string Name => "Restricted Trading";
public override string Description =>
"Demonstrates Entry/Exit Restriction Framework with configurable trading rules for stocks and options";
public override void OnInitialize()
{
SmartLog("RestrictedTradingTemplate.OnInitialize() starting...");
try
{
Configure<RestrictedTradingConfig>();
_config = (RestrictedTradingConfig)Config;
SmartLog("Restricted Trading configuration loaded successfully");
}
catch (Exception ex)
{
SmartError($"Failed to load configuration: {ex.Message}");
throw;
}
// Log restriction configuration
SmartLog("=== Entry/Exit Restrictions Configuration ===");
SmartLog($"Trading Hours: {_config.TradingStartTime} - {_config.TradingEndTime}");
SmartLog($"Max Positions: {_config.MaxPositions}");
SmartLog($"Allocation Per Position: {_config.AllocationPerPosition:P1}");
SmartLog($"Profit Target: {_config.ProfitTarget:P1}");
SmartLog($"Stop Loss: {_config.StopLoss:P1}");
SmartLog($"Max Days in Trade: {_config.MaxDaysInTrade}");
if (_config.TradeOptions)
{
SmartLog($"Options Entry Delta: {_config.EntryDeltaMin:F2} - {_config.EntryDeltaMax:F2}");
SmartLog($"Options Exit Delta: {_config.ExitDelta:F2}");
SmartLog($"Min Implied Volatility: {_config.MinImpliedVolatility:P1}");
}
// Add underlying using AssetManager (supports SPY/SPX/QQQ/ES)
try
{
var asset = AssetManager.AddAsset(this, _config.UnderlyingSymbol, Resolution.Minute);
_underlying = asset.Symbol;
SmartLog($"Added underlying: {_underlying} (Type: {asset.Type})");
}
catch (Exception ex)
{
SmartError($"Failed to add underlying {_config.UnderlyingSymbol}: {ex.Message}");
throw;
}
// Add options if configured
if (_config.TradeOptions)
{
try
{
_optionSymbol = AssetManager.AddOptionsChain(this, Securities[_underlying], Resolution.Minute);
// Configure option filter based on our entry delta requirements
var option = Securities[_optionSymbol] as Option;
option?.SetFilter(universe => universe
.IncludeWeeklys()
.Strikes(-10, 10)
.Expiration(7, 45));
SmartLog($"Added options chain: {_optionSymbol}");
}
catch (Exception ex)
{
SmartWarn($"Could not add options for {_underlying}: {ex.Message}");
_config.TradeOptions = false; // Disable options trading
}
}
SmartLog("Restricted Trading strategy initialized successfully!");
}
protected override void OnExecute(Slice slice)
{
Debug($"RestrictedTrading OnExecute at {slice.Time}");
// First, check all exit conditions for existing positions
CheckExitConditions(slice);
// Then, look for new entry opportunities
CheckEntryOpportunities(slice);
}
private void CheckExitConditions(Slice slice)
{
// Get all positions that need exit checks
var exitStatuses = ExitRestrictions.GetAllPositionExitStatus();
foreach (var status in exitStatuses.Where(s => s.ShouldExit))
{
SmartLog($"=== EXIT SIGNAL for {status.Symbol} ===");
SmartLog($" Reason: {status.ExitReason}");
SmartLog($" Profit: {status.UnrealizedProfitPercent:P2}");
SmartLog($" Days Held: {status.DaysHeld:F1}");
SmartLog($" Exit Urgency: {status.ExitUrgency:F2}");
// Execute the exit
var holding = Portfolio[status.Symbol];
if (holding.Invested)
{
var exitOrder = Algorithm.MarketOrderWithRetry(status.Symbol, -(int)holding.Quantity);
if (exitOrder?.WasSuccessful() == true)
{
SmartLog($"[OK] Exit order placed: {exitOrder.GetStatusDescription()}");
ExitRestrictions.ClearPositionEntry(status.Symbol);
}
else
{
SmartError($"[FAILED] Failed to exit position: {exitOrder?.GetStatusDescription() ?? "null ticket"}");
}
}
}
}
private void CheckEntryOpportunities(Slice slice)
{
// Check entry restrictions first
if (!EntryRestrictions.CanEnterPosition(_underlying, slice, out var reason))
{
Debug($"Entry restricted for {_underlying}: {reason}");
return;
}
// Get current restriction status for logging
var status = EntryRestrictions.GetRestrictionStatus();
Debug($"Entry allowed - Positions: {status["ActivePositions"]}/{status["MaxPositions"]}, " +
$"Cash: ${status["AvailableCash"]:F2}");
// Decide whether to trade stocks or options
if (_config.TradeOptions && slice.OptionChains.ContainsKey(_optionSymbol))
{
CheckOptionEntry(slice);
}
else if (slice.Bars.ContainsKey(_underlying))
{
CheckStockEntry(slice);
}
}
private void CheckStockEntry(Slice slice)
{
var bar = slice.Bars[_underlying];
var price = bar.Close;
// Simple momentum signal for demonstration
if (IsStockEntrySignal(_underlying, price, slice.Time))
{
// Use PositionSizer to calculate quantity
var quantity = PositionSizer.CalculateSafeQuantity(
Portfolio,
_config.AllocationPerPosition,
price,
safetyBuffer: 0.05m // 5% safety buffer
);
if (quantity > 0)
{
SmartLog($"=== STOCK ENTRY SIGNAL ===");
SmartLog($" Symbol: {_underlying}");
SmartLog($" Price: ${price:F2}");
SmartLog($" Quantity: {quantity}");
SmartLog($" Position Value: ${quantity * price:F2}");
var order = Algorithm.MarketOrderWithRetry(_underlying, (int)quantity);
if (order?.WasSuccessful() == true)
{
SmartLog($"[OK] Entry order placed: {order.GetStatusDescription()}");
ExitRestrictions.RecordPositionEntry(_underlying, slice.Time);
}
else
{
SmartError($"[FAILED] Failed to enter position: {order?.GetStatusDescription() ?? "null ticket"}");
}
}
}
}
private void CheckOptionEntry(Slice slice)
{
var chain = slice.OptionChains[_optionSymbol];
if (!chain.Any())
return;
// Filter for puts (cash secured put strategy)
var puts = chain.Where(x => x.Right == OptionRight.Put);
// Find candidates that meet our entry restrictions
var candidates = puts
.Where(contract => EntryRestrictions.CanEnterOptionPosition(contract, out _))
.OrderBy(x => Math.Abs(x.Greeks.Delta - (_config.EntryDeltaMin + _config.EntryDeltaMax) / 2))
.Take(3);
var selected = candidates.FirstOrDefault();
if (selected != null)
{
// Additional IV check
if (selected.ImpliedVolatility < _config.MinImpliedVolatility)
{
Debug($"IV too low: {selected.ImpliedVolatility:F2} < {_config.MinImpliedVolatility:F2}");
return;
}
// Calculate safe position size for options
var contracts = PositionSizer.CalculateSafeOptionsQuantity(
Portfolio,
_config.AllocationPerPosition,
selected.BidPrice,
safetyBuffer: 0.10m // 10% buffer for options
);
if (contracts > 0)
{
SmartLog($"=== OPTION ENTRY SIGNAL ===");
SmartLog($" Contract: {selected.Symbol}");
SmartLog($" Strike: ${selected.Strike}");
SmartLog($" Expiry: {selected.Expiry:yyyy-MM-dd}");
SmartLog($" Delta: {selected.Greeks.Delta:F3}");
SmartLog($" IV: {selected.ImpliedVolatility:P1}");
SmartLog($" Premium: ${selected.BidPrice:F2} x {contracts} = ${selected.BidPrice * contracts * 100:F2}");
// Sell the put (negative quantity)
var order = Algorithm.MarketOrderWithRetry(selected.Symbol, -(int)contracts);
if (order?.WasSuccessful() == true)
{
SmartLog($"[OK] Option order placed: {order.GetStatusDescription()}");
ExitRestrictions.RecordPositionEntry(selected.Symbol, slice.Time);
}
else
{
SmartError($"[FAILED] Failed to enter option position: {order?.GetStatusDescription() ?? "null ticket"}");
}
}
}
}
private bool IsStockEntrySignal(Symbol symbol, decimal price, DateTime time)
{
try
{
// Simple RSI-based entry signal for demonstration
var history = Algorithm.History(symbol, 14, Resolution.Daily);
if (history.Count() < 14)
return false;
var prices = history.Select(x => x.Close).ToArray();
var rsi = CalculateRSI(prices);
// Buy on oversold conditions
return rsi < 35;
}
catch
{
return false;
}
}
private double CalculateRSI(decimal[] prices, int period = 14)
{
if (prices.Length < period)
return 50; // Neutral
var gains = new decimal[prices.Length - 1];
var losses = new decimal[prices.Length - 1];
for (int i = 1; i < prices.Length; i++)
{
var change = prices[i] - prices[i - 1];
gains[i - 1] = change > 0 ? change : 0;
losses[i - 1] = change < 0 ? -change : 0;
}
var avgGain = gains.Skip(gains.Length - period).Average();
var avgLoss = losses.Skip(losses.Length - period).Average();
if (avgLoss == 0)
return 100;
var rs = avgGain / avgLoss;
return 100 - (100 / (1 + (double)rs));
}
protected void LogDailySummary()
{
SmartLog("=== Daily Restriction Summary ===");
// Log entry restriction status
var entryStatus = EntryRestrictions.GetRestrictionStatus();
SmartLog($"Entry Status:");
SmartLog($" Trading Hours Active: {entryStatus["TradingHoursActive"]}");
SmartLog($" Active Positions: {entryStatus["ActivePositions"]}/{entryStatus["MaxPositions"]}");
SmartLog($" Available Cash: ${entryStatus["AvailableCash"]:F2}");
SmartLog($" Required per Position: ${entryStatus["RequiredCapitalPerPosition"]:F2}");
// Log exit status for all positions
var exitStatuses = ExitRestrictions.GetAllPositionExitStatus();
if (exitStatuses.Any())
{
SmartLog($"Exit Status:");
foreach (var status in exitStatuses)
{
SmartLog($" {status.Symbol}: P/L={status.UnrealizedProfitPercent:P2}, " +
$"Days={status.DaysHeld:F1}, Urgency={status.ExitUrgency:F2}");
}
}
SmartLog($"Portfolio Value: ${Portfolio.TotalPortfolioValue:N2}");
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Data;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Implementations;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.QC.Helpers;
namespace CoreAlgo.Architecture.Core.Templates
{
/// <summary>
/// Universe Selection strategy template that demonstrates the new QC helpers.
/// Tests AssetManager, PositionSizer, OrderExtensions, and enhanced StrategyConfig.
/// Can dynamically select stocks based on fundamental criteria and trade them with options.
/// </summary>
public class UniverseSelectionTemplate : SimpleBaseStrategy
{
private UniverseSelectionConfig _config;
private Dictionary<Symbol, Symbol> _underlyingToOptions = new Dictionary<Symbol, Symbol>();
private Dictionary<Symbol, DateTime> _lastTradeTime = new Dictionary<Symbol, DateTime>();
private List<Symbol> _activeSymbols = new List<Symbol>();
public override string Name => "Universe Selection";
public override string Description =>
"Dynamic universe selection strategy that demonstrates AssetManager, PositionSizer, and OrderExtensions with configurable fundamental filters";
public override void OnInitialize()
{
SmartLog("UniverseSelectionTemplate.OnInitialize() starting...");
try
{
Configure<UniverseSelectionConfig>();
_config = (UniverseSelectionConfig)Config;
SmartLog("Universe selection configuration loaded successfully");
}
catch (Exception ex)
{
SmartError($"Failed to load configuration: {ex.Message}");
throw;
}
// Use the enhanced StrategyConfig parameters
SmartLog($"Strategy Configuration:");
SmartLog($" UseUniverseSelection: {_config.UseUniverseSelection}");
SmartLog($" Manual Symbols: [{string.Join(", ", _config.Symbols)}]");
SmartLog($" Max Positions: {_config.MaxPositions}");
SmartLog($" Allocation Per Position: {_config.AllocationPerPosition:P1}");
SmartLog($" Min Market Cap: ${_config.MinMarketCap:N0}");
SmartLog($" Min Volume: {_config.MinDollarVolume:N0}");
SmartLog($" Trading Hours: {_config.TradingStartTime} - {_config.TradingEndTime}");
SmartLog($" P/E Ratio Range: {_config.MinPERatio} - {_config.MaxPERatio}");
if (_config.UseUniverseSelection)
{
SmartLog("Setting up dynamic universe selection...");
// Add universe selection using coarse data (simpler approach)
Algorithm.AddUniverse(coarse =>
{
return coarse
.Where(c => c.HasFundamentalData)
.Where(c => c.DollarVolume >= (double)_config.MinDollarVolume)
.Where(c => c.Price > 10) // Avoid penny stocks
.OrderByDescending(c => c.DollarVolume)
.Take(_config.MaxPositions)
.Select(c => c.Symbol);
});
SmartLog($"Universe selection configured with fundamental filters");
}
else
{
SmartLog("Using manual symbol list...");
// Add manual symbols using AssetManager
foreach (var symbolStr in _config.Symbols)
{
try
{
SmartLog($"Adding asset: {symbolStr}");
// Test AssetManager with different asset types
var underlying = AssetManager.AddAsset(this, symbolStr, Resolution.Minute);
_activeSymbols.Add(underlying.Symbol);
SmartLog($" Asset Type: {underlying.Type}");
SmartLog($" Is Index: {AssetManager.IsIndex(symbolStr)}");
SmartLog($" Is Future: {AssetManager.IsFuture(symbolStr)}");
SmartLog($" Is Equity: {AssetManager.IsEquity(symbolStr)}");
// Add options if configured
if (_config.TradeOptions)
{
try
{
var optionSymbol = AssetManager.AddOptionsChain(this, underlying, Resolution.Minute);
_underlyingToOptions[underlying.Symbol] = optionSymbol;
SmartLog($" Options added: {optionSymbol}");
}
catch (Exception ex)
{
SmartLog($" Could not add options for {symbolStr}: {ex.Message}");
}
}
}
catch (Exception ex)
{
SmartError($"Failed to add asset {symbolStr}: {ex.Message}");
}
}
}
SmartLog($"Universe Selection initialized with {_activeSymbols.Count} initial symbols");
SmartLog("Ready for dynamic trading!");
}
public void HandleSecuritiesChanged(SecurityChanges changes)
{
SmartLog($"HandleSecuritiesChanged: +{changes.AddedSecurities.Count}, -{changes.RemovedSecurities.Count}");
// Handle added securities from universe selection
foreach (var security in changes.AddedSecurities)
{
if (!_activeSymbols.Contains(security.Symbol))
{
_activeSymbols.Add(security.Symbol);
SmartLog($"Added to universe: {security.Symbol} (Type: {security.Type})");
// Add options for new securities if configured
if (_config.TradeOptions && security.Type == SecurityType.Equity)
{
try
{
var optionSymbol = AssetManager.AddOptionsChain(this, security, Resolution.Minute);
_underlyingToOptions[security.Symbol] = optionSymbol;
SmartLog($" Options added for {security.Symbol}: {optionSymbol}");
}
catch (Exception ex)
{
SmartLog($" Could not add options for {security.Symbol}: {ex.Message}");
}
}
}
}
// Handle removed securities
foreach (var security in changes.RemovedSecurities)
{
if (_activeSymbols.Contains(security.Symbol))
{
_activeSymbols.Remove(security.Symbol);
_underlyingToOptions.Remove(security.Symbol);
_lastTradeTime.Remove(security.Symbol);
SmartLog($"Removed from universe: {security.Symbol}");
// Liquidate positions if we have any
if (Portfolio[security.Symbol].Invested)
{
SmartLog($"Liquidating position in removed security: {security.Symbol}");
var ticket = Algorithm.MarketOrderWithRetry(security.Symbol, -(int)Portfolio[security.Symbol].Quantity);
if (ticket?.WasSuccessful() == true)
{
SmartLog($"Successfully liquidated {security.Symbol}: {ticket.GetStatusDescription()}");
}
else
{
SmartError($"Failed to liquidate {security.Symbol}: {ticket?.GetStatusDescription() ?? "null ticket"}");
}
}
}
}
}
protected override void OnExecute(Slice slice)
{
// Check trading hours
var currentTime = slice.Time.TimeOfDay;
if (currentTime < _config.TradingStartTime || currentTime > _config.TradingEndTime)
{
return;
}
// Check if we can take new positions
var activePositions = Portfolio.Count(x => x.Value.Invested);
if (activePositions >= _config.MaxPositions)
{
return;
}
// Look for trading opportunities in our universe
foreach (var symbol in _activeSymbols.Take(_config.MaxPositions))
{
// Skip if we already have a position or recently traded
if (Portfolio[symbol].Invested)
continue;
if (_lastTradeTime.ContainsKey(symbol) &&
slice.Time - _lastTradeTime[symbol] < TimeSpan.FromMinutes(_config.MinutesBetwenTrades))
continue;
// Check if we have price data
if (!slice.Bars.ContainsKey(symbol))
continue;
var bar = slice.Bars[symbol];
var price = bar.Close;
// Simple momentum strategy: buy if price is above recent average
if (ShouldBuySymbol(symbol, price, slice.Time))
{
TakePosition(symbol, price, slice.Time);
// Limit to one trade per OnData call to avoid overtrading
break;
}
}
// Check exit conditions for existing positions
CheckExitConditions(slice);
}
private bool ShouldBuySymbol(Symbol symbol, decimal price, DateTime time)
{
try
{
// Simple momentum check: price above 5-day average
var history = Algorithm.History(symbol, 5, Resolution.Daily);
if (history.Count() < 5)
return false;
var averagePrice = history.Select(h => h.Close).Average();
var momentum = (price - averagePrice) / averagePrice;
// Buy if momentum is positive and reasonable
return momentum > 0.02m && momentum < 0.10m; // 2-10% above average
}
catch (Exception ex)
{
SmartError($"Error checking buy signal for {symbol}: {ex.Message}");
return false;
}
}
private void TakePosition(Symbol symbol, decimal price, DateTime time)
{
try
{
// Use PositionSizer to calculate appropriate quantity
var quantity = PositionSizer.CalculateQuantity(Portfolio, _config.AllocationPerPosition, price);
if (quantity <= 0)
{
SmartLog($"Calculated quantity <= 0 for {symbol}, skipping");
return;
}
SmartLog($"Taking position in {symbol}:");
SmartLog($" Price: ${price:F2}");
SmartLog($" Quantity: {quantity}");
SmartLog($" Allocation: {_config.AllocationPerPosition:P1}");
SmartLog($" Position Value: ${PositionSizer.CalculatePositionValue(quantity, price):F2}");
// Use OrderExtensions for retry logic
var ticket = Algorithm.MarketOrderWithRetry(symbol, quantity, maxRetries: 3);
if (ticket?.WasSuccessful() == true)
{
_lastTradeTime[symbol] = time;
SmartLog($"[SUCCESS] Position taken in {symbol}: {ticket.GetStatusDescription()}");
}
else
{
SmartError($"[FAILED] Failed to take position in {symbol}: {ticket?.GetStatusDescription() ?? "null ticket"}");
}
}
catch (Exception ex)
{
SmartError($"Error taking position in {symbol}: {ex.Message}");
}
}
private void CheckExitConditions(Slice slice)
{
foreach (var position in Portfolio.Where(x => x.Value.Invested))
{
var symbol = position.Key;
var holding = position.Value;
// Skip if no price data
if (!slice.Bars.ContainsKey(symbol))
continue;
var currentPrice = slice.Bars[symbol].Close;
var unrealizedProfitPercent = holding.UnrealizedProfitPercent;
// Check profit target
if (unrealizedProfitPercent >= _config.ProfitTarget)
{
SmartLog($"Profit target hit for {symbol}: {unrealizedProfitPercent:P2} >= {_config.ProfitTarget:P2}");
ExitPosition(symbol, holding.Quantity, "Profit Target");
continue;
}
// Check stop loss
if (unrealizedProfitPercent <= _config.StopLoss)
{
SmartLog($"Stop loss hit for {symbol}: {unrealizedProfitPercent:P2} <= {_config.StopLoss:P2}");
ExitPosition(symbol, holding.Quantity, "Stop Loss");
continue;
}
// Check time-based exit
if (_lastTradeTime.ContainsKey(symbol))
{
var timeInTrade = slice.Time - _lastTradeTime[symbol];
if (timeInTrade.TotalDays >= _config.MaxDaysInTrade)
{
SmartLog($"Max time in trade reached for {symbol}: {timeInTrade.TotalDays:F1} days");
ExitPosition(symbol, holding.Quantity, "Time Limit");
continue;
}
}
}
}
private void ExitPosition(Symbol symbol, decimal quantity, string reason)
{
try
{
var ticket = Algorithm.MarketOrderWithRetry(symbol, -(int)quantity, maxRetries: 3);
if (ticket?.WasSuccessful() == true)
{
SmartLog($"[SUCCESS] Exited position in {symbol} ({reason}): {ticket.GetStatusDescription()}");
}
else
{
SmartError($"[FAILED] Failed to exit position in {symbol} ({reason}): {ticket?.GetStatusDescription() ?? "null ticket"}");
}
}
catch (Exception ex)
{
SmartError($"Error exiting position in {symbol}: {ex.Message}");
}
}
public void PrintSummary()
{
SmartLog("=== Universe Selection Strategy Summary ===");
SmartLog($"Final Portfolio Value: ${Portfolio.TotalPortfolioValue:F2}");
SmartLog($"Total Trades: {Algorithm.Transactions.GetOrders().Count()}");
SmartLog($"Active Symbols Tracked: {_activeSymbols.Count}");
SmartLog($"Symbols with Options: {_underlyingToOptions.Count}");
// Test final AssetManager capabilities
SmartLog("=== AssetManager Test Results ===");
var testSymbols = new[] { "SPY", "SPX", "QQQ", "VIX", "ES" };
foreach (var symbol in testSymbols)
{
SmartLog($"{symbol}: Index={AssetManager.IsIndex(symbol)}, Future={AssetManager.IsFuture(symbol)}, Equity={AssetManager.IsEquity(symbol)}");
}
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Securities;
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()
/// </summary>
private static readonly HashSet<string> Futures = new HashSet<string>
{
"ES", "NQ", "YM", "RTY", "CL", "GC", "SI"
};
/// <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().
/// </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>
/// <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)
{
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()");
addedSecurity = context.Algorithm.AddFuture(upperSymbol, resolution);
}
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");
optionSymbol = context.Algorithm.AddIndexOption(underlying.Symbol, "SPXW", resolution).Symbol;
}
else
{
// Other indices use standard index options
((dynamic)context.Logger).Info($"AssetManager: Adding index options for {symbol}");
context.Algorithm.AddIndexOption(underlying.Symbol);
optionSymbol = Symbol.CreateCanonicalOption(underlying.Symbol);
}
}
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];
((dynamic)context.Logger).Info($"AssetManager: [DEBUG] Option Security Details:");
((dynamic)context.Logger).Info($" Type: {optionSecurity.Type}");
((dynamic)context.Logger).Info($" Resolution: {optionSecurity.Subscriptions.GetHighestResolution()}");
((dynamic)context.Logger).Info($" Exchange: {optionSecurity.Exchange}");
((dynamic)context.Logger).Info($" IsMarketOpen: {optionSecurity.Exchange.ExchangeOpen}");
}
// [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");
((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 new string[Futures.Count];
}
}
}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}"
};
}
}
}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 if the selected strikes maintain strategy integrity
/// </summary>
public bool ValidateStrikeSpacing(StrikeRange strikes, string strategyType)
{
switch (strategyType.ToUpperInvariant())
{
case "IRONCONDOR":
// Ensure proper Iron Condor structure
return strikes.LongPutStrike < strikes.ShortPutStrike &&
strikes.ShortPutStrike < strikes.ATMStrike &&
strikes.ATMStrike < strikes.ShortCallStrike &&
strikes.ShortCallStrike < strikes.LongCallStrike;
case "STRANGLE":
case "STRADDLE":
// Ensure short strikes are on opposite sides of ATM
return strikes.ShortPutStrike < strikes.ATMStrike &&
strikes.ShortCallStrike > strikes.ATMStrike;
default:
return true;
}
}
/// <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
};
}
}
}#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; }
// 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()
{
// CRITICAL: Absolute minimal test - if this doesn't appear, our class isn't running
Log("HELLO WORLD - COREALGO INITIALIZE CALLED");
Error("HELLO WORLD - COREALGO INITIALIZE CALLED VIA ERROR");
Debug("HELLO WORLD - COREALGO INITIALIZE CALLED VIA DEBUG");
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);
Log("BASIC SETUP COMPLETED");
// Initialize smart logger with debug logging
Log("=== DEBUG: About to create QCLogger ===");
var logLevel = GetParameter("LogLevel", 3); // Default to Debug level (3 = Debug, 2 = Info)
Logger = new QCLogger<CoreAlgo>(this, logLevel);
Log($"=== DEBUG: Smart logger created: {Logger != null} ===");
// 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++;
Logger.Debug($"OnData call at {data.Time}");
Logger.Debug($"Strategy is null: {_strategy == null}");
if (_strategy != null)
{
Logger.Debug($"Strategy name: {_strategy.Name}");
Logger.Debug($"Strategy state: {_strategy.State}");
}
Logger.Debug($"Options chains: {data.OptionChains.Count}");
Logger.Debug($"Bars: {data.Bars.Count}");
if (_debugCallCount <= 5)
{
Logger.Debug($"Keys: {string.Join(", ", data.Keys.Select(k => k.ToString()))}");
}
// 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>
/// 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
/// Processes and outputs daily smart logs for better real-time visibility
/// </summary>
public override void OnEndOfDay(Symbol symbol)
{
// Debug message to verify OnEndOfDay is called
Logger?.Debug($"OnEndOfDay called for symbol {symbol}");
// Process and output the daily logs
// This will give us daily log output instead of only at algorithm end
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
security.SetDataNormalizationMode(DataNormalizationMode.Raw);
security.SetMarketPrice(GetLastKnownPrice(security));
// Type-specific configurations for index options
// Note: Option assignment model is handled at the strategy level
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using CoreAlgo.Architecture.Core.Attributes;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.Core.Services;
namespace TestUtils.ConfigGenerator
{
/// <summary>
/// Extracts configuration defaults from template classes using reflection.
/// Reads [StrategyParameter] attributes to build JSON configuration for test.sh
/// </summary>
public static class ConfigExtractor
{
/// <summary>
/// Extract template configuration defaults for a given strategy
/// </summary>
/// <param name="strategyName">Strategy name (e.g., MULTIASSETSPX)</param>
/// <returns>Dictionary of parameter names to default values</returns>
public static Dictionary<string, object> ExtractTemplateDefaults(string strategyName)
{
return ExtractTemplateDefaults(strategyName, null);
}
/// <summary>
/// Extract template configuration defaults for a given strategy with optional config variant
/// </summary>
/// <param name="strategyName">Strategy name (e.g., MULTIASSETIRONCONDOR)</param>
/// <param name="configVariant">Config variant name (e.g., MultiAssetSPX, MultiAssetEquity)</param>
/// <returns>Dictionary of parameter names to default values</returns>
public static Dictionary<string, object> ExtractTemplateDefaults(string strategyName, string configVariant)
{
var configType = GetConfigTypeForStrategy(strategyName, configVariant);
if (configType == null)
{
var errorMessage = $"No configuration type found for strategy: {strategyName}";
if (!string.IsNullOrEmpty(configVariant))
{
errorMessage += $" with config variant: {configVariant}";
}
throw new ArgumentException(errorMessage);
}
Console.Error.WriteLine($"Using configuration type: {configType.Name}");
// Create instance of the config class
var configInstance = Activator.CreateInstance(configType);
var parameters = new Dictionary<string, object>();
// Extract all properties with [StrategyParameter] attributes
var properties = configType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in properties)
{
var attribute = property.GetCustomAttribute<StrategyParameterAttribute>();
if (attribute != null)
{
try
{
var value = property.GetValue(configInstance);
// Convert value to JSON-serializable format
var jsonValue = ConvertToJsonValue(value, property.PropertyType);
parameters[attribute.Name] = jsonValue;
Console.Error.WriteLine($" {attribute.Name} = {jsonValue} (from {property.Name})");
}
catch (Exception ex)
{
Console.Error.WriteLine($" Warning: Could not extract {property.Name}: {ex.Message}");
}
}
}
// Always ensure Strategy parameter is set
parameters["Strategy"] = strategyName;
return parameters;
}
/// <summary>
/// Map strategy names to their corresponding configuration types using discovery
/// </summary>
private static Type GetConfigTypeForStrategy(string strategyName)
{
return GetConfigTypeForStrategy(strategyName, null);
}
/// <summary>
/// Map strategy names to their corresponding configuration types with optional config variant
/// </summary>
/// <param name="strategyName">Strategy name (e.g., MULTIASSETIRONCONDOR)</param>
/// <param name="configVariant">Config variant name (e.g., MultiAssetSPX, MultiAssetEquity)</param>
/// <returns>Configuration Type</returns>
private static Type GetConfigTypeForStrategy(string strategyName, string configVariant)
{
// If config variant is specified, look for exact config class name
if (!string.IsNullOrEmpty(configVariant))
{
var configClassName = $"{configVariant}Config";
Console.Error.WriteLine($"Looking for config variant: {configClassName}");
// Look in all loaded assemblies, not just the executing assembly
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
Type configType = null;
foreach (var assembly in assemblies)
{
try
{
configType = assembly.GetTypes()
.FirstOrDefault(t => t.Name == configClassName &&
typeof(StrategyConfig).IsAssignableFrom(t));
if (configType != null)
break;
}
catch (ReflectionTypeLoadException)
{
// Ignore assemblies that can't be loaded
continue;
}
}
if (configType != null)
{
Console.Error.WriteLine($"Found config variant: {configType.FullName}");
return configType;
}
else
{
Console.Error.WriteLine($"Config variant not found: {configClassName}");
return null;
}
}
// Fall back to standard strategy-based discovery
return StrategyDiscovery.GetConfigType(strategyName);
}
/// <summary>
/// Convert property values to JSON-serializable format
/// </summary>
private static object ConvertToJsonValue(object value, Type propertyType)
{
if (value == null)
return null;
// Handle arrays (especially string arrays like Symbols)
if (propertyType.IsArray && propertyType.GetElementType() == typeof(string))
{
var stringArray = (string[])value;
return string.Join(",", stringArray);
}
// Handle TimeSpan - convert to string format
if (propertyType == typeof(TimeSpan))
{
var timeSpan = (TimeSpan)value;
return timeSpan.ToString(@"hh\:mm\:ss");
}
// Handle DateTime - convert to string format
if (propertyType == typeof(DateTime))
{
var dateTime = (DateTime)value;
return dateTime.ToString("yyyy-MM-dd");
}
// Handle decimals - ensure proper formatting
if (propertyType == typeof(decimal))
{
return value;
}
// Handle other numeric types
if (propertyType == typeof(int) || propertyType == typeof(bool))
{
return value;
}
// Default: convert to string
return value.ToString();
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using CoreAlgo.Architecture.Core.Services;
namespace TestUtils.ConfigGenerator
{
/// <summary>
/// Console tool to extract template configuration defaults for test.sh
/// Usage: dotnet run --project TestUtils/ConfigGenerator -- MULTIASSETSPX
/// Usage: dotnet run --project TestUtils/ConfigGenerator -- MULTIASSETIRONCONDOR --config=MultiAssetSPX
/// Usage: dotnet run --project TestUtils/ConfigGenerator -- --list-strategies
/// Output: JSON configuration parameters from template class or list of strategies
/// </summary>
class Program
{
static void Main(string[] args)
{
if (args.Length == 0)
{
Console.Error.WriteLine("Usage: ConfigGenerator <STRATEGY_NAME> [--config=CONFIG_VARIANT]");
Console.Error.WriteLine(" ConfigGenerator --list-strategies");
Console.Error.WriteLine("Examples:");
Console.Error.WriteLine(" ConfigGenerator MULTIASSETSPX");
Console.Error.WriteLine(" ConfigGenerator MULTIASSETIRONCONDOR --config=MultiAssetSPX");
Console.Error.WriteLine(" ConfigGenerator MULTIASSETIRONCONDOR --config=MultiAssetEquity");
Environment.Exit(1);
}
// Handle --list-strategies option
if (args[0] == "--list-strategies")
{
try
{
var strategies = StrategyDiscovery.GetAllStrategyNames().OrderBy(s => s).ToList();
foreach (var strategy in strategies)
{
Console.WriteLine(strategy.ToUpperInvariant());
}
Environment.Exit(0);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error discovering strategies: {ex.Message}");
Environment.Exit(1);
}
}
try
{
var strategy = args[0].ToUpperInvariant();
string configVariant = null;
// Parse optional --config parameter
if (args.Length > 1)
{
for (int i = 1; i < args.Length; i++)
{
if (args[i].StartsWith("--config="))
{
configVariant = args[i].Substring("--config=".Length);
break;
}
}
}
if (!string.IsNullOrEmpty(configVariant))
{
Console.Error.WriteLine($"Extracting configuration for strategy: {strategy} with config variant: {configVariant}");
}
else
{
Console.Error.WriteLine($"Extracting configuration for strategy: {strategy}");
}
var parameters = ConfigExtractor.ExtractTemplateDefaults(strategy, configVariant);
if (parameters.Count == 0)
{
Console.Error.WriteLine($"No configuration parameters found for strategy: {strategy}" +
(!string.IsNullOrEmpty(configVariant) ? $" with config variant: {configVariant}" : ""));
Environment.Exit(1);
}
// Output clean JSON for shell consumption (stdout only)
var json = JsonConvert.SerializeObject(parameters, Formatting.None);
Console.WriteLine(json);
Console.Error.WriteLine($"Successfully extracted {parameters.Count} parameters");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error extracting config for {args[0]}: {ex.Message}");
Console.Error.WriteLine($"Stack trace: {ex.StackTrace}");
Environment.Exit(1);
}
}
}
}using System;
using System.Linq;
using NUnit.Framework;
using QuantConnect;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Templates;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Tests
{
[TestFixture]
public class CashSecuredPutTemplateTests
{
private CashSecuredPutTemplate _strategy;
[SetUp]
public void SetUp()
{
_strategy = new CashSecuredPutTemplate();
}
[Test]
public void Name_ReturnsCorrectValue()
{
// Act & Assert
Assert.That(_strategy.Name, Is.EqualTo("Cash Secured Put"));
}
[Test]
public void Description_ReturnsCorrectValue()
{
// Act & Assert
Assert.That(_strategy.Description, Is.EqualTo("Income strategy that generates premium by selling put options backed by cash"));
}
[Test]
public void CashSecuredPutConfig_DefaultValues_AreCorrect()
{
// Arrange & Act
var config = new CashSecuredPutConfig();
// Assert
Assert.That(config.UnderlyingSymbol, Is.EqualTo("SPY"));
Assert.That(config.PutStrikeOffset, Is.EqualTo(0.02m));
Assert.That(config.MinDaysToExpiration, Is.EqualTo(1));
Assert.That(config.MaxDaysToExpiration, Is.EqualTo(60));
Assert.That(config.MinimumPremium, Is.EqualTo(0.25m));
Assert.That(config.MaxActivePositions, Is.EqualTo(2));
Assert.That(config.AcceptAssignment, Is.True);
}
[Test]
public void CashSecuredPutConfig_Validate_WithValidValues_ReturnsNoErrors()
{
// Arrange
var config = new CashSecuredPutConfig
{
PutStrikeOffset = 0.03m,
MinDaysToExpiration = 7,
MaxDaysToExpiration = 30,
MinimumPremium = 0.50m,
MaxActivePositions = 3
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.EqualTo(0));
}
[Test]
public void CashSecuredPutConfig_Validate_WithNegativeStrikeOffset_ReturnsError()
{
// Arrange
var config = new CashSecuredPutConfig
{
PutStrikeOffset = -0.01m // Invalid negative offset
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("PutStrikeOffset")), Is.True);
}
[Test]
public void CashSecuredPutConfig_Validate_WithInvalidDTERange_ReturnsError()
{
// Arrange
var config = new CashSecuredPutConfig
{
MinDaysToExpiration = 45,
MaxDaysToExpiration = 7 // Max < Min - invalid
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("DTE")), Is.True);
}
[Test]
public void CashSecuredPutConfig_Validate_WithNegativePremium_ReturnsError()
{
// Arrange
var config = new CashSecuredPutConfig
{
MinimumPremium = -0.10m // Invalid negative premium
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("MinimumPremium")), Is.True);
}
[Test]
public void CashSecuredPutConfig_Validate_WithInvalidMaxPositions_ReturnsError()
{
// Arrange
var config = new CashSecuredPutConfig
{
MaxActivePositions = 0 // Must be positive
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("MaxActivePositions")), Is.True);
}
[Test]
public void CashSecuredPutConfig_Validate_WithExcessiveStrikeOffset_ReturnsError()
{
// Arrange
var config = new CashSecuredPutConfig
{
PutStrikeOffset = 0.25m // 25% offset - too high
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("PutStrikeOffset")), Is.True);
}
[Test]
public void CashSecuredPutConfig_ToString_ReturnsFormattedString()
{
// Arrange
var config = new CashSecuredPutConfig();
// Act
var result = config.ToString();
// Assert
Assert.IsNotNull(result);
Assert.That(result.Contains("CashSecuredPut"), Is.True);
Assert.That(result.Contains("SPY"), Is.True);
Assert.That(result.Contains("2.0%"), Is.True);
}
[Test]
public void CashSecuredPutConfig_HasStrategyParameterAttributes()
{
// Arrange
var config = new CashSecuredPutConfig();
var type = config.GetType();
// Act & Assert - Check that properties have StrategyParameterAttribute
var putOffsetProperty = type.GetProperty("PutStrikeOffset");
Assert.IsNotNull(putOffsetProperty);
var minDteProperty = type.GetProperty("MinDaysToExpiration");
Assert.IsNotNull(minDteProperty);
var maxDteProperty = type.GetProperty("MaxDaysToExpiration");
Assert.IsNotNull(maxDteProperty);
var minPremiumProperty = type.GetProperty("MinimumPremium");
Assert.IsNotNull(minPremiumProperty);
}
[Test]
public void CashSecuredPutConfig_LoadFromParameters_UpdatesConfigCorrectly()
{
// This test would require a mock QCAlgorithm instance
// For now, we'll test the parameter validation and structure
// Arrange
var config = new CashSecuredPutConfig();
var originalOffset = config.PutStrikeOffset;
// Act - manually set a new value
config.PutStrikeOffset = 0.05m;
// Assert
Assert.That(config.PutStrikeOffset, Is.Not.EqualTo(originalOffset));
Assert.That(config.PutStrikeOffset, Is.EqualTo(0.05m));
}
[Test]
public void CashSecuredPutConfig_EdgeCases_HandleCorrectly()
{
// Test boundary values
var config = new CashSecuredPutConfig
{
PutStrikeOffset = 0.01m, // Minimum reasonable value
MinDaysToExpiration = 1, // Minimum DTE
MaxDaysToExpiration = 365, // Maximum reasonable DTE
MinimumPremium = 0.01m, // Very small premium
MaxActivePositions = 1 // Single position
};
var errors = config.Validate();
Assert.That(errors.Length, Is.EqualTo(0), "Boundary values should be valid");
}
[Test]
public void CashSecuredPutConfig_MultipleValidationErrors_ReturnsAllErrors()
{
// Arrange
var config = new CashSecuredPutConfig
{
PutStrikeOffset = -0.01m, // Invalid
MinDaysToExpiration = 90, // Invalid (> Max)
MaxDaysToExpiration = 7, // Invalid (< Min)
MinimumPremium = -0.10m, // Invalid
MaxActivePositions = 0 // Invalid
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(2)); // Multiple validation errors
}
[Test]
public void CashSecuredPutConfig_AcceptAssignment_BehaviorCorrect()
{
// Arrange
var config = new CashSecuredPutConfig();
// Act & Assert - Test default behavior
Assert.That(config.AcceptAssignment, Is.True, "Should accept assignment by default");
// Act & Assert - Test setting to false
config.AcceptAssignment = false;
Assert.That(config.AcceptAssignment, Is.False, "Should be able to disable assignment acceptance");
}
[Test]
public void CashSecuredPutConfig_AggressiveDefaults_AreOptimal()
{
// Arrange & Act
var config = new CashSecuredPutConfig();
// Assert - Verify aggressive defaults that led to 14,912 trades
Assert.That(config.PutStrikeOffset, Is.EqualTo(0.02m), "Strike offset should be aggressive 2%");
Assert.That(config.MinimumPremium, Is.EqualTo(0.25m), "Minimum premium should be low for more opportunities");
Assert.That(config.MinDaysToExpiration, Is.EqualTo(1), "Min DTE should be 1 for maximum opportunities");
Assert.That(config.MaxDaysToExpiration, Is.EqualTo(60), "Max DTE should be 60 for broader range");
Assert.That(config.MaxActivePositions, Is.EqualTo(2), "Should allow multiple positions");
}
[Test]
public void CashSecuredPutConfig_ComparedToOtherStrategies_HasCorrectRiskProfile()
{
// Arrange
var cspConfig = new CashSecuredPutConfig();
var ccConfig = new CoveredCallConfig();
// Act & Assert - CSP should be more aggressive for income generation
Assert.That(cspConfig.PutStrikeOffset, Is.LessThanOrEqualTo(ccConfig.CallStrikeOffset),
"CSP offset should be equal or smaller for more aggressive income generation");
Assert.That(cspConfig.MaxActivePositions, Is.LessThanOrEqualTo(ccConfig.MaxActivePositions),
"CSP currently uses conservative position limits");
Assert.That(cspConfig.MinimumPremium, Is.LessThan(ccConfig.MinimumPremium),
"CSP should accept smaller premiums for more opportunities");
}
}
}using System;
using Microsoft.CSharp.RuntimeBinder;
using NUnit.Framework;
namespace CoreAlgo.Tests
{
[TestFixture]
public class ContextInjectionTests
{
[Test]
public void SetContext_WithValidLogger_ShouldSetLoggerProperty()
{
// Arrange
var strategy = new TestStrategy();
var mockLogger = new MockLogger();
// Act
strategy.SetContext(mockLogger);
// Assert
Assert.That(strategy.Logger, Is.EqualTo(mockLogger));
}
[Test]
public void SetContext_WithNullLogger_ShouldThrowArgumentNullException()
{
// Arrange
var strategy = new TestStrategy();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => strategy.SetContext(null));
}
[Test]
public void DynamicLoggerCall_WithValidLogger_ShouldWork()
{
// Arrange
var strategy = new TestStrategy();
var mockLogger = new MockLogger();
strategy.SetContext(mockLogger);
// Act & Assert - Test dynamic logger call
Assert.DoesNotThrow(() => strategy.TestDynamicLoggerCall("Test message"));
Assert.That(mockLogger.LastMessage, Is.EqualTo("Test message"));
}
[Test]
public void DynamicLoggerCall_WithNullLogger_ShouldThrowRuntimeBinderException()
{
// Arrange
var strategy = new TestStrategy();
// Note: Don't call SetContext, so Logger remains null
// Act & Assert - This simulates the exact error from the backtest logs
var ex = Assert.Throws<RuntimeBinderException>(() => strategy.TestDynamicLoggerCall("Test message"));
Assert.That(ex.Message, Does.Contain("Cannot perform runtime binding on a null reference"));
}
[Test]
public void CorrectSequence_SetContextThenInitialize_ShouldWork()
{
// This test simulates the correct sequence we implemented in Main.cs
// Arrange
var strategy = new TestStrategy();
var mockLogger = new MockLogger();
// Act 1: Set context FIRST (like Main.cs does now)
strategy.SetContext(mockLogger);
// Act 2: Then initialize (which calls dynamic logger methods)
Assert.DoesNotThrow(() => strategy.SimulateInitializeWithLogging());
// Assert
Assert.That(mockLogger.LastMessage, Is.EqualTo("Initializing strategy"));
}
[Test]
public void IncorrectSequence_InitializeWithoutSetContext_ShouldThrow()
{
// This test simulates the broken sequence that caused the backtest error
// Arrange
var strategy = new TestStrategy();
// Note: Skip SetContext call
// Act & Assert: Initialize without setting context should fail
var ex = Assert.Throws<RuntimeBinderException>(() => strategy.SimulateInitializeWithLogging());
Assert.That(ex.Message, Does.Contain("Cannot perform runtime binding on a null reference"));
}
}
// Test strategy that mimics the pattern in SimpleBaseStrategy
public class TestStrategy
{
public object Logger { get; private set; }
public void SetContext(object logger)
{
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
// This simulates the dynamic logger calls like SmartLog() in SimpleBaseStrategy
public void TestDynamicLoggerCall(string message)
{
// This is the exact pattern used in SimpleBaseStrategy for dynamic logger calls
((dynamic)Logger).Info(message);
}
// This simulates OnInitialize() which calls SmartLog during strategy initialization
public void SimulateInitializeWithLogging()
{
// This simulates what happens in MultiAssetIronCondorTemplate.OnInitialize()
// It calls SmartLog() which uses dynamic Logger
((dynamic)Logger).Info("Initializing strategy");
}
}
// Mock logger that implements the expected interface
public class MockLogger
{
public string LastMessage { get; private set; }
public void Info(string message)
{
LastMessage = message;
}
public void Debug(string message)
{
LastMessage = message;
}
public void Error(string message)
{
LastMessage = message;
}
public void Warning(string message)
{
LastMessage = message;
}
}
}using System;
using System.Linq;
using NUnit.Framework;
using QuantConnect;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Templates;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Tests
{
[TestFixture]
public class CoveredCallTemplateTests
{
private CoveredCallTemplate _strategy;
[SetUp]
public void SetUp()
{
_strategy = new CoveredCallTemplate();
}
[Test]
public void Name_ReturnsCorrectValue()
{
// Act & Assert
Assert.That(_strategy.Name, Is.EqualTo("Covered Call"));
}
[Test]
public void Description_ReturnsCorrectValue()
{
// Act & Assert
Assert.That(_strategy.Description, Is.EqualTo("Income strategy that generates premium by selling call options against owned stock"));
}
[Test]
public void CoveredCallConfig_DefaultValues_AreCorrect()
{
// Arrange & Act
var config = new CoveredCallConfig();
// Assert
Assert.That(config.UnderlyingSymbol, Is.EqualTo("SPY"));
Assert.That(config.CallStrikeOffset, Is.EqualTo(0.02m));
Assert.That(config.MinDaysToExpiration, Is.EqualTo(7));
Assert.That(config.MaxDaysToExpiration, Is.EqualTo(45));
Assert.That(config.MinimumPremium, Is.EqualTo(0.50m));
Assert.That(config.MaxActivePositions, Is.EqualTo(3));
Assert.That(config.SharesPerContract, Is.EqualTo(100));
Assert.That(config.BuySharesIfNeeded, Is.True);
}
[Test]
public void CoveredCallConfig_Validate_WithValidValues_ReturnsNoErrors()
{
// Arrange
var config = new CoveredCallConfig
{
CallStrikeOffset = 0.02m,
MinDaysToExpiration = 5,
MaxDaysToExpiration = 30,
MinimumPremium = 0.25m,
MaxActivePositions = 2
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.EqualTo(0));
}
[Test]
public void CoveredCallConfig_Validate_WithNegativeStrikeOffset_ReturnsError()
{
// Arrange
var config = new CoveredCallConfig
{
CallStrikeOffset = -0.01m // Invalid negative offset
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("CallStrikeOffset")), Is.True);
}
[Test]
public void CoveredCallConfig_Validate_WithInvalidDTERange_ReturnsError()
{
// Arrange
var config = new CoveredCallConfig
{
MinDaysToExpiration = 30,
MaxDaysToExpiration = 7 // Max < Min - invalid
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("DTE")), Is.True);
}
[Test]
public void CoveredCallConfig_Validate_WithNegativePremium_ReturnsError()
{
// Arrange
var config = new CoveredCallConfig
{
MinimumPremium = -0.10m // Invalid negative premium
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("MinimumPremium")), Is.True);
}
[Test]
public void CoveredCallConfig_Validate_WithInvalidSharesPerContract_ReturnsError()
{
// Arrange
var config = new CoveredCallConfig
{
SharesPerContract = 50 // Must be 100 for standard options
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("SharesPerContract")), Is.True);
}
[Test]
public void CoveredCallConfig_Validate_WithExcessiveStrikeOffset_ReturnsError()
{
// Arrange
var config = new CoveredCallConfig
{
CallStrikeOffset = 0.25m // 25% - too high
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("CallStrikeOffset")), Is.True);
}
[Test]
public void CoveredCallConfig_ToString_ReturnsFormattedString()
{
// Arrange
var config = new CoveredCallConfig();
// Act
var result = config.ToString();
// Assert
Assert.IsNotNull(result);
Assert.That(result.Contains("CoveredCall"), Is.True);
Assert.That(result.Contains("SPY"), Is.True);
Assert.That(result.Contains("2.0%"), Is.True);
}
[Test]
public void CoveredCallConfig_HasStrategyParameterAttributes()
{
// Arrange
var config = new CoveredCallConfig();
var type = config.GetType();
// Act & Assert - Check that properties have StrategyParameterAttribute
var callOffsetProperty = type.GetProperty("CallStrikeOffset");
Assert.IsNotNull(callOffsetProperty);
var minDteProperty = type.GetProperty("MinDaysToExpiration");
Assert.IsNotNull(minDteProperty);
var maxDteProperty = type.GetProperty("MaxDaysToExpiration");
Assert.IsNotNull(maxDteProperty);
var minPremiumProperty = type.GetProperty("MinimumPremium");
Assert.IsNotNull(minPremiumProperty);
}
[Test]
public void CoveredCallConfig_LoadFromParameters_UpdatesConfigCorrectly()
{
// This test would require a mock QCAlgorithm instance
// For now, we'll test the parameter validation and structure
// Arrange
var config = new CoveredCallConfig();
var originalOffset = config.CallStrikeOffset;
// Act - manually set a new value
config.CallStrikeOffset = 0.05m;
// Assert
Assert.That(config.CallStrikeOffset, Is.Not.EqualTo(originalOffset));
Assert.That(config.CallStrikeOffset, Is.EqualTo(0.05m));
}
[Test]
public void CoveredCallConfig_EdgeCases_HandleCorrectly()
{
// Test boundary values
var config = new CoveredCallConfig
{
CallStrikeOffset = 0.01m, // Minimum reasonable value
MinDaysToExpiration = 1, // Minimum DTE
MaxDaysToExpiration = 365, // Maximum reasonable DTE
MinimumPremium = 0.01m, // Very small premium
MaxActivePositions = 1 // Single position
};
var errors = config.Validate();
Assert.That(errors.Length, Is.EqualTo(0), "Boundary values should be valid");
}
[Test]
public void CoveredCallConfig_MultipleValidationErrors_ReturnsAllErrors()
{
// Arrange
var config = new CoveredCallConfig
{
CallStrikeOffset = -0.01m, // Invalid
MinDaysToExpiration = 50, // Invalid (> Max)
MaxDaysToExpiration = 7, // Invalid (< Min)
MinimumPremium = -0.10m, // Invalid
SharesPerContract = 50 // Invalid - must be 100
};
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(2)); // Multiple validation errors
}
}
}using NUnit.Framework;
using System.Linq;
using CoreAlgo.Architecture.Core.Models;
using CoreAlgo.Architecture.Core.Execution;
namespace CoreAlgo.Tests
{
/// <summary>
/// Tests for SmartPricing configuration integration with StrategyConfig
/// </summary>
[TestFixture]
public class SmartPricingConfigurationTests
{
[Test]
public void StrategyConfig_SmartPricingMode_DefaultValue_IsNormal()
{
// Arrange
var config = new TestStrategyConfig();
// Act & Assert
Assert.That(config.SmartPricingMode, Is.EqualTo("Normal"));
}
[Test]
public void StrategyConfig_CreateSmartPricingEngine_Normal_ReturnsNormalEngine()
{
// Arrange
var config = new TestStrategyConfig { SmartPricingMode = "Normal" };
// Act
var engine = config.CreateSmartPricingEngine();
// Assert
Assert.IsNotNull(engine);
Assert.That(engine.Mode, Is.EqualTo(SmartPricingMode.Normal));
Assert.IsInstanceOf<NormalPricingStrategy>(engine);
}
[Test]
public void StrategyConfig_CreateSmartPricingEngine_Fast_ReturnsFastEngine()
{
// Arrange
var config = new TestStrategyConfig { SmartPricingMode = "Fast" };
// Act
var engine = config.CreateSmartPricingEngine();
// Assert
Assert.IsNotNull(engine);
Assert.That(engine.Mode, Is.EqualTo(SmartPricingMode.Fast));
Assert.IsInstanceOf<FastPricingStrategy>(engine);
}
[Test]
public void StrategyConfig_CreateSmartPricingEngine_Patient_ReturnsPatientEngine()
{
// Arrange
var config = new TestStrategyConfig { SmartPricingMode = "Patient" };
// Act
var engine = config.CreateSmartPricingEngine();
// Assert
Assert.IsNotNull(engine);
Assert.That(engine.Mode, Is.EqualTo(SmartPricingMode.Patient));
Assert.IsInstanceOf<PatientPricingStrategy>(engine);
}
[Test]
public void StrategyConfig_CreateSmartPricingEngine_Off_ReturnsNull()
{
// Arrange
var config = new TestStrategyConfig { SmartPricingMode = "Off" };
// Act
var engine = config.CreateSmartPricingEngine();
// Assert
Assert.IsNull(engine);
}
[Test]
public void StrategyConfig_CreateSmartPricingEngine_Disabled_ReturnsNull()
{
// Arrange
var config = new TestStrategyConfig { SmartPricingMode = "Disabled" };
// Act
var engine = config.CreateSmartPricingEngine();
// Assert
Assert.IsNull(engine);
}
[Test]
public void StrategyConfig_CreateSmartPricingEngine_CaseInsensitive_Works()
{
// Arrange
var config = new TestStrategyConfig { SmartPricingMode = "NORMAL" };
// Act
var engine = config.CreateSmartPricingEngine();
// Assert
Assert.IsNotNull(engine);
Assert.That(engine.Mode, Is.EqualTo(SmartPricingMode.Normal));
}
[Test]
public void StrategyConfig_Validate_InvalidSmartPricingMode_ReturnsError()
{
// Arrange
var config = new TestStrategyConfig { SmartPricingMode = "InvalidMode" };
// Act
var errors = config.Validate();
// Assert
Assert.That(errors.Length, Is.GreaterThan(0));
Assert.That(errors.Any(e => e.Contains("Invalid SmartPricingMode")), Is.True);
}
[Test]
public void StrategyConfig_Validate_ValidSmartPricingModes_NoErrors()
{
// Test all valid modes
var validModes = new[] { "Normal", "Fast", "Patient", "Off", "Disabled", "False" };
foreach (var mode in validModes)
{
// Arrange
var config = new TestStrategyConfig { SmartPricingMode = mode };
// Act
var errors = config.Validate();
// Assert
var smartPricingErrors = errors.Where(e => e.Contains("SmartPricingMode")).ToArray();
Assert.That(smartPricingErrors.Length, Is.EqualTo(0),
$"Mode '{mode}' should be valid but got errors: {string.Join(", ", smartPricingErrors)}");
}
}
[Test]
public void IronCondorConfig_InheritsSmartPricingMode()
{
// Arrange
var config = new IronCondorConfig();
// Act & Assert
Assert.That(config.SmartPricingMode, Is.EqualTo("Normal"));
// Test that it can create engine
var engine = config.CreateSmartPricingEngine();
Assert.IsNotNull(engine);
Assert.That(engine.Mode, Is.EqualTo(SmartPricingMode.Normal));
}
[Test]
public void CoveredCallConfig_InheritsSmartPricingMode()
{
// Arrange
var config = new CoveredCallConfig();
// Act & Assert
Assert.That(config.SmartPricingMode, Is.EqualTo("Normal"));
// Test that it can create engine
var engine = config.CreateSmartPricingEngine();
Assert.IsNotNull(engine);
Assert.That(engine.Mode, Is.EqualTo(SmartPricingMode.Normal));
}
/// <summary>
/// Test strategy config for testing purposes
/// </summary>
private class TestStrategyConfig : StrategyConfig
{
// Empty implementation for testing base class functionality
}
}
}using NUnit.Framework;
using System;
using CoreAlgo.Architecture.Core.Execution;
namespace CoreAlgo.Tests
{
/// <summary>
/// Unit tests for SmartPricing execution functionality
/// </summary>
[TestFixture]
public class SmartPricingTests
{
[Test]
public void SmartPricingEngineFactory_Create_Normal_ReturnsNormalStrategy()
{
// Arrange & Act
var engine = SmartPricingEngineFactory.Create(SmartPricingMode.Normal);
// Assert
Assert.IsNotNull(engine);
Assert.That(engine.Mode, Is.EqualTo(SmartPricingMode.Normal));
Assert.IsInstanceOf<NormalPricingStrategy>(engine);
}
[Test]
public void SmartPricingEngineFactory_Create_Fast_ReturnsFastStrategy()
{
// Arrange & Act
var engine = SmartPricingEngineFactory.Create(SmartPricingMode.Fast);
// Assert
Assert.IsNotNull(engine);
Assert.That(engine.Mode, Is.EqualTo(SmartPricingMode.Fast));
Assert.IsInstanceOf<FastPricingStrategy>(engine);
}
[Test]
public void SmartPricingEngineFactory_Create_Patient_ReturnsPatientStrategy()
{
// Arrange & Act
var engine = SmartPricingEngineFactory.Create(SmartPricingMode.Patient);
// Assert
Assert.IsNotNull(engine);
Assert.That(engine.Mode, Is.EqualTo(SmartPricingMode.Patient));
Assert.IsInstanceOf<PatientPricingStrategy>(engine);
}
[Test]
public void SmartPricingEngineFactory_Create_Off_ThrowsException()
{
// Arrange & Act & Assert
Assert.Throws<InvalidOperationException>(() =>
SmartPricingEngineFactory.Create(SmartPricingMode.Off));
}
[Test]
public void SmartPricingEngineFactory_CreateFromString_ValidModes_ReturnsCorrectEngine()
{
// Test case insensitive string parsing
var normalEngine = SmartPricingEngineFactory.Create("normal");
var fastEngine = SmartPricingEngineFactory.Create("FAST");
var patientEngine = SmartPricingEngineFactory.Create("Patient");
Assert.That(normalEngine.Mode, Is.EqualTo(SmartPricingMode.Normal));
Assert.That(fastEngine.Mode, Is.EqualTo(SmartPricingMode.Fast));
Assert.That(patientEngine.Mode, Is.EqualTo(SmartPricingMode.Patient));
}
[Test]
public void SmartPricingEngineFactory_ParseMode_InvalidString_ReturnsNormalDefault()
{
// Arrange & Act
var mode = SmartPricingEngineFactory.ParseMode("InvalidMode");
// Assert
Assert.That(mode, Is.EqualTo(SmartPricingMode.Normal));
}
[Test]
public void SmartPricingEngineFactory_ParseMode_OffModes_ReturnsOff()
{
Assert.That(SmartPricingEngineFactory.ParseMode("OFF"), Is.EqualTo(SmartPricingMode.Off));
Assert.That(SmartPricingEngineFactory.ParseMode("disabled"), Is.EqualTo(SmartPricingMode.Off));
Assert.That(SmartPricingEngineFactory.ParseMode("false"), Is.EqualTo(SmartPricingMode.Off));
}
[Test]
public void NormalPricingStrategy_CalculateInitialPrice_ReturnsMidSpread()
{
// Arrange
var strategy = new NormalPricingStrategy();
var quote = new Quote(10.0m, 10.50m); // Bid: $10.00, Ask: $10.50, Mid: $10.25
// Act
var initialPrice = strategy.CalculateInitialPrice(quote, OrderDirection.Buy);
// Assert
Assert.That(initialPrice, Is.EqualTo(10.25m));
}
[Test]
public void NormalPricingStrategy_CalculateNextPrice_ProgressesTowardAsk()
{
// Arrange
var strategy = new NormalPricingStrategy();
var quote = new Quote(10.0m, 10.50m); // Spread: $0.50
var currentPrice = 10.25m; // Mid-spread
// Act - Second attempt should move 25% toward ask
var nextPrice = strategy.CalculateNextPrice(currentPrice, quote, OrderDirection.Buy, 2);
// Assert
Assert.IsNotNull(nextPrice);
// Should be mid + (halfSpread * 0.25) = 10.25 + (0.25 * 0.25) = 10.3125
Assert.That(nextPrice.Value, Is.EqualTo(10.3125m));
}
[Test]
public void NormalPricingStrategy_CalculateNextPrice_ProgressesTowardBid()
{
// Arrange
var strategy = new NormalPricingStrategy();
var quote = new Quote(10.0m, 10.50m); // Spread: $0.50
var currentPrice = 10.25m; // Mid-spread
// Act - Second attempt should move 25% toward bid
var nextPrice = strategy.CalculateNextPrice(currentPrice, quote, OrderDirection.Sell, 2);
// Assert
Assert.IsNotNull(nextPrice);
// Should be mid - (halfSpread * 0.25) = 10.25 - (0.25 * 0.25) = 10.1875
Assert.That(nextPrice.Value, Is.EqualTo(10.1875m));
}
[Test]
public void NormalPricingStrategy_CalculateNextPrice_ExceedsMaxAttempts_ReturnsNull()
{
// Arrange
var strategy = new NormalPricingStrategy();
var quote = new Quote(10.0m, 10.50m);
var currentPrice = 10.25m;
// Act - Attempt number exceeds max attempts (4)
var nextPrice = strategy.CalculateNextPrice(currentPrice, quote, OrderDirection.Buy, 5);
// Assert
Assert.IsNull(nextPrice);
}
[Test]
public void FastPricingStrategy_GetPricingInterval_Returns5Seconds()
{
// Arrange
var strategy = new FastPricingStrategy();
// Act
var interval = strategy.GetPricingInterval();
// Assert
Assert.That(interval, Is.EqualTo(TimeSpan.FromSeconds(5)));
}
[Test]
public void PatientPricingStrategy_GetPricingInterval_Returns20Seconds()
{
// Arrange
var strategy = new PatientPricingStrategy();
// Act
var interval = strategy.GetPricingInterval();
// Assert
Assert.That(interval, Is.EqualTo(TimeSpan.FromSeconds(20)));
}
[Test]
public void NormalPricingStrategy_GetMaxAttempts_Returns4()
{
// Arrange
var strategy = new NormalPricingStrategy();
// Act
var maxAttempts = strategy.GetMaxAttempts();
// Assert
Assert.That(maxAttempts, Is.EqualTo(4));
}
[Test]
public void FastPricingStrategy_GetMaxAttempts_Returns3()
{
// Arrange
var strategy = new FastPricingStrategy();
// Act
var maxAttempts = strategy.GetMaxAttempts();
// Assert
Assert.That(maxAttempts, Is.EqualTo(3));
}
[Test]
public void PatientPricingStrategy_GetMaxAttempts_Returns5()
{
// Arrange
var strategy = new PatientPricingStrategy();
// Act
var maxAttempts = strategy.GetMaxAttempts();
// Assert
Assert.That(maxAttempts, Is.EqualTo(5));
}
[Test]
public void PricingStrategy_ShouldAttemptPricing_NarrowSpread_ReturnsFalse()
{
// Arrange
var strategy = new NormalPricingStrategy();
var quote = new Quote(10.0m, 10.02m); // Very narrow spread: $0.02
// Act
var shouldAttempt = strategy.ShouldAttemptPricing(quote, OrderDirection.Buy);
// Assert
Assert.That(shouldAttempt, Is.False);
}
[Test]
public void PricingStrategy_ShouldAttemptPricing_WideSpread_ReturnsFalse()
{
// Arrange
var strategy = new NormalPricingStrategy();
var quote = new Quote(10.0m, 20.0m); // Very wide spread: $10.00 (100% of mid-price)
// Act
var shouldAttempt = strategy.ShouldAttemptPricing(quote, OrderDirection.Buy);
// Assert
Assert.That(shouldAttempt, Is.False);
}
[Test]
public void PricingStrategy_ShouldAttemptPricing_ReasonableSpread_ReturnsTrue()
{
// Arrange
var strategy = new NormalPricingStrategy();
var quote = new Quote(10.0m, 10.20m); // Reasonable spread: $0.20
// Act
var shouldAttempt = strategy.ShouldAttemptPricing(quote, OrderDirection.Buy);
// Assert
Assert.That(shouldAttempt, Is.True);
}
[Test]
public void Quote_Properties_CalculatedCorrectly()
{
// Arrange
var quote = new Quote(10.0m, 10.50m);
// Act & Assert
Assert.That(quote.Bid, Is.EqualTo(10.0m));
Assert.That(quote.Ask, Is.EqualTo(10.50m));
Assert.That(quote.Price, Is.EqualTo(10.25m)); // Mid-spread
Assert.That(quote.Spread, Is.EqualTo(0.50m)); // Ask - Bid
}
}
}using System;
using System.Linq;
using NUnit.Framework;
using QuantConnect;
using QuantConnect.Securities;
using CoreAlgo.Architecture.Core.Templates;
using CoreAlgo.Architecture.Core.Models;
namespace CoreAlgo.Tests
{
[TestFixture]
public class ORBTemplateTests
{
private ORBTemplate _strategy;
private ORBConfig _config;
[SetUp]
public void Setup()
{
_strategy = new ORBTemplate();
_config = new ORBConfig();
}
[Test]
public void Name_ReturnsCorrectValue()
{
// Act & Assert
Assert.That(_strategy.Name, Is.EqualTo("Opening Range Breakout"));
}
[Test]
public void Description_ReturnsCorrectText()
{
// Assert
Assert.That(_strategy.Description,
Does.Contain("Monitors opening range"));
Assert.That(_strategy.Description,
Does.Contain("0DTE credit spreads"));
Assert.That(_strategy.Description,
Does.Contain("12:00 PM"));
Assert.That(_strategy.Description,
Does.Contain("full trading days"));
}
[Test]
public void ToString_ReturnsFormattedConfigString()
{
// Arrange
_config.UnderlyingSymbol = "SPX";
_config.RangePeriodMinutes = 60;
_config.MinRangeWidthPercent = 0.2m;
_config.SpreadWidth = 15m;
_config.EntryHour = 12;
_config.EntryMinute = 0;
// Act
var result = _config.ToString();
// Assert
Assert.That(result, Does.Contain("ORB[SPX]"));
Assert.That(result, Does.Contain("Range:60min"));
Assert.That(result, Does.Contain("MinWidth:20.0%"));
Assert.That(result, Does.Contain("SpreadWidth:$15"));
Assert.That(result, Does.Contain("Entry:12:00"));
}
[Test]
public void ORBConfig_HasCorrectDefaults()
{
// Assert original parameters
Assert.That(_config.RangePeriodMinutes, Is.EqualTo(60));
Assert.That(_config.MinRangeWidthPercent, Is.EqualTo(0.2m));
Assert.That(_config.SpreadWidth, Is.EqualTo(15m));
Assert.That(_config.EntryHour, Is.EqualTo(12));
Assert.That(_config.EntryMinute, Is.EqualTo(0));
Assert.That(_config.MaxPositionsPerDay, Is.EqualTo(1));
Assert.That(_config.ContractSize, Is.EqualTo(10));
Assert.That(_config.MinStrikeOffset, Is.EqualTo(0.01m));
}
[Test]
public void ORBConfig_HasCorrectNewDefaults()
{
// Assert new ORB strategy parameters
Assert.That(_config.UseSmaTwentyFilter, Is.EqualTo(true));
Assert.That(_config.CapitalAllocation, Is.EqualTo(100000m));
Assert.That(_config.SkipFomcDays, Is.EqualTo(true));
Assert.That(_config.SlippageAmount, Is.EqualTo(0.10m));
}
[Test]
public void ORBConfig_FomcDatesArePopulated()
{
// Verify FOMC dates collection contains expected dates
Assert.That(ORBConfig.FomcDates2024_2025, Is.Not.Empty);
Assert.That(ORBConfig.FomcDates2024_2025.Count, Is.GreaterThan(10));
// Test some specific known FOMC dates for 2024
Assert.That(ORBConfig.FomcDates2024_2025, Does.Contain(new DateTime(2024, 1, 31)));
Assert.That(ORBConfig.FomcDates2024_2025, Does.Contain(new DateTime(2024, 3, 20)));
Assert.That(ORBConfig.FomcDates2024_2025, Does.Contain(new DateTime(2024, 12, 18)));
}
[Test]
public void ORBConfig_ToString_IncludesNewParameters()
{
// Arrange
_config.UnderlyingSymbol = "SPX";
_config.RangePeriodMinutes = 60;
_config.MinRangeWidthPercent = 0.2m;
_config.SpreadWidth = 15m;
_config.EntryHour = 12;
_config.EntryMinute = 0;
_config.ContractSize = 10;
_config.UseSmaTwentyFilter = true;
_config.CapitalAllocation = 100000m;
// Act
var result = _config.ToString();
// Assert includes original parameters
Assert.That(result, Does.Contain("ORB[SPX]"));
Assert.That(result, Does.Contain("Range:60min"));
Assert.That(result, Does.Contain("MinWidth:20.0%"));
Assert.That(result, Does.Contain("SpreadWidth:$15"));
Assert.That(result, Does.Contain("Entry:12:00"));
// Assert includes new parameters
Assert.That(result, Does.Contain("Contracts:10"));
Assert.That(result, Does.Contain("SMA:True"));
Assert.That(result, Does.Contain("Capital:$100,000"));
}
}
}