"""Technical indicator tools. These tools allow the agent to: 1. Discover available indicators (list, search, get info) 2. Add indicators to the chart 3. Update/remove indicators 4. Query currently applied indicators """ from typing import Dict, Any, List, Optional from langchain_core.tools import tool import logging import time logger = logging.getLogger(__name__) def _get_indicator_registry(): """Get the global indicator registry instance.""" from . import _indicator_registry return _indicator_registry def _get_registry(): """Get the global sync registry instance.""" from . import _registry return _registry def _get_indicator_store(): """Get the global IndicatorStore instance.""" registry = _get_registry() if registry and "IndicatorStore" in registry.entries: return registry.entries["IndicatorStore"].model return None @tool def list_indicators() -> List[str]: """List all available technical indicators. Returns: List of indicator names that can be used in analysis and strategies """ registry = _get_indicator_registry() if not registry: return [] return registry.list_indicators() @tool def get_indicator_info(indicator_name: str) -> Dict[str, Any]: """Get detailed information about a specific indicator. Retrieves metadata including description, parameters, category, use cases, input/output schemas, and references. Args: indicator_name: Name of the indicator (e.g., "RSI", "SMA", "MACD") Returns: Dictionary containing: - name: Indicator name - display_name: Human-readable name - description: What the indicator computes and why it's useful - category: Category (momentum, trend, volatility, volume, etc.) - parameters: List of configurable parameters with types and defaults - use_cases: Common trading scenarios where this indicator helps - tags: Searchable tags - input_schema: Required input columns (e.g., OHLCV requirements) - output_schema: Columns this indicator produces Raises: ValueError: If indicator_name is not found """ registry = _get_indicator_registry() if not registry: raise ValueError("IndicatorRegistry not initialized") metadata = registry.get_metadata(indicator_name) if not metadata: total_count = len(registry.list_indicators()) raise ValueError( f"Indicator '{indicator_name}' not found. " f"Total available: {total_count} indicators. " f"Use search_indicators() to find indicators by name, category, or tag." ) input_schema = registry.get_input_schema(indicator_name) output_schema = registry.get_output_schema(indicator_name) result = metadata.model_dump() result["input_schema"] = input_schema.model_dump() if input_schema else None result["output_schema"] = output_schema.model_dump() if output_schema else None return result @tool def search_indicators( query: Optional[str] = None, category: Optional[str] = None, tag: Optional[str] = None ) -> List[Dict[str, Any]]: """Search for indicators by text query, category, or tag. Returns lightweight summaries - use get_indicator_info() for full details on specific indicators. Use this to discover relevant indicators for your trading strategy or analysis. Can filter by category (momentum, trend, volatility, etc.) or search by keywords. Args: query: Optional text search across names, descriptions, and use cases category: Optional category filter (momentum, trend, volatility, volume, pattern, etc.) tag: Optional tag filter (e.g., "oscillator", "moving-average", "talib") Returns: List of lightweight indicator summaries. Each contains: - name: Indicator name (use with get_indicator_info() for full details) - display_name: Human-readable name - description: Brief one-line description - category: Category (momentum, trend, volatility, etc.) Example: # Find all momentum indicators results = search_indicators(category="momentum") # Returns [{name: "RSI", display_name: "RSI", description: "...", category: "momentum"}, ...] # Then get details on interesting ones rsi_details = get_indicator_info("RSI") # Full parameters, schemas, use cases # Search for moving average indicators search_indicators(query="moving average") # Find all TA-Lib indicators search_indicators(tag="talib") """ registry = _get_indicator_registry() if not registry: raise ValueError("IndicatorRegistry not initialized") results = [] if query: results = registry.search_by_text(query) elif category: results = registry.search_by_category(category) elif tag: results = registry.search_by_tag(tag) else: # Return all indicators if no filter results = registry.get_all_metadata() # Return lightweight summaries only return [ { "name": r.name, "display_name": r.display_name, "description": r.description, "category": r.category } for r in results ] @tool def get_indicator_categories() -> Dict[str, int]: """Get all indicator categories and their counts. Returns a summary of available indicator categories, useful for exploring what types of indicators are available. Returns: Dictionary mapping category name to count of indicators in that category. Example: {"momentum": 25, "trend": 15, "volatility": 8, ...} """ registry = _get_indicator_registry() if not registry: raise ValueError("IndicatorRegistry not initialized") categories: Dict[str, int] = {} for metadata in registry.get_all_metadata(): category = metadata.category categories[category] = categories.get(category, 0) + 1 return categories @tool async def add_indicator_to_chart( indicator_id: str, talib_name: str, parameters: Optional[Dict[str, Any]] = None, symbol: Optional[str] = None ) -> Dict[str, Any]: """Add a technical indicator to the chart. This will create a new indicator instance and display it on the TradingView chart. The indicator will be synchronized with the frontend in real-time. Args: indicator_id: Unique identifier for this indicator instance (e.g., 'rsi_14', 'sma_50') talib_name: Name of the TA-Lib indicator (e.g., 'RSI', 'SMA', 'MACD', 'BBANDS') Use search_indicators() or get_indicator_info() to find available indicators parameters: Optional dictionary of indicator parameters Example for RSI: {'timeperiod': 14} Example for SMA: {'timeperiod': 50} Example for MACD: {'fastperiod': 12, 'slowperiod': 26, 'signalperiod': 9} Example for BBANDS: {'timeperiod': 20, 'nbdevup': 2, 'nbdevdn': 2} symbol: Optional symbol to apply the indicator to (defaults to current chart symbol) Returns: Dictionary with: - status: 'created' or 'updated' - indicator: The complete indicator object Example: # Add RSI(14) await add_indicator_to_chart( indicator_id='rsi_14', talib_name='RSI', parameters={'timeperiod': 14} ) # Add 50-period SMA await add_indicator_to_chart( indicator_id='sma_50', talib_name='SMA', parameters={'timeperiod': 50} ) # Add MACD with default parameters await add_indicator_to_chart( indicator_id='macd_default', talib_name='MACD' ) """ from schema.indicator import IndicatorInstance registry = _get_registry() if not registry: raise ValueError("SyncRegistry not initialized") indicator_store = _get_indicator_store() if not indicator_store: raise ValueError("IndicatorStore not initialized") # Verify the indicator exists indicator_registry = _get_indicator_registry() if not indicator_registry: raise ValueError("IndicatorRegistry not initialized") metadata = indicator_registry.get_metadata(talib_name) if not metadata: raise ValueError( f"Indicator '{talib_name}' not found. " f"Use search_indicators() to find available indicators." ) # Check if updating existing indicator existing_indicator = indicator_store.indicators.get(indicator_id) is_update = existing_indicator is not None # If symbol is not provided, try to get it from ChartStore if symbol is None and "ChartStore" in registry.entries: chart_store = registry.entries["ChartStore"].model if hasattr(chart_store, 'chart_state') and hasattr(chart_store.chart_state, 'symbol'): symbol = chart_store.chart_state.symbol logger.info(f"Using current chart symbol for indicator: {symbol}") now = int(time.time()) # Create indicator instance indicator = IndicatorInstance( id=indicator_id, talib_name=talib_name, instance_name=f"{talib_name}_{indicator_id}", parameters=parameters or {}, visible=True, pane='chart', # Most indicators go on the chart pane symbol=symbol, created_at=existing_indicator.get('created_at') if existing_indicator else now, modified_at=now ) # Update the store indicator_store.indicators[indicator_id] = indicator.model_dump(mode="json") # Trigger sync await registry.push_all() logger.info( f"{'Updated' if is_update else 'Created'} indicator '{indicator_id}' " f"(TA-Lib: {talib_name}) with parameters: {parameters}" ) return { "status": "updated" if is_update else "created", "indicator": indicator.model_dump(mode="json") } @tool async def remove_indicator_from_chart(indicator_id: str) -> Dict[str, str]: """Remove an indicator from the chart. Args: indicator_id: ID of the indicator instance to remove Returns: Dictionary with status message Raises: ValueError: If indicator doesn't exist Example: await remove_indicator_from_chart('rsi_14') """ registry = _get_registry() if not registry: raise ValueError("SyncRegistry not initialized") indicator_store = _get_indicator_store() if not indicator_store: raise ValueError("IndicatorStore not initialized") if indicator_id not in indicator_store.indicators: raise ValueError(f"Indicator '{indicator_id}' not found") # Delete the indicator del indicator_store.indicators[indicator_id] # Trigger sync await registry.push_all() logger.info(f"Removed indicator '{indicator_id}'") return { "status": "success", "message": f"Indicator '{indicator_id}' removed" } @tool def list_chart_indicators(symbol: Optional[str] = None) -> List[Dict[str, Any]]: """List all indicators currently applied to the chart. Args: symbol: Optional filter by symbol (defaults to current chart symbol) Returns: List of indicator instances, each containing: - id: Indicator instance ID - talib_name: TA-Lib indicator name - instance_name: Display name - parameters: Current parameter values - visible: Whether indicator is visible - pane: Which pane it's displayed in - symbol: Symbol it's applied to Example: # List all indicators on current symbol indicators = list_chart_indicators() # List indicators on specific symbol btc_indicators = list_chart_indicators(symbol='BINANCE:BTC/USDT') """ indicator_store = _get_indicator_store() if not indicator_store: raise ValueError("IndicatorStore not initialized") logger.info(f"list_chart_indicators: Raw store indicators: {indicator_store.indicators}") # If symbol is not provided, try to get it from ChartStore if symbol is None: registry = _get_registry() if registry and "ChartStore" in registry.entries: chart_store = registry.entries["ChartStore"].model if hasattr(chart_store, 'chart_state') and hasattr(chart_store.chart_state, 'symbol'): symbol = chart_store.chart_state.symbol indicators = list(indicator_store.indicators.values()) logger.info(f"list_chart_indicators: Converted to list: {indicators}") logger.info(f"list_chart_indicators: Filtering by symbol: {symbol}") # Filter by symbol if provided if symbol: indicators = [ind for ind in indicators if ind.get('symbol') == symbol] logger.info(f"list_chart_indicators: Returning {len(indicators)} indicators") return indicators @tool def get_chart_indicator(indicator_id: str) -> Dict[str, Any]: """Get details of a specific indicator on the chart. Args: indicator_id: ID of the indicator instance Returns: Dictionary containing the indicator data Raises: ValueError: If indicator doesn't exist Example: indicator = get_chart_indicator('rsi_14') print(f"Indicator: {indicator['talib_name']}") print(f"Parameters: {indicator['parameters']}") """ indicator_store = _get_indicator_store() if not indicator_store: raise ValueError("IndicatorStore not initialized") indicator = indicator_store.indicators.get(indicator_id) if not indicator: raise ValueError(f"Indicator '{indicator_id}' not found") return indicator INDICATOR_TOOLS = [ # Discovery tools list_indicators, get_indicator_info, search_indicators, get_indicator_categories, # Chart indicator management tools add_indicator_to_chart, remove_indicator_from_chart, list_chart_indicators, get_chart_indicator ]