""" ================================================================================ Quantum Terminal — Base Provider Interface ================================================================================ Abstract contract that every data/execution provider must implement. The data_server and config_manager talk to providers ONLY through this interface. MT5, Binance, Polygon, or any future source plugs in by subclassing BaseProvider and implementing the required methods. Design principles: - All methods are synchronous (callers use asyncio.to_thread) - Providers manage their own connection lifecycle - Canonical ticker names everywhere — providers map internally - Providers declare their capabilities (data-only vs data+execution) Usage: from providers.base_provider import BaseProvider from providers.mt5_provider import MT5Provider provider = MT5Provider(account_config) provider.connect() ticks = provider.get_latest_ticks(["XAUUSD", "EURUSD"]) ================================================================================ """ from abc import ABC, abstractmethod from typing import Dict, List, Optional from models import ( TickData, BarData, AccountInfo, SymbolInfo, OrderRequest, OrderResult, Position, PendingOrder, ) class BaseProvider(ABC): """ Abstract provider interface. Every provider has: - A type name (e.g., "mt5", "binance") - A unique instance ID (e.g., "mt5_primary", "binance_spot") - Connection lifecycle (connect/disconnect/reconnect) - Market data methods (ticks, bars, symbol info) - Optional execution methods (orders, positions) Providers are synchronous. The data_server wraps calls in asyncio.to_thread() to avoid blocking the event loop. """ # ── Identity ── @property @abstractmethod def provider_type(self) -> str: """Provider type identifier. E.g., 'mt5', 'binance', 'polygon'.""" ... @property @abstractmethod def provider_id(self) -> str: """Unique instance ID. E.g., 'mt5_primary'. Set from account config.""" ... @property @abstractmethod def label(self) -> str: """Human-readable label. E.g., 'MT5 — CFI (Live)'.""" ... # ── Capabilities ── @property def can_stream_ticks(self) -> bool: """Whether this provider supports live tick polling.""" return True @property def can_execute(self) -> bool: """Whether this provider supports order execution.""" return False @property def supported_timeframes(self) -> List[str]: """List of timeframe strings this provider supports.""" return ["M1", "M5", "M15", "M30", "H1", "H4", "D1", "W1"] # ── Connection Lifecycle ── @property @abstractmethod def connected(self) -> bool: """Whether the provider is currently connected.""" ... @abstractmethod def connect(self) -> bool: """ Establish connection to the data/execution source. Returns True on success, False on failure. Must be idempotent — calling connect() when already connected is safe. """ ... @abstractmethod def disconnect(self) -> None: """Cleanly shut down the connection.""" ... def reconnect(self) -> bool: """Disconnect and reconnect. Override for custom reconnect logic.""" self.disconnect() return self.connect() def heartbeat(self) -> bool: """ Lightweight connection health check. Returns True if the connection is alive, False otherwise. If False, sets internal connected state to False so reconnect_loop picks it up. Override in subclasses for provider-specific health checks. """ return self.connected # ── Market Data ── @abstractmethod def get_latest_ticks(self, symbols: List[str]) -> Dict[str, TickData]: """ Fetch latest tick for each symbol. Args: symbols: List of canonical ticker names (e.g., ["XAUUSD", "EURUSD"]) Returns: Dict mapping canonical ticker → TickData. Missing/failed symbols are simply omitted. """ ... @abstractmethod def get_bars( self, ticker: str, timeframe: str = "M15", count: int = 200 ) -> List[BarData]: """ Fetch recent OHLCV bars for a canonical ticker. Args: ticker: Canonical symbol name timeframe: Timeframe string (M1, M5, M15, H1, H4, D1, etc.) count: Number of bars to fetch Returns: List of BarData, oldest first. Empty list on failure. """ ... @abstractmethod def check_new_bars( self, symbols: List[str], timeframe: str = "M15" ) -> List[dict]: """ Detect newly closed bars since last check. Returns list of dicts: {"type": "bar", "ticker": str, "timeframe": str, "bar": BarData.to_dict()} Implementation must track last-seen bar timestamps internally. """ ... @abstractmethod def get_symbol_info(self, ticker: str) -> Optional[SymbolInfo]: """ Get metadata for a symbol (decimals, lot sizing, contract size, etc.) Returns None if symbol not found. """ ... def get_all_symbol_info(self, symbols: List[str]) -> Dict[str, SymbolInfo]: """ Batch symbol info for multiple tickers. Default implementation calls get_symbol_info() in a loop. Override for providers that support batch queries. """ result = {} for s in symbols: info = self.get_symbol_info(s) if info is not None: result[s] = info return result # ── Account Info ── @abstractmethod def get_account_info(self) -> Optional[AccountInfo]: """ Get current account snapshot (balance, equity, margin, etc.) Returns None if not connected or not applicable. """ ... # ── Execution (optional — override if can_execute is True) ── def place_order(self, order: OrderRequest) -> OrderResult: """Place an order. Override in execution-capable providers.""" return OrderResult( success=False, error=f"Provider '{self.provider_type}' does not support execution", ) def get_positions(self) -> List[Position]: """Get all open positions. Override in execution-capable providers.""" return [] def close_position(self, ticket: str, lots: Optional[float] = None) -> OrderResult: """ Close a position (fully or partially). Override in execution-capable providers. """ return OrderResult( success=False, error=f"Provider '{self.provider_type}' does not support execution", ) def get_bars_range(self, ticker, timeframe, from_dt, to_dt): """Fetch OHLCV bars between two datetimes. Override in providers that support historical range queries.""" return [] def get_pending_orders(self) -> List[PendingOrder]: """ List all resting (non-filled) pending orders (LIMIT / STOP). Override in execution-capable providers. """ return [] def cancel_order(self, ticket: str) -> OrderResult: """Cancel a resting pending order. Override in execution-capable providers.""" return OrderResult( success=False, error=f"Provider '{self.provider_type}' does not support execution", ) def modify_order( self, ticket: str, price: Optional[float] = None, stop_loss: Optional[float] = None, take_profit: Optional[float] = None, ) -> OrderResult: """Modify price / SL / TP on a pending order. Override in execution-capable providers.""" return OrderResult( success=False, error=f"Provider '{self.provider_type}' does not support execution", ) # ── Symbol Resolution ── @abstractmethod def resolve_symbol(self, canonical: str) -> Optional[str]: """ Map a canonical ticker name to the provider's native symbol. Returns None if the symbol is not available in this provider. """ ... def get_available_symbols(self) -> List[str]: """ Return list of all canonical symbols this provider can serve. Default: empty (override to support symbol discovery). """ return [] # ── String Representation ── def __repr__(self) -> str: status = "connected" if self.connected else "disconnected" return f"<{self.__class__.__name__} id='{self.provider_id}' [{status}]>"