Lean  $LEAN_TAG$
SignalExportManager.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using Python.Runtime;
18 using QuantConnect.Orders;
19 using QuantConnect.Python;
21 using System.Collections.Generic;
22 using System;
23 using System.Linq;
24 using QuantConnect.Util;
25 
27 {
28  /// <summary>
29  /// Class manager to send portfolio targets to different 3rd party API's
30  /// For example, it allows Collective2, CrunchDAO and Numerai signal export providers
31  /// </summary>
32  public class SignalExportManager
33  {
34  /// <summary>
35  /// Records the time of the first order event of a group of events
36  /// </summary>
37  private ReferenceWrapper<DateTime> _initialOrderEventTimeUtc = new(Time.EndOfTime);
38 
39  /// <summary>
40  /// List of signal export providers
41  /// </summary>
42  private List<ISignalExportTarget> _signalExports;
43 
44  /// <summary>
45  /// Algorithm being ran
46  /// </summary>
47  private readonly IAlgorithm _algorithm;
48 
49  /// <summary>
50  /// Flag to indicate if the user has tried to send signals with live mode off
51  /// </summary>
52  private bool _isLiveWarningModeLog;
53 
54  /// <summary>
55  /// Gets the maximim time span elapsed to export signals after an order event
56  /// If null, disable automatic export.
57  /// </summary>
58  public TimeSpan? AutomaticExportTimeSpan { get; set; } = TimeSpan.FromSeconds(5);
59 
60  /// <summary>
61  /// SignalExportManager Constructor, obtains the entry information needed to send signals
62  /// and initializes the fields to be used
63  /// </summary>
64  /// <param name="algorithm">Algorithm being run</param>
65  public SignalExportManager(IAlgorithm algorithm)
66  {
67  _algorithm = algorithm;
68  _isLiveWarningModeLog = false;
69  }
70 
71  /// <summary>
72  /// Adds a new signal exports provider
73  /// </summary>
74  /// <param name="signalExport">Signal export provider</param>
75  public void AddSignalExportProvider(ISignalExportTarget signalExport)
76  {
77  _signalExports ??= [];
78  _signalExports.Add(signalExport);
79  }
80 
81  /// <summary>
82  /// Adds a new signal exports provider
83  /// </summary>
84  /// <param name="signalExport">Signal export provider</param>
85  public void AddSignalExportProvider(PyObject signalExport)
86  {
87  if (!signalExport.TryConvert<ISignalExportTarget>(out var managedSignalExport))
88  {
89  managedSignalExport = new SignalExportTargetPythonWrapper(signalExport);
90  }
91  AddSignalExportProvider(managedSignalExport);
92  }
93 
94  /// <summary>
95  /// Adds one or more new signal exports providers
96  /// </summary>
97  /// <param name="signalExports">One or more signal export provider</param>
98  public void AddSignalExportProviders(params ISignalExportTarget[] signalExports)
99  {
100  signalExports.DoForEach(AddSignalExportProvider);
101  }
102 
103  /// <summary>
104  /// Adds one or more new signal exports providers
105  /// </summary>
106  /// <param name="signalExports">One or more signal export provider</param>
107  public void AddSignalExportProviders(PyObject signalExports)
108  {
109  using var _ = Py.GIL();
110  if (!signalExports.IsIterable())
111  {
112  AddSignalExportProvider(signalExports);
113  return;
114  }
115  PyList.AsList(signalExports).DoForEach(AddSignalExportProvider);
116  }
117 
118  /// <summary>
119  /// Sets the portfolio targets from the algorihtm's Portfolio and sends them with the
120  /// algorithm being ran to the signal exports providers already set
121  /// </summary>
122  /// <returns>True if the target list could be obtained from the algorithm's Portfolio and they
123  /// were successfully sent to the signal export providers</returns>
125  {
126  if (!GetPortfolioTargets(out PortfolioTarget[] targets))
127  {
128  return false;
129  }
130  var result = SetTargetPortfolio(targets);
131  return result;
132  }
133 
134  /// <summary>
135  /// Obtains an array of portfolio targets from algorithm's Portfolio and returns them.
136  /// See <see cref="PortfolioTarget.Percent(IAlgorithm, Symbol, decimal, bool, string)"/> for more
137  /// information about how each symbol quantity was calculated
138  /// </summary>
139  /// <param name="targets">An array of portfolio targets from the algorithm's Portfolio</param>
140  /// <returns>True if TotalPortfolioValue was bigger than zero, false otherwise</returns>
141  protected bool GetPortfolioTargets(out PortfolioTarget[] targets)
142  {
143  var totalPortfolioValue = _algorithm.Portfolio.TotalPortfolioValue;
144  if (totalPortfolioValue <= 0)
145  {
146  _algorithm.Error("Total portfolio value was less than or equal to 0");
147  targets = Array.Empty<PortfolioTarget>();
148  return false;
149  }
150 
151  targets = GetPortfolioTargets(totalPortfolioValue).ToArray();
152  return true;
153  }
154 
155  /// <summary>
156  /// Sets the portfolio targets with the given entries and sends them with the algorithm
157  /// being ran to the signal exports providers set, as long as the algorithm is in live mode
158  /// </summary>
159  /// <param name="portfolioTargets">One or more portfolio targets to be sent to the defined signal export providers</param>
160  /// <returns>True if the portfolio targets could be sent to the different signal export providers successfully, false otherwise</returns>
161  public bool SetTargetPortfolio(params PortfolioTarget[] portfolioTargets)
162  {
163  if (!_algorithm.LiveMode)
164  {
165  if (!_isLiveWarningModeLog)
166  {
167  _algorithm.Debug("Portfolio targets are only sent in live mode");
168  _isLiveWarningModeLog = true;
169  }
170 
171  return true;
172  }
173 
174  if (_signalExports.IsNullOrEmpty())
175  {
176  return false;
177  }
178 
179  if (portfolioTargets == null || portfolioTargets.Length == 0)
180  {
181  _algorithm.Debug("No portfolio target given");
182  return false;
183  }
184 
185  var targets = new List<PortfolioTarget>(portfolioTargets);
186  var signalExportTargetParameters = new SignalExportTargetParameters
187  {
188  Targets = targets,
189  Algorithm = _algorithm
190  };
191 
192  var result = true;
193  foreach (var signalExport in _signalExports)
194  {
195  result &= signalExport.Send(signalExportTargetParameters);
196  }
197 
198  return result;
199  }
200 
201  private IEnumerable<PortfolioTarget> GetPortfolioTargets(decimal totalPortfolioValue)
202  {
203  foreach (var holding in _algorithm.Portfolio.Values)
204  {
205  var security = _algorithm.Securities[holding.Symbol];
206 
207  // Skip non-tradeable securities except canonical futures as some signal providers
208  // like Collective2 accept them.
209  // See https://collective2.com/api-docs/latest#Basic_submitsignal_format
210  if (!security.IsTradable && !security.Symbol.IsCanonical())
211  {
212  continue;
213  }
214 
215  var marginParameters = new InitialMarginParameters(security, holding.Quantity);
216  var adjustedPercent = Math.Abs(security.BuyingPowerModel.GetInitialMarginRequirement(marginParameters) / totalPortfolioValue);
217 
218  // See PortfolioTarget.Percent:
219  // we normalize the target buying power by the leverage so we work in the land of margin
220  var holdingPercent = adjustedPercent * security.BuyingPowerModel.GetLeverage(security);
221 
222  // FreePortfolioValue is used for orders not to be rejected due to volatility when using SetHoldings and CalculateOrderQuantity
223  // Then, we need to substract its value from the TotalPortfolioValue and obtain again the holding percentage for our holding
224  var adjustedHoldingPercent = (holdingPercent * totalPortfolioValue) / _algorithm.Portfolio.TotalPortfolioValueLessFreeBuffer;
225  if (holding.Quantity < 0)
226  {
227  adjustedHoldingPercent *= -1;
228  }
229 
230  yield return new PortfolioTarget(holding.Symbol, adjustedHoldingPercent);
231  }
232  }
233 
234  /// <summary>
235  /// New order event handler: on order status changes (filled, partially filled, cancelled etc).
236  /// </summary>
237  /// <param name="orderEvent">Event information</param>
238  public void OnOrderEvent(OrderEvent orderEvent)
239  {
240  if (_initialOrderEventTimeUtc.Value == Time.EndOfTime && orderEvent.Status.IsFill())
241  {
242  _initialOrderEventTimeUtc = new(orderEvent.UtcTime);
243  }
244  }
245 
246  /// <summary>
247  /// Set the target portfolio after order events.
248  /// </summary>
249  /// <param name="currentTimeUtc">The current time of synchronous events</param>
250  public void Flush(DateTime currentTimeUtc)
251  {
252  var initialOrderEventTimeUtc = _initialOrderEventTimeUtc.Value;
253  if (_signalExports.IsNullOrEmpty() || initialOrderEventTimeUtc == Time.EndOfTime || !AutomaticExportTimeSpan.HasValue)
254  {
255  return;
256  }
257 
258  if (currentTimeUtc - initialOrderEventTimeUtc < AutomaticExportTimeSpan)
259  {
260  return;
261  }
262 
263  try
264  {
266  }
267  catch (Exception exception)
268  {
269  // SetTargetPortfolioFromPortfolio logs all known error on LEAN side.
270  // Exceptions occurs in the ISignalExportTarget.Send method (user-defined).
271  _algorithm.Error($"Failed to send portfolio target(s). Reason: {exception.Message}.{Environment.NewLine}{exception.StackTrace}");
272  }
273  _initialOrderEventTimeUtc = new(Time.EndOfTime);
274  }
275  }
276 }