@@ -51,6 +51,57 @@ def __init__(
5151 self .trade_take_profit_service : TradeTakeProfitService = \
5252 trade_take_profit_service
5353
54+ def _validate_target_symbol (self , target_symbol , market = None ):
55+ """
56+ Validate the target_symbol for order creation:
57+ 1. Prevents orders where target_symbol equals trading_symbol
58+ (e.g. EUR/EUR).
59+ 2. Checks that a data source exists for the
60+ target_symbol/trading_symbol combination (e.g. BTC/EUR).
61+
62+ Args:
63+ target_symbol: The symbol of the asset to trade
64+ market: The market to check against
65+
66+ Raises:
67+ OperationalException: If validation fails
68+ """
69+ portfolio = self .portfolio_service .find ({"market" : market })
70+ trading_symbol = portfolio .trading_symbol
71+
72+ # Check target_symbol != trading_symbol
73+ if target_symbol .upper () == trading_symbol .upper ():
74+ raise OperationalException (
75+ f"target_symbol '{ target_symbol } ' is the same as "
76+ f"the trading_symbol '{ trading_symbol } '. "
77+ f"This would result in a "
78+ f"'{ trading_symbol } /{ trading_symbol } ' "
79+ f"order which is not valid. "
80+ f"To skip this check, set validate_symbol=False "
81+ f"or omit the parameter."
82+ )
83+
84+ # Check that a data source is registered for this pair
85+ expected_symbol = f"{ target_symbol } /{ trading_symbol } " .upper ()
86+ known_symbols = set ()
87+
88+ if self .data_provider_service .data_provider_index is not None :
89+ for data_source , _ in \
90+ self .data_provider_service \
91+ .data_provider_index .get_all ():
92+ if data_source .symbol is not None :
93+ known_symbols .add (data_source .symbol .upper ())
94+
95+ if expected_symbol not in known_symbols :
96+ sorted_symbols = sorted (known_symbols )
97+ raise OperationalException (
98+ f"No data source registered for '{ expected_symbol } '. "
99+ f"A data source is required to track price history. "
100+ f"Registered data source symbols: { sorted_symbols } . "
101+ f"To skip this check, set validate_symbol=False "
102+ f"or omit the parameter."
103+ )
104+
54105 @property
55106 def config (self ):
56107 """
@@ -78,7 +129,8 @@ def create_order(
78129 market = None ,
79130 execute = True ,
80131 validate = True ,
81- sync = True
132+ sync = True ,
133+ validate_symbol = False
82134 ) -> Order :
83135 """
84136 Function to create an order. This function will create an order
@@ -96,10 +148,15 @@ def create_order(
96148 validate: If set to True, the order will be validated
97149 sync: If set to True, the created order will be synced
98150 with the portfolio of the algorithm.
151+ validate_symbol: Default False. If set to True,
152+ validates that target_symbol is not the trading_symbol.
99153
100154 Returns:
101155 The order created
102156 """
157+ if validate_symbol :
158+ self ._validate_target_symbol (target_symbol , market = market )
159+
103160 portfolio = self .portfolio_service .find ({"market" : market })
104161 order_data = {
105162 "target_symbol" : target_symbol ,
@@ -162,7 +219,8 @@ def create_limit_order(
162219 execute = True ,
163220 validate = True ,
164221 sync = True ,
165- metadata = None
222+ metadata = None ,
223+ validate_symbol = False
166224 ) -> Order :
167225 """
168226 Function to create a limit order. This function will create a limit
@@ -193,10 +251,15 @@ def create_limit_order(
193251 sync (optional): Default True. If set to True,
194252 the created order will be synced with the
195253 portfolio of the algorithm
254+ validate_symbol (optional): Default False. If set to True,
255+ validates that target_symbol is not the trading_symbol.
196256
197257 Returns:
198258 Order: Instance of the order created
199259 """
260+ if validate_symbol :
261+ self ._validate_target_symbol (target_symbol , market = market )
262+
200263 portfolio = self .portfolio_service .find ({"market" : market })
201264
202265 if percentage_of_portfolio is not None :
@@ -270,6 +333,225 @@ def create_limit_order(
270333 order_data , execute = execute , validate = validate , sync = sync
271334 )
272335
336+ def create_market_order (
337+ self ,
338+ target_symbol ,
339+ order_side ,
340+ amount = None ,
341+ amount_trading_symbol = None ,
342+ percentage = None ,
343+ percentage_of_portfolio = None ,
344+ percentage_of_position = None ,
345+ precision = None ,
346+ market = None ,
347+ execute = True ,
348+ validate = True ,
349+ sync = True ,
350+ metadata = None
351+ ) -> Order :
352+ """
353+ Function to create a market order. Market orders execute at
354+ the best available price. In backtesting, this means the
355+ open price of the next candle (+ slippage).
356+
357+ An estimated price (current latest price) is used for amount
358+ calculation and cash reservation. The actual fill price is
359+ determined at fill time and the portfolio is reconciled.
360+
361+ Args:
362+ target_symbol: The symbol of the asset to trade
363+ order_side: The side of the order (BUY or SELL)
364+ amount (optional): The amount of the asset to trade
365+ amount_trading_symbol (optional): The amount of the
366+ trading symbol to trade
367+ percentage (optional): The percentage of the portfolio
368+ to allocate to the order
369+ percentage_of_portfolio (optional): The percentage
370+ of the portfolio to allocate to the order
371+ percentage_of_position (optional): The percentage
372+ of the position to allocate to the
373+ order. (Only supported for SELL orders)
374+ precision (optional): The precision of the amount
375+ market (optional): The market to trade the asset
376+ execute (optional): Default True. If set to True,
377+ the order will be executed
378+ validate (optional): Default True. If set to
379+ True, the order will be validated
380+ sync (optional): Default True. If set to True,
381+ the created order will be synced with the
382+ portfolio of the algorithm
383+ metadata (optional): Additional metadata for the order
384+
385+ Returns:
386+ Order: Instance of the order created
387+ """
388+ portfolio = self .portfolio_service .find ({"market" : market })
389+ full_symbol = (f"{ target_symbol } /{ portfolio .trading_symbol } " )
390+ estimated_price = self .get_latest_price (full_symbol , market = market )
391+
392+ if estimated_price is None :
393+ raise OperationalException (
394+ f"Cannot create market order for { target_symbol } : "
395+ f"no price data available to estimate order size."
396+ )
397+
398+ if percentage_of_portfolio is not None :
399+ if not OrderSide .BUY .equals (order_side ):
400+ raise OperationalException (
401+ "Percentage of portfolio is only supported for BUY orders."
402+ )
403+
404+ net_size = portfolio .get_net_size ()
405+ size = net_size * (percentage_of_portfolio / 100 )
406+ amount = size / estimated_price
407+
408+ elif percentage_of_position is not None :
409+
410+ if not OrderSide .SELL .equals (order_side ):
411+ raise OperationalException (
412+ "Percentage of position is only supported for SELL orders."
413+ )
414+
415+ position = self .position_service .find (
416+ {
417+ "symbol" : target_symbol ,
418+ "portfolio" : portfolio .id
419+ }
420+ )
421+ amount = position .get_amount () * (percentage_of_position / 100 )
422+
423+ elif percentage is not None :
424+ net_size = portfolio .get_net_size ()
425+ size = net_size * (percentage / 100 )
426+ amount = size / estimated_price
427+
428+ if precision is not None :
429+ amount = RoundingService .round_down (amount , precision )
430+
431+ if amount_trading_symbol is not None :
432+ amount = amount_trading_symbol / estimated_price
433+
434+ if amount is None :
435+ raise OperationalException (
436+ "The amount parameter is required to create a market order. "
437+ "Either the amount, amount_trading_symbol, percentage, "
438+ "percentage_of_portfolio or percentage_of_position "
439+ "parameter must be specified."
440+ )
441+
442+ logger .info (
443+ f"Creating market order: { target_symbol } "
444+ f"{ order_side } { amount } @ estimated { estimated_price } "
445+ )
446+
447+ order_metadata = metadata if metadata is not None else {}
448+ order_metadata ["estimated_price" ] = estimated_price
449+
450+ order_data = {
451+ "target_symbol" : target_symbol ,
452+ "price" : estimated_price ,
453+ "amount" : amount ,
454+ "order_type" : OrderType .MARKET .value ,
455+ "order_side" : OrderSide .from_value (order_side ).value ,
456+ "portfolio_id" : portfolio .id ,
457+ "status" : OrderStatus .CREATED .value ,
458+ "trading_symbol" : portfolio .trading_symbol ,
459+ "metadata" : order_metadata ,
460+ }
461+
462+ if BACKTESTING_FLAG in self .configuration_service .config \
463+ and self .configuration_service .config [BACKTESTING_FLAG ]:
464+ order_data ["created_at" ] = \
465+ self .configuration_service .config [INDEX_DATETIME ]
466+
467+ return self .order_service .create (
468+ order_data , execute = execute , validate = validate , sync = sync
469+ )
470+
471+ def create_market_buy_order (
472+ self ,
473+ target_symbol ,
474+ amount = None ,
475+ percentage_of_portfolio = None ,
476+ market = None ,
477+ portfolio_id = None ,
478+ metadata = None
479+ ) -> Order :
480+ """
481+ Function to create a market buy order.
482+
483+ Args:
484+ target_symbol (str): The symbol of the asset to buy
485+ amount (float, optional): The amount of the asset to buy
486+ percentage_of_portfolio (float, optional): The percentage of the
487+ portfolio to buy.
488+ market (str, optional): the portfolio corresponding to the market
489+ to buy the asset
490+ portfolio_id (str, optional): The ID of the portfolio to buy
491+ the asset from.
492+ metadata (dict, optional): Additional metadata for the order
493+
494+ Returns:
495+ Order: The order created
496+ """
497+
498+ if amount is None and percentage_of_portfolio is None :
499+ raise OperationalException (
500+ "Either amount or percentage_of_portfolio must be specified "
501+ "to create a market buy order."
502+ )
503+
504+ return self .create_market_order (
505+ target_symbol = target_symbol ,
506+ order_side = OrderSide .BUY ,
507+ amount = amount ,
508+ percentage_of_portfolio = percentage_of_portfolio ,
509+ market = market ,
510+ metadata = metadata
511+ )
512+
513+ def create_market_sell_order (
514+ self ,
515+ target_symbol ,
516+ amount = None ,
517+ percentage_of_position = None ,
518+ market = None ,
519+ portfolio_id = None ,
520+ metadata = None
521+ ) -> Order :
522+ """
523+ Function to create a market sell order.
524+
525+ Args:
526+ target_symbol (str): The symbol of the asset to sell
527+ amount (float, optional): The amount of the asset to sell
528+ percentage_of_position (float, optional): The percentage of the
529+ position to sell.
530+ market (str, optional): the portfolio corresponding to the market
531+ to sell the asset
532+ portfolio_id (str, optional): The ID of the portfolio to sell
533+ the asset from.
534+ metadata (dict, optional): Additional metadata for the order
535+
536+ Returns:
537+ Order: The order created
538+ """
539+
540+ if amount is None and percentage_of_position is None :
541+ raise OperationalException (
542+ "Either amount or percentage_of_position must be specified "
543+ "to create a market sell order."
544+ )
545+
546+ return self .create_market_order (
547+ target_symbol = target_symbol ,
548+ order_side = OrderSide .SELL ,
549+ amount = amount ,
550+ percentage_of_position = percentage_of_position ,
551+ market = market ,
552+ metadata = metadata
553+ )
554+
273555 def create_limit_sell_order (
274556 self ,
275557 target_symbol ,
0 commit comments