16 using Newtonsoft.Json;
20 using System.Collections.Generic;
21 using System.Globalization;
22 using System.Net.Http;
23 using System.Net.Http.Json;
38 private HashSet<string> _unknownMarketSymbols;
43 private HashSet<SecurityType> _unknownSecurityTypes;
48 private readonly
string _apiKey;
53 private readonly
int _systemId;
63 private bool _isZeroPriceWarningPrinted;
73 protected override string Name {
get; } =
"Collective2";
78 private static Lazy<RateGate> _tenSecondsRateLimiter =
new Lazy<RateGate>(() =>
new RateGate(100, TimeSpan.FromMilliseconds(1000)));
83 private static Lazy<RateGate> _hourlyRateLimiter =
new Lazy<RateGate>(() =>
new RateGate(1000, TimeSpan.FromHours(1)));
88 private static Lazy<RateGate> _dailyRateLimiter =
new Lazy<RateGate>(() =>
new RateGate(20000, TimeSpan.FromDays(1)));
100 _unknownMarketSymbols =
new HashSet<string>();
101 _unknownSecurityTypes =
new HashSet<SecurityType>();
103 _systemId = systemId;
105 ?
"https://api4-wl.collective2.com/Strategies/SetDesiredPositions"
106 :
"https://api4-general.collective2.com/Strategies/SetDesiredPositions");
118 if (!base.Send(parameters))
128 _tenSecondsRateLimiter.Value.WaitToProceed();
129 _hourlyRateLimiter.Value.WaitToProceed();
130 _dailyRateLimiter.Value.WaitToProceed();
131 var result = SendPositions(message);
146 var targets = parameters.
Targets;
148 foreach (var target
in targets)
152 _algorithm.
Error(
"One portfolio target was null");
156 var securityType = GetSecurityTypeAcronym(target.Symbol.SecurityType);
157 if (securityType ==
null)
162 var maturityMonthYear = GetMaturityMonthYear(target.Symbol);
163 if (maturityMonthYear?.Length == 0)
172 Symbol = GetSymbol(target.Symbol),
173 Currency = parameters.Algorithm.AccountCurrency,
174 SecurityExchange = GetMICExchangeCode(target.Symbol),
176 MaturityMonthYear = maturityMonthYear,
177 PutOrCall = GetPutOrCallValue(target.Symbol),
178 StrikePrice = GetStrikePrice(target.Symbol)
196 if (numberShares ==
null)
198 if (algorithm.
Securities.TryGetValue(target.
Symbol, out var security) && security.Price == 0 && target.
Quantity == 0)
200 if (!_isZeroPriceWarningPrinted)
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.");
207 throw new InvalidOperationException($
"Collective2 failed to calculate target quantity for {target}");
210 return (
int)numberShares.Quantity;
222 StrategyId = _systemId,
223 Positions = positions,
226 var jsonMessage = JsonConvert.SerializeObject(payload);
236 private bool SendPositions(
string message)
238 using var httpMessage =
new StringContent(message, Encoding.UTF8,
"application/json");
241 httpMessage.Headers.Add(
"X-AppId",
"OPA1N90E71");
244 HttpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer", _apiKey);
250 var responseObject = response.Content.ReadFromJsonAsync<C2Response>().
Result;
253 var debuggingMessage = Logging.Log.DebuggingEnabled ? $
" | Message={message}" :
string.Empty;
255 if (!response.IsSuccessStatusCode)
257 _algorithm.
Error($
"Collective2 API returned the following errors: {string.Join(",
", PrintErrors(responseObject.ResponseStatus.Errors))}{debuggingMessage}");
260 else if (responseObject.Results.Count > 0)
262 _algorithm.
Debug($
"Collective2: NewSignals={string.Join(',', responseObject.Results[0].NewSignals)} | CanceledSignals={string.Join(',', responseObject.Results[0].CanceledSignals)}{debuggingMessage}");
268 private static string PrintErrors(List<ResponseError> errors)
270 if (errors?.Count == 0)
275 StringBuilder sb =
new StringBuilder();
276 foreach (var error
in errors)
278 sb.AppendLine(CultureInfo.InvariantCulture, $
"({error.ErrorCode}) {error.FieldName}: {error.Message}");
281 return sb.ToString();
287 private class C2Response
289 [JsonProperty(PropertyName =
"Results")]
290 public virtual List<DesiredPositionResponse> Results {
get;
set; }
293 [JsonProperty(PropertyName =
"ResponseStatus")]
294 public ResponseStatus ResponseStatus {
get;
set; }
300 private class DesiredPositionResponse
302 [JsonProperty(PropertyName =
"NewSignals")]
303 public List<long> NewSignals {
get;
set; } =
new List<long>();
306 [JsonProperty(PropertyName =
"CanceledSignals")]
307 public List<long> CanceledSignals {
get;
set; } =
new List<long>();
313 private string GetSymbol(Symbol symbol)
317 return $
"{baseCurrency}/{quoteCurrency}";
319 else if (symbol.SecurityType.IsOption())
321 return symbol.Underlying.Value;
325 return symbol.ID.Symbol;
329 private string GetMICExchangeCode(Symbol symbol)
331 if (symbol.SecurityType ==
SecurityType.Equity || symbol.SecurityType.IsOption())
336 switch (symbol.ID.Market)
342 case Market.NYSELIFFE:
361 return symbol.ID.Market.ToUpper();
364 return $
"X{symbol.ID.Market.ToUpper()}";
366 if (_unknownMarketSymbols.Add(symbol.Value))
368 _algorithm.
Debug($
"The market of the symbol {symbol.Value} was unexpected: {symbol.ID.Market}. Using 'DEFAULT' as market");
378 private string GetSecurityTypeAcronym(
SecurityType securityType)
380 switch (securityType)
392 if (_unknownSecurityTypes.Add(securityType))
394 _algorithm.
Debug($
"Unexpected security type found: {securityType}. Collective2 just accepts: Equity, Future, Option, Index Option and Stock");
403 private string GetMaturityMonthYear(Symbol symbol)
405 var delistingDate = symbol.GetDelistingDate();
406 if (delistingDate == Time.EndOfTime)
411 if (delistingDate < _algorithm.
Securities[symbol].LocalTime.Date)
413 _algorithm.
Error($
"Instrument {symbol} has already expired. Its delisting date was: {delistingDate}. This signal won't be sent to Collective2.");
417 return $
"{delistingDate:yyyyMMdd}";
420 private int? GetPutOrCallValue(Symbol symbol)
422 if (symbol.SecurityType.IsOption())
424 switch (symbol.ID.OptionRight)
436 private decimal? GetStrikePrice(Symbol symbol)
438 if (symbol.SecurityType.IsOption())
440 return symbol.ID.StrikePrice;
451 private class ResponseStatus
470 [JsonProperty(PropertyName =
"ErrorCode")]
471 public string ErrorCode {
get;
set; }
474 [JsonProperty(PropertyName =
"Message")]
475 public string Message {
get;
set; }
478 [JsonProperty(PropertyName =
"Errors")]
479 public List<ResponseError> Errors {
get;
set; }
486 private class ResponseError
488 [JsonProperty(PropertyName =
"ErrorCode")]
489 public string ErrorCode {
get;
set; }
492 [JsonProperty(PropertyName =
"FieldName")]
493 public string FieldName {
get;
set; }
496 [JsonProperty(PropertyName =
"Message")]
497 public string Message {
get;
set; }
509 [JsonProperty(PropertyName =
"exchangeSymbol")]
516 [JsonProperty(PropertyName =
"quantity")]
528 [JsonProperty(PropertyName =
"symbol")]
534 [JsonProperty(PropertyName =
"currency")]
542 [JsonProperty(PropertyName =
"securityExchange")]
549 [JsonProperty(PropertyName =
"securityType")]
555 [JsonProperty(PropertyName =
"maturityMonthYear")]
561 [JsonProperty(PropertyName =
"putOrCall")]
567 [JsonProperty(PropertyName =
"strikePrice")]
573 [JsonProperty(PropertyName =
"priceMultiplier")]