Lean  $LEAN_TAG$
All Classes Namespaces Functions Variables Enumerations Enumerator Properties Events Pages
Collective2SignalExport.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 Newtonsoft.Json;
18 using QuantConnect.Util;
19 using System;
20 using System.Collections.Generic;
21 using System.Globalization;
22 using System.Net.Http;
23 using System.Net.Http.Json;
24 using System.Text;
25 
27 {
28  /// <summary>
29  /// Exports signals of desired positions to Collective2 API using JSON and HTTPS.
30  /// Accepts signals in quantity(number of shares) i.e symbol:"SPY", quant:40
31  /// </summary>
33  {
34  /// <summary>
35  /// API key provided by Collective2
36  /// </summary>
37  private readonly string _apiKey;
38 
39  /// <summary>
40  /// Trading system's ID number
41  /// </summary>
42  private readonly int _systemId;
43 
44  /// <summary>
45  /// Algorithm being ran
46  /// </summary>
47  private IAlgorithm _algorithm;
48 
49  /// <summary>
50  /// Flag to track if the warning has already been printed.
51  /// </summary>
52  private bool _isZeroPriceWarningPrinted;
53 
54  /// <summary>
55  /// Collective2 API endpoint
56  /// </summary>
57  public Uri Destination { get; set; }
58 
59  /// <summary>
60  /// The name of this signal export
61  /// </summary>
62  protected override string Name { get; } = "Collective2";
63 
64  /// <summary>
65  /// Lazy initialization of ten seconds rate limiter
66  /// </summary>
67  private static Lazy<RateGate> _tenSecondsRateLimiter = new Lazy<RateGate>(() => new RateGate(100, TimeSpan.FromMilliseconds(1000)));
68 
69  /// <summary>
70  /// Lazy initialization of one hour rate limiter
71  /// </summary>
72  private static Lazy<RateGate> _hourlyRateLimiter = new Lazy<RateGate>(() => new RateGate(1000, TimeSpan.FromHours(1)));
73 
74  /// <summary>
75  /// Lazy initialization of one day rate limiter
76  /// </summary>
77  private static Lazy<RateGate> _dailyRateLimiter = new Lazy<RateGate>(() => new RateGate(20000, TimeSpan.FromDays(1)));
78 
79 
80  /// <summary>
81  /// Collective2SignalExport constructor. It obtains the entry information for Collective2 API requests.
82  /// See API documentation at https://trade.collective2.com/c2-api
83  /// </summary>
84  /// <param name="apiKey">API key provided by Collective2</param>
85  /// <param name="systemId">Trading system's ID number</param>
86  /// <param name="useWhiteLabelApi">Whether to use the white-label API instead of the general one</param>
87  public Collective2SignalExport(string apiKey, int systemId, bool useWhiteLabelApi = false)
88  {
89  _apiKey = apiKey;
90  _systemId = systemId;
91  Destination = new Uri(useWhiteLabelApi
92  ? "https://api4-wl.collective2.com/Strategies/SetDesiredPositions"
93  : "https://api4-general.collective2.com/Strategies/SetDesiredPositions");
94  }
95 
96  /// <summary>
97  /// Creates a JSON message with the desired positions using the expected
98  /// Collective2 API format and then sends it
99  /// </summary>
100  /// <param name="parameters">A list of holdings from the portfolio
101  /// expected to be sent to Collective2 API and the algorithm being ran</param>
102  /// <returns>True if the positions were sent correctly and Collective2 sent no errors, false otherwise</returns>
103  public override bool Send(SignalExportTargetParameters parameters)
104  {
105  if (!base.Send(parameters))
106  {
107  return false;
108  }
109 
110  if (!ConvertHoldingsToCollective2(parameters, out List<Collective2Position> positions))
111  {
112  return false;
113  }
114  var message = CreateMessage(positions);
115  _tenSecondsRateLimiter.Value.WaitToProceed();
116  _hourlyRateLimiter.Value.WaitToProceed();
117  _dailyRateLimiter.Value.WaitToProceed();
118  var result = SendPositions(message);
119 
120  return result;
121  }
122 
123  /// <summary>
124  /// Converts a list of targets to a list of Collective2 positions
125  /// </summary>
126  /// <param name="parameters">A list of targets from the portfolio
127  /// expected to be sent to Collective2 API and the algorithm being ran</param>
128  /// <param name="positions">A list of Collective2 positions</param>
129  /// <returns>True if the given targets could be converted to a Collective2Position list, false otherwise</returns>
130  protected bool ConvertHoldingsToCollective2(SignalExportTargetParameters parameters, out List<Collective2Position> positions)
131  {
132  _algorithm = parameters.Algorithm;
133  var targets = parameters.Targets;
134  positions = new List<Collective2Position>();
135  foreach (var target in targets)
136  {
137  if (target == null)
138  {
139  _algorithm.Error("One portfolio target was null");
140  return false;
141  }
142 
143  if (!ConvertTypeOfSymbol(target.Symbol, out string typeOfSymbol))
144  {
145  return false;
146  }
147 
148  var symbol = _algorithm.Ticker(target.Symbol);
149  if (target.Symbol.SecurityType == SecurityType.Future)
150  {
151  symbol = $"@{SymbolRepresentation.GenerateFutureTicker(target.Symbol.ID.Symbol, target.Symbol.ID.Date, doubleDigitsYear: false, includeExpirationDate: false)}";
152  }
153  else if (target.Symbol.SecurityType.IsOption())
154  {
155  symbol = SymbolRepresentation.GenerateOptionTicker(target.Symbol);
156  }
157 
158  positions.Add(new Collective2Position
159  {
160  C2Symbol = new C2Symbol
161  {
162  FullSymbol = symbol,
163  SymbolType = typeOfSymbol,
164  },
165  Quantity = ConvertPercentageToQuantity(_algorithm, target),
166  });
167  }
168 
169  return true;
170  }
171 
172  /// <summary>
173  /// Classifies a symbol type into the possible symbol types values defined
174  /// by Collective2 API.
175  /// </summary>
176  /// <param name="targetSymbol">Symbol of the desired position</param>
177  /// <param name="typeOfSymbol">The type of the symbol according to Collective2 API</param>
178  /// <returns>True if the symbol's type is supported by Collective2, false otherwise</returns>
179  private bool ConvertTypeOfSymbol(Symbol targetSymbol, out string typeOfSymbol)
180  {
181  switch (targetSymbol.SecurityType)
182  {
183  case SecurityType.Equity:
184  typeOfSymbol = "stock";
185  break;
186  case SecurityType.Option:
187  typeOfSymbol = "option";
188  break;
189  case SecurityType.Future:
190  typeOfSymbol = "future";
191  break;
192  case SecurityType.Forex:
193  typeOfSymbol = "forex";
194  break;
195  case SecurityType.IndexOption:
196  typeOfSymbol = "option";
197  break;
198  default:
199  typeOfSymbol = "NotImplemented";
200  break;
201  }
202 
203  if (typeOfSymbol == "NotImplemented")
204  {
205  _algorithm.Error($"{targetSymbol.SecurityType} security type is not supported by Collective2.");
206  return false;
207  }
208 
209  return true;
210  }
211 
212  /// <summary>
213  /// Converts a given percentage of a position into the number of shares of it
214  /// </summary>
215  /// <param name="algorithm">Algorithm being ran</param>
216  /// <param name="target">Desired position to be sent to the Collective2 API</param>
217  /// <returns>Number of shares hold of the given position</returns>
218  protected int ConvertPercentageToQuantity(IAlgorithm algorithm, PortfolioTarget target)
219  {
220  var numberShares = PortfolioTarget.Percent(algorithm, target.Symbol, target.Quantity);
221  if (numberShares == null)
222  {
223  if (algorithm.Securities.TryGetValue(target.Symbol, out var security) && security.Price == 0 && target.Quantity == 0)
224  {
225  if (!_isZeroPriceWarningPrinted)
226  {
227  _isZeroPriceWarningPrinted = true;
228  algorithm.Debug($"Warning: Collective2 failed to calculate target quantity for {target}. The price for {target.Symbol} is 0, and the target quantity is 0. Will return 0 for all similar cases.");
229  }
230  return 0;
231  }
232  throw new InvalidOperationException($"Collective2 failed to calculate target quantity for {target}");
233  }
234 
235  return (int)numberShares.Quantity;
236  }
237 
238  /// <summary>
239  /// Serializes the list of desired positions with the needed credentials in JSON format
240  /// </summary>
241  /// <param name="positions">List of Collective2 positions to be sent to Collective2 API</param>
242  /// <returns>A JSON request string of the desired positions to be sent by a POST request to Collective2 API</returns>
243  protected string CreateMessage(List<Collective2Position> positions)
244  {
245  var payload = new
246  {
247  StrategyId = _systemId,
248  Positions = positions,
249  };
250 
251  var jsonMessage = JsonConvert.SerializeObject(payload);
252  return jsonMessage;
253  }
254 
255  /// <summary>
256  /// Sends the desired positions list in JSON format to Collective2 API using a POST request. It logs
257  /// the message retrieved by the Collective2 API if there was a HttpRequestException
258  /// </summary>
259  /// <param name="message">A JSON request string of the desired positions list with the credentials</param>
260  /// <returns>True if the positions were sent correctly and Collective2 API sent no errors, false otherwise</returns>
261  private bool SendPositions(string message)
262  {
263  using var httpMessage = new StringContent(message, Encoding.UTF8, "application/json");
264 
265  //Add the QuantConnect app header
266  httpMessage.Headers.Add("X-AppId", "OPA1N90E71");
267 
268  //Add the Authorization header
269  HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiKey);
270 
271  //Send the message
272  using HttpResponseMessage response = HttpClient.PostAsync(Destination, httpMessage).Result;
273 
274  //Parse it
275  var responseObject = response.Content.ReadFromJsonAsync<C2Response>().Result;
276 
277  if (!response.IsSuccessStatusCode)
278  {
279  _algorithm.Error($"Collective2 API returned the following errors: {string.Join(",", PrintErrors(responseObject.ResponseStatus.Errors))}");
280  return false;
281  }
282  else if (responseObject.Results.Count > 0)
283  {
284  _algorithm.Debug($"Collective2: NewSignals={string.Join(',', responseObject.Results[0].NewSignals)} | CanceledSignals={string.Join(',', responseObject.Results[0].CanceledSignals)}");
285  }
286 
287  return true;
288  }
289 
290  private static string PrintErrors(List<ResponseError> errors)
291  {
292  if (errors?.Count == 0)
293  {
294  return "NULL";
295  }
296 
297  StringBuilder sb = new StringBuilder();
298  foreach (var error in errors)
299  {
300  sb.AppendLine(CultureInfo.InvariantCulture, $"({error.ErrorCode}) {error.FieldName}: {error.Message}");
301  }
302 
303  return sb.ToString();
304  }
305 
306  /// <summary>
307  /// The main C2 response class for this endpoint
308  /// </summary>
309  private class C2Response
310  {
311  [JsonProperty(PropertyName = "Results")]
312  public virtual List<DesiredPositionResponse> Results { get; set; }
313 
314 
315  [JsonProperty(PropertyName = "ResponseStatus")]
316  public ResponseStatus ResponseStatus { get; set; }
317  }
318 
319  /// <summary>
320  /// The Results object
321  /// </summary>
322  private class DesiredPositionResponse
323  {
324  [JsonProperty(PropertyName = "NewSignals")]
325  public List<long> NewSignals { get; set; } = new List<long>();
326 
327 
328  [JsonProperty(PropertyName = "CanceledSignals")]
329  public List<long> CanceledSignals { get; set; } = new List<long>();
330  }
331 
332  /// <summary>
333  /// The C2 ResponseStatus object
334  /// </summary>
335  private class ResponseStatus
336  {
337  /* Example:
338 
339  "ResponseStatus":
340  {
341  "ErrorCode": ""401",
342  "Message": ""Unauthorized",
343  "Errors": [
344  {
345  "ErrorCode": "2015",
346  "FieldName": "APIKey",
347  "Message": ""Unknown API Key"
348  }
349  ]
350  }
351  */
352 
353 
354  [JsonProperty(PropertyName = "ErrorCode")]
355  public string ErrorCode { get; set; }
356 
357 
358  [JsonProperty(PropertyName = "Message")]
359  public string Message { get; set; }
360 
361 
362  [JsonProperty(PropertyName = "Errors")]
363  public List<ResponseError> Errors { get; set; }
364 
365  }
366 
367  /// <summary>
368  /// The ResponseError object
369  /// </summary>
370  private class ResponseError
371  {
372  [JsonProperty(PropertyName = "ErrorCode")]
373  public string ErrorCode { get; set; }
374 
375 
376  [JsonProperty(PropertyName = "FieldName")]
377  public string FieldName { get; set; }
378 
379 
380  [JsonProperty(PropertyName = "Message")]
381  public string Message { get; set; }
382  }
383 
384  /// <summary>
385  /// Stores position's needed information to be serialized in JSON format
386  /// and then sent to Collective2 API
387  /// </summary>
388  protected class Collective2Position
389  {
390  /// <summary>
391  /// Position symbol
392  /// </summary>
393  [JsonProperty(PropertyName = "C2Symbol")]
394  public C2Symbol C2Symbol { get; set; }
395 
396  /// <summary>
397  /// Number of shares/contracts of the given symbol. Positive quantites are long positions
398  /// and negative short positions.
399  /// </summary>
400  [JsonProperty(PropertyName = "Quantity")]
401  public decimal Quantity { get; set; } // number of shares, not % of the portfolio
402  }
403 
404  /// <summary>
405  /// The Collective2 symbol
406  /// </summary>
407  protected class C2Symbol
408  {
409  /// <summary>
410  /// The The full native C2 symbol e.g. BSRR2121Q22.5
411  /// </summary>
412  [JsonProperty(PropertyName = "FullSymbol")]
413  public string FullSymbol { get; set; }
414 
415 
416  /// <summary>
417  /// The type of instrument. e.g. 'stock', 'option', 'future', 'forex'
418  /// </summary>
419  [JsonProperty(PropertyName = "SymbolType")]
420  public string SymbolType { get; set; }
421  }
422  }
423 }