diff --git a/config/indicators/config_utils.py b/config/indicators/config_utils.py index d4c3638..53c1cc6 100644 --- a/config/indicators/config_utils.py +++ b/config/indicators/config_utils.py @@ -6,6 +6,7 @@ import json import os import logging from typing import List, Dict, Any, Optional +from dash import Output, Input, State logger = logging.getLogger(__name__) @@ -164,4 +165,174 @@ def generate_parameter_fields_config(indicator_type: str) -> Optional[Dict[str, fields_config[param_name] = field_config - return fields_config \ No newline at end of file + return fields_config + + +def get_parameter_field_outputs() -> List[Output]: + """Generate dynamic Output components for parameter field visibility callbacks. + + Returns: + List[Output]: List of Output components for all parameter containers + """ + templates = load_indicator_templates() + outputs = [Output('indicator-parameters-message', 'style')] + + for indicator_type in templates.keys(): + outputs.append(Output(f'{indicator_type}-parameters', 'style')) + + return outputs + + +def get_parameter_field_states() -> List[State]: + """Generate dynamic State components for parameter input fields. + + Returns: + List[State]: List of State components for all parameter input fields + """ + templates = load_indicator_templates() + states = [] + + for indicator_type, template in templates.items(): + config = generate_parameter_fields_config(indicator_type) + if config: + for field_config in config.values(): + states.append(State(field_config['input_id'], 'value')) + + return states + + +def get_parameter_field_edit_outputs() -> List[Output]: + """Generate dynamic Output components for parameter fields in edit mode. + + Returns: + List[Output]: List of Output components for setting parameter values + """ + templates = load_indicator_templates() + outputs = [] + + for indicator_type, template in templates.items(): + config = generate_parameter_fields_config(indicator_type) + if config: + for field_config in config.values(): + outputs.append(Output(field_config['input_id'], 'value')) + + return outputs + + +def get_parameter_field_reset_outputs() -> List[Output]: + """Generate dynamic Output components for resetting parameter fields. + + Returns: + List[Output]: List of Output components for resetting parameter values + """ + templates = load_indicator_templates() + outputs = [] + + for indicator_type, template in templates.items(): + config = generate_parameter_fields_config(indicator_type) + if config: + for field_config in config.values(): + outputs.append(Output(field_config['input_id'], 'value', allow_duplicate=True)) + + return outputs + + +def collect_parameter_values(indicator_type: str, all_parameter_values: Dict[str, Any]) -> Dict[str, Any]: + """Collect parameter values for a specific indicator type from callback arguments. + + Args: + indicator_type (str): The indicator type + all_parameter_values (Dict[str, Any]): All parameter values from callback + + Returns: + Dict[str, Any]: Parameters specific to the indicator type + """ + config = generate_parameter_fields_config(indicator_type) + if not config: + return {} + + parameters = {} + defaults = get_indicator_default_parameters(indicator_type) + + for param_name, field_config in config.items(): + field_id = field_config['input_id'] + value = all_parameter_values.get(field_id) + default_value = defaults.get(param_name, field_config.get('default')) + + # Use provided value or fall back to default + parameters[param_name] = value if value is not None else default_value + + return parameters + + +def set_parameter_values(indicator_type: str, parameters: Dict[str, Any]) -> List[Any]: + """Generate parameter values for setting in edit mode. + + Args: + indicator_type (str): The indicator type + parameters (Dict[str, Any]): Parameter values to set + + Returns: + List[Any]: Values in the order expected by the callback outputs + """ + templates = load_indicator_templates() + values = [] + + for current_type, template in templates.items(): + config = generate_parameter_fields_config(current_type) + if config: + for param_name, field_config in config.items(): + if current_type == indicator_type: + # Set the actual parameter value for the matching indicator type + value = parameters.get(param_name) + else: + # Set None for other indicator types + value = None + values.append(value) + + return values + + +def reset_parameter_values() -> List[Any]: + """Generate default parameter values for resetting the form. + + Returns: + List[Any]: Default values in the order expected by reset callback outputs + """ + templates = load_indicator_templates() + values = [] + + for indicator_type, template in templates.items(): + config = generate_parameter_fields_config(indicator_type) + if config: + defaults = get_indicator_default_parameters(indicator_type) + for param_name, field_config in config.items(): + default_value = defaults.get(param_name, field_config.get('default')) + values.append(default_value) + + return values + + +def get_parameter_visibility_styles(selected_indicator_type: str) -> List[Dict[str, str]]: + """Generate visibility styles for parameter containers. + + Args: + selected_indicator_type (str): The currently selected indicator type + + Returns: + List[Dict[str, str]]: Visibility styles for each parameter container + """ + templates = load_indicator_templates() + hidden_style = {'display': 'none'} + visible_style = {'display': 'block'} + + # First style is for the message + message_style = {'display': 'block'} if not selected_indicator_type else {'display': 'none'} + styles = [message_style] + + # Then styles for each indicator type container + for indicator_type in templates.keys(): + style = visible_style if indicator_type == selected_indicator_type else hidden_style + styles.append(style) + + return styles \ No newline at end of file diff --git a/dashboard/callbacks/indicators.py b/dashboard/callbacks/indicators.py index c66aeb9..a2e90ab 100644 --- a/dashboard/callbacks/indicators.py +++ b/dashboard/callbacks/indicators.py @@ -7,6 +7,16 @@ from dash import Output, Input, State, html, dcc, callback_context, no_update import dash_bootstrap_components as dbc import json from utils.logger import get_logger +from config.indicators.config_utils import ( + get_parameter_field_outputs, + get_parameter_field_states, + get_parameter_field_edit_outputs, + get_parameter_field_reset_outputs, + get_parameter_visibility_styles, + collect_parameter_values, + set_parameter_values, + reset_parameter_values +) logger = get_logger("default_logger") @@ -46,48 +56,17 @@ def register_indicator_callbacks(app): return is_open - # Update parameter fields based on indicator type + # Update parameter fields based on indicator type - now fully dynamic! @app.callback( - [Output('indicator-parameters-message', 'style'), - Output('sma-parameters', 'style'), - Output('ema-parameters', 'style'), - Output('rsi-parameters', 'style'), - Output('macd-parameters', 'style'), - Output('bollinger_bands-parameters', 'style')], + get_parameter_field_outputs(), Input('indicator-type-dropdown', 'value'), prevent_initial_call=True ) def update_parameter_fields(indicator_type): """Show/hide parameter input fields based on selected indicator type.""" - # Default styles - hidden_style = {'display': 'none'} - visible_style = {'display': 'block'} - - # Default message visibility - message_style = {'display': 'block'} if not indicator_type else {'display': 'none'} - - # Initialize all as hidden - sma_style = hidden_style - ema_style = hidden_style - rsi_style = hidden_style - macd_style = hidden_style - bb_style = hidden_style - - # Show the relevant parameter section - if indicator_type == 'sma': - sma_style = visible_style - elif indicator_type == 'ema': - ema_style = visible_style - elif indicator_type == 'rsi': - rsi_style = visible_style - elif indicator_type == 'macd': - macd_style = visible_style - elif indicator_type == 'bollinger_bands': - bb_style = visible_style - - return message_style, sma_style, ema_style, rsi_style, macd_style, bb_style + return get_parameter_visibility_styles(indicator_type) - # Save indicator callback + # Save indicator callback - now fully dynamic! @app.callback( [Output('save-indicator-feedback', 'children'), Output('overlay-indicators-checklist', 'options'), @@ -99,27 +78,10 @@ def register_indicator_callbacks(app): State('indicator-timeframe-dropdown', 'value'), State('indicator-color-input', 'value'), State('indicator-line-width-slider', 'value'), - # SMA parameters - State('sma-period-input', 'value'), - # EMA parameters - State('ema-period-input', 'value'), - # RSI parameters - State('rsi-period-input', 'value'), - # MACD parameters - State('macd-fast-period-input', 'value'), - State('macd-slow-period-input', 'value'), - State('macd-signal-period-input', 'value'), - # Bollinger Bands parameters - State('bollinger_bands-period-input', 'value'), - State('bollinger_bands-std-dev-input', 'value'), - # Edit mode data - State('edit-indicator-store', 'data')], + State('edit-indicator-store', 'data')] + get_parameter_field_states(), prevent_initial_call=True ) - def save_new_indicator(n_clicks, name, indicator_type, description, timeframe, color, line_width, - sma_period, ema_period, rsi_period, - macd_fast, macd_slow, macd_signal, - bb_period, bb_stddev, edit_data): + def save_new_indicator(n_clicks, name, indicator_type, description, timeframe, color, line_width, edit_data, *parameter_values): """Save a new indicator or update an existing one.""" if not n_clicks or not name or not indicator_type: return "", no_update, no_update @@ -129,26 +91,16 @@ def register_indicator_callbacks(app): from components.charts.indicator_manager import get_indicator_manager manager = get_indicator_manager() - # Collect parameters based on indicator type and actual input values - parameters = {} + # Create mapping of parameter field IDs to values + parameter_states = get_parameter_field_states() + all_parameter_values = {} + for i, state in enumerate(parameter_states): + if i < len(parameter_values): + field_id = state.component_id + all_parameter_values[field_id] = parameter_values[i] - if indicator_type == 'sma': - parameters = {'period': sma_period or 20} - elif indicator_type == 'ema': - parameters = {'period': ema_period or 12} - elif indicator_type == 'rsi': - parameters = {'period': rsi_period or 14} - elif indicator_type == 'macd': - parameters = { - 'fast_period': macd_fast or 12, - 'slow_period': macd_slow or 26, - 'signal_period': macd_signal or 9 - } - elif indicator_type == 'bollinger_bands': - parameters = { - 'period': bb_period or 20, - 'std_dev': bb_stddev or 2.0 - } + # Collect parameters for the specific indicator type + parameters = collect_parameter_values(indicator_type, all_parameter_values) feedback_msg = None # Check if this is an edit operation @@ -381,7 +333,7 @@ def register_indicator_callbacks(app): error_msg = dbc.Alert(f"Error: {str(e)}", color="danger") return error_msg, no_update, no_update - # Handle edit indicator - open modal with existing data + # Handle edit indicator - open modal with existing data - now fully dynamic! @app.callback( [Output('modal-title', 'children'), Output('indicator-name-input', 'value'), @@ -389,16 +341,7 @@ def register_indicator_callbacks(app): Output('indicator-description-input', 'value'), Output('indicator-timeframe-dropdown', 'value'), Output('indicator-color-input', 'value'), - Output('edit-indicator-store', 'data'), - # Add parameter field outputs - Output('sma-period-input', 'value'), - Output('ema-period-input', 'value'), - Output('rsi-period-input', 'value'), - Output('macd-fast-period-input', 'value'), - Output('macd-slow-period-input', 'value'), - Output('macd-signal-period-input', 'value'), - Output('bollinger_bands-period-input', 'value'), - Output('bollinger_bands-std-dev-input', 'value')], + Output('edit-indicator-store', 'data')] + get_parameter_field_edit_outputs(), [Input({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'n_clicks')], [State({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'id')], prevent_initial_call=True @@ -407,7 +350,10 @@ def register_indicator_callbacks(app): """Load indicator data for editing.""" ctx = callback_context if not ctx.triggered or not any(edit_clicks): - return [no_update] * 15 + # Return the correct number of no_updates for all outputs + basic_outputs = 7 # Modal title, name, type, description, timeframe, color, edit_data + parameter_outputs = len(get_parameter_field_edit_outputs()) + return [no_update] * (basic_outputs + parameter_outputs) # Find which button was clicked triggered_id = ctx.triggered[0]['prop_id'] @@ -424,59 +370,31 @@ def register_indicator_callbacks(app): # Store indicator ID for update edit_data = {'indicator_id': indicator_id, 'mode': 'edit'} - # Extract parameter values based on indicator type - params = indicator.parameters - - # Default parameter values - sma_period = None - ema_period = None - rsi_period = None - macd_fast = None - macd_slow = None - macd_signal = None - bb_period = None - bb_stddev = None - - # Update with actual saved values - if indicator.type == 'sma': - sma_period = params.get('period') - elif indicator.type == 'ema': - ema_period = params.get('period') - elif indicator.type == 'rsi': - rsi_period = params.get('period') - elif indicator.type == 'macd': - macd_fast = params.get('fast_period') - macd_slow = params.get('slow_period') - macd_signal = params.get('signal_period') - elif indicator.type == 'bollinger_bands': - bb_period = params.get('period') - bb_stddev = params.get('std_dev') + # Generate parameter values for all fields + parameter_values = set_parameter_values(indicator.type, indicator.parameters) + # Return all values: basic fields + dynamic parameter fields return ( - f"✏️ Edit Indicator: {indicator.name}", - indicator.name, - indicator.type, - indicator.description, - indicator.timeframe, - indicator.styling.color, - edit_data, - sma_period, - ema_period, - rsi_period, - macd_fast, - macd_slow, - macd_signal, - bb_period, - bb_stddev + [f"✏️ Edit Indicator: {indicator.name}", + indicator.name, + indicator.type, + indicator.description, + indicator.timeframe, + indicator.styling.color, + edit_data] + parameter_values ) else: - return [no_update] * 15 + basic_outputs = 7 + parameter_outputs = len(get_parameter_field_edit_outputs()) + return [no_update] * (basic_outputs + parameter_outputs) except Exception as e: logger.error(f"Indicator callback: Error loading indicator for edit: {e}") - return [no_update] * 15 + basic_outputs = 7 + parameter_outputs = len(get_parameter_field_edit_outputs()) + return [no_update] * (basic_outputs + parameter_outputs) - # Reset modal form when closed or saved + # Reset modal form when closed or saved - now fully dynamic! @app.callback( [Output('indicator-name-input', 'value', allow_duplicate=True), Output('indicator-type-dropdown', 'value', allow_duplicate=True), @@ -485,22 +403,19 @@ def register_indicator_callbacks(app): Output('indicator-color-input', 'value', allow_duplicate=True), Output('indicator-line-width-slider', 'value'), Output('modal-title', 'children', allow_duplicate=True), - Output('edit-indicator-store', 'data', allow_duplicate=True), - # Add parameter field resets - Output('sma-period-input', 'value', allow_duplicate=True), - Output('ema-period-input', 'value', allow_duplicate=True), - Output('rsi-period-input', 'value', allow_duplicate=True), - Output('macd-fast-period-input', 'value', allow_duplicate=True), - Output('macd-slow-period-input', 'value', allow_duplicate=True), - Output('macd-signal-period-input', 'value', allow_duplicate=True), - Output('bollinger_bands-period-input', 'value', allow_duplicate=True), - Output('bollinger_bands-std-dev-input', 'value', allow_duplicate=True)], + Output('edit-indicator-store', 'data', allow_duplicate=True)] + get_parameter_field_reset_outputs(), [Input('cancel-indicator-btn', 'n_clicks'), Input('save-indicator-btn', 'n_clicks')], # Also reset on successful save prevent_initial_call=True ) def reset_modal_form(cancel_clicks, save_clicks): """Reset the modal form to its default state.""" - return "", "", "", "", "", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0 + # Basic form reset values + basic_values = ["", "", "", "", "", 2, "📊 Add New Indicator", None] + + # Dynamic parameter reset values + parameter_values = reset_parameter_values() + + return basic_values + parameter_values logger.info("Indicator callbacks: registered successfully") \ No newline at end of file diff --git a/dashboard/components/indicator_modal.py b/dashboard/components/indicator_modal.py index 3697ba4..ac32ad6 100644 --- a/dashboard/components/indicator_modal.py +++ b/dashboard/components/indicator_modal.py @@ -5,7 +5,7 @@ Indicator modal component for creating and editing indicators. from dash import html, dcc import dash_bootstrap_components as dbc from utils.timeframe_utils import load_timeframe_options -from config.indicators.config_utils import get_indicator_dropdown_options, generate_parameter_fields_config +from config.indicators.config_utils import get_indicator_dropdown_options, generate_parameter_fields_config, load_indicator_templates def create_dynamic_parameter_fields(indicator_type: str) -> html.Div: @@ -137,12 +137,8 @@ def create_indicator_modal(): children=[html.P("Select an indicator type to configure parameters", className="text-muted fst-italic")] ), - # Parameter fields (SMA, EMA, etc.) - create_dynamic_parameter_fields('sma'), - create_dynamic_parameter_fields('ema'), - create_dynamic_parameter_fields('rsi'), - create_dynamic_parameter_fields('macd'), - create_dynamic_parameter_fields('bollinger_bands'), + # Dynamically generate parameter fields for all indicator types + *[create_dynamic_parameter_fields(indicator_type) for indicator_type in load_indicator_templates().keys()], html.Hr(), # Styling Section diff --git a/docs/decisions/ADR-005-Data-Driven-Indicators.md b/docs/decisions/ADR-005-Data-Driven-Indicators.md new file mode 100644 index 0000000..ba9a83d --- /dev/null +++ b/docs/decisions/ADR-005-Data-Driven-Indicators.md @@ -0,0 +1,46 @@ +# ADR-005: Data-Driven Indicator System + +## Status +Accepted + +## Context +Previously, the technical indicator configurations, including their parameters and UI generation logic, were partially hardcoded within Python files (e.g., `dashboard/components/indicator_modal.py`, `dashboard/callbacks/indicators.py`). This approach made adding new indicators, modifying existing ones, or updating their parameter schemas a cumbersome process, requiring direct code modifications in multiple files. + +The need arose for a more flexible, scalable, and maintainable system that allows for easier management and extension of technical indicators without requiring constant code deployments. + +## Decision +We will refactor the technical indicator system to be fully data-driven. This involves: + +1. **Centralizing Indicator Definitions**: Moving indicator metadata, default parameters, and parameter schemas into JSON template files located in `config/indicators/templates/`. +2. **Dynamic UI Generation**: The `dashboard/components/indicator_modal.py` component will dynamically read these JSON templates to generate parameter input fields for the indicator modal, eliminating hardcoded UI elements. +3. **Dynamic Callback Handling**: The `dashboard/callbacks/indicators.py` callbacks will be refactored to dynamically collect, set, and reset indicator parameters based on the schema defined in the JSON templates, removing hardcoded logic for each indicator type. +4. **Runtime Loading**: A new utility (`config/indicators/config_utils.py`) will be responsible for loading and parsing these JSON templates at runtime. + +## Consequences + +### Positive + +- **Increased Extensibility**: Adding new indicators or modifying existing ones now primarily involves creating or updating a JSON file, significantly reducing the development overhead and time to market for new indicator support. +- **Improved Maintainability**: Centralized, data-driven configurations reduce code duplication and simplify updates, as changes are made in one place (the JSON template) rather than across multiple Python files. +- **Reduced Code Complexity**: The `indicator_modal.py` and `indicators.py` files are now more concise and generic, focusing on dynamic generation rather than specific indicator logic. +- **Enhanced Scalability**: The system can easily scale to support a large number of indicators without a proportional increase in Python code complexity. +- **Better Separation of Concerns**: UI presentation logic is decoupled from indicator definition and business logic. + +### Negative + +- **Initial Refactoring Effort**: Requires a significant refactoring effort to migrate existing indicators and update dependent components. +- **New File Type Introduction**: Introduces JSON files as a new configuration format, requiring developers to understand its structure. +- **Runtime Overhead (Minor)**: Small overhead for loading and parsing JSON files at application startup, though this is negligible for typical application sizes. +- **Debugging Configuration Issues**: Issues with JSON formatting or schema mismatches may require checking JSON files in addition to Python code. + +## Alternatives Considered + +- **Keeping Hardcoded Logic**: Rejected due to the high maintenance burden and lack of scalability. +- **Database-Driven Configuration**: Considered storing indicator configurations in a database. Rejected for initial implementation due to added complexity of database schema management, migration, and the overhead of a full CRUD API for configurations, which was deemed unnecessary for the current scope. JSON files provide a simpler, file-based persistence model that meets the immediate needs. +- **YAML/TOML Configuration**: Considered other configuration formats like YAML or TOML. JSON was chosen due to its widespread use in web contexts (Dash/Plotly integration) and native Python support. + +## Decision Makers +[Your Name/Team Lead] + +## Date +2024-06-12 \ No newline at end of file diff --git a/docs/guides/adding-new-indicators.md b/docs/guides/adding-new-indicators.md index 14b5509..64dcb89 100644 --- a/docs/guides/adding-new-indicators.md +++ b/docs/guides/adding-new-indicators.md @@ -202,35 +202,7 @@ SUBPLOT_REGISTRY = { ### 3. Add UI Components -Update `dashboard/components/indicator_modal.py`: - -```python -def create_parameter_fields(): - return html.Div([ - # ... existing fields ... - html.Div([ - dbc.Row([ - dbc.Col([ - dbc.Label("%K Period:"), - dcc.Input( - id='stochastic-k-period-input', - type='number', - value=14 - ) - ], width=6), - dbc.Col([ - dbc.Label("%D Period:"), - dcc.Input( - id='stochastic-d-period-input', - type='number', - value=3 - ) - ], width=6), - ]), - dbc.FormText("Stochastic oscillator periods") - ], id='stochastic-parameters', style={'display': 'none'}) - ]) -``` +***(No longer needed - UI is dynamically generated from JSON templates)*** ## Best Practices diff --git a/docs/modules/charts/configuration.md b/docs/modules/charts/configuration.md index 2a878ef..ef3475c 100644 --- a/docs/modules/charts/configuration.md +++ b/docs/modules/charts/configuration.md @@ -35,11 +35,57 @@ components/charts/config/ ## Indicator Definitions -### Core Classes +The indicator definitions are now primarily managed through **JSON template files** located in `config/indicators/templates/`. These JSON files define the schema, default parameters, display properties, and styling for each technical indicator. This approach allows for easy addition and modification of indicators without requiring code changes. -#### `ChartIndicatorConfig` +### Core Schema Fields (defined in JSON templates) -The main configuration class for individual indicators: +Each indicator JSON template includes the following key fields: + +- **`name`**: Display name of the indicator (e.g., "Simple Moving Average") +- **`description`**: Brief explanation of the indicator. +- **`type`**: Unique identifier for the indicator (e.g., "sma", "ema"). This is used for internal mapping. +- **`display_type`**: How the indicator is rendered on the chart ("overlay" or "subplot"). +- **`timeframe`**: Optional default timeframe for the indicator (can be null for chart timeframe). +- **`default_parameters`**: Default values for the indicator's calculation parameters. +- **`parameter_schema`**: Defines the type, validation rules (min/max), default values, and descriptions for each parameter. +- **`default_styling`**: Default color, line width, and other visual properties. + +**Example JSON Template (`config/indicators/templates/sma_template.json`):** + +```json +{ + "name": "Simple Moving Average", + "description": "Simple Moving Average indicator", + "type": "sma", + "display_type": "overlay", + "timeframe": null, + "default_parameters": { + "period": 20 + }, + "parameter_schema": { + "period": { + "type": "int", + "min": 1, + "max": 200, + "default": 20, + "description": "Period for SMA calculation" + }, + "timeframe": { + "type": "string", + "default": null, + "description": "Indicator timeframe (e.g., '1h', '4h'). Null for chart timeframe." + } + }, + "default_styling": { + "color": "#007bff", + "line_width": 2 + } +} +``` + +### `ChartIndicatorConfig` (Python representation) + +The `ChartIndicatorConfig` Python dataclass in `components/charts/config/indicator_defs.py` serves as the runtime representation of an indicator's configuration, parsed from the JSON templates. ```python @dataclass @@ -56,7 +102,9 @@ class ChartIndicatorConfig: subplot_height_ratio: float = 0.3 # For subplot indicators ``` -#### Enums +### Enums (for internal type safety) + +Enums like `IndicatorType`, `DisplayType`, `LineStyle` are still used internally for type safety and consistent value representation within the Python codebase. **IndicatorType** ```python @@ -67,6 +115,7 @@ class IndicatorType(str, Enum): MACD = "macd" BOLLINGER_BANDS = "bollinger_bands" VOLUME = "volume" + # ... new indicator types should be added here for internal consistency ``` **DisplayType** @@ -86,7 +135,19 @@ class LineStyle(str, Enum): DASH_DOT = "dashdot" ``` -### Schema Validation +**PriceColumn** +```python +class PriceColumn(str, Enum): + OPEN = "open" + HIGH = "high" + LOW = "low" + CLOSE = "close" + VOLUME = "volume" +``` + +### Schema Validation (driven by JSON templates) + +The validation system now primarily reads parameter schemas from the JSON templates. The `IndicatorParameterSchema` and `IndicatorSchema` dataclasses are used for internal representation when parsing and validating these JSON definitions. #### `IndicatorParameterSchema` @@ -119,39 +180,9 @@ class IndicatorSchema: description: str = "" ``` -### Schema Definitions +### Schema Definitions (now loaded dynamically) -The system includes complete schemas for all supported indicators: - -```python -INDICATOR_SCHEMAS = { - IndicatorType.SMA: IndicatorSchema( - indicator_type=IndicatorType.SMA, - display_type=DisplayType.OVERLAY, - parameters=[ - IndicatorParameterSchema( - name="period", - type=int, - min_value=1, - max_value=200, - default_value=20, - description="Number of periods for the moving average" - ), - IndicatorParameterSchema( - name="price_column", - type=str, - required=False, - default_value="close", - valid_values=["open", "high", "low", "close"], - description="Price column to use for calculation" - ) - ], - description="Simple Moving Average - arithmetic mean of prices", - calculation_description="Sum of closing prices divided by period" - ), - # ... more schemas -} -``` +The `INDICATOR_SCHEMAS` dictionary is now populated dynamically at runtime by loading and parsing the JSON template files. Manual definitions in `indicator_defs.py` are deprecated. ### Utility Functions @@ -636,33 +667,58 @@ if performance_issues: ### Adding New Indicators 1. **Define Indicator Type** - ```python - # Add to IndicatorType enum - class IndicatorType(str, Enum): - # ... existing types - STOCHASTIC = "stochastic" - ``` + - Add to `IndicatorType` enum (if not already present) -2. **Create Schema** - ```python - # Add to INDICATOR_SCHEMAS - INDICATOR_SCHEMAS[IndicatorType.STOCHASTIC] = IndicatorSchema( - indicator_type=IndicatorType.STOCHASTIC, - display_type=DisplayType.SUBPLOT, - parameters=[ - IndicatorParameterSchema( - name="k_period", - type=int, - min_value=1, - max_value=100, - default_value=14 - ), - # ... more parameters - ], - description="Stochastic Oscillator", - calculation_description="Momentum indicator comparing closing price to price range" - ) - ``` +2. **Create JSON Template** + - Create a new JSON file in `config/indicators/templates/` (e.g., `stochastic_template.json`) + - Define the indicator's name, type, display type, default parameters, parameter schema, and default styling. + - **Example (`stochastic_template.json`):** + ```json + { + "name": "Stochastic Oscillator", + "description": "Stochastic momentum oscillator indicator", + "type": "stochastic", + "display_type": "subplot", + "timeframe": null, + "default_parameters": { + "k_period": 14, + "d_period": 3, + "smooth_k": 1 + }, + "parameter_schema": { + "k_period": { + "type": "int", + "min": 2, + "max": 50, + "default": 14, + "description": "Period for %K calculation" + }, + "d_period": { + "type": "int", + "min": 1, + "max": 20, + "default": 3, + "description": "Period for %D (moving average of %K)" + }, + "smooth_k": { + "type": "int", + "min": 1, + "max": 10, + "default": 1, + "description": "Smoothing factor for %K" + }, + "timeframe": { + "type": "string", + "default": null, + "description": "Indicator timeframe (e.g., '1h', '4h'). Null for chart timeframe." + } + }, + "default_styling": { + "color": "#e83e8c", + "line_width": 2 + } + } + ``` 3. **Create Default Presets** ```python diff --git a/docs/modules/charts/indicators.md b/docs/modules/charts/indicators.md index 7d35c22..9f66a42 100644 --- a/docs/modules/charts/indicators.md +++ b/docs/modules/charts/indicators.md @@ -89,7 +89,7 @@ These indicators are displayed in separate panels: - Name: Custom name for the indicator - Type: Select from available indicator types - Description: Optional description -3. **Set Parameters**: Type-specific parameters appear dynamically +3. **Set Parameters**: Type-specific parameters appear dynamically (generated from JSON templates) 4. **Customize Styling**: - Color: Hex color code - Line Width: 1-5 pixels @@ -169,7 +169,7 @@ class UserIndicator: description: str # User description type: str # Indicator type (sma, ema, etc.) display_type: str # "overlay" or "subplot" - parameters: Dict[str, Any] # Type-specific parameters + parameters: Dict[str, Any] # Type-specific parameters, dynamically loaded from JSON templates styling: IndicatorStyling # Appearance settings visible: bool = True # Default visibility created_date: datetime # Creation timestamp diff --git a/docs/modules/technical-indicators.md b/docs/modules/technical-indicators.md index 716e309..91ed149 100644 --- a/docs/modules/technical-indicators.md +++ b/docs/modules/technical-indicators.md @@ -304,12 +304,13 @@ for timestamp, row in result_df.iterrows(): ## Contributing When adding new indicators: -1. Create a new class in `implementations/` -2. Inherit from `BaseIndicator` -3. Implement the `calculate` method to return a DataFrame -4. Ensure proper warm-up periods -5. Add comprehensive tests -6. Update documentation +1. Create a new class in `implementations/`. +2. Inherit from `BaseIndicator`. +3. Implement the `calculate` method to return a DataFrame. +4. Ensure proper warm-up periods. +5. Add comprehensive tests. +6. Create a corresponding **JSON template file** in `config/indicators/templates/` to define its parameters, display properties, and styling for UI integration. +7. Update documentation in `docs/guides/adding-new-indicators.md`. See [Adding New Indicators](./adding-new-indicators.md) for detailed instructions.