@@ -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