Refactor indicator management to a data-driven approach
- Introduced dynamic generation of parameter fields and callback handling for indicators, enhancing modularity and maintainability. - Updated `config_utils.py` with new utility functions to load indicator templates and generate dynamic outputs and states for parameter fields. - Refactored `indicators.py` to utilize these utilities, streamlining the callback logic and improving user experience by reducing hardcoded elements. - Modified `indicator_modal.py` to create parameter fields dynamically based on JSON templates, eliminating the need for manual updates when adding new indicators. - Added documentation outlining the new data-driven architecture for indicators, improving clarity and guidance for future development. These changes significantly enhance the flexibility and scalability of the indicator system, aligning with project goals for maintainability and performance.
This commit is contained in:
parent
89b071230e
commit
3e0e89b826
@ -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
|
||||
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
|
||||
@ -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")
|
||||
@ -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
|
||||
|
||||
46
docs/decisions/ADR-005-Data-Driven-Indicators.md
Normal file
46
docs/decisions/ADR-005-Data-Driven-Indicators.md
Normal file
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user