diff --git a/.gitignore b/.gitignore index f2653f8..5219fee 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,19 @@ Thumbs.db # Ignore local AI/tooling settings .claude/ +# Ignore test screenshots and playwright artifacts +*.png +*.yml +!requirements.yml +.playwright-cli/ +output.text + +# Ignore backup files +*.backup.js + +# Ignore test state files +src/app_loaded.yaml + # Ignore local symlinked docs from centralized docs repo docs/ docs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..32e7809 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# Repository Guidelines + +## Project Structure & Module Organization +`src/` contains the application code. `src/app.py` is the Flask-SocketIO entrypoint; trading logic lives in modules such as `BrighterTrades.py`, `Exchange.py`, `trade.py`, and `DataCache_v3.py`. Keep browser assets in `src/static/` and Jinja templates in `src/templates/`. Service-specific code lives under `src/brokers/`, `src/wallet/`, and `src/edm_client/`. `tests/` mirrors backend features with `test_*.py`. Treat `archived_code/` as reference-only, not active code. `markdown/` and `UML/` hold supporting docs and design notes. + +## Build, Test, and Development Commands +`python -m venv .venv && source .venv/bin/activate` creates a local environment. + +`pip install -r requirements.txt` installs runtime and test dependencies. + +`python src/app.py` starts the app on `127.0.0.1:5002`. + +`pytest` runs the default suite from `tests/` and skips `integration` tests per `pytest.ini`. + +`pytest -m integration` runs tests that call external services. + +`pytest -m live_testnet` runs tests that require live testnet credentials. + +## Coding Style & Naming Conventions +Use 4-space indentation in Python and group imports as standard library, third-party, then local modules. Prefer `snake_case` for functions, variables, and new module names; use `PascalCase` for classes. This repository has legacy CamelCase modules such as `BrighterTrades.py` and `Exchange.py`; preserve existing naming when editing nearby code instead of renaming broadly. Keep frontend filenames descriptive, for example `formations.js` or `backtesting.js`. No formatter or linter config is committed, so keep edits focused and style-consistent with the surrounding file. + +## Testing Guidelines +`pytest.ini` expects `test_*.py`, `Test*`, and `test_*`. Add tests alongside the behavior you change and mark external coverage with `@pytest.mark.integration`, `@pytest.mark.live_integration`, or `@pytest.mark.live_testnet` as appropriate. For normal PRs, include at least one automated test for each bug fix or behavior change. Prefer mocks or fakes for exchange and wallet paths unless you are intentionally covering live integrations. + +## Commit & Pull Request Guidelines +Recent history favors short imperative subjects like `Fix chart sync...`, `Add Blockly integration...`, and `Improve line drawing UX...`. Keep commits scoped and descriptive. PRs should include a brief summary, affected areas, test commands run, and screenshots for UI changes in `src/static/` or `src/templates/`. Call out any new environment variables, API keys, or YAML/config updates reviewers need. + +## Configuration & Security Tips +Use `src/config.example.py` as the template for local configuration and keep secrets in environment variables such as `BRIGHTER_BINANCE_API_KEY` and `BRIGHTER_ALPACA_API_KEY`. Do not commit real credentials, local database files, or live/testnet secrets. diff --git a/src/app.py b/src/app.py index 9d01c65..36b91bc 100644 --- a/src/app.py +++ b/src/app.py @@ -1212,6 +1212,151 @@ def delete_external_indicator(tbl_key): return jsonify(result) +# ============================================================================= +# Health Check Routes +# ============================================================================= + +# ============================================================================= +# Chart Analysis API Routes +# ============================================================================= + +@app.route('/api/detect_patterns', methods=['POST']) +def detect_patterns(): + """ + Detect all candlestick patterns in a given time range. + + This endpoint runs ALL CDL_* pattern indicators on the candle data + within the specified time range and returns any detected patterns. + + Request body: + user_name: str - The authenticated user + exchange: str - Exchange name (e.g., 'binance') + symbol: str - Trading pair (e.g., 'BTC/USDT') + timeframe: str - Candle timeframe (e.g., '1h') + start_time: int - Start timestamp (seconds) + end_time: int - End timestamp (seconds) + + Returns: + patterns: list - List of detected patterns with name and value + """ + from indicators import indicators_registry, CandlestickPattern + import pandas as pd + import numpy as np + + data = request.get_json() or {} + user_name = data.get('user_name') + exchange = data.get('exchange', 'binance') + symbol = data.get('symbol', 'BTC/USDT') + timeframe = data.get('timeframe', '1h') + start_time = data.get('start_time') + end_time = data.get('end_time') + + if not user_name: + return jsonify({'success': False, 'error': 'user_name is required'}), 400 + + if not start_time or not end_time: + return jsonify({'success': False, 'error': 'start_time and end_time are required'}), 400 + + try: + # Get candle data - fetch enough to cover historical chart data + # Charts typically show ~500 candles, so fetch more to ensure coverage + num_candles = 1000 + + candles_df = brighter_trades.candles.get_last_n_candles( + num_candles=num_candles, + asset=symbol, + timeframe=timeframe, + exchange=exchange, + user_name=user_name + ) + + if candles_df is None or candles_df.empty: + return jsonify({'success': True, 'patterns': []}) + + # Calculate context window based on timeframe + # Pattern detection needs ~10 candles before for context (3-5 candle patterns) + timeframe_seconds = { + '1m': 60, '3m': 180, '5m': 300, '15m': 900, '30m': 1800, + '1h': 3600, '2h': 7200, '4h': 14400, '6h': 21600, + '12h': 43200, '1d': 86400, '1w': 604800 + } + tf_secs = timeframe_seconds.get(timeframe, 3600) + context_before = tf_secs * 10 # 10 candles of context + + # Filter candles to ONLY the clicked range plus minimal context + context_candles = candles_df[ + (candles_df['time'] >= start_time - context_before) & + (candles_df['time'] <= end_time) + ].copy() + + if context_candles.empty: + return jsonify({'success': True, 'patterns': []}) + + # Get all CDL_* pattern indicators + cdl_patterns = { + name: cls for name, cls in indicators_registry.items() + if name.startswith('CDL_') + } + + detected_patterns = [] + + # Extract OHLC data + opens = context_candles['open'].to_numpy(dtype='float') + highs = context_candles['high'].to_numpy(dtype='float') + lows = context_candles['low'].to_numpy(dtype='float') + closes = context_candles['close'].to_numpy(dtype='float') + times = context_candles['time'].to_numpy() + + # Run each pattern detector + for pattern_name, pattern_class in cdl_patterns.items(): + try: + # Instantiate the pattern indicator + pattern_indicator = pattern_class( + name=pattern_name, + indicator_type=pattern_name, + properties={} + ) + + # Run detection + pattern_values = pattern_indicator.detect(opens, highs, lows, closes) + + # Check if any patterns were detected in the target range + # Only keep one instance per pattern name (the most recent) + pattern_found = False + for i, (time_val, value) in enumerate(zip(times, pattern_values)): + if time_val >= start_time and time_val <= end_time and value != 0: + if not pattern_found: + # Format pattern name nicely + display_name = pattern_name.replace('CDL_', '').replace('_', ' ').title() + detected_patterns.append({ + 'name': display_name, + 'value': int(value), # 100 = bullish, -100 = bearish + }) + pattern_found = True + break # Only one per pattern type + except Exception as e: + logging.warning(f"Error detecting pattern {pattern_name}: {e}") + continue + + # Remove duplicates by pattern name (keep first occurrence) + seen = set() + unique_patterns = [] + for p in detected_patterns: + key = p['name'] + if key not in seen: + seen.add(key) + unique_patterns.append(p) + + return jsonify({ + 'success': True, + 'patterns': unique_patterns + }) + + except Exception as e: + logging.error(f"Error detecting patterns: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + # ============================================================================= # Health Check Routes # ============================================================================= diff --git a/src/static/brighterStyles.css b/src/static/brighterStyles.css index cc6b23a..d00dbdb 100644 --- a/src/static/brighterStyles.css +++ b/src/static/brighterStyles.css @@ -226,11 +226,171 @@ height: 500px; grid-row: 1; } #user_login{ - width: 350; + width: 450px; margin-left: auto; margin-right: auto; grid-column: 1; grid-row: 1; + display: flex; + align-items: center; + gap: 10px; +} + +/* Analyze Charts Dropdown */ +.analyze-dropdown-container { + position: relative; + display: inline-block; +} + +.analyze-btn { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: white; + border: none; + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 12px; + display: flex; + align-items: center; + gap: 5px; + transition: all 0.2s ease; +} + +.analyze-btn:hover { + background: linear-gradient(135deg, #5558e8 0%, #7c4ddb 100%); + transform: translateY(-1px); +} + +.analyze-icon { + font-size: 14px; +} + +.analyze-dropdown { + position: absolute; + top: 100%; + left: 0; + background: linear-gradient(135deg, #1e1e2e 0%, #2d2d44 100%); + border: 1px solid #444; + border-radius: 8px; + min-width: 180px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); + z-index: 200; + margin-top: 5px; + overflow: hidden; +} + +.analyze-option { + padding: 12px 15px; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + color: #e0e0e0; + font-size: 13px; + transition: all 0.2s ease; +} + +.analyze-option:hover { + background: rgba(99, 102, 241, 0.3); + color: white; +} + +.option-icon { + font-size: 16px; +} + +/* Pattern detection cursor overlay */ +#pattern_detection_cursor { + position: fixed; + pointer-events: none; + z-index: 1000; + border: 2px solid #6366f1; + border-radius: 50%; + background: rgba(99, 102, 241, 0.1); + display: none; +} + +/* Pattern detection results popup */ +#pattern_results_popup { + position: fixed; + background: linear-gradient(135deg, #1e1e2e 0%, #2d2d44 100%); + border: 1px solid #6366f1; + border-radius: 10px; + padding: 0; + min-width: 220px; + max-width: 300px; + max-height: 400px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.5); + z-index: 1001; + display: none; + color: #e0e0e0; + overflow: hidden; +} + +#pattern_results_popup .popup-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + border-bottom: 1px solid #444; + background: rgba(99, 102, 241, 0.2); +} + +#pattern_results_popup h4 { + margin: 0; + color: #6366f1; + font-size: 14px; +} + +#pattern_results_popup .popup-close-btn { + background: none; + border: none; + color: #888; + font-size: 20px; + cursor: pointer; + padding: 0; + line-height: 1; +} + +#pattern_results_popup .popup-close-btn:hover { + color: #f87171; +} + +#pattern_results_list { + padding: 10px 15px; + max-height: 280px; + overflow-y: auto; +} + +#pattern_results_popup .popup-footer { + padding: 8px 15px; + border-top: 1px solid #444; + font-size: 10px; + color: #666; + text-align: center; +} + +.pattern-result-item { + padding: 6px 0; + font-size: 12px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.pattern-result-item.bullish { + color: #4ade80; +} + +.pattern-result-item.bearish { + color: #f87171; +} + +.pattern-strength { + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + background: rgba(255,255,255,0.1); } #login_button{ width: 100px; diff --git a/src/static/chart_analysis.js b/src/static/chart_analysis.js new file mode 100644 index 0000000..69c2319 --- /dev/null +++ b/src/static/chart_analysis.js @@ -0,0 +1,280 @@ +/** + * ChartAnalysis - Tools for analyzing chart patterns and data + * + * Features: + * - Pattern Detection: Hover over candles to detect candlestick patterns + */ +class ChartAnalysis { + constructor() { + this.isDetectionMode = false; + this.cursorElement = null; + this.resultsPopup = null; + this.cursorRadius = 40; // pixels + this.chartContainer = null; + this.chart = null; + this.candleSeries = null; + + // Bind methods + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseClick = this._onMouseClick.bind(this); + this._onKeyDown = this._onKeyDown.bind(this); + this._closeDropdownOnClickOutside = this._closeDropdownOnClickOutside.bind(this); + } + + /** + * Initialize with chart references + */ + initialize(chartContainerId, chart, candleSeries) { + this.chartContainer = document.getElementById(chartContainerId); + this.chart = chart; + this.candleSeries = candleSeries; + this._createCursorElement(); + this._createResultsPopup(); + + // Close dropdown when clicking outside + document.addEventListener('click', this._closeDropdownOnClickOutside); + } + + /** + * Toggle the analyze dropdown + */ + toggleDropdown() { + const dropdown = document.getElementById('analyze_dropdown'); + if (dropdown) { + const isVisible = dropdown.style.display !== 'none'; + dropdown.style.display = isVisible ? 'none' : 'block'; + } + } + + _closeDropdownOnClickOutside(e) { + const container = document.getElementById('analyze_charts_container'); + const dropdown = document.getElementById('analyze_dropdown'); + if (container && dropdown && !container.contains(e.target)) { + dropdown.style.display = 'none'; + } + } + + /** + * Create the circular cursor element + */ + _createCursorElement() { + if (this.cursorElement) return; + + this.cursorElement = document.createElement('div'); + this.cursorElement.id = 'pattern_detection_cursor'; + this.cursorElement.style.width = (this.cursorRadius * 2) + 'px'; + this.cursorElement.style.height = (this.cursorRadius * 2) + 'px'; + document.body.appendChild(this.cursorElement); + } + + /** + * Create the results popup element + */ + _createResultsPopup() { + if (this.resultsPopup) return; + + this.resultsPopup = document.createElement('div'); + this.resultsPopup.id = 'pattern_results_popup'; + this.resultsPopup.innerHTML = ` +