diff --git a/.gitignore b/.gitignore index 4bd3ecdb1720..270f172b4f13 100644 --- a/.gitignore +++ b/.gitignore @@ -281,3 +281,4 @@ Research/Notebooks #Docker result files Results/ QuantConnect.Lean.sln.DotSettings +DataLibraries/ diff --git a/Algorithm.CSharp/Demonstration.cs b/Algorithm.CSharp/Demonstration.cs new file mode 100644 index 000000000000..f98b935b2d23 --- /dev/null +++ b/Algorithm.CSharp/Demonstration.cs @@ -0,0 +1,94 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +using QuantConnect.Algorithm; +using QuantConnect.Data.Market; +using QuantConnect.Interfaces; +using QuantConnect; +using QuantConnect.Data; +using QuantConnect.Securities.Future; +using QuantConnect.Util; +using System; +using System.Linq; + +namespace QuantConnect.Algorithm.CSharp +{ + public class DatabentoFuturesTestAlgorithm : QCAlgorithm + { + private Future _es; + + public override void Initialize() + { + Log("Algorithm Initialize"); + + SetStartDate(2025, 10, 1); + SetEndDate(2025, 10, 16); + SetCash(100000); + + var exp = new DateTime(2025, 12, 19); + var symbol = QuantConnect.Symbol.CreateFuture("ES", Market.CME, exp); + //_es = AddFutureContract(symbol, Resolution.Tick, true, 1, true); + _es = AddFutureContract(symbol, Resolution.Second, true, 1, true); + Log($"_es: {_es}"); + + var history = History(_es.Symbol, 10, Resolution.Minute).ToList(); + + Log($"History returned {history.Count} bars"); + + foreach (var bar in history) + { + Log($"History Bar: {bar.Time} - O:{bar.Open} H:{bar.High} L:{bar.Low} C:{bar.Close} V:{bar.Volume}"); + } + + } + + public override void OnData(Slice slice) + { + if (!slice.HasData) + { + Log("Slice has no data"); + return; + } + + Log($"OnData: Slice has {slice.Count} data points"); + + // For Tick resolution, check Ticks collection + if (slice.Ticks.ContainsKey(_es.Symbol)) + { + var ticks = slice.Ticks[_es.Symbol]; + Log($"Received {ticks.Count} ticks for {_es.Symbol}"); + + foreach (var tick in ticks) + { + if (tick.TickType == TickType.Trade) + { + Log($"Trade Tick - Price: {tick.Price}, Quantity: {tick.Quantity}, Time: {tick.Time}"); + } + else if (tick.TickType == TickType.Quote) + { + Log($"Quote Tick - Bid: {tick.BidPrice}x{tick.BidSize}, Ask: {tick.AskPrice}x{tick.AskSize}, Time: {tick.Time}"); + } + } + } + + // Access OHLCV bars + foreach (var bar in slice.Bars.Values) + { + Log($"OHLCV BAR: {bar.Symbol.Value} - O: {bar.Open}, H: {bar.High}, L: {bar.Low}, C: {bar.Close}, V: {bar.Volume}"); + } + } + } +} diff --git a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj index 688414008d10..abf0b94beb51 100644 --- a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj +++ b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Algorithm.CSharp QuantConnect.Algorithm.CSharp - net9.0 + net10.0 false bin\$(Configuration)\ AllEnabledByDefault diff --git a/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj b/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj index 81697dda1300..2614a4e5ea8e 100644 --- a/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj +++ b/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Algorithm.Framework QuantConnect.Algorithm.Framework - net9.0 + net10.0 false bin\$(Configuration)\ AllEnabledByDefault diff --git a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj index 246af65ed265..58b4f7b58cc3 100644 --- a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj +++ b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Algorithm.Python QuantConnect.Algorithm.Python - net9.0 + net10.0 AllEnabledByDefault bin\$(Configuration)\ false diff --git a/Algorithm/QuantConnect.Algorithm.csproj b/Algorithm/QuantConnect.Algorithm.csproj index 24ece5b173b6..0a8f0aa8d0bc 100644 --- a/Algorithm/QuantConnect.Algorithm.csproj +++ b/Algorithm/QuantConnect.Algorithm.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Algorithm QuantConnect.Algorithm - net9.0 + net10.0 ..\ false AllEnabledByDefault diff --git a/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj b/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj index f67eb2becc41..1780c6c55dd1 100644 --- a/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj +++ b/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.AlgorithmFactory QuantConnect.AlgorithmFactory - net9.0 + net10.0 false bin\$(Configuration)\ AllEnabledByDefault diff --git a/Api/QuantConnect.Api.csproj b/Api/QuantConnect.Api.csproj index 20fb363b8669..4c06a2e5213e 100644 --- a/Api/QuantConnect.Api.csproj +++ b/Api/QuantConnect.Api.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Api QuantConnect.Api - net9.0 + net10.0 ..\ true AllEnabledByDefault diff --git a/Brokerages/QuantConnect.Brokerages.csproj b/Brokerages/QuantConnect.Brokerages.csproj index 96f6221d4106..e44042df0a51 100644 --- a/Brokerages/QuantConnect.Brokerages.csproj +++ b/Brokerages/QuantConnect.Brokerages.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Brokerages QuantConnect.Brokerages - net9.0 + net10.0 AllEnabledByDefault false bin\$(Configuration)\ diff --git a/Common/Brokerages/BrokerageName.cs b/Common/Brokerages/BrokerageName.cs index 58d238626917..94092863302d 100644 --- a/Common/Brokerages/BrokerageName.cs +++ b/Common/Brokerages/BrokerageName.cs @@ -197,6 +197,11 @@ public enum BrokerageName /// /// Transaction and submit/execution rules will use dYdX models /// - dYdX + dYdX, + + /// + /// Transaction and submit/execution rules will use Tradovate models + /// + Tradovate } } diff --git a/Common/Brokerages/TradovateBrokerageModel.cs b/Common/Brokerages/TradovateBrokerageModel.cs new file mode 100644 index 000000000000..eb2f12335ad8 --- /dev/null +++ b/Common/Brokerages/TradovateBrokerageModel.cs @@ -0,0 +1,161 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using QuantConnect.Orders; +using QuantConnect.Orders.Fees; +using QuantConnect.Orders.TimeInForces; +using QuantConnect.Securities; +using QuantConnect.Util; + +namespace QuantConnect.Brokerages +{ + /// + /// Provides an implementation of specific to Tradovate brokerage. + /// Tradovate is a futures-focused brokerage supporting CME Group products. + /// + public class TradovateBrokerageModel : DefaultBrokerageModel + { + /// + /// The default markets for Tradovate brokerage + /// + public new static readonly IReadOnlyDictionary DefaultMarketMap = new Dictionary + { + { SecurityType.Future, Market.CME }, + { SecurityType.FutureOption, Market.CME } + }.ToReadOnlyDictionary(); + + /// + /// Order types supported by Tradovate + /// + protected virtual HashSet SupportedOrderTypes { get; } = new HashSet + { + OrderType.Market, + OrderType.Limit, + OrderType.StopMarket, + OrderType.StopLimit, + OrderType.TrailingStop + }; + + /// + /// Time in force types supported by Tradovate + /// + protected virtual Type[] SupportedTimeInForces { get; } = + { + typeof(GoodTilCanceledTimeInForce), + typeof(DayTimeInForce) + }; + + /// + /// Initializes a new instance of the class + /// + /// The type of account to be modeled, defaults to + public TradovateBrokerageModel(AccountType accountType = AccountType.Margin) + : base(accountType) + { + } + + /// + /// Gets a map of the default markets to be used for each security type + /// + public override IReadOnlyDictionary DefaultMarkets => DefaultMarketMap; + + /// + /// Returns true if the brokerage could accept this order. This takes into account + /// order type, security type, and order size limits. + /// + /// The security being ordered + /// The order to be processed + /// If this function returns false, a brokerage message detailing why the order may not be submitted + /// True if the brokerage could process the order, false otherwise + public override bool CanSubmitOrder(Security security, Order order, out BrokerageMessageEvent message) + { + message = null; + + // Validate security type - Tradovate only supports futures + if (security.Type != SecurityType.Future && security.Type != SecurityType.FutureOption) + { + message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", + $"Tradovate does not support {security.Type} security type. Only Future and FutureOption are supported."); + return false; + } + + // Validate order type + if (!SupportedOrderTypes.Contains(order.Type)) + { + message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", + $"Tradovate does not support {order.Type} order type. " + + $"Supported order types are: {string.Join(", ", SupportedOrderTypes)}"); + return false; + } + + // Validate time in force + if (!IsValidTimeInForce(order.TimeInForce)) + { + message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", + $"Tradovate does not support {order.TimeInForce.GetType().Name} time in force. " + + "Supported time in force types are: Day, GoodTilCanceled"); + return false; + } + + return base.CanSubmitOrder(security, order, out message); + } + + /// + /// Returns true if the brokerage would allow updating the order as specified by the request. + /// Tradovate supports order modifications via the order/modifyorder endpoint. + /// + /// The security of the order + /// The order to be updated + /// The requested update to be made to the order + /// If this function returns false, a brokerage message detailing why the order may not be updated + /// True if the brokerage would allow updating the order, false otherwise + public override bool CanUpdateOrder(Security security, Order order, UpdateOrderRequest request, out BrokerageMessageEvent message) + { + message = null; + + // Tradovate supports order modifications + // Note: When modifying orders, the timeInForce must match the existing order's setting + return true; + } + + /// + /// Gets a new fee model that represents Tradovate's fee structure + /// + /// The security to get a fee model for + /// The new fee model for this brokerage + public override IFeeModel GetFeeModel(Security security) + { + return new TradovateFeeModel(); + } + + /// + /// Validates that the time in force is supported by Tradovate + /// + private bool IsValidTimeInForce(TimeInForce timeInForce) + { + var timeInForceType = timeInForce.GetType(); + foreach (var supportedType in SupportedTimeInForces) + { + if (supportedType == timeInForceType) + { + return true; + } + } + return false; + } + } +} diff --git a/Common/Orders/Fees/TradovateFeeModel.cs b/Common/Orders/Fees/TradovateFeeModel.cs new file mode 100644 index 000000000000..f0d146692612 --- /dev/null +++ b/Common/Orders/Fees/TradovateFeeModel.cs @@ -0,0 +1,126 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System.Collections.Generic; +using QuantConnect.Securities; + +namespace QuantConnect.Orders.Fees +{ + /// + /// Provides an implementation of for Tradovate brokerage. + /// Tradovate charges per-contract fees that vary by contract type. + /// + /// + /// Fee structure based on Tradovate pricing: https://www.tradovate.com/pricing/ + /// Fees include Tradovate commission + exchange fees + NFA fee. + /// + public class TradovateFeeModel : FeeModel + { + /// + /// Default fee per contract for futures not in the fee dictionary + /// + public const decimal DefaultFeePerContract = 1.29m; + + /// + /// Fee per contract for micro futures + /// + public const decimal MicroFuturesFee = 0.79m; + + /// + /// Fee per contract for E-mini futures + /// + public const decimal EminiFuturesFee = 1.29m; + + /// + /// Fee per contract for standard futures + /// + public const decimal StandardFuturesFee = 1.79m; + + /// + /// Per-contract fees by futures symbol + /// + private static readonly Dictionary FuturesFees = new Dictionary + { + // Micro E-mini Index Futures ($0.79/contract) + { "MYM", MicroFuturesFee }, // Micro E-mini Dow + { "MES", MicroFuturesFee }, // Micro E-mini S&P 500 + { "MNQ", MicroFuturesFee }, // Micro E-mini NASDAQ-100 + { "M2K", MicroFuturesFee }, // Micro E-mini Russell 2000 + + // E-mini Index Futures ($1.29/contract) + { "YM", EminiFuturesFee }, // E-mini Dow + { "ES", EminiFuturesFee }, // E-mini S&P 500 + { "NQ", EminiFuturesFee }, // E-mini NASDAQ-100 + { "RTY", EminiFuturesFee }, // E-mini Russell 2000 + + // Micro Treasury Futures ($0.79/contract) + { "2YY", MicroFuturesFee }, // Micro 2-Year Treasury + { "5YY", MicroFuturesFee }, // Micro 5-Year Treasury + { "10Y", MicroFuturesFee }, // Micro 10-Year Treasury + { "30Y", MicroFuturesFee }, // Micro 30-Year Treasury + + // Standard Treasury Futures ($1.79/contract) + { "ZB", StandardFuturesFee }, // 30-Year T-Bond + { "ZN", StandardFuturesFee }, // 10-Year T-Note + { "ZF", StandardFuturesFee }, // 5-Year T-Note + { "ZT", StandardFuturesFee }, // 2-Year T-Note + + // Micro Energy Futures ($0.79/contract) + { "MCL", MicroFuturesFee }, // Micro Crude Oil + + // Standard Energy Futures ($1.79/contract) + { "CL", StandardFuturesFee }, // Crude Oil WTI + { "NG", StandardFuturesFee }, // Natural Gas + + // Micro Metals Futures ($0.79/contract) + { "MGC", MicroFuturesFee }, // Micro Gold + { "SIL", MicroFuturesFee }, // Micro Silver + + // Standard Metals Futures ($1.79/contract) + { "GC", StandardFuturesFee }, // Gold + { "SI", StandardFuturesFee }, // Silver + + // Agriculture Futures ($1.79/contract) + { "ZC", StandardFuturesFee }, // Corn + { "ZS", StandardFuturesFee }, // Soybeans + { "ZW", StandardFuturesFee }, // Wheat + }; + + /// + /// Gets the order fee associated with the specified order. + /// + /// A object containing the security and order + /// The cost of the order in units of the account currency (USD) + public override OrderFee GetOrderFee(OrderFeeParameters parameters) + { + var order = parameters.Order; + var security = parameters.Security; + var quantity = order.AbsoluteQuantity; + + // Get the root symbol for the futures contract + var symbol = security.Symbol.ID.Symbol; + + // Look up the fee for this symbol, or use default + if (!FuturesFees.TryGetValue(symbol, out var feePerContract)) + { + feePerContract = DefaultFeePerContract; + } + + var totalFee = quantity * feePerContract; + + return new OrderFee(new CashAmount(totalFee, Currencies.USD)); + } + } +} diff --git a/Common/QuantConnect.csproj b/Common/QuantConnect.csproj index 4bdfd41aa6ce..44ddd84076b3 100644 --- a/Common/QuantConnect.csproj +++ b/Common/QuantConnect.csproj @@ -2,7 +2,7 @@ Debug AnyCPU - net9.0 + net10.0 QuantConnect.Common ..\ true diff --git a/Compression/QuantConnect.Compression.csproj b/Compression/QuantConnect.Compression.csproj index 1f626d954416..66a19b888f51 100644 --- a/Compression/QuantConnect.Compression.csproj +++ b/Compression/QuantConnect.Compression.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Compression QuantConnect.Compression - net9.0 + net10.0 AllEnabledByDefault false bin\$(Configuration)\ diff --git a/Configuration/QuantConnect.Configuration.csproj b/Configuration/QuantConnect.Configuration.csproj index f222de092f40..e2b797aee2b4 100644 --- a/Configuration/QuantConnect.Configuration.csproj +++ b/Configuration/QuantConnect.Configuration.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Configuration QuantConnect.Configuration - net9.0 + net10.0 ..\ true AllEnabledByDefault diff --git a/Dockerfile b/Dockerfile index 8ebd317176e6..61aab5512e50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # -# LEAN Docker Container 20200522 -# Cross platform deployment for multiple brokerages +# LEAN Docker Container - ECS Live Trading Edition +# Custom build with Tradovate brokerage and SNS notifications # # Use base system @@ -8,20 +8,41 @@ FROM quantconnect/lean:foundation MAINTAINER QuantConnect -#Install debugpy and PyDevD for remote python debugging +# Install debugpy and PyDevD for remote python debugging RUN pip install --no-cache-dir ptvsd==4.3.2 debugpy~=1.6.7 pydevd-pycharm~=231.9225.15 +# Install AWS SDK for SNS notifications and AWS CLI for S3 sync +RUN pip install --no-cache-dir boto3 awscli + # Install vsdbg for remote C# debugging in Visual Studio and Visual Studio Code RUN wget https://aka.ms/getvsdbgsh -O - 2>/dev/null | /bin/sh /dev/stdin -v 17.10.20209.7 -l /root/vsdbg +# Copy custom plugins (DataBento, Tradovate, etc.) COPY ./DataLibraries /Lean/Launcher/bin/Debug/ -COPY ./Lean/Data/ /Lean/Data/ -COPY ./Lean/Launcher/bin/Debug/ /Lean/Launcher/bin/Debug/ -COPY ./Lean/Optimizer.Launcher/bin/Debug/ /Lean/Optimizer.Launcher/bin/Debug/ -COPY ./Lean/Report/bin/Debug/ /Lean/Report/bin/Debug/ -COPY ./Lean/DownloaderDataProvider/bin/Debug/ /Lean/DownloaderDataProvider/bin/Debug/ -# Can override with '-w' +# Copy essential Data files (symbol properties, market hours - required for live trading) +# Full market data omitted for size +COPY ./Data/symbol-properties/ /Lean/Data/symbol-properties/ +COPY ./Data/market-hours/ /Lean/Data/market-hours/ + +# Copy LEAN engine binaries +COPY ./Launcher/bin/Debug/ /Lean/Launcher/bin/Debug/ +COPY ./Optimizer.Launcher/bin/Debug/ /Lean/Optimizer.Launcher/bin/Debug/ +COPY ./Report/bin/Debug/ /Lean/Report/bin/Debug/ +COPY ./DownloaderDataProvider/bin/Debug/ /Lean/DownloaderDataProvider/bin/Debug/ + +# Copy config template and entrypoint script +COPY config-template.json /config-template.json +COPY scripts/docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + +# Create directories for algorithm and results +# /LeanCLI will be mounted from EFS at runtime (allows updates without image rebuild) +# /Results will be mounted from EFS for persistent output +RUN mkdir -p /LeanCLI /Results + +# Working directory WORKDIR /Lean/Launcher/bin/Debug -ENTRYPOINT [ "dotnet", "QuantConnect.Lean.Launcher.dll" ] +# Use wrapper entrypoint that handles credential injection +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/DownloaderDataProvider/QuantConnect.DownloaderDataProvider.Launcher.csproj b/DownloaderDataProvider/QuantConnect.DownloaderDataProvider.Launcher.csproj index 3409754162db..7e8ae152ec8a 100644 --- a/DownloaderDataProvider/QuantConnect.DownloaderDataProvider.Launcher.csproj +++ b/DownloaderDataProvider/QuantConnect.DownloaderDataProvider.Launcher.csproj @@ -6,7 +6,7 @@ Exe QuantConnect.DownloaderDataProvider.Launcher QuantConnect.DownloaderDataProvider.Launcher - net9.0 + net10.0 AllEnabledByDefault false bin\$(Configuration)\ diff --git a/Engine/QuantConnect.Lean.Engine.csproj b/Engine/QuantConnect.Lean.Engine.csproj index 9501da0dab81..44d450439e98 100644 --- a/Engine/QuantConnect.Lean.Engine.csproj +++ b/Engine/QuantConnect.Lean.Engine.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Lean.Engine QuantConnect.Lean.Engine - net9.0 + net10.0 ..\ true publish\ diff --git a/Indicators/QuantConnect.Indicators.csproj b/Indicators/QuantConnect.Indicators.csproj index 92e3c6ee59f9..43ae38d942a3 100644 --- a/Indicators/QuantConnect.Indicators.csproj +++ b/Indicators/QuantConnect.Indicators.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Indicators QuantConnect.Indicators - net9.0 + net10.0 AllEnabledByDefault false bin\$(Configuration)\ diff --git a/Launcher/QuantConnect.Lean.Launcher.csproj b/Launcher/QuantConnect.Lean.Launcher.csproj index 299780332618..dfea79a8e24c 100644 --- a/Launcher/QuantConnect.Lean.Launcher.csproj +++ b/Launcher/QuantConnect.Lean.Launcher.csproj @@ -5,7 +5,7 @@ Exe QuantConnect.Lean.Launcher QuantConnect.Lean.Launcher - net9.0 + net10.0 AllEnabledByDefault false bin\$(Configuration)\ diff --git a/Logging/QuantConnect.Logging.csproj b/Logging/QuantConnect.Logging.csproj index c603d354cdce..4040ff227d09 100644 --- a/Logging/QuantConnect.Logging.csproj +++ b/Logging/QuantConnect.Logging.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Logging QuantConnect.Logging - net9.0 + net10.0 ..\ true AllEnabledByDefault diff --git a/Messaging/QuantConnect.Messaging.csproj b/Messaging/QuantConnect.Messaging.csproj index 80db8f6c7e4d..285983c458a5 100644 --- a/Messaging/QuantConnect.Messaging.csproj +++ b/Messaging/QuantConnect.Messaging.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Messaging QuantConnect.Messaging - net9.0 + net10.0 AllEnabledByDefault false bin\$(Configuration)\ diff --git a/Optimizer.Launcher/QuantConnect.Optimizer.Launcher.csproj b/Optimizer.Launcher/QuantConnect.Optimizer.Launcher.csproj index 7c9555446fc7..e9661b733e81 100644 --- a/Optimizer.Launcher/QuantConnect.Optimizer.Launcher.csproj +++ b/Optimizer.Launcher/QuantConnect.Optimizer.Launcher.csproj @@ -5,7 +5,7 @@ Exe QuantConnect.Optimizer.Launcher QuantConnect.Optimizer.Launcher - net9.0 + net10.0 false bin\$(Configuration)\ AllEnabledByDefault diff --git a/Optimizer/QuantConnect.Optimizer.csproj b/Optimizer/QuantConnect.Optimizer.csproj index 2154ba7021e8..552252fde267 100644 --- a/Optimizer/QuantConnect.Optimizer.csproj +++ b/Optimizer/QuantConnect.Optimizer.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Optimizer QuantConnect.Optimizer - net9.0 + net10.0 false bin\$(Configuration)\ AllEnabledByDefault diff --git a/Queues/QuantConnect.Queues.csproj b/Queues/QuantConnect.Queues.csproj index c59e858512fa..7a24256fe9d7 100644 --- a/Queues/QuantConnect.Queues.csproj +++ b/Queues/QuantConnect.Queues.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Queues QuantConnect.Queues - net9.0 + net10.0 false bin\$(Configuration)\ bin\$(Configuration)\QuantConnect.Queues.xml diff --git a/Report/QuantConnect.Report.csproj b/Report/QuantConnect.Report.csproj index 0edb390d8356..e726bd8c8572 100644 --- a/Report/QuantConnect.Report.csproj +++ b/Report/QuantConnect.Report.csproj @@ -5,7 +5,7 @@ Exe QuantConnect.Report QuantConnect.Report - net9.0 + net10.0 false bin\$(Configuration)\ bin\$(Configuration)\QuantConnect.Report.xml diff --git a/Research/QuantConnect.Research.csproj b/Research/QuantConnect.Research.csproj index 381c3ef07a4b..a95a42d466d0 100644 --- a/Research/QuantConnect.Research.csproj +++ b/Research/QuantConnect.Research.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Research QuantConnect.Research - net9.0 + net10.0 AllEnabledByDefault bin\$(Configuration)\ false diff --git a/Tests/Common/Brokerages/TradovateBrokerageModelTests.cs b/Tests/Common/Brokerages/TradovateBrokerageModelTests.cs new file mode 100644 index 000000000000..50534452a2db --- /dev/null +++ b/Tests/Common/Brokerages/TradovateBrokerageModelTests.cs @@ -0,0 +1,377 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using QuantConnect.Brokerages; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using QuantConnect.Orders; +using QuantConnect.Orders.Fees; +using QuantConnect.Orders.TimeInForces; +using QuantConnect.Securities; +using QuantConnect.Securities.Future; +using QuantConnect.Securities.Option; + +namespace QuantConnect.Tests.Common.Brokerages +{ + [TestFixture, Parallelizable(ParallelScope.All)] + public class TradovateBrokerageModelTests + { + private TradovateBrokerageModel _brokerageModel; + + [SetUp] + public void SetUp() + { + _brokerageModel = new TradovateBrokerageModel(); + } + + #region Supported Order Types Tests + + [TestCase(OrderType.Market, true)] + [TestCase(OrderType.Limit, true)] + [TestCase(OrderType.StopMarket, true)] + [TestCase(OrderType.StopLimit, true)] + [TestCase(OrderType.TrailingStop, true)] + [TestCase(OrderType.MarketOnOpen, false)] + [TestCase(OrderType.MarketOnClose, false)] + [TestCase(OrderType.LimitIfTouched, false)] + public void CanSubmitOrder_ValidatesOrderType(OrderType orderType, bool shouldBeAllowed) + { + var security = CreateFutureSecurity(); + var order = CreateOrder(security.Symbol, orderType); + + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + Assert.AreEqual(shouldBeAllowed, canSubmit); + if (!shouldBeAllowed) + { + Assert.IsNotNull(message); + Assert.AreEqual(BrokerageMessageType.Warning, message.Type); + } + } + + #endregion + + #region Supported Security Types Tests + + [TestCase(SecurityType.Future, true)] + [TestCase(SecurityType.FutureOption, true)] + [TestCase(SecurityType.Equity, false)] + [TestCase(SecurityType.Option, false)] + [TestCase(SecurityType.Forex, false)] + [TestCase(SecurityType.Crypto, false)] + public void CanSubmitOrder_ValidatesSecurityType(SecurityType securityType, bool shouldBeAllowed) + { + var security = CreateSecurity(securityType); + var order = new MarketOrder(security.Symbol, 1, DateTime.UtcNow); + + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + Assert.AreEqual(shouldBeAllowed, canSubmit); + if (!shouldBeAllowed) + { + Assert.IsNotNull(message); + Assert.AreEqual(BrokerageMessageType.Warning, message.Type); + StringAssert.Contains("security type", message.Message.ToLower()); + } + } + + #endregion + + #region Time In Force Tests + + [Test] + public void CanSubmitOrder_AllowsDayTimeInForce() + { + var security = CreateFutureSecurity(); + var order = new LimitOrder(security.Symbol, 1, 100m, DateTime.UtcNow) + { + Properties = { TimeInForce = TimeInForce.Day } + }; + + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + Assert.IsTrue(canSubmit); + Assert.IsNull(message); + } + + [Test] + public void CanSubmitOrder_AllowsGoodTilCanceledTimeInForce() + { + var security = CreateFutureSecurity(); + var order = new LimitOrder(security.Symbol, 1, 100m, DateTime.UtcNow) + { + Properties = { TimeInForce = TimeInForce.GoodTilCanceled } + }; + + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + Assert.IsTrue(canSubmit); + Assert.IsNull(message); + } + + [Test] + public void CanSubmitOrder_RejectsGoodTilDateTimeInForce() + { + var security = CreateFutureSecurity(); + var order = new LimitOrder(security.Symbol, 1, 100m, DateTime.UtcNow) + { + Properties = { TimeInForce = TimeInForce.GoodTilDate(DateTime.UtcNow.AddDays(7)) } + }; + + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + Assert.IsFalse(canSubmit); + Assert.IsNotNull(message); + Assert.AreEqual(BrokerageMessageType.Warning, message.Type); + } + + #endregion + + #region Order Update Tests + + [Test] + public void CanUpdateOrder_AllowsUpdateForLimitOrder() + { + var security = CreateFutureSecurity(); + var order = new LimitOrder(security.Symbol, 1, 100m, DateTime.UtcNow); + var request = new UpdateOrderRequest(DateTime.UtcNow, 1, new UpdateOrderFields { LimitPrice = 101m }); + + var canUpdate = _brokerageModel.CanUpdateOrder(security, order, request, out var message); + + Assert.IsTrue(canUpdate); + Assert.IsNull(message); + } + + [Test] + public void CanUpdateOrder_AllowsUpdateForStopOrder() + { + var security = CreateFutureSecurity(); + var order = new StopMarketOrder(security.Symbol, 1, 100m, DateTime.UtcNow); + var request = new UpdateOrderRequest(DateTime.UtcNow, 1, new UpdateOrderFields { StopPrice = 99m }); + + var canUpdate = _brokerageModel.CanUpdateOrder(security, order, request, out var message); + + Assert.IsTrue(canUpdate); + Assert.IsNull(message); + } + + [Test] + public void CanUpdateOrder_AllowsQuantityUpdate() + { + var security = CreateFutureSecurity(); + var order = new LimitOrder(security.Symbol, 1, 100m, DateTime.UtcNow); + var request = new UpdateOrderRequest(DateTime.UtcNow, 1, new UpdateOrderFields { Quantity = 2 }); + + var canUpdate = _brokerageModel.CanUpdateOrder(security, order, request, out var message); + + Assert.IsTrue(canUpdate); + Assert.IsNull(message); + } + + #endregion + + #region Fee Model Tests + + [Test] + public void GetFeeModel_ReturnsTradovateFeeModel() + { + var security = CreateFutureSecurity(); + + var feeModel = _brokerageModel.GetFeeModel(security); + + Assert.IsInstanceOf(feeModel); + } + + #endregion + + #region Default Markets Tests + + [Test] + public void DefaultMarkets_ContainsFutureWithCME() + { + var defaultMarkets = _brokerageModel.DefaultMarkets; + + Assert.IsTrue(defaultMarkets.ContainsKey(SecurityType.Future)); + Assert.AreEqual(Market.CME, defaultMarkets[SecurityType.Future]); + } + + [Test] + public void DefaultMarkets_ContainsFutureOptionWithCME() + { + var defaultMarkets = _brokerageModel.DefaultMarkets; + + Assert.IsTrue(defaultMarkets.ContainsKey(SecurityType.FutureOption)); + Assert.AreEqual(Market.CME, defaultMarkets[SecurityType.FutureOption]); + } + + #endregion + + #region Account Type Tests + + [Test] + public void Constructor_DefaultsToMarginAccount() + { + var model = new TradovateBrokerageModel(); + + Assert.AreEqual(AccountType.Margin, model.AccountType); + } + + [Test] + public void Constructor_AcceptsAccountTypeParameter() + { + var model = new TradovateBrokerageModel(AccountType.Cash); + + Assert.AreEqual(AccountType.Cash, model.AccountType); + } + + #endregion + + #region Trailing Stop Order Tests + + [Test] + public void CanSubmitOrder_AllowsTrailingStopOrder() + { + var security = CreateFutureSecurity(); + security.SetMarketPrice(new Tick(DateTime.UtcNow, security.Symbol, 100m, 100m)); + + var order = new TrailingStopOrder(security.Symbol, 1, 95m, 5m, false, DateTime.UtcNow); + + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + Assert.IsTrue(canSubmit); + Assert.IsNull(message); + } + + #endregion + + #region Helper Methods + + private static Future CreateFutureSecurity() + { + var symbol = Symbols.CreateFutureSymbol(Futures.Indices.Dow30EMini, new DateTime(2025, 3, 21)); + return new Future( + symbol, + SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork), + new Cash("USD", 0, 1), + SymbolProperties.GetDefault("USD"), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache() + ); + } + + private static Security CreateSecurity(SecurityType securityType) + { + Symbol symbol; + switch (securityType) + { + case SecurityType.Future: + symbol = Symbols.CreateFutureSymbol(Futures.Indices.Dow30EMini, new DateTime(2025, 3, 21)); + return new Future( + symbol, + SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork), + new Cash("USD", 0, 1), + SymbolProperties.GetDefault("USD"), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache() + ); + + case SecurityType.FutureOption: + var futureSymbol = Symbols.CreateFutureSymbol(Futures.Indices.Dow30EMini, new DateTime(2025, 3, 21)); + symbol = Symbols.CreateFutureOptionSymbol(futureSymbol, OptionRight.Call, 40000m, new DateTime(2025, 3, 21)); + var future = new Future( + futureSymbol, + SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork), + new Cash("USD", 0, 1), + SymbolProperties.GetDefault("USD"), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache() + ); + return new QuantConnect.Securities.FutureOption.FutureOption( + symbol, + SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork), + new Cash("USD", 0, 1), + new OptionSymbolProperties(SymbolProperties.GetDefault("USD")), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache(), + future + ); + + case SecurityType.Equity: + symbol = Symbol.Create("SPY", SecurityType.Equity, Market.USA); + break; + + case SecurityType.Option: + symbol = Symbols.CreateOptionSymbol("SPY", OptionRight.Call, 400m, new DateTime(2025, 3, 21)); + break; + + case SecurityType.Forex: + symbol = Symbol.Create("EURUSD", SecurityType.Forex, Market.Oanda); + break; + + case SecurityType.Crypto: + symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Coinbase); + break; + + default: + symbol = Symbol.Create("TEST", securityType, Market.USA); + break; + } + + return new Security( + SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork), + new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, false, true, false), + new Cash("USD", 0, 1), + SymbolProperties.GetDefault("USD"), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache() + ); + } + + private static Order CreateOrder(Symbol symbol, OrderType orderType) + { + switch (orderType) + { + case OrderType.Market: + return new MarketOrder(symbol, 1, DateTime.UtcNow); + case OrderType.Limit: + return new LimitOrder(symbol, 1, 100m, DateTime.UtcNow); + case OrderType.StopMarket: + return new StopMarketOrder(symbol, 1, 100m, DateTime.UtcNow); + case OrderType.StopLimit: + return new StopLimitOrder(symbol, 1, 100m, 99m, DateTime.UtcNow); + case OrderType.TrailingStop: + return new TrailingStopOrder(symbol, 1, 95m, 5m, false, DateTime.UtcNow); + case OrderType.MarketOnOpen: + return new MarketOnOpenOrder(symbol, 1, DateTime.UtcNow); + case OrderType.MarketOnClose: + return new MarketOnCloseOrder(symbol, 1, DateTime.UtcNow); + case OrderType.LimitIfTouched: + return new LimitIfTouchedOrder(symbol, 1, 100m, 99m, DateTime.UtcNow); + default: + throw new ArgumentException($"Unsupported order type: {orderType}"); + } + } + + #endregion + } +} diff --git a/Tests/Common/Orders/Fees/TradovateFeeModelTests.cs b/Tests/Common/Orders/Fees/TradovateFeeModelTests.cs new file mode 100644 index 000000000000..1713d8d937d1 --- /dev/null +++ b/Tests/Common/Orders/Fees/TradovateFeeModelTests.cs @@ -0,0 +1,317 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using System.Linq; +using NUnit.Framework; +using QuantConnect.Data.Market; +using QuantConnect.Orders; +using QuantConnect.Orders.Fees; +using QuantConnect.Securities; +using QuantConnect.Securities.Future; + +namespace QuantConnect.Tests.Common.Orders.Fees +{ + [TestFixture, Parallelizable(ParallelScope.All)] + public class TradovateFeeModelTests + { + private readonly IFeeModel _feeModel = new TradovateFeeModel(); + + #region Micro E-mini Futures Fee Tests + + [TestCase(Futures.Indices.MicroDow30EMini, 0.79)] // MYM + [TestCase(Futures.Indices.MicroSP500EMini, 0.79)] // MES + [TestCase(Futures.Indices.MicroNASDAQ100EMini, 0.79)] // MNQ + [TestCase(Futures.Indices.MicroRussell2000EMini, 0.79)] // M2K + public void MicroEminiFuturesFee(string futureSymbol, double expectedFeePerContract) + { + var symbol = Symbols.CreateFutureSymbol(futureSymbol, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 1, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency); + Assert.AreEqual((decimal)expectedFeePerContract, fee.Value.Amount); + } + + [Test] + public void MicroEminiFuturesFee_MultipleContracts() + { + var symbol = Symbols.CreateFutureSymbol(Futures.Indices.MicroDow30EMini, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 10, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency); + Assert.AreEqual(10 * 0.79m, fee.Value.Amount); + } + + #endregion + + #region E-mini Futures Fee Tests + + [TestCase(Futures.Indices.Dow30EMini, 1.29)] // YM + [TestCase(Futures.Indices.SP500EMini, 1.29)] // ES + [TestCase(Futures.Indices.NASDAQ100EMini, 1.29)] // NQ + [TestCase(Futures.Indices.Russell2000EMini, 1.29)] // RTY + public void EminiFuturesFee(string futureSymbol, double expectedFeePerContract) + { + var symbol = Symbols.CreateFutureSymbol(futureSymbol, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 1, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency); + Assert.AreEqual((decimal)expectedFeePerContract, fee.Value.Amount); + } + + [Test] + public void EminiFuturesFee_MultipleContracts() + { + var symbol = Symbols.CreateFutureSymbol(Futures.Indices.Dow30EMini, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 5, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency); + Assert.AreEqual(5 * 1.29m, fee.Value.Amount); + } + + #endregion + + #region Treasury Futures Fee Tests + + [TestCase(Futures.Financials.Y30TreasuryBond, 1.79)] // ZB + [TestCase(Futures.Financials.Y10TreasuryNote, 1.79)] // ZN + [TestCase(Futures.Financials.Y5TreasuryNote, 1.79)] // ZF + [TestCase(Futures.Financials.Y2TreasuryNote, 1.79)] // ZT + public void TreasuryFuturesFee(string futureSymbol, double expectedFeePerContract) + { + var symbol = Symbols.CreateFutureSymbol(futureSymbol, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 1, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency); + Assert.AreEqual((decimal)expectedFeePerContract, fee.Value.Amount); + } + + #endregion + + #region Energy Futures Fee Tests + + [TestCase(Futures.Energy.CrudeOilWTI, 1.79)] // CL + [TestCase(Futures.Energy.NaturalGas, 1.79)] // NG + public void EnergyFuturesFee(string futureSymbol, double expectedFeePerContract) + { + var symbol = Symbols.CreateFutureSymbol(futureSymbol, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 1, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency); + Assert.AreEqual((decimal)expectedFeePerContract, fee.Value.Amount); + } + + [TestCase(Futures.Energy.MicroCrudeOilWTI, 0.79)] // MCL + public void MicroEnergyFuturesFee(string futureSymbol, double expectedFeePerContract) + { + var symbol = Symbols.CreateFutureSymbol(futureSymbol, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 1, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency); + Assert.AreEqual((decimal)expectedFeePerContract, fee.Value.Amount); + } + + #endregion + + #region Metals Futures Fee Tests + + [TestCase(Futures.Metals.Gold, 1.79)] // GC + [TestCase(Futures.Metals.Silver, 1.79)] // SI + public void MetalsFuturesFee(string futureSymbol, double expectedFeePerContract) + { + var symbol = Symbols.CreateFutureSymbol(futureSymbol, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 1, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency); + Assert.AreEqual((decimal)expectedFeePerContract, fee.Value.Amount); + } + + [TestCase(Futures.Metals.MicroGold, 0.79)] // MGC + [TestCase(Futures.Metals.MicroSilver, 0.79)] // SIL + public void MicroMetalsFuturesFee(string futureSymbol, double expectedFeePerContract) + { + var symbol = Symbols.CreateFutureSymbol(futureSymbol, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 1, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency); + Assert.AreEqual((decimal)expectedFeePerContract, fee.Value.Amount); + } + + #endregion + + #region Default Fee Tests + + [Test] + public void UnknownFuture_ReturnsDefaultFee() + { + // Use a future that might not be in the fee dictionary + var symbol = Symbols.CreateFutureSymbol(Futures.Grains.Wheat, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 1, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency); + // Default fee should be applied for unknown contracts + Assert.Greater(fee.Value.Amount, 0); + } + + #endregion + + #region Order Type Fee Tests + + [Test] + public void LimitOrder_SameFeeAsMarket() + { + var symbol = Symbols.CreateFutureSymbol(Futures.Indices.Dow30EMini, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + security.SetMarketPrice(new Tick(DateTime.UtcNow, symbol, 44000m, 44000m)); + + var marketOrder = new MarketOrder(symbol, 1, DateTime.UtcNow); + var limitOrder = new LimitOrder(symbol, 1, 44000m, DateTime.UtcNow); + + var marketFee = _feeModel.GetOrderFee(new OrderFeeParameters(security, marketOrder)); + var limitFee = _feeModel.GetOrderFee(new OrderFeeParameters(security, limitOrder)); + + Assert.AreEqual(marketFee.Value.Amount, limitFee.Value.Amount); + Assert.AreEqual(marketFee.Value.Currency, limitFee.Value.Currency); + } + + [Test] + public void StopOrder_SameFeeAsMarket() + { + var symbol = Symbols.CreateFutureSymbol(Futures.Indices.Dow30EMini, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + security.SetMarketPrice(new Tick(DateTime.UtcNow, symbol, 44000m, 44000m)); + + var marketOrder = new MarketOrder(symbol, 1, DateTime.UtcNow); + var stopOrder = new StopMarketOrder(symbol, 1, 43500m, DateTime.UtcNow); + + var marketFee = _feeModel.GetOrderFee(new OrderFeeParameters(security, marketOrder)); + var stopFee = _feeModel.GetOrderFee(new OrderFeeParameters(security, stopOrder)); + + Assert.AreEqual(marketFee.Value.Amount, stopFee.Value.Amount); + Assert.AreEqual(marketFee.Value.Currency, stopFee.Value.Currency); + } + + [Test] + public void TrailingStopOrder_SameFeeAsMarket() + { + var symbol = Symbols.CreateFutureSymbol(Futures.Indices.Dow30EMini, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + security.SetMarketPrice(new Tick(DateTime.UtcNow, symbol, 44000m, 44000m)); + + var marketOrder = new MarketOrder(symbol, 1, DateTime.UtcNow); + var trailingStopOrder = new TrailingStopOrder(symbol, 1, 43500m, 500m, false, DateTime.UtcNow); + + var marketFee = _feeModel.GetOrderFee(new OrderFeeParameters(security, marketOrder)); + var trailingStopFee = _feeModel.GetOrderFee(new OrderFeeParameters(security, trailingStopOrder)); + + Assert.AreEqual(marketFee.Value.Amount, trailingStopFee.Value.Amount); + Assert.AreEqual(marketFee.Value.Currency, trailingStopFee.Value.Currency); + } + + #endregion + + #region Sell Order Fee Tests + + [Test] + public void SellOrder_SameFeeAsBuyOrder() + { + var symbol = Symbols.CreateFutureSymbol(Futures.Indices.Dow30EMini, new DateTime(2025, 3, 21)); + var security = CreateFutureSecurity(symbol); + + var buyOrder = new MarketOrder(symbol, 1, DateTime.UtcNow); + var sellOrder = new MarketOrder(symbol, -1, DateTime.UtcNow); + + var buyFee = _feeModel.GetOrderFee(new OrderFeeParameters(security, buyOrder)); + var sellFee = _feeModel.GetOrderFee(new OrderFeeParameters(security, sellOrder)); + + Assert.AreEqual(buyFee.Value.Amount, sellFee.Value.Amount); + Assert.AreEqual(buyFee.Value.Currency, sellFee.Value.Currency); + } + + #endregion + + #region Currency Tests + + [Test] + public void Fee_AlwaysInUSD() + { + var symbols = new[] + { + Symbols.CreateFutureSymbol(Futures.Indices.Dow30EMini, new DateTime(2025, 3, 21)), + Symbols.CreateFutureSymbol(Futures.Indices.MicroDow30EMini, new DateTime(2025, 3, 21)), + Symbols.CreateFutureSymbol(Futures.Financials.Y30TreasuryBond, new DateTime(2025, 3, 21)) + }; + + foreach (var symbol in symbols) + { + var security = CreateFutureSecurity(symbol); + var order = new MarketOrder(symbol, 1, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.AreEqual(Currencies.USD, fee.Value.Currency, $"Fee currency should be USD for {symbol}"); + } + } + + #endregion + + #region Helper Methods + + private static Future CreateFutureSecurity(Symbol symbol) + { + return new Future( + symbol, + SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork), + new Cash("USD", 0, 1), + SymbolProperties.GetDefault("USD"), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache() + ); + } + + #endregion + } +} diff --git a/Tests/QuantConnect.Tests.csproj b/Tests/QuantConnect.Tests.csproj index f8ced114d264..515c09194e5e 100644 --- a/Tests/QuantConnect.Tests.csproj +++ b/Tests/QuantConnect.Tests.csproj @@ -4,7 +4,7 @@ AnyCPU QuantConnect.Tests QuantConnect.Tests - net9.0 + net10.0 ..\ AllEnabledByDefault false diff --git a/ToolBox/QuantConnect.ToolBox.csproj b/ToolBox/QuantConnect.ToolBox.csproj index 52fd576aee02..196fe98c86f9 100644 --- a/ToolBox/QuantConnect.ToolBox.csproj +++ b/ToolBox/QuantConnect.ToolBox.csproj @@ -5,7 +5,7 @@ Exe QuantConnect.ToolBox QuantConnect.ToolBox - net9.0 + net10.0 AllEnabledByDefault false bin\$(Configuration)\ diff --git a/config-template.json b/config-template.json new file mode 100644 index 000000000000..3c05c3df1936 --- /dev/null +++ b/config-template.json @@ -0,0 +1,62 @@ +{ + "environment": "live-tradovate", + "live-mode": true, + "live-mode-brokerage": "TradovateBrokerage", + + "algorithm-type-name": "Breakoutbox", + "algorithm-language": "Python", + "algorithm-location": "/LeanCLI/main.py", + + "parameters": { + "symbol": "MYM", + "resolution": "minute", + "risk_per_trade": "300", + "max_daily_risk": "1250", + "max_daily_profit": "720", + "max_position_size": "50", + "session_start_time": "03:34", + "session_duration_minutes": "506", + "lookback_period_minutes": "240", + "session_timezone": "America/Los_Angeles", + "entry_offset": "12", + "stop_offset": "2", + "take_profit_ratio": "0.56", + "min_range_points": "50", + "max_range_points": "350", + "enable_logging": "True", + "enable_visualization": "True", + "recoup_trade_enabled": "True", + "use_news_filtering": "True", + "news_delay_minutes": "4", + "trade_set_limit": "3", + "mid_range_size_percent": "70" + }, + + "data-folder": "/Lean/Data/", + "results-destination-folder": "/Results", + + "log-handler": "QuantConnect.Logging.CompositeLogHandler", + "messaging-handler": "QuantConnect.Messaging.Messaging", + "job-queue-handler": "QuantConnect.Queues.JobQueue", + "api-handler": "QuantConnect.Api.Api", + "map-file-provider": "QuantConnect.Data.Auxiliary.LocalDiskMapFileProvider", + "factor-file-provider": "QuantConnect.Data.Auxiliary.LocalDiskFactorFileProvider", + "data-provider": "QuantConnect.Lean.Engine.DataFeeds.DefaultDataProvider", + "data-channel-provider": "DataChannelProvider", + "object-store": "QuantConnect.Lean.Engine.Storage.LocalObjectStore", + "data-aggregator": "QuantConnect.Lean.Engine.DataFeeds.AggregationManager", + + "setup-handler": "QuantConnect.Lean.Engine.Setup.BrokerageSetupHandler", + "result-handler": "QuantConnect.Lean.Engine.Results.LiveTradingResultHandler", + "data-feed-handler": "QuantConnect.Lean.Engine.DataFeeds.LiveTradingDataFeed", + "real-time-handler": "QuantConnect.Lean.Engine.RealTime.LiveTradingRealTimeHandler", + "transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BrokerageTransactionHandler", + "history-provider": "BrokerageHistoryProvider", + "data-queue-handler": "DataBentoProvider", + + "show-missing-data-logs": true, + + "job-user-id": "0", + "api-access-token": "", + "job-organization-id": "" +}