Add chart analysis feature with candlestick pattern detection
- Add /api/detect_patterns endpoint to detect CDL_* patterns in time range - Add ChartAnalysis class with hover-based pattern detection mode - Add Analyze dropdown button in header with pattern detection option - Style dropdown and pattern detection UI with gradient theme - Add AGENTS.md with repository guidelines for AI agents - Update gitignore to exclude test screenshots and artifacts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9a389d20d2
commit
2dccabfac3
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
145
src/app.py
145
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
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div class="popup-header">
|
||||
<h4>Detected Patterns</h4>
|
||||
<button class="popup-close-btn" onclick="UI.chartAnalysis.stopPatternDetection()">×</button>
|
||||
</div>
|
||||
<div id="pattern_results_list"></div>
|
||||
<div class="popup-footer">Press ESC to exit</div>
|
||||
`;
|
||||
document.body.appendChild(this.resultsPopup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start pattern detection mode
|
||||
*/
|
||||
startPatternDetection() {
|
||||
// Close dropdown
|
||||
const dropdown = document.getElementById('analyze_dropdown');
|
||||
if (dropdown) dropdown.style.display = 'none';
|
||||
|
||||
if (!this.chartContainer) {
|
||||
console.error('ChartAnalysis: Chart container not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDetectionMode = true;
|
||||
|
||||
// Show cursor
|
||||
if (this.cursorElement) {
|
||||
this.cursorElement.style.display = 'block';
|
||||
}
|
||||
|
||||
// Change chart cursor
|
||||
this.chartContainer.style.cursor = 'none';
|
||||
|
||||
// Add event listeners
|
||||
this.chartContainer.addEventListener('mousemove', this._onMouseMove);
|
||||
this.chartContainer.addEventListener('click', this._onMouseClick);
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
|
||||
console.log('Pattern detection mode activated. Click on candles to detect patterns. Press ESC to exit.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop pattern detection mode
|
||||
*/
|
||||
stopPatternDetection() {
|
||||
this.isDetectionMode = false;
|
||||
|
||||
// Hide cursor
|
||||
if (this.cursorElement) {
|
||||
this.cursorElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Hide results popup
|
||||
if (this.resultsPopup) {
|
||||
this.resultsPopup.style.display = 'none';
|
||||
}
|
||||
|
||||
// Restore chart cursor
|
||||
if (this.chartContainer) {
|
||||
this.chartContainer.style.cursor = 'crosshair';
|
||||
this.chartContainer.removeEventListener('mousemove', this._onMouseMove);
|
||||
this.chartContainer.removeEventListener('click', this._onMouseClick);
|
||||
}
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
|
||||
console.log('Pattern detection mode deactivated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse movement - update cursor position
|
||||
*/
|
||||
_onMouseMove(e) {
|
||||
if (!this.isDetectionMode || !this.cursorElement) return;
|
||||
|
||||
const x = e.clientX - this.cursorRadius;
|
||||
const y = e.clientY - this.cursorRadius;
|
||||
|
||||
this.cursorElement.style.left = x + 'px';
|
||||
this.cursorElement.style.top = y + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click - detect patterns at this location
|
||||
*/
|
||||
async _onMouseClick(e) {
|
||||
if (!this.isDetectionMode) return;
|
||||
|
||||
// Get chart container bounds
|
||||
const rect = this.chartContainer.getBoundingClientRect();
|
||||
const localX = e.clientX - rect.left;
|
||||
|
||||
// Get time range covered by cursor
|
||||
const timeScale = this.chart.timeScale();
|
||||
const leftX = localX - this.cursorRadius;
|
||||
const rightX = localX + this.cursorRadius;
|
||||
|
||||
const leftTime = timeScale.coordinateToTime(leftX);
|
||||
const rightTime = timeScale.coordinateToTime(rightX);
|
||||
|
||||
if (!leftTime || !rightTime) {
|
||||
console.log('Could not determine time range');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
this._showResults([{name: 'Analyzing...', value: 0}], e.clientX, e.clientY);
|
||||
|
||||
// Request pattern detection from backend
|
||||
try {
|
||||
const patterns = await this._detectPatterns(leftTime, rightTime);
|
||||
this._showResults(patterns, e.clientX, e.clientY);
|
||||
} catch (error) {
|
||||
console.error('Pattern detection failed:', error);
|
||||
this._showResults([{name: 'Error detecting patterns', value: 0}], e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard events
|
||||
*/
|
||||
_onKeyDown(e) {
|
||||
if (e.key === 'Escape' && this.isDetectionMode) {
|
||||
this.stopPatternDetection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call backend to detect patterns in time range
|
||||
*/
|
||||
async _detectPatterns(startTime, endTime) {
|
||||
const response = await fetch('/api/detect_patterns', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_name: window.UI.data.user_name,
|
||||
exchange: window.UI.data.exchange,
|
||||
symbol: window.UI.data.trading_pair,
|
||||
timeframe: window.UI.data.interval,
|
||||
start_time: startTime,
|
||||
end_time: endTime
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Pattern detection request failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.patterns || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show results popup
|
||||
*/
|
||||
_showResults(patterns, x, y) {
|
||||
if (!this.resultsPopup) return;
|
||||
|
||||
const resultsList = document.getElementById('pattern_results_list');
|
||||
if (!resultsList) return;
|
||||
|
||||
if (patterns.length === 0) {
|
||||
resultsList.innerHTML = '<div class="pattern-result-item">No patterns detected</div>';
|
||||
} else {
|
||||
resultsList.innerHTML = patterns.map(p => {
|
||||
const isBullish = p.value > 0;
|
||||
const className = p.value === 0 ? '' : (isBullish ? 'bullish' : 'bearish');
|
||||
const arrow = p.value === 0 ? '' : (isBullish ? '↑' : '↓');
|
||||
const strength = Math.abs(p.value);
|
||||
|
||||
return `
|
||||
<div class="pattern-result-item ${className}">
|
||||
<span>${p.name} ${arrow}</span>
|
||||
${strength > 0 ? `<span class="pattern-strength">${strength}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Position popup near click but ensure it stays on screen
|
||||
const popupWidth = 250;
|
||||
const popupHeight = 200;
|
||||
let popupX = x + 20;
|
||||
let popupY = y - 50;
|
||||
|
||||
if (popupX + popupWidth > window.innerWidth) {
|
||||
popupX = x - popupWidth - 20;
|
||||
}
|
||||
if (popupY + popupHeight > window.innerHeight) {
|
||||
popupY = window.innerHeight - popupHeight - 10;
|
||||
}
|
||||
if (popupY < 10) popupY = 10;
|
||||
|
||||
this.resultsPopup.style.left = popupX + 'px';
|
||||
this.resultsPopup.style.top = popupY + 'px';
|
||||
this.resultsPopup.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use
|
||||
window.ChartAnalysis = ChartAnalysis;
|
||||
|
|
@ -11,6 +11,7 @@ class User_Interface {
|
|||
this.indicators = new Indicators(this.data.comms);
|
||||
this.signals = new Signals(this);
|
||||
this.formations = new Formations(this);
|
||||
this.chartAnalysis = new ChartAnalysis();
|
||||
this.backtesting = new Backtesting(this);
|
||||
this.statistics = new Statistics(this.data.comms);
|
||||
this.account = new Account();
|
||||
|
|
@ -81,6 +82,7 @@ class User_Interface {
|
|||
this.signals.initialize('signal_list', 'new_sig_form');
|
||||
this.formations.initialize('formations_list');
|
||||
this.formations.initOverlay(this.data.chart1_id, this.charts.chart_1, this.charts.candleSeries);
|
||||
this.chartAnalysis.initialize(this.data.chart1_id, this.charts.chart_1, this.charts.candleSeries);
|
||||
this.alerts.set_target();
|
||||
this.alerts.initialize(this.data.comms);
|
||||
this.controls.init_TP_selector();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<link rel="icon" href="{{ url_for('static', filename='brightertrades_favicon.ico') }}" type="image/x-icon">
|
||||
|
||||
<!-- Load style sheets and set the title -->
|
||||
<link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='brighterStyles.css') }}">
|
||||
<link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='brighterStyles.css') }}?v=2">
|
||||
<title>{{ title }}</title>
|
||||
<!-- Load lightweight charts -->
|
||||
<script src="{{ url_for('static', filename='lightweight-charts.standalone.production.js') }}"></script>
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
<script src="{{ url_for('static', filename='signals.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='formation_overlay.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='formations.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='chart_analysis.js') }}?v=2"></script>
|
||||
<script src="{{ url_for('static', filename='trade.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='backtesting.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='Statistics.js') }}?v=1"></script>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
<div id="user_login">
|
||||
<!-- Analyze Charts dropdown -->
|
||||
<div id="analyze_charts_container" class="analyze-dropdown-container">
|
||||
<button class="btn analyze-btn" onclick="UI.chartAnalysis.toggleDropdown()">
|
||||
<span class="analyze-icon">📊</span> Analyze
|
||||
</button>
|
||||
<div id="analyze_dropdown" class="analyze-dropdown" style="display: none;">
|
||||
<div class="analyze-option" onclick="UI.chartAnalysis.startPatternDetection()">
|
||||
<span class="option-icon">🔍</span> Detect Patterns
|
||||
</div>
|
||||
<!-- Future analysis tools can be added here -->
|
||||
</div>
|
||||
</div>
|
||||
<span id="username_display" class="username-link" onclick="UI.account.showAccountSettings()">{{user_name}}</span>
|
||||
<button class="btn" type="button" id="login_button" name="login" onclick="UI.users.toggleLogin()">
|
||||
Sign in
|
||||
|
|
|
|||
Loading…
Reference in New Issue