Add indicator signals feature with buy/sell/hold analysis
- Add signals-calculator.js module for calculating buy/sell/hold signals for all indicators - Integrate signals into Trend Analysis panel (renamed to Indicator Analysis) - Display individual indicator signals with badges, values, strength bars, and detailed reasoning - Add aggregate summary signal showing overall recommendation from all indicators - Support signals for RSI, MACD, Stochastic, Bollinger Bands, SMA/EMA, ATR, and HTS - Provide tooltips on hover showing indicator value, configuration, and reasoning - Ensure indicators calculate on all available candles, not just recent ones - Cache indicator calculations for performance while recalculating on historical data loads - Style improvements: monospace font, consistent button widths, reduced margins - Add AGENTS.md documentation file with project guidelines
This commit is contained in:
160
AGENTS.md
Normal file
160
AGENTS.md
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
# Agent Development Guidelines
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
This is a Bitcoin trading dashboard with FastAPI backend, PostgreSQL database, and technical analysis features. The system consists of:
|
||||||
|
- Backend: FastAPI (Python 3.9+)
|
||||||
|
- Frontend: HTML/JS dashboard with lightweight-charts
|
||||||
|
- Database: PostgreSQL (TimescaleDB optimized)
|
||||||
|
- Features: Real-time candle data, technical indicators (SMA, EMA, RSI, MACD, Bollinger Bands), trading strategy simulation, backtesting
|
||||||
|
|
||||||
|
## Build/Lint/Test Commands
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
```bash
|
||||||
|
# Create and activate virtual environment
|
||||||
|
python -m venv venv
|
||||||
|
venv\Scripts\activate # Windows
|
||||||
|
# or source venv/bin/activate # Linux/Mac
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Development Server
|
||||||
|
```bash
|
||||||
|
# Quick start (Windows)
|
||||||
|
start_dev.cmd
|
||||||
|
|
||||||
|
# Quick start (Linux/Mac)
|
||||||
|
chmod +x start_dev.sh
|
||||||
|
./start_dev.sh
|
||||||
|
|
||||||
|
# Manual start
|
||||||
|
uvicorn src.api.server:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Test database connection
|
||||||
|
python test_db.py
|
||||||
|
|
||||||
|
# Run single test (no existing test framework found but for any future tests)
|
||||||
|
python -m pytest <test_file>.py::test_<function_name> -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
Environment variables in `.env` file:
|
||||||
|
```
|
||||||
|
DB_HOST=20.20.20.20
|
||||||
|
DB_PORT=5433
|
||||||
|
DB_NAME=btc_data
|
||||||
|
DB_USER=btc_bot
|
||||||
|
DB_PASSWORD=your_password
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Python Standards
|
||||||
|
- Follow PEP 8 style guide
|
||||||
|
- Use type hints consistently throughout
|
||||||
|
- Module names should be lowercase with underscores
|
||||||
|
- Class names should use PascalCase
|
||||||
|
- Function and variable names should use snake_case
|
||||||
|
- Constants should use UPPER_CASE
|
||||||
|
- All functions should have docstrings
|
||||||
|
- Use meaningful variable names (avoid single letter names except for loop counters)
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
- Group imports in order: standard library, third-party, local
|
||||||
|
- Use relative imports for internal modules
|
||||||
|
- Sort imports alphabetically within each group
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Use explicit exception handling with specific exceptions
|
||||||
|
- Log errors with appropriate context
|
||||||
|
- Don't suppress exceptions silently
|
||||||
|
- Use try/except/finally blocks for resource management
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- Classes: PascalCase
|
||||||
|
- Functions and variables: snake_case
|
||||||
|
- Constants: UPPER_CASE
|
||||||
|
- Private methods: _private_method
|
||||||
|
- Protected attributes: _protected_attribute
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- All public functions should have docstrings in Google style format
|
||||||
|
- Class docstrings should explain the class purpose and usage
|
||||||
|
- Complex logic should be commented appropriately
|
||||||
|
- API endpoints should be documented in docstrings
|
||||||
|
- Use inline comments for complex operations
|
||||||
|
|
||||||
|
### Data Processing
|
||||||
|
- Use async/await for database operations
|
||||||
|
- Handle database connection pooling properly
|
||||||
|
- Validate incoming data before processing
|
||||||
|
- Use pydantic models for data validation
|
||||||
|
- Ensure proper timezone handling for datetime operations
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Never log sensitive information (passwords, tokens)
|
||||||
|
- Use environment variables for configuration
|
||||||
|
- Validate all input data
|
||||||
|
- Use prepared statements for database queries to prevent injection
|
||||||
|
|
||||||
|
### Asynchronous Programming
|
||||||
|
- Use asyncio for concurrent database operations
|
||||||
|
- Use async context managers for resource management
|
||||||
|
- Implement timeouts for database operations
|
||||||
|
- Handle task cancellation appropriately
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- Use pydantic-settings for configuration management
|
||||||
|
- Load environment variables with python-dotenv
|
||||||
|
- Provide default values for configuration settings
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Use logging module with appropriate log levels (DEBUG, INFO, WARNING, ERROR)
|
||||||
|
- Include contextual information in log messages
|
||||||
|
- Use structured logging where appropriate
|
||||||
|
- Log exceptions with traceback information
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Write unit tests for core components
|
||||||
|
- Test database operations asynchronously
|
||||||
|
- Mock external services where appropriate
|
||||||
|
- Test both success and failure cases
|
||||||
|
- Ensure tests are isolated
|
||||||
|
|
||||||
|
## AI Coding Agent Rules
|
||||||
|
|
||||||
|
### File Structure and Organization
|
||||||
|
- Organize code into logical modules: api, data_collector, strategies, etc.
|
||||||
|
- Use consistent naming across the codebase
|
||||||
|
- Follow existing project conventions when adding new features
|
||||||
|
- Place new code in corresponding directories (src/strategies/ for strategies)
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- Maintain clean, readable code
|
||||||
|
- Write efficient code with good performance characteristics
|
||||||
|
- Follow existing code patterns for consistency
|
||||||
|
- Ensure proper error handling in all code paths
|
||||||
|
- Use type hints and validate with mypy when applicable
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Update docstrings when modifying functions or classes
|
||||||
|
- Add usage comments for complex logic
|
||||||
|
- Update README.md if adding major new features
|
||||||
|
- Document any new environment variables or configuration options
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
- Respect existing patterns for API endpoints and database access
|
||||||
|
- Follow established data flow patterns
|
||||||
|
- Ensure compatibility with existing code when making changes
|
||||||
|
- Maintain backward compatibility for public APIs
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Only add dependencies to requirements.txt when necessary
|
||||||
|
- Check for conflicts with existing dependencies
|
||||||
|
- Keep dependency versions pinned to avoid breaking changes
|
||||||
|
- Avoid adding heavyweight dependencies unless truly required
|
||||||
@ -715,13 +715,16 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ta-signal {
|
.ta-signal {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-top: 8px;
|
margin-top: 4px;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ta-signal.buy {
|
.ta-signal.buy {
|
||||||
@ -1270,11 +1273,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.ta-content {
|
.ta-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Signal Styles */
|
||||||
|
.ta-summary-badge {
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-summary-badge.buy {
|
||||||
|
background: rgba(38, 166, 154, 0.2);
|
||||||
|
color: var(--tv-green);
|
||||||
|
border-color: var(--tv-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-summary-badge.sell {
|
||||||
|
background: rgba(239, 83, 80, 0.2);
|
||||||
|
color: var(--tv-red);
|
||||||
|
border-color: var(--tv-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-summary-badge.hold {
|
||||||
|
background: rgba(120, 123, 134, 0.2);
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
border-color: var(--tv-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-strength-fill {
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="./css/indicators-new.css">
|
<link rel="stylesheet" href="./css/indicators-new.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { INTERVALS, COLORS } from '../core/index.js';
|
import { INTERVALS, COLORS } from '../core/index.js';
|
||||||
|
import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals-calculator.js';
|
||||||
|
|
||||||
export class TradingDashboard {
|
export class TradingDashboard {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.chart = null;
|
this.chart = null;
|
||||||
this.candleSeries = null;
|
this.candleSeries = null;
|
||||||
this.currentInterval = '1d';
|
this.currentInterval = '1d';
|
||||||
@ -10,6 +11,8 @@ export class TradingDashboard {
|
|||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.hasInitialLoad = false;
|
this.hasInitialLoad = false;
|
||||||
this.taData = null;
|
this.taData = null;
|
||||||
|
this.indicatorSignals = [];
|
||||||
|
this.summarySignal = null;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
@ -286,7 +289,7 @@ export class TradingDashboard {
|
|||||||
|
|
||||||
async loadInitialData() {
|
async loadInitialData() {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.loadData(1000, true),
|
this.loadData(2000, true),
|
||||||
this.loadStats()
|
this.loadStats()
|
||||||
]);
|
]);
|
||||||
this.hasInitialLoad = true;
|
this.hasInitialLoad = true;
|
||||||
@ -340,14 +343,14 @@ if (data.candles && data.candles.length > 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadNewData() {
|
async loadNewData() {
|
||||||
if (!this.hasInitialLoad || this.isLoading) return;
|
if (!this.hasInitialLoad || this.isLoading) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`);
|
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.candles && data.candles.length > 0) {
|
if (data.candles && data.candles.length > 0) {
|
||||||
const atEdge = this.isAtRightEdge();
|
const atEdge = this.isAtRightEdge();
|
||||||
|
|
||||||
const currentSeriesData = this.candleSeries.data();
|
const currentSeriesData = this.candleSeries.data();
|
||||||
@ -373,6 +376,8 @@ if (data.candles && data.candles.length > 0) {
|
|||||||
const existingData = this.allData.get(this.currentInterval) || [];
|
const existingData = this.allData.get(this.currentInterval) || [];
|
||||||
this.allData.set(this.currentInterval, this.mergeData(existingData, chartData));
|
this.allData.set(this.currentInterval, this.mergeData(existingData, chartData));
|
||||||
|
|
||||||
|
console.log(`[NewData Load] Added ${chartData.length} new candles, total in dataset: ${this.allData.get(this.currentInterval).length}`);
|
||||||
|
|
||||||
if (atEdge) {
|
if (atEdge) {
|
||||||
this.chart.timeScale().scrollToRealTime();
|
this.chart.timeScale().scrollToRealTime();
|
||||||
}
|
}
|
||||||
@ -380,10 +385,11 @@ if (data.candles && data.candles.length > 0) {
|
|||||||
const latest = chartData[chartData.length - 1];
|
const latest = chartData[chartData.length - 1];
|
||||||
this.updateStats(latest);
|
this.updateStats(latest);
|
||||||
|
|
||||||
// Redraw indicators when new data loads
|
// Recalculate indicators and signals when new data loads
|
||||||
if (window.drawIndicatorsOnChart) {
|
if (window.drawIndicatorsOnChart) {
|
||||||
window.drawIndicatorsOnChart();
|
window.drawIndicatorsOnChart();
|
||||||
}
|
}
|
||||||
|
await this.loadSignals();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading new data:', error);
|
console.error('Error loading new data:', error);
|
||||||
@ -397,7 +403,7 @@ if (data.candles && data.candles.length > 0) {
|
|||||||
return Array.from(dataMap.values()).sort((a, b) => a.time - b.time);
|
return Array.from(dataMap.values()).sort((a, b) => a.time - b.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleRangeChange() {
|
onVisibleRangeChange() {
|
||||||
if (!this.hasInitialLoad || this.isLoading) {
|
if (!this.hasInitialLoad || this.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -408,6 +414,8 @@ if (data.candles && data.candles.length > 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = this.candleSeries.data();
|
const data = this.candleSeries.data();
|
||||||
|
const allData = this.allData.get(this.currentInterval);
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -416,22 +424,37 @@ if (data.candles && data.candles.length > 0) {
|
|||||||
const bufferSize = visibleBars * 2;
|
const bufferSize = visibleBars * 2;
|
||||||
const refillThreshold = bufferSize * 0.8;
|
const refillThreshold = bufferSize * 0.8;
|
||||||
const barsFromLeft = Math.floor(visibleRange.from);
|
const barsFromLeft = Math.floor(visibleRange.from);
|
||||||
|
const visibleOldestTime = data[Math.floor(visibleRange.from)]?.time;
|
||||||
|
const visibleNewestTime = data[Math.ceil(visibleRange.to)]?.time;
|
||||||
|
|
||||||
|
console.log(`[VisibleRange] Visible: ${visibleBars} bars (${data.length} in chart, ${allData?.length || 0} in dataset)`);
|
||||||
|
console.log(`[VisibleRange] Time range: ${new Date((visibleOldestTime || 0) * 1000).toLocaleDateString()} to ${new Date((visibleNewestTime || 0) * 1000).toLocaleDateString()}`);
|
||||||
|
|
||||||
if (barsFromLeft < refillThreshold) {
|
if (barsFromLeft < refillThreshold) {
|
||||||
console.log(`Buffer low (${barsFromLeft} < ${refillThreshold.toFixed(0)}), silently prefetching ${bufferSize} candles...`);
|
console.log(`Buffer low (${barsFromLeft} < ${refillThreshold.toFixed(0)}), prefetching ${bufferSize} candles...`);
|
||||||
const oldestCandle = data[0];
|
const oldestCandle = data[0];
|
||||||
if (oldestCandle) {
|
if (oldestCandle) {
|
||||||
this.loadHistoricalData(oldestCandle.time, bufferSize);
|
this.loadHistoricalData(oldestCandle.time, bufferSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recalculate indicators when data changes
|
||||||
|
if (data.length !== allData?.length) {
|
||||||
|
console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.drawIndicatorsOnChart?.();
|
||||||
|
this.loadSignals().catch(e => console.error('Error loading signals:', e));
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadHistoricalData(beforeTime, limit = 1000) {
|
async loadHistoricalData(beforeTime, limit = 1000) {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
|
console.log(`[Historical] Loading historical data before ${new Date(beforeTime * 1000).toLocaleDateString()}, limit=${limit}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const endTime = new Date((beforeTime - 1) * 1000);
|
const endTime = new Date((beforeTime - 1) * 1000);
|
||||||
|
|
||||||
@ -459,17 +482,24 @@ if (data.candles && data.candles.length > 0) {
|
|||||||
const mergedData = this.mergeData(existingData, chartData);
|
const mergedData = this.mergeData(existingData, chartData);
|
||||||
this.allData.set(this.currentInterval, mergedData);
|
this.allData.set(this.currentInterval, mergedData);
|
||||||
|
|
||||||
|
console.log(`[Historical] SUCCESS: Added ${chartData.length} candles`);
|
||||||
|
console.log(`[Historical] Total candles in dataset: ${mergedData.length}`);
|
||||||
|
console.log(`[Historical] Oldest: ${new Date(mergedData[0]?.time * 1000).toLocaleDateString()}`);
|
||||||
|
console.log(`[Historical] Newest: ${new Date(mergedData[mergedData.length - 1]?.time * 1000).toLocaleDateString()}`);
|
||||||
|
|
||||||
this.candleSeries.setData(mergedData);
|
this.candleSeries.setData(mergedData);
|
||||||
|
|
||||||
// Recalculate indicators with the expanded dataset
|
// Recalculate indicators and signals with the expanded dataset
|
||||||
|
console.log(`[Historical] Recalculating indicators...`);
|
||||||
window.drawIndicatorsOnChart?.();
|
window.drawIndicatorsOnChart?.();
|
||||||
|
await this.loadSignals();
|
||||||
|
|
||||||
console.log(`Prefetched ${chartData.length} candles, total: ${mergedData.length}`);
|
console.log(`[Historical] Indicators recalculated for ${mergedData.length} candles`);
|
||||||
} else {
|
} else {
|
||||||
console.log('No more historical data available');
|
console.log('[Historical] No more historical data available from database');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading historical data:', error);
|
console.error('[Historical] Error loading historical data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
@ -492,6 +522,7 @@ async loadTA() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.taData = data;
|
this.taData = data;
|
||||||
|
await this.loadSignals();
|
||||||
this.renderTA();
|
this.renderTA();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading TA:', error);
|
console.error('Error loading TA:', error);
|
||||||
@ -499,6 +530,17 @@ async loadTA() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadSignals() {
|
||||||
|
try {
|
||||||
|
this.indicatorSignals = calculateAllIndicatorSignals();
|
||||||
|
this.summarySignal = calculateSummarySignal(this.indicatorSignals);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading signals:', error);
|
||||||
|
this.indicatorSignals = [];
|
||||||
|
this.summarySignal = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderTA() {
|
renderTA() {
|
||||||
if (!this.taData || this.taData.error) {
|
if (!this.taData || this.taData.error) {
|
||||||
document.getElementById('taContent').innerHTML = `<div class="ta-error">${this.taData?.error || 'No data available'}</div>`;
|
document.getElementById('taContent').innerHTML = `<div class="ta-error">${this.taData?.error || 'No data available'}</div>`;
|
||||||
@ -515,14 +557,35 @@ renderTA() {
|
|||||||
document.getElementById('taInterval').textContent = this.currentInterval.toUpperCase();
|
document.getElementById('taInterval').textContent = this.currentInterval.toUpperCase();
|
||||||
document.getElementById('taLastUpdate').textContent = new Date().toLocaleTimeString();
|
document.getElementById('taLastUpdate').textContent = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
const summary = this.summarySignal || {};
|
||||||
|
const summarySignalClass = summary.signal || 'hold';
|
||||||
|
|
||||||
|
const signalsHtml = this.indicatorSignals?.length > 0 ? this.indicatorSignals.map(indSignal => {
|
||||||
|
const signalIcon = indSignal.signal === 'buy' ? '🟢' : indSignal.signal === 'sell' ? '🔴' : '🟡';
|
||||||
|
const signalClass = indSignal.signal || 'hold';
|
||||||
|
const valueStr = indSignal.value !== null && indSignal.value !== undefined ? indSignal.value.toFixed(2) : 'N/A';
|
||||||
|
const indicatorConfig = indSignal.params ? `\nConfiguration: ${indSignal.params}` : '';
|
||||||
|
const tooltipText = `Value: ${valueStr}${indicatorConfig}\n\n${indSignal.reasoning}`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="ta-ma-row" style="border-bottom: none; padding: 4px 0; cursor: help;">
|
||||||
|
<span class="ta-ma-label" title="${tooltipText}">${indSignal.name}</span>
|
||||||
|
<span class="ta-ma-value" style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span class="ta-signal ${signalClass}" style="font-size: 11px; padding: 2px 8px; min-width: 60px; text-align: center;" title="${tooltipText}">${signalIcon} ${indSignal.signal.toUpperCase()}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('') : '';
|
||||||
|
|
||||||
|
const summaryBadge = '';
|
||||||
|
|
||||||
document.getElementById('taContent').innerHTML = `
|
document.getElementById('taContent').innerHTML = `
|
||||||
<div class="ta-section">
|
<div class="ta-section">
|
||||||
<div class="ta-section-title">Trend Analysis</div>
|
<div class="ta-section-title">
|
||||||
<div class="ta-trend ${trendClass}">
|
Indicator Analysis
|
||||||
${data.trend.direction} ${trendClass === 'bullish' ? '↑' : trendClass === 'bearish' ? '↓' : '→'}
|
${summaryBadge}
|
||||||
</div>
|
</div>
|
||||||
<div class="ta-strength">${data.trend.strength}</div>
|
${signalsHtml ? signalsHtml : `<div style="padding: 8px 0; color: var(--tv-text-secondary); font-size: 12px;">No indicators selected. Add indicators from the sidebar panel to view signals.</div>`}
|
||||||
<span class="ta-signal ${signalClass}">${data.trend.signal}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ta-section">
|
<div class="ta-section">
|
||||||
@ -542,9 +605,35 @@ renderTA() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="ta-section">
|
||||||
|
<div class="ta-section-title">Support / Resistance</div>
|
||||||
|
<div class="ta-level">
|
||||||
|
<span class="ta-level-label">Resistance</span>
|
||||||
|
<span class="ta-level-value">${data.levels.resistance.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ta-level">
|
||||||
|
<span class="ta-level-label">Support</span>
|
||||||
|
<span class="ta-level-value">${data.levels.support.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ta-section">
|
||||||
|
<div class="ta-section-title">Price Position</div>
|
||||||
|
<div class="ta-position-bar">
|
||||||
|
<div class="ta-position-marker" style="left: ${Math.min(Math.max(data.levels.position_in_range, 5), 95)}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="ta-strength" style="margin-top: 8px; font-size: 11px;">
|
||||||
|
${data.levels.position_in_range.toFixed(0)}% in range
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSignalsSection() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
async loadStats() {
|
async loadStats() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/stats?symbol=BTC');
|
const response = await fetch('/api/v1/stats?symbol=BTC');
|
||||||
|
|||||||
@ -101,6 +101,8 @@ export function setActiveIndicators(indicators) {
|
|||||||
renderIndicatorPanel();
|
renderIndicatorPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.getActiveIndicators = getActiveIndicators;
|
||||||
|
|
||||||
// Render main panel
|
// Render main panel
|
||||||
export function renderIndicatorPanel() {
|
export function renderIndicatorPanel() {
|
||||||
const container = document.getElementById('indicatorPanel');
|
const container = document.getElementById('indicatorPanel');
|
||||||
@ -659,8 +661,20 @@ function saveUserPresets() {
|
|||||||
|
|
||||||
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
|
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
|
||||||
// Recalculate with current TF candles
|
// Recalculate with current TF candles
|
||||||
|
console.log(`[renderIndicatorOnPane] ${indicator.name}: START`);
|
||||||
|
console.log(`[renderIndicatorOnPane] ${indicator.name}: Input candles = ${candles.length}`);
|
||||||
|
console.log(`[renderIndicatorOnPane] ${indicator.name}: Calling instance.calculate()...`);
|
||||||
|
|
||||||
const results = instance.calculate(candles);
|
const results = instance.calculate(candles);
|
||||||
|
|
||||||
|
console.log(`[renderIndicatorOnPane] ${indicator.name}: calculate() returned ${results?.length || 0} results`);
|
||||||
|
console.log(`[renderIndicatorOnPane] ${indicator.name}: Expected ${candles.length} results, got ${results?.length || 0}`);
|
||||||
|
|
||||||
|
if (results.length !== candles.length) {
|
||||||
|
console.error(`[renderIndicatorOnPane] ${indicator.name}: MISMATCH! Expected ${candles.length} results but got ${results.length}`);
|
||||||
|
console.error(`[renderIndicatorOnPane] ${indicator.name}: This means instance.calculate() is not returning the correct number of results!`);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear previous series for this indicator
|
// Clear previous series for this indicator
|
||||||
if (indicator.series && indicator.series.length > 0) {
|
if (indicator.series && indicator.series.length > 0) {
|
||||||
indicator.series.forEach(s => {
|
indicator.series.forEach(s => {
|
||||||
@ -678,6 +692,8 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||||
|
|
||||||
let plotsCreated = 0;
|
let plotsCreated = 0;
|
||||||
|
let dataPointsAdded = 0;
|
||||||
|
|
||||||
meta.plots.forEach((plot, plotIdx) => {
|
meta.plots.forEach((plot, plotIdx) => {
|
||||||
if (isObjectResult) {
|
if (isObjectResult) {
|
||||||
const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null);
|
const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null);
|
||||||
@ -687,6 +703,8 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||||
|
|
||||||
const data = [];
|
const data = [];
|
||||||
|
let firstDataIndex = -1;
|
||||||
|
|
||||||
for (let i = 0; i < candles.length; i++) {
|
for (let i = 0; i < candles.length; i++) {
|
||||||
let value;
|
let value;
|
||||||
if (isObjectResult) {
|
if (isObjectResult) {
|
||||||
@ -696,6 +714,9 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (value !== null && value !== undefined) {
|
if (value !== null && value !== undefined) {
|
||||||
|
if (firstDataIndex === -1) {
|
||||||
|
firstDataIndex = i;
|
||||||
|
}
|
||||||
data.push({
|
data.push({
|
||||||
time: candles[i].time,
|
time: candles[i].time,
|
||||||
value: value
|
value: value
|
||||||
@ -703,7 +724,14 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.length === 0) return;
|
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: ${data.length} data points created, first data at index ${firstDataIndex}/${candles.length}`);
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: No data to render`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: Creating series with ${data.length} data points [${data[0].time} to ${data[data.length - 1].time}]`);
|
||||||
|
|
||||||
let series;
|
let series;
|
||||||
let plotLineStyle = lineStyle;
|
let plotLineStyle = lineStyle;
|
||||||
@ -792,12 +820,26 @@ export function drawIndicatorsOnChart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentInterval = window.dashboard.currentInterval;
|
const currentInterval = window.dashboard.currentInterval;
|
||||||
const candles = window.dashboard.allData.get(currentInterval);
|
const candles = window.dashboard?.allData?.get(currentInterval);
|
||||||
|
|
||||||
if (!candles || candles.length === 0) {
|
if (!candles || candles.length === 0) {
|
||||||
|
console.log('[Indicators] No candles available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[Indicators] ========== drawIndicatorsOnChart START ==========`);
|
||||||
|
console.log(`[Indicators] Candles from allData: ${candles.length}`);
|
||||||
|
console.log(`[Indicators] First candle time: ${candles[0]?.time} (${new Date(candles[0]?.time * 1000).toLocaleDateString()})`);
|
||||||
|
console.log(`[Indicators] Last candle time: ${candles[candles.length - 1]?.time} (${new Date(candles[candles.length - 1]?.time * 1000).toLocaleDateString()})`);
|
||||||
|
|
||||||
|
const oldestTime = candles[0]?.time;
|
||||||
|
const newestTime = candles[candles.length - 1]?.time;
|
||||||
|
const oldestDate = oldestTime ? new Date(oldestTime * 1000).toLocaleDateString() : 'N/A';
|
||||||
|
const newestDate = newestTime ? new Date(newestTime * 1000).toLocaleDateString() : 'N/A';
|
||||||
|
|
||||||
|
console.log(`[Indicators] ========== Redrawing ==========`);
|
||||||
|
console.log(`[Indicators] Candles: ${candles.length} | Time range: ${oldestDate} (${oldestTime}) to ${newestDate} (${newestTime})`);
|
||||||
|
|
||||||
// Remove all existing series
|
// Remove all existing series
|
||||||
activeIndicators.forEach(ind => {
|
activeIndicators.forEach(ind => {
|
||||||
ind.series?.forEach(s => {
|
ind.series?.forEach(s => {
|
||||||
@ -827,9 +869,18 @@ export function drawIndicatorsOnChart() {
|
|||||||
const IndicatorClass = IR?.[ind.type];
|
const IndicatorClass = IR?.[ind.type];
|
||||||
if (!IndicatorClass) return;
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
const instance = new IndicatorClass({ type: ind.type, params: ind.params, name: ind.name });
|
const instance = new IndicatorClass(ind);
|
||||||
const meta = instance.getMetadata();
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
// Store calculated results and metadata for signal calculation
|
||||||
|
const results = instance.calculate(candles);
|
||||||
|
ind.cachedResults = results;
|
||||||
|
ind.cachedMeta = meta;
|
||||||
|
|
||||||
|
const validResults = results.filter(r => r !== null && r !== undefined);
|
||||||
|
const warmupPeriod = ind.params?.period || 44;
|
||||||
|
console.log(`[Indicators] ${ind.name}: ${validResults.length} valid results (need ${warmupPeriod} candles warmup)`);
|
||||||
|
|
||||||
if (meta.displayMode === 'pane') {
|
if (meta.displayMode === 'pane') {
|
||||||
paneIndicators.push({ indicator: ind, meta, instance });
|
paneIndicators.push({ indicator: ind, meta, instance });
|
||||||
} else {
|
} else {
|
||||||
@ -844,21 +895,32 @@ export function drawIndicatorsOnChart() {
|
|||||||
|
|
||||||
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
|
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
|
||||||
|
|
||||||
|
console.log(`[Indicators] ========== Rendering Indicators ==========`);
|
||||||
|
console.log(`[Indicators] Input candles: ${candles.length} | Panel count: ${totalPanes}`);
|
||||||
|
|
||||||
overlayIndicators.forEach(({ indicator, meta, instance }) => {
|
overlayIndicators.forEach(({ indicator, meta, instance }) => {
|
||||||
|
console.log(`[Indicators] Processing overlay: ${indicator.name}`);
|
||||||
|
console.log(`[Indicators] Before renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
|
||||||
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
|
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
|
||||||
|
console.log(`[Indicators] After renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
|
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
|
||||||
const paneIndex = nextPaneIndex++;
|
const paneIndex = nextPaneIndex++;
|
||||||
indicatorPanes.set(indicator.id, paneIndex);
|
indicatorPanes.set(indicator.id, paneIndex);
|
||||||
|
|
||||||
|
console.log(`[Indicators] Processing pane: ${indicator.name} (pane ${paneIndex})`);
|
||||||
|
console.log(`[Indicators] Before renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
|
||||||
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
|
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
|
||||||
|
console.log(`[Indicators] After renderIndicatorOnPane: indicator.cachedResults length = ${indicator.cachedResults?.length || 0}`);
|
||||||
|
|
||||||
const pane = window.dashboard.chart.panes()[paneIndex];
|
const pane = window.dashboard.chart.panes()[paneIndex];
|
||||||
if (pane) {
|
if (pane) {
|
||||||
pane.setHeight(paneHeight);
|
pane.setHeight(paneHeight);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[Indicators] ========== drawIndicatorsOnChart END ==========`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetIndicator(id) {
|
function resetIndicator(id) {
|
||||||
|
|||||||
499
src/api/dashboard/static/js/ui/signals-calculator.js
Normal file
499
src/api/dashboard/static/js/ui/signals-calculator.js
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
// Signal Calculator for Technical Indicators
|
||||||
|
// Calculates buy/hold/sell signals for all active indicators
|
||||||
|
|
||||||
|
import { IndicatorRegistry as IR } from '../indicators/index.js';
|
||||||
|
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate signal for a single indicator
|
||||||
|
* @param {Object} indicator - Indicator object with type, params, etc.
|
||||||
|
* @param {Array} candles - Recent candle data
|
||||||
|
* @param {Object} indicatorValues - Computed indicator values for last candle
|
||||||
|
* @returns {Object} Signal object with type, strength, value, reasoning
|
||||||
|
*/
|
||||||
|
function calculateIndicatorSignal(indicator, candles, indicatorValues) {
|
||||||
|
const lastCandle = candles[candles.length - 1];
|
||||||
|
const prevCandle = candles[candles.length - 2];
|
||||||
|
|
||||||
|
console.log('[calculateIndicatorSignal] Type:', indicator.type, 'Values:', indicatorValues, 'LastCandle:', lastCandle?.close);
|
||||||
|
|
||||||
|
if (!lastCandle) {
|
||||||
|
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No data' };
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (indicator.type) {
|
||||||
|
case 'sma':
|
||||||
|
case 'ema':
|
||||||
|
case 'ma':
|
||||||
|
return calculateMASignal(indicator, lastCandle, prevCandle, indicatorValues);
|
||||||
|
case 'rsi':
|
||||||
|
return calculateRSISignal(indicator, lastCandle, indicatorValues);
|
||||||
|
case 'macd':
|
||||||
|
return calculateMACDSignal(indicator, lastCandle, prevCandle, indicatorValues);
|
||||||
|
case 'stoch':
|
||||||
|
return calculateStochSignal(indicator, lastCandle, prevCandle, indicatorValues);
|
||||||
|
case 'bb':
|
||||||
|
return calculateBollingerBandsSignal(indicator, lastCandle, indicatorValues);
|
||||||
|
case 'sma':
|
||||||
|
case 'ema':
|
||||||
|
return calculateMASignal(indicator, lastCandle, prevCandle, indicatorValues);
|
||||||
|
case 'atr':
|
||||||
|
return calculateATRSignal(indicator, indicatorValues);
|
||||||
|
case 'hts':
|
||||||
|
return calculateHTSSignal(indicator, lastCandle, prevCandle, indicatorValues);
|
||||||
|
default:
|
||||||
|
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'Unknown indicator type' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSI Signal Calculation
|
||||||
|
*/
|
||||||
|
function calculateRSISignal(indicator, lastCandle, indicatorValues) {
|
||||||
|
const rsi = indicatorValues?.rsi;
|
||||||
|
const overbought = indicator.params.overbought || 70;
|
||||||
|
const oversold = indicator.params.oversold || 30;
|
||||||
|
|
||||||
|
if (rsi === null || rsi === undefined) {
|
||||||
|
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No RSI data' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let signal, strength, reasoning;
|
||||||
|
|
||||||
|
if (rsi <= oversold) {
|
||||||
|
signal = SIGNAL_TYPES.BUY;
|
||||||
|
strength = 80 + Math.min((oversold - rsi) * 0.5, 20);
|
||||||
|
reasoning = `RSI ${rsi.toFixed(1)} is extremely oversold (${oversold}), suggesting the price may be approaching a bottom and potential rebound`;
|
||||||
|
} else if (rsi >= overbought) {
|
||||||
|
signal = SIGNAL_TYPES.SELL;
|
||||||
|
strength = 80 + Math.min((rsi - overbought) * 0.5, 20);
|
||||||
|
reasoning = `RSI ${rsi.toFixed(1)} is overbought (${overbought}), indicating the asset may be overvalued due for a correction`;
|
||||||
|
} else if (rsi < 50) {
|
||||||
|
signal = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 30;
|
||||||
|
reasoning = `RSI ${rsi.toFixed(1)} shows bearish momentum below 50, sellers currently in control`;
|
||||||
|
} else if (rsi > 50) {
|
||||||
|
signal = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 30;
|
||||||
|
reasoning = `RSI ${rsi.toFixed(1)} shows bullish momentum above 50, buyers currently in control`;
|
||||||
|
} else {
|
||||||
|
signal = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 0;
|
||||||
|
reasoning = 'RSI at 50 indicates neutral market conditions with balanced buying/selling pressure';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: signal, strength, value: rsi, reasoning };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MACD Signal Calculation
|
||||||
|
*/
|
||||||
|
function calculateMACDSignal(indicator, lastCandle, prevCandle, values) {
|
||||||
|
const macd = values?.macd;
|
||||||
|
const signalLine = values?.signal;
|
||||||
|
const histogram = values?.histogram;
|
||||||
|
|
||||||
|
if (macd === null || signalLine === null) {
|
||||||
|
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No MACD data' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let macdSignal, strength, reasoning;
|
||||||
|
|
||||||
|
if (macd > signalLine && histogram > 0) {
|
||||||
|
macdSignal = SIGNAL_TYPES.BUY;
|
||||||
|
strength = 75 + Math.min((macd - signalLine) * 10, 25);
|
||||||
|
reasoning = `MACD (${macd.toFixed(2)}) is above signal line (${signalLine.toFixed(2)}) with positive histogram (${histogram.toFixed(2)}), indicating strong bullish momentum`;
|
||||||
|
} else if (macd < signalLine && histogram < 0) {
|
||||||
|
macdSignal = SIGNAL_TYPES.SELL;
|
||||||
|
strength = 75 + Math.min((signalLine - macd) * 10, 25);
|
||||||
|
reasoning = `MACD (${macd.toFixed(2)}) is below signal line (${signalLine.toFixed(2)}) with negative histogram (${histogram.toFixed(2)}), indicating strong bearish momentum`;
|
||||||
|
} else if (macd > 0 && signalLine < 0) {
|
||||||
|
macdSignal = SIGNAL_TYPES.BUY;
|
||||||
|
strength = 85;
|
||||||
|
reasoning = `Bullish crossover: MACD (${macd.toFixed(2)}) crossed above zero while signal (${signalLine.toFixed(2)}) is still negative, potential trend reversal upward`;
|
||||||
|
} else if (macd < 0 && signalLine > 0) {
|
||||||
|
macdSignal = SIGNAL_TYPES.SELL;
|
||||||
|
strength = 85;
|
||||||
|
reasoning = `Bearish crossover: MACD (${macd.toFixed(2)}) crossed below zero while signal (${signalLine.toFixed(2)}) is still positive, potential trend reversal downward`;
|
||||||
|
} else {
|
||||||
|
macdSignal = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 30;
|
||||||
|
reasoning = `MACD (${macd.toFixed(2)}) and signal (${signalLine.toFixed(2)}) are close together with no clear directional bias`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: macdSignal, strength, value: histogram, reasoning };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stochastic Signal Calculation
|
||||||
|
*/
|
||||||
|
function calculateStochSignal(indicator, lastCandle, prevCandle, values) {
|
||||||
|
const k = values?.k;
|
||||||
|
const d = values?.d;
|
||||||
|
|
||||||
|
if (k === null || d === null) {
|
||||||
|
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No Stochastic data' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevK = prevCandle?.values?.k;
|
||||||
|
|
||||||
|
let signalType, strength, reasoning;
|
||||||
|
|
||||||
|
if (k < 20 && prevK < 20 && k > d) {
|
||||||
|
signalType = SIGNAL_TYPES.BUY;
|
||||||
|
strength = 80;
|
||||||
|
reasoning = `Strong buy signal: %K (${k.toFixed(1)}) crossed above %D (${d.toFixed(1)}) in oversold territory (<20), likely upward reversal`;
|
||||||
|
} else if (k > 80 && prevK > 80 && k < d) {
|
||||||
|
signalType = SIGNAL_TYPES.SELL;
|
||||||
|
strength = 80;
|
||||||
|
reasoning = `Strong sell signal: %K (${k.toFixed(1)}) crossed below %D (${d.toFixed(1)}) in overbought territory (>80), likely downward reversal`;
|
||||||
|
} else if (k < 20) {
|
||||||
|
signalType = SIGNAL_TYPES.BUY;
|
||||||
|
strength = 60;
|
||||||
|
reasoning = `%K (${k.toFixed(1)}) is in oversold zone (<20), price may be near a bottom and ready to bounce`;
|
||||||
|
} else if (k > 80) {
|
||||||
|
signalType = SIGNAL_TYPES.SELL;
|
||||||
|
strength = 60;
|
||||||
|
reasoning = `%K (${k.toFixed(1)}) is in overbought zone (>80), price may be overextended and ready for correction`;
|
||||||
|
} else {
|
||||||
|
signalType = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 30;
|
||||||
|
reasoning = `Stochastic (${k.toFixed(1)}) is in neutral range (20-80) with no clear directional signal`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: signalType, strength, value: k, reasoning };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bollinger Bands Signal Calculation
|
||||||
|
*/
|
||||||
|
function calculateBollingerBandsSignal(indicator, lastCandle, values) {
|
||||||
|
const upper = values?.upper;
|
||||||
|
const lower = values?.lower;
|
||||||
|
const middle = values?.middle;
|
||||||
|
|
||||||
|
if (!upper || !lower || !middle) {
|
||||||
|
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No BB data' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = lastCandle.close;
|
||||||
|
const range = upper - lower;
|
||||||
|
const position = (price - lower) / range;
|
||||||
|
|
||||||
|
let signalType, strength, reasoning;
|
||||||
|
|
||||||
|
if (position <= 0.1 || price <= lower) {
|
||||||
|
signalType = SIGNAL_TYPES.BUY;
|
||||||
|
strength = Math.floor(70 + (0.1 - position) * 300);
|
||||||
|
reasoning = `Price (${price.toFixed(2)}) is at or touching the lower Bollinger Band (${lower.toFixed(2)}), potential oversold bounce opportunity`;
|
||||||
|
} else if (position >= 0.9 || price >= upper) {
|
||||||
|
signalType = SIGNAL_TYPES.SELL;
|
||||||
|
strength = Math.floor(70 + (position - 0.9) * 300);
|
||||||
|
reasoning = `Price (${price.toFixed(2)}) is at or touching the upper Bollinger Band (${upper.toFixed(2)}), potential overextended sell signal`;
|
||||||
|
} else if (middle && price > middle) {
|
||||||
|
signalType = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 40;
|
||||||
|
reasoning = `Price (${price.toFixed(2)}) is above the middle band (${middle.toFixed(2)}), generally bullish but not extreme`;
|
||||||
|
} else {
|
||||||
|
signalType = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 40;
|
||||||
|
reasoning = `Price (${price.toFixed(2)}) is within normal Bollinger Band range, no extreme signals`;
|
||||||
|
}
|
||||||
|
|
||||||
|
strength = Math.min(Math.max(strength, 0), 100);
|
||||||
|
|
||||||
|
return { type: signalType, strength, value: position * 100, reasoning };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moving Average Signal Calculation (SMA/EMA)
|
||||||
|
*/
|
||||||
|
function calculateMASignal(indicator, lastCandle, prevCandle, values) {
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const ma = values?.ma;
|
||||||
|
|
||||||
|
console.log('[calculateMASignal] values:', values, 'ma:', ma, 'close:', close);
|
||||||
|
|
||||||
|
if (!ma && ma !== 0) {
|
||||||
|
console.log('[calculateMASignal] No valid MA value');
|
||||||
|
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No MA data' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
const period = indicator.params?.period || 44;
|
||||||
|
const maLabel = indicator.name || `MA (${period})`;
|
||||||
|
|
||||||
|
let signalType, strength, reasoning;
|
||||||
|
|
||||||
|
if (close > ma * 1.02) {
|
||||||
|
signalType = SIGNAL_TYPES.BUY;
|
||||||
|
strength = Math.min(60 + ((close - ma) / ma) * 500, 100);
|
||||||
|
reasoning = `Price (${close.toFixed(2)}) is strongly above ${maLabel} (${ma.toFixed(2)}), bullish trend`;
|
||||||
|
} else if (close < ma * 0.98) {
|
||||||
|
signalType = SIGNAL_TYPES.SELL;
|
||||||
|
strength = Math.min(60 + ((ma - close) / ma) * 500, 100);
|
||||||
|
reasoning = `Price (${close.toFixed(2)}) is strongly below ${maLabel} (${ma.toFixed(2)}), bearish trend`;
|
||||||
|
} else {
|
||||||
|
signalType = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 30;
|
||||||
|
reasoning = `Price (${close.toFixed(2)}) is near ${maLabel} (${ma.toFixed(2)}), sideways/consolidating`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[calculateMASignal] Result:', signalType, strength);
|
||||||
|
return { type: signalType, strength, value: close, reasoning };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ATR Signal Calculation
|
||||||
|
*/
|
||||||
|
function calculateATRSignal(indicator, values) {
|
||||||
|
const atr = values?.atr;
|
||||||
|
|
||||||
|
if (!atr) {
|
||||||
|
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No ATR data' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const period = indicator.params?.period || 14;
|
||||||
|
|
||||||
|
// ATR is volatility indicator, used with other signals
|
||||||
|
let signalType, strength, reasoning;
|
||||||
|
|
||||||
|
if (atr > 0) {
|
||||||
|
signalType = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = Math.min(atr * 10, 100);
|
||||||
|
|
||||||
|
if (atr > 100) {
|
||||||
|
reasoning = `High volatility detected (ATR: ${atr.toFixed(2)}), expect larger moves and wider stop-losses`;
|
||||||
|
} else if (atr > 50) {
|
||||||
|
reasoning = `Moderate volatility (ATR: ${atr.toFixed(2)}), normal market conditions`;
|
||||||
|
} else {
|
||||||
|
reasoning = `Low volatility (ATR: ${atr.toFixed(2)}), market may be consolidating`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
signalType = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 0;
|
||||||
|
reasoning = 'No volatility data available';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: signalType, strength, value: atr, reasoning };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTS (Hull Trend System) Signal Calculation
|
||||||
|
*/
|
||||||
|
function calculateHTSSignal(indicator, lastCandle, prevCandle, values) {
|
||||||
|
const fastHigh = values?.fastHigh;
|
||||||
|
const fastLow = values?.fastLow;
|
||||||
|
const slowHigh = values?.slowHigh;
|
||||||
|
const slowLow = values?.slowLow;
|
||||||
|
|
||||||
|
if (!fastHigh || !slowLow) {
|
||||||
|
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No HTS data' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = lastCandle.close;
|
||||||
|
const midpointLow = (slowHigh[slowHigh.length - 1] + slowLow[slowLow.length - 1]) / 2;
|
||||||
|
const midpointHigh = (fastHigh[fastHigh.length - 1] + fastLow[fastLow.length - 1]) / 2;
|
||||||
|
|
||||||
|
let signalType, strength, reasoning;
|
||||||
|
|
||||||
|
if (price > midpointHigh) {
|
||||||
|
signalType = SIGNAL_TYPES.BUY;
|
||||||
|
strength = Math.min(50 + ((price - midpointHigh) / midpointHigh) * 200, 100);
|
||||||
|
reasoning = `Price (${price.toFixed(2)}) is above the fast channel (${midpointHigh.toFixed(2)}), strong bullish trend in place`;
|
||||||
|
} else if (price < midpointLow) {
|
||||||
|
signalType = SIGNAL_TYPES.SELL;
|
||||||
|
strength = Math.min(50 + ((midpointLow - price) / midpointLow) * 200, 100);
|
||||||
|
reasoning = `Price (${price.toFixed(2)}) is below the slow channel (${midpointLow.toFixed(2)}), strong bearish trend in place`;
|
||||||
|
} else if (midpointHigh > midpointLow) {
|
||||||
|
signalType = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 40;
|
||||||
|
reasoning = `Fast and slow channels are wide apart (${midpointHigh.toFixed(2)} vs ${midpointLow.toFixed(2)}), trend is established but price is in neutral zone`;
|
||||||
|
} else {
|
||||||
|
signalType = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 30;
|
||||||
|
reasoning = `Channels are close together, no clear directional trend yet`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: signalType, strength, value: price, reasoning };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate signals for all active indicators
|
||||||
|
* @returns {Array} Array of indicator signals
|
||||||
|
*/
|
||||||
|
export function calculateAllIndicatorSignals() {
|
||||||
|
const activeIndicators = window.getActiveIndicators?.() || [];
|
||||||
|
const candles = window.dashboard?.allData?.get(window.dashboard?.currentInterval);
|
||||||
|
|
||||||
|
console.log('[Signals] ========== calculateAllIndicatorSignals START ==========');
|
||||||
|
console.log('[Signals] Active indicators:', activeIndicators.length, 'Candles:', candles?.length || 0);
|
||||||
|
|
||||||
|
if (!candles || candles.length < 2) {
|
||||||
|
console.log('[Signals] Insufficient candles available:', candles?.length || 0);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activeIndicators || activeIndicators.length === 0) {
|
||||||
|
console.log('[Signals] No active indicators');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Signals] Calculating for', activeIndicators.length, 'indicators with', candles.length, 'candles');
|
||||||
|
const signals = [];
|
||||||
|
|
||||||
|
for (const indicator of activeIndicators) {
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) {
|
||||||
|
console.log('[Signals] No class for indicator type:', indicator.type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cached results if available, otherwise calculate
|
||||||
|
let results = indicator.cachedResults;
|
||||||
|
let meta = indicator.cachedMeta;
|
||||||
|
|
||||||
|
console.log(`[Signals] ${indicator.name}: indicator.cachedResults length = ${results?.length || 0}`);
|
||||||
|
|
||||||
|
if (!results || !meta || results.length !== candles.length) {
|
||||||
|
console.log(`[Signals] ${indicator.name}: Results mismatch or missing - recalculating`);
|
||||||
|
console.log(`[Signals] ${indicator.name}: candles.length=${candles.length}, results.length=${results?.length || 0}`);
|
||||||
|
const instance = new IndicatorClass(indicator);
|
||||||
|
meta = instance.getMetadata();
|
||||||
|
results = instance.calculate(candles);
|
||||||
|
console.log(`[Signals] ${indicator.name}: New results length = ${results?.length || 0}`);
|
||||||
|
indicator.cachedResults = results;
|
||||||
|
indicator.cachedMeta = meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Signals]', indicator.type, '- Results length:', results?.length, 'Last result:', results?.[results.length - 1]);
|
||||||
|
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
console.log('[Signals] No results for indicator:', indicator.type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastResult = results[results.length - 1];
|
||||||
|
if (lastResult === null || lastResult === undefined) {
|
||||||
|
console.log('[Signals] No valid last result for indicator:', indicator.type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let values;
|
||||||
|
if (typeof lastResult === 'object' && lastResult !== null && !Array.isArray(lastResult)) {
|
||||||
|
values = lastResult;
|
||||||
|
} else if (typeof lastResult === 'number') {
|
||||||
|
values = { ma: lastResult };
|
||||||
|
} else {
|
||||||
|
console.log('[Signals] Unexpected result type for', indicator.type, ':', typeof lastResult, lastResult);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indicator.type === 'sma') {
|
||||||
|
console.log('[Signals] SMA result:', lastResult, 'values:', values);
|
||||||
|
}
|
||||||
|
|
||||||
|
const signal = calculateIndicatorSignal(indicator, candles, values);
|
||||||
|
|
||||||
|
const label = indicator.type?.toUpperCase();
|
||||||
|
const params = indicator.params && typeof indicator.params === 'object'
|
||||||
|
? Object.entries(indicator.params)
|
||||||
|
.filter(([k, v]) => !k.startsWith('_') && v !== undefined && v !== null)
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join(', ')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
signals.push({
|
||||||
|
id: indicator.id,
|
||||||
|
name: meta?.name || indicator.type,
|
||||||
|
label: label,
|
||||||
|
params: params || null,
|
||||||
|
type: indicator.type,
|
||||||
|
signal: signal.type,
|
||||||
|
strength: Math.round(signal.strength),
|
||||||
|
value: signal.value,
|
||||||
|
reasoning: signal.reasoning,
|
||||||
|
color: SIGNAL_COLORS[signal.type]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Signals] ========== calculateAllIndicatorSignals END ==========');
|
||||||
|
console.log('[Signals] Total signals calculated:', signals.length);
|
||||||
|
return signals;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate aggregate summary signal from all indicators
|
||||||
|
*/
|
||||||
|
export function calculateSummarySignal(signals) {
|
||||||
|
console.log('[calculateSummarySignal] Input signals:', signals?.length);
|
||||||
|
|
||||||
|
if (!signals || signals.length === 0) {
|
||||||
|
return {
|
||||||
|
signal: SIGNAL_TYPES.HOLD,
|
||||||
|
strength: 0,
|
||||||
|
reasoning: 'No active indicators',
|
||||||
|
buyCount: 0,
|
||||||
|
sellCount: 0,
|
||||||
|
holdCount: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const buySignals = signals.filter(s => s.signal === SIGNAL_TYPES.BUY);
|
||||||
|
const sellSignals = signals.filter(s => s.signal === SIGNAL_TYPES.SELL);
|
||||||
|
const holdSignals = signals.filter(s => s.signal === SIGNAL_TYPES.HOLD);
|
||||||
|
|
||||||
|
const buyCount = buySignals.length;
|
||||||
|
const sellCount = sellSignals.length;
|
||||||
|
const holdCount = holdSignals.length;
|
||||||
|
const total = signals.length;
|
||||||
|
|
||||||
|
console.log('[calculateSummarySignal] BUY:', buyCount, 'SELL:', sellCount, 'HOLD:', holdCount);
|
||||||
|
|
||||||
|
const buyWeight = buySignals.reduce((sum, s) => sum + (s.strength || 0), 0);
|
||||||
|
const sellWeight = sellSignals.reduce((sum, s) => sum + (s.strength || 0), 0);
|
||||||
|
|
||||||
|
let summarySignal, strength, reasoning;
|
||||||
|
|
||||||
|
if (buyCount > sellCount && buyCount > holdCount) {
|
||||||
|
summarySignal = SIGNAL_TYPES.BUY;
|
||||||
|
const avgBuyStrength = buyWeight / buyCount;
|
||||||
|
strength = Math.round(avgBuyStrength * (buyCount / total));
|
||||||
|
reasoning = `${buyCount} buy signals, ${sellCount} sell, ${holdCount} hold`;
|
||||||
|
} else if (sellCount > buyCount && sellCount > holdCount) {
|
||||||
|
summarySignal = SIGNAL_TYPES.SELL;
|
||||||
|
const avgSellStrength = sellWeight / sellCount;
|
||||||
|
strength = Math.round(avgSellStrength * (sellCount / total));
|
||||||
|
reasoning = `${sellCount} sell signals, ${buyCount} buy, ${holdCount} hold`;
|
||||||
|
} else {
|
||||||
|
summarySignal = SIGNAL_TYPES.HOLD;
|
||||||
|
strength = 30;
|
||||||
|
reasoning = `Mixed signals: ${buyCount} buy, ${sellCount} sell, ${holdCount} hold`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
signal: summarySignal,
|
||||||
|
strength: Math.min(Math.max(strength, 0), 100),
|
||||||
|
reasoning,
|
||||||
|
buyCount,
|
||||||
|
sellCount,
|
||||||
|
holdCount,
|
||||||
|
color: SIGNAL_COLORS[summarySignal]
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[calculateSummarySignal] Result:', result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SIGNAL_TYPES, SIGNAL_COLORS };
|
||||||
Reference in New Issue
Block a user