From 6437840e6d49c16a40c227366c54849d4cb31367 Mon Sep 17 00:00:00 2001 From: Alexandre Catarino Date: Fri, 4 Apr 2025 21:39:36 +0100 Subject: [PATCH 1/3] Adds example of custom signal exports. - SignalExportManager send signals automatically after 5 seconds. - Missing example and Python support. --- ...ustomSignalExportDemonstrationAlgorithm.cs | 101 ++++++++++++++++++ ...ustomSignalExportDemonstrationAlgorithm.py | 70 ++++++++++++ .../Python/Wrappers/AlgorithmPythonWrapper.cs | 7 ++ .../SignalExports/SignalExportManager.cs | 73 ++++++++++++- .../Python/SignalExportTargetPythonWrapper.cs | 47 ++++++++ .../BrokerageTransactionHandler.cs | 12 +++ 6 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 Algorithm.CSharp/CustomSignalExportDemonstrationAlgorithm.cs create mode 100644 Algorithm.Python/CustomSignalExportDemonstrationAlgorithm.py create mode 100644 Common/Python/SignalExportTargetPythonWrapper.cs diff --git a/Algorithm.CSharp/CustomSignalExportDemonstrationAlgorithm.cs b/Algorithm.CSharp/CustomSignalExportDemonstrationAlgorithm.cs new file mode 100644 index 000000000000..18b1f8e02c06 --- /dev/null +++ b/Algorithm.CSharp/CustomSignalExportDemonstrationAlgorithm.cs @@ -0,0 +1,101 @@ +/* + * 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 Newtonsoft.Json; +using QuantConnect.Algorithm.Framework.Portfolio.SignalExports; +using QuantConnect.Api; +using QuantConnect.Data; +using QuantConnect.Interfaces; +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// This algorithm sends a list of portfolio targets to custom endpoint + /// + /// + /// + /// + public class CustomSignalExportDemonstrationAlgorithm : QCAlgorithm + { + /// + /// Initialize the date and add all equity symbols present + /// + public override void Initialize() + { + SetStartDate(2013, 10, 07); + SetEndDate(2013, 10, 11); + + /// Our custom signal export accepts all asset types + AddEquity("SPY", Resolution.Second); + AddForex("EURUSD", Resolution.Second); + AddFutureContract(QuantConnect.Symbol.CreateFuture("ES", Market.CME, new DateTime(2023, 12, 15), null)); + AddOptionContract(QuantConnect.Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, OptionRight.Call, 130, new DateTime(2023, 9, 1))); + + // Set CustomSignalExport signal export provider. + SignalExport.AddSignalExportProvider(new CustomSignalExport()); + } + + /// + /// Buy and hold EURUSD and SPY + /// + /// + public override void OnData(Slice slice) + { + foreach (var ticker in new[] { "SPY", "EURUSD" }) + { + if (!Portfolio[ticker].Invested && Securities[ticker].HasData) + { + SetHoldings(ticker, 0.5m); + } + } + } + } + + internal class CustomSignalExport : ISignalExportTarget + { + private readonly Uri _requestUri = new ("http://localhost:5000/"); + private readonly HttpClient _httpClient = new(); + + public bool Send(SignalExportTargetParameters parameters) + { + var message = JsonConvert.SerializeObject(parameters.Targets); + using var httpMessage = new StringContent(message, Encoding.UTF8, "application/json"); + using HttpResponseMessage response = _httpClient.PostAsync(_requestUri, httpMessage).Result; + var result = response.Content.ReadFromJsonAsync().Result; + return result.Success; + } + + public void Dispose() => _httpClient.Dispose(); + } +} + +/* +# $ flask --app app run + +# app.py: +from flask import Flask, request, jsonify +from json import loads +app = Flask(__name__) +@app.post('/') +def handle_positions(): + result = loads(request.data) + return jsonify({'success': True,'message': f'{len(result)} positions received'}) +if __name__ == '__main__': + app.run(debug=True) +*/ diff --git a/Algorithm.Python/CustomSignalExportDemonstrationAlgorithm.py b/Algorithm.Python/CustomSignalExportDemonstrationAlgorithm.py new file mode 100644 index 000000000000..75edce5437bc --- /dev/null +++ b/Algorithm.Python/CustomSignalExportDemonstrationAlgorithm.py @@ -0,0 +1,70 @@ +# 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. + +from AlgorithmImports import * + +### +### his algorithm sends a list of portfolio targets to custom endpoint +### +### +### +### +class CustomSignalExportDemonstrationAlgorithm(QCAlgorithm): + + def initialize(self) -> None: + ''' Initialize the date and add all equity symbols present in list _symbols ''' + + self.set_start_date(2013, 10, 7) #Set Start Date + self.set_end_date(2013, 10, 11) #Set End Date + self.set_cash(100000) #Set Strategy Cash + + # Our custom signal export accepts all asset types + self.add_equity("SPY", Resolution.SECOND) + self.add_forex("EURUSD", Resolution.SECOND) + self.add_future_contract(Symbol.create_future("ES", Market.CME, datetime(2023, 12, 15), None)) + self.add_option_contract(Symbol.create_option("SPY", Market.USA, OptionStyle.American, OptionRight.Call, 130, datetime(2023, 9, 1))) + + # Set CustomSignalExport signal export provider. + self.signal_export.add_signal_export_provider(CustomSignalExport()) + + def on_data(self, data: Slice) -> None: + '''Buy and hold EURUSD and SPY''' + for ticker in [ "SPY", "EURUSD" ]: + if not self.portfolio[ticker].invested and self.securities[ticker].has_data: + self.set_holdings(ticker, 0.5) + +from requests import post +class CustomSignalExport: + def send(self, parameters: SignalExportTargetParameters) -> bool: + data = { x.symbol.value: x.quantity for x in parameters.targets } + response = post("http://localhost:5000/", json = data) + result = response.json() + return result.get('success', False) + + def dispose(self): + pass + +''' +# $ flask --app app run + +# app.py: +from flask import Flask, request, jsonify +from json import loads +app = Flask(__name__) +@app.post('/') +def handle_positions(): + result = loads(request.data) + return jsonify({'success': True,'message': f'{len(result)} positions received'}) +if __name__ == '__main__': + app.run(debug=True) +''' diff --git a/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs b/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs index 0ab8d255923f..9c5b7896026d 100644 --- a/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs +++ b/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs @@ -38,6 +38,7 @@ using QuantConnect.Data.Market; using QuantConnect.Algorithm.Framework.Alphas.Analysis; using QuantConnect.Commands; +using QuantConnect.Algorithm.Framework.Portfolio.SignalExports; namespace QuantConnect.AlgorithmFactory.Python.Wrappers { @@ -564,6 +565,12 @@ public int ProjectId /// public StatisticsResults Statistics => _baseAlgorithm.Statistics; + /// + /// SignalExport - Allows sending export signals to different 3rd party API's. For example, it allows to send signals + /// to Collective2, CrunchDAO and Numerai API's + /// + public SignalExportManager SignalExport => ((QCAlgorithm)_baseAlgorithm).SignalExport; + /// /// Set a required SecurityType-symbol and resolution for algorithm /// diff --git a/Common/Algorithm/Framework/Portfolio/SignalExports/SignalExportManager.cs b/Common/Algorithm/Framework/Portfolio/SignalExports/SignalExportManager.cs index e9e8afac6e08..b3550cc14a1b 100644 --- a/Common/Algorithm/Framework/Portfolio/SignalExports/SignalExportManager.cs +++ b/Common/Algorithm/Framework/Portfolio/SignalExports/SignalExportManager.cs @@ -13,11 +13,16 @@ * limitations under the License. */ +using Python.Runtime; using QuantConnect.Interfaces; +using QuantConnect.Orders; +using QuantConnect.Python; using QuantConnect.Securities; using System.Collections.Generic; using System; using System.Linq; +using QuantConnect.Util; +using QLNet; namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports { @@ -27,6 +32,11 @@ namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports /// public class SignalExportManager { + /// + /// Records the time of the first order event of a group of events + /// + private DateTime _initialOrderEventTimeUtc = Time.EndOfTime; + /// /// List of signal export providers /// @@ -42,6 +52,12 @@ public class SignalExportManager /// private bool _isLiveWarningModeLog; + /// + /// Gets the maximim time span elapsed to export signals after an order event + /// If null, disable automatic export. + /// + public TimeSpan? AutomaticExportTimeSpan { get; set; } = TimeSpan.FromSeconds(5); + /// /// SignalExportManager Constructor, obtains the entry information needed to send signals /// and initializes the fields to be used @@ -53,15 +69,33 @@ public SignalExportManager(IAlgorithm algorithm) _isLiveWarningModeLog = false; } + /// + /// Adds a new signal exports provider + /// + /// Signal export provider + public void AddSignalExportProvider(ISignalExportTarget signalExport) + { + _signalExports ??= []; + _signalExports.Add(signalExport); + } + + /// + /// Adds a new signal exports provider + /// + /// Signal export provider + public void AddSignalExportProvider(PyObject signalExport) + { + AddSignalExportProvider(new SignalExportTargetPythonWrapper(signalExport)); + } + /// /// Adds one or more new signal exports providers /// /// One or more signal export provider + [Obsolete("This method is deprecated. Please use AddSignalExportProvider(ISignalExportTarget).")] public void AddSignalExportProviders(params ISignalExportTarget[] signalExports) { - _signalExports ??= new List(); - - _signalExports.AddRange(signalExports); + signalExports.DoForEach(AddSignalExportProvider); } /// @@ -174,5 +208,38 @@ private IEnumerable GetPortfolioTargets(decimal totalPortfolioV yield return new PortfolioTarget(holding.Symbol, adjustedHoldingPercent); } } + + /// + /// New order event handler: on order status changes (filled, partially filled, cancelled etc). + /// + /// Event information + public void OnOrderEvent(OrderEvent orderEvent) + { + if (_initialOrderEventTimeUtc == Time.EndOfTime && orderEvent.Status.IsFill()) + { + _initialOrderEventTimeUtc = DateTime.UtcNow; + } + } + + /// + /// Set the target portfolio after order events. + /// + /// The current time of synchronous events + public void Flush(DateTime currentTimeUtc) + { + if (_initialOrderEventTimeUtc == Time.EndOfTime || !AutomaticExportTimeSpan.HasValue) + { + return; + } + + if (currentTimeUtc - _initialOrderEventTimeUtc < AutomaticExportTimeSpan) + { + return; + } + + var success = SetTargetPortfolioFromPortfolio(); + Logging.Log.Trace($"SignalExportManager.Flush({currentTimeUtc:T}) :: Success: {success}. Initial OrderEvent Time: {_initialOrderEventTimeUtc:T}"); + _initialOrderEventTimeUtc = Time.EndOfTime; + } } } diff --git a/Common/Python/SignalExportTargetPythonWrapper.cs b/Common/Python/SignalExportTargetPythonWrapper.cs new file mode 100644 index 000000000000..d3c94267d2db --- /dev/null +++ b/Common/Python/SignalExportTargetPythonWrapper.cs @@ -0,0 +1,47 @@ +/* + * 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 Python.Runtime; +using QuantConnect.Algorithm.Framework.Portfolio.SignalExports; +using QuantConnect.Interfaces; + +namespace QuantConnect.Python +{ + /// + /// Provides an implementation of that wraps a object + /// + /// + /// Constructor for initialising the class with wrapped object + /// + /// Python benchmark model + public class SignalExportTargetPythonWrapper(PyObject model) : BasePythonWrapper(model), ISignalExportTarget + { + /// + /// Interface to send positions holdings to different 3rd party API's + /// + public bool Send(SignalExportTargetParameters parameters) + { + return InvokeMethod(nameof(Send), parameters); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + InvokeMethod(nameof(Dispose)); + } + } +} diff --git a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs index cb9b9274e646..22abb1d79550 100644 --- a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs +++ b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs @@ -19,6 +19,9 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading; +using QuantConnect.Algorithm; +using QuantConnect.Algorithm.Framework.Portfolio.SignalExports; +using QuantConnect.AlgorithmFactory.Python.Wrappers; using QuantConnect.Brokerages; using QuantConnect.Brokerages.Backtesting; using QuantConnect.Interfaces; @@ -38,6 +41,7 @@ namespace QuantConnect.Lean.Engine.TransactionHandlers public class BrokerageTransactionHandler : ITransactionHandler { private IAlgorithm _algorithm; + private SignalExportManager _signalExport; private IBrokerage _brokerage; private bool _brokerageIsBacktesting; private bool _loggedFeeAdjustmentWarning; @@ -205,6 +209,12 @@ public virtual void Initialize(IAlgorithm algorithm, IBrokerage brokerage, IResu IsActive = true; _algorithm = algorithm; + + _signalExport = _algorithm is QCAlgorithm + ? (_algorithm as QCAlgorithm).SignalExport + : (_algorithm as AlgorithmPythonWrapper).SignalExport; + + NewOrderEvent += (s, e) => _signalExport.OnOrderEvent(e); InitializeTransactionThread(); } @@ -659,6 +669,8 @@ public virtual void ProcessSynchronousEvents() return; } + _signalExport.Flush(CurrentTimeUtc); + // check if the brokerage should perform cash sync now if (!_algorithm.IsWarmingUp && _brokerage.ShouldPerformCashSync(CurrentTimeUtc)) { From 2da8092770bab07d78fee48ab387451e2e5e0204 Mon Sep 17 00:00:00 2001 From: Alexandre Catarino Date: Fri, 4 Apr 2025 21:41:50 +0100 Subject: [PATCH 2/3] Updates Examples to Disable Automatic Export --- ...llective2PortfolioSignalExportDemonstrationAlgorithm.cs | 4 +++- .../Collective2SignalExportDemonstrationAlgorithm.cs | 5 ++++- .../IndexSecurityCanBeTradableRegressionAlgorithm.cs | 1 + .../NumeraiSignalExportDemonstrationAlgorithm.cs | 7 ++++++- .../RegressionTests/Collective2IndexOptionAlgorithm.cs | 4 +++- ...llective2PortfolioSignalExportDemonstrationAlgorithm.py | 6 +++++- .../Collective2SignalExportDemonstrationAlgorithm.py | 5 ++++- .../CrunchDAOSignalExportDemonstrationAlgorithm.py | 7 +++++-- .../NumeraiSignalExportDemonstrationAlgorithm.py | 7 ++++++- 9 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Algorithm.CSharp/Collective2PortfolioSignalExportDemonstrationAlgorithm.cs b/Algorithm.CSharp/Collective2PortfolioSignalExportDemonstrationAlgorithm.cs index dfe6b2919814..9879374ca80f 100644 --- a/Algorithm.CSharp/Collective2PortfolioSignalExportDemonstrationAlgorithm.cs +++ b/Algorithm.CSharp/Collective2PortfolioSignalExportDemonstrationAlgorithm.cs @@ -80,8 +80,10 @@ public override void Initialize() // Initialize this flag, to check when the ema indicators crosses between themselves _emaFastIsNotSet = true; + // Disable automatic exports as we manually set them + SignalExport.AutomaticExportTimeSpan = null; // Set Collective2 signal export provider - SignalExport.AddSignalExportProviders(new Collective2SignalExport(_collective2ApiKey, _collective2SystemId)); + SignalExport.AddSignalExportProvider(new Collective2SignalExport(_collective2ApiKey, _collective2SystemId)); SetWarmUp(100); } diff --git a/Algorithm.CSharp/Collective2SignalExportDemonstrationAlgorithm.cs b/Algorithm.CSharp/Collective2SignalExportDemonstrationAlgorithm.cs index 9b59b1072ae0..0f394777b7fa 100644 --- a/Algorithm.CSharp/Collective2SignalExportDemonstrationAlgorithm.cs +++ b/Algorithm.CSharp/Collective2SignalExportDemonstrationAlgorithm.cs @@ -95,12 +95,15 @@ public override void Initialize() // Initialize this flag, to check when the ema indicators crosses between themselves _emaFastIsNotSet = true; + // Disable automatic exports as we manually set them + SignalExport.AutomaticExportTimeSpan = null; + // Set Collective2 signal export provider. // If using the Collective2 white-label API, you can specify it in the constructor with the optional parameter `useWhiteLabelApi`: // e.g. new Collective2SignalExport(_collective2ApiKey, _collective2SystemId, useWhiteLabelApi: true) // The API url can also be overridden by setting the Destination property: // e.g. new Collective2SignalExport(_collective2ApiKey, _collective2SystemId) { Destination = new Uri("your url") } - SignalExport.AddSignalExportProviders(new Collective2SignalExport(_collective2ApiKey, _collective2SystemId)); + SignalExport.AddSignalExportProvider(new Collective2SignalExport(_collective2ApiKey, _collective2SystemId)); SetWarmUp(100); } diff --git a/Algorithm.CSharp/IndexSecurityCanBeTradableRegressionAlgorithm.cs b/Algorithm.CSharp/IndexSecurityCanBeTradableRegressionAlgorithm.cs index 67cd539d57e6..1744bb33dd4b 100644 --- a/Algorithm.CSharp/IndexSecurityCanBeTradableRegressionAlgorithm.cs +++ b/Algorithm.CSharp/IndexSecurityCanBeTradableRegressionAlgorithm.cs @@ -43,6 +43,7 @@ public override void Initialize() _index = AddIndex("SPX").Symbol; _equity = AddEquity("SPY").Symbol; + SignalExport.AutomaticExportTimeSpan = null; _signalExportManagerTest = new SignalExportManagerTest(this); Securities[_index].IsTradable = IsTradable; } diff --git a/Algorithm.CSharp/NumeraiSignalExportDemonstrationAlgorithm.cs b/Algorithm.CSharp/NumeraiSignalExportDemonstrationAlgorithm.cs index 8242edb13f6c..0524092c3808 100644 --- a/Algorithm.CSharp/NumeraiSignalExportDemonstrationAlgorithm.cs +++ b/Algorithm.CSharp/NumeraiSignalExportDemonstrationAlgorithm.cs @@ -64,7 +64,12 @@ public override void Initialize() var numeraiModelId = ""; var numeraiFilename = ""; // (Optional) Replace this value with your submission filename - SignalExport.AddSignalExportProviders(new NumeraiSignalExport(numeraiPublicId, numeraiSecretId, numeraiModelId, numeraiFilename)); + + // Disable automatic exports as we manually set them + SignalExport.AutomaticExportTimeSpan = null; + + // Set Numerai signal export provider + SignalExport.AddSignalExportProvider(new NumeraiSignalExport(numeraiPublicId, numeraiSecretId, numeraiModelId, numeraiFilename)); } public void SubmitSignals() diff --git a/Algorithm.CSharp/RegressionTests/Collective2IndexOptionAlgorithm.cs b/Algorithm.CSharp/RegressionTests/Collective2IndexOptionAlgorithm.cs index 9bcb3f8d2f8f..b0232dd33e24 100644 --- a/Algorithm.CSharp/RegressionTests/Collective2IndexOptionAlgorithm.cs +++ b/Algorithm.CSharp/RegressionTests/Collective2IndexOptionAlgorithm.cs @@ -63,8 +63,10 @@ public override void Initialize() _fast = EMA(underlying, 10, Resolution.Minute); _slow = EMA(underlying, 50, Resolution.Minute); + // Disable automatic exports as we manually set them + SignalExport.AutomaticExportTimeSpan = null; // Set up the Collective2 Signal Export with the provided API key and system ID - SignalExport.AddSignalExportProviders(new Collective2SignalExport(_collective2ApiKey, _collective2SystemId)); + SignalExport.AddSignalExportProvider(new Collective2SignalExport(_collective2ApiKey, _collective2SystemId)); // Set warm-up period for the indicators SetWarmUp(50); diff --git a/Algorithm.Python/Collective2PortfolioSignalExportDemonstrationAlgorithm.py b/Algorithm.Python/Collective2PortfolioSignalExportDemonstrationAlgorithm.py index 2682712fa47d..543c124a84d5 100644 --- a/Algorithm.Python/Collective2PortfolioSignalExportDemonstrationAlgorithm.py +++ b/Algorithm.Python/Collective2PortfolioSignalExportDemonstrationAlgorithm.py @@ -54,7 +54,11 @@ def initialize(self): # Collective2 System ID: This value is found beside the system's name (strategy's name) on the main system page self.collective2_system_id = 0 - self.signal_export.add_signal_export_providers(Collective2SignalExport(self.collective2_apikey, self.collective2_system_id)) + # Disable automatic exports as we manually set them + self.signal_export.automatic_export_time_span = None + + # Set Collective2 signal export provider + self.signal_export.add_signal_export_provider(Collective2SignalExport(self.collective2_apikey, self.collective2_system_id)) self.first_call = True diff --git a/Algorithm.Python/Collective2SignalExportDemonstrationAlgorithm.py b/Algorithm.Python/Collective2SignalExportDemonstrationAlgorithm.py index 4a670cecb50d..8b793649c08e 100644 --- a/Algorithm.Python/Collective2SignalExportDemonstrationAlgorithm.py +++ b/Algorithm.Python/Collective2SignalExportDemonstrationAlgorithm.py @@ -61,11 +61,14 @@ def initialize(self): # Collective2 System ID: This value is found beside the system's name (strategy's name) on the main system page self.collective2_system_id = 0 + # Disable automatic exports as we manually set them + self.signal_export.automatic_export_time_span = None + # If using the Collective2 white-label API, you can specify it in the constructor with the optional parameter `use_white_label_api`: # e.g. Collective2SignalExport(self.collective2_apikey, self.collective2_system_id, use_white_label_api=True) # The API url can also be overridden by setting the Destination property: # e.g. Collective2SignalExport(self.collective2_apikey, self.collective2_system_id) { Destination = new Uri("your url") } - self.signal_export.add_signal_export_providers(Collective2SignalExport(self.collective2_apikey, self.collective2_system_id)) + self.signal_export.add_signal_export_provider(Collective2SignalExport(self.collective2_apikey, self.collective2_system_id)) self.first_call = True diff --git a/Algorithm.Python/CrunchDAOSignalExportDemonstrationAlgorithm.py b/Algorithm.Python/CrunchDAOSignalExportDemonstrationAlgorithm.py index 4c140d8d7020..e6d7e1622dc8 100644 --- a/Algorithm.Python/CrunchDAOSignalExportDemonstrationAlgorithm.py +++ b/Algorithm.Python/CrunchDAOSignalExportDemonstrationAlgorithm.py @@ -28,15 +28,18 @@ def initialize(self): self.set_end_date(2023, 5, 26) self.set_cash(1_000_000) + # Disable automatic exports as we manually set them + self.signal_export.automatic_export_time_span = None + # Connect to CrunchDAO api_key = "" # Your CrunchDAO API key model = "" # The Id of your CrunchDAO model submission_name = "" # A name for the submission to distinguish it from your other submissions comment = "" # A comment for the submission - self.signal_export.add_signal_export_providers(CrunchDAOSignalExport(api_key, model, submission_name, comment)) + self.signal_export.add_signal_export_provider(CrunchDAOSignalExport(api_key, model, submission_name, comment)) self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))) - + # Add a custom data universe to read the CrunchDAO skeleton self.add_universe(CrunchDaoSkeleton, "CrunchDaoSkeleton", Resolution.DAILY, self.select_symbols) diff --git a/Algorithm.Python/NumeraiSignalExportDemonstrationAlgorithm.py b/Algorithm.Python/NumeraiSignalExportDemonstrationAlgorithm.py index 000b258c1088..7f95dd3b7d3e 100644 --- a/Algorithm.Python/NumeraiSignalExportDemonstrationAlgorithm.py +++ b/Algorithm.Python/NumeraiSignalExportDemonstrationAlgorithm.py @@ -55,7 +55,12 @@ def initialize(self): numerai_model_id = "" numerai_filename = "" # (Optional) Replace this value with your submission filename - self.signal_export.add_signal_export_providers(NumeraiSignalExport(numerai_public_id, numerai_secret_id, numerai_model_id, numerai_filename)) + + # Disable automatic exports as we manually set them + self.signal_export.automatic_export_time_span = None + + # Set Numerai signal export provider + self.signal_export.add_signal_export_provider(NumeraiSignalExport(numerai_public_id, numerai_secret_id, numerai_model_id, numerai_filename)) def submit_signals(self): From f4dd7630f7413bf014633aad208470f993201518 Mon Sep 17 00:00:00 2001 From: Alexandre Catarino Date: Mon, 7 Apr 2025 23:41:39 +0100 Subject: [PATCH 3/3] Addresses Peer-Review --- ...ustomSignalExportDemonstrationAlgorithm.cs | 16 +++++- ...ustomSignalExportDemonstrationAlgorithm.py | 13 +++-- .../SignalExports/SignalExportManager.cs | 50 ++++++++++++++----- .../Python/SignalExportTargetPythonWrapper.cs | 12 +++-- 4 files changed, 69 insertions(+), 22 deletions(-) diff --git a/Algorithm.CSharp/CustomSignalExportDemonstrationAlgorithm.cs b/Algorithm.CSharp/CustomSignalExportDemonstrationAlgorithm.cs index 18b1f8e02c06..f302f8bce321 100644 --- a/Algorithm.CSharp/CustomSignalExportDemonstrationAlgorithm.cs +++ b/Algorithm.CSharp/CustomSignalExportDemonstrationAlgorithm.cs @@ -14,11 +14,13 @@ */ using Newtonsoft.Json; +using QuantConnect.Algorithm.Framework.Portfolio; using QuantConnect.Algorithm.Framework.Portfolio.SignalExports; using QuantConnect.Api; using QuantConnect.Data; using QuantConnect.Interfaces; using System; +using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Text; @@ -43,6 +45,7 @@ public override void Initialize() /// Our custom signal export accepts all asset types AddEquity("SPY", Resolution.Second); + AddCrypto("BTCUSD", Resolution.Second); AddForex("EURUSD", Resolution.Second); AddFutureContract(QuantConnect.Symbol.CreateFuture("ES", Market.CME, new DateTime(2023, 12, 15), null)); AddOptionContract(QuantConnect.Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, OptionRight.Call, 130, new DateTime(2023, 9, 1))); @@ -57,7 +60,7 @@ public override void Initialize() /// public override void OnData(Slice slice) { - foreach (var ticker in new[] { "SPY", "EURUSD" }) + foreach (var ticker in new[] { "SPY", "EURUSD", "BTCUSD" }) { if (!Portfolio[ticker].Invested && Securities[ticker].HasData) { @@ -74,10 +77,17 @@ internal class CustomSignalExport : ISignalExportTarget public bool Send(SignalExportTargetParameters parameters) { - var message = JsonConvert.SerializeObject(parameters.Targets); + object SimplePayload(PortfolioTarget target) + { + var newTarget = PortfolioTarget.Percent(parameters.Algorithm, target.Symbol, target.Quantity); + return new { symbol = newTarget.Symbol.Value, quantity = newTarget.Quantity }; + }; + + var message = JsonConvert.SerializeObject(parameters.Targets.Select(SimplePayload)); using var httpMessage = new StringContent(message, Encoding.UTF8, "application/json"); using HttpResponseMessage response = _httpClient.PostAsync(_requestUri, httpMessage).Result; var result = response.Content.ReadFromJsonAsync().Result; + parameters.Algorithm.Log($"Send #{parameters.Targets.Count} targets. Success: {result.Success}"); return result.Success; } @@ -86,6 +96,7 @@ public bool Send(SignalExportTargetParameters parameters) } /* +# To test the algorithm, you can create a simple Python Flask application (app.py) and run flask # $ flask --app app run # app.py: @@ -95,6 +106,7 @@ from json import loads @app.post('/') def handle_positions(): result = loads(request.data) + print(result) return jsonify({'success': True,'message': f'{len(result)} positions received'}) if __name__ == '__main__': app.run(debug=True) diff --git a/Algorithm.Python/CustomSignalExportDemonstrationAlgorithm.py b/Algorithm.Python/CustomSignalExportDemonstrationAlgorithm.py index 75edce5437bc..1779b17fa747 100644 --- a/Algorithm.Python/CustomSignalExportDemonstrationAlgorithm.py +++ b/Algorithm.Python/CustomSignalExportDemonstrationAlgorithm.py @@ -30,6 +30,7 @@ def initialize(self) -> None: # Our custom signal export accepts all asset types self.add_equity("SPY", Resolution.SECOND) + self.add_crypto("BTCUSD", Resolution.SECOND) self.add_forex("EURUSD", Resolution.SECOND) self.add_future_contract(Symbol.create_future("ES", Market.CME, datetime(2023, 12, 15), None)) self.add_option_contract(Symbol.create_option("SPY", Market.USA, OptionStyle.American, OptionRight.Call, 130, datetime(2023, 9, 1))) @@ -39,22 +40,27 @@ def initialize(self) -> None: def on_data(self, data: Slice) -> None: '''Buy and hold EURUSD and SPY''' - for ticker in [ "SPY", "EURUSD" ]: + for ticker in [ "SPY", "EURUSD", "BTCUSD" ]: if not self.portfolio[ticker].invested and self.securities[ticker].has_data: self.set_holdings(ticker, 0.5) from requests import post class CustomSignalExport: def send(self, parameters: SignalExportTargetParameters) -> bool: - data = { x.symbol.value: x.quantity for x in parameters.targets } + targets = [PortfolioTarget.percent(parameters.algorithm, x.symbol, x.quantity) + for x in parameters.targets] ; + data = [ {'symbol' : x.symbol.value, 'quantity': x.quantity} for x in targets ] response = post("http://localhost:5000/", json = data) result = response.json() - return result.get('success', False) + success = result.get('success', False) + parameters.algorithm.log(f"Send #{len(parameters.targets)} targets. Success: {success}"); + return success def dispose(self): pass ''' +# To test the algorithm, you can create a simple Python Flask application (app.py) and run flask # $ flask --app app run # app.py: @@ -64,6 +70,7 @@ def dispose(self): @app.post('/') def handle_positions(): result = loads(request.data) + print(result) return jsonify({'success': True,'message': f'{len(result)} positions received'}) if __name__ == '__main__': app.run(debug=True) diff --git a/Common/Algorithm/Framework/Portfolio/SignalExports/SignalExportManager.cs b/Common/Algorithm/Framework/Portfolio/SignalExports/SignalExportManager.cs index b3550cc14a1b..b1b076f71e7d 100644 --- a/Common/Algorithm/Framework/Portfolio/SignalExports/SignalExportManager.cs +++ b/Common/Algorithm/Framework/Portfolio/SignalExports/SignalExportManager.cs @@ -22,7 +22,6 @@ using System; using System.Linq; using QuantConnect.Util; -using QLNet; namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports { @@ -35,7 +34,7 @@ public class SignalExportManager /// /// Records the time of the first order event of a group of events /// - private DateTime _initialOrderEventTimeUtc = Time.EndOfTime; + private ReferenceWrapper _initialOrderEventTimeUtc = new(Time.EndOfTime); /// /// List of signal export providers @@ -85,19 +84,37 @@ public void AddSignalExportProvider(ISignalExportTarget signalExport) /// Signal export provider public void AddSignalExportProvider(PyObject signalExport) { - AddSignalExportProvider(new SignalExportTargetPythonWrapper(signalExport)); + if (!signalExport.TryConvert(out var managedSignalExport)) + { + managedSignalExport = new SignalExportTargetPythonWrapper(signalExport); + } + AddSignalExportProvider(managedSignalExport); } /// /// Adds one or more new signal exports providers /// /// One or more signal export provider - [Obsolete("This method is deprecated. Please use AddSignalExportProvider(ISignalExportTarget).")] public void AddSignalExportProviders(params ISignalExportTarget[] signalExports) { signalExports.DoForEach(AddSignalExportProvider); } + /// + /// Adds one or more new signal exports providers + /// + /// One or more signal export provider + public void AddSignalExportProviders(PyObject signalExports) + { + using var _ = Py.GIL(); + if (!signalExports.IsIterable()) + { + AddSignalExportProvider(signalExports); + return; + } + PyList.AsList(signalExports).DoForEach(AddSignalExportProvider); + } + /// /// Sets the portfolio targets from the algorihtm's Portfolio and sends them with the /// algorithm being ran to the signal exports providers already set @@ -215,9 +232,9 @@ private IEnumerable GetPortfolioTargets(decimal totalPortfolioV /// Event information public void OnOrderEvent(OrderEvent orderEvent) { - if (_initialOrderEventTimeUtc == Time.EndOfTime && orderEvent.Status.IsFill()) + if (_initialOrderEventTimeUtc.Value == Time.EndOfTime && orderEvent.Status.IsFill()) { - _initialOrderEventTimeUtc = DateTime.UtcNow; + _initialOrderEventTimeUtc = new(orderEvent.UtcTime); } } @@ -227,19 +244,28 @@ public void OnOrderEvent(OrderEvent orderEvent) /// The current time of synchronous events public void Flush(DateTime currentTimeUtc) { - if (_initialOrderEventTimeUtc == Time.EndOfTime || !AutomaticExportTimeSpan.HasValue) + var initialOrderEventTimeUtc = _initialOrderEventTimeUtc.Value; + if (initialOrderEventTimeUtc == Time.EndOfTime || !AutomaticExportTimeSpan.HasValue) { return; } - if (currentTimeUtc - _initialOrderEventTimeUtc < AutomaticExportTimeSpan) + if (currentTimeUtc - initialOrderEventTimeUtc < AutomaticExportTimeSpan) { return; } - - var success = SetTargetPortfolioFromPortfolio(); - Logging.Log.Trace($"SignalExportManager.Flush({currentTimeUtc:T}) :: Success: {success}. Initial OrderEvent Time: {_initialOrderEventTimeUtc:T}"); - _initialOrderEventTimeUtc = Time.EndOfTime; + + try + { + SetTargetPortfolioFromPortfolio(); + } + catch (Exception exception) + { + // SetTargetPortfolioFromPortfolio logs all known error on LEAN side. + // Exceptions occurs in the ISignalExportTarget.Send method (user-defined). + _algorithm.Error($"Failed to send portfolio target(s). Reason: {exception.Message}.{Environment.NewLine}{exception.StackTrace}"); + } + _initialOrderEventTimeUtc = new(Time.EndOfTime); } } } diff --git a/Common/Python/SignalExportTargetPythonWrapper.cs b/Common/Python/SignalExportTargetPythonWrapper.cs index d3c94267d2db..6e30270514d4 100644 --- a/Common/Python/SignalExportTargetPythonWrapper.cs +++ b/Common/Python/SignalExportTargetPythonWrapper.cs @@ -22,12 +22,14 @@ namespace QuantConnect.Python /// /// Provides an implementation of that wraps a object /// - /// - /// Constructor for initialising the class with wrapped object - /// - /// Python benchmark model - public class SignalExportTargetPythonWrapper(PyObject model) : BasePythonWrapper(model), ISignalExportTarget + public class SignalExportTargetPythonWrapper : BasePythonWrapper, ISignalExportTarget { + /// + /// Constructor for initialising the class with wrapped object + /// + /// The underlying python instance + public SignalExportTargetPythonWrapper(PyObject instance) : base(instance) { } + /// /// Interface to send positions holdings to different 3rd party API's ///