/* * 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.Interfaces; using QuantConnect.Orders; using QuantConnect.Python; using QuantConnect.Securities; using System.Collections.Generic; using System; using System.Linq; using QuantConnect.Util; namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports { /// /// Class manager to send portfolio targets to different 3rd party API's /// For example, it allows Collective2, CrunchDAO and Numerai signal export providers /// public class SignalExportManager { /// /// Records the time of the first order event of a group of events /// private ReferenceWrapper _initialOrderEventTimeUtc = new(Time.EndOfTime); /// /// List of signal export providers /// private List _signalExports; /// /// Algorithm being ran /// private readonly IAlgorithm _algorithm; /// /// Flag to indicate if the user has tried to send signals with live mode off /// 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 /// /// Algorithm being run public SignalExportManager(IAlgorithm algorithm) { _algorithm = 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) { 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 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 /// /// True if the target list could be obtained from the algorithm's Portfolio and they /// were successfully sent to the signal export providers public bool SetTargetPortfolioFromPortfolio() { if (!GetPortfolioTargets(out PortfolioTarget[] targets)) { return false; } var result = SetTargetPortfolio(targets); return result; } /// /// Obtains an array of portfolio targets from algorithm's Portfolio and returns them. /// See for more /// information about how each symbol quantity was calculated /// /// An array of portfolio targets from the algorithm's Portfolio /// True if TotalPortfolioValue was bigger than zero, false otherwise protected bool GetPortfolioTargets(out PortfolioTarget[] targets) { var totalPortfolioValue = _algorithm.Portfolio.TotalPortfolioValue; if (totalPortfolioValue <= 0) { _algorithm.Error("Total portfolio value was less than or equal to 0"); targets = Array.Empty(); return false; } targets = GetPortfolioTargets(totalPortfolioValue).ToArray(); return true; } /// /// Sets the portfolio targets with the given entries and sends them with the algorithm /// being ran to the signal exports providers set, as long as the algorithm is in live mode /// /// One or more portfolio targets to be sent to the defined signal export providers /// True if the portfolio targets could be sent to the different signal export providers successfully, false otherwise public bool SetTargetPortfolio(params PortfolioTarget[] portfolioTargets) { if (!_algorithm.LiveMode) { if (!_isLiveWarningModeLog) { _algorithm.Debug("Portfolio targets are only sent in live mode"); _isLiveWarningModeLog = true; } return true; } if (_signalExports.IsNullOrEmpty()) { return false; } if (portfolioTargets == null || portfolioTargets.Length == 0) { _algorithm.Debug("No portfolio target given"); return false; } var targets = new List(portfolioTargets); var signalExportTargetParameters = new SignalExportTargetParameters { Targets = targets, Algorithm = _algorithm }; var result = true; foreach (var signalExport in _signalExports) { result &= signalExport.Send(signalExportTargetParameters); } return result; } private IEnumerable GetPortfolioTargets(decimal totalPortfolioValue) { foreach (var holding in _algorithm.Portfolio.Values) { var security = _algorithm.Securities[holding.Symbol]; // Skip non-tradeable securities except canonical futures as some signal providers // like Collective2 accept them. // See https://collective2.com/api-docs/latest#Basic_submitsignal_format if (!security.IsTradable && !security.Symbol.IsCanonical()) { continue; } var marginParameters = new InitialMarginParameters(security, holding.Quantity); var adjustedPercent = Math.Abs(security.BuyingPowerModel.GetInitialMarginRequirement(marginParameters) / totalPortfolioValue); // See PortfolioTarget.Percent: // we normalize the target buying power by the leverage so we work in the land of margin var holdingPercent = adjustedPercent * security.BuyingPowerModel.GetLeverage(security); // FreePortfolioValue is used for orders not to be rejected due to volatility when using SetHoldings and CalculateOrderQuantity // Then, we need to substract its value from the TotalPortfolioValue and obtain again the holding percentage for our holding var adjustedHoldingPercent = (holdingPercent * totalPortfolioValue) / _algorithm.Portfolio.TotalPortfolioValueLessFreeBuffer; if (holding.Quantity < 0) { adjustedHoldingPercent *= -1; } 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.Value == Time.EndOfTime && orderEvent.Status.IsFill()) { _initialOrderEventTimeUtc = new(orderEvent.UtcTime); } } /// /// Set the target portfolio after order events. /// /// The current time of synchronous events public void Flush(DateTime currentTimeUtc) { var initialOrderEventTimeUtc = _initialOrderEventTimeUtc.Value; if (_signalExports.IsNullOrEmpty() || initialOrderEventTimeUtc == Time.EndOfTime || !AutomaticExportTimeSpan.HasValue) { return; } if (currentTimeUtc - initialOrderEventTimeUtc < AutomaticExportTimeSpan) { return; } 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); } } }