@@ -309,13 +309,41 @@ impl FeeModel for PerContractFeeModel {
309309 _order : & OrderAny ,
310310 fill_quantity : Quantity ,
311311 _fill_px : Price ,
312- _instrument : & InstrumentAny ,
312+ instrument : & InstrumentAny ,
313313 ) -> anyhow:: Result < Money > {
314- let total = self . commission . as_decimal ( ) * fill_quantity. as_decimal ( ) ;
314+ let total = self . commission . as_decimal ( )
315+ * fill_quantity. as_decimal ( )
316+ * spread_contract_count ( instrument) ;
315317 Money :: from_decimal ( total, self . commission . currency ) . map_err ( Into :: into)
316318 }
317319}
318320
321+ fn spread_contract_count ( instrument : & InstrumentAny ) -> Decimal {
322+ let symbol = instrument. id ( ) . symbol . to_string ( ) ;
323+ if !symbol. contains ( "___" ) {
324+ return Decimal :: ONE ;
325+ }
326+
327+ symbol
328+ . split ( "___" )
329+ . filter_map ( spread_leg_ratio)
330+ . sum :: < i64 > ( )
331+ . max ( 1 )
332+ . into ( )
333+ }
334+
335+ fn spread_leg_ratio ( component : & str ) -> Option < i64 > {
336+ let ratio = component
337+ . strip_prefix ( "((" )
338+ . and_then ( |rest| rest. split_once ( "))" ) . map ( |( ratio, _) | ratio) )
339+ . or_else ( || {
340+ component
341+ . strip_prefix ( '(' )
342+ . and_then ( |rest| rest. split_once ( ')' ) . map ( |( ratio, _) | ratio) )
343+ } ) ?;
344+ ratio. parse :: < i64 > ( ) . ok ( )
345+ }
346+
319347#[ derive( Debug , Clone ) ]
320348#[ cfg_attr(
321349 feature = "python" ,
@@ -605,9 +633,13 @@ mod tests {
605633
606634 use nautilus_model:: {
607635 enums:: { LiquiditySide , OrderSide , OrderType } ,
636+ identifiers:: InstrumentId ,
608637 instruments:: {
609638 BinaryOption , CryptoOption , Instrument , InstrumentAny , OptionContract ,
610- stubs:: { audusd_sim, binary_option, crypto_option_btc_deribit, option_contract_appl} ,
639+ stubs:: {
640+ audusd_sim, binary_option, crypto_option_btc_deribit, option_contract_appl,
641+ option_spread,
642+ } ,
611643 } ,
612644 orders:: {
613645 Order , OrderAny ,
@@ -785,6 +817,33 @@ mod tests {
785817 assert_eq ! ( commission, Money :: new( 50.0 , Currency :: USD ( ) ) ) ;
786818 }
787819
820+ #[ rstest]
821+ fn test_per_contract_fee_model_option_spread_charges_each_contract ( ) {
822+ let commission_per_contract = Money :: from ( "1.25 USD" ) ;
823+ let fee_model = PerContractFeeModel :: new ( commission_per_contract) . unwrap ( ) ;
824+ let spread_id = InstrumentId :: from ( "((2))SPY C410___(1)SPY C400.SMART" ) ;
825+ let mut option_spread = option_spread ( ) ;
826+ option_spread. id = spread_id;
827+ let instrument = InstrumentAny :: OptionSpread ( option_spread) ;
828+ let market_order = OrderTestBuilder :: new ( OrderType :: Market )
829+ . instrument_id ( instrument. id ( ) )
830+ . side ( OrderSide :: Buy )
831+ . quantity ( Quantity :: from ( 2 ) )
832+ . build ( ) ;
833+ let accepted_order = TestOrderStubs :: make_accepted_order ( & market_order) ;
834+
835+ let commission = fee_model
836+ . get_commission (
837+ & accepted_order,
838+ Quantity :: from ( 2 ) ,
839+ Price :: from ( "1.0" ) ,
840+ & instrument,
841+ )
842+ . unwrap ( ) ;
843+
844+ assert_eq ! ( commission, Money :: from( "7.50 USD" ) ) ;
845+ }
846+
788847 #[ rstest]
789848 fn test_per_contract_fee_model_partial_fill ( ) {
790849 let commission_per_contract = Money :: new ( 1.25 , Currency :: USD ( ) ) ;
0 commit comments