Skip to content

Commit bca9ada

Browse files
authored
Replace C2 symbol for C2 Exchange symbol (#8604)
* Initial draft of the solution * Remove old format * Address requested changes * Add Index option test case * nit changes * Address Martin reviews, improve unit tests and solve 8577 * Add unit tests * Nit change * Address requested changes * Address Martin reviews * Nit changes and more unit tests * Nit change * Address requested changes * Address requested changes
1 parent 5cba110 commit bca9ada

File tree

2 files changed

+322
-82
lines changed

2 files changed

+322
-82
lines changed

Common/Algorithm/Framework/Portfolio/SignalExports/Collective2SignalExport.cs

Lines changed: 214 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ namespace QuantConnect.Algorithm.Framework.Portfolio.SignalExports
3131
/// </summary>
3232
public class Collective2SignalExport : BaseSignalExport
3333
{
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+
3445
/// <summary>
3546
/// API key provided by Collective2
3647
/// </summary>
@@ -86,6 +97,8 @@ public class Collective2SignalExport : BaseSignalExport
8697
/// <param name="useWhiteLabelApi">Whether to use the white-label API instead of the general one</param>
8798
public Collective2SignalExport(string apiKey, int systemId, bool useWhiteLabelApi = false)
8899
{
100+
_unknownMarketSymbols = new HashSet<string>();
101+
_unknownSecurityTypes = new HashSet<SecurityType>();
89102
_apiKey = apiKey;
90103
_systemId = systemId;
91104
Destination = new Uri(useWhiteLabelApi
@@ -131,7 +144,7 @@ protected bool ConvertHoldingsToCollective2(SignalExportTargetParameters paramet
131144
{
132145
_algorithm = parameters.Algorithm;
133146
var targets = parameters.Targets;
134-
positions = new List<Collective2Position>();
147+
positions = [];
135148
foreach (var target in targets)
136149
{
137150
if (target == null)
@@ -140,27 +153,29 @@ protected bool ConvertHoldingsToCollective2(SignalExportTargetParameters paramet
140153
return false;
141154
}
142155

143-
if (!ConvertTypeOfSymbol(target.Symbol, out string typeOfSymbol))
156+
var securityType = GetSecurityTypeAcronym(target.Symbol.SecurityType);
157+
if (securityType == null)
144158
{
145-
return false;
159+
continue;
146160
}
147161

148-
var symbol = _algorithm.Ticker(target.Symbol);
149-
if (target.Symbol.SecurityType == SecurityType.Future)
162+
var maturityMonthYear = GetMaturityMonthYear(target.Symbol);
163+
if (maturityMonthYear?.Length == 0)
150164
{
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);
165+
continue;
156166
}
157167

158168
positions.Add(new Collective2Position
159169
{
160-
C2Symbol = new C2Symbol
170+
ExchangeSymbol = new C2ExchangeSymbol
161171
{
162-
FullSymbol = symbol,
163-
SymbolType = typeOfSymbol,
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)
164179
},
165180
Quantity = ConvertPercentageToQuantity(_algorithm, target),
166181
});
@@ -169,46 +184,6 @@ protected bool ConvertHoldingsToCollective2(SignalExportTargetParameters paramet
169184
return true;
170185
}
171186

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-
212187
/// <summary>
213188
/// Converts a given percentage of a position into the number of shares of it
214189
/// </summary>
@@ -332,6 +307,144 @@ private class DesiredPositionResponse
332307
public List<long> CanceledSignals { get; set; } = new List<long>();
333308
}
334309

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+
335448
/// <summary>
336449
/// The C2 ResponseStatus object
337450
/// </summary>
@@ -393,34 +506,72 @@ protected class Collective2Position
393506
/// <summary>
394507
/// Position symbol
395508
/// </summary>
396-
[JsonProperty(PropertyName = "C2Symbol")]
397-
public C2Symbol C2Symbol { get; set; }
509+
[JsonProperty(PropertyName = "exchangeSymbol")]
510+
public C2ExchangeSymbol ExchangeSymbol { get; set; }
398511

399512
/// <summary>
400513
/// Number of shares/contracts of the given symbol. Positive quantites are long positions
401514
/// and negative short positions.
402515
/// </summary>
403-
[JsonProperty(PropertyName = "Quantity")]
516+
[JsonProperty(PropertyName = "quantity")]
404517
public decimal Quantity { get; set; } // number of shares, not % of the portfolio
405518
}
406519

407520
/// <summary>
408521
/// The Collective2 symbol
409522
/// </summary>
410-
protected class C2Symbol
523+
protected class C2ExchangeSymbol
411524
{
412525
/// <summary>
413-
/// The The full native C2 symbol e.g. BSRR2121Q22.5
526+
/// The exchange root symbol e.g. AAPL
414527
/// </summary>
415-
[JsonProperty(PropertyName = "FullSymbol")]
416-
public string FullSymbol { get; set; }
528+
[JsonProperty(PropertyName = "symbol")]
529+
public string Symbol { get; set; }
417530

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; }
418569

419570
/// <summary>
420-
/// The type of instrument. e.g. 'stock', 'option', 'future', 'forex'
571+
/// The multiplier to apply to the Exchange price to get the C2-formatted price. Default is 1
421572
/// </summary>
422-
[JsonProperty(PropertyName = "SymbolType")]
423-
public string SymbolType { get; set; }
573+
[JsonProperty(PropertyName = "priceMultiplier")]
574+
public decimal PriceMultiplier { get; set; } = 1;
424575
}
425576
}
426577
}

0 commit comments

Comments
 (0)