From 476bd67f144d99d65ddd5bb1598264e952e1c30d Mon Sep 17 00:00:00 2001 From: "Vasily.onl" Date: Wed, 4 Jun 2025 13:01:57 +0800 Subject: [PATCH] 3.4 Implement user-defined indicator management system and enhance chart capabilities - Introduced a comprehensive user indicator management system in `components/charts/indicator_manager.py`, allowing users to create, edit, and manage custom indicators with JSON persistence. - Added new default indicators in `components/charts/indicator_defaults.py` to provide users with immediate options for technical analysis. - Enhanced the chart rendering capabilities by implementing the `create_chart_with_indicators` function in `components/charts/builder.py`, supporting both overlay and subplot indicators. - Updated the main application layout in `app.py` to include a modal for adding and editing indicators, improving user interaction. - Enhanced documentation to cover the new indicator system, including a quick guide for adding new indicators and detailed usage examples. - Added unit tests to ensure the reliability and functionality of the new indicator management features. --- app.py | 1237 ++++++++++++++++- components/charts/__init__.py | 29 +- components/charts/builder.py | 251 +++- components/charts/indicator_defaults.py | 133 ++ components/charts/indicator_manager.py | 446 ++++++ components/charts/layers/indicators.py | 15 +- .../templates/bollinger_bands_template.json | 30 + config/indicators/templates/ema_template.json | 22 + .../indicators/templates/macd_template.json | 38 + config/indicators/templates/rsi_template.json | 22 + config/indicators/templates/sma_template.json | 22 + .../bollinger_bands_08c5ed71.json | 20 + .../bollinger_bands_69b378e2.json | 20 + .../user_indicators/ema_ca5fd53d.json | 19 + .../user_indicators/ema_de4fc14c.json | 19 + .../user_indicators/macd_307935a7.json | 21 + .../user_indicators/macd_7335a9bd.json | 21 + .../user_indicators/rsi_1a0e1320.json | 19 + .../user_indicators/rsi_5d160ff7.json | 19 + .../user_indicators/sma_0e235df1.json | 19 + .../user_indicators/sma_8c487df2.json | 19 + docs/components/charts/README.md | 65 +- .../charts/adding-new-indicators.md | 393 ++++++ docs/components/charts/indicators.md | 310 +++++ tasks/3.4. Chart layers.md | 6 +- 25 files changed, 3160 insertions(+), 55 deletions(-) create mode 100644 components/charts/indicator_defaults.py create mode 100644 components/charts/indicator_manager.py create mode 100644 config/indicators/templates/bollinger_bands_template.json create mode 100644 config/indicators/templates/ema_template.json create mode 100644 config/indicators/templates/macd_template.json create mode 100644 config/indicators/templates/rsi_template.json create mode 100644 config/indicators/templates/sma_template.json create mode 100644 config/indicators/user_indicators/bollinger_bands_08c5ed71.json create mode 100644 config/indicators/user_indicators/bollinger_bands_69b378e2.json create mode 100644 config/indicators/user_indicators/ema_ca5fd53d.json create mode 100644 config/indicators/user_indicators/ema_de4fc14c.json create mode 100644 config/indicators/user_indicators/macd_307935a7.json create mode 100644 config/indicators/user_indicators/macd_7335a9bd.json create mode 100644 config/indicators/user_indicators/rsi_1a0e1320.json create mode 100644 config/indicators/user_indicators/rsi_5d160ff7.json create mode 100644 config/indicators/user_indicators/sma_0e235df1.json create mode 100644 config/indicators/user_indicators/sma_8c487df2.json create mode 100644 docs/components/charts/adding-new-indicators.md create mode 100644 docs/components/charts/indicators.md diff --git a/app.py b/app.py index 0b36e9d..b3728a3 100644 --- a/app.py +++ b/app.py @@ -20,7 +20,7 @@ logging.getLogger('sqlalchemy.dialects').setLevel(logging.WARNING) logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING) import dash -from dash import dcc, html, Input, Output, callback +from dash import dcc, html, Input, Output, State, callback import plotly.graph_objects as go from datetime import datetime, timedelta import pandas as pd @@ -33,8 +33,18 @@ from components.charts import ( create_candlestick_chart, get_market_statistics, get_supported_symbols, get_supported_timeframes, create_data_status_indicator, check_data_availability, - create_error_chart + create_error_chart, create_strategy_chart, create_chart_with_indicators ) +from components.charts.config import ( + get_available_strategy_names, + get_all_example_strategies, + get_overlay_indicators, + get_subplot_indicators, + get_all_default_indicators, + get_indicators_by_category +) +from components.charts.indicator_manager import get_indicator_manager +from components.charts.indicator_defaults import ensure_default_indicators # Initialize logger logger = get_logger("dashboard_app") @@ -87,10 +97,296 @@ app.layout = html.Div([ # Store components for data sharing between callbacks dcc.Store(id='market-data-store'), dcc.Store(id='bot-status-store'), + + # Hidden button for callback compatibility (real button is in market data layout) + html.Button(id='add-indicator-btn', style={'display': 'none'}), + + # Add Indicator Modal + html.Div([ + dcc.Store(id='edit-indicator-store', data=None), # Store for edit mode - explicitly start with None + + # Modal Background + html.Div( + id='indicator-modal-background', + style={ + 'display': 'none', + 'position': 'fixed', + 'z-index': '1000', + 'left': '0', + 'top': '0', + 'width': '100%', + 'height': '100%', + 'background-color': 'rgba(0,0,0,0.5)', + 'visibility': 'hidden' + } + ), + + # Modal Content + html.Div([ + html.Div([ + # Modal Header + html.Div([ + html.H4("📊 Add New Indicator", id="modal-title", style={'margin': '0', 'color': '#2c3e50'}), + html.Button( + "✕", + id="close-modal-btn", + style={ + 'background': 'none', + 'border': 'none', + 'font-size': '24px', + 'cursor': 'pointer', + 'color': '#999', + 'float': 'right' + } + ) + ], style={'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '20px', 'border-bottom': '1px solid #eee', 'padding-bottom': '10px'}), + + # Modal Body + html.Div([ + # Basic Settings + html.Div([ + html.H5("Basic Settings", style={'color': '#2c3e50', 'margin-bottom': '15px'}), + + # Indicator Name + html.Div([ + html.Label("Indicator Name:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='indicator-name-input', + type='text', + placeholder='e.g., "SMA 30 Custom"', + style={'width': '100%', 'padding': '8px', 'margin-bottom': '10px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ) + ]), + + # Indicator Type + html.Div([ + html.Label("Indicator Type:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Dropdown( + id='indicator-type-dropdown', + options=[ + {'label': 'Simple Moving Average (SMA)', 'value': 'sma'}, + {'label': 'Exponential Moving Average (EMA)', 'value': 'ema'}, + {'label': 'Relative Strength Index (RSI)', 'value': 'rsi'}, + {'label': 'MACD', 'value': 'macd'}, + {'label': 'Bollinger Bands', 'value': 'bollinger_bands'} + ], + placeholder='Select indicator type', + style={'margin-bottom': '10px'} + ) + ]), + + # Description + html.Div([ + html.Label("Description (Optional):", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Textarea( + id='indicator-description-input', + placeholder='Brief description of this indicator configuration...', + style={'width': '100%', 'height': '60px', 'padding': '8px', 'margin-bottom': '15px', 'border': '1px solid #ddd', 'border-radius': '4px', 'resize': 'vertical'} + ) + ]) + ], style={'margin-bottom': '20px'}), + + # Parameters Section + html.Div([ + html.H5("Parameters", style={'color': '#2c3e50', 'margin-bottom': '15px'}), + + # Default message + html.Div( + id='indicator-parameters-message', + children=[html.P("Select an indicator type to configure parameters", style={'color': '#7f8c8d', 'font-style': 'italic'})], + style={'display': 'block'} + ), + + # SMA Parameters (hidden by default) + html.Div([ + html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='sma-period-input', + type='number', + value=20, + min=1, max=200, + style={'width': '100px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ), + html.P("Number of periods for Simple Moving Average calculation", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) + ], id='sma-parameters', style={'display': 'none', 'margin-bottom': '10px'}), + + # EMA Parameters (hidden by default) + html.Div([ + html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='ema-period-input', + type='number', + value=12, + min=1, max=200, + style={'width': '100px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ), + html.P("Number of periods for Exponential Moving Average calculation", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) + ], id='ema-parameters', style={'display': 'none', 'margin-bottom': '10px'}), + + # RSI Parameters (hidden by default) + html.Div([ + html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='rsi-period-input', + type='number', + value=14, + min=2, max=50, + style={'width': '100px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ), + html.P("Number of periods for RSI calculation (typically 14)", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) + ], id='rsi-parameters', style={'display': 'none', 'margin-bottom': '10px'}), + + # MACD Parameters (hidden by default) + html.Div([ + html.Div([ + html.Label("Fast Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='macd-fast-period-input', + type='number', + value=12, + min=2, max=50, + style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ) + ], style={'margin-bottom': '10px'}), + html.Div([ + html.Label("Slow Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='macd-slow-period-input', + type='number', + value=26, + min=5, max=100, + style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ) + ], style={'margin-bottom': '10px'}), + html.Div([ + html.Label("Signal Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='macd-signal-period-input', + type='number', + value=9, + min=2, max=30, + style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ) + ]), + html.P("MACD periods: Fast EMA, Slow EMA, and Signal line", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) + ], id='macd-parameters', style={'display': 'none', 'margin-bottom': '10px'}), + + # Bollinger Bands Parameters (hidden by default) + html.Div([ + html.Div([ + html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='bb-period-input', + type='number', + value=20, + min=5, max=100, + style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ) + ], style={'margin-bottom': '10px'}), + html.Div([ + html.Label("Standard Deviation:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='bb-stddev-input', + type='number', + value=2.0, + min=0.5, max=5.0, step=0.1, + style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ) + ]), + html.P("Period for middle line (SMA) and standard deviation multiplier", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) + ], id='bb-parameters', style={'display': 'none', 'margin-bottom': '10px'}) + + ], style={'margin-bottom': '20px'}), + + # Styling Section + html.Div([ + html.H5("Styling", style={'color': '#2c3e50', 'margin-bottom': '15px'}), + + html.Div([ + # Color Picker + html.Div([ + html.Label("Color:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='indicator-color-input', + type='text', + value='#007bff', + style={'width': '100px', 'padding': '8px', 'margin-bottom': '10px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ) + ], style={'width': '48%', 'display': 'inline-block', 'margin-right': '4%'}), + + # Line Width + html.Div([ + html.Label("Line Width:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Slider( + id='indicator-line-width-slider', + min=1, max=5, step=1, value=2, + marks={i: str(i) for i in range(1, 6)}, + tooltip={'placement': 'bottom', 'always_visible': True} + ) + ], style={'width': '48%', 'display': 'inline-block'}) + ]) + ], style={'margin-bottom': '20px'}) + ]), + + # Modal Footer + html.Div([ + html.Button( + "Cancel", + id="cancel-indicator-btn", + style={ + 'background-color': '#6c757d', + 'color': 'white', + 'border': 'none', + 'padding': '10px 20px', + 'border-radius': '4px', + 'cursor': 'pointer', + 'margin-right': '10px' + } + ), + html.Button( + "Save Indicator", + id="save-indicator-btn", + style={ + 'background-color': '#28a745', + 'color': 'white', + 'border': 'none', + 'padding': '10px 20px', + 'border-radius': '4px', + 'cursor': 'pointer', + 'font-weight': 'bold' + } + ), + html.Div(id='save-indicator-feedback', style={'margin-top': '10px'}) + ], style={'text-align': 'right', 'border-top': '1px solid #eee', 'padding-top': '15px'}) + + ], style={ + 'background-color': 'white', + 'margin': '5% auto', + 'padding': '30px', + 'border-radius': '8px', + 'box-shadow': '0 4px 6px rgba(0, 0, 0, 0.1)', + 'width': '600px', + 'max-width': '90%', + 'max-height': '80%', + 'overflow-y': 'auto' + }) + ], + id='indicator-modal', + style={ + 'display': 'none', + 'position': 'fixed', + 'z-index': '1001', + 'left': '0', + 'top': '0', + 'width': '100%', + 'height': '100%', + 'visibility': 'hidden' + }) + ]) ]) def get_market_data_layout(): - """Create the market data visualization layout.""" + """Create the market data visualization layout with indicator controls.""" # Get available symbols and timeframes from database symbols = get_supported_symbols() timeframes = get_supported_timeframes() @@ -113,43 +409,270 @@ def get_market_data_layout(): timeframe_options = [opt for opt in timeframe_options if opt['value'] in available_timeframes] - return html.Div([ - html.H2("📊 Real-time Market Data", style={'color': '#2c3e50'}), + # Get available strategies and indicators + try: + strategy_names = get_available_strategy_names() + strategy_options = [{'label': name.replace('_', ' ').title(), 'value': name} for name in strategy_names] - # Symbol selector + # Get user indicators from the new indicator manager + indicator_manager = get_indicator_manager() + + # Ensure default indicators exist + ensure_default_indicators() + + # Get indicators by display type + overlay_indicators = indicator_manager.get_indicators_by_type('overlay') + subplot_indicators = indicator_manager.get_indicators_by_type('subplot') + + # Create checkbox options for overlay indicators + overlay_options = [] + for indicator in overlay_indicators: + display_name = f"{indicator.name} ({indicator.type.upper()})" + overlay_options.append({'label': display_name, 'value': indicator.id}) + + # Create checkbox options for subplot indicators + subplot_options = [] + for indicator in subplot_indicators: + display_name = f"{indicator.name} ({indicator.type.upper()})" + subplot_options.append({'label': display_name, 'value': indicator.id}) + + except Exception as e: + logger.warning(f"Error loading indicator options: {e}") + strategy_options = [{'label': 'Basic Chart', 'value': 'basic'}] + overlay_options = [] + subplot_options = [] + + # Chart Configuration Panel with Add/Edit UI + chart_config_panel = html.Div([ + html.H5("🎯 Chart Configuration", style={'color': '#2c3e50', 'margin-bottom': '15px'}), + + # Add New Indicator Button html.Div([ - html.Label("Select Trading Pair:", style={'font-weight': 'bold'}), - dcc.Dropdown( - id='symbol-dropdown', - options=symbol_options, - value=symbols[0] if symbols else 'BTC-USDT', - style={'margin': '10px 0'} + html.Button( + "➕ Add New Indicator", + id="add-indicator-btn-visible", + className="btn btn-primary", + style={ + 'background-color': '#007bff', + 'color': 'white', + 'border': 'none', + 'padding': '8px 16px', + 'border-radius': '4px', + 'cursor': 'pointer', + 'margin-bottom': '15px', + 'font-weight': 'bold' + } ) - ], style={'width': '300px', 'margin': '20px 0'}), + ]), - # Timeframe selector + # Strategy Selection html.Div([ - html.Label("Timeframe:", style={'font-weight': 'bold'}), + html.Label("Strategy Template:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), dcc.Dropdown( - id='timeframe-dropdown', - options=timeframe_options, - value=available_timeframes[0] if available_timeframes else '1h', - style={'margin': '10px 0'} + id='strategy-dropdown', + options=strategy_options, + value=None, + placeholder="Select a strategy template (optional)", + style={'margin-bottom': '15px'} ) - ], style={'width': '300px', 'margin': '20px 0'}), + ]), - # Price chart - dcc.Graph( - id='price-chart', - style={'height': '600px', 'margin': '20px 0'}, - config={'displayModeBar': True, 'displaylogo': False} + # Indicator Controls with Edit Buttons + html.Div([ + # Overlay Indicators + html.Div([ + html.Label("Overlay Indicators:", style={'font-weight': 'bold', 'margin-bottom': '10px', 'display': 'block'}), + html.Div([ + # Hidden checklist for callback compatibility + dcc.Checklist( + id='overlay-indicators-checklist', + options=overlay_options, + value=[], # Start with no indicators selected + style={'display': 'none'} # Hide the basic checklist + ), + # Custom indicator list with edit buttons + html.Div(id='overlay-indicators-list', children=[ + # This will be populated dynamically + ]) + ]) + ], style={'width': '48%', 'display': 'inline-block', 'margin-right': '4%', 'vertical-align': 'top'}), + + # Subplot Indicators + html.Div([ + html.Label("Subplot Indicators:", style={'font-weight': 'bold', 'margin-bottom': '10px', 'display': 'block'}), + html.Div([ + # Hidden checklist for callback compatibility + dcc.Checklist( + id='subplot-indicators-checklist', + options=subplot_options, + value=[], # Start with no indicators selected + style={'display': 'none'} # Hide the basic checklist + ), + # Custom indicator list with edit buttons + html.Div(id='subplot-indicators-list', children=[ + # This will be populated dynamically + ]) + ]) + ], style={'width': '48%', 'display': 'inline-block', 'vertical-align': 'top'}) + ]) + ], style={ + 'border': '1px solid #bdc3c7', + 'border-radius': '8px', + 'padding': '15px', + 'background-color': '#f8f9fa', + 'margin-bottom': '20px' + }) + + # Parameter Controls Section + parameter_controls = html.Div([ + html.H5("📊 Indicator Parameters", style={'color': '#2c3e50', 'margin-bottom': '15px'}), + + # SMA/EMA Period Controls + html.Div([ + html.Label("Moving Average Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Slider( + id='ma-period-slider', + min=5, max=200, step=5, value=20, + marks={i: str(i) for i in [5, 20, 50, 100, 200]}, + tooltip={'placement': 'bottom', 'always_visible': True} + ) + ], style={'margin-bottom': '20px'}), + + # RSI Period Control + html.Div([ + html.Label("RSI Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Slider( + id='rsi-period-slider', + min=7, max=30, step=1, value=14, + marks={i: str(i) for i in [7, 14, 21, 30]}, + tooltip={'placement': 'bottom', 'always_visible': True} + ) + ], style={'margin-bottom': '20px'}), + + # MACD Parameters + html.Div([ + html.Label("MACD Parameters:", style={'font-weight': 'bold', 'margin-bottom': '10px'}), + html.Div([ + html.Div([ + html.Label("Fast:", style={'font-size': '12px'}), + dcc.Input( + id='macd-fast-input', + type='number', + value=12, + min=5, max=50, + style={'width': '60px', 'margin-left': '5px'} + ) + ], style={'display': 'inline-block', 'margin-right': '15px'}), + html.Div([ + html.Label("Slow:", style={'font-size': '12px'}), + dcc.Input( + id='macd-slow-input', + type='number', + value=26, + min=10, max=100, + style={'width': '60px', 'margin-left': '5px'} + ) + ], style={'display': 'inline-block', 'margin-right': '15px'}), + html.Div([ + html.Label("Signal:", style={'font-size': '12px'}), + dcc.Input( + id='macd-signal-input', + type='number', + value=9, + min=3, max=20, + style={'width': '60px', 'margin-left': '5px'} + ) + ], style={'display': 'inline-block'}) + ]) + ], style={'margin-bottom': '20px'}), + + # Bollinger Bands Parameters + html.Div([ + html.Label("Bollinger Bands:", style={'font-weight': 'bold', 'margin-bottom': '10px'}), + html.Div([ + html.Div([ + html.Label("Period:", style={'font-size': '12px'}), + dcc.Input( + id='bb-period-input', + type='number', + value=20, + min=5, max=50, + style={'width': '60px', 'margin-left': '5px'} + ) + ], style={'display': 'inline-block', 'margin-right': '15px'}), + html.Div([ + html.Label("Std Dev:", style={'font-size': '12px'}), + dcc.Input( + id='bb-stddev-input', + type='number', + value=2.0, + min=1.0, max=3.0, step=0.1, + style={'width': '70px', 'margin-left': '5px'} + ) + ], style={'display': 'inline-block'}) + ]) + ]) + ], style={ + 'border': '1px solid #bdc3c7', + 'border-radius': '8px', + 'padding': '15px', + 'background-color': '#f8f9fa', + 'margin-bottom': '20px' + }) + + # Auto-update control + auto_update_control = html.Div([ + dcc.Checklist( + id='auto-update-checkbox', + options=[{'label': ' Auto-update charts', 'value': 'auto'}], + value=['auto'], + style={'margin-bottom': '10px'} ), + html.Div(id='update-status', style={'font-size': '12px', 'color': '#7f8c8d'}) + ]) + + return html.Div([ + # Title and basic controls + html.H3("💹 Market Data Visualization", style={'color': '#2c3e50', 'margin-bottom': '20px'}), + + # Main chart controls + html.Div([ + html.Div([ + html.Label("Symbol:", style={'font-weight': 'bold'}), + dcc.Dropdown( + id='symbol-dropdown', + options=symbol_options, + value=symbols[0] if symbols else 'BTC-USDT', + clearable=False, + style={'margin-bottom': '10px'} + ) + ], style={'width': '48%', 'display': 'inline-block'}), + html.Div([ + html.Label("Timeframe:", style={'font-weight': 'bold'}), + dcc.Dropdown( + id='timeframe-dropdown', + options=timeframe_options, + value='1h', + clearable=False, + style={'margin-bottom': '10px'} + ) + ], style={'width': '48%', 'float': 'right', 'display': 'inline-block'}) + ], style={'margin-bottom': '20px'}), + + # Chart Configuration Panel + chart_config_panel, + + # Parameter Controls Section + parameter_controls, + + # Auto-update control + auto_update_control, + + # Chart + dcc.Graph(id='price-chart'), # Market statistics - html.Div(id='market-stats', style={'margin': '20px 0'}), - - # Data status indicator - html.Div(id='data-status', style={'margin': '20px 0'}) + html.Div(id='market-stats', style={'margin-top': '20px'}) ]) def get_bot_management_layout(): @@ -227,23 +750,73 @@ def render_tab_content(active_tab): Output('price-chart', 'figure'), [Input('symbol-dropdown', 'value'), Input('timeframe-dropdown', 'value'), + Input('overlay-indicators-checklist', 'value'), + Input('subplot-indicators-checklist', 'value'), + Input('strategy-dropdown', 'value'), Input('interval-component', 'n_intervals')] ) -def update_price_chart(symbol, timeframe, n_intervals): - """Update the price chart with latest market data.""" +def update_price_chart(symbol, timeframe, overlay_indicators, subplot_indicators, selected_strategy, n_intervals): + """Update the price chart with latest market data and selected indicators.""" try: - # Use the real chart component instead of sample data - fig = create_candlestick_chart(symbol, timeframe) + # If a strategy is selected, use strategy chart + if selected_strategy and selected_strategy != 'basic': + fig = create_strategy_chart(symbol, timeframe, selected_strategy) + logger.debug(f"Created strategy chart for {symbol} ({timeframe}) with strategy: {selected_strategy}") + else: + # Create chart with dynamically selected indicators + fig = create_chart_with_indicators( + symbol=symbol, + timeframe=timeframe, + overlay_indicators=overlay_indicators or [], + subplot_indicators=subplot_indicators or [], + days_back=7 + ) + + indicator_count = len(overlay_indicators or []) + len(subplot_indicators or []) + logger.debug(f"Created dynamic chart for {symbol} ({timeframe}) with {indicator_count} indicators") - logger.debug(f"Updated chart for {symbol} ({timeframe}) - interval {n_intervals}") return fig except Exception as e: logger.error(f"Error updating price chart: {e}") - - # Return error chart on failure return create_error_chart(f"Error loading chart: {str(e)}") +# Strategy selection callback - automatically load strategy indicators +@app.callback( + [Output('overlay-indicators-checklist', 'value'), + Output('subplot-indicators-checklist', 'value')], + [Input('strategy-dropdown', 'value')] +) +def update_indicators_from_strategy(selected_strategy): + """Update indicator selections when a strategy is chosen.""" + if not selected_strategy or selected_strategy == 'basic': + return [], [] + + try: + # Get strategy configuration + all_strategies = get_all_example_strategies() + if selected_strategy in all_strategies: + strategy_example = all_strategies[selected_strategy] + config = strategy_example.config + + # Extract overlay and subplot indicators from strategy + overlay_indicators = config.overlay_indicators or [] + + # Extract subplot indicators from subplot configs + subplot_indicators = [] + for subplot_config in config.subplot_configs or []: + subplot_indicators.extend(subplot_config.indicators or []) + + logger.debug(f"Loaded strategy {selected_strategy}: {len(overlay_indicators)} overlays, {len(subplot_indicators)} subplots") + return overlay_indicators, subplot_indicators + else: + logger.warning(f"Strategy {selected_strategy} not found") + return [], [] + + except Exception as e: + logger.error(f"Error loading strategy indicators: {e}") + return [], [] + # Market statistics callback @app.callback( Output('market-stats', 'children'), @@ -337,6 +910,598 @@ def update_data_status(symbol, timeframe, n_intervals): ]) ]) +# Modal control callbacks +@app.callback( + [Output('indicator-modal', 'style'), + Output('indicator-modal-background', 'style')], + [Input('add-indicator-btn', 'n_clicks'), + Input('close-modal-btn', 'n_clicks'), + Input('cancel-indicator-btn', 'n_clicks'), + Input('edit-indicator-store', 'data')] +) +def toggle_indicator_modal(add_clicks, close_clicks, cancel_clicks, edit_data): + """Toggle the visibility of the add indicator modal.""" + + # Default hidden styles + hidden_modal_style = { + 'display': 'none', + 'position': 'fixed', + 'z-index': '1001', + 'left': '0', + 'top': '0', + 'width': '100%', + 'height': '100%', + 'visibility': 'hidden' + } + + hidden_background_style = { + 'display': 'none', + 'position': 'fixed', + 'z-index': '1000', + 'left': '0', + 'top': '0', + 'width': '100%', + 'height': '100%', + 'background-color': 'rgba(0,0,0,0.5)', + 'visibility': 'hidden' + } + + # Visible styles + visible_modal_style = { + 'display': 'block', + 'position': 'fixed', + 'z-index': '1001', + 'left': '0', + 'top': '0', + 'width': '100%', + 'height': '100%', + 'visibility': 'visible' + } + + visible_background_style = { + 'display': 'block', + 'position': 'fixed', + 'z-index': '1000', + 'left': '0', + 'top': '0', + 'width': '100%', + 'height': '100%', + 'background-color': 'rgba(0,0,0,0.5)', + 'visibility': 'visible' + } + + ctx = dash.callback_context + + # If no trigger or initial load, return hidden + if not ctx.triggered: + return [hidden_modal_style, hidden_background_style] + + triggered_id = ctx.triggered[0]['prop_id'].split('.')[0] + + # Only open modal if explicitly requested + should_open = False + + # Check if add button was clicked (and has a click count > 0) + if triggered_id == 'add-indicator-btn' and add_clicks and add_clicks > 0: + should_open = True + + # Check if edit button triggered and should open modal + elif triggered_id == 'edit-indicator-store' and edit_data and edit_data.get('open_modal') and edit_data.get('mode') == 'edit': + should_open = True + + # Check if close/cancel buttons were clicked + elif triggered_id in ['close-modal-btn', 'cancel-indicator-btn']: + should_open = False + + # Default: don't open + else: + should_open = False + + if should_open: + return [visible_modal_style, visible_background_style] + else: + return [hidden_modal_style, hidden_background_style] + +# Sync visible button clicks to hidden button +@app.callback( + Output('add-indicator-btn', 'n_clicks'), + Input('add-indicator-btn-visible', 'n_clicks'), + prevent_initial_call=True +) +def sync_add_button_clicks(visible_clicks): + """Sync clicks from visible button to hidden button.""" + return visible_clicks or 0 + +# Update parameter fields based on indicator type +@app.callback( + [Output('indicator-parameters-message', 'style'), + Output('sma-parameters', 'style'), + Output('ema-parameters', 'style'), + Output('rsi-parameters', 'style'), + Output('macd-parameters', 'style'), + Output('bb-parameters', 'style')], + 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', 'margin-bottom': '10px'} + visible_style = {'display': 'block', 'margin-bottom': '10px'} + + # 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 + +# Save indicator callback +@app.callback( + [Output('save-indicator-feedback', 'children'), + Output('overlay-indicators-checklist', 'options'), + Output('subplot-indicators-checklist', 'options')], + Input('save-indicator-btn', 'n_clicks'), + [State('indicator-name-input', 'value'), + State('indicator-type-dropdown', 'value'), + State('indicator-description-input', '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('bb-period-input', 'value'), + State('bb-stddev-input', 'value'), + # Edit mode data + State('edit-indicator-store', 'data')], + prevent_initial_call=True +) +def save_new_indicator(n_clicks, name, indicator_type, description, color, line_width, + sma_period, ema_period, rsi_period, + macd_fast, macd_slow, macd_signal, + bb_period, bb_stddev, edit_data): + """Save a new indicator or update an existing one.""" + if not n_clicks or not name or not indicator_type: + return "", dash.no_update, dash.no_update + + try: + # Get indicator manager + from components.charts.indicator_manager import get_indicator_manager + manager = get_indicator_manager() + + # Collect parameters based on indicator type and actual input values + parameters = {} + + 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 + } + + # Check if this is an edit operation + is_edit = edit_data and edit_data.get('mode') == 'edit' + + if is_edit: + # Update existing indicator + indicator_id = edit_data.get('indicator_id') + success = manager.update_indicator( + indicator_id, + name=name, + description=description or "", + parameters=parameters, + styling={'color': color or "#007bff", 'line_width': line_width or 2} + ) + + if success: + success_msg = html.Div([ + html.Span("✅ ", style={'color': '#28a745'}), + html.Span(f"Indicator '{name}' updated successfully!", style={'color': '#28a745'}) + ]) + else: + error_msg = html.Div([ + html.Span("❌ ", style={'color': '#dc3545'}), + html.Span("Failed to update indicator. Please try again.", style={'color': '#dc3545'}) + ]) + return error_msg, dash.no_update, dash.no_update + else: + # Create new indicator + new_indicator = manager.create_indicator( + name=name, + indicator_type=indicator_type, + parameters=parameters, + description=description or "", + color=color or "#007bff" + ) + + if not new_indicator: + error_msg = html.Div([ + html.Span("❌ ", style={'color': '#dc3545'}), + html.Span("Failed to save indicator. Please try again.", style={'color': '#dc3545'}) + ]) + return error_msg, dash.no_update, dash.no_update + + success_msg = html.Div([ + html.Span("✅ ", style={'color': '#28a745'}), + html.Span(f"Indicator '{name}' saved successfully!", style={'color': '#28a745'}) + ]) + + # Refresh the indicator options + overlay_indicators = manager.get_indicators_by_type('overlay') + subplot_indicators = manager.get_indicators_by_type('subplot') + + overlay_options = [] + for indicator in overlay_indicators: + display_name = f"{indicator.name} ({indicator.type.upper()})" + overlay_options.append({'label': display_name, 'value': indicator.id}) + + subplot_options = [] + for indicator in subplot_indicators: + display_name = f"{indicator.name} ({indicator.type.upper()})" + subplot_options.append({'label': display_name, 'value': indicator.id}) + + return success_msg, overlay_options, subplot_options + + except Exception as e: + logger.error(f"Error saving indicator: {e}") + error_msg = html.Div([ + html.Span("❌ ", style={'color': '#dc3545'}), + html.Span(f"Error: {str(e)}", style={'color': '#dc3545'}) + ]) + return error_msg, dash.no_update, dash.no_update + +# Update custom indicator lists with edit/delete buttons +@app.callback( + [Output('overlay-indicators-list', 'children'), + Output('subplot-indicators-list', 'children')], + [Input('overlay-indicators-checklist', 'options'), + Input('subplot-indicators-checklist', 'options'), + Input('overlay-indicators-checklist', 'value'), + Input('subplot-indicators-checklist', 'value')] +) +def update_custom_indicator_lists(overlay_options, subplot_options, overlay_values, subplot_values): + """Create custom indicator lists with edit and delete buttons.""" + + def create_indicator_item(option, is_checked): + """Create a single indicator item with checkbox and buttons.""" + indicator_id = option['value'] + indicator_name = option['label'] + + return html.Div([ + # Checkbox and name + html.Div([ + dcc.Checklist( + options=[{'label': '', 'value': indicator_id}], + value=[indicator_id] if is_checked else [], + id={'type': 'indicator-checkbox', 'index': indicator_id}, + style={'display': 'inline-block', 'margin-right': '8px'} + ), + html.Span(indicator_name, style={'display': 'inline-block', 'vertical-align': 'top'}) + ], style={'display': 'inline-block', 'width': '70%'}), + + # Edit and Delete buttons + html.Div([ + html.Button( + "✏️", + id={'type': 'edit-indicator-btn', 'index': indicator_id}, + title="Edit indicator", + style={ + 'background': 'none', + 'border': 'none', + 'cursor': 'pointer', + 'margin-left': '5px', + 'font-size': '14px', + 'color': '#007bff' + } + ), + html.Button( + "🗑️", + id={'type': 'delete-indicator-btn', 'index': indicator_id}, + title="Delete indicator", + style={ + 'background': 'none', + 'border': 'none', + 'cursor': 'pointer', + 'margin-left': '5px', + 'font-size': '14px', + 'color': '#dc3545' + } + ) + ], style={'display': 'inline-block', 'width': '30%', 'text-align': 'right'}) + ], style={ + 'display': 'block', + 'padding': '5px 0', + 'border-bottom': '1px solid #f0f0f0', + 'margin-bottom': '5px' + }) + + # Create overlay indicators list + overlay_list = [] + for option in overlay_options: + is_checked = option['value'] in (overlay_values or []) + overlay_list.append(create_indicator_item(option, is_checked)) + + # Create subplot indicators list + subplot_list = [] + for option in subplot_options: + is_checked = option['value'] in (subplot_values or []) + subplot_list.append(create_indicator_item(option, is_checked)) + + return overlay_list, subplot_list + +# Sync individual indicator checkboxes with main checklist +@app.callback( + Output('overlay-indicators-checklist', 'value', allow_duplicate=True), + [Input({'type': 'indicator-checkbox', 'index': dash.ALL}, 'value')], + [State('overlay-indicators-checklist', 'options')], + prevent_initial_call=True +) +def sync_overlay_indicators(checkbox_values, overlay_options): + """Sync individual indicator checkboxes with main overlay checklist.""" + if not checkbox_values or not overlay_options: + return [] + + selected_indicators = [] + overlay_ids = [opt['value'] for opt in overlay_options] + + # Flatten the checkbox values and filter for overlay indicators + for values in checkbox_values: + if values: # values is a list, check if not empty + for indicator_id in values: + if indicator_id in overlay_ids: + selected_indicators.append(indicator_id) + + # Remove duplicates + return list(set(selected_indicators)) + +@app.callback( + Output('subplot-indicators-checklist', 'value', allow_duplicate=True), + [Input({'type': 'indicator-checkbox', 'index': dash.ALL}, 'value')], + [State('subplot-indicators-checklist', 'options')], + prevent_initial_call=True +) +def sync_subplot_indicators(checkbox_values, subplot_options): + """Sync individual indicator checkboxes with main subplot checklist.""" + if not checkbox_values or not subplot_options: + return [] + + selected_indicators = [] + subplot_ids = [opt['value'] for opt in subplot_options] + + # Flatten the checkbox values and filter for subplot indicators + for values in checkbox_values: + if values: # values is a list, check if not empty + for indicator_id in values: + if indicator_id in subplot_ids: + selected_indicators.append(indicator_id) + + # Remove duplicates + return list(set(selected_indicators)) + +# Handle delete indicator +@app.callback( + [Output('save-indicator-feedback', 'children', allow_duplicate=True), + Output('overlay-indicators-checklist', 'options', allow_duplicate=True), + Output('subplot-indicators-checklist', 'options', allow_duplicate=True)], + [Input({'type': 'delete-indicator-btn', 'index': dash.ALL}, 'n_clicks')], + [State({'type': 'delete-indicator-btn', 'index': dash.ALL}, 'id')], + prevent_initial_call=True +) +def delete_indicator(delete_clicks, button_ids): + """Delete an indicator when delete button is clicked.""" + ctx = dash.callback_context + if not ctx.triggered or not any(delete_clicks): + return dash.no_update, dash.no_update, dash.no_update + + # Find which button was clicked + triggered_id = ctx.triggered[0]['prop_id'] + import json + button_info = json.loads(triggered_id.split('.')[0]) + indicator_id = button_info['index'] + + try: + # Get indicator manager and delete the indicator + from components.charts.indicator_manager import get_indicator_manager + manager = get_indicator_manager() + + # Load indicator to get its name before deletion + indicator = manager.load_indicator(indicator_id) + indicator_name = indicator.name if indicator else indicator_id + + if manager.delete_indicator(indicator_id): + # Refresh the indicator options + overlay_indicators = manager.get_indicators_by_type('overlay') + subplot_indicators = manager.get_indicators_by_type('subplot') + + overlay_options = [] + for indicator in overlay_indicators: + display_name = f"{indicator.name} ({indicator.type.upper()})" + overlay_options.append({'label': display_name, 'value': indicator.id}) + + subplot_options = [] + for indicator in subplot_indicators: + display_name = f"{indicator.name} ({indicator.type.upper()})" + subplot_options.append({'label': display_name, 'value': indicator.id}) + + success_msg = html.Div([ + html.Span("🗑️ ", style={'color': '#dc3545'}), + html.Span(f"Indicator '{indicator_name}' deleted successfully!", style={'color': '#dc3545'}) + ]) + + return success_msg, overlay_options, subplot_options + else: + error_msg = html.Div([ + html.Span("❌ ", style={'color': '#dc3545'}), + html.Span("Failed to delete indicator.", style={'color': '#dc3545'}) + ]) + return error_msg, dash.no_update, dash.no_update + + except Exception as e: + logger.error(f"Error deleting indicator: {e}") + error_msg = html.Div([ + html.Span("❌ ", style={'color': '#dc3545'}), + html.Span(f"Error: {str(e)}", style={'color': '#dc3545'}) + ]) + return error_msg, dash.no_update, dash.no_update + +# Handle edit indicator - open modal with existing data +@app.callback( + [Output('modal-title', 'children'), + Output('indicator-name-input', 'value'), + Output('indicator-type-dropdown', 'value'), + Output('indicator-description-input', '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('bb-period-input', 'value'), + Output('bb-stddev-input', 'value')], + [Input({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'n_clicks')], + [State({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'id')], + prevent_initial_call=True +) +def edit_indicator(edit_clicks, button_ids): + """Load indicator data for editing.""" + ctx = dash.callback_context + if not ctx.triggered or not any(edit_clicks): + return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update + + # Find which button was clicked + triggered_id = ctx.triggered[0]['prop_id'] + import json + button_info = json.loads(triggered_id.split('.')[0]) + indicator_id = button_info['index'] + + try: + # Load the indicator data + from components.charts.indicator_manager import get_indicator_manager + manager = get_indicator_manager() + indicator = manager.load_indicator(indicator_id) + + if indicator: + # Store indicator ID for update + edit_data = {'indicator_id': indicator_id, 'mode': 'edit', 'open_modal': True} + + # Extract parameter values based on indicator type + params = indicator.parameters + + # Default parameter values + sma_period = 20 + ema_period = 12 + rsi_period = 14 + macd_fast = 12 + macd_slow = 26 + macd_signal = 9 + bb_period = 20 + bb_stddev = 2.0 + + # Update with actual saved values + if indicator.type == 'sma': + sma_period = params.get('period', 20) + elif indicator.type == 'ema': + ema_period = params.get('period', 12) + elif indicator.type == 'rsi': + rsi_period = params.get('period', 14) + elif indicator.type == 'macd': + macd_fast = params.get('fast_period', 12) + macd_slow = params.get('slow_period', 26) + macd_signal = params.get('signal_period', 9) + elif indicator.type == 'bollinger_bands': + bb_period = params.get('period', 20) + bb_stddev = params.get('std_dev', 2.0) + + return ( + "✏️ Edit Indicator", + indicator.name, + indicator.type, + indicator.description, + indicator.styling.color, + edit_data, + sma_period, + ema_period, + rsi_period, + macd_fast, + macd_slow, + macd_signal, + bb_period, + bb_stddev + ) + else: + return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update + + except Exception as e: + logger.error(f"Error loading indicator for edit: {e}") + return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update + +# Reset modal form when closed +@app.callback( + [Output('indicator-name-input', 'value', allow_duplicate=True), + Output('indicator-type-dropdown', 'value', allow_duplicate=True), + Output('indicator-description-input', 'value', allow_duplicate=True), + 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('bb-period-input', 'value', allow_duplicate=True), + Output('bb-stddev-input', 'value', allow_duplicate=True)], + [Input('close-modal-btn', 'n_clicks'), + Input('cancel-indicator-btn', 'n_clicks')], + prevent_initial_call=True +) +def reset_modal_form(close_clicks, cancel_clicks): + """Reset the modal form when it's closed.""" + if close_clicks or cancel_clicks: + return "", None, "", "#007bff", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0 + return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update + def main(): """Main function to run the dashboard.""" try: diff --git a/components/charts/__init__.py b/components/charts/__init__.py index 9214829..fbfa014 100644 --- a/components/charts/__init__.py +++ b/components/charts/__init__.py @@ -14,6 +14,7 @@ Main Components: """ import plotly.graph_objects as go +from typing import List from .builder import ChartBuilder from .utils import ( validate_market_data, @@ -137,7 +138,8 @@ __all__ = [ # Convenience functions "create_basic_chart", - "create_indicator_chart" + "create_indicator_chart", + "create_chart_with_indicators" ] # Initialize logger @@ -447,4 +449,27 @@ def create_indicator_chart(symbol: str, data: list, return create_basic_chart(symbol, data, indicators=[indicator_config]) except Exception as e: - return create_basic_chart(symbol, data, indicators=[]) # Fallback to basic chart \ No newline at end of file + return create_basic_chart(symbol, data, indicators=[]) # Fallback to basic chart + +def create_chart_with_indicators(symbol: str, timeframe: str, + overlay_indicators: List[str] = None, + subplot_indicators: List[str] = None, + days_back: int = 7, **kwargs) -> go.Figure: + """ + Create a chart with dynamically selected indicators. + + Args: + symbol: Trading pair (e.g., 'BTC-USDT') + timeframe: Timeframe (e.g., '1h', '1d') + overlay_indicators: List of overlay indicator names + subplot_indicators: List of subplot indicator names + days_back: Number of days to look back + **kwargs: Additional chart parameters + + Returns: + Plotly figure with selected indicators + """ + builder = ChartBuilder() + return builder.create_chart_with_indicators( + symbol, timeframe, overlay_indicators, subplot_indicators, days_back, **kwargs + ) \ No newline at end of file diff --git a/components/charts/builder.py b/components/charts/builder.py index 0222183..ff7c9b8 100644 --- a/components/charts/builder.py +++ b/components/charts/builder.py @@ -349,4 +349,253 @@ class ChartBuilder: 'data_age_minutes': None, 'sufficient_for_indicators': False, 'message': f"Error checking data: {str(e)}" - } \ No newline at end of file + } + + def create_chart_with_indicators(self, symbol: str, timeframe: str, + overlay_indicators: List[str] = None, + subplot_indicators: List[str] = None, + days_back: int = 7, **kwargs) -> go.Figure: + """ + Create a chart with dynamically selected indicators. + + Args: + symbol: Trading pair + timeframe: Timeframe + overlay_indicators: List of overlay indicator names + subplot_indicators: List of subplot indicator names + days_back: Number of days to look back + **kwargs: Additional chart parameters + + Returns: + Plotly Figure object with selected indicators + """ + try: + # Fetch market data + candles = self.fetch_market_data_enhanced(symbol, timeframe, days_back) + + if not candles: + self.logger.warning(f"No data available for {symbol} {timeframe}") + return self._create_empty_chart(f"No data available for {symbol} {timeframe}") + + # Validate and prepare data + if not validate_market_data(candles): + self.logger.error(f"Invalid market data for {symbol} {timeframe}") + return self._create_error_chart("Invalid market data format") + + df = prepare_chart_data(candles) + + # Import layer classes + from .layers import ( + LayerManager, CandlestickLayer, VolumeLayer, + SMALayer, EMALayer, BollingerBandsLayer, + RSILayer, MACDLayer, IndicatorLayerConfig + ) + from .indicator_manager import get_indicator_manager + + # Get user indicators instead of default configurations + indicator_manager = get_indicator_manager() + + # Calculate subplot requirements + subplot_count = 0 + volume_enabled = 'volume' in df.columns and df['volume'].sum() > 0 + if volume_enabled: + subplot_count += 1 + + if subplot_indicators: + subplot_count += len(subplot_indicators) + + # Create subplot structure if needed + if subplot_count > 0: + # Calculate height ratios + main_height = 0.7 # Main chart gets 70% + subplot_height = 0.3 / subplot_count if subplot_count > 0 else 0 + + # Create subplot specifications + subplot_specs = [[{"secondary_y": False}]] # Main chart + row_heights = [main_height] + + if volume_enabled: + subplot_specs.append([{"secondary_y": False}]) + row_heights.append(subplot_height) + + if subplot_indicators: + for _ in subplot_indicators: + subplot_specs.append([{"secondary_y": False}]) + row_heights.append(subplot_height) + + # Create subplots figure + from plotly.subplots import make_subplots + fig = make_subplots( + rows=len(subplot_specs), + cols=1, + shared_xaxes=True, + vertical_spacing=0.02, + row_heights=row_heights, + specs=subplot_specs, + subplot_titles=[f"{symbol} - {timeframe}"] + [""] * (len(subplot_specs) - 1) + ) + else: + # Create simple figure for main chart only + fig = go.Figure() + + current_row = 1 + + # Add candlestick layer (always included) + candlestick_trace = go.Candlestick( + x=df['timestamp'], + open=df['open'], + high=df['high'], + low=df['low'], + close=df['close'], + name=symbol, + increasing_line_color=self.default_colors['bullish'], + decreasing_line_color=self.default_colors['bearish'], + showlegend=False + ) + fig.add_trace(candlestick_trace, row=current_row, col=1) + + # Add overlay indicators + if overlay_indicators: + for indicator_id in overlay_indicators: + try: + # Load user indicator + user_indicator = indicator_manager.load_indicator(indicator_id) + + if user_indicator is None: + self.logger.warning(f"Overlay indicator {indicator_id} not found") + continue + + # Create appropriate indicator layer using user configuration + if user_indicator.type == 'sma': + period = user_indicator.parameters.get('period', 20) + layer_config = IndicatorLayerConfig( + name=user_indicator.name, + indicator_type='sma', + color=user_indicator.styling.color, + parameters={'period': period}, + line_width=user_indicator.styling.line_width + ) + sma_layer = SMALayer(layer_config) + traces = sma_layer.create_traces(df.to_dict('records')) + for trace in traces: + fig.add_trace(trace, row=current_row, col=1) + + elif user_indicator.type == 'ema': + period = user_indicator.parameters.get('period', 12) + layer_config = IndicatorLayerConfig( + name=user_indicator.name, + indicator_type='ema', + color=user_indicator.styling.color, + parameters={'period': period}, + line_width=user_indicator.styling.line_width + ) + ema_layer = EMALayer(layer_config) + traces = ema_layer.create_traces(df.to_dict('records')) + for trace in traces: + fig.add_trace(trace, row=current_row, col=1) + + elif user_indicator.type == 'bollinger_bands': + period = user_indicator.parameters.get('period', 20) + std_dev = user_indicator.parameters.get('std_dev', 2.0) + layer_config = IndicatorLayerConfig( + name=user_indicator.name, + indicator_type='bollinger_bands', + color=user_indicator.styling.color, + parameters={'period': period, 'std_dev': std_dev}, + line_width=user_indicator.styling.line_width, + show_middle_line=True + ) + bb_layer = BollingerBandsLayer(layer_config) + traces = bb_layer.create_traces(df.to_dict('records')) + for trace in traces: + fig.add_trace(trace, row=current_row, col=1) + + self.logger.debug(f"Added overlay indicator: {user_indicator.name}") + except Exception as e: + self.logger.error(f"Error adding overlay indicator {indicator_id}: {e}") + + # Move to next row for volume if enabled + if volume_enabled: + current_row += 1 + volume_colors = [self.default_colors['bullish'] if close >= open else self.default_colors['bearish'] + for close, open in zip(df['close'], df['open'])] + + volume_trace = go.Bar( + x=df['timestamp'], + y=df['volume'], + name='Volume', + marker_color=volume_colors, + opacity=0.7, + showlegend=False + ) + fig.add_trace(volume_trace, row=current_row, col=1) + fig.update_yaxes(title_text="Volume", row=current_row, col=1) + + # Add subplot indicators + if subplot_indicators: + for indicator_id in subplot_indicators: + current_row += 1 + try: + # Load user indicator + user_indicator = indicator_manager.load_indicator(indicator_id) + + if user_indicator is None: + self.logger.warning(f"Subplot indicator {indicator_id} not found") + continue + + # Create appropriate subplot indicator layer + if user_indicator.type == 'rsi': + period = user_indicator.parameters.get('period', 14) + rsi_layer = RSILayer(period=period, color=user_indicator.styling.color, name=user_indicator.name) + + # Use the render method + fig = rsi_layer.render(fig, df, row=current_row, col=1) + + # Add RSI reference lines + fig.add_hline(y=70, line_dash="dash", line_color="red", opacity=0.5, row=current_row, col=1) + fig.add_hline(y=30, line_dash="dash", line_color="green", opacity=0.5, row=current_row, col=1) + fig.update_yaxes(title_text="RSI", range=[0, 100], row=current_row, col=1) + + elif user_indicator.type == 'macd': + fast_period = user_indicator.parameters.get('fast_period', 12) + slow_period = user_indicator.parameters.get('slow_period', 26) + signal_period = user_indicator.parameters.get('signal_period', 9) + macd_layer = MACDLayer(fast_period=fast_period, slow_period=slow_period, + signal_period=signal_period, color=user_indicator.styling.color, name=user_indicator.name) + + # Use the render method + fig = macd_layer.render(fig, df, row=current_row, col=1) + + # Add zero line for MACD + fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.5, row=current_row, col=1) + fig.update_yaxes(title_text="MACD", row=current_row, col=1) + + self.logger.debug(f"Added subplot indicator: {user_indicator.name}") + except Exception as e: + self.logger.error(f"Error adding subplot indicator {indicator_id}: {e}") + + # Update layout + height = kwargs.get('height', self.default_height) + template = kwargs.get('template', self.default_template) + + fig.update_layout( + title=f"{symbol} - {timeframe} Chart", + template=template, + height=height, + showlegend=True, + legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01), + xaxis_rangeslider_visible=False, + hovermode='x unified' + ) + + # Update x-axis for all subplots + fig.update_xaxes(title_text="Time", row=current_row, col=1) + fig.update_yaxes(title_text="Price (USDT)", row=1, col=1) + + indicator_count = len(overlay_indicators or []) + len(subplot_indicators or []) + self.logger.debug(f"Created chart for {symbol} {timeframe} with {indicator_count} indicators") + return fig + + except Exception as e: + self.logger.error(f"Error creating chart with indicators: {e}") + return self._create_error_chart(f"Chart creation failed: {str(e)}") \ No newline at end of file diff --git a/components/charts/indicator_defaults.py b/components/charts/indicator_defaults.py new file mode 100644 index 0000000..bb87648 --- /dev/null +++ b/components/charts/indicator_defaults.py @@ -0,0 +1,133 @@ +""" +Default Indicator Creation + +This module creates a set of default indicators that users can start with. +These are common indicator configurations that are immediately useful. +""" + +from .indicator_manager import get_indicator_manager, IndicatorType, DisplayType + + +def create_default_indicators(): + """Create default indicators if they don't exist.""" + manager = get_indicator_manager() + + # Check if we already have indicators + existing_indicators = manager.list_indicators() + if existing_indicators: + manager.logger.info(f"Found {len(existing_indicators)} existing indicators, skipping defaults creation") + return + + # Define default indicators + default_indicators = [ + # Moving Averages + { + "name": "SMA 20", + "description": "20-period Simple Moving Average for short-term trend", + "type": IndicatorType.SMA.value, + "parameters": {"period": 20}, + "color": "#007bff" + }, + { + "name": "SMA 50", + "description": "50-period Simple Moving Average for medium-term trend", + "type": IndicatorType.SMA.value, + "parameters": {"period": 50}, + "color": "#6c757d" + }, + { + "name": "EMA 12", + "description": "12-period Exponential Moving Average for fast signals", + "type": IndicatorType.EMA.value, + "parameters": {"period": 12}, + "color": "#ff6b35" + }, + { + "name": "EMA 26", + "description": "26-period Exponential Moving Average for slower signals", + "type": IndicatorType.EMA.value, + "parameters": {"period": 26}, + "color": "#28a745" + }, + + # Oscillators + { + "name": "RSI 14", + "description": "14-period RSI for momentum analysis", + "type": IndicatorType.RSI.value, + "parameters": {"period": 14}, + "color": "#20c997" + }, + { + "name": "RSI 21", + "description": "21-period RSI for less sensitive momentum signals", + "type": IndicatorType.RSI.value, + "parameters": {"period": 21}, + "color": "#17a2b8" + }, + + # MACD Variants + { + "name": "MACD Standard", + "description": "Standard MACD (12, 26, 9) for trend changes", + "type": IndicatorType.MACD.value, + "parameters": {"fast_period": 12, "slow_period": 26, "signal_period": 9}, + "color": "#fd7e14" + }, + { + "name": "MACD Fast", + "description": "Fast MACD (5, 13, 4) for quick signals", + "type": IndicatorType.MACD.value, + "parameters": {"fast_period": 5, "slow_period": 13, "signal_period": 4}, + "color": "#dc3545" + }, + + # Bollinger Bands + { + "name": "Bollinger Bands", + "description": "Standard Bollinger Bands (20, 2) for volatility analysis", + "type": IndicatorType.BOLLINGER_BANDS.value, + "parameters": {"period": 20, "std_dev": 2.0}, + "color": "#6f42c1" + }, + { + "name": "Bollinger Tight", + "description": "Tight Bollinger Bands (20, 1.5) for sensitive volatility", + "type": IndicatorType.BOLLINGER_BANDS.value, + "parameters": {"period": 20, "std_dev": 1.5}, + "color": "#e83e8c" + } + ] + + # Create indicators + created_count = 0 + for indicator_config in default_indicators: + indicator = manager.create_indicator( + name=indicator_config["name"], + indicator_type=indicator_config["type"], + parameters=indicator_config["parameters"], + description=indicator_config["description"], + color=indicator_config["color"] + ) + + if indicator: + created_count += 1 + manager.logger.info(f"Created default indicator: {indicator.name}") + else: + manager.logger.error(f"Failed to create indicator: {indicator_config['name']}") + + manager.logger.info(f"Created {created_count} default indicators") + + +def ensure_default_indicators(): + """Ensure default indicators exist (called during app startup).""" + try: + create_default_indicators() + except Exception as e: + manager = get_indicator_manager() + manager.logger.error(f"Error creating default indicators: {e}") + + +if __name__ == "__main__": + # Create defaults when run directly + create_default_indicators() \ No newline at end of file diff --git a/components/charts/indicator_manager.py b/components/charts/indicator_manager.py new file mode 100644 index 0000000..43cf095 --- /dev/null +++ b/components/charts/indicator_manager.py @@ -0,0 +1,446 @@ +""" +Indicator Management System + +This module provides functionality to manage user-defined indicators with +file-based storage. Each indicator is saved as a separate JSON file for +portability and easy sharing. +""" + +import json +import os +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, List, Optional, Any, Tuple +from dataclasses import dataclass, asdict +from enum import Enum + +from utils.logger import get_logger + +# Initialize logger +logger = get_logger("indicator_manager") + +# Base directory for indicators +INDICATORS_DIR = Path("config/indicators") +USER_INDICATORS_DIR = INDICATORS_DIR / "user_indicators" +TEMPLATES_DIR = INDICATORS_DIR / "templates" + + +class IndicatorType(str, Enum): + """Supported indicator types.""" + SMA = "sma" + EMA = "ema" + RSI = "rsi" + MACD = "macd" + BOLLINGER_BANDS = "bollinger_bands" + + +class DisplayType(str, Enum): + """Chart display types for indicators.""" + OVERLAY = "overlay" + SUBPLOT = "subplot" + + +@dataclass +class IndicatorStyling: + """Styling configuration for indicators.""" + color: str = "#007bff" + line_width: int = 2 + opacity: float = 1.0 + line_style: str = "solid" # solid, dash, dot, dashdot + + +@dataclass +class UserIndicator: + """User-defined indicator configuration.""" + id: str + name: str + description: str + type: str # IndicatorType + display_type: str # DisplayType + parameters: Dict[str, Any] + styling: IndicatorStyling + visible: bool = True + created_date: str = "" + modified_date: str = "" + + def __post_init__(self): + """Initialize timestamps if not provided.""" + current_time = datetime.now(timezone.utc).isoformat() + if not self.created_date: + self.created_date = current_time + if not self.modified_date: + self.modified_date = current_time + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'type': self.type, + 'display_type': self.display_type, + 'parameters': self.parameters, + 'styling': asdict(self.styling), + 'visible': self.visible, + 'created_date': self.created_date, + 'modified_date': self.modified_date + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'UserIndicator': + """Create UserIndicator from dictionary.""" + styling_data = data.get('styling', {}) + styling = IndicatorStyling(**styling_data) + + return cls( + id=data['id'], + name=data['name'], + description=data.get('description', ''), + type=data['type'], + display_type=data['display_type'], + parameters=data.get('parameters', {}), + styling=styling, + visible=data.get('visible', True), + created_date=data.get('created_date', ''), + modified_date=data.get('modified_date', '') + ) + + +class IndicatorManager: + """Manager for user-defined indicators with file-based storage.""" + + def __init__(self): + """Initialize the indicator manager.""" + self.logger = logger + self._ensure_directories() + self._create_default_templates() + + def _ensure_directories(self): + """Ensure indicator directories exist.""" + try: + USER_INDICATORS_DIR.mkdir(parents=True, exist_ok=True) + TEMPLATES_DIR.mkdir(parents=True, exist_ok=True) + self.logger.debug("Indicator directories created/verified") + except Exception as e: + self.logger.error(f"Error creating indicator directories: {e}") + + def _get_indicator_file_path(self, indicator_id: str) -> Path: + """Get file path for an indicator.""" + return USER_INDICATORS_DIR / f"{indicator_id}.json" + + def _get_template_file_path(self, indicator_type: str) -> Path: + """Get file path for an indicator template.""" + return TEMPLATES_DIR / f"{indicator_type}_template.json" + + def save_indicator(self, indicator: UserIndicator) -> bool: + """ + Save an indicator to file. + + Args: + indicator: UserIndicator instance to save + + Returns: + True if saved successfully, False otherwise + """ + try: + # Update modified date + indicator.modified_date = datetime.now(timezone.utc).isoformat() + + file_path = self._get_indicator_file_path(indicator.id) + + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(indicator.to_dict(), f, indent=2, ensure_ascii=False) + + self.logger.info(f"Saved indicator: {indicator.name} ({indicator.id})") + return True + + except Exception as e: + self.logger.error(f"Error saving indicator {indicator.id}: {e}") + return False + + def load_indicator(self, indicator_id: str) -> Optional[UserIndicator]: + """ + Load an indicator from file. + + Args: + indicator_id: ID of the indicator to load + + Returns: + UserIndicator instance or None if not found/error + """ + try: + file_path = self._get_indicator_file_path(indicator_id) + + if not file_path.exists(): + self.logger.warning(f"Indicator file not found: {indicator_id}") + return None + + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + indicator = UserIndicator.from_dict(data) + self.logger.debug(f"Loaded indicator: {indicator.name} ({indicator.id})") + return indicator + + except Exception as e: + self.logger.error(f"Error loading indicator {indicator_id}: {e}") + return None + + def list_indicators(self, visible_only: bool = False) -> List[UserIndicator]: + """ + List all user indicators. + + Args: + visible_only: If True, only return visible indicators + + Returns: + List of UserIndicator instances + """ + indicators = [] + + try: + for file_path in USER_INDICATORS_DIR.glob("*.json"): + indicator_id = file_path.stem + indicator = self.load_indicator(indicator_id) + + if indicator: + if not visible_only or indicator.visible: + indicators.append(indicator) + + # Sort by name + indicators.sort(key=lambda x: x.name.lower()) + self.logger.debug(f"Listed {len(indicators)} indicators") + + except Exception as e: + self.logger.error(f"Error listing indicators: {e}") + + return indicators + + def delete_indicator(self, indicator_id: str) -> bool: + """ + Delete an indicator. + + Args: + indicator_id: ID of the indicator to delete + + Returns: + True if deleted successfully, False otherwise + """ + try: + file_path = self._get_indicator_file_path(indicator_id) + + if file_path.exists(): + file_path.unlink() + self.logger.info(f"Deleted indicator: {indicator_id}") + return True + else: + self.logger.warning(f"Indicator file not found for deletion: {indicator_id}") + return False + + except Exception as e: + self.logger.error(f"Error deleting indicator {indicator_id}: {e}") + return False + + def create_indicator(self, name: str, indicator_type: str, parameters: Dict[str, Any], + description: str = "", color: str = "#007bff", + display_type: str = None) -> Optional[UserIndicator]: + """ + Create a new indicator. + + Args: + name: Display name for the indicator + indicator_type: Type of indicator (sma, ema, etc.) + parameters: Indicator parameters + description: Optional description + color: Color for chart display + display_type: overlay or subplot (auto-detected if None) + + Returns: + Created UserIndicator instance or None if error + """ + try: + # Generate unique ID + indicator_id = f"{indicator_type}_{uuid.uuid4().hex[:8]}" + + # Auto-detect display type if not provided + if display_type is None: + display_type = self._get_default_display_type(indicator_type) + + # Create styling + styling = IndicatorStyling(color=color) + + # Create indicator + indicator = UserIndicator( + id=indicator_id, + name=name, + description=description, + type=indicator_type, + display_type=display_type, + parameters=parameters, + styling=styling + ) + + # Save to file + if self.save_indicator(indicator): + self.logger.info(f"Created new indicator: {name} ({indicator_id})") + return indicator + else: + return None + + except Exception as e: + self.logger.error(f"Error creating indicator: {e}") + return None + + def update_indicator(self, indicator_id: str, **updates) -> bool: + """ + Update an existing indicator. + + Args: + indicator_id: ID of indicator to update + **updates: Fields to update + + Returns: + True if updated successfully, False otherwise + """ + try: + indicator = self.load_indicator(indicator_id) + if not indicator: + return False + + # Update fields + for field, value in updates.items(): + if hasattr(indicator, field): + if field == 'styling' and isinstance(value, dict): + # Update styling fields + for style_field, style_value in value.items(): + if hasattr(indicator.styling, style_field): + setattr(indicator.styling, style_field, style_value) + else: + setattr(indicator, field, value) + + return self.save_indicator(indicator) + + except Exception as e: + self.logger.error(f"Error updating indicator {indicator_id}: {e}") + return False + + def get_indicators_by_type(self, display_type: str) -> List[UserIndicator]: + """Get indicators by display type (overlay/subplot).""" + indicators = self.list_indicators(visible_only=True) + return [ind for ind in indicators if ind.display_type == display_type] + + def get_available_indicator_types(self) -> List[str]: + """Get list of available indicator types.""" + return [t.value for t in IndicatorType] + + def _get_default_display_type(self, indicator_type: str) -> str: + """Get default display type for an indicator type.""" + overlay_types = {IndicatorType.SMA, IndicatorType.EMA, IndicatorType.BOLLINGER_BANDS} + subplot_types = {IndicatorType.RSI, IndicatorType.MACD} + + if indicator_type in [t.value for t in overlay_types]: + return DisplayType.OVERLAY.value + elif indicator_type in [t.value for t in subplot_types]: + return DisplayType.SUBPLOT.value + else: + return DisplayType.OVERLAY.value # Default + + def _create_default_templates(self): + """Create default indicator templates if they don't exist.""" + templates = { + IndicatorType.SMA.value: { + "name": "Simple Moving Average", + "description": "Simple Moving Average indicator", + "type": IndicatorType.SMA.value, + "display_type": DisplayType.OVERLAY.value, + "default_parameters": {"period": 20}, + "parameter_schema": { + "period": {"type": "int", "min": 1, "max": 200, "default": 20, "description": "Period for SMA calculation"} + }, + "default_styling": {"color": "#007bff", "line_width": 2} + }, + IndicatorType.EMA.value: { + "name": "Exponential Moving Average", + "description": "Exponential Moving Average indicator", + "type": IndicatorType.EMA.value, + "display_type": DisplayType.OVERLAY.value, + "default_parameters": {"period": 12}, + "parameter_schema": { + "period": {"type": "int", "min": 1, "max": 200, "default": 12, "description": "Period for EMA calculation"} + }, + "default_styling": {"color": "#ff6b35", "line_width": 2} + }, + IndicatorType.RSI.value: { + "name": "Relative Strength Index", + "description": "RSI oscillator indicator", + "type": IndicatorType.RSI.value, + "display_type": DisplayType.SUBPLOT.value, + "default_parameters": {"period": 14}, + "parameter_schema": { + "period": {"type": "int", "min": 2, "max": 50, "default": 14, "description": "Period for RSI calculation"} + }, + "default_styling": {"color": "#20c997", "line_width": 2} + }, + IndicatorType.MACD.value: { + "name": "MACD", + "description": "Moving Average Convergence Divergence", + "type": IndicatorType.MACD.value, + "display_type": DisplayType.SUBPLOT.value, + "default_parameters": {"fast_period": 12, "slow_period": 26, "signal_period": 9}, + "parameter_schema": { + "fast_period": {"type": "int", "min": 2, "max": 50, "default": 12, "description": "Fast EMA period"}, + "slow_period": {"type": "int", "min": 5, "max": 100, "default": 26, "description": "Slow EMA period"}, + "signal_period": {"type": "int", "min": 2, "max": 30, "default": 9, "description": "Signal line period"} + }, + "default_styling": {"color": "#fd7e14", "line_width": 2} + }, + IndicatorType.BOLLINGER_BANDS.value: { + "name": "Bollinger Bands", + "description": "Bollinger Bands volatility indicator", + "type": IndicatorType.BOLLINGER_BANDS.value, + "display_type": DisplayType.OVERLAY.value, + "default_parameters": {"period": 20, "std_dev": 2.0}, + "parameter_schema": { + "period": {"type": "int", "min": 5, "max": 100, "default": 20, "description": "Period for middle line (SMA)"}, + "std_dev": {"type": "float", "min": 0.5, "max": 5.0, "default": 2.0, "description": "Standard deviation multiplier"} + }, + "default_styling": {"color": "#6f42c1", "line_width": 1} + } + } + + for indicator_type, template_data in templates.items(): + template_path = self._get_template_file_path(indicator_type) + + if not template_path.exists(): + try: + with open(template_path, 'w', encoding='utf-8') as f: + json.dump(template_data, f, indent=2, ensure_ascii=False) + self.logger.debug(f"Created template: {indicator_type}") + except Exception as e: + self.logger.error(f"Error creating template {indicator_type}: {e}") + + def get_template(self, indicator_type: str) -> Optional[Dict[str, Any]]: + """Get indicator template by type.""" + try: + template_path = self._get_template_file_path(indicator_type) + + if template_path.exists(): + with open(template_path, 'r', encoding='utf-8') as f: + return json.load(f) + else: + self.logger.warning(f"Template not found: {indicator_type}") + return None + + except Exception as e: + self.logger.error(f"Error loading template {indicator_type}: {e}") + return None + + +# Global instance +indicator_manager = IndicatorManager() + + +def get_indicator_manager() -> IndicatorManager: + """Get the global indicator manager instance.""" + return indicator_manager \ No newline at end of file diff --git a/components/charts/layers/indicators.py b/components/charts/layers/indicators.py index 913c5d3..c555869 100644 --- a/components/charts/layers/indicators.py +++ b/components/charts/layers/indicators.py @@ -32,6 +32,7 @@ class IndicatorLayerConfig(LayerConfig): parameters: Dict[str, Any] = None # Indicator-specific parameters line_width: int = 2 opacity: float = 1.0 + show_middle_line: bool = True # For indicators like Bollinger Bands def __post_init__(self): super().__post_init__() @@ -341,9 +342,7 @@ class SMALayer(BaseIndicatorLayer): line=dict( color=self.config.color or '#2196F3', width=self.config.line_width - ), - row=subplot_row, - col=1 + ) ) self.traces = [sma_trace] @@ -442,9 +441,7 @@ class EMALayer(BaseIndicatorLayer): line=dict( color=self.config.color or '#FF9800', width=self.config.line_width - ), - row=subplot_row, - col=1 + ) ) self.traces = [ema_trace] @@ -550,8 +547,6 @@ class BollingerBandsLayer(BaseIndicatorLayer): mode='lines', name=f'BB Upper({period})', line=dict(color=self.config.color or '#9C27B0', width=1), - row=subplot_row, - col=1, showlegend=True ) traces.append(upper_trace) @@ -565,8 +560,6 @@ class BollingerBandsLayer(BaseIndicatorLayer): line=dict(color=self.config.color or '#9C27B0', width=1), fill='tonexty', fillcolor='rgba(156, 39, 176, 0.1)', - row=subplot_row, - col=1, showlegend=True ) traces.append(lower_trace) @@ -579,8 +572,6 @@ class BollingerBandsLayer(BaseIndicatorLayer): mode='lines', name=f'BB Middle({period})', line=dict(color=self.config.color or '#9C27B0', width=1, dash='dash'), - row=subplot_row, - col=1, showlegend=True ) traces.append(middle_trace) diff --git a/config/indicators/templates/bollinger_bands_template.json b/config/indicators/templates/bollinger_bands_template.json new file mode 100644 index 0000000..34ccacb --- /dev/null +++ b/config/indicators/templates/bollinger_bands_template.json @@ -0,0 +1,30 @@ +{ + "name": "Bollinger Bands", + "description": "Bollinger Bands volatility indicator", + "type": "bollinger_bands", + "display_type": "overlay", + "default_parameters": { + "period": 20, + "std_dev": 2.0 + }, + "parameter_schema": { + "period": { + "type": "int", + "min": 5, + "max": 100, + "default": 20, + "description": "Period for middle line (SMA)" + }, + "std_dev": { + "type": "float", + "min": 0.5, + "max": 5.0, + "default": 2.0, + "description": "Standard deviation multiplier" + } + }, + "default_styling": { + "color": "#6f42c1", + "line_width": 1 + } +} \ No newline at end of file diff --git a/config/indicators/templates/ema_template.json b/config/indicators/templates/ema_template.json new file mode 100644 index 0000000..b26a5d6 --- /dev/null +++ b/config/indicators/templates/ema_template.json @@ -0,0 +1,22 @@ +{ + "name": "Exponential Moving Average", + "description": "Exponential Moving Average indicator", + "type": "ema", + "display_type": "overlay", + "default_parameters": { + "period": 12 + }, + "parameter_schema": { + "period": { + "type": "int", + "min": 1, + "max": 200, + "default": 12, + "description": "Period for EMA calculation" + } + }, + "default_styling": { + "color": "#ff6b35", + "line_width": 2 + } +} \ No newline at end of file diff --git a/config/indicators/templates/macd_template.json b/config/indicators/templates/macd_template.json new file mode 100644 index 0000000..828c6f8 --- /dev/null +++ b/config/indicators/templates/macd_template.json @@ -0,0 +1,38 @@ +{ + "name": "MACD", + "description": "Moving Average Convergence Divergence", + "type": "macd", + "display_type": "subplot", + "default_parameters": { + "fast_period": 12, + "slow_period": 26, + "signal_period": 9 + }, + "parameter_schema": { + "fast_period": { + "type": "int", + "min": 2, + "max": 50, + "default": 12, + "description": "Fast EMA period" + }, + "slow_period": { + "type": "int", + "min": 5, + "max": 100, + "default": 26, + "description": "Slow EMA period" + }, + "signal_period": { + "type": "int", + "min": 2, + "max": 30, + "default": 9, + "description": "Signal line period" + } + }, + "default_styling": { + "color": "#fd7e14", + "line_width": 2 + } +} \ No newline at end of file diff --git a/config/indicators/templates/rsi_template.json b/config/indicators/templates/rsi_template.json new file mode 100644 index 0000000..d1619dc --- /dev/null +++ b/config/indicators/templates/rsi_template.json @@ -0,0 +1,22 @@ +{ + "name": "Relative Strength Index", + "description": "RSI oscillator indicator", + "type": "rsi", + "display_type": "subplot", + "default_parameters": { + "period": 14 + }, + "parameter_schema": { + "period": { + "type": "int", + "min": 2, + "max": 50, + "default": 14, + "description": "Period for RSI calculation" + } + }, + "default_styling": { + "color": "#20c997", + "line_width": 2 + } +} \ No newline at end of file diff --git a/config/indicators/templates/sma_template.json b/config/indicators/templates/sma_template.json new file mode 100644 index 0000000..e6a9935 --- /dev/null +++ b/config/indicators/templates/sma_template.json @@ -0,0 +1,22 @@ +{ + "name": "Simple Moving Average", + "description": "Simple Moving Average indicator", + "type": "sma", + "display_type": "overlay", + "default_parameters": { + "period": 20 + }, + "parameter_schema": { + "period": { + "type": "int", + "min": 1, + "max": 200, + "default": 20, + "description": "Period for SMA calculation" + } + }, + "default_styling": { + "color": "#007bff", + "line_width": 2 + } +} \ No newline at end of file diff --git a/config/indicators/user_indicators/bollinger_bands_08c5ed71.json b/config/indicators/user_indicators/bollinger_bands_08c5ed71.json new file mode 100644 index 0000000..8b11e94 --- /dev/null +++ b/config/indicators/user_indicators/bollinger_bands_08c5ed71.json @@ -0,0 +1,20 @@ +{ + "id": "bollinger_bands_08c5ed71", + "name": "Bollinger Tight", + "description": "Tight Bollinger Bands (20, 1.5) for sensitive volatility", + "type": "bollinger_bands", + "display_type": "overlay", + "parameters": { + "period": 20, + "std_dev": 1.5 + }, + "styling": { + "color": "#e83e8c", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.460797+00:00", + "modified_date": "2025-06-04T04:16:35.460797+00:00" +} \ No newline at end of file diff --git a/config/indicators/user_indicators/bollinger_bands_69b378e2.json b/config/indicators/user_indicators/bollinger_bands_69b378e2.json new file mode 100644 index 0000000..74f6163 --- /dev/null +++ b/config/indicators/user_indicators/bollinger_bands_69b378e2.json @@ -0,0 +1,20 @@ +{ + "id": "bollinger_bands_69b378e2", + "name": "Bollinger Bands", + "description": "Standard Bollinger Bands (20, 2) for volatility analysis", + "type": "bollinger_bands", + "display_type": "overlay", + "parameters": { + "period": 20, + "std_dev": 2.0 + }, + "styling": { + "color": "#6f42c1", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.460105+00:00", + "modified_date": "2025-06-04T04:16:35.460105+00:00" +} \ No newline at end of file diff --git a/config/indicators/user_indicators/ema_ca5fd53d.json b/config/indicators/user_indicators/ema_ca5fd53d.json new file mode 100644 index 0000000..e5b5981 --- /dev/null +++ b/config/indicators/user_indicators/ema_ca5fd53d.json @@ -0,0 +1,19 @@ +{ + "id": "ema_ca5fd53d", + "name": "EMA 10", + "description": "12-period Exponential Moving Average for fast signals", + "type": "ema", + "display_type": "overlay", + "parameters": { + "period": 10 + }, + "styling": { + "color": "#ff6b35", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.455729+00:00", + "modified_date": "2025-06-04T04:54:49.608549+00:00" +} \ No newline at end of file diff --git a/config/indicators/user_indicators/ema_de4fc14c.json b/config/indicators/user_indicators/ema_de4fc14c.json new file mode 100644 index 0000000..c08e9b3 --- /dev/null +++ b/config/indicators/user_indicators/ema_de4fc14c.json @@ -0,0 +1,19 @@ +{ + "id": "ema_de4fc14c", + "name": "EMA 26", + "description": "26-period Exponential Moving Average for slower signals", + "type": "ema", + "display_type": "overlay", + "parameters": { + "period": 26 + }, + "styling": { + "color": "#28a745", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.456253+00:00", + "modified_date": "2025-06-04T04:16:35.456253+00:00" +} \ No newline at end of file diff --git a/config/indicators/user_indicators/macd_307935a7.json b/config/indicators/user_indicators/macd_307935a7.json new file mode 100644 index 0000000..bb4e439 --- /dev/null +++ b/config/indicators/user_indicators/macd_307935a7.json @@ -0,0 +1,21 @@ +{ + "id": "macd_307935a7", + "name": "MACD Fast", + "description": "Fast MACD (5, 13, 4) for quick signals", + "type": "macd", + "display_type": "subplot", + "parameters": { + "fast_period": 5, + "slow_period": 13, + "signal_period": 4 + }, + "styling": { + "color": "#dc3545", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.459602+00:00", + "modified_date": "2025-06-04T04:16:35.459602+00:00" +} \ No newline at end of file diff --git a/config/indicators/user_indicators/macd_7335a9bd.json b/config/indicators/user_indicators/macd_7335a9bd.json new file mode 100644 index 0000000..a987506 --- /dev/null +++ b/config/indicators/user_indicators/macd_7335a9bd.json @@ -0,0 +1,21 @@ +{ + "id": "macd_7335a9bd", + "name": "MACD Standard", + "description": "Standard MACD (12, 26, 9) for trend changes", + "type": "macd", + "display_type": "subplot", + "parameters": { + "fast_period": 12, + "slow_period": 26, + "signal_period": 9 + }, + "styling": { + "color": "#fd7e14", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.459030+00:00", + "modified_date": "2025-06-04T04:16:35.459030+00:00" +} \ No newline at end of file diff --git a/config/indicators/user_indicators/rsi_1a0e1320.json b/config/indicators/user_indicators/rsi_1a0e1320.json new file mode 100644 index 0000000..4c002d7 --- /dev/null +++ b/config/indicators/user_indicators/rsi_1a0e1320.json @@ -0,0 +1,19 @@ +{ + "id": "rsi_1a0e1320", + "name": "RSI 21", + "description": "21-period RSI for less sensitive momentum signals", + "type": "rsi", + "display_type": "subplot", + "parameters": { + "period": 21 + }, + "styling": { + "color": "#17a2b8", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.458018+00:00", + "modified_date": "2025-06-04T04:16:35.458018+00:00" +} \ No newline at end of file diff --git a/config/indicators/user_indicators/rsi_5d160ff7.json b/config/indicators/user_indicators/rsi_5d160ff7.json new file mode 100644 index 0000000..4ab8b63 --- /dev/null +++ b/config/indicators/user_indicators/rsi_5d160ff7.json @@ -0,0 +1,19 @@ +{ + "id": "rsi_5d160ff7", + "name": "RSI 14", + "description": "14-period RSI for momentum analysis", + "type": "rsi", + "display_type": "subplot", + "parameters": { + "period": 14 + }, + "styling": { + "color": "#20c997", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.457515+00:00", + "modified_date": "2025-06-04T04:16:35.457515+00:00" +} \ No newline at end of file diff --git a/config/indicators/user_indicators/sma_0e235df1.json b/config/indicators/user_indicators/sma_0e235df1.json new file mode 100644 index 0000000..21213b0 --- /dev/null +++ b/config/indicators/user_indicators/sma_0e235df1.json @@ -0,0 +1,19 @@ +{ + "id": "sma_0e235df1", + "name": "SMA 50", + "description": "50-period Simple Moving Average for medium-term trend", + "type": "sma", + "display_type": "overlay", + "parameters": { + "period": 50 + }, + "styling": { + "color": "#6c757d", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.454653+00:00", + "modified_date": "2025-06-04T04:16:35.454653+00:00" +} \ No newline at end of file diff --git a/config/indicators/user_indicators/sma_8c487df2.json b/config/indicators/user_indicators/sma_8c487df2.json new file mode 100644 index 0000000..8533c3c --- /dev/null +++ b/config/indicators/user_indicators/sma_8c487df2.json @@ -0,0 +1,19 @@ +{ + "id": "sma_8c487df2", + "name": "SMA 20", + "description": "20-period Simple Moving Average for short-term trend", + "type": "sma", + "display_type": "overlay", + "parameters": { + "period": 20 + }, + "styling": { + "color": "#007bff", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.453614+00:00", + "modified_date": "2025-06-04T04:16:35.453614+00:00" +} \ No newline at end of file diff --git a/docs/components/charts/README.md b/docs/components/charts/README.md index 612bf64..2bde219 100644 --- a/docs/components/charts/README.md +++ b/docs/components/charts/README.md @@ -8,6 +8,7 @@ The Modular Chart Layers System is a flexible, strategy-driven chart system that - [Architecture](#architecture) - [Quick Start](#quick-start) - [Components](#components) +- [User Indicator Management](#user-indicator-management) - [Configuration System](#configuration-system) - [Example Strategies](#example-strategies) - [Validation System](#validation-system) @@ -20,6 +21,7 @@ The Modular Chart Layers System is a flexible, strategy-driven chart system that ### Key Features - **Modular Architecture**: Chart layers can be independently tested and composed +- **User Indicator Management**: Create, edit, and manage custom indicators with JSON persistence - **Strategy-Driven Configuration**: JSON-based configurations for different trading strategies - **Comprehensive Validation**: 10+ validation rules with detailed error reporting - **Example Strategies**: 5 real-world trading strategy templates @@ -44,7 +46,9 @@ The Modular Chart Layers System is a flexible, strategy-driven chart system that ``` components/charts/ -├── config/ # Configuration management +├── indicator_manager.py # User indicator CRUD operations +├── indicator_defaults.py # Default indicator templates +├── config/ # Configuration management │ ├── indicator_defs.py # Indicator schemas and validation │ ├── defaults.py # Default configurations and presets │ ├── strategy_charts.py # Strategy-specific configurations @@ -58,6 +62,65 @@ components/charts/ │ └── signals.py # Signal overlays (future) ├── builder.py # Main chart builder └── utils.py # Chart utilities + +config/indicators/ +└── user_indicators/ # User-created indicators (JSON files) + ├── sma_abc123.json + ├── ema_def456.json + └── ... +``` + +## User Indicator Management + +The system includes a comprehensive user indicator management system that allows creating, editing, and managing custom technical indicators. + +### Features + +- **Interactive UI**: Modal dialog for creating and editing indicators +- **Real-time Updates**: Charts update immediately when indicators are toggled +- **JSON Persistence**: Each indicator saved as individual JSON file +- **Full CRUD Operations**: Create, Read, Update, Delete functionality +- **Type Validation**: Parameter validation based on indicator type +- **Custom Styling**: Color, line width, and appearance customization + +### Quick Access + +- **📊 [Complete Indicator Documentation](./indicators.md)** - Comprehensive guide to the indicator system +- **⚡ [Quick Guide: Adding New Indicators](./adding-new-indicators.md)** - Step-by-step checklist for developers + +### Current User Indicators + +| Indicator | Type | Parameters | Display | +|-----------|------|------------|---------| +| Simple Moving Average (SMA) | `sma` | period (1-200) | Overlay | +| Exponential Moving Average (EMA) | `ema` | period (1-200) | Overlay | +| Bollinger Bands | `bollinger_bands` | period (5-100), std_dev (0.5-5.0) | Overlay | +| Relative Strength Index (RSI) | `rsi` | period (2-50) | Subplot | +| MACD | `macd` | fast_period, slow_period, signal_period | Subplot | + +### Usage Example + +```python +# Get indicator manager +from components.charts.indicator_manager import get_indicator_manager +manager = get_indicator_manager() + +# Create new indicator +indicator = manager.create_indicator( + name="My SMA 50", + indicator_type="sma", + parameters={"period": 50}, + description="50-period Simple Moving Average", + color="#ff0000" +) + +# Load and update +loaded = manager.load_indicator("sma_abc123") +success = manager.update_indicator("sma_abc123", name="Updated SMA") + +# Get indicators by type +overlay_indicators = manager.get_indicators_by_type("overlay") +subplot_indicators = manager.get_indicators_by_type("subplot") ``` ## Quick Start diff --git a/docs/components/charts/adding-new-indicators.md b/docs/components/charts/adding-new-indicators.md new file mode 100644 index 0000000..65cc511 --- /dev/null +++ b/docs/components/charts/adding-new-indicators.md @@ -0,0 +1,393 @@ +# Quick Guide: Adding New Indicators + +## Overview + +This guide provides a step-by-step checklist for adding new technical indicators to the Crypto Trading Bot Dashboard. + +## Prerequisites + +- Understanding of Python and technical analysis +- Familiarity with the project structure +- Knowledge of the indicator type (overlay vs subplot) + +## Step-by-Step Checklist + +### ✅ Step 1: Plan Your Indicator + +- [ ] Determine indicator type (overlay or subplot) +- [ ] Define required parameters +- [ ] Choose default styling +- [ ] Research calculation formula + +### ✅ Step 2: Create Indicator Class + +**File**: `components/charts/layers/indicators.py` (overlay) or `components/charts/layers/subplots.py` (subplot) + +```python +class StochasticLayer(IndicatorLayer): + """Stochastic Oscillator indicator implementation.""" + + def __init__(self, config: Dict[str, Any]): + super().__init__(config) + self.name = "stochastic" + self.display_type = "subplot" # or "overlay" + + def calculate_values(self, df: pd.DataFrame) -> Dict[str, pd.Series]: + """Calculate stochastic oscillator values.""" + k_period = self.config.get('k_period', 14) + d_period = self.config.get('d_period', 3) + + # Calculate %K and %D lines + lowest_low = df['low'].rolling(window=k_period).min() + highest_high = df['high'].rolling(window=k_period).max() + + k_percent = 100 * ((df['close'] - lowest_low) / (highest_high - lowest_low)) + d_percent = k_percent.rolling(window=d_period).mean() + + return { + 'k_percent': k_percent, + 'd_percent': d_percent + } + + def create_traces(self, df: pd.DataFrame, values: Dict[str, pd.Series]) -> List[go.Scatter]: + """Create plotly traces for stochastic oscillator.""" + traces = [] + + # %K line + traces.append(go.Scatter( + x=df.index, + y=values['k_percent'], + mode='lines', + name=f"%K ({self.config.get('k_period', 14)})", + line=dict( + color=self.config.get('color', '#007bff'), + width=self.config.get('line_width', 2) + ) + )) + + # %D line + traces.append(go.Scatter( + x=df.index, + y=values['d_percent'], + mode='lines', + name=f"%D ({self.config.get('d_period', 3)})", + line=dict( + color=self.config.get('secondary_color', '#ff6b35'), + width=self.config.get('line_width', 2) + ) + )) + + return traces +``` + +### ✅ Step 3: Register Indicator + +**File**: `components/charts/layers/__init__.py` + +```python +# Import the new class +from .subplots import StochasticLayer + +# Add to appropriate registry +SUBPLOT_REGISTRY = { + 'rsi': RSILayer, + 'macd': MACDLayer, + 'stochastic': StochasticLayer, # Add this line +} + +# For overlay indicators, add to INDICATOR_REGISTRY instead +INDICATOR_REGISTRY = { + 'sma': SMALayer, + 'ema': EMALayer, + 'bollinger_bands': BollingerBandsLayer, + 'stochastic': StochasticLayer, # Only if overlay +} +``` + +### ✅ Step 4: Add UI Dropdown Option + +**File**: `app.py` (in the indicator type dropdown) + +```python +dcc.Dropdown( + id='indicator-type-dropdown', + options=[ + {'label': 'Simple Moving Average (SMA)', 'value': 'sma'}, + {'label': 'Exponential Moving Average (EMA)', 'value': 'ema'}, + {'label': 'Relative Strength Index (RSI)', 'value': 'rsi'}, + {'label': 'MACD', 'value': 'macd'}, + {'label': 'Bollinger Bands', 'value': 'bollinger_bands'}, + {'label': 'Stochastic Oscillator', 'value': 'stochastic'}, # Add this + ] +) +``` + +### ✅ Step 5: Add Parameter Fields to Modal + +**File**: `app.py` (in the modal parameters section) + +```python +# Add parameter section for stochastic +html.Div([ + html.Div([ + html.Label("%K Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='stochastic-k-period-input', + type='number', + value=14, + min=5, max=50, + style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ) + ], style={'margin-bottom': '10px'}), + html.Div([ + html.Label("%D Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), + dcc.Input( + id='stochastic-d-period-input', + type='number', + value=3, + min=2, max=10, + style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} + ) + ]), + html.P("Stochastic oscillator periods for %K and %D lines", + style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) +], id='stochastic-parameters', style={'display': 'none', 'margin-bottom': '10px'}) +``` + +### ✅ Step 6: Update Parameter Visibility Callback + +**File**: `app.py` (in `update_parameter_fields` callback) + +```python +@app.callback( + [Output('indicator-parameters-message', 'style'), + Output('sma-parameters', 'style'), + Output('ema-parameters', 'style'), + Output('rsi-parameters', 'style'), + Output('macd-parameters', 'style'), + Output('bb-parameters', 'style'), + Output('stochastic-parameters', 'style')], # Add this output + Input('indicator-type-dropdown', 'value'), + prevent_initial_call=True +) +def update_parameter_fields(indicator_type): + # ... existing code ... + + # Add stochastic style + stochastic_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 + elif indicator_type == 'stochastic': # Add this + stochastic_style = visible_style + + return message_style, sma_style, ema_style, rsi_style, macd_style, bb_style, stochastic_style +``` + +### ✅ Step 7: Update Save Indicator Callback + +**File**: `app.py` (in `save_new_indicator` callback) + +```python +# Add stochastic parameters to State inputs +State('stochastic-k-period-input', 'value'), +State('stochastic-d-period-input', 'value'), + +# Add to parameter collection logic +def save_new_indicator(n_clicks, name, indicator_type, description, color, line_width, + sma_period, ema_period, rsi_period, + macd_fast, macd_slow, macd_signal, + bb_period, bb_stddev, + stochastic_k, stochastic_d, # Add these + edit_data): + + # ... existing code ... + + elif indicator_type == 'stochastic': + parameters = { + 'k_period': stochastic_k or 14, + 'd_period': stochastic_d or 3 + } +``` + +### ✅ Step 8: Update Edit Callback Parameters + +**File**: `app.py` (in `edit_indicator` callback) + +```python +# Add output for stochastic parameters +Output('stochastic-k-period-input', 'value'), +Output('stochastic-d-period-input', 'value'), + +# Add parameter loading logic +elif indicator.type == 'stochastic': + stochastic_k = params.get('k_period', 14) + stochastic_d = params.get('d_period', 3) + +# Add to return statement +return ( + "✏️ Edit Indicator", + indicator.name, + indicator.type, + indicator.description, + indicator.styling.color, + edit_data, + sma_period, + ema_period, + rsi_period, + macd_fast, + macd_slow, + macd_signal, + bb_period, + bb_stddev, + stochastic_k, # Add these + stochastic_d +) +``` + +### ✅ Step 9: Update Reset Callback + +**File**: `app.py` (in `reset_modal_form` callback) + +```python +# Add outputs +Output('stochastic-k-period-input', 'value', allow_duplicate=True), +Output('stochastic-d-period-input', 'value', allow_duplicate=True), + +# Add default values to return +return "", None, "", "#007bff", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0, 14, 3 +``` + +### ✅ Step 10: Create Default Template + +**File**: `components/charts/indicator_defaults.py` + +```python +def create_stochastic_template() -> UserIndicator: + """Create default Stochastic Oscillator template.""" + return UserIndicator( + id=f"stochastic_{generate_short_id()}", + name="Stochastic 14,3", + description="14-period %K with 3-period %D smoothing", + type="stochastic", + display_type="subplot", + parameters={ + "k_period": 14, + "d_period": 3 + }, + styling=IndicatorStyling( + color="#9c27b0", + line_width=2 + ) + ) + +# Add to DEFAULT_TEMPLATES +DEFAULT_TEMPLATES = { + "sma": create_sma_template, + "ema": create_ema_template, + "rsi": create_rsi_template, + "macd": create_macd_template, + "bollinger_bands": create_bollinger_bands_template, + "stochastic": create_stochastic_template, # Add this +} +``` + +### ✅ Step 11: Add Calculation Function (Optional) + +**File**: `data/common/indicators.py` + +```python +def calculate_stochastic(df: pd.DataFrame, k_period: int = 14, d_period: int = 3) -> tuple: + """Calculate Stochastic Oscillator (%K and %D).""" + lowest_low = df['low'].rolling(window=k_period).min() + highest_high = df['high'].rolling(window=k_period).max() + + k_percent = 100 * ((df['close'] - lowest_low) / (highest_high - lowest_low)) + d_percent = k_percent.rolling(window=d_period).mean() + + return k_percent, d_percent +``` + +## Testing Checklist + +- [ ] Indicator appears in dropdown +- [ ] Parameter fields show/hide correctly +- [ ] Default values are set properly +- [ ] Indicator saves and loads correctly +- [ ] Edit functionality works +- [ ] Chart updates with indicator +- [ ] Delete functionality works +- [ ] Error handling works with insufficient data + +## Common Patterns + +### Single Line Overlay +```python +# Simple indicators like SMA, EMA +def create_traces(self, df: pd.DataFrame, values: Dict[str, pd.Series]) -> List[go.Scatter]: + return [go.Scatter( + x=df.index, + y=values['indicator_name'], + mode='lines', + name=self.config.get('name', 'Indicator'), + line=dict(color=self.config.get('color', '#007bff')) + )] +``` + +### Multi-Line Subplot +```python +# Complex indicators like MACD, Stochastic +def create_traces(self, df: pd.DataFrame, values: Dict[str, pd.Series]) -> List[go.Scatter]: + traces = [] + for key, series in values.items(): + traces.append(go.Scatter( + x=df.index, + y=series, + mode='lines', + name=f"{key.title()}" + )) + return traces +``` + +### Band Indicators +```python +# Indicators with bands like Bollinger Bands +def create_traces(self, df: pd.DataFrame, values: Dict[str, pd.Series]) -> List[go.Scatter]: + return [ + # Upper band + go.Scatter(x=df.index, y=values['upper'], name='Upper'), + # Middle line + go.Scatter(x=df.index, y=values['middle'], name='Middle'), + # Lower band with fill + go.Scatter(x=df.index, y=values['lower'], name='Lower', + fill='tonexty', fillcolor='rgba(0,123,255,0.1)') + ] +``` + +## File Change Summary + +When adding a new indicator, you'll typically modify these files: + +1. **`components/charts/layers/indicators.py`** or **`subplots.py`** - Indicator class +2. **`components/charts/layers/__init__.py`** - Registry registration +3. **`app.py`** - UI dropdown, parameter fields, callbacks +4. **`components/charts/indicator_defaults.py`** - Default template +5. **`data/common/indicators.py`** - Calculation function (optional) + +## Tips + +- Start with a simple single-line indicator first +- Test each step before moving to the next +- Use existing indicators as templates +- Check console/logs for errors +- Test with different parameter values +- Verify calculations with known data \ No newline at end of file diff --git a/docs/components/charts/indicators.md b/docs/components/charts/indicators.md new file mode 100644 index 0000000..a3a54d9 --- /dev/null +++ b/docs/components/charts/indicators.md @@ -0,0 +1,310 @@ +# Indicator System Documentation + +## Overview + +The Crypto Trading Bot Dashboard features a comprehensive modular indicator system that allows users to create, customize, and manage technical indicators for chart analysis. The system supports both overlay indicators (displayed on the main price chart) and subplot indicators (displayed in separate panels below the main chart). + +## Table of Contents + +1. [System Architecture](#system-architecture) +2. [Current Indicators](#current-indicators) +3. [User Interface](#user-interface) +4. [File Structure](#file-structure) +5. [Adding New Indicators](#adding-new-indicators) +6. [Configuration Format](#configuration-format) +7. [API Reference](#api-reference) +8. [Troubleshooting](#troubleshooting) + +## System Architecture + +### Core Components + +``` +components/charts/ +├── indicator_manager.py # Core indicator CRUD operations +├── indicator_defaults.py # Default indicator templates +├── layers/ +│ ├── indicators.py # Overlay indicator rendering +│ └── subplots.py # Subplot indicator rendering +└── config/ + └── indicator_defs.py # Indicator definitions and schemas + +config/indicators/ +└── user_indicators/ # User-created indicators (JSON files) + ├── sma_abc123.json + ├── ema_def456.json + └── ... +``` + +### Key Classes + +- **`IndicatorManager`**: Handles CRUD operations for user indicators +- **`UserIndicator`**: Data structure for indicator configuration +- **`IndicatorStyling`**: Appearance and styling configuration +- **Indicator Layers**: Rendering classes for different indicator types + +## Current Indicators + +### Overlay Indicators +These indicators are displayed directly on the price chart: + +| Indicator | Type | Parameters | Description | +|-----------|------|------------|-------------| +| **Simple Moving Average (SMA)** | `sma` | `period` (1-200) | Average price over N periods | +| **Exponential Moving Average (EMA)** | `ema` | `period` (1-200) | Weighted average giving more weight to recent prices | +| **Bollinger Bands** | `bollinger_bands` | `period` (5-100), `std_dev` (0.5-5.0) | Price channels based on standard deviation | + +### Subplot Indicators +These indicators are displayed in separate panels: + +| Indicator | Type | Parameters | Description | +|-----------|------|------------|-------------| +| **Relative Strength Index (RSI)** | `rsi` | `period` (2-50) | Momentum oscillator (0-100 scale) | +| **MACD** | `macd` | `fast_period` (2-50), `slow_period` (5-100), `signal_period` (2-30) | Moving average convergence divergence | + +## User Interface + +### Adding Indicators + +1. **Click "➕ Add New Indicator"** button +2. **Configure Basic Settings**: + - Name: Custom name for the indicator + - Type: Select from available indicator types + - Description: Optional description +3. **Set Parameters**: Type-specific parameters appear dynamically +4. **Customize Styling**: + - Color: Hex color code + - Line Width: 1-5 pixels +5. **Save**: Creates a new JSON file and updates the UI + +### Managing Indicators + +- **✅ Checkboxes**: Toggle indicator visibility on chart +- **✏️ Edit Button**: Modify existing indicator settings +- **🗑️ Delete Button**: Remove indicator permanently + +### Real-time Updates + +- Chart updates automatically when indicators are toggled +- Changes are saved immediately to JSON files +- No page refresh required + +## File Structure + +### Indicator JSON Format + +```json +{ + "id": "ema_ca5fd53d", + "name": "EMA 10", + "description": "10-period Exponential Moving Average for fast signals", + "type": "ema", + "display_type": "overlay", + "parameters": { + "period": 10 + }, + "styling": { + "color": "#ff6b35", + "line_width": 2, + "opacity": 1.0, + "line_style": "solid" + }, + "visible": true, + "created_date": "2025-06-04T04:16:35.455729+00:00", + "modified_date": "2025-06-04T04:54:49.608549+00:00" +} +``` + +### Directory Structure + +``` +config/indicators/ +└── user_indicators/ + ├── sma_abc123.json # Individual indicator files + ├── ema_def456.json + ├── rsi_ghi789.json + └── macd_jkl012.json +``` + +## Adding New Indicators + +For developers who want to add new indicator types to the system, please refer to the comprehensive step-by-step guide: + +**📋 [Quick Guide: Adding New Indicators](./adding-new-indicators.md)** + +This guide covers: +- ✅ Complete 11-step implementation checklist +- ✅ Full code examples (Stochastic Oscillator implementation) +- ✅ File modification requirements +- ✅ Testing checklist and common patterns +- ✅ Tips and best practices + +## Configuration Format + +### User Indicator Structure + +```python +@dataclass +class UserIndicator: + id: str # Unique identifier + name: str # Display name + description: str # User description + type: str # Indicator type (sma, ema, etc.) + display_type: str # "overlay" or "subplot" + parameters: Dict[str, Any] # Type-specific parameters + styling: IndicatorStyling # Appearance settings + visible: bool = True # Default visibility + created_date: datetime # Creation timestamp + modified_date: datetime # Last modification timestamp +``` + +### Styling Options + +```python +@dataclass +class IndicatorStyling: + color: str = "#007bff" # Hex color code + line_width: int = 2 # Line thickness (1-5) + opacity: float = 1.0 # Transparency (0.0-1.0) + line_style: str = "solid" # Line style +``` + +### Parameter Examples + +```python +# SMA/EMA Parameters +{"period": 20} + +# RSI Parameters +{"period": 14} + +# MACD Parameters +{ + "fast_period": 12, + "slow_period": 26, + "signal_period": 9 +} + +# Bollinger Bands Parameters +{ + "period": 20, + "std_dev": 2.0 +} +``` + +## API Reference + +### IndicatorManager Class + +```python +class IndicatorManager: + def create_indicator(self, name: str, indicator_type: str, + parameters: Dict[str, Any], **kwargs) -> Optional[UserIndicator] + + def load_indicator(self, indicator_id: str) -> Optional[UserIndicator] + + def update_indicator(self, indicator_id: str, **kwargs) -> bool + + def delete_indicator(self, indicator_id: str) -> bool + + def list_indicators(self) -> List[UserIndicator] + + def get_indicators_by_type(self, display_type: str) -> List[UserIndicator] +``` + +### Usage Examples + +```python +# Get indicator manager +manager = get_indicator_manager() + +# Create new indicator +indicator = manager.create_indicator( + name="My SMA 50", + indicator_type="sma", + parameters={"period": 50}, + description="50-period Simple Moving Average", + color="#ff0000" +) + +# Load indicator +loaded = manager.load_indicator("sma_abc123") + +# Update indicator +success = manager.update_indicator( + "sma_abc123", + name="Updated SMA", + parameters={"period": 30} +) + +# Delete indicator +deleted = manager.delete_indicator("sma_abc123") + +# List all indicators +all_indicators = manager.list_indicators() + +# Get by type +overlay_indicators = manager.get_indicators_by_type("overlay") +subplot_indicators = manager.get_indicators_by_type("subplot") +``` + +## Troubleshooting + +### Common Issues + +1. **Indicator not appearing in dropdown** + - Check if registered in `INDICATOR_REGISTRY` + - Verify the indicator type matches the class name + +2. **Parameters not saving** + - Ensure parameter fields are added to save callback + - Check parameter collection logic in `save_new_indicator` + +3. **Chart not updating** + - Verify the indicator layer implements `calculate_values` and `create_traces` + - Check if indicator is registered in the correct registry + +4. **File permission errors** + - Ensure `config/indicators/user_indicators/` directory is writable + - Check file permissions on existing JSON files + +### Debug Information + +- Check browser console for JavaScript errors +- Look at application logs for Python exceptions +- Verify JSON file structure with a validator +- Test indicator calculations with sample data + +### Performance Considerations + +- Indicators with large periods may take longer to calculate +- Consider data availability when setting parameter limits +- Subplot indicators require additional chart space +- Real-time updates may impact performance with many indicators + +## Best Practices + +1. **Naming Conventions** + - Use descriptive names for indicators + - Include parameter values in names (e.g., "SMA 20") + - Use consistent naming patterns + +2. **Parameter Validation** + - Set appropriate min/max values for parameters + - Provide helpful descriptions for parameters + - Use sensible default values + +3. **Error Handling** + - Handle insufficient data gracefully + - Provide meaningful error messages + - Log errors for debugging + +4. **Performance** + - Cache calculated values when possible + - Optimize calculation algorithms + - Limit the number of active indicators + +5. **User Experience** + - Provide immediate visual feedback + - Use intuitive color schemes + - Group related indicators logically \ No newline at end of file diff --git a/tasks/3.4. Chart layers.md b/tasks/3.4. Chart layers.md index db1cedc..d413601 100644 --- a/tasks/3.4. Chart layers.md +++ b/tasks/3.4. Chart layers.md @@ -73,9 +73,9 @@ Implementation of a flexible, strategy-driven chart system that supports technic - [x] 3.7 Unit test configuration system and validation - [ ] 4.0 Dashboard Integration and UI Controls - - [ ] 4.1 Add indicator selection checkboxes to dashboard layout - - [ ] 4.2 Create real-time chart updates with indicator toggling - - [ ] 4.3 Implement parameter adjustment controls for indicators + - [x] 4.1 Add indicator selection checkboxes to dashboard layout + - [x] 4.2 Create real-time chart updates with indicator toggling + - [x] 4.3 Implement parameter adjustment controls for indicators - [ ] 4.4 Add strategy selection dropdown for predefined configurations - [ ] 4.5 Update chart callback functions to handle new layer system - [ ] 4.6 Ensure backward compatibility with existing dashboard features