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  /// Hashset of symbols whose market is unknown but have already been seen by
36  /// this signal export manager
37  /// </summary>
38  private HashSet<string> _unknownMarketSymbols;
39 
40  /// <summary>
41  /// Hashset of security types seen that are unsupported by C2 API
42  /// </summary>
43  private HashSet<SecurityType> _unknownSecurityTypes;
44 
45  /// <summary>
46  /// API key provided by Collective2
47  /// </summary>
48  private readonly string _apiKey;
49 
50  /// <summary>
51  /// Trading system's ID number
52  /// </summary>
53  private readonly int _systemId;
54 
55  /// <summary>
56  /// Algorithm being ran
57  /// </summary>
58  private IAlgorithm _algorithm;
59 
60  /// <summary>
61  /// Flag to track if the warning has already been printed.
62  /// </summary>
63  private bool _isZeroPriceWarningPrinted;
64 
65  /// <summary>
66  /// Collective2 API endpoint
67  /// </summary>
68  public Uri Destination { get; set; }
69 
70  /// <summary>
71  /// The name of this signal export
72  /// </summary>
73  protected override string Name { get; } = "Collective2";
74 
75  /// <summary>
76  /// Lazy initialization of ten seconds rate limiter
77  /// </summary>
78  private static Lazy<RateGate> _tenSecondsRateLimiter = new Lazy<RateGate>(() => new RateGate(100, TimeSpan.FromMilliseconds(1000)));
79 
80  /// <summary>
81  /// Lazy initialization of one hour rate limiter
82  /// </summary>
83  private static Lazy<RateGate> _hourlyRateLimiter = new Lazy<RateGate>(() => new RateGate(1000, TimeSpan.FromHours(1)));
84 
85  /// <summary>
86  /// Lazy initialization of one day rate limiter
87  /// </summary>
88  private static Lazy<RateGate> _dailyRateLimiter = new Lazy<RateGate>(() => new RateGate(20000, TimeSpan.FromDays(1)));
89 
90 
91  /// <summary>
92  /// Collective2SignalExport constructor. It obtains the entry information for Collective2 API requests.
93  /// See API documentation at https://trade.collective2.com/c2-api
94  /// </summary>
95  /// <param name="apiKey">API key provided by Collective2</param>
96  /// <param name="systemId">Trading system's ID number</param>
97  /// <param name="useWhiteLabelApi">Whether to use the white-label API instead of the general one</param>
98  public Collective2SignalExport(string apiKey, int systemId, bool useWhiteLabelApi = false)
99  {
100  _unknownMarketSymbols = new HashSet<string>();
101  _unknownSecurityTypes = new HashSet<SecurityType>();
102  _apiKey = apiKey;
103  _systemId = systemId;
104  Destination = new Uri(useWhiteLabelApi
105  ? "https://api4-wl.collective2.com/Strategies/SetDesiredPositions"
106  : "https://api4-general.collective2.com/Strategies/SetDesiredPositions");
107  }
108 
109  /// <summary>
110  /// Creates a JSON message with the desired positions using the expected
111  /// Collective2 API format and then sends it
112  /// </summary>
113  /// <param name="parameters">A list of holdings from the portfolio
114  /// expected to be sent to Collective2 API and the algorithm being ran</param>
115  /// <returns>True if the positions were sent correctly and Collective2 sent no errors, false otherwise</returns>
116  public override bool Send(SignalExportTargetParameters parameters)
117  {
118  if (!base.Send(parameters))
119  {
120  return false;
121  }
122 
123  if (!ConvertHoldingsToCollective2(parameters, out List<Collective2Position> positions))
124  {
125  return false;
126  }
127  var message = CreateMessage(positions);
128  _tenSecondsRateLimiter.Value.WaitToProceed();
129  _hourlyRateLimiter.Value.WaitToProceed();
130  _dailyRateLimiter.Value.WaitToProceed();
131  var result = SendPositions(message);
132 
133  return result;
134  }
135 
136  /// <summary>
137  /// Converts a list of targets to a list of Collective2 positions
138  /// </summary>
139  /// <param name="parameters">A list of targets from the portfolio
140  /// expected to be sent to Collective2 API and the algorithm being ran</param>
141  /// <param name="positions">A list of Collective2 positions</param>
142  /// <returns>True if the given targets could be converted to a Collective2Position list, false otherwise</returns>
143  protected bool ConvertHoldingsToCollective2(SignalExportTargetParameters parameters, out List<Collective2Position> positions)
144  {
145  _algorithm = parameters.Algorithm;
146  var targets = parameters.Targets;
147  positions = [];
148  foreach (var target in targets)
149  {
150  if (target == null)
151  {
152  _algorithm.Error("One portfolio target was null");
153  return false;
154  }
155 
156  var securityType = GetSecurityTypeAcronym(target.Symbol.SecurityType);
157  if (securityType == null)
158  {
159  continue;
160  }
161 
162  var maturityMonthYear = GetMaturityMonthYear(target.Symbol);
163  if (maturityMonthYear?.Length == 0)
164  {
165  continue;
166  }
167 
168  positions.Add(new Collective2Position
169  {
170  ExchangeSymbol = new C2ExchangeSymbol
171  {
172  Symbol = GetSymbol(target.Symbol),
173  Currency = parameters.Algorithm.AccountCurrency,
174  SecurityExchange = GetMICExchangeCode(target.Symbol),
175  SecurityType = securityType,
176  MaturityMonthYear = maturityMonthYear,
177  PutOrCall = GetPutOrCallValue(target.Symbol),
178  StrikePrice = GetStrikePrice(target.Symbol)
179  },
180  Quantity = ConvertPercentageToQuantity(_algorithm, target),
181  });
182  }
183 
184  return true;
185  }
186 
187  /// <summary>
188  /// Converts a given percentage of a position into the number of shares of it
189  /// </summary>
190  /// <param name="algorithm">Algorithm being ran</param>
191  /// <param name="target">Desired position to be sent to the Collective2 API</param>
192  /// <returns>Number of shares hold of the given position</returns>
193  protected int ConvertPercentageToQuantity(IAlgorithm algorithm, PortfolioTarget target)
194  {
195  var numberShares = PortfolioTarget.Percent(algorithm, target.Symbol, target.Quantity);
196  if (numberShares == null)
197  {
198  if (algorithm.Securities.TryGetValue(target.Symbol, out var security) && security.Price == 0 && target.Quantity == 0)
199  {
200  if (!_isZeroPriceWarningPrinted)
201  {
202  _isZeroPriceWarningPrinted = true;
203  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.");
204  }
205  return 0;
206  }
207  throw new InvalidOperationException($"Collective2 failed to calculate target quantity for {target}");
208  }
209 
210  return (int)numberShares.Quantity;
211  }
212 
213  /// <summary>
214  /// Serializes the list of desired positions with the needed credentials in JSON format
215  /// </summary>
216  /// <param name="positions">List of Collective2 positions to be sent to Collective2 API</param>
217  /// <returns>A JSON request string of the desired positions to be sent by a POST request to Collective2 API</returns>
218  protected string CreateMessage(List<Collective2Position> positions)
219  {
220  var payload = new
221  {
222  StrategyId = _systemId,
223  Positions = positions,
224  };
225 
226  var jsonMessage = JsonConvert.SerializeObject(payload);
227  return jsonMessage;
228  }
229 
230  /// <summary>
231  /// Sends the desired positions list in JSON format to Collective2 API using a POST request. It logs
232  /// the message retrieved by the Collective2 API if there was a HttpRequestException
233  /// </summary>
234  /// <param name="message">A JSON request string of the desired positions list with the credentials</param>
235  /// <returns>True if the positions were sent correctly and Collective2 API sent no errors, false otherwise</returns>
236  private bool SendPositions(string message)
237  {
238  using var httpMessage = new StringContent(message, Encoding.UTF8, "application/json");
239 
240  //Add the QuantConnect app header
241  httpMessage.Headers.Add("X-AppId", "OPA1N90E71");
242 
243  //Add the Authorization header
244  HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiKey);
245 
246  //Send the message
247  using HttpResponseMessage response = HttpClient.PostAsync(Destination, httpMessage).Result;
248 
249  //Parse it
250  var responseObject = response.Content.ReadFromJsonAsync<C2Response>().Result;
251 
252  //For debugging purposes, append the message sent to Collective2 to the algorithms log
253  var debuggingMessage = Logging.Log.DebuggingEnabled ? $" | Message={message}" : string.Empty;
254 
255  if (!response.IsSuccessStatusCode)
256  {
257  _algorithm.Error($"Collective2 API returned the following errors: {string.Join(",", PrintErrors(responseObject.ResponseStatus.Errors))}{debuggingMessage}");
258  return false;
259  }
260  else if (responseObject.Results.Count > 0)
261  {
262  _algorithm.Debug($"Collective2: NewSignals={string.Join(',', responseObject.Results[0].NewSignals)} | CanceledSignals={string.Join(',', responseObject.Results[0].CanceledSignals)}{debuggingMessage}");
263  }
264 
265  return true;
266  }
267 
268  private static string PrintErrors(List<ResponseError> errors)
269  {
270  if (errors?.Count == 0)
271  {
272  return "NULL";
273  }
274 
275  StringBuilder sb = new StringBuilder();
276  foreach (var error in errors)
277  {
278  sb.AppendLine(CultureInfo.InvariantCulture, $"({error.ErrorCode}) {error.FieldName}: {error.Message}");
279  }
280 
281  return sb.ToString();
282  }
283 
284  /// <summary>
285  /// The main C2 response class for this endpoint
286  /// </summary>
287  private class C2Response
288  {
289  [JsonProperty(PropertyName = "Results")]
290  public virtual List<DesiredPositionResponse> Results { get; set; }
291 
292 
293  [JsonProperty(PropertyName = "ResponseStatus")]
294  public ResponseStatus ResponseStatus { get; set; }
295  }
296 
297  /// <summary>
298  /// The Results object
299  /// </summary>
300  private class DesiredPositionResponse
301  {
302  [JsonProperty(PropertyName = "NewSignals")]
303  public List<long> NewSignals { get; set; } = new List<long>();
304 
305 
306  [JsonProperty(PropertyName = "CanceledSignals")]
307  public List<long> CanceledSignals { get; set; } = new List<long>();
308  }
309 
310  /// <summary>
311  /// Returns the given symbol in the expected C2 format
312  /// </summary>
313  private string GetSymbol(Symbol symbol)
314  {
315  if (CurrencyPairUtil.TryDecomposeCurrencyPair(symbol, out var baseCurrency, out var quoteCurrency))
316  {
317  return $"{baseCurrency}/{quoteCurrency}";
318  }
319  else if (symbol.SecurityType.IsOption())
320  {
321  return symbol.Underlying.Value;
322  }
323  else
324  {
325  return symbol.ID.Symbol;
326  }
327  }
328 
329  private string GetMICExchangeCode(Symbol symbol)
330  {
331  if (symbol.SecurityType == SecurityType.Equity || symbol.SecurityType.IsOption())
332  {
333  return "DEFAULT";
334  }
335 
336  switch (symbol.ID.Market)
337  {
338  case Market.India:
339  return "XNSE";
340  case Market.HKFE:
341  return "XHKF";
342  case Market.NYSELIFFE:
343  return "XNLI";
344  case Market.EUREX:
345  return "XEUR";
346  case Market.ICE:
347  return "IEPA";
348  case Market.CBOE:
349  return "XCBO";
350  case Market.CFE:
351  return "XCBF";
352  case Market.CBOT:
353  return "XCBT";
354  case Market.COMEX:
355  return "XCEC";
356  case Market.NYMEX:
357  return "XNYM";
358  case Market.SGX:
359  return "XSES";
360  case Market.FXCM:
361  return symbol.ID.Market.ToUpper();
362  case Market.OSE:
363  case Market.CME:
364  return $"X{symbol.ID.Market.ToUpper()}";
365  default:
366  if (_unknownMarketSymbols.Add(symbol.Value))
367  {
368  _algorithm.Debug($"The market of the symbol {symbol.Value} was unexpected: {symbol.ID.Market}. Using 'DEFAULT' as market");
369  }
370 
371  return "DEFAULT";
372  }
373  }
374 
375  /// <summary>
376  /// Returns the given security type in the format C2 expects
377  /// </summary>
378  private string GetSecurityTypeAcronym(SecurityType securityType)
379  {
380  switch (securityType)
381  {
382  case SecurityType.Equity:
383  return "CS";
384  case SecurityType.Future:
385  return "FUT";
386  case SecurityType.Option:
387  case SecurityType.IndexOption:
388  return "OPT";
389  case SecurityType.Forex:
390  return "FOR";
391  default:
392  if (_unknownSecurityTypes.Add(securityType))
393  {
394  _algorithm.Debug($"Unexpected security type found: {securityType}. Collective2 just accepts: Equity, Future, Option, Index Option and Stock");
395  }
396  return null;
397  }
398  }
399 
400  /// <summary>
401  /// Returns the expiration date in the format C2 expects
402  /// </summary>
403  private string GetMaturityMonthYear(Symbol symbol)
404  {
405  var delistingDate = symbol.GetDelistingDate();
406  if (delistingDate == Time.EndOfTime) // The given symbol is equity or forex
407  {
408  return null;
409  }
410 
411  if (delistingDate < _algorithm.Securities[symbol].LocalTime.Date) // The given symbol has already expired
412  {
413  _algorithm.Error($"Instrument {symbol} has already expired. Its delisting date was: {delistingDate}. This signal won't be sent to Collective2.");
414  return string.Empty;
415  }
416 
417  return $"{delistingDate:yyyyMMdd}";
418  }
419 
420  private int? GetPutOrCallValue(Symbol symbol)
421  {
422  if (symbol.SecurityType.IsOption())
423  {
424  switch (symbol.ID.OptionRight)
425  {
426  case OptionRight.Put:
427  return 0;
428  case OptionRight.Call:
429  return 1;
430  }
431  }
432 
433  return null;
434  }
435 
436  private decimal? GetStrikePrice(Symbol symbol)
437  {
438  if (symbol.SecurityType.IsOption())
439  {
440  return symbol.ID.StrikePrice;
441  }
442  else
443  {
444  return null;
445  }
446  }
447 
448  /// <summary>
449  /// The C2 ResponseStatus object
450  /// </summary>
451  private class ResponseStatus
452  {
453  /* Example:
454 
455  "ResponseStatus":
456  {
457  "ErrorCode": ""401",
458  "Message": ""Unauthorized",
459  "Errors": [
460  {
461  "ErrorCode": "2015",
462  "FieldName": "APIKey",
463  "Message": ""Unknown API Key"
464  }
465  ]
466  }
467  */
468 
469 
470  [JsonProperty(PropertyName = "ErrorCode")]
471  public string ErrorCode { get; set; }
472 
473 
474  [JsonProperty(PropertyName = "Message")]
475  public string Message { get; set; }
476 
477 
478  [JsonProperty(PropertyName = "Errors")]
479  public List<ResponseError> Errors { get; set; }
480 
481  }
482 
483  /// <summary>
484  /// The ResponseError object
485  /// </summary>
486  private class ResponseError
487  {
488  [JsonProperty(PropertyName = "ErrorCode")]
489  public string ErrorCode { get; set; }
490 
491 
492  [JsonProperty(PropertyName = "FieldName")]
493  public string FieldName { get; set; }
494 
495 
496  [JsonProperty(PropertyName = "Message")]
497  public string Message { get; set; }
498  }
499 
500  /// <summary>
501  /// Stores position's needed information to be serialized in JSON format
502  /// and then sent to Collective2 API
503  /// </summary>
504  protected class Collective2Position
505  {
506  /// <summary>
507  /// Position symbol
508  /// </summary>
509  [JsonProperty(PropertyName = "exchangeSymbol")]
510  public C2ExchangeSymbol ExchangeSymbol { get; set; }
511 
512  /// <summary>
513  /// Number of shares/contracts of the given symbol. Positive quantites are long positions
514  /// and negative short positions.
515  /// </summary>
516  [JsonProperty(PropertyName = "quantity")]
517  public decimal Quantity { get; set; } // number of shares, not % of the portfolio
518  }
519 
520  /// <summary>
521  /// The Collective2 symbol
522  /// </summary>
523  protected class C2ExchangeSymbol
524  {
525  /// <summary>
526  /// The exchange root symbol e.g. AAPL
527  /// </summary>
528  [JsonProperty(PropertyName = "symbol")]
529  public string Symbol { get; set; }
530 
531  /// <summary>
532  /// The 3-character ISO instrument currency. E.g. 'USD'
533  /// </summary>
534  [JsonProperty(PropertyName = "currency")]
535  public string Currency { get; set; }
536 
537  /// <summary>
538  /// The MIC Exchange code e.g. DEFAULT (for stocks & options),
539  /// XCME, XEUR, XICE, XLIF, XNYB, XNYM, XASX, XCBF, XCBT, XCEC,
540  /// XKBT, XSES. See details at http://www.iso15022.org/MIC/homepageMIC.htm
541  /// </summary>
542  [JsonProperty(PropertyName = "securityExchange")]
543  public string SecurityExchange { get; set; }
544 
545 
546  /// <summary>
547  /// The SecurityType e.g. 'CS'(Common Stock), 'FUT' (Future), 'OPT' (Option), 'FOR' (Forex)
548  /// </summary>
549  [JsonProperty(PropertyName = "securityType")]
550  public string SecurityType { get; set; }
551 
552  /// <summary>
553  /// The MaturityMonthYear e.g. '202103' (March 2021), or if the contract requires a day: '20210521' (May 21, 2021)
554  /// </summary>
555  [JsonProperty(PropertyName = "maturityMonthYear")]
556  public string MaturityMonthYear { get; set; }
557 
558  /// <summary>
559  /// The Option PutOrCall e.g. 0 = Put, 1 = Call
560  /// </summary>
561  [JsonProperty(PropertyName = "putOrCall")]
562  public int? PutOrCall { get; set; }
563 
564  /// <summary>
565  /// The ISO Option Strike Price. Zero means none
566  /// </summary>
567  [JsonProperty(PropertyName = "strikePrice")]
568  public decimal? StrikePrice { get; set; }
569 
570  /// <summary>
571  /// The multiplier to apply to the Exchange price to get the C2-formatted price. Default is 1
572  /// </summary>
573  [JsonProperty(PropertyName = "priceMultiplier")]
574  public decimal PriceMultiplier { get; set; } = 1;
575  }
576  }
577 }