"""Core: Order execution and management.""" import asyncio from datetime import datetime from typing import Dict, Any from loguru import logger from storage.models import Order from .risk_manager import RiskManager class OrderManager: """Handles order creation, tracking, and lifecycle management.""" def __init__(self, broker, db_session, risk_manager: RiskManager): """ Initializes the OrderManager. Args: broker: The broker instance to execute trades. db_session: The database session for persistence. risk_manager: The RiskManager instance for risk validation. """ self.broker = broker self.db_session = db_session self.risk_manager = risk_manager self.orders = {} # In-memory cache for active orders async def place_order( self, symbol: str, side: str, current_price: float, quantity: int, order_type: str, limit_price: float = None, stop_price: float = None, is_async: bool = True, ) -> Dict[str, Any]: """ Places a new order with the broker and stores it in the database. Args: symbol (str): The symbol to trade (e.g., 'NSE:SBIN-EQ'). side (str): 'BUY' or 'SELL'. quantity (int): The number of shares. order_type (str): 'LIMIT', 'MARKET', 'STOP', etc. limit_price (float, optional): The limit price for LIMIT orders. stop_price (float, optional): The stop price for STOP orders. is_async (bool, optional): FYERS specific flag. Defaults to True. Returns: dict: The response from the broker. """ logger.info( f"Placing {side} order for {quantity} {symbol} at {order_type}." ) order_data_for_validation = { "symbol": symbol, "side": side, "quantity": quantity, "order_type": order_type, "limit_price": limit_price, "stop_price": stop_price, "current_price": current_price, # Add current price for risk manager } if not self.risk_manager.validate_order(order_data_for_validation): return {"error": "Order rejected by risk manager"} # Use adjusted quantity, stop_loss_price, take_profit_price from risk manager adjusted_quantity = order_data_for_validation["quantity"] adjusted_stop_loss_price = order_data_for_validation.get("stop_loss_price") adjusted_take_profit_price = order_data_for_validation.get("take_profit_price") try: order_data = { "symbol": symbol, "qty": adjusted_quantity, "type": self._map_order_type(order_type), "side": self._map_side(side), "productType": "INTRADAY", # Or CNC, MARGIN, etc. "limitPrice": adjusted_take_profit_price if adjusted_take_profit_price else 0, # Use take profit as limit for now "stopPrice": adjusted_stop_loss_price if adjusted_stop_loss_price else 0, "validity": "DAY", "disclosedQty": 0, "offlineOrder": "False", } # The fyers_sdk places orders synchronously, but we run it in an executor loop = asyncio.get_running_loop() broker_response = await loop.run_in_executor( None, self.broker.place_order, order_data ) if broker_response and broker_response.get("s") == "ok": order_id = broker_response.get("id") logger.success(f"Order placed successfully. Broker ID: {order_id}") await self._store_order( order_id, symbol, side, quantity, order_type, limit_price ) return broker_response else: error_msg = broker_response.get( "message", "Unknown error from broker" ) logger.error(f"Failed to place order: {error_msg}") return {"error": error_msg} except Exception as e: logger.error(f"Exception placing order: {e}") return {"error": str(e)} async def _store_order( self, order_id, symbol, side, quantity, order_type, price ): """Stores the order details in the database.""" try: new_order = Order( broker_order_id=order_id, symbol=symbol, side=side, quantity=quantity, order_type=order_type, price=price, status="PENDING", # Initial status timestamp=datetime.utcnow(), ) self.db_session.add(new_order) self.db_session.commit() self.orders[order_id] = new_order # Cache it logger.info(f"Order {order_id} stored in database.") except Exception as e: logger.error(f"Failed to store order {order_id}: {e}") self.db_session.rollback() def _map_order_type(self, order_type: str) -> int: """Maps internal order types to FYERS API integer codes.""" mapping = {"LIMIT": 1, "MARKET": 2, "STOP": 3, "STOP_LIMIT": 4} return mapping.get(order_type.upper(), 2) # Default to MARKET def _map_side(self, side: str) -> int: """Maps internal side to FYERS API integer codes.""" mapping = {"BUY": 1, "SELL": -1} return mapping.get(side.upper(), 1) # Default to BUY async def get_order_status(self, order_id: str): """Retrieves the status of an order from the broker.""" # Placeholder for fetching order status from broker pass async def cancel_order(self, order_id: str): """Cancels an active order.""" # Placeholder for cancelling an order pass