From 3976fc83660f79f3363002562e24c6289098bd12 Mon Sep 17 00:00:00 2001 From: rob Date: Fri, 6 Mar 2026 00:05:31 -0400 Subject: [PATCH] Fix backtest data source and chart view detection bugs - Fix market/symbol key mismatch in PythonGenerator.py (6 locations) causing backtest to use wrong trading pair (BTC/USD vs BTC/USDT) - Fix backtesting.py to always use default_source for backtest data - Fix exchange/exchange_name key mismatch in app.py and BrighterTrades.py causing strategy dialog to show wrong current chart exchange - Add favicon links to standalone HTML templates - Add AI strategy dialog template - Update tests and documentation Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 9 + pytest.ini | 4 +- src/BrighterTrades.py | 4 +- src/Configuration.py | 4 +- src/Database.py | 5 +- src/PythonGenerator.py | 71 +++-- src/app.py | 94 +++++- src/backtesting.py | 15 + src/candles.py | 10 +- src/static/Strategies.js | 192 +++++++++++ src/static/communication.js | 6 +- src/static/general.js | 1 + src/templates/ai_strategy_dialog.html | 81 +++++ src/templates/index.html | 1 + src/templates/login_page.html | 1 + src/templates/new_strategy_popup.html | 10 + src/templates/sign_up.html | 1 + src/templates/welcome.html | 1 + tests/test_DataCache.py | 7 +- tests/test_Exchange.py | 37 +-- tests/test_Users.py | 243 +++++++------- tests/test_app.py | 59 ++-- tests/test_candles.py | 328 +++++++++---------- tests/test_database.py | 17 +- tests/test_exchangeinterface.py | 97 +++--- tests/test_live_exchange_integration.py | 6 + tests/test_shared_utilities.py | 29 +- tests/test_strategy_generation.py | 403 ++++++++++++++++++++++++ tests/test_trade.py | 2 +- tests/test_trade2.py | 25 +- 30 files changed, 1293 insertions(+), 470 deletions(-) create mode 100644 src/templates/ai_strategy_dialog.html create mode 100644 tests/test_strategy_generation.py diff --git a/CLAUDE.md b/CLAUDE.md index 4476df4..c494940 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,6 +108,15 @@ Flask web application with SocketIO for real-time communication, using eventlet | `shared_utilities.py` | Time/date conversion utilities | | `utils.py` | JSON serialization helpers | +### EDM Client Module (`src/edm_client/`) + +| Module | Purpose | +|--------|---------| +| `client.py` | REST client for Exchange Data Manager service | +| `websocket_client.py` | WebSocket client for real-time candle data | +| `models.py` | Data models (Candle, Subscription, EdmConfig) | +| `exceptions.py` | EDM-specific exceptions | + ### Broker Module (`src/brokers/`) | Module | Purpose | diff --git a/pytest.ini b/pytest.ini index de67734..fe048b6 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,8 +3,10 @@ testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* -addopts = -v --tb=short +# Default: exclude integration tests (run with: pytest -m integration) +addopts = -v --tb=short -m "not integration" markers = live_testnet: marks tests as requiring live testnet API keys (deselect with '-m "not live_testnet"') live_integration: marks tests as live integration tests (deselect with '-m "not live_integration"') + integration: marks tests as integration tests that make network calls or require external services (run with: pytest -m integration) diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index b4e5aea..0039500 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -310,7 +310,7 @@ class BrighterTrades: print(f"Error getting data from EDM for '{exchange_name}': {e}") if not chart_view: - chart_view = {'timeframe': '', 'exchange_name': '', 'market': ''} + chart_view = {'timeframe': '', 'exchange': '', 'market': ''} if not indicator_types: indicator_types = [] if not available_indicators: @@ -324,7 +324,7 @@ class BrighterTrades: 'i_types': indicator_types, 'indicators': available_indicators, 'timeframe': chart_view.get('timeframe'), - 'exchange_name': chart_view.get('exchange_name'), + 'exchange_name': chart_view.get('exchange'), 'trading_pair': chart_view.get('market'), 'user_name': user_name, 'public_exchanges': self.exchanges.get_public_exchanges(), diff --git a/src/Configuration.py b/src/Configuration.py index 6a816e0..b47db19 100644 --- a/src/Configuration.py +++ b/src/Configuration.py @@ -30,7 +30,7 @@ class Configuration: # Exchange Data Manager (EDM) defaults 'edm': { 'rest_url': 'http://localhost:8080', - 'ws_url': 'ws://localhost:8765', + 'ws_url': 'ws://localhost:8080/ws', 'timeout': 30, 'enabled': True, 'reconnect_interval': 5.0, @@ -123,7 +123,7 @@ class Configuration: edm_settings = self.get_setting('edm') or {} return EdmConfig( rest_url=edm_settings.get('rest_url', 'http://localhost:8080'), - ws_url=edm_settings.get('ws_url', 'ws://localhost:8765'), + ws_url=edm_settings.get('ws_url', 'ws://localhost:8080/ws'), timeout=edm_settings.get('timeout', 30.0), enabled=edm_settings.get('enabled', True), reconnect_interval=edm_settings.get('reconnect_interval', 5.0), diff --git a/src/Database.py b/src/Database.py index 911bf84..9008c27 100644 --- a/src/Database.py +++ b/src/Database.py @@ -98,7 +98,10 @@ class Database: """ with SQLite(self.db_file) as con: cur = con.cursor() - cur.execute(sql, params) + if params is None: + cur.execute(sql) + else: + cur.execute(sql, params) def get_all_rows(self, table_name: str) -> pd.DataFrame: """ diff --git a/src/PythonGenerator.py b/src/PythonGenerator.py index f464d82..372907e 100644 --- a/src/PythonGenerator.py +++ b/src/PythonGenerator.py @@ -139,7 +139,12 @@ class PythonGenerator: continue # Skip nodes without a type logger.debug(f"Handling node of type: {node_type}") - handler_method = getattr(self, f'handle_{node_type}', self.handle_default) + + # Route indicator_* types to the generic indicator handler + if node_type.startswith('indicator_'): + handler_method = self.handle_indicator + else: + handler_method = getattr(self, f'handle_{node_type}', self.handle_default) handler_code = handler_method(node, indent_level) if isinstance(handler_code, list): @@ -183,7 +188,11 @@ class PythonGenerator: return 'False' # Default to False if node type is missing # Retrieve the handler method based on node_type - handler_method = getattr(self, f'handle_{node_type}', self.handle_default) + # Route indicator_* types to the generic indicator handler + if node_type.startswith('indicator_'): + handler_method = self.handle_indicator + else: + handler_method = getattr(self, f'handle_{node_type}', self.handle_default) condition_code = handler_method(condition_node, indent_level=indent_level) return condition_code @@ -195,18 +204,28 @@ class PythonGenerator: def handle_indicator(self, node: Dict[str, Any], indent_level: int) -> str: """ - Handles the 'indicator_a_bolengerband' node type by generating a function call to retrieve indicator values. + Handles indicator nodes by generating a function call to retrieve indicator values. + Supports both: + - Generic 'indicator' type with NAME field + - Custom 'indicator_' types where name is extracted from the type - :param node: The indicator_a_bolengerband node. + :param node: The indicator node. :param indent_level: Current indentation level. :return: A string representing the indicator value retrieval. """ fields = node.get('fields', {}) + node_type = node.get('type', '') + + # Try to get indicator name from fields first, then from type indicator_name = fields.get('NAME') + if not indicator_name and node_type.startswith('indicator_'): + # Extract name from type, e.g., 'indicator_ema2' -> 'ema2' + indicator_name = node_type[len('indicator_'):] + output_field = fields.get('OUTPUT') if not indicator_name or not output_field: - logger.error("indicator node missing 'NAME' or 'OUTPUT'.") + logger.error(f"indicator node missing name or OUTPUT. type={node_type}, fields={fields}") return 'None' # Collect the indicator information @@ -472,7 +491,8 @@ class PythonGenerator: source_node = inputs.get('source', {}) timeframe = source_node.get('timeframe', self.default_source.get('timeframe', '1m')) exchange = source_node.get('exchange', self.default_source.get('exchange', 'Binance')) - symbol = source_node.get('symbol', self.default_source.get('market', 'BTC/USD')) + # Support both 'symbol' and 'market' keys (default_source uses 'market') + symbol = source_node.get('symbol') or source_node.get('market') or self.default_source.get('symbol') or self.default_source.get('market', 'BTC/USDT') # Track data sources self.data_sources_used.add((exchange, symbol, timeframe)) @@ -493,7 +513,8 @@ class PythonGenerator: source_node = inputs.get('source', {}) timeframe = source_node.get('timeframe', self.default_source.get('timeframe', '1m')) exchange = source_node.get('exchange', self.default_source.get('exchange', 'Binance')) - symbol = source_node.get('symbol', self.default_source.get('market', 'BTC/USD')) + # Support both 'symbol' and 'market' keys (default_source uses 'market') + symbol = source_node.get('symbol') or source_node.get('market') or self.default_source.get('symbol') or self.default_source.get('market', 'BTC/USDT') # Track data sources self.data_sources_used.add((exchange, symbol, timeframe)) @@ -514,7 +535,8 @@ class PythonGenerator: source_node = inputs.get('source', {}) timeframe = source_node.get('timeframe', self.default_source.get('timeframe', '1m')) exchange = source_node.get('exchange', self.default_source.get('exchange', 'Binance')) - symbol = source_node.get('symbol', self.default_source.get('market', 'BTC/USD')) + # Support both 'symbol' and 'market' keys (default_source uses 'market') + symbol = source_node.get('symbol') or source_node.get('market') or self.default_source.get('symbol') or self.default_source.get('market', 'BTC/USDT') # Track data sources self.data_sources_used.add((exchange, symbol, timeframe)) @@ -541,7 +563,8 @@ class PythonGenerator: source_node = node.get('source', {}) timeframe = source_node.get('timeframe', self.default_source.get('timeframe', '1m')) exchange = source_node.get('exchange', self.default_source.get('exchange', 'Binance')) - symbol = source_node.get('symbol', self.default_source.get('market', 'BTC/USD')) + # Support both 'symbol' and 'market' keys (default_source uses 'market') + symbol = source_node.get('symbol') or source_node.get('market') or self.default_source.get('symbol') or self.default_source.get('market', 'BTC/USDT') # Track data sources self.data_sources_used.add((exchange, symbol, timeframe)) @@ -560,7 +583,8 @@ class PythonGenerator: """ timeframe = node.get('time_frame', '5m') exchange = node.get('exchange', 'Binance') - symbol = node.get('symbol', 'BTC/USD') + # Support both 'symbol' and 'market' keys + symbol = node.get('symbol') or node.get('market', 'BTC/USDT') # Track data sources self.data_sources_used.add((exchange, symbol, timeframe)) @@ -589,11 +613,12 @@ class PythonGenerator: """ operator = node.get('operator') inputs = node.get('inputs', {}) - left_node = inputs.get('LEFT') - right_node = inputs.get('RIGHT') + # Support both uppercase (LEFT/RIGHT) and lowercase (left/right) keys + left_node = inputs.get('LEFT') or inputs.get('left') + right_node = inputs.get('RIGHT') or inputs.get('right') if not operator or not left_node or not right_node: - logger.error("comparison node missing 'operator', 'LEFT', or 'RIGHT'.") + logger.error(f"comparison node missing 'operator', 'LEFT', or 'RIGHT'. inputs={inputs}") return 'False' operator_map = { @@ -624,11 +649,12 @@ class PythonGenerator: :return: A string representing the condition. """ inputs = node.get('inputs', {}) - left_node = inputs.get('LEFT') - right_node = inputs.get('RIGHT') + # Support both uppercase (LEFT/RIGHT) and lowercase (left/right) keys + left_node = inputs.get('LEFT') or inputs.get('left') + right_node = inputs.get('RIGHT') or inputs.get('right') if not left_node or not right_node: - logger.warning("logical_and node missing 'LEFT' or 'RIGHT'. Defaulting to 'False'.") + logger.warning(f"logical_and node missing 'LEFT' or 'RIGHT'. inputs={inputs}. Defaulting to 'False'.") return 'False' left_expr = self.generate_condition_code(left_node, indent_level) @@ -646,11 +672,12 @@ class PythonGenerator: :return: A string representing the condition. """ inputs = node.get('inputs', {}) - left_node = inputs.get('LEFT') - right_node = inputs.get('RIGHT') + # Support both uppercase (LEFT/RIGHT) and lowercase (left/right) keys + left_node = inputs.get('LEFT') or inputs.get('left') + right_node = inputs.get('RIGHT') or inputs.get('right') if not left_node or not right_node: - logger.warning("logical_or node missing 'LEFT' or 'RIGHT'. Defaulting to 'False'.") + logger.warning(f"logical_or node missing 'LEFT' or 'RIGHT'. inputs={inputs}. Defaulting to 'False'.") return 'False' left_expr = self.generate_condition_code(left_node, indent_level) @@ -724,7 +751,8 @@ class PythonGenerator: # Collect data sources source = trade_options.get('source', self.default_source) exchange = source.get('exchange', 'binance') - symbol = source.get('symbol', 'BTC/USD') + # Support both 'symbol' and 'market' keys (default_source uses 'market') + symbol = source.get('symbol') or source.get('market', 'BTC/USDT') timeframe = source.get('timeframe', '5m') self.data_sources_used.add((exchange, symbol, timeframe)) @@ -929,7 +957,8 @@ class PythonGenerator: """ time_frame = inputs.get('time_frame', '1m') exchange = inputs.get('exchange', 'Binance') - symbol = inputs.get('symbol', 'BTC/USD') + # Support both 'symbol' and 'market' keys + symbol = inputs.get('symbol') or inputs.get('market', 'BTC/USDT') target_market = { 'time_frame': time_frame, diff --git a/src/app.py b/src/app.py index 9868ef3..3775eca 100644 --- a/src/app.py +++ b/src/app.py @@ -7,8 +7,9 @@ eventlet.monkey_patch() # noqa: E402 # Standard library imports import logging # noqa: E402 import os # noqa: E402 -# import json # noqa: E402 -# import datetime as dt # noqa: E402 +import json # noqa: E402 +import subprocess # noqa: E402 +import xml.etree.ElementTree as ET # noqa: E402 # Third-party imports from flask import Flask, render_template, request, redirect, jsonify, session, flash # noqa: E402 @@ -562,7 +563,7 @@ def edm_config(): edm_settings = brighter_trades.config.get_setting('edm') or {} return jsonify({ 'rest_url': edm_settings.get('rest_url', 'http://localhost:8080'), - 'ws_url': edm_settings.get('ws_url', 'ws://localhost:8765'), + 'ws_url': edm_settings.get('ws_url', 'ws://localhost:8080/ws'), 'enabled': edm_settings.get('enabled', True), }), 200 @@ -583,7 +584,7 @@ def get_chart_view(): chart_view = brighter_trades.users.get_chart_view(user_name=user_name) if chart_view: return jsonify({ - 'exchange': chart_view.get('exchange_name', 'binance'), + 'exchange': chart_view.get('exchange', 'binance'), 'market': chart_view.get('market', 'BTC/USDT'), 'timeframe': chart_view.get('timeframe', '1h'), }), 200 @@ -603,6 +604,91 @@ def get_chart_view(): }), 200 +@app.route('/api/generate-strategy', methods=['POST']) +def generate_strategy(): + """ + Generate a Blockly strategy from natural language description using AI. + Calls the CmdForge strategy-builder tool. + """ + data = request.get_json() or {} + description = data.get('description', '').strip() + indicators = data.get('indicators', []) + signals = data.get('signals', []) + default_source = data.get('default_source', { + 'exchange': 'binance', + 'market': 'BTC/USDT', + 'timeframe': '5m' + }) + + if not description: + return jsonify({'error': 'Description is required'}), 400 + + try: + # Build input for the strategy-builder tool + tool_input = json.dumps({ + 'description': description, + 'indicators': indicators, + 'signals': signals, + 'default_source': default_source + }) + + # Call CmdForge strategy-builder tool + result = subprocess.run( + ['strategy-builder'], + input=tool_input, + capture_output=True, + text=True, + timeout=120 # 2 minute timeout for AI generation + ) + + if result.returncode != 0: + error_msg = result.stderr.strip() or 'Strategy generation failed' + logging.error(f"strategy-builder failed: {error_msg}") + return jsonify({'error': error_msg}), 500 + + workspace_xml = result.stdout.strip() + + # Validate the generated XML + if not _validate_blockly_xml(workspace_xml): + logging.error(f"Invalid Blockly XML generated: {workspace_xml[:200]}") + return jsonify({'error': 'Generated strategy is invalid'}), 500 + + return jsonify({ + 'success': True, + 'workspace_xml': workspace_xml + }), 200 + + except subprocess.TimeoutExpired: + logging.error("strategy-builder timed out") + return jsonify({'error': 'Strategy generation timed out'}), 504 + except FileNotFoundError: + logging.error("strategy-builder tool not found") + return jsonify({'error': 'Strategy builder tool not installed'}), 500 + except Exception as e: + logging.error(f"Error generating strategy: {e}") + return jsonify({'error': str(e)}), 500 + + +def _validate_blockly_xml(xml_string: str) -> bool: + """Validate that the string is valid Blockly XML.""" + try: + root = ET.fromstring(xml_string) + # Check it's a Blockly XML document (handle namespace prefixed tags) + # Tag can be 'xml' or '{namespace}xml' + tag_name = root.tag.split('}')[-1] if '}' in root.tag else root.tag + if tag_name != 'xml' and 'blockly' not in root.tag.lower(): + return False + # Check it has at least one block using namespace-aware search + # Use .//* to find all descendants, then filter by local name + blocks = [elem for elem in root.iter() + if (elem.tag.split('}')[-1] if '}' in elem.tag else elem.tag) == 'block'] + return len(blocks) > 0 + except ET.ParseError: + return False + except Exception: + return False + + @app.route('/health/edm', methods=['GET']) def edm_health(): """ diff --git a/src/backtesting.py b/src/backtesting.py index 6783df3..bd0c016 100644 --- a/src/backtesting.py +++ b/src/backtesting.py @@ -583,6 +583,21 @@ class Backtester: # Prepare the source and indicator feeds referenced in the strategy strategy_components = user_strategy.get('strategy_components', {}) + + # Always use default_source as the primary data source if available. + # This ensures we use the user's explicitly configured trading source, + # even if data_sources was generated with incorrect symbol format. + default_source = user_strategy.get('default_source') + if default_source: + # Map 'market' to 'symbol' if needed (default_source uses 'market', prepare_data_feed expects 'symbol') + source = { + 'exchange': default_source.get('exchange'), + 'symbol': default_source.get('symbol') or default_source.get('market'), + 'timeframe': default_source.get('timeframe') + } + logger.info(f"Using default_source for backtest data: {source}") + strategy_components['data_sources'] = [source] + try: data_feed, precomputed_indicators = self.prepare_backtest_data(msg_data, strategy_components) except ValueError as ve: diff --git a/src/candles.py b/src/candles.py index deadfdd..be3dea7 100644 --- a/src/candles.py +++ b/src/candles.py @@ -193,14 +193,14 @@ class Candles: Converts a dataframe of candlesticks into the format lightweight charts expects. :param candles: dt.dataframe - :return: List - [{'time': value, 'open': value,...},...] + :return: DataFrame with columns time, open, high, low, close, volume """ + if candles.empty: + return candles - new_candles = candles.loc[:, ['time', 'open', 'high', 'low', 'close', 'volume']] - - # The timestamps are in milliseconds but lightweight charts needs it divided by 1000. - new_candles.loc[:, ['time']] = new_candles.loc[:, ['time']].div(1000) + new_candles = candles.loc[:, ['time', 'open', 'high', 'low', 'close', 'volume']].copy() + # EDM sends timestamps in seconds - no conversion needed for lightweight charts return new_candles def get_candle_history(self, num_records: int, symbol: str = None, interval: str = None, diff --git a/src/static/Strategies.js b/src/static/Strategies.js index 2225dba..758271f 100644 --- a/src/static/Strategies.js +++ b/src/static/Strategies.js @@ -506,6 +506,175 @@ class StratUIManager { registerDeleteStrategyCallback(callback) { this.onDeleteStrategy = callback; } + + // ========== AI Strategy Builder Methods ========== + + /** + * Opens the AI strategy builder dialog. + */ + openAIDialog() { + const dialog = document.getElementById('ai_strategy_form'); + if (dialog) { + // Reset state + const descriptionEl = document.getElementById('ai_strategy_description'); + const loadingEl = document.getElementById('ai_strategy_loading'); + const errorEl = document.getElementById('ai_strategy_error'); + const generateBtn = document.getElementById('ai_generate_btn'); + + if (descriptionEl) descriptionEl.value = ''; + if (loadingEl) loadingEl.style.display = 'none'; + if (errorEl) errorEl.style.display = 'none'; + if (generateBtn) generateBtn.disabled = false; + + // Show and center the dialog + dialog.style.display = 'block'; + dialog.style.left = '50%'; + dialog.style.top = '50%'; + dialog.style.transform = 'translate(-50%, -50%)'; + } + } + + /** + * Closes the AI strategy builder dialog. + */ + closeAIDialog() { + const dialog = document.getElementById('ai_strategy_form'); + if (dialog) { + dialog.style.display = 'none'; + } + } + + /** + * Calls the API to generate a strategy from the natural language description. + */ + async generateWithAI() { + const descriptionEl = document.getElementById('ai_strategy_description'); + const description = descriptionEl ? descriptionEl.value.trim() : ''; + + if (!description) { + alert('Please enter a strategy description.'); + return; + } + + const loadingEl = document.getElementById('ai_strategy_loading'); + const errorEl = document.getElementById('ai_strategy_error'); + const generateBtn = document.getElementById('ai_generate_btn'); + + // Gather user's available indicators and signals + const indicators = this._getAvailableIndicators(); + const signals = this._getAvailableSignals(); + const defaultSource = this._getDefaultSource(); + + // Check if description mentions indicators but none are configured + const indicatorKeywords = ['ema', 'sma', 'rsi', 'macd', 'bollinger', 'bb', 'atr', 'adx', 'stochastic']; + const descLower = description.toLowerCase(); + const mentionsIndicators = indicatorKeywords.some(kw => descLower.includes(kw)); + + if (mentionsIndicators && indicators.length === 0) { + const proceed = confirm( + 'Your strategy mentions indicators (EMA, RSI, Bollinger Bands, etc.) but you haven\'t configured any indicators yet.\n\n' + + 'Please add the required indicators in the Indicators panel on the right side of the screen first.\n\n' + + 'Click OK to proceed anyway (the AI will use price-based logic only), or Cancel to add indicators first.' + ); + if (!proceed) { + return; + } + } + + // Show loading state + if (loadingEl) loadingEl.style.display = 'block'; + if (errorEl) errorEl.style.display = 'none'; + if (generateBtn) generateBtn.disabled = true; + + console.log('Generating strategy with:', { description, indicators, signals, defaultSource }); + + try { + + const response = await fetch('/api/generate-strategy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + description, + indicators, + signals, + default_source: defaultSource + }) + }); + + const data = await response.json(); + + if (!response.ok || !data.success) { + throw new Error(data.error || 'Strategy generation failed'); + } + + // Load the generated Blockly XML into the workspace + if (this.workspaceManager && data.workspace_xml) { + this.workspaceManager.loadWorkspaceFromXml(data.workspace_xml); + } + + // Close the AI dialog + this.closeAIDialog(); + + console.log('Strategy generated successfully with AI'); + + } catch (error) { + console.error('AI generation error:', error); + if (errorEl) { + errorEl.textContent = `Error: ${error.message}`; + errorEl.style.display = 'block'; + } + } finally { + if (loadingEl) loadingEl.style.display = 'none'; + if (generateBtn) generateBtn.disabled = false; + } + } + + /** + * Gets the user's available indicators for the AI prompt. + * @returns {Array} Array of indicator objects with name and outputs. + * @private + */ + _getAvailableIndicators() { + // Use getIndicatorOutputs() which returns {name: outputs[]} from i_objs + const indicatorOutputs = window.UI?.indicators?.getIndicatorOutputs?.() || {}; + const indicatorObjs = window.UI?.indicators?.i_objs || {}; + + return Object.entries(indicatorOutputs).map(([name, outputs]) => ({ + name: name, + type: indicatorObjs[name]?.constructor?.name || 'unknown', + outputs: outputs + })); + } + + /** + * Gets the user's available signals for the AI prompt. + * @returns {Array} Array of signal objects with name. + * @private + */ + _getAvailableSignals() { + // Get from UI.signals if available + const signals = window.UI?.signals?.signals || []; + return signals.map(sig => ({ + name: sig.name || sig.id + })); + } + + /** + * Gets the current default trading source from the strategy form. + * @returns {Object} Object with exchange, market, and timeframe. + * @private + */ + _getDefaultSource() { + const exchangeEl = document.getElementById('strategy_exchange'); + const symbolEl = document.getElementById('strategy_symbol'); + const timeframeEl = document.getElementById('strategy_timeframe'); + + return { + exchange: exchangeEl ? exchangeEl.value : 'binance', + market: symbolEl ? symbolEl.value : 'BTC/USDT', + timeframe: timeframeEl ? timeframeEl.value : '5m' + }; + } } class StratDataManager { @@ -1818,4 +1987,27 @@ class Strategies { } return modes; } + + // ========== AI Strategy Builder Wrappers ========== + + /** + * Opens the AI strategy builder dialog. + */ + openAIDialog() { + this.uiManager.openAIDialog(); + } + + /** + * Closes the AI strategy builder dialog. + */ + closeAIDialog() { + this.uiManager.closeAIDialog(); + } + + /** + * Generates a strategy from natural language using AI. + */ + async generateWithAI() { + await this.uiManager.generateWithAI(); + } } diff --git a/src/static/communication.js b/src/static/communication.js index 4dbb5c0..ca81baf 100644 --- a/src/static/communication.js +++ b/src/static/communication.js @@ -303,9 +303,9 @@ class Comms { const data = await response.json(); const candles = data.candles || []; - // Convert to lightweight charts format (time in seconds) + // EDM already sends time in seconds, no conversion needed return candles.map(c => ({ - time: c.time / 1000, + time: c.time, open: c.open, high: c.high, low: c.low, @@ -531,7 +531,7 @@ class Comms { if (messageType === 'candle') { const candle = message.data; const newCandle = { - time: candle.time / 1000, // Convert ms to seconds + time: candle.time, // EDM sends time in seconds open: parseFloat(candle.open), high: parseFloat(candle.high), low: parseFloat(candle.low), diff --git a/src/static/general.js b/src/static/general.js index 4501bc1..ab763b8 100644 --- a/src/static/general.js +++ b/src/static/general.js @@ -33,6 +33,7 @@ class User_Interface { this.initializeResizablePopup("new_ind_form", null, "indicator_draggable_header", "resize-indicator"); this.initializeResizablePopup("new_sig_form", null, "signal_draggable_header", "resize-signal"); this.initializeResizablePopup("new_trade_form", null, "trade_draggable_header", "resize-trade"); + this.initializeResizablePopup("ai_strategy_form", null, "ai_strategy_header", "resize-ai-strategy"); // Initialize Backtesting's DOM elements this.backtesting.initialize(); diff --git a/src/templates/ai_strategy_dialog.html b/src/templates/ai_strategy_dialog.html new file mode 100644 index 0000000..eb46fca --- /dev/null +++ b/src/templates/ai_strategy_dialog.html @@ -0,0 +1,81 @@ + + + + + diff --git a/src/templates/index.html b/src/templates/index.html index 142168f..a27774b 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -40,6 +40,7 @@ {% include "backtest_popup.html" %} {% include "new_trade_popup.html" %} {% include "new_strategy_popup.html" %} + {% include "ai_strategy_dialog.html" %} {% include "new_signal_popup.html" %} {% include "new_indicator_popup.html" %} {% include "trade_details_popup.html" %} diff --git a/src/templates/login_page.html b/src/templates/login_page.html index feef57c..de2b1c0 100644 --- a/src/templates/login_page.html +++ b/src/templates/login_page.html @@ -6,6 +6,7 @@ {{ title }} | BrighterTrades + diff --git a/src/templates/new_strategy_popup.html b/src/templates/new_strategy_popup.html index e9db4d3..3abc544 100644 --- a/src/templates/new_strategy_popup.html +++ b/src/templates/new_strategy_popup.html @@ -9,6 +9,16 @@
+ +
+ +
+
diff --git a/src/templates/sign_up.html b/src/templates/sign_up.html index 5db7d75..91402c6 100644 --- a/src/templates/sign_up.html +++ b/src/templates/sign_up.html @@ -12,6 +12,7 @@ {{ title }} | BrighterTrades + diff --git a/src/templates/welcome.html b/src/templates/welcome.html index 968c6c9..e7ac61a 100644 --- a/src/templates/welcome.html +++ b/src/templates/welcome.html @@ -4,6 +4,7 @@ BrighterTrading - Welcome +