@@ -299,7 +299,7 @@ public Universe AddUniverse(PyObject pyObject)
299299 return AddUniverse ( pyObject , null , null ) ;
300300 }
301301 // TODO: to be removed when https://github.com/QuantConnect/pythonnet/issues/62 is solved
302- else if ( pyObject . TryConvert ( out universe ) )
302+ else if ( pyObject . TryConvert ( out universe ) )
303303 {
304304 return AddUniverse ( universe ) ;
305305 }
@@ -569,6 +569,65 @@ public void AddUniverseOptions(PyObject universe, PyObject optionFilter)
569569 }
570570 }
571571
572+ /// <summary>
573+ /// Creates a new <see cref="CompositeIndicator"/> using two indicators and a custom Python function as a handler.
574+ /// </summary>
575+ /// <param name="name">The name of the composite indicator.</param>
576+ /// <param name="left">The first indicator used in the composition.</param>
577+ /// <param name="right">The second indicator used in the composition.</param>
578+ /// <param name="handler">A Python function that takes two indicator values and returns the computed result.</param>
579+ /// <returns>A new instance of <see cref="CompositeIndicator"/>.</returns>
580+ /// <exception cref="ArgumentException">
581+ /// Thrown when the provided left or right indicator is not a valid QuantConnect Indicator object.
582+ /// </exception>
583+ [ DocumentationAttribute ( Universes ) ]
584+ public CompositeIndicator CompositeIndicator ( string name , PyObject left , PyObject right , PyObject handler )
585+ {
586+ var leftIndicator = GetIndicator ( left ) ;
587+ var rightIndicator = GetIndicator ( right ) ;
588+ if ( leftIndicator == null )
589+ {
590+ throw new ArgumentException ( $ "The left argument should be a QuantConnect Indicator object, { left } was provided.") ;
591+ }
592+ if ( rightIndicator == null )
593+ {
594+ throw new ArgumentException ( $ "The right argument should be a QuantConnect Indicator object, { right } was provided.") ;
595+ }
596+ CompositeIndicator . IndicatorComposer composer = ( left , right ) =>
597+ {
598+ using ( Py . GIL ( ) )
599+ {
600+ dynamic result = handler . Invoke ( left . Current . Value , right . Current . Value ) ;
601+ return new IndicatorResult ( result ) ;
602+ }
603+ } ;
604+ return new CompositeIndicator ( name , leftIndicator , rightIndicator , null ) ;
605+ }
606+
607+ /// <summary>
608+ /// Attempts to convert a Python object into a valid QuantConnect indicator.
609+ /// </summary>
610+ /// <param name="pyObject">The Python object to convert.</param>
611+ /// <returns>
612+ /// A valid <see cref="IndicatorBase"/> instance if conversion is successful; otherwise, <c>null</c>.
613+ /// </returns>
614+ public IndicatorBase GetIndicator ( PyObject pyObject )
615+ {
616+ if ( pyObject . TryConvert ( out IndicatorBase < IndicatorDataPoint > indicatorDataPoint ) )
617+ {
618+ return indicatorDataPoint ;
619+ }
620+ if ( pyObject . TryConvert ( out IndicatorBase < IBaseDataBar > indicatorDataBar ) )
621+ {
622+ return indicatorDataBar ;
623+ }
624+ if ( pyObject . TryConvert ( out IndicatorBase < TradeBar > indicatorTradeBar ) )
625+ {
626+ return indicatorTradeBar ;
627+ }
628+ return null ;
629+ }
630+
572631 /// <summary>
573632 /// Registers the consolidator to receive automatic updates as well as configures the indicator to receive updates
574633 /// from the consolidator.
@@ -1768,7 +1827,7 @@ private dynamic[] GetIndicatorArray(PyObject first, PyObject second = null, PyOb
17681827 {
17691828 using ( Py . GIL ( ) )
17701829 {
1771- var array = new [ ] { first , second , third , fourth }
1830+ var array = new [ ] { first , second , third , fourth }
17721831 . Select (
17731832 x =>
17741833 {
0 commit comments