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