backend redesign

This commit is contained in:
2026-03-11 18:47:11 -04:00
parent 8ff277c8c6
commit e99ef5d2dd
210 changed files with 12147 additions and 155 deletions

View File

@@ -0,0 +1,349 @@
"""
Indicator registry for managing and discovering indicators.
Provides AI agents with a queryable catalog of available indicators,
their capabilities, and metadata.
"""
from typing import Dict, List, Optional, Type
from .base import Indicator
from .schema import IndicatorMetadata, InputSchema, OutputSchema
class IndicatorRegistry:
"""
Central registry for indicator classes.
Enables:
- Registration of indicator implementations
- Discovery by name, category, or tags
- Schema validation
- AI agent tool generation
"""
def __init__(self):
self._indicators: Dict[str, Type[Indicator]] = {}
def register(self, indicator_class: Type[Indicator]) -> None:
"""
Register an indicator class.
Args:
indicator_class: Indicator class to register
Raises:
ValueError: If an indicator with this name is already registered
"""
metadata = indicator_class.get_metadata()
if metadata.name in self._indicators:
raise ValueError(
f"Indicator '{metadata.name}' is already registered"
)
self._indicators[metadata.name] = indicator_class
def unregister(self, name: str) -> None:
"""
Unregister an indicator class.
Args:
name: Indicator class name
"""
self._indicators.pop(name, None)
def get(self, name: str) -> Optional[Type[Indicator]]:
"""
Get an indicator class by name.
Args:
name: Indicator class name
Returns:
Indicator class or None if not found
"""
return self._indicators.get(name)
def list_indicators(self) -> List[str]:
"""
Get names of all registered indicators.
Returns:
List of indicator class names
"""
return list(self._indicators.keys())
def get_metadata(self, name: str) -> Optional[IndicatorMetadata]:
"""
Get metadata for a specific indicator.
Args:
name: Indicator class name
Returns:
IndicatorMetadata or None if not found
"""
indicator_class = self.get(name)
if indicator_class:
return indicator_class.get_metadata()
return None
def get_all_metadata(self) -> List[IndicatorMetadata]:
"""
Get metadata for all registered indicators.
Useful for AI agent tool generation and discovery.
Returns:
List of IndicatorMetadata for all registered indicators
"""
return [cls.get_metadata() for cls in self._indicators.values()]
def search_by_category(self, category: str) -> List[IndicatorMetadata]:
"""
Find indicators by category.
Args:
category: Category name (e.g., 'momentum', 'trend', 'volatility')
Returns:
List of matching indicator metadata
"""
results = []
for indicator_class in self._indicators.values():
metadata = indicator_class.get_metadata()
if metadata.category.lower() == category.lower():
results.append(metadata)
return results
def search_by_tag(self, tag: str) -> List[IndicatorMetadata]:
"""
Find indicators by tag.
Args:
tag: Tag to search for (case-insensitive)
Returns:
List of matching indicator metadata
"""
tag_lower = tag.lower()
results = []
for indicator_class in self._indicators.values():
metadata = indicator_class.get_metadata()
if any(t.lower() == tag_lower for t in metadata.tags):
results.append(metadata)
return results
def search_by_text(self, query: str) -> List[IndicatorMetadata]:
"""
Full-text search across indicator names, descriptions, and use cases.
Args:
query: Search query (case-insensitive)
Returns:
List of matching indicator metadata, ranked by relevance
"""
query_lower = query.lower()
results = []
for indicator_class in self._indicators.values():
metadata = indicator_class.get_metadata()
score = 0
# Check name (highest weight)
if query_lower in metadata.name.lower():
score += 10
if query_lower in metadata.display_name.lower():
score += 8
# Check description
if query_lower in metadata.description.lower():
score += 5
# Check use cases
for use_case in metadata.use_cases:
if query_lower in use_case.lower():
score += 3
# Check tags
for tag in metadata.tags:
if query_lower in tag.lower():
score += 2
if score > 0:
results.append((score, metadata))
# Sort by score descending
results.sort(key=lambda x: x[0], reverse=True)
return [metadata for _, metadata in results]
def find_compatible_indicators(
self,
available_columns: List[str],
column_types: Dict[str, str]
) -> List[IndicatorMetadata]:
"""
Find indicators that can be computed from available columns.
Args:
available_columns: List of column names available
column_types: Mapping of column name to type
Returns:
List of indicators whose input schema is satisfied
"""
from datasource.schema import ColumnInfo
# Build ColumnInfo list from available data
available_schema = [
ColumnInfo(
name=name,
type=column_types.get(name, "float"),
description=f"Column {name}"
)
for name in available_columns
]
results = []
for indicator_class in self._indicators.values():
input_schema = indicator_class.get_input_schema()
if input_schema.matches(available_schema):
results.append(indicator_class.get_metadata())
return results
def validate_indicator_chain(
self,
indicator_chain: List[tuple[str, Dict]]
) -> tuple[bool, Optional[str]]:
"""
Validate that a chain of indicators can be connected.
Args:
indicator_chain: List of (indicator_name, params) tuples in execution order
Returns:
Tuple of (is_valid, error_message)
"""
if not indicator_chain:
return True, None
# For now, just check that all indicators exist
# More sophisticated DAG validation happens in the pipeline engine
for indicator_name, params in indicator_chain:
if indicator_name not in self._indicators:
return False, f"Indicator '{indicator_name}' not found in registry"
return True, None
def get_input_schema(self, name: str) -> Optional[InputSchema]:
"""
Get input schema for a specific indicator.
Args:
name: Indicator class name
Returns:
InputSchema or None if not found
"""
indicator_class = self.get(name)
if indicator_class:
return indicator_class.get_input_schema()
return None
def get_output_schema(self, name: str, **params) -> Optional[OutputSchema]:
"""
Get output schema for a specific indicator with given parameters.
Args:
name: Indicator class name
**params: Indicator parameters
Returns:
OutputSchema or None if not found
"""
indicator_class = self.get(name)
if indicator_class:
return indicator_class.get_output_schema(**params)
return None
def create_instance(self, name: str, instance_name: str, **params) -> Optional[Indicator]:
"""
Create an indicator instance with validation.
Args:
name: Indicator class name
instance_name: Unique instance name (for output column prefixing)
**params: Indicator configuration parameters
Returns:
Indicator instance or None if class not found
Raises:
ValueError: If parameters are invalid
"""
indicator_class = self.get(name)
if not indicator_class:
return None
return indicator_class(instance_name=instance_name, **params)
def generate_ai_tool_spec(self) -> Dict:
"""
Generate a JSON specification for AI agent tools.
Creates a structured representation of all indicators that can be
used to build agent tools for indicator selection and composition.
Returns:
Dict suitable for AI agent tool registration
"""
tools = []
for indicator_class in self._indicators.values():
metadata = indicator_class.get_metadata()
# Build parameter spec
parameters = {
"type": "object",
"properties": {},
"required": []
}
for param in metadata.parameters:
param_spec = {
"type": param.type,
"description": param.description
}
if param.default is not None:
param_spec["default"] = param.default
if param.min_value is not None:
param_spec["minimum"] = param.min_value
if param.max_value is not None:
param_spec["maximum"] = param.max_value
parameters["properties"][param.name] = param_spec
if param.required:
parameters["required"].append(param.name)
tool = {
"name": f"indicator_{metadata.name.lower()}",
"description": f"{metadata.display_name}: {metadata.description}",
"category": metadata.category,
"use_cases": metadata.use_cases,
"tags": metadata.tags,
"parameters": parameters,
"input_schema": indicator_class.get_input_schema().model_dump(),
"output_schema": indicator_class.get_output_schema().model_dump()
}
tools.append(tool)
return {
"indicator_tools": tools,
"total_count": len(tools)
}