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