feat: add Hurst Bands, strategy panel, signal markers and multiple UI enhancements
This commit is contained in:
76
GEMINI.md
Normal file
76
GEMINI.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# BTC Accumulation Bot - Context & Guidelines
|
||||||
|
|
||||||
|
High-performance crypto data collection and trading system for cbBTC on Hyperliquid, optimized for Synology NAS deployment.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
- **Purpose**: Collect 1-minute candle data for cbBTC-PERP from Hyperliquid, compute technical indicators on custom timeframes (e.g., 37m, 148m), and execute accumulation strategies.
|
||||||
|
- **Tech Stack**:
|
||||||
|
- **Backend**: Python 3.11+ (FastAPI, asyncio, websockets, asyncpg, pandas, numpy).
|
||||||
|
- **Database**: TimescaleDB (PostgreSQL 15 extension) on Synology DS218+.
|
||||||
|
- **Infrastructure**: Docker Compose (Network Mode: host for performance).
|
||||||
|
- **Frontend**: Vanilla JS dashboard for real-time monitoring and charts.
|
||||||
|
- **Architecture**:
|
||||||
|
- `DataCollector`: Main orchestrator managing WebSocket ingestion, buffering, and database writes.
|
||||||
|
- `IndicatorEngine`: Computes technical indicators (MA44, MA125) on multiple timeframes.
|
||||||
|
- `Brain`: Decision engine that evaluates signals based on indicator state.
|
||||||
|
- `CustomTimeframeGenerator`: Dynamically generates non-standard intervals from 1m base candles.
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
### Development & Deployment
|
||||||
|
- **Deployment**: Run `chmod +x scripts/deploy.sh && ./scripts/deploy.sh` to scaffold directories and start Docker services.
|
||||||
|
- **Service Control**:
|
||||||
|
- Start: `cd docker && docker-compose up -d`
|
||||||
|
- Stop: `cd docker && docker-compose down`
|
||||||
|
- Logs: `docker-compose logs -f [service_name]` (e.g., `data_collector`, `api_server`, `timescaledb`)
|
||||||
|
- **Backups**: `scripts/backup.sh` (manages PostgreSQL dumps with 7-day retention).
|
||||||
|
|
||||||
|
### Local API Server (Hybrid Setup)
|
||||||
|
To run the dashboard/API locally while the database remains on the NAS:
|
||||||
|
1. **NAS Config**: Ensure `5433:5432` mapping is active in `docker-compose.yml`.
|
||||||
|
2. **Local Environment**: Create a `.env` file locally:
|
||||||
|
```env
|
||||||
|
DB_HOST=NAS_IP_ADDRESS
|
||||||
|
DB_PORT=5433
|
||||||
|
DB_NAME=btc_data
|
||||||
|
DB_USER=btc_bot
|
||||||
|
DB_PASSWORD=YOUR_PASSWORD
|
||||||
|
```
|
||||||
|
3. **Run Locally**:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python -m uvicorn src.api.server:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- **Manual Verification**:
|
||||||
|
- API Health: `curl http://localhost:8000/api/v1/health`
|
||||||
|
- DB Status: `docker exec btc_timescale pg_isready -U btc_bot`
|
||||||
|
- **Indicator Testing**: `scripts/test_ma44_performance.py`
|
||||||
|
|
||||||
|
## Development Conventions
|
||||||
|
|
||||||
|
### Coding Standards
|
||||||
|
- **Modularity**: Keep files small (< 500 lines) and focused on a single responsibility.
|
||||||
|
- **Async First**: Use `asyncio` for all I/O bound operations (WebSockets, Database, API).
|
||||||
|
- **Type Safety**: Use Pydantic models for configuration and API responses.
|
||||||
|
- **Logging**: Use structured logging with console prefixes: `[SYSTEM]`, `[CLP]`, `[HEDGE]`, `[MONITOR]`.
|
||||||
|
- **Golden Rule (Data)**: Always read the source of truth from the blockchain or primary API (Hyperliquid) rather than relying on local calculations if possible.
|
||||||
|
- **Golden Rule (Positioning)**: CLP positions must use symmetric grid snapping.
|
||||||
|
|
||||||
|
### Database Design
|
||||||
|
- **Hypertables**: Use TimescaleDB hypertables for `candles` and `indicators` with weekly partitioning.
|
||||||
|
- **Compression**: Automatic compression is enabled for data older than 7 days to save space on NAS.
|
||||||
|
- **Gaps**: Continuous gap detection and automatic backfill from Hyperliquid REST API.
|
||||||
|
|
||||||
|
### Dashboard & API
|
||||||
|
- **FastAPI**: Main entry point in `src/api/server.py`.
|
||||||
|
- **Static Assets**: Dashboard located in `src/api/dashboard/static/`.
|
||||||
|
- **Indicator Engine**: JS-side mirror of indicators for the UI chart located in `src/api/dashboard/static/js/indicators/`.
|
||||||
|
|
||||||
|
## Project Roadmap
|
||||||
|
- **Phase 1 (Complete)**: Data collection, TimescaleDB integration, Basic API.
|
||||||
|
- **Phase 2 (In-Progress)**: Indicator Engine (SMA, EMA, RSI, MACD), Brain decision logic.
|
||||||
|
- **Phase 3 (TODO)**: Web3 execution layer (Uniswap V3 swaps on Base).
|
||||||
|
- **Phase 4 (TODO)**: Aave V3 integration for yield on collected cbBTC.
|
||||||
42
check_db.py
Normal file
42
check_db.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import asyncpg
|
||||||
|
|
||||||
|
DB_HOST = os.getenv('DB_HOST', 'localhost')
|
||||||
|
DB_PORT = int(os.getenv('DB_PORT', 5432))
|
||||||
|
DB_NAME = os.getenv('DB_NAME', 'btc_data')
|
||||||
|
DB_USER = os.getenv('DB_USER', 'btc_bot')
|
||||||
|
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
|
||||||
|
|
||||||
|
async def check_data():
|
||||||
|
conn = await asyncpg.connect(
|
||||||
|
host=DB_HOST,
|
||||||
|
port=DB_PORT,
|
||||||
|
database=DB_NAME,
|
||||||
|
user=DB_USER,
|
||||||
|
password=DB_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("Checking candle counts...")
|
||||||
|
rows = await conn.fetch("""
|
||||||
|
SELECT interval, COUNT(*), MIN(time), MAX(time)
|
||||||
|
FROM candles
|
||||||
|
GROUP BY interval
|
||||||
|
ORDER BY interval
|
||||||
|
""")
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
print(f"Interval: {row['interval']}")
|
||||||
|
print(f" Count: {row['count']}")
|
||||||
|
print(f" Min Time: {row['min']}")
|
||||||
|
print(f" Max Time: {row['max']}")
|
||||||
|
print("-" * 20)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(check_data())
|
||||||
91
scratch.js
Normal file
91
scratch.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { BaseIndicator } from './base.js';
|
||||||
|
|
||||||
|
export class RSIIndicator extends BaseIndicator {
|
||||||
|
calculate(candles) {
|
||||||
|
const period = this.params.period || 14;
|
||||||
|
|
||||||
|
// 1. Calculate RSI using RMA (Wilder's Smoothing)
|
||||||
|
let rsiValues = new Array(candles.length).fill(null);
|
||||||
|
let upSum = 0;
|
||||||
|
let downSum = 0;
|
||||||
|
const rmaAlpha = 1 / period;
|
||||||
|
|
||||||
|
for (let i = 1; i < candles.length; i++) {
|
||||||
|
const diff = candles[i].close - candles[i-1].close;
|
||||||
|
const up = diff > 0 ? diff : 0;
|
||||||
|
const down = diff < 0 ? -diff : 0;
|
||||||
|
|
||||||
|
if (i < period) {
|
||||||
|
upSum += up;
|
||||||
|
downSum += down;
|
||||||
|
} else if (i === period) {
|
||||||
|
upSum += up;
|
||||||
|
downSum += down;
|
||||||
|
const avgUp = upSum / period;
|
||||||
|
const avgDown = downSum / period;
|
||||||
|
rsiValues[i] = avgDown === 0 ? 100 : (avgUp === 0 ? 0 : 100 - (100 / (1 + avgUp / avgDown)));
|
||||||
|
upSum = avgUp; // Store for next RMA step
|
||||||
|
downSum = avgDown;
|
||||||
|
} else {
|
||||||
|
upSum = (up - upSum) * rmaAlpha + upSum;
|
||||||
|
downSum = (down - downSum) * rmaAlpha + downSum;
|
||||||
|
rsiValues[i] = downSum === 0 ? 100 : (upSum === 0 ? 0 : 100 - (100 / (1 + upSum / downSum)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine results
|
||||||
|
return rsiValues.map((rsi, i) => {
|
||||||
|
return {
|
||||||
|
paneBg: 100, // Background lightening trick
|
||||||
|
rsi: rsi,
|
||||||
|
upperBand: 70,
|
||||||
|
lowerBand: 30
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
const plots = [
|
||||||
|
// Background lightness trick (spans from 0 to 100 constantly)
|
||||||
|
// Making it darker by reducing opacity since the main background is dark.
|
||||||
|
{ id: 'paneBg', type: 'baseline', baseValue: 0, color: 'transparent',
|
||||||
|
topLineColor: 'transparent', bottomLineColor: 'transparent',
|
||||||
|
topFillColor1: 'rgba(255, 255, 255, 0.015)', topFillColor2: 'rgba(255, 255, 255, 0.015)',
|
||||||
|
bottomFillColor1: 'transparent', bottomFillColor2: 'transparent',
|
||||||
|
title: '', lastValueVisible: false, width: 0 },
|
||||||
|
|
||||||
|
// Overbought Gradient Fill (> 70)
|
||||||
|
{ id: 'rsi', type: 'baseline', baseValue: 70, color: 'transparent',
|
||||||
|
topLineColor: 'transparent', bottomLineColor: 'transparent',
|
||||||
|
topFillColor1: 'rgba(244, 67, 54, 0.8)', topFillColor2: 'rgba(244, 67, 54, 0.2)',
|
||||||
|
bottomFillColor1: 'transparent', bottomFillColor2: 'transparent',
|
||||||
|
title: '', lastValueVisible: false, width: 0 },
|
||||||
|
|
||||||
|
// Oversold Gradient Fill (< 30)
|
||||||
|
{ id: 'rsi', type: 'baseline', baseValue: 30, color: 'transparent',
|
||||||
|
topLineColor: 'transparent', bottomLineColor: 'transparent',
|
||||||
|
topFillColor1: 'transparent', topFillColor2: 'transparent',
|
||||||
|
bottomFillColor1: 'rgba(76, 175, 80, 0.2)', bottomFillColor2: 'rgba(76, 175, 80, 0.8)',
|
||||||
|
title: '', lastValueVisible: false, width: 0 },
|
||||||
|
|
||||||
|
// RSI Line
|
||||||
|
{ id: 'rsi', color: '#7E57C2', title: '', width: 1, lastValueVisible: true },
|
||||||
|
|
||||||
|
// Bands
|
||||||
|
{ id: 'upperBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false },
|
||||||
|
{ id: 'lowerBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'RSI',
|
||||||
|
description: 'Relative Strength Index',
|
||||||
|
inputs: [
|
||||||
|
{ name: 'period', label: 'RSI Length', type: 'number', default: 14, min: 1, max: 100 }
|
||||||
|
],
|
||||||
|
plots: plots,
|
||||||
|
displayMode: 'pane',
|
||||||
|
paneMin: 0,
|
||||||
|
paneMax: 100
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
65
scripts/generate_custom_history.py
Normal file
65
scripts/generate_custom_history.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add src to path
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from src.data_collector.database import DatabaseManager
|
||||||
|
from src.data_collector.custom_timeframe_generator import CustomTimeframeGenerator
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
logger.info("Starting custom timeframe generation...")
|
||||||
|
|
||||||
|
# DB connection settings from env or defaults
|
||||||
|
db_host = os.getenv('DB_HOST', 'localhost')
|
||||||
|
db_port = int(os.getenv('DB_PORT', 5432))
|
||||||
|
db_name = os.getenv('DB_NAME', 'btc_data')
|
||||||
|
db_user = os.getenv('DB_USER', 'btc_bot')
|
||||||
|
db_password = os.getenv('DB_PASSWORD', '')
|
||||||
|
|
||||||
|
db = DatabaseManager(
|
||||||
|
host=db_host,
|
||||||
|
port=db_port,
|
||||||
|
database=db_name,
|
||||||
|
user=db_user,
|
||||||
|
password=db_password
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.connect()
|
||||||
|
|
||||||
|
try:
|
||||||
|
generator = CustomTimeframeGenerator(db)
|
||||||
|
await generator.initialize()
|
||||||
|
|
||||||
|
# Generate 37m from 1m
|
||||||
|
logger.info("Generating 37m candles from 1m data...")
|
||||||
|
count_37m = await generator.generate_historical('37m')
|
||||||
|
logger.info(f"Generated {count_37m} candles for 37m")
|
||||||
|
|
||||||
|
# Generate 148m from 37m
|
||||||
|
# Note: 148m generation relies on 37m data existing
|
||||||
|
logger.info("Generating 148m candles from 37m data...")
|
||||||
|
count_148m = await generator.generate_historical('148m')
|
||||||
|
logger.info(f"Generated {count_148m} candles for 148m")
|
||||||
|
|
||||||
|
logger.info("Done!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating custom timeframes: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
await db.disconnect()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
86
src/TV/HTS.pine
Normal file
86
src/TV/HTS.pine
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
//@version=5
|
||||||
|
indicator(title='HTS p1otek (Fixed)', overlay=true )
|
||||||
|
|
||||||
|
// Helper function to return the correct timeframe string for request.security
|
||||||
|
// Note: We let Pine Script infer the return type to avoid syntax errors
|
||||||
|
getAutoTFString(chartTFInMinutes) =>
|
||||||
|
float autoTFMinutes = chartTFInMinutes / 4.0
|
||||||
|
|
||||||
|
// Use an existing time resolution string if possible (D, W, M)
|
||||||
|
if timeframe.isdaily
|
||||||
|
// 'D' timeframe is 1440 minutes. 1440 / 4 = 360 minutes (6 hours)
|
||||||
|
// We return "360" which Pine Script accepts as a resolution
|
||||||
|
str.tostring(math.round(autoTFMinutes))
|
||||||
|
else if timeframe.isweekly or timeframe.ismonthly
|
||||||
|
// Cannot divide W or M timeframes reliably, return current timeframe string
|
||||||
|
timeframe.period
|
||||||
|
else
|
||||||
|
// For standard minute timeframes, use the calculated minutes
|
||||||
|
str.tostring(math.round(autoTFMinutes))
|
||||||
|
|
||||||
|
// Inputs
|
||||||
|
// FIXED: Changed input.integer to input.int
|
||||||
|
short = input.int(33, "fast")
|
||||||
|
long = input.int(144, "slow")
|
||||||
|
auto = input.bool(false, title = "auto HTS (timeframe/4)")
|
||||||
|
draw_1h = input.bool(false, title = "draw 1h slow HTS")
|
||||||
|
|
||||||
|
metoda = input.string(title = "type average", defval = "RMA", options=["RMA", "EMA", "SMA", "WMA", "VWMA"])
|
||||||
|
|
||||||
|
// Calculate chart TF in minutes
|
||||||
|
float chartTFInMinutes = timeframe.in_seconds() / 60
|
||||||
|
// Get the auto-calculated timeframe string
|
||||||
|
string autoTFString = getAutoTFString(chartTFInMinutes)
|
||||||
|
|
||||||
|
|
||||||
|
srednia(src, length, type) =>
|
||||||
|
switch type
|
||||||
|
"RMA" => ta.rma(src, length)
|
||||||
|
"EMA" => ta.ema(src, length)
|
||||||
|
"SMA" => ta.sma(src, length)
|
||||||
|
"WMA" => ta.wma(src, length)
|
||||||
|
"VWMA" => ta.vwma(src, length)
|
||||||
|
|
||||||
|
// === Non-Auto (Current Timeframe) Calculations ===
|
||||||
|
string currentTFString = timeframe.period
|
||||||
|
|
||||||
|
shortl = request.security(syminfo.tickerid, currentTFString, srednia(low, short, metoda))
|
||||||
|
shorth = request.security(syminfo.tickerid, currentTFString, srednia(high, short, metoda))
|
||||||
|
longl = request.security(syminfo.tickerid, currentTFString, srednia(low, long, metoda))
|
||||||
|
longh = request.security(syminfo.tickerid, currentTFString, srednia(high, long, metoda))
|
||||||
|
|
||||||
|
// === Auto Timeframe Calculations ===
|
||||||
|
shortl_auto = request.security(syminfo.tickerid, autoTFString, srednia(low, short, metoda))
|
||||||
|
shorth_auto = request.security(syminfo.tickerid, autoTFString, srednia(high, short, metoda))
|
||||||
|
longl_auto = request.security(syminfo.tickerid, autoTFString, srednia(low, long, metoda))
|
||||||
|
longh_auto = request.security(syminfo.tickerid, autoTFString, srednia(high, long, metoda))
|
||||||
|
|
||||||
|
// === 1H Timeframe Calculations ===
|
||||||
|
// Use a fixed '60' for 1 hour
|
||||||
|
longl_1h = request.security(syminfo.tickerid, "60", srednia(low, long, metoda))
|
||||||
|
longh_1h = request.security(syminfo.tickerid, "60", srednia(high, long, metoda))
|
||||||
|
|
||||||
|
|
||||||
|
// === Plotting ===
|
||||||
|
|
||||||
|
// Auto HTS
|
||||||
|
plot(auto ? shortl_auto: na, color=color.new(color.aqua, 0), linewidth=1, title="fast low auto")
|
||||||
|
plot(auto ? shorth_auto: na, color=color.new(color.aqua, 0), linewidth=1, title="fast high auto")
|
||||||
|
plot(auto ? longl_auto: na, color=color.new(color.red, 0), linewidth=1, title="slow low auto")
|
||||||
|
plot(auto ? longh_auto: na, color=color.new(color.red, 0), linewidth=1, title="slow high auto")
|
||||||
|
|
||||||
|
// Current TF (only when Auto is enabled, for reference)
|
||||||
|
ll = plot( auto ? longl: na, color=color.new(color.red, 80), linewidth=1, title="current slow low")
|
||||||
|
oo = plot( auto ? longh: na, color=color.new(color.red, 80), linewidth=1, title="current slow high")
|
||||||
|
fill(ll,oo, color=color.new(color.red, 90))
|
||||||
|
|
||||||
|
// 1H Zone
|
||||||
|
zone_1hl = plot( draw_1h ? longl_1h: na, color=color.new(color.red, 80), linewidth=1, title="1h slow low")
|
||||||
|
zone_1hh = plot( draw_1h ? longh_1h: na, color=color.new(color.red, 80), linewidth=1, title="1h slow high")
|
||||||
|
fill(zone_1hl,zone_1hh, color=color.new(color.red, 90))
|
||||||
|
|
||||||
|
// Non-Auto HTS
|
||||||
|
plot(not auto ? shortl: na, color=color.new(color.aqua, 0), linewidth=1, title="fast low")
|
||||||
|
plot(not auto ? shorth: na, color=color.new(color.aqua, 0), linewidth=1, title="fast high")
|
||||||
|
plot(not auto ? longl: na, color=color.new(color.red, 0), linewidth=1, title="slow low")
|
||||||
|
plot(not auto ? longh: na, color=color.new(color.red, 0), linewidth=1, title="slow high")
|
||||||
BIN
src/api/dashboard.7z
Normal file
BIN
src/api/dashboard.7z
Normal file
Binary file not shown.
726
src/api/dashboard/static/css/indicators-new.css
Normal file
726
src/api/dashboard/static/css/indicators-new.css
Normal file
@ -0,0 +1,726 @@
|
|||||||
|
/* ============================================================================
|
||||||
|
NEW INDICATOR PANEL STYLES - Single Panel, TradingView-inspired
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.indicator-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subrbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.indicator-panel::-webkit-scrollbar-thumb {
|
||||||
|
background: #363a44;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.indicator-panel::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Bar */
|
||||||
|
.indicator-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 8px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.indicator-search:focus-within {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
.search-icon {
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.indicator-search input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text);
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.indicator-search input::placeholder {
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
}
|
||||||
|
.search-clear {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.search-clear:hover {
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category Tabs */
|
||||||
|
.category-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.category-tabs::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.category-tab {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.category-tab:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
.category-tab.active {
|
||||||
|
background: rgba(41, 98, 255, 0.1);
|
||||||
|
color: var(--tv-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicator Sections */
|
||||||
|
.indicator-section {
|
||||||
|
margin: 8px 12px 12px;
|
||||||
|
}
|
||||||
|
.indicator-section.favorites {
|
||||||
|
background: rgba(41, 98, 255, 0.05);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.section-title button.clear-all,
|
||||||
|
.section-title button.visibility-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.section-title:hover button.clear-all,
|
||||||
|
.section-title:hover button.visibility-toggle {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.visibility-toggle,
|
||||||
|
.clear-all {
|
||||||
|
background: var(--tv-red);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.visibility-toggle {
|
||||||
|
background: var(--tv-blue);
|
||||||
|
}
|
||||||
|
.visibility-toggle:hover,
|
||||||
|
.clear-all:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicator Items */
|
||||||
|
.indicator-item {
|
||||||
|
background: var(--tv-panel-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.indicator-item:hover {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
.indicator-item.favorite {
|
||||||
|
border-color: rgba(41, 98, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.indicator-btn:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
color: var(--tv-text);
|
||||||
|
border-color: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.indicator-btn.add:hover {
|
||||||
|
background: var(--tv-blue);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-presets {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.indicator-presets {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.indicator-desc {
|
||||||
|
display: inline;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Indicator Item */
|
||||||
|
.indicator-item.active {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item.active .indicator-name {
|
||||||
|
color: var(--tv-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item.active.expanded {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
background: rgba(41, 98, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
.drag-handle:hover {
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-btn.visible,
|
||||||
|
.indicator-btn.expand,
|
||||||
|
.indicator-btn.favorite {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.indicator-btn.expand.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicator Config (Expanded) */
|
||||||
|
.indicator-config {
|
||||||
|
border-top: 1px solid var(--tv-border);
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
animation: slideDown 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 1000px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-sections {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.config-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-action-btn {
|
||||||
|
background: var(--tv-blue);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.preset-action-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Config Row */
|
||||||
|
.config-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.config-row label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
.config-row select,
|
||||||
|
.config-row input[type="text"],
|
||||||
|
.config-row input[type="number"] {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.config-row select:focus,
|
||||||
|
.config-row input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-preset {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.input-with-preset input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.presets-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.presets-btn:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color Picker */
|
||||||
|
.color-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.color-picker input[type="color"] {
|
||||||
|
width: 32px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.color-preview {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Range Slider */
|
||||||
|
.config-row input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
accent-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.config-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--tv-border);
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
color: var(--tv-text);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--tv-red);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Results */
|
||||||
|
.no-results {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
padding: 40px 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Presets List */
|
||||||
|
.presets-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.preset-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.preset-item:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.preset-item.applied {
|
||||||
|
background: rgba(38, 166, 154, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.preset-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
.preset-delete {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.preset-delete:hover {
|
||||||
|
color: var(--tv-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-presets {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Range Value Display */
|
||||||
|
.range-value {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preset Indicator Button */
|
||||||
|
.preset-indicator {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.preset-indicator:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.category-tabs {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.category-tab {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-main {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-row label {
|
||||||
|
min-width: 60px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch-friendly styles for mobile */
|
||||||
|
@media (hover: none) {
|
||||||
|
.indicator-btn {
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tab {
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-main {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme improvements */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.indicator-search {
|
||||||
|
background: #1e222d;
|
||||||
|
}
|
||||||
|
.indicator-item {
|
||||||
|
background: #1e222d;
|
||||||
|
}
|
||||||
|
.indicator-config {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
.indicator-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-config > * {
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for presets list */
|
||||||
|
.presets-list::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.presets-list::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--tv-border);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Tabs */
|
||||||
|
.sidebar-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tab {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tab:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tab.active {
|
||||||
|
background: rgba(41, 98, 255, 0.15);
|
||||||
|
color: var(--tv-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Tab Panels */
|
||||||
|
.sidebar-tab-panel {
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tab-panel.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsed sidebar adjustments */
|
||||||
|
.right-sidebar.collapsed .sidebar-tabs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strategy Panel Styles */
|
||||||
|
.indicator-checklist {
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.indicator-checklist::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.indicator-checklist::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--tv-border);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.checklist-item:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.checklist-item input {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equity-chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
margin-top: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
background: var(--tv-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group {
|
||||||
|
display: flex;
|
||||||
|
background: var(--tv-hover);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group .toggle-btn {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group .toggle-btn.active {
|
||||||
|
background: var(--tv-border);
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group .toggle-btn:hover:not(.active) {
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
@ -523,7 +523,77 @@
|
|||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Price Scale Controls */
|
/* Chart Settings Menu */
|
||||||
|
.chart-settings-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: rgba(42, 46, 57, 0.9);
|
||||||
|
border: 1px solid #363c4e;
|
||||||
|
color: #d1d4dc;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-settings-btn:hover {
|
||||||
|
background: #363c4e;
|
||||||
|
border-color: #4a4f5e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Popup */
|
||||||
|
.chart-settings-popup {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 26px;
|
||||||
|
background: #1e222d;
|
||||||
|
border: 1px solid #363c4e;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 200px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-settings-popup.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #787b86;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select {
|
||||||
|
width: 100%;
|
||||||
|
background: #131722;
|
||||||
|
border: 1px solid #363c4e;
|
||||||
|
color: #d1d4dc;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2962ff;
|
||||||
|
}
|
||||||
.price-scale-controls {
|
.price-scale-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@ -721,7 +791,10 @@
|
|||||||
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 {
|
||||||
@ -1275,7 +1348,42 @@
|
|||||||
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">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@ -1317,8 +1425,26 @@
|
|||||||
<div class="chart-wrapper" id="chartWrapper">
|
<div class="chart-wrapper" id="chartWrapper">
|
||||||
<div id="chart"></div>
|
<div id="chart"></div>
|
||||||
<div class="price-scale-controls" id="priceScaleControls">
|
<div class="price-scale-controls" id="priceScaleControls">
|
||||||
|
<button class="chart-settings-btn" id="btnSettings" title="Settings (S)">☰</button>
|
||||||
<button class="ps-control-btn auto-scale active" id="btnAutoScale" title="Auto Scale (A)">A</button>
|
<button class="ps-control-btn auto-scale active" id="btnAutoScale" title="Auto Scale (A)">A</button>
|
||||||
<button class="ps-control-btn log-scale" id="btnLogScale" title="Logarithmic Scale">L</button>
|
<button class="ps-control-btn log-scale" id="btnLogScale" title="Logarithmic Scale">L</button>
|
||||||
|
<div class="chart-settings-popup" id="settingsPopup">
|
||||||
|
<div class="settings-group">
|
||||||
|
<label class="settings-label">Timezone</label>
|
||||||
|
<select class="settings-select" id="timezoneSelect">
|
||||||
|
<option value="UTC">UTC</option>
|
||||||
|
<option value="Europe/London">London (GMT/BST)</option>
|
||||||
|
<option value="Europe/Paris">Central Europe (CET/CEST)</option>
|
||||||
|
<option value="Europe/Warsaw" selected>Warsaw (CET/CEST)</option>
|
||||||
|
<option value="America/New_York">New York (EST/EDT)</option>
|
||||||
|
<option value="America/Chicago">Chicago (CST/CDT)</option>
|
||||||
|
<option value="America/Los_Angeles">Los Angeles (PST/PDT)</option>
|
||||||
|
<option value="Asia/Tokyo">Tokyo (JST)</option>
|
||||||
|
<option value="Asia/Shanghai">Shanghai (CST)</option>
|
||||||
|
<option value="Australia/Sydney">Sydney (AEST/AEDT)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-controls" id="navControls">
|
<div class="nav-controls" id="navControls">
|
||||||
<button class="nav-btn" id="navLeft" title="Navigate Left (←)">‹</button>
|
<button class="nav-btn" id="navLeft" title="Navigate Left (←)">‹</button>
|
||||||
@ -1344,127 +1470,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ta-content" id="taContent">
|
<div class="ta-content" id="taContent">
|
||||||
<div class="ta-loading">Loading technical analysis...</div>
|
<div class="ta-loading">Waiting for candle data...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Sidebar - Strategy Simulation -->
|
<!-- Right Sidebar - Tools Panel -->
|
||||||
<div class="right-sidebar" id="rightSidebar">
|
<div class="right-sidebar collapsed" id="rightSidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div class="sidebar-title">
|
<div class="sidebar-tabs">
|
||||||
<span>🎯</span>
|
<button class="sidebar-tab active" data-tab="indicators">📊 Indicators</button>
|
||||||
<span class="sidebar-title-text">Strategy Sim</span>
|
<button class="sidebar-tab" data-tab="strategy">⚙️ Strategy</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="sidebar-toggle" onclick="toggleSidebar()">◀</button>
|
<button class="sidebar-toggle" id="sidebarToggleBtn">◀</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<!-- Strategy Selection -->
|
<!-- Indicators Tab -->
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-tab-panel active" id="tab-indicators">
|
||||||
<div class="sidebar-section-header">
|
<div class="sidebar-section-indicators" id="indicatorPanel">
|
||||||
<span>📋</span> Select Strategy
|
<div class="sidebar-section-header" style="padding: 8px 12px;">
|
||||||
</div>
|
<span>📊</span> Indicators
|
||||||
<div class="sidebar-section-content" id="strategyList">
|
|
||||||
<div class="loading-strategies" style="text-align: center; color: var(--tv-text-secondary); padding: 20px;">
|
|
||||||
Loading strategies...
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Configuration -->
|
<!-- Strategy Tab -->
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-tab-panel" id="tab-strategy">
|
||||||
<div class="sidebar-section-header">
|
<div id="strategyPanel">
|
||||||
<span>⚙️</span> Configuration
|
<!-- Content will be injected by strategy-panel.js -->
|
||||||
</div>
|
<div class="loading-strategies">Loading strategy tools...</div>
|
||||||
<div class="sidebar-section-content">
|
|
||||||
<div class="config-group">
|
|
||||||
<label class="config-label">Start Date</label>
|
|
||||||
<input type="datetime-local" id="simStartDate" class="config-input">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-group">
|
|
||||||
<label class="config-label">Confirmation TF (Optional)</label>
|
|
||||||
<select id="simSecondaryTF" class="config-input">
|
|
||||||
<option value="">None</option>
|
|
||||||
<option value="1h">1h</option>
|
|
||||||
<option value="4h">4h</option>
|
|
||||||
<option value="1d" selected>1d</option>
|
|
||||||
<option value="1w">1w</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-group">
|
|
||||||
<label class="config-label">Risk % per Trade</label>
|
|
||||||
<input type="number" id="simRiskPercent" class="config-input" value="2" min="0.1" max="100" step="0.1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-group">
|
|
||||||
<label class="config-label">Stop Loss %</label>
|
|
||||||
<input type="number" id="simStopLoss" class="config-input" value="2" min="0.1" max="20" step="0.1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dynamic Strategy Parameters -->
|
|
||||||
<div id="strategyParams"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Run Button -->
|
|
||||||
<button class="action-btn primary" onclick="runSimulation()" id="runSimBtn" disabled>
|
|
||||||
▶ Run Simulation
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Results Section -->
|
|
||||||
<div class="sidebar-section" id="resultsSection" style="display: none;">
|
|
||||||
<div class="sidebar-section-header">
|
|
||||||
<span>📊</span> Results
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-section-content">
|
|
||||||
<!-- Equity Sparkline -->
|
|
||||||
<div class="equity-sparkline" id="equitySparkline"></div>
|
|
||||||
|
|
||||||
<!-- Stats -->
|
|
||||||
<div class="results-summary">
|
|
||||||
<div class="result-stat">
|
|
||||||
<div class="result-stat-value" id="simTrades">--</div>
|
|
||||||
<div class="result-stat-label">Trades</div>
|
|
||||||
</div>
|
|
||||||
<div class="result-stat">
|
|
||||||
<div class="result-stat-value" id="simWinRate">--</div>
|
|
||||||
<div class="result-stat-label">Win Rate</div>
|
|
||||||
</div>
|
|
||||||
<div class="result-stat">
|
|
||||||
<div class="result-stat-value" id="simPnL">--</div>
|
|
||||||
<div class="result-stat-label">Total P&L</div>
|
|
||||||
</div>
|
|
||||||
<div class="result-stat">
|
|
||||||
<div class="result-stat-value" id="simProfitFactor">--</div>
|
|
||||||
<div class="result-stat-label">Profit Factor</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<button class="action-btn secondary" onclick="showSimulationMarkers()">
|
|
||||||
📍 Plot on Chart
|
|
||||||
</button>
|
|
||||||
<button class="action-btn secondary" onclick="saveSimulation()">
|
|
||||||
💾 Save Simulation
|
|
||||||
</button>
|
|
||||||
<button class="action-btn success" onclick="showExportDialog()">
|
|
||||||
📥 Export Report
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Saved Simulations -->
|
|
||||||
<div class="sidebar-section">
|
|
||||||
<div class="sidebar-section-header">
|
|
||||||
<span>💾</span> Saved Simulations
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-section-content" id="savedSimulations">
|
|
||||||
<div style="text-align: center; color: var(--tv-text-secondary); padding: 10px; font-size: 12px;">
|
|
||||||
No saved simulations
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1481,7 +1516,8 @@
|
|||||||
{ type: 'bb', name: 'Bollinger Bands', description: 'Volatility bands' },
|
{ type: 'bb', name: 'Bollinger Bands', description: 'Volatility bands' },
|
||||||
{ type: 'macd', name: 'MACD', description: 'Moving Average Convergence Divergence' },
|
{ type: 'macd', name: 'MACD', description: 'Moving Average Convergence Divergence' },
|
||||||
{ type: 'stoch', name: 'Stochastic', description: 'Stochastic Oscillator' },
|
{ type: 'stoch', name: 'Stochastic', description: 'Stochastic Oscillator' },
|
||||||
{ type: 'atr', name: 'ATR', description: 'Average True Range' }
|
{ type: 'atr', name: 'ATR', description: 'Average True Range' },
|
||||||
|
{ type: 'hurst', name: 'Hurst Bands', description: 'Cyclic price channels' }
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,94 +1,82 @@
|
|||||||
import { TradingDashboard, refreshTA, openAIAnalysis } from './ui/chart.js';
|
import { TradingDashboard, refreshTA, openAIAnalysis } from './ui/chart.js';
|
||||||
import { restoreSidebarState, toggleSidebar } from './ui/sidebar.js';
|
import { restoreSidebarState, toggleSidebar, initSidebarTabs, restoreSidebarTabState } from './ui/sidebar.js';
|
||||||
import { SimulationStorage } from './ui/storage.js';
|
|
||||||
import { showExportDialog, closeExportDialog, performExport, exportSavedSimulation } from './ui/export.js';
|
|
||||||
import {
|
import {
|
||||||
runSimulation,
|
initIndicatorPanel,
|
||||||
displayEnhancedResults,
|
getActiveIndicators,
|
||||||
showSimulationMarkers,
|
setActiveIndicators,
|
||||||
clearSimulationResults,
|
drawIndicatorsOnChart,
|
||||||
getLastResults,
|
|
||||||
setLastResults
|
|
||||||
} from './ui/simulation.js';
|
|
||||||
import {
|
|
||||||
renderStrategies,
|
|
||||||
selectStrategy,
|
|
||||||
loadStrategies,
|
|
||||||
saveSimulation,
|
|
||||||
renderSavedSimulations,
|
|
||||||
loadSavedSimulation,
|
|
||||||
deleteSavedSimulation,
|
|
||||||
setCurrentStrategy
|
|
||||||
} from './ui/strategies-panel.js';
|
|
||||||
import {
|
|
||||||
renderIndicatorList,
|
|
||||||
addIndicator,
|
addIndicator,
|
||||||
toggleIndicator,
|
removeIndicatorById
|
||||||
showIndicatorConfig,
|
} from './ui/indicators-panel-new.js';
|
||||||
applyIndicatorConfig,
|
import { initStrategyPanel } from './ui/strategy-panel.js';
|
||||||
removeIndicator,
|
|
||||||
removeIndicatorById,
|
|
||||||
removeIndicatorByIndex,
|
|
||||||
drawIndicatorsOnChart
|
|
||||||
} from './ui/indicators-panel.js';
|
|
||||||
import { StrategyParams } from './strategies/config.js';
|
|
||||||
import { IndicatorRegistry } from './indicators/index.js';
|
import { IndicatorRegistry } from './indicators/index.js';
|
||||||
|
import { TimezoneConfig } from './config/timezone.js';
|
||||||
|
|
||||||
window.dashboard = null;
|
window.dashboard = null;
|
||||||
|
|
||||||
function setDefaultStartDate() {
|
|
||||||
const startDateInput = document.getElementById('simStartDate');
|
|
||||||
if (startDateInput) {
|
|
||||||
const sevenDaysAgo = new Date();
|
|
||||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
||||||
startDateInput.value = sevenDaysAgo.toISOString().slice(0, 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTimeframeDisplay() {
|
|
||||||
const display = document.getElementById('simTimeframeDisplay');
|
|
||||||
if (display && window.dashboard) {
|
|
||||||
display.value = window.dashboard.currentInterval.toUpperCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.toggleSidebar = toggleSidebar;
|
window.toggleSidebar = toggleSidebar;
|
||||||
window.refreshTA = refreshTA;
|
window.refreshTA = refreshTA;
|
||||||
window.openAIAnalysis = openAIAnalysis;
|
window.openAIAnalysis = openAIAnalysis;
|
||||||
window.showExportDialog = showExportDialog;
|
window.TimezoneConfig = TimezoneConfig;
|
||||||
window.closeExportDialog = closeExportDialog;
|
window.renderIndicatorList = function() {
|
||||||
window.performExport = performExport;
|
// This function is no longer needed for sidebar indicators
|
||||||
window.exportSavedSimulation = exportSavedSimulation;
|
};
|
||||||
window.runSimulation = runSimulation;
|
|
||||||
window.saveSimulation = saveSimulation;
|
// Export init function for global access
|
||||||
window.renderSavedSimulations = renderSavedSimulations;
|
window.initIndicatorPanel = initIndicatorPanel;
|
||||||
window.loadSavedSimulation = loadSavedSimulation;
|
window.addIndicator = addIndicator;
|
||||||
window.deleteSavedSimulation = deleteSavedSimulation;
|
window.toggleIndicator = addIndicator;
|
||||||
window.clearSimulationResults = clearSimulationResults;
|
window.drawIndicatorsOnChart = drawIndicatorsOnChart;
|
||||||
window.updateTimeframeDisplay = updateTimeframeDisplay;
|
window.updateIndicatorCandles = drawIndicatorsOnChart;
|
||||||
window.renderIndicatorList = renderIndicatorList;
|
|
||||||
window.addIndicator = addIndicator;
|
|
||||||
window.toggleIndicator = toggleIndicator;
|
|
||||||
window.showIndicatorConfig = showIndicatorConfig;
|
|
||||||
|
|
||||||
window.StrategyParams = StrategyParams;
|
|
||||||
window.SimulationStorage = SimulationStorage;
|
|
||||||
window.IndicatorRegistry = IndicatorRegistry;
|
window.IndicatorRegistry = IndicatorRegistry;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
// Attach toggle sidebar event listener
|
||||||
|
const toggleBtn = document.getElementById('sidebarToggleBtn');
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.addEventListener('click', toggleSidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize timezone selector
|
||||||
|
const timezoneSelect = document.getElementById('timezoneSelect');
|
||||||
|
const settingsPopup = document.getElementById('settingsPopup');
|
||||||
|
const settingsBtn = document.getElementById('btnSettings');
|
||||||
|
|
||||||
|
if (timezoneSelect) {
|
||||||
|
timezoneSelect.value = TimezoneConfig.getTimezone();
|
||||||
|
timezoneSelect.addEventListener('change', (e) => {
|
||||||
|
TimezoneConfig.setTimezone(e.target.value);
|
||||||
|
settingsPopup.classList.remove('show');
|
||||||
|
// Redraw chart and indicators
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.drawIndicatorsOnChart?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle settings popup
|
||||||
|
if (settingsBtn && settingsPopup) {
|
||||||
|
settingsBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
settingsPopup.classList.toggle('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
settingsPopup.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', () => {
|
||||||
|
settingsPopup.classList.remove('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.dashboard = new TradingDashboard();
|
window.dashboard = new TradingDashboard();
|
||||||
restoreSidebarState();
|
restoreSidebarState();
|
||||||
setDefaultStartDate();
|
restoreSidebarTabState();
|
||||||
updateTimeframeDisplay();
|
initSidebarTabs();
|
||||||
renderSavedSimulations();
|
|
||||||
|
|
||||||
await loadStrategies();
|
// Initialize panels
|
||||||
|
window.initIndicatorPanel();
|
||||||
renderIndicatorList();
|
initStrategyPanel();
|
||||||
|
|
||||||
const originalSwitchTimeframe = window.dashboard.switchTimeframe.bind(window.dashboard);
|
|
||||||
window.dashboard.switchTimeframe = function(interval) {
|
|
||||||
originalSwitchTimeframe(interval);
|
|
||||||
setTimeout(() => drawIndicatorsOnChart(), 500);
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|||||||
76
src/api/dashboard/static/js/config/timezone.js
Normal file
76
src/api/dashboard/static/js/config/timezone.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const TimezoneConfig = {
|
||||||
|
timezone: localStorage.getItem('timezone') || 'Europe/Warsaw',
|
||||||
|
|
||||||
|
availableTimezones: [
|
||||||
|
{ value: 'UTC', label: 'UTC', offset: 0 },
|
||||||
|
{ value: 'Europe/London', label: 'London (GMT/BST)', offset: 0 },
|
||||||
|
{ value: 'Europe/Paris', label: 'Central Europe (CET/CEST)', offset: 1 },
|
||||||
|
{ value: 'Europe/Warsaw', label: 'Warsaw (CET/CEST)', offset: 1 },
|
||||||
|
{ value: 'America/New_York', label: 'New York (EST/EDT)', offset: -5 },
|
||||||
|
{ value: 'America/Chicago', label: 'Chicago (CST/CDT)', offset: -6 },
|
||||||
|
{ value: 'America/Los_Angeles', label: 'Los Angeles (PST/PDT)', offset: -8 },
|
||||||
|
{ value: 'Asia/Tokyo', label: 'Tokyo (JST)', offset: 9 },
|
||||||
|
{ value: 'Asia/Shanghai', label: 'Shanghai (CST)', offset: 8 },
|
||||||
|
{ value: 'Australia/Sydney', label: 'Sydney (AEST/AEDT)', offset: 10 },
|
||||||
|
],
|
||||||
|
|
||||||
|
setTimezone(tz) {
|
||||||
|
this.timezone = tz;
|
||||||
|
localStorage.setItem('timezone', tz);
|
||||||
|
document.dispatchEvent(new CustomEvent('timezone-changed', { detail: tz }));
|
||||||
|
},
|
||||||
|
|
||||||
|
getTimezone() {
|
||||||
|
return this.timezone;
|
||||||
|
},
|
||||||
|
|
||||||
|
getOffsetHours(tz = this.timezone) {
|
||||||
|
const now = new Date();
|
||||||
|
const tzDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));
|
||||||
|
const utcDate = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }));
|
||||||
|
return (tzDate - utcDate) / 3600000;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(timestamp) {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const tz = this.timezone;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
timeZone: tz,
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat('en-GB', options);
|
||||||
|
const parts = formatter.formatToParts(date);
|
||||||
|
const get = (type) => parts.find(p => p.type === type).value;
|
||||||
|
|
||||||
|
return `${get('day')}/${get('month')}/${get('year')} ${get('hour')}:${get('minute')}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTickMark(timestamp) {
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
const tz = this.timezone;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
timeZone: tz,
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat('en-GB', options);
|
||||||
|
const parts = formatter.formatToParts(date);
|
||||||
|
const get = (type) => parts.find(p => p.type === type).value;
|
||||||
|
|
||||||
|
// If it's exactly midnight, just show the date, otherwise show time too
|
||||||
|
const isMidnight = get('hour') === '00' && get('minute') === '00';
|
||||||
|
if (isMidnight) {
|
||||||
|
return `${get('day')}/${get('month')}/${get('year')}`;
|
||||||
|
}
|
||||||
|
return `${get('day')}/${get('month')} ${get('hour')}:${get('minute')}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TimezoneConfig };
|
||||||
@ -1,6 +1,70 @@
|
|||||||
import { BaseIndicator } from './base.js';
|
// Self-contained ATR indicator
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for ATR
|
||||||
|
function calculateATRSignal(indicator, lastCandle, prevCandle, values) {
|
||||||
|
const atr = values?.atr;
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
|
||||||
|
if (!atr || atr === null || !prevClose) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const atrPercent = atr / close * 100;
|
||||||
|
const priceChange = Math.abs(close - prevClose);
|
||||||
|
const atrRatio = priceChange / atr;
|
||||||
|
|
||||||
|
if (atrRatio > 1.5) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.HOLD,
|
||||||
|
strength: 70,
|
||||||
|
value: atr,
|
||||||
|
reasoning: `High volatility: ATR (${atr.toFixed(2)}, ${atrPercent.toFixed(2)}%)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ATR Indicator class
|
||||||
export class ATRIndicator extends BaseIndicator {
|
export class ATRIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles) {
|
calculate(candles) {
|
||||||
const period = this.params.period || 14;
|
const period = this.params.period || 14;
|
||||||
const results = new Array(candles.length).fill(null);
|
const results = new Array(candles.length).fill(null);
|
||||||
@ -23,16 +87,32 @@ export class ATRIndicator extends BaseIndicator {
|
|||||||
atr = (atr * (period - 1) + tr[i]) / period;
|
atr = (atr * (period - 1) + tr[i]) / period;
|
||||||
results[i] = atr;
|
results[i] = atr;
|
||||||
}
|
}
|
||||||
return results;
|
|
||||||
|
return results.map(atr => ({ atr }));
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetadata() {
|
getMetadata() {
|
||||||
return {
|
return {
|
||||||
name: 'ATR',
|
name: 'ATR',
|
||||||
description: 'Average True Range - measures market volatility',
|
description: 'Average True Range - measures market volatility',
|
||||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
|
inputs: [{
|
||||||
plots: [{ id: 'value', color: '#795548', title: 'ATR' }],
|
name: 'period',
|
||||||
|
label: 'Period',
|
||||||
|
type: 'number',
|
||||||
|
default: 14,
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
description: 'Period for ATR calculation'
|
||||||
|
}],
|
||||||
|
plots: [{
|
||||||
|
id: 'value',
|
||||||
|
color: '#795548',
|
||||||
|
title: 'ATR',
|
||||||
|
lineWidth: 1
|
||||||
|
}],
|
||||||
displayMode: 'pane'
|
displayMode: 'pane'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateATRSignal };
|
||||||
@ -1,6 +1,79 @@
|
|||||||
import { BaseIndicator } from './base.js';
|
// Self-contained Bollinger Bands indicator
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for Bollinger Bands
|
||||||
|
function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
const upper = values?.upper;
|
||||||
|
const lower = values?.lower;
|
||||||
|
const prevUpper = prevValues?.upper;
|
||||||
|
const prevLower = prevValues?.lower;
|
||||||
|
|
||||||
|
if (!upper || !lower || prevUpper === undefined || prevLower === undefined || prevClose === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUY: Price crosses DOWN through lower band (reversal/bounce play)
|
||||||
|
if (prevClose > prevLower && close <= lower) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 70,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed DOWN through lower Bollinger Band`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL: Price crosses UP through upper band (overextended play)
|
||||||
|
else if (prevClose < prevUpper && close >= upper) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 70,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed UP through upper Bollinger Band`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bollinger Bands Indicator class
|
||||||
export class BollingerBandsIndicator extends BaseIndicator {
|
export class BollingerBandsIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles) {
|
calculate(candles) {
|
||||||
const period = this.params.period || 20;
|
const period = this.params.period || 20;
|
||||||
const stdDevMult = this.params.stdDev || 2;
|
const stdDevMult = this.params.stdDev || 2;
|
||||||
@ -41,3 +114,5 @@ export class BollingerBandsIndicator extends BaseIndicator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateBollingerBandsSignal };
|
||||||
@ -1,41 +1,255 @@
|
|||||||
import { MA } from './ma.js';
|
// Self-contained HTS Trend System indicator
|
||||||
import { BaseIndicator } from './base.js';
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MA calculations inline (SMA/EMA/RMA/WMA/VWMA)
|
||||||
|
function calculateSMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i >= period) sum -= candles[i - period][source];
|
||||||
|
if (i >= period - 1) results[i] = sum / period;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateEMA(candles, period, source = 'close') {
|
||||||
|
const multiplier = 2 / (period + 1);
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let ema = 0;
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
if (i < period) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i === period - 1) {
|
||||||
|
ema = sum / period;
|
||||||
|
results[i] = ema;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ema = (candles[i][source] - ema) * multiplier + ema;
|
||||||
|
results[i] = ema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRMA(candles, period, source = 'close') {
|
||||||
|
const multiplier = 1 / period;
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let rma = 0;
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
if (i < period) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i === period - 1) {
|
||||||
|
rma = sum / period;
|
||||||
|
results[i] = rma;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rma = (candles[i][source] - rma) * multiplier + rma;
|
||||||
|
results[i] = rma;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateWMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
const weightSum = (period * (period + 1)) / 2;
|
||||||
|
|
||||||
|
for (let i = period - 1; i < candles.length; i++) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let j = 0; j < period; j++) {
|
||||||
|
sum += candles[i - j][source] * (period - j);
|
||||||
|
}
|
||||||
|
results[i] = sum / weightSum;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateVWMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
|
||||||
|
for (let i = period - 1; i < candles.length; i++) {
|
||||||
|
let sumPV = 0;
|
||||||
|
let sumV = 0;
|
||||||
|
for (let j = 0; j < period; j++) {
|
||||||
|
sumPV += candles[i - j][source] * candles[i - j].volume;
|
||||||
|
sumV += candles[i - j].volume;
|
||||||
|
}
|
||||||
|
results[i] = sumV !== 0 ? sumPV / sumV : null;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MA dispatcher function
|
||||||
|
function getMA(type, candles, period, source = 'close') {
|
||||||
|
switch (type.toUpperCase()) {
|
||||||
|
case 'SMA': return calculateSMA(candles, period, source);
|
||||||
|
case 'EMA': return calculateEMA(candles, period, source);
|
||||||
|
case 'RMA': return calculateRMA(candles, period, source);
|
||||||
|
case 'WMA': return calculateWMA(candles, period, source);
|
||||||
|
case 'VWMA': return calculateVWMA(candles, period, source);
|
||||||
|
default: return calculateSMA(candles, period, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for HTS
|
||||||
|
function calculateHTSSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const slowLow = values?.slowLow;
|
||||||
|
const slowHigh = values?.slowHigh;
|
||||||
|
const prevSlowLow = prevValues?.slowLow;
|
||||||
|
const prevSlowHigh = prevValues?.slowHigh;
|
||||||
|
|
||||||
|
if (!slowLow || !slowHigh || !prevSlowLow || !prevSlowHigh) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
|
||||||
|
if (prevClose === undefined) return null;
|
||||||
|
|
||||||
|
// BUY: Price crosses UP through slow low
|
||||||
|
if (prevClose <= prevSlowLow && close > slowLow) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 85,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed UP through slow low`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL: Price crosses DOWN through slow high
|
||||||
|
else if (prevClose >= prevSlowHigh && close < slowHigh) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 85,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed DOWN through slow high`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTS Indicator class
|
||||||
export class HTSIndicator extends BaseIndicator {
|
export class HTSIndicator extends BaseIndicator {
|
||||||
calculate(candles) {
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles, oneMinCandles = null, targetTF = null) {
|
||||||
const shortPeriod = this.params.short || 33;
|
const shortPeriod = this.params.short || 33;
|
||||||
const longPeriod = this.params.long || 144;
|
const longPeriod = this.params.long || 144;
|
||||||
const maType = this.params.maType || 'RMA';
|
const maType = this.params.maType || 'RMA';
|
||||||
|
const useAutoHTS = this.params.useAutoHTS || false;
|
||||||
|
|
||||||
const shortHigh = MA.get(maType, candles, shortPeriod, 'high');
|
let workingCandles = candles;
|
||||||
const shortLow = MA.get(maType, candles, shortPeriod, 'low');
|
|
||||||
const longHigh = MA.get(maType, candles, longPeriod, 'high');
|
|
||||||
const longLow = MA.get(maType, candles, longPeriod, 'low');
|
|
||||||
|
|
||||||
return candles.map((_, i) => ({
|
if (useAutoHTS && oneMinCandles && targetTF) {
|
||||||
|
const tfMultipliers = {
|
||||||
|
'5m': 5,
|
||||||
|
'15m': 15,
|
||||||
|
'30m': 30,
|
||||||
|
'37m': 37,
|
||||||
|
'1h': 60,
|
||||||
|
'4h': 240
|
||||||
|
};
|
||||||
|
|
||||||
|
const tfGroup = tfMultipliers[targetTF] || 5;
|
||||||
|
|
||||||
|
const grouped = [];
|
||||||
|
let currentGroup = [];
|
||||||
|
for (let i = 0; i < oneMinCandles.length; i++) {
|
||||||
|
currentGroup.push(oneMinCandles[i]);
|
||||||
|
if (currentGroup.length >= tfGroup) {
|
||||||
|
grouped.push({
|
||||||
|
time: currentGroup[tfGroup - 1].time,
|
||||||
|
open: currentGroup[tfGroup - 1].open,
|
||||||
|
high: currentGroup[tfGroup - 1].high,
|
||||||
|
low: currentGroup[tfGroup - 1].low,
|
||||||
|
close: currentGroup[tfGroup - 1].close,
|
||||||
|
volume: currentGroup[tfGroup - 1].volume
|
||||||
|
});
|
||||||
|
currentGroup = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
workingCandles = grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortHigh = getMA(maType, workingCandles, shortPeriod, 'high');
|
||||||
|
const shortLow = getMA(maType, workingCandles, shortPeriod, 'low');
|
||||||
|
const longHigh = getMA(maType, workingCandles, longPeriod, 'high');
|
||||||
|
const longLow = getMA(maType, workingCandles, longPeriod, 'low');
|
||||||
|
|
||||||
|
return workingCandles.map((_, i) => ({
|
||||||
fastHigh: shortHigh[i],
|
fastHigh: shortHigh[i],
|
||||||
fastLow: shortLow[i],
|
fastLow: shortLow[i],
|
||||||
slowHigh: longHigh[i],
|
slowHigh: longHigh[i],
|
||||||
slowLow: longLow[i]
|
slowLow: longLow[i],
|
||||||
|
fastMidpoint: ((shortHigh[i] || 0) + (shortLow[i] || 0)) / 2,
|
||||||
|
slowMidpoint: ((longHigh[i] || 0) + (longLow[i] || 0)) / 2
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetadata() {
|
getMetadata() {
|
||||||
|
const useAutoHTS = this.params?.useAutoHTS || false;
|
||||||
|
|
||||||
|
const fastLineWidth = useAutoHTS ? 1 : 1;
|
||||||
|
const slowLineWidth = useAutoHTS ? 2 : 2;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'HTS Trend System',
|
name: 'HTS Trend System',
|
||||||
description: 'High/Low Trend System with Fast and Slow MAs',
|
description: 'High/Low Trend System with Fast and Slow MAs',
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: 'short', label: 'Fast Period', type: 'number', default: 33, min: 1, max: 500 },
|
{ name: 'short', label: 'Fast Period', type: 'number', default: 33, min: 1, max: 500 },
|
||||||
{ name: 'long', label: 'Slow Period', type: 'number', default: 144, min: 1, max: 500 },
|
{ name: 'long', label: 'Slow Period', type: 'number', default: 144, min: 1, max: 500 },
|
||||||
{ name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'RMA' }
|
{ name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'RMA' },
|
||||||
|
{ name: 'useAutoHTS', label: 'Auto HTS (TF/4)', type: 'boolean', default: false }
|
||||||
],
|
],
|
||||||
plots: [
|
plots: [
|
||||||
{ id: 'fastHigh', color: '#00bcd4', title: 'Fast High', width: 1 },
|
{ id: 'fastHigh', color: '#00bcd4', title: 'Fast High', width: fastLineWidth },
|
||||||
{ id: 'fastLow', color: '#00bcd4', title: 'Fast Low', width: 1 },
|
{ id: 'fastLow', color: '#00bcd4', title: 'Fast Low', width: fastLineWidth },
|
||||||
{ id: 'slowHigh', color: '#f44336', title: 'Slow High', width: 2 },
|
{ id: 'slowHigh', color: '#f44336', title: 'Slow High', width: slowLineWidth },
|
||||||
{ id: 'slowLow', color: '#f44336', title: 'Slow Low', width: 2 }
|
{ id: 'slowLow', color: '#f44336', title: 'Slow Low', width: slowLineWidth }
|
||||||
],
|
],
|
||||||
displayMode: 'overlay'
|
displayMode: 'overlay'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateHTSSignal };
|
||||||
179
src/api/dashboard/static/js/indicators/hurst.js
Normal file
179
src/api/dashboard/static/js/indicators/hurst.js
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// Self-contained Hurst Bands indicator
|
||||||
|
// Based on J.M. Hurst's cyclic price channel theory
|
||||||
|
// Using RMA + ATR displacement method
|
||||||
|
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#9e9e9e',
|
||||||
|
sell: '#9e9e9e'
|
||||||
|
};
|
||||||
|
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate RMA (Rolling Moving Average - Wilder's method)
|
||||||
|
// Recreates Pine Script's ta.rma() exactly
|
||||||
|
function calculateRMA(sourceArray, length) {
|
||||||
|
const rma = new Array(sourceArray.length).fill(null);
|
||||||
|
let sum = 0;
|
||||||
|
const alpha = 1 / length;
|
||||||
|
|
||||||
|
// PineScript implicitly rounds float lengths for SMA initialization
|
||||||
|
const smaLength = Math.round(length);
|
||||||
|
|
||||||
|
for (let i = 0; i < sourceArray.length; i++) {
|
||||||
|
if (i < smaLength - 1) {
|
||||||
|
// Accumulate first N-1 bars
|
||||||
|
sum += sourceArray[i];
|
||||||
|
} else if (i === smaLength - 1) {
|
||||||
|
// On the Nth bar, the first RMA value is the SMA
|
||||||
|
sum += sourceArray[i];
|
||||||
|
rma[i] = sum / smaLength;
|
||||||
|
} else {
|
||||||
|
// Subsequent bars use the RMA formula
|
||||||
|
const prevRMA = rma[i - 1];
|
||||||
|
rma[i] = (prevRMA === null || isNaN(prevRMA))
|
||||||
|
? alpha * sourceArray[i]
|
||||||
|
: alpha * sourceArray[i] + (1 - alpha) * prevRMA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rma;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateHurstSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
const upper = values?.upper;
|
||||||
|
const lower = values?.lower;
|
||||||
|
const prevUpper = prevValues?.upper;
|
||||||
|
const prevLower = prevValues?.lower;
|
||||||
|
|
||||||
|
if (close === undefined || prevClose === undefined || !upper || !lower || !prevUpper || !prevLower) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUY: Price crosses DOWN through lower Hurst Band (dip entry)
|
||||||
|
if (prevClose > prevLower && close <= lower) {
|
||||||
|
return {
|
||||||
|
type: 'buy',
|
||||||
|
strength: 80,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed DOWN through lower Hurst Band`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELL: Price crosses DOWN through upper Hurst Band (reversal entry)
|
||||||
|
if (prevClose > prevUpper && close <= upper) {
|
||||||
|
return {
|
||||||
|
type: 'sell',
|
||||||
|
strength: 80,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed DOWN through upper Hurst Band`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HurstBandsIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
|
||||||
|
if (!this.params.markerBuyShape) this.params.markerBuyShape = 'custom';
|
||||||
|
if (!this.params.markerSellShape) this.params.markerSellShape = 'custom';
|
||||||
|
if (!this.params.markerBuyColor) this.params.markerBuyColor = '#9e9e9e';
|
||||||
|
if (!this.params.markerSellColor) this.params.markerSellColor = '#9e9e9e';
|
||||||
|
if (!this.params.markerBuyCustom) this.params.markerBuyCustom = '▲';
|
||||||
|
if (!this.params.markerSellCustom) this.params.markerSellCustom = '▼';
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles) {
|
||||||
|
const mcl_t = this.params.period || 30;
|
||||||
|
const mcm = this.params.multiplier || 1.8;
|
||||||
|
|
||||||
|
const mcl = mcl_t / 2;
|
||||||
|
|
||||||
|
// FIX: PineScript rounds implicit floats for history references [].
|
||||||
|
// 15/2 = 7.5. Pine rounds this to 8. Math.floor gives 7.
|
||||||
|
const mcl_2 = Math.round(mcl / 2);
|
||||||
|
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
const closes = candles.map(c => c.close);
|
||||||
|
|
||||||
|
const trArray = candles.map((d, i) => {
|
||||||
|
const prevClose = i > 0 ? candles[i - 1].close : null;
|
||||||
|
const high = d.high;
|
||||||
|
const low = d.low;
|
||||||
|
|
||||||
|
if (prevClose === null || prevClose === undefined || isNaN(prevClose)) {
|
||||||
|
return high - low;
|
||||||
|
}
|
||||||
|
return Math.max(
|
||||||
|
high - low,
|
||||||
|
Math.abs(high - prevClose),
|
||||||
|
Math.abs(low - prevClose)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ma_mcl = calculateRMA(closes, mcl);
|
||||||
|
const atr = calculateRMA(trArray, mcl);
|
||||||
|
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
const src = closes[i];
|
||||||
|
const mcm_off = mcm * (atr[i] || 0);
|
||||||
|
|
||||||
|
const historicalIndex = i - mcl_2;
|
||||||
|
const historical_ma = historicalIndex >= 0 ? ma_mcl[historicalIndex] : null;
|
||||||
|
|
||||||
|
const centerLine = (historical_ma === null || historical_ma === undefined || isNaN(historical_ma)) ? src : historical_ma;
|
||||||
|
|
||||||
|
results[i] = {
|
||||||
|
upper: centerLine + mcm_off,
|
||||||
|
lower: centerLine - mcm_off
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
return {
|
||||||
|
name: 'Hurst Bands',
|
||||||
|
description: 'Cyclic price channels based on Hurst theory',
|
||||||
|
inputs: [
|
||||||
|
{ name: 'period', label: 'Hurst Cycle Length (mcl_t)', type: 'number', default: 30, min: 5, max: 200 },
|
||||||
|
{ name: 'multiplier', label: 'Multiplier (mcm)', type: 'number', default: 1.8, min: 0.5, max: 10, step: 0.1 }
|
||||||
|
],
|
||||||
|
plots: [
|
||||||
|
{ id: 'upper', color: '#808080', title: 'Upper', lineWidth: 1 },
|
||||||
|
{ id: 'lower', color: '#808080', title: 'Lower', lineWidth: 1 }
|
||||||
|
],
|
||||||
|
bands: [
|
||||||
|
{ topId: 'upper', bottomId: 'lower', color: 'rgba(128, 128, 128, 0.05)' }
|
||||||
|
],
|
||||||
|
displayMode: 'overlay'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { calculateHurstSignal };
|
||||||
@ -1,34 +1,51 @@
|
|||||||
export { MA } from './ma.js';
|
// Indicator registry and exports for self-contained indicators
|
||||||
export { BaseIndicator } from './base.js';
|
|
||||||
export { HTSIndicator } from './hts.js';
|
|
||||||
export { MAIndicator } from './ma_indicator.js';
|
|
||||||
export { RSIIndicator } from './rsi.js';
|
|
||||||
export { BollingerBandsIndicator } from './bb.js';
|
|
||||||
export { MACDIndicator } from './macd.js';
|
|
||||||
export { StochasticIndicator } from './stoch.js';
|
|
||||||
export { ATRIndicator } from './atr.js';
|
|
||||||
|
|
||||||
import { HTSIndicator } from './hts.js';
|
// Import all indicator classes and their signal functions
|
||||||
import { MAIndicator } from './ma_indicator.js';
|
export { MAIndicator, calculateMASignal } from './moving_average.js';
|
||||||
import { RSIIndicator } from './rsi.js';
|
export { MACDIndicator, calculateMACDSignal } from './macd.js';
|
||||||
import { BollingerBandsIndicator } from './bb.js';
|
export { HTSIndicator, calculateHTSSignal } from './hts.js';
|
||||||
import { MACDIndicator } from './macd.js';
|
export { RSIIndicator, calculateRSISignal } from './rsi.js';
|
||||||
import { StochasticIndicator } from './stoch.js';
|
export { BollingerBandsIndicator, calculateBollingerBandsSignal } from './bb.js';
|
||||||
import { ATRIndicator } from './atr.js';
|
export { StochasticIndicator, calculateStochSignal } from './stoch.js';
|
||||||
|
export { ATRIndicator, calculateATRSignal } from './atr.js';
|
||||||
|
export { HurstBandsIndicator, calculateHurstSignal } from './hurst.js';
|
||||||
|
|
||||||
|
// Import for registry
|
||||||
|
import { MAIndicator as MAI, calculateMASignal as CMA } from './moving_average.js';
|
||||||
|
import { MACDIndicator as MACDI, calculateMACDSignal as CMC } from './macd.js';
|
||||||
|
import { HTSIndicator as HTSI, calculateHTSSignal as CHTS } from './hts.js';
|
||||||
|
import { RSIIndicator as RSII, calculateRSISignal as CRSI } from './rsi.js';
|
||||||
|
import { BollingerBandsIndicator as BBI, calculateBollingerBandsSignal as CBB } from './bb.js';
|
||||||
|
import { StochasticIndicator as STOCHI, calculateStochSignal as CST } from './stoch.js';
|
||||||
|
import { ATRIndicator as ATRI, calculateATRSignal as CATR } from './atr.js';
|
||||||
|
import { HurstBandsIndicator as HURSTI, calculateHurstSignal as CHURST } from './hurst.js';
|
||||||
|
|
||||||
|
// Signal function registry for easy dispatch
|
||||||
|
export const SignalFunctionRegistry = {
|
||||||
|
ma: CMA,
|
||||||
|
macd: CMC,
|
||||||
|
hts: CHTS,
|
||||||
|
rsi: CRSI,
|
||||||
|
bb: CBB,
|
||||||
|
stoch: CST,
|
||||||
|
atr: CATR,
|
||||||
|
hurst: CHURST
|
||||||
|
};
|
||||||
|
|
||||||
|
// Indicator registry for UI
|
||||||
export const IndicatorRegistry = {
|
export const IndicatorRegistry = {
|
||||||
hts: HTSIndicator,
|
ma: MAI,
|
||||||
ma: MAIndicator,
|
macd: MACDI,
|
||||||
rsi: RSIIndicator,
|
hts: HTSI,
|
||||||
bb: BollingerBandsIndicator,
|
rsi: RSII,
|
||||||
macd: MACDIndicator,
|
bb: BBI,
|
||||||
stoch: StochasticIndicator,
|
stoch: STOCHI,
|
||||||
atr: ATRIndicator
|
atr: ATRI,
|
||||||
|
hurst: HURSTI
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically build the available indicators list from the registry.
|
* Get list of available indicators for the UI catalog
|
||||||
* Each indicator class provides its own name and description via getMetadata().
|
|
||||||
*/
|
*/
|
||||||
export function getAvailableIndicators() {
|
export function getAvailableIndicators() {
|
||||||
return Object.entries(IndicatorRegistry).map(([type, IndicatorClass]) => {
|
return Object.entries(IndicatorRegistry).map(([type, IndicatorClass]) => {
|
||||||
@ -41,3 +58,12 @@ export function getAvailableIndicators() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get signal function for an indicator type
|
||||||
|
* @param {string} indicatorType - The type of indicator (e.g., 'ma', 'rsi')
|
||||||
|
* @returns {Function|null} The signal calculation function or null if not found
|
||||||
|
*/
|
||||||
|
export function getSignalFunction(indicatorType) {
|
||||||
|
return SignalFunctionRegistry[indicatorType] || null;
|
||||||
|
}
|
||||||
@ -1,37 +1,128 @@
|
|||||||
import { MA } from './ma.js';
|
// Self-contained MACD indicator
|
||||||
import { BaseIndicator } from './base.js';
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EMA calculation inline (needed for MACD)
|
||||||
|
function calculateEMAInline(data, period) {
|
||||||
|
const multiplier = 2 / (period + 1);
|
||||||
|
const ema = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
if (i < period - 1) {
|
||||||
|
ema.push(null);
|
||||||
|
} else if (i === period - 1) {
|
||||||
|
ema.push(data[i]);
|
||||||
|
} else {
|
||||||
|
ema.push((data[i] - ema[i - 1]) * multiplier + ema[i - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for MACD
|
||||||
|
function calculateMACDSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const macd = values?.macd;
|
||||||
|
const signal = values?.signal;
|
||||||
|
const prevMacd = prevValues?.macd;
|
||||||
|
const prevSignal = prevValues?.signal;
|
||||||
|
|
||||||
|
if (macd === undefined || macd === null || signal === undefined || signal === null ||
|
||||||
|
prevMacd === undefined || prevMacd === null || prevSignal === undefined || prevSignal === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUY: MACD crosses UP through Signal line
|
||||||
|
if (prevMacd <= prevSignal && macd > signal) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 80,
|
||||||
|
value: macd,
|
||||||
|
reasoning: `MACD crossed UP through Signal line`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL: MACD crosses DOWN through Signal line
|
||||||
|
else if (prevMacd >= prevSignal && macd < signal) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 80,
|
||||||
|
value: macd,
|
||||||
|
reasoning: `MACD crossed DOWN through Signal line`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MACD Indicator class
|
||||||
export class MACDIndicator extends BaseIndicator {
|
export class MACDIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles) {
|
calculate(candles) {
|
||||||
const fast = this.params.fast || 12;
|
const fast = this.params.fast || 12;
|
||||||
const slow = this.params.slow || 26;
|
const slow = this.params.slow || 26;
|
||||||
const signal = this.params.signal || 9;
|
const signalPeriod = this.params.signal || 9;
|
||||||
|
|
||||||
const fastEma = MA.ema(candles, fast, 'close');
|
const closes = candles.map(c => c.close);
|
||||||
const slowEma = MA.ema(candles, slow, 'close');
|
|
||||||
|
|
||||||
const macdLine = fastEma.map((f, i) => (f !== null && slowEma[i] !== null) ? f - slowEma[i] : null);
|
// Use inline EMA calculation instead of MA.ema()
|
||||||
|
const fastEMA = calculateEMAInline(closes, fast);
|
||||||
|
const slowEMA = calculateEMAInline(closes, slow);
|
||||||
|
|
||||||
|
const macdLine = fastEMA.map((f, i) => (f !== null && slowEMA[i] !== null) ? f - slowEMA[i] : null);
|
||||||
|
|
||||||
const signalLine = new Array(candles.length).fill(null);
|
|
||||||
const multiplier = 2 / (signal + 1);
|
|
||||||
let ema = 0;
|
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
|
let ema = 0;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
for (let i = 0; i < macdLine.length; i++) {
|
const signalLine = macdLine.map(m => {
|
||||||
if (macdLine[i] === null) continue;
|
if (m === null) return null;
|
||||||
count++;
|
count++;
|
||||||
if (count < signal) {
|
if (count < signalPeriod) {
|
||||||
sum += macdLine[i];
|
sum += m;
|
||||||
} else if (count === signal) {
|
return null;
|
||||||
sum += macdLine[i];
|
} else if (count === signalPeriod) {
|
||||||
ema = sum / signal;
|
sum += m;
|
||||||
signalLine[i] = ema;
|
ema = sum / signalPeriod;
|
||||||
|
return ema;
|
||||||
} else {
|
} else {
|
||||||
ema = (macdLine[i] - ema) * multiplier + ema;
|
ema = (m - ema) * (2 / (signalPeriod + 1)) + ema;
|
||||||
signalLine[i] = ema;
|
return ema;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return macdLine.map((m, i) => ({
|
return macdLine.map((m, i) => ({
|
||||||
macd: m,
|
macd: m,
|
||||||
@ -58,3 +149,5 @@ export class MACDIndicator extends BaseIndicator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateMACDSignal };
|
||||||
221
src/api/dashboard/static/js/indicators/moving_average.js
Normal file
221
src/api/dashboard/static/js/indicators/moving_average.js
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
// Self-contained Moving Average indicator with SMA/EMA/RMA/WMA/VWMA support
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moving Average math (SMA/EMA/RMA/WMA/VWMA)
|
||||||
|
function calculateSMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i >= period) sum -= candles[i - period][source];
|
||||||
|
if (i >= period - 1) results[i] = sum / period;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateEMA(candles, period, source = 'close') {
|
||||||
|
const multiplier = 2 / (period + 1);
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let ema = 0;
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
if (i < period) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i === period - 1) {
|
||||||
|
ema = sum / period;
|
||||||
|
results[i] = ema;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ema = (candles[i][source] - ema) * multiplier + ema;
|
||||||
|
results[i] = ema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRMA(candles, period, source = 'close') {
|
||||||
|
const multiplier = 1 / period;
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let rma = 0;
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
if (i < period) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i === period - 1) {
|
||||||
|
rma = sum / period;
|
||||||
|
results[i] = rma;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rma = (candles[i][source] - rma) * multiplier + rma;
|
||||||
|
results[i] = rma;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateWMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
const weightSum = (period * (period + 1)) / 2;
|
||||||
|
|
||||||
|
for (let i = period - 1; i < candles.length; i++) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let j = 0; j < period; j++) {
|
||||||
|
sum += candles[i - j][source] * (period - j);
|
||||||
|
}
|
||||||
|
results[i] = sum / weightSum;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateVWMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
|
||||||
|
for (let i = period - 1; i < candles.length; i++) {
|
||||||
|
let sumPV = 0;
|
||||||
|
let sumV = 0;
|
||||||
|
for (let j = 0; j < period; j++) {
|
||||||
|
sumPV += candles[i - j][source] * candles[i - j].volume;
|
||||||
|
sumV += candles[i - j].volume;
|
||||||
|
}
|
||||||
|
results[i] = sumV !== 0 ? sumPV / sumV : null;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for Moving Average
|
||||||
|
function calculateMASignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
const ma = values?.ma;
|
||||||
|
const prevMa = prevValues?.ma;
|
||||||
|
|
||||||
|
if (!ma && ma !== 0) return null;
|
||||||
|
if (prevClose === undefined || prevMa === undefined || prevMa === null) return null;
|
||||||
|
|
||||||
|
// BUY: Price crosses UP through MA
|
||||||
|
if (prevClose <= prevMa && close > ma) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 80,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed UP through MA`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL: Price crosses DOWN through MA
|
||||||
|
else if (prevClose >= prevMa && close < ma) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 80,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed DOWN through MA`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MA Indicator class
|
||||||
|
export class MAIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles) {
|
||||||
|
const maType = (this.params.maType || 'SMA').toLowerCase();
|
||||||
|
const period = this.params.period || 44;
|
||||||
|
|
||||||
|
let maValues;
|
||||||
|
|
||||||
|
switch (maType) {
|
||||||
|
case 'sma':
|
||||||
|
maValues = calculateSMA(candles, period, this.params.source || 'close');
|
||||||
|
break;
|
||||||
|
case 'ema':
|
||||||
|
maValues = calculateEMA(candles, period, this.params.source || 'close');
|
||||||
|
break;
|
||||||
|
case 'rma':
|
||||||
|
maValues = calculateRMA(candles, period, this.params.source || 'close');
|
||||||
|
break;
|
||||||
|
case 'wma':
|
||||||
|
maValues = calculateWMA(candles, period, this.params.source || 'close');
|
||||||
|
break;
|
||||||
|
case 'vwma':
|
||||||
|
maValues = calculateVWMA(candles, period, this.params.source || 'close');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
maValues = calculateSMA(candles, period, this.params.source || 'close');
|
||||||
|
}
|
||||||
|
|
||||||
|
return maValues.map(ma => ({ ma }));
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
return {
|
||||||
|
name: 'MA',
|
||||||
|
description: 'Moving Average (SMA/EMA/RMA/WMA/VWMA)',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
name: 'period',
|
||||||
|
label: 'Period',
|
||||||
|
type: 'number',
|
||||||
|
default: 44,
|
||||||
|
min: 1,
|
||||||
|
max: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maType',
|
||||||
|
label: 'MA Type',
|
||||||
|
type: 'select',
|
||||||
|
options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'],
|
||||||
|
default: 'SMA'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plots: [
|
||||||
|
{
|
||||||
|
id: 'ma',
|
||||||
|
color: '#2962ff',
|
||||||
|
title: 'MA',
|
||||||
|
style: 'solid',
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
displayMode: 'overlay'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export signal function for external use
|
||||||
|
export { calculateMASignal };
|
||||||
@ -1,41 +1,141 @@
|
|||||||
import { BaseIndicator } from './base.js';
|
// Self-contained RSI indicator
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for RSI
|
||||||
|
function calculateRSISignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const rsi = values?.rsi;
|
||||||
|
const prevRsi = prevValues?.rsi;
|
||||||
|
const overbought = indicator.params?.overbought || 70;
|
||||||
|
const oversold = indicator.params?.oversold || 30;
|
||||||
|
|
||||||
|
if (rsi === undefined || rsi === null || prevRsi === undefined || prevRsi === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUY when RSI crosses UP through oversold level
|
||||||
|
if (prevRsi < oversold && rsi >= oversold) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 75,
|
||||||
|
value: rsi,
|
||||||
|
reasoning: `RSI crossed UP through oversold level (${oversold})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL when RSI crosses DOWN through overbought level
|
||||||
|
else if (prevRsi > overbought && rsi <= overbought) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 75,
|
||||||
|
value: rsi,
|
||||||
|
reasoning: `RSI crossed DOWN through overbought level (${overbought})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSI Indicator class
|
||||||
export class RSIIndicator extends BaseIndicator {
|
export class RSIIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles) {
|
calculate(candles) {
|
||||||
const period = this.params.period || 14;
|
const period = this.params.period || 14;
|
||||||
const results = new Array(candles.length).fill(null);
|
const overbought = this.params.overbought || 70;
|
||||||
let gains = 0, losses = 0;
|
const oversold = this.params.oversold || 30;
|
||||||
|
|
||||||
|
// 1. Calculate RSI using RMA (Wilder's Smoothing)
|
||||||
|
let rsiValues = new Array(candles.length).fill(null);
|
||||||
|
let upSum = 0;
|
||||||
|
let downSum = 0;
|
||||||
|
const rmaAlpha = 1 / period;
|
||||||
|
|
||||||
for (let i = 1; i < candles.length; i++) {
|
for (let i = 1; i < candles.length; i++) {
|
||||||
const diff = candles[i].close - candles[i-1].close;
|
const diff = candles[i].close - candles[i-1].close;
|
||||||
const gain = diff > 0 ? diff : 0;
|
const up = diff > 0 ? diff : 0;
|
||||||
const loss = diff < 0 ? -diff : 0;
|
const down = diff < 0 ? -diff : 0;
|
||||||
if (i <= period) {
|
|
||||||
gains += gain;
|
if (i < period) {
|
||||||
losses += loss;
|
upSum += up;
|
||||||
if (i === period) {
|
downSum += down;
|
||||||
let avgGain = gains / period;
|
} else if (i === period) {
|
||||||
let avgLoss = losses / period;
|
upSum += up;
|
||||||
results[i] = 100 - (100 / (1 + (avgGain / (avgLoss || 0.00001))));
|
downSum += down;
|
||||||
}
|
const avgUp = upSum / period;
|
||||||
|
const avgDown = downSum / period;
|
||||||
|
rsiValues[i] = avgDown === 0 ? 100 : (avgUp === 0 ? 0 : 100 - (100 / (1 + avgUp / avgDown)));
|
||||||
|
upSum = avgUp;
|
||||||
|
downSum = avgDown;
|
||||||
} else {
|
} else {
|
||||||
const lastAvgGain = (results[i-1] ? (results[i-1] > 0 ? (period-1) * (results[i-1] * (gains+losses)/100) : 0) : 0);
|
upSum = (up - upSum) * rmaAlpha + upSum;
|
||||||
gains = (gains * (period - 1) + gain) / period;
|
downSum = (down - downSum) * rmaAlpha + downSum;
|
||||||
losses = (losses * (period - 1) + loss) / period;
|
rsiValues[i] = downSum === 0 ? 100 : (upSum === 0 ? 0 : 100 - (100 / (1 + upSum / downSum)));
|
||||||
results[i] = 100 - (100 / (1 + (gains / (losses || 0.00001))));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
|
||||||
|
// Combine results
|
||||||
|
return rsiValues.map((rsi, i) => {
|
||||||
|
return {
|
||||||
|
paneBg: 80,
|
||||||
|
rsi: rsi,
|
||||||
|
overboughtBand: overbought,
|
||||||
|
oversoldBand: oversold
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetadata() {
|
getMetadata() {
|
||||||
return {
|
return {
|
||||||
name: 'RSI',
|
name: 'RSI',
|
||||||
description: 'Relative Strength Index - momentum oscillator (0-100)',
|
description: 'Relative Strength Index',
|
||||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
|
inputs: [
|
||||||
plots: [{ id: 'value', color: '#9c27b0', title: 'RSI' }],
|
{ name: 'period', label: 'RSI Length', type: 'number', default: 14, min: 1, max: 100 },
|
||||||
|
{ name: 'overbought', label: 'Overbought Level', type: 'number', default: 70, min: 50, max: 95 },
|
||||||
|
{ name: 'oversold', label: 'Oversold Level', type: 'number', default: 30, min: 5, max: 50 }
|
||||||
|
],
|
||||||
|
plots: [
|
||||||
|
{ id: 'rsi', color: '#7E57C2', title: '', style: 'solid', width: 1, lastValueVisible: true },
|
||||||
|
{ id: 'overboughtBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false },
|
||||||
|
{ id: 'oversoldBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false }
|
||||||
|
],
|
||||||
displayMode: 'pane',
|
displayMode: 'pane',
|
||||||
paneMin: 0,
|
paneMin: 0,
|
||||||
paneMax: 100
|
paneMax: 100
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateRSISignal };
|
||||||
@ -1,6 +1,79 @@
|
|||||||
import { BaseIndicator } from './base.js';
|
// Self-contained Stochastic Oscillator indicator
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for Stochastic
|
||||||
|
function calculateStochSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const k = values?.k;
|
||||||
|
const d = values?.d;
|
||||||
|
const prevK = prevValues?.k;
|
||||||
|
const prevD = prevValues?.d;
|
||||||
|
const overbought = indicator.params?.overbought || 80;
|
||||||
|
const oversold = indicator.params?.oversold || 20;
|
||||||
|
|
||||||
|
if (k === undefined || d === undefined || prevK === undefined || prevD === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUY: %K crosses UP through %D while both are oversold
|
||||||
|
if (prevK <= prevD && k > d && k < oversold) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 80,
|
||||||
|
value: k,
|
||||||
|
reasoning: `Stochastic %K crossed UP through %D in oversold zone`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL: %K crosses DOWN through %D while both are overbought
|
||||||
|
else if (prevK >= prevD && k < d && k > overbought) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 80,
|
||||||
|
value: k,
|
||||||
|
reasoning: `Stochastic %K crossed DOWN through %D in overbought zone`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stochastic Oscillator Indicator class
|
||||||
export class StochasticIndicator extends BaseIndicator {
|
export class StochasticIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles) {
|
calculate(candles) {
|
||||||
const kPeriod = this.params.kPeriod || 14;
|
const kPeriod = this.params.kPeriod || 14;
|
||||||
const dPeriod = this.params.dPeriod || 3;
|
const dPeriod = this.params.dPeriod || 3;
|
||||||
@ -33,12 +106,28 @@ export class StochasticIndicator extends BaseIndicator {
|
|||||||
name: 'Stochastic',
|
name: 'Stochastic',
|
||||||
description: 'Stochastic Oscillator - compares close to high-low range',
|
description: 'Stochastic Oscillator - compares close to high-low range',
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: 'kPeriod', label: 'K Period', type: 'number', default: 14 },
|
{
|
||||||
{ name: 'dPeriod', label: 'D Period', type: 'number', default: 3 }
|
name: 'kPeriod',
|
||||||
|
label: '%K Period',
|
||||||
|
type: 'number',
|
||||||
|
default: 14,
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
description: 'Lookback period for %K calculation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dPeriod',
|
||||||
|
label: '%D Period',
|
||||||
|
type: 'number',
|
||||||
|
default: 3,
|
||||||
|
min: 1,
|
||||||
|
max: 20,
|
||||||
|
description: 'Smoothing period for %D (SMA of %K)'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
plots: [
|
plots: [
|
||||||
{ id: 'k', color: '#3f51b5', title: '%K' },
|
{ id: 'k', color: '#3f51b5', title: '%K', style: 'solid', width: 1 },
|
||||||
{ id: 'd', color: '#ff9800', title: '%D' }
|
{ id: 'd', color: '#ff9800', title: '%D', style: 'solid', width: 1 }
|
||||||
],
|
],
|
||||||
displayMode: 'pane',
|
displayMode: 'pane',
|
||||||
paneMin: 0,
|
paneMin: 0,
|
||||||
@ -46,3 +135,5 @@ export class StochasticIndicator extends BaseIndicator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateStochSignal };
|
||||||
@ -1,4 +1,12 @@
|
|||||||
import { INTERVALS, COLORS } from '../core/index.js';
|
import { INTERVALS, COLORS } from '../core/index.js';
|
||||||
|
import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals-calculator.js';
|
||||||
|
import { calculateSignalMarkers } from './signal-markers.js';
|
||||||
|
import { updateIndicatorCandles } from './indicators-panel-new.js';
|
||||||
|
import { TimezoneConfig } from '../config/timezone.js';
|
||||||
|
|
||||||
|
function formatDate(timestamp) {
|
||||||
|
return TimezoneConfig.formatDate(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
export class TradingDashboard {
|
export class TradingDashboard {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -10,10 +18,37 @@ 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.lastCandleTimestamp = null;
|
||||||
|
this.simulationMarkers = [];
|
||||||
|
this.avgPriceSeries = null;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSimulationMarkers(markers) {
|
||||||
|
this.simulationMarkers = markers || [];
|
||||||
|
this.updateSignalMarkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSimulationMarkers() {
|
||||||
|
this.simulationMarkers = [];
|
||||||
|
this.updateSignalMarkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvgPriceData(data) {
|
||||||
|
if (this.avgPriceSeries) {
|
||||||
|
this.avgPriceSeries.setData(data || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAvgPriceData() {
|
||||||
|
if (this.avgPriceSeries) {
|
||||||
|
this.avgPriceSeries.setData([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.createTimeframeButtons();
|
this.createTimeframeButtons();
|
||||||
this.initChart();
|
this.initChart();
|
||||||
@ -62,28 +97,34 @@ export class TradingDashboard {
|
|||||||
background: { color: COLORS.tvBg },
|
background: { color: COLORS.tvBg },
|
||||||
textColor: COLORS.tvText,
|
textColor: COLORS.tvText,
|
||||||
panes: {
|
panes: {
|
||||||
|
background: { color: '#1e222d' },
|
||||||
separatorColor: '#2a2e39',
|
separatorColor: '#2a2e39',
|
||||||
separatorHoverColor: '#363c4e',
|
separatorHoverColor: '#363c4e',
|
||||||
enableResize: true
|
enableResize: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
vertLines: { color: COLORS.tvBorder },
|
vertLines: { color: '#363d4e' },
|
||||||
horzLines: { color: COLORS.tvBorder },
|
horzLines: { color: '#363d4e' },
|
||||||
},
|
|
||||||
crosshair: {
|
|
||||||
mode: LightweightCharts.CrosshairMode.Normal,
|
|
||||||
},
|
},
|
||||||
rightPriceScale: {
|
rightPriceScale: {
|
||||||
borderColor: COLORS.tvBorder,
|
borderColor: '#363d4e',
|
||||||
autoScale: true,
|
autoScale: true,
|
||||||
},
|
},
|
||||||
timeScale: {
|
timeScale: {
|
||||||
borderColor: COLORS.tvBorder,
|
borderColor: '#363d4e',
|
||||||
timeVisible: true,
|
timeVisible: true,
|
||||||
secondsVisible: false,
|
secondsVisible: false,
|
||||||
rightOffset: 12,
|
rightOffset: 12,
|
||||||
barSpacing: 10,
|
barSpacing: 10,
|
||||||
|
tickMarkFormatter: (time, tickMarkType, locale) => {
|
||||||
|
return TimezoneConfig.formatTickMark(time);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
localization: {
|
||||||
|
timeFormatter: (timestamp) => {
|
||||||
|
return TimezoneConfig.formatDate(timestamp * 1000);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handleScroll: {
|
handleScroll: {
|
||||||
vertTouchDrag: false,
|
vertTouchDrag: false,
|
||||||
@ -101,6 +142,16 @@ export class TradingDashboard {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: '#00bcd4',
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||||
|
lastValueVisible: true,
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false,
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
|
||||||
this.currentPriceLine = this.candleSeries.createPriceLine({
|
this.currentPriceLine = this.candleSeries.createPriceLine({
|
||||||
price: 0,
|
price: 0,
|
||||||
color: '#26a69a',
|
color: '#26a69a',
|
||||||
@ -286,12 +337,30 @@ export class TradingDashboard {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearIndicatorCaches(clearSignalState = false) {
|
||||||
|
const activeIndicators = window.getActiveIndicators?.() || [];
|
||||||
|
activeIndicators.forEach(indicator => {
|
||||||
|
// Always clear calculation caches
|
||||||
|
indicator.cachedResults = null;
|
||||||
|
indicator.cachedMeta = null;
|
||||||
|
|
||||||
|
// Only clear signal state if explicitly requested (e.g., timeframe change)
|
||||||
|
// Do not clear on new candle completion - preserve signal change tracking
|
||||||
|
if (clearSignalState) {
|
||||||
|
indicator.lastSignalTimestamp = null;
|
||||||
|
indicator.lastSignalType = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`[Dashboard] Cleared caches for ${activeIndicators.length} indicators (signals: ${clearSignalState})`);
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
this.loadTA();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadData(limit = 1000, fitToContent = false) {
|
async loadData(limit = 1000, fitToContent = false) {
|
||||||
@ -310,7 +379,8 @@ export class TradingDashboard {
|
|||||||
open: parseFloat(c.open),
|
open: parseFloat(c.open),
|
||||||
high: parseFloat(c.high),
|
high: parseFloat(c.high),
|
||||||
low: parseFloat(c.low),
|
low: parseFloat(c.low),
|
||||||
close: parseFloat(c.close)
|
close: parseFloat(c.close),
|
||||||
|
volume: parseFloat(c.volume || 0)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const existingData = this.allData.get(this.currentInterval) || [];
|
const existingData = this.allData.get(this.currentInterval) || [];
|
||||||
@ -328,6 +398,8 @@ export class TradingDashboard {
|
|||||||
const latest = mergedData[mergedData.length - 1];
|
const latest = mergedData[mergedData.length - 1];
|
||||||
this.updateStats(latest);
|
this.updateStats(latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.drawIndicatorsOnChart?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading data:', error);
|
console.error('Error loading data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -355,9 +427,23 @@ export class TradingDashboard {
|
|||||||
open: parseFloat(c.open),
|
open: parseFloat(c.open),
|
||||||
high: parseFloat(c.high),
|
high: parseFloat(c.high),
|
||||||
low: parseFloat(c.low),
|
low: parseFloat(c.low),
|
||||||
close: parseFloat(c.close)
|
close: parseFloat(c.close),
|
||||||
|
volume: parseFloat(c.volume || 0)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const latest = chartData[chartData.length - 1];
|
||||||
|
|
||||||
|
// Check if new candle detected
|
||||||
|
const isNewCandle = this.lastCandleTimestamp !== null && latest.time > this.lastCandleTimestamp;
|
||||||
|
|
||||||
|
if (isNewCandle) {
|
||||||
|
console.log(`[NewData Load] New candle detected: ${this.lastCandleTimestamp} -> ${latest.time}`);
|
||||||
|
// Clear indicator caches but preserve signal state
|
||||||
|
this.clearIndicatorCaches(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastCandleTimestamp = latest.time;
|
||||||
|
|
||||||
chartData.forEach(candle => {
|
chartData.forEach(candle => {
|
||||||
if (candle.time >= lastTimestamp) {
|
if (candle.time >= lastTimestamp) {
|
||||||
this.candleSeries.update(candle);
|
this.candleSeries.update(candle);
|
||||||
@ -367,12 +453,19 @@ export class TradingDashboard {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
const latest = chartData[chartData.length - 1];
|
|
||||||
this.updateStats(latest);
|
this.updateStats(latest);
|
||||||
|
|
||||||
|
//console.log('[Chart] Calling drawIndicatorsOnChart after new data');
|
||||||
|
window.drawIndicatorsOnChart?.();
|
||||||
|
window.updateIndicatorCandles?.();
|
||||||
|
|
||||||
|
await this.loadSignals();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading new data:', error);
|
console.error('Error loading new data:', error);
|
||||||
@ -397,6 +490,8 @@ export class TradingDashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -405,14 +500,27 @@ export class TradingDashboard {
|
|||||||
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) {
|
||||||
@ -421,6 +529,8 @@ export class TradingDashboard {
|
|||||||
}
|
}
|
||||||
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);
|
||||||
|
|
||||||
@ -440,40 +550,165 @@ export class TradingDashboard {
|
|||||||
open: parseFloat(c.open),
|
open: parseFloat(c.open),
|
||||||
high: parseFloat(c.high),
|
high: parseFloat(c.high),
|
||||||
low: parseFloat(c.low),
|
low: parseFloat(c.low),
|
||||||
close: parseFloat(c.close)
|
close: parseFloat(c.close),
|
||||||
|
volume: parseFloat(c.volume || 0)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const existingData = this.allData.get(this.currentInterval) || [];
|
const existingData = this.allData.get(this.currentInterval) || [];
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTA() {
|
async loadTA() {
|
||||||
|
if (!this.hasInitialLoad) {
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
document.getElementById('taContent').innerHTML = `<div class="ta-loading">Loading technical analysis... ${time}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/ta?symbol=BTC&interval=${this.currentInterval}`);
|
const response = await fetch(`/api/v1/ta?symbol=BTC&interval=${this.currentInterval}`);
|
||||||
this.taData = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
document.getElementById('taContent').innerHTML = `<div class="ta-error">${data.error}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
document.getElementById('taContent').innerHTML = '<div class="ta-error">Failed to load technical analysis</div>';
|
document.getElementById('taContent').innerHTML = '<div class="ta-error">Failed to load technical analysis. Please check if the database has candle data.</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadSignals() {
|
||||||
|
try {
|
||||||
|
this.indicatorSignals = calculateAllIndicatorSignals();
|
||||||
|
this.summarySignal = calculateSummarySignal(this.indicatorSignals);
|
||||||
|
this.updateSignalMarkers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading signals:', error);
|
||||||
|
this.indicatorSignals = [];
|
||||||
|
this.summarySignal = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSignalMarkers() {
|
||||||
|
const candles = this.allData.get(this.currentInterval);
|
||||||
|
if (!candles || candles.length === 0) return;
|
||||||
|
|
||||||
|
let markers = calculateSignalMarkers(candles);
|
||||||
|
|
||||||
|
// Merge simulation markers if present
|
||||||
|
if (this.simulationMarkers && this.simulationMarkers.length > 0) {
|
||||||
|
markers = [...markers, ...this.simulationMarkers];
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL: Filter out any markers with invalid timestamps before passing to chart
|
||||||
|
markers = markers.filter(m => m && m.time !== null && m.time !== undefined && !isNaN(m.time));
|
||||||
|
|
||||||
|
// Re-sort combined markers by time
|
||||||
|
markers.sort((a, b) => a.time - b.time);
|
||||||
|
|
||||||
|
// If we have a marker controller, update markers through it
|
||||||
|
if (this.markerController) {
|
||||||
|
try {
|
||||||
|
this.markerController.setMarkers(markers);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[SignalMarkers] setMarkers error:', e.message);
|
||||||
|
this.markerController = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear price lines
|
||||||
|
if (this.markerPriceLines) {
|
||||||
|
this.markerPriceLines.forEach(ml => {
|
||||||
|
try { this.candleSeries.removePriceLine(ml); } catch (e) {}
|
||||||
|
});
|
||||||
|
this.markerPriceLines = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markers.length === 0) return;
|
||||||
|
|
||||||
|
// Create new marker controller
|
||||||
|
if (typeof LightweightCharts.createSeriesMarkers === 'function') {
|
||||||
|
try {
|
||||||
|
this.markerController = LightweightCharts.createSeriesMarkers(this.candleSeries, markers);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[SignalMarkers] createSeriesMarkers error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: use price lines
|
||||||
|
this.addMarkerPriceLines(markers);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMarkerPriceLines(markers) {
|
||||||
|
if (this.markerPriceLines) {
|
||||||
|
this.markerPriceLines.forEach(ml => {
|
||||||
|
try { this.candleSeries.removePriceLine(ml); } catch (e) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.markerPriceLines = [];
|
||||||
|
|
||||||
|
const recentMarkers = markers.slice(-20);
|
||||||
|
|
||||||
|
recentMarkers.forEach(m => {
|
||||||
|
const isBuy = m.position === 'belowBar';
|
||||||
|
const price = isBuy ? this.getMarkerLowPrice(m.time) : this.getMarkerHighPrice(m.time);
|
||||||
|
|
||||||
|
const priceLine = this.candleSeries.createPriceLine({
|
||||||
|
price: price,
|
||||||
|
color: m.color,
|
||||||
|
lineWidth: 2,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||||
|
axisLabelVisible: true,
|
||||||
|
title: m.text
|
||||||
|
});
|
||||||
|
|
||||||
|
this.markerPriceLines.push(priceLine);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getMarkerLowPrice(time) {
|
||||||
|
const candles = this.allData.get(this.currentInterval);
|
||||||
|
const candle = candles?.find(c => c.time === time);
|
||||||
|
return candle ? candle.low * 0.995 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMarkerHighPrice(time) {
|
||||||
|
const candles = this.allData.get(this.currentInterval);
|
||||||
|
const candle = candles?.find(c => c.time === time);
|
||||||
|
return candle ? candle.high * 1.005 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
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>`;
|
||||||
@ -490,14 +725,40 @@ export class TradingDashboard {
|
|||||||
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 signalColor = indSignal.signal === 'buy' ? '#26a69a' : indSignal.signal === 'sell' ? '#ef5350' : '#787b86';
|
||||||
|
const lastSignalDate = indSignal.lastSignalDate ? formatDate(indSignal.lastSignalDate * 1000) : '-';
|
||||||
|
|
||||||
|
// Format params as "MA(44)" style
|
||||||
|
let paramsStr = '';
|
||||||
|
if (indSignal.params !== null && indSignal.params !== undefined) {
|
||||||
|
paramsStr = `(${indSignal.params})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="ta-ma-row" style="border-bottom: none; padding: 6px 0; align-items: center;">
|
||||||
|
<span class="ta-ma-label">${indSignal.name}${paramsStr}</span>
|
||||||
|
<span class="ta-ma-value" style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span style="font-size: 11px; padding: 2px 8px; min-width: 60px; text-align: center; background: ${signalColor}; color: white; border-radius: 3px;">${signalIcon} ${indSignal.signal.toUpperCase()}</span>
|
||||||
|
<span style="font-size: 10px; color: var(--tv-text-secondary);">${lastSignalDate}</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">
|
||||||
@ -519,21 +780,31 @@ export class TradingDashboard {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ta-section">
|
<div class="ta-section">
|
||||||
<div class="ta-section-title">Indicators</div>
|
<div class="ta-section-title">Support / Resistance</div>
|
||||||
<div id="indicatorList" class="indicator-list"></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>
|
||||||
|
|
||||||
<div class="ta-section" id="indicatorConfigPanel">
|
<div class="ta-section">
|
||||||
<div class="ta-section-title">Configuration</div>
|
<div class="ta-section-title">Price Position</div>
|
||||||
<div id="configForm" style="margin-top: 8px;"></div>
|
<div class="ta-position-bar">
|
||||||
<div style="display: flex; gap: 8px; margin-top: 12px;" id="configButtons">
|
<div class="ta-position-marker" style="left: ${Math.min(Math.max(data.levels.position_in_range, 5), 95)}%"></div>
|
||||||
<button class="ta-btn" onclick="applyIndicatorConfig()" style="flex: 1; font-size: 11px; background: var(--tv-blue); color: white; border: none;">Apply</button>
|
</div>
|
||||||
<button class="ta-btn" onclick="removeIndicator()" style="flex: 1; font-size: 11px; border-color: var(--tv-red); color: var(--tv-red);">Remove</button>
|
<div class="ta-strength" style="margin-top: 8px; font-size: 11px;">
|
||||||
|
${data.levels.position_in_range.toFixed(0)}% in range
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
window.renderIndicatorList?.();
|
renderSignalsSection() {
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadStats() {
|
async loadStats() {
|
||||||
@ -571,6 +842,7 @@ export class TradingDashboard {
|
|||||||
switchTimeframe(interval) {
|
switchTimeframe(interval) {
|
||||||
if (!this.intervals.includes(interval) || interval === this.currentInterval) return;
|
if (!this.intervals.includes(interval) || interval === this.currentInterval) return;
|
||||||
|
|
||||||
|
const oldInterval = this.currentInterval;
|
||||||
this.currentInterval = interval;
|
this.currentInterval = interval;
|
||||||
this.hasInitialLoad = false;
|
this.hasInitialLoad = false;
|
||||||
|
|
||||||
@ -578,9 +850,14 @@ export class TradingDashboard {
|
|||||||
btn.classList.toggle('active', btn.dataset.interval === interval);
|
btn.classList.toggle('active', btn.dataset.interval === interval);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.allData.delete(interval);
|
// Clear indicator caches and signal state before switching timeframe
|
||||||
|
this.clearIndicatorCaches(true);
|
||||||
|
|
||||||
|
// Clear old interval data, not new interval
|
||||||
|
this.allData.delete(oldInterval);
|
||||||
|
this.lastCandleTimestamp = null;
|
||||||
|
|
||||||
this.loadInitialData();
|
this.loadInitialData();
|
||||||
this.loadTA();
|
|
||||||
|
|
||||||
window.clearSimulationResults?.();
|
window.clearSimulationResults?.();
|
||||||
window.updateTimeframeDisplay?.();
|
window.updateTimeframeDisplay?.();
|
||||||
@ -589,6 +866,8 @@ export class TradingDashboard {
|
|||||||
|
|
||||||
export function refreshTA() {
|
export function refreshTA() {
|
||||||
if (window.dashboard) {
|
if (window.dashboard) {
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
document.getElementById('taContent').innerHTML = `<div class="ta-loading">Refreshing... ${time}</div>`;
|
||||||
window.dashboard.loadTA();
|
window.dashboard.loadTA();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
231
src/api/dashboard/static/js/ui/hts-visualizer.js
Normal file
231
src/api/dashboard/static/js/ui/hts-visualizer.js
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
const HTS_COLORS = {
|
||||||
|
fastHigh: '#00bcd4',
|
||||||
|
fastLow: '#00bcd4',
|
||||||
|
slowHigh: '#f44336',
|
||||||
|
slowLow: '#f44336',
|
||||||
|
bullishZone: 'rgba(38, 166, 154, 0.1)',
|
||||||
|
bearishZone: 'rgba(239, 83, 80, 0.1)',
|
||||||
|
channelRegion: 'rgba(41, 98, 255, 0.05)'
|
||||||
|
};
|
||||||
|
|
||||||
|
let HTSOverlays = [];
|
||||||
|
|
||||||
|
export class HTSVisualizer {
|
||||||
|
constructor(chart, candles) {
|
||||||
|
this.chart = chart;
|
||||||
|
this.candles = candles;
|
||||||
|
this.overlays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.overlays.forEach(overlay => {
|
||||||
|
try {
|
||||||
|
this.chart.removeSeries(overlay.series);
|
||||||
|
} catch (e) { }
|
||||||
|
});
|
||||||
|
this.overlays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addHTSChannels(htsData, isAutoHTS = false) {
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
if (!htsData || htsData.length === 0) return;
|
||||||
|
|
||||||
|
const alpha = isAutoHTS ? 0.3 : 0.3;
|
||||||
|
const lineWidth = isAutoHTS ? 1 : 2;
|
||||||
|
|
||||||
|
const fastHighSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: `rgba(0, 188, 212, ${alpha})`,
|
||||||
|
lineWidth: lineWidth,
|
||||||
|
lastValueVisible: false,
|
||||||
|
title: 'HTS Fast High' + (isAutoHTS ? ' (Auto)' : ''),
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const fastLowSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: `rgba(0, 188, 212, ${alpha})`,
|
||||||
|
lineWidth: lineWidth,
|
||||||
|
lastValueVisible: false,
|
||||||
|
title: 'HTS Fast Low' + (isAutoHTS ? ' (Auto)' : ''),
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const slowHighSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: `rgba(244, 67, 54, ${alpha})`,
|
||||||
|
lineWidth: lineWidth + 1,
|
||||||
|
lastValueVisible: false,
|
||||||
|
title: 'HTS Slow High' + (isAutoHTS ? ' (Auto)' : ''),
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const slowLowSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: `rgba(244, 67, 54, ${alpha})`,
|
||||||
|
lineWidth: lineWidth + 1,
|
||||||
|
lastValueVisible: false,
|
||||||
|
title: 'HTS Slow Low' + (isAutoHTS ? ' (Auto)' : ''),
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const fastHighData = htsData.map(h => ({ time: h.time, value: h.fastHigh }));
|
||||||
|
const fastLowData = htsData.map(h => ({ time: h.time, value: h.fastLow }));
|
||||||
|
const slowHighData = htsData.map(h => ({ time: h.time, value: h.slowHigh }));
|
||||||
|
const slowLowData = htsData.map(h => ({ time: h.time, value: h.slowLow }));
|
||||||
|
|
||||||
|
fastHighSeries.setData(fastHighData);
|
||||||
|
fastLowSeries.setData(fastLowData);
|
||||||
|
slowHighSeries.setData(slowHighData);
|
||||||
|
slowLowSeries.setData(slowLowData);
|
||||||
|
|
||||||
|
this.overlays.push(
|
||||||
|
{ series: fastHighSeries, name: 'fastHigh' },
|
||||||
|
{ series: fastLowSeries, name: 'fastLow' },
|
||||||
|
{ series: slowHighSeries, name: 'slowHigh' },
|
||||||
|
{ series: slowLowSeries, name: 'slowLow' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fastHigh: fastHighSeries,
|
||||||
|
fastLow: fastLowSeries,
|
||||||
|
slowHigh: slowHighSeries,
|
||||||
|
slowLow: slowLowSeries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
addTrendZones(htsData) {
|
||||||
|
if (!htsData || htsData.length < 2) return;
|
||||||
|
|
||||||
|
const trendZones = [];
|
||||||
|
let currentZone = null;
|
||||||
|
|
||||||
|
for (let i = 1; i < htsData.length; i++) {
|
||||||
|
const prev = htsData[i - 1];
|
||||||
|
const curr = htsData[i];
|
||||||
|
|
||||||
|
const prevBullish = prev.fastLow > prev.slowLow && prev.fastHigh > prev.slowHigh;
|
||||||
|
const currBullish = curr.fastLow > curr.slowLow && curr.fastHigh > curr.slowHigh;
|
||||||
|
|
||||||
|
const prevBearish = prev.fastLow < prev.slowLow && prev.fastHigh < prev.slowHigh;
|
||||||
|
const currBearish = curr.fastLow < curr.slowLow && curr.fastHigh < curr.slowHigh;
|
||||||
|
|
||||||
|
if (currBullish && !prevBullish) {
|
||||||
|
currentZone = { type: 'bullish', start: curr.time };
|
||||||
|
} else if (currBearish && !prevBearish) {
|
||||||
|
currentZone = { type: 'bearish', start: curr.time };
|
||||||
|
} else if (!currBullish && !currBearish && currentZone) {
|
||||||
|
currentZone.end = prev.time;
|
||||||
|
trendZones.push({ ...currentZone });
|
||||||
|
currentZone = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentZone) {
|
||||||
|
currentZone.end = htsData[htsData.length - 1].time;
|
||||||
|
trendZones.push(currentZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
trendZones.forEach(zone => {
|
||||||
|
const zoneSeries = this.chart.addSeries(LightweightCharts.AreaSeries, {
|
||||||
|
topColor: zone.type === 'bullish' ? 'rgba(38, 166, 154, 0.02)' : 'rgba(239, 83, 80, 0.02)',
|
||||||
|
bottomColor: zone.type === 'bullish' ? 'rgba(38, 166, 154, 0.02)' : 'rgba(239, 83, 80, 0.02)',
|
||||||
|
lineColor: 'transparent',
|
||||||
|
lastValueVisible: false,
|
||||||
|
priceLineVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.candles && this.candles.length > 0) {
|
||||||
|
const maxPrice = Math.max(...this.candles.map(c => c.high)) * 2;
|
||||||
|
const minPrice = Math.min(...this.candles.map(c => c.low)) * 0.5;
|
||||||
|
|
||||||
|
const startTime = zone.start || (this.candles[0]?.time);
|
||||||
|
const endTime = zone.end || (this.candles[this.candles.length - 1]?.time);
|
||||||
|
|
||||||
|
zoneSeries.setData([
|
||||||
|
{ time: startTime, value: minPrice },
|
||||||
|
{ time: startTime, value: maxPrice },
|
||||||
|
{ time: endTime, value: maxPrice },
|
||||||
|
{ time: endTime, value: minPrice }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.overlays.push({ series: zoneSeries, name: `trendZone_${zone.type}_${zone.start}` });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addCrossoverMarkers(htsData) {
|
||||||
|
if (!htsData || htsData.length < 2) return;
|
||||||
|
|
||||||
|
const markers = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < htsData.length; i++) {
|
||||||
|
const prev = htsData[i - 1];
|
||||||
|
const curr = htsData[i];
|
||||||
|
|
||||||
|
if (!prev || !curr) continue;
|
||||||
|
|
||||||
|
const price = curr.price;
|
||||||
|
|
||||||
|
const prevFastLow = prev.fastLow;
|
||||||
|
const currFastLow = curr.fastLow;
|
||||||
|
const prevFastHigh = prev.fastHigh;
|
||||||
|
const currFastHigh = curr.fastHigh;
|
||||||
|
const prevSlowLow = prev.slowLow;
|
||||||
|
const currSlowLow = curr.slowLow;
|
||||||
|
const prevSlowHigh = prev.slowHigh;
|
||||||
|
const currSlowHigh = curr.slowHigh;
|
||||||
|
|
||||||
|
if (prevFastLow <= prevSlowLow && currFastLow > currSlowLow && price > currSlowLow) {
|
||||||
|
markers.push({
|
||||||
|
time: curr.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: '#26a69a',
|
||||||
|
shape: 'arrowUp',
|
||||||
|
text: 'BUY',
|
||||||
|
size: 1.2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevFastHigh >= prevSlowHigh && currFastHigh < currSlowHigh && price < currSlowHigh) {
|
||||||
|
markers.push({
|
||||||
|
time: curr.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: '#ef5350',
|
||||||
|
shape: 'arrowDown',
|
||||||
|
text: 'SELL',
|
||||||
|
size: 1.2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const candleSeries = this.candleData?.series;
|
||||||
|
if (candleSeries && typeof candleSeries.setMarkers === 'function') {
|
||||||
|
candleSeries.setMarkers(markers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addHTSVisualization(chart, candleSeries, htsData, candles, isAutoHTS = false) {
|
||||||
|
const visualizer = new HTSVisualizer(chart, candles);
|
||||||
|
visualizer.candleData = { series: candleSeries };
|
||||||
|
visualizer.addHTSChannels(htsData, isAutoHTS);
|
||||||
|
|
||||||
|
// Disable trend zones to avoid visual clutter
|
||||||
|
// visualizer.addTrendZones(htsData);
|
||||||
|
|
||||||
|
if (window.showCrossoverMarkers !== false) {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
visualizer.addCrossoverMarkers(htsData);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Crossover markers skipped (API limitation):', e.message);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return visualizer;
|
||||||
|
}
|
||||||
@ -1,28 +1,5 @@
|
|||||||
export { TradingDashboard, refreshTA, openAIAnalysis } from './chart.js';
|
export { TradingDashboard, refreshTA, openAIAnalysis } from './chart.js';
|
||||||
export { toggleSidebar, restoreSidebarState } from './sidebar.js';
|
export { toggleSidebar, restoreSidebarState } from './sidebar.js';
|
||||||
export { SimulationStorage } from './storage.js';
|
|
||||||
export { showExportDialog, closeExportDialog, performExport, exportSavedSimulation } from './export.js';
|
|
||||||
export {
|
|
||||||
runSimulation,
|
|
||||||
displayEnhancedResults,
|
|
||||||
showSimulationMarkers,
|
|
||||||
clearSimulationMarkers,
|
|
||||||
clearSimulationResults,
|
|
||||||
getLastResults,
|
|
||||||
setLastResults
|
|
||||||
} from './simulation.js';
|
|
||||||
export {
|
|
||||||
renderStrategies,
|
|
||||||
selectStrategy,
|
|
||||||
renderStrategyParams,
|
|
||||||
loadStrategies,
|
|
||||||
saveSimulation,
|
|
||||||
renderSavedSimulations,
|
|
||||||
loadSavedSimulation,
|
|
||||||
deleteSavedSimulation,
|
|
||||||
getCurrentStrategy,
|
|
||||||
setCurrentStrategy
|
|
||||||
} from './strategies-panel.js';
|
|
||||||
export {
|
export {
|
||||||
renderIndicatorList,
|
renderIndicatorList,
|
||||||
addNewIndicator,
|
addNewIndicator,
|
||||||
|
|||||||
1157
src/api/dashboard/static/js/ui/indicators-panel-new.js
Normal file
1157
src/api/dashboard/static/js/ui/indicators-panel-new.js
Normal file
File diff suppressed because it is too large
Load Diff
868
src/api/dashboard/static/js/ui/indicators-panel-new.js.bak
Normal file
868
src/api/dashboard/static/js/ui/indicators-panel-new.js.bak
Normal file
@ -0,0 +1,868 @@
|
|||||||
|
import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js';
|
||||||
|
|
||||||
|
// State management
|
||||||
|
let activeIndicators = [];
|
||||||
|
let configuringId = null;
|
||||||
|
let searchQuery = '';
|
||||||
|
let selectedCategory = 'all';
|
||||||
|
let nextInstanceId = 1;
|
||||||
|
let listenersAttached = false; // Single flag to track if any listeners are attached
|
||||||
|
|
||||||
|
// Chart pane management
|
||||||
|
let indicatorPanes = new Map();
|
||||||
|
let nextPaneIndex = 1;
|
||||||
|
|
||||||
|
// Presets storage
|
||||||
|
let userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}');
|
||||||
|
|
||||||
|
// Categories
|
||||||
|
const CATEGORIES = [
|
||||||
|
{ id: 'all', name: 'All Indicators', icon: '📊' },
|
||||||
|
{ id: 'trend', name: 'Trend', icon: '📊' },
|
||||||
|
{ id: 'momentum', name: 'Momentum', icon: '📈' },
|
||||||
|
{ id: 'volatility', name: 'Volatility', icon: '📉' },
|
||||||
|
{ id: 'volume', name: 'Volume', icon: '🔀' },
|
||||||
|
{ id: 'favorites', name: 'Favorites', icon: '★' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const CATEGORY_MAP = {
|
||||||
|
sma: 'trend', ema: 'trend', hts: 'trend',
|
||||||
|
rsi: 'momentum', macd: 'momentum', stoch: 'momentum',
|
||||||
|
bb: 'volatility', atr: 'volatility',
|
||||||
|
others: 'volume'
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_COLORS = ['#2962ff', '#26a69a', '#ef5350', '#ff9800', '#9c27b0', '#00bcd4', '#ffeb3b', '#e91e63'];
|
||||||
|
const LINE_TYPES = ['solid', 'dotted', 'dashed'];
|
||||||
|
|
||||||
|
function getDefaultColor(index) {
|
||||||
|
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorCategory(indicator) {
|
||||||
|
return CATEGORY_MAP[indicator.type] || 'trend';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorLabel(indicator) {
|
||||||
|
const meta = getIndicatorMeta(indicator);
|
||||||
|
if (!meta) return indicator.name;
|
||||||
|
|
||||||
|
const paramParts = meta.inputs.map(input => {
|
||||||
|
const val = indicator.params[input.name];
|
||||||
|
if (val !== undefined && val !== input.default) return val;
|
||||||
|
return null;
|
||||||
|
}).filter(v => v !== null);
|
||||||
|
|
||||||
|
if (paramParts.length > 0) {
|
||||||
|
return `${indicator.name} (${paramParts.join(', ')})`;
|
||||||
|
}
|
||||||
|
return indicator.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorMeta(indicator) {
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) return null;
|
||||||
|
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||||
|
return instance.getMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupPlotsByColor(plots) {
|
||||||
|
const groups = {};
|
||||||
|
plots.forEach((plot, idx) => {
|
||||||
|
const groupMap = {
|
||||||
|
'fast': 'Fast', 'slow': 'Slow', 'upper': 'Upper', 'lower': 'Lower',
|
||||||
|
'middle': 'Middle', 'basis': 'Middle', 'signal': 'Signal',
|
||||||
|
'histogram': 'Histogram', 'k': '%K', 'd': '%D'
|
||||||
|
};
|
||||||
|
const groupName = Object.entries(groupMap).find(([k, v]) => plot.id.toLowerCase().includes(k))?.[1] || plot.id;
|
||||||
|
if (!groups[groupName]) {
|
||||||
|
groups[groupName] = { name: groupName, indices: [], plots: [] };
|
||||||
|
}
|
||||||
|
groups[groupName].indices.push(idx);
|
||||||
|
groups[groupName].plots.push(plot);
|
||||||
|
});
|
||||||
|
return Object.values(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initIndicatorPanel() {
|
||||||
|
console.log('[IndicatorPanel] Initializing...');
|
||||||
|
renderIndicatorPanel();
|
||||||
|
console.log('[IndicatorPanel] Initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveIndicators() {
|
||||||
|
return activeIndicators;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActiveIndicators(indicators) {
|
||||||
|
activeIndicators = indicators;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render main panel
|
||||||
|
export function renderIndicatorPanel() {
|
||||||
|
const container = document.getElementById('indicatorPanel');
|
||||||
|
if (!container) {
|
||||||
|
console.error('[IndicatorPanel] Container #indicatorPanel not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[IndicatorPanel] Rendering panel, searchQuery:', searchQuery, 'selectedCategory:', selectedCategory);
|
||||||
|
|
||||||
|
const available = getAvailableIndicators();
|
||||||
|
const catalog = available.filter(ind => {
|
||||||
|
if (searchQuery && !ind.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||||
|
if (selectedCategory === 'all') return true;
|
||||||
|
if (selectedCategory === 'favorites') return false;
|
||||||
|
const cat = CATEGORY_MAP[ind.type] || 'trend';
|
||||||
|
return cat === selectedCategory;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[IndicatorPanel] Total indicators:", available.length, "Filtered to:", catalog.length);
|
||||||
|
|
||||||
|
const favoriteIds = new Set(userPresets.favorites || []);
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="indicator-panel">
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="indicator-search">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="indicatorSearch"
|
||||||
|
placeholder="Search indicators..."
|
||||||
|
value="${searchQuery}"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
${searchQuery ? `<button class="search-clear">×</button>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Categories -->
|
||||||
|
<div class="category-tabs">
|
||||||
|
${CATEGORIES.map(cat => `
|
||||||
|
<button class="category-tab ${selectedCategory === cat.id ? 'active' : ''}" data-category="${cat.id}">
|
||||||
|
${cat.icon} ${cat.name}
|
||||||
|
</button>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Favorites (if any) -->
|
||||||
|
${[...favoriteIds].length > 0 ? `
|
||||||
|
<div class="indicator-section favorites">
|
||||||
|
<div class="section-title">★ Favorites</div>
|
||||||
|
${[...favoriteIds].map(id => {
|
||||||
|
const ind = available.find(a => {
|
||||||
|
return a.type === id || (activeIndicators.find(ai => ai.id === id)?.type === '');
|
||||||
|
});
|
||||||
|
if (!ind) return '';
|
||||||
|
return renderIndicatorItem(ind, true);
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Active Indicators -->
|
||||||
|
${activeIndicators.length > 0 ? `
|
||||||
|
<div class="indicator-section active">
|
||||||
|
<div class="section-title">
|
||||||
|
${activeIndicators.length} Active
|
||||||
|
${activeIndicators.length > 0 ? `<button class="clear-all">Clear All</button>` : ''}
|
||||||
|
</div>
|
||||||
|
${activeIndicators.slice().reverse().map(ind => renderActiveIndicator(ind)).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Available Indicators -->
|
||||||
|
${catalog.length > 0 ? `
|
||||||
|
<div class="indicator-section catalog">
|
||||||
|
<div class="section-title">Available Indicators</div>
|
||||||
|
${catalog.map(ind => renderIndicatorItem(ind, false)).join('')}
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<div class="no-results">
|
||||||
|
No indicators found
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Only setup event listeners once
|
||||||
|
if (!listenersAttached) {
|
||||||
|
setupEventListeners();
|
||||||
|
listenersAttached = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorItem(indicator, isFavorite) {
|
||||||
|
const colorDots = '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="indicator-item ${isFavorite ? 'favorite' : ''}" data-type="${indicator.type}">
|
||||||
|
<div class="indicator-item-main">
|
||||||
|
<span class="indicator-name">${indicator.name}</span>
|
||||||
|
<span class="indicator-desc">${indicator.description || ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="indicator-actions">
|
||||||
|
<button class="indicator-btn add" data-type="${indicator.type}" title="Add to chart">+</button>
|
||||||
|
${isFavorite ? '' : `
|
||||||
|
<button class="indicator-btn favorite" data-type="${indicator.type}" title="Add to favorites">
|
||||||
|
${userPresets.favorites?.includes(indicator.type) ? '★' : '☆'}
|
||||||
|
</button>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderActiveIndicator(indicator) {
|
||||||
|
const isExpanded = configuringId === indicator.id;
|
||||||
|
const meta = getIndicatorMeta(indicator);
|
||||||
|
const label = getIndicatorLabel(indicator);
|
||||||
|
const isFavorite = userPresets.favorites?.includes(indicator.type) || false;
|
||||||
|
const showPresets = meta.name && function() {
|
||||||
|
const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
|
||||||
|
if (!hasPresets || hasPresets.length === 0) return '';
|
||||||
|
return `<div class="indicator-presets">
|
||||||
|
<button class="preset-indicator" title="${hasPresets.length} saved presets">💾</button>
|
||||||
|
</div>`;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="indicator-item active ${isExpanded ? 'expanded' : ''}" data-id="${indicator.id}">
|
||||||
|
<div class="indicator-item-main" onclick="window.toggleIndicatorExpand && window.toggleIndicatorExpand('${indicator.id}');">
|
||||||
|
<div class="drag-handle" title="Drag to reorder">⋮⋮</div>
|
||||||
|
<button class="indicator-btn visible" onclick="event.stopPropagation(); window.toggleIndicatorVisibility && window.toggleIndicatorVisibility('${indicator.id}')" title="${indicator.visible !== false ? 'Hide' : 'Show'}">
|
||||||
|
${indicator.visible !== false ? '👁' : '👁🗨'}
|
||||||
|
</button>
|
||||||
|
<span class="indicator-name">${label}</span>
|
||||||
|
${showPresets}
|
||||||
|
<button class="indicator-btn favorite" onclick="event.stopPropagation(); window.toggleFavorite && window.toggleFavorite('${indicator.type}')" title="Add to favorites">
|
||||||
|
${isFavorite ? '★' : '☆'}
|
||||||
|
</button>
|
||||||
|
<button class="indicator-btn expand ${isExpanded ? 'rotated' : ''}" title="Show settings">
|
||||||
|
${isExpanded ? '▼' : '▶'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${isExpanded ? `
|
||||||
|
<div class="indicator-config">
|
||||||
|
${typeof renderIndicatorConfig === 'function' ? renderIndicatorConfig(indicator, meta) : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPresetIndicatorIndicator(meta, indicator) {
|
||||||
|
const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
|
||||||
|
if (!hasPresets || hasPresets.length === 0) return '';
|
||||||
|
|
||||||
|
return `<button class="preset-indicator" title="${hasPresets.length} saved presets" onclick="event.stopPropagation(); window.showPresets && window.showPresets('${meta.name}')">💾</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorConfig(indicator, meta) {
|
||||||
|
const plotGroups = groupPlotsByColor(meta?.plots || []);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="config-sections">
|
||||||
|
<!-- Colors -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">Visual Settings</div>
|
||||||
|
${plotGroups.map(group => {
|
||||||
|
const firstIdx = group.indices[0];
|
||||||
|
const color = indicator.params[`_color_${firstIdx}`] || meta.plots[firstIdx]?.color || getDefaultColor(activeIndicators.indexOf(indicator));
|
||||||
|
return `
|
||||||
|
<div class="config-row">
|
||||||
|
<label>${group.name} Color</label>
|
||||||
|
<div class="color-picker">
|
||||||
|
<input type="color" id="color_${indicator.id}_${firstIdx}" value="${color}" onchange="window.updateIndicatorColor && window.updateIndicatorColor('${indicator.id}', ${firstIdx}, this.value)">
|
||||||
|
<span class="color-preview" style="background: ${color};"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`.trim() + '';
|
||||||
|
}).join('')}
|
||||||
|
|
||||||
|
${indicator.type !== 'rsi' ? `
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Line Type</label>
|
||||||
|
<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '_lineType', this.value)">
|
||||||
|
${LINE_TYPES.map(lt => `<option value="${lt}" ${indicator.params._lineType === lt ? 'selected' : ''}>${lt.charAt(0).toUpperCase() + lt.slice(1)}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Line Width</label>
|
||||||
|
<input type="range" min="1" max="5" value="${indicator.params._lineWidth || 2}" onchange="this.nextElementSibling.textContent = this.value; window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '_lineWidth', parseInt(this.value))">
|
||||||
|
<span class="range-value">${indicator.params._lineWidth || 2}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${meta?.inputs && meta.inputs.length > 0 ? `
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">Parameters</div>
|
||||||
|
${meta.inputs.map(input => `
|
||||||
|
<div class="config-row">
|
||||||
|
<label>${input.label}</label>
|
||||||
|
${input.type === 'select' ?
|
||||||
|
`<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '${input.name}', this.value)">
|
||||||
|
${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}
|
||||||
|
</select>` :
|
||||||
|
`<input
|
||||||
|
type="number"
|
||||||
|
value="${indicator.params[input.name]}"
|
||||||
|
${input.min !== undefined ? `min="${input.min}"` : ''}
|
||||||
|
${input.max !== undefined ? `max="${input.max}"` : ''}
|
||||||
|
${input.step !== undefined ? `step="${input.step}"` : ''}
|
||||||
|
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '${input.name}', parseFloat(this.value))"
|
||||||
|
>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${meta?.inputs && meta.inputs.length > 0 ? `
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">Parameters</div>
|
||||||
|
${meta.inputs.map(input => `
|
||||||
|
${console.log("[DEBUG] Input:", input.name, "value:", indicator.params[input.name])}`
|
||||||
|
<div class="config-row">
|
||||||
|
<label>${input.label}</label>
|
||||||
|
${input.type === 'select' ?
|
||||||
|
`<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '${input.name}', this.value)">
|
||||||
|
${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}
|
||||||
|
</select>` :
|
||||||
|
`<input
|
||||||
|
type="number"
|
||||||
|
value="${indicator.params[input.name]}"
|
||||||
|
${input.min !== undefined ? `min="${input.min}"` : ''}
|
||||||
|
${input.max !== undefined ? `max="${input.max}"` : ''}
|
||||||
|
${input.step !== undefined ? `step="${input.step}"` : ''}
|
||||||
|
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '${input.name}', parseFloat(this.value))"
|
||||||
|
>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">
|
||||||
|
Presets
|
||||||
|
<button class="preset-action-btn" onclick="window.savePreset && window.savePreset('${indicator.id}')">+ Save Preset</button>
|
||||||
|
</div>
|
||||||
|
${typeof renderIndicatorPresets === 'function' ? renderIndicatorPresets(indicator, meta) : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-actions">
|
||||||
|
<button class="btn-secondary" onclick="window.resetIndicator && window.resetIndicator('${indicator.id}')">Reset to Defaults</button>
|
||||||
|
<button class="btn-danger" onclick="window.removeIndicator && window.removeIndicator('${indicator.id}')">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorPresets(indicator, meta) {
|
||||||
|
const presets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
|
||||||
|
|
||||||
|
return presets.length > 0 ? `
|
||||||
|
<div class="presets-list">
|
||||||
|
${presets.map(p => {
|
||||||
|
const isApplied = meta.inputs.every(input =>
|
||||||
|
(indicator.params[input.name] === (preset.values?.[input.name] ?? input.default))
|
||||||
|
);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="preset-item ${isApplied ? 'applied' : ''}" data-preset="${preset.id}">
|
||||||
|
<span class="preset-label" onclick="window.applyPreset && window.applyPreset('${indicator.id}', '${preset.id}')">${preset.name}</span>
|
||||||
|
<button class="preset-delete" onclick="window.deletePreset && window.deletePreset('${preset.id}')">×</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
` : '<div class="no-presets">No saved presets</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
function setupEventListeners() {
|
||||||
|
const container = document.getElementById('indicatorPanel');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
console.log('[IndicatorPanel] Setting up event listeners...');
|
||||||
|
|
||||||
|
// Single event delegation handler for add button
|
||||||
|
container.addEventListener('click', (e) => {
|
||||||
|
const addBtn = e.target.closest('.indicator-btn.add');
|
||||||
|
if (addBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const type = addBtn.dataset.type;
|
||||||
|
if (type && window.addIndicator) {
|
||||||
|
console.log('[IndicatorPanel] Add button clicked for type:', type);
|
||||||
|
window.addIndicator(type);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand/collapse button
|
||||||
|
const expandBtn = e.target.closest('.indicator-btn.expand');
|
||||||
|
if (expandBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const id = expandBtn.dataset.id;
|
||||||
|
if (id && window.toggleIndicatorExpand) {
|
||||||
|
window.toggleIndicatorExpand(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove button
|
||||||
|
const removeBtn = e.target.closest('.indicator-btn.remove');
|
||||||
|
if (removeBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const id = removeBtn.dataset.id;
|
||||||
|
if (id && window.removeIndicatorById) {
|
||||||
|
window.removeIndicatorById(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favorite button
|
||||||
|
const favoriteBtn = e.target.closest('.indicator-btn.favorite');
|
||||||
|
if (favoriteBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const type = favoriteBtn.dataset.type;
|
||||||
|
if (type && window.toggleFavorite) {
|
||||||
|
window.toggleFavorite(type);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search input
|
||||||
|
const searchInput = document.getElementById('indicatorSearch');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
searchQuery = e.target.value;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search clear button
|
||||||
|
const searchClear = container.querySelector('.search-clear');
|
||||||
|
if (searchClear) {
|
||||||
|
searchClear.addEventListener('click', (e) => {
|
||||||
|
searchQuery = '';
|
||||||
|
renderIndicatorPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category tabs
|
||||||
|
document.querySelectorAll('.category-tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', (e) => {
|
||||||
|
selectedCategory = tab.dataset.category;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear all button
|
||||||
|
const clearAllBtn = container.querySelector('.clear-all');
|
||||||
|
if (clearAllBtn) {
|
||||||
|
clearAllBtn.addEventListener('click', () => {
|
||||||
|
window.clearAllIndicators();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[IndicatorPanel] Event listeners setup complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
window.toggleIndicatorExpand = function(id) {
|
||||||
|
configuringId = configuringId === id ? null : id;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearSearch = function() {
|
||||||
|
searchQuery = '';
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateIndicatorColor = function(id, index, color) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
indicator.params[`_color_${index}`] = color;
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateIndicatorSetting = function(id, key, value) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
indicator.params[key] = value;
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearAllIndicators = function() {
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
ind.series?.forEach(s => {
|
||||||
|
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
activeIndicators = [];
|
||||||
|
configuringId = null;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIndicatorById(id) {
|
||||||
|
const idx = activeIndicators.findIndex(a => a.id === id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
activeIndicators[idx].series?.forEach(s => {
|
||||||
|
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
activeIndicators.splice(idx, 1);
|
||||||
|
|
||||||
|
if (configuringId === id) {
|
||||||
|
configuringId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presets
|
||||||
|
function getPresetsForIndicator(indicatorName) {
|
||||||
|
if (!userPresets || !userPresets.presets) return [];
|
||||||
|
return userPresets.presets.filter(p => p.indicatorName === indicatorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.savePreset = function(id) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
const presetName = prompt('Enter preset name:');
|
||||||
|
if (!presetName) return;
|
||||||
|
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
const preset = {
|
||||||
|
id: `preset_${Date.now()}`,
|
||||||
|
name: presetName,
|
||||||
|
indicatorName: meta.name,
|
||||||
|
values: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
meta.inputs.forEach(input => {
|
||||||
|
preset.values[input.name] = indicator.params[input.name];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userPresets.presets) userPresets.presets = [];
|
||||||
|
userPresets.presets.push(preset);
|
||||||
|
saveUserPresets();
|
||||||
|
renderIndicatorPanel();
|
||||||
|
|
||||||
|
alert(`Preset "${presetName}" saved!`);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.applyPreset = function(id, presetId) {
|
||||||
|
const allPresets = (userPresets?.presets || []).filter(p => typeof p === 'object' && p.id);
|
||||||
|
const preset = allPresets.find(p => p.id === presetId);
|
||||||
|
if (!preset) return;
|
||||||
|
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
Object.keys(preset.values).forEach(key => {
|
||||||
|
indicator.params[key] = preset.values[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.deletePreset = function(presetId) {
|
||||||
|
if (!confirm('Delete this preset?')) return;
|
||||||
|
|
||||||
|
if (userPresets?.presets) {
|
||||||
|
userPresets.presets = userPresets.presets.filter(p => p.id !== presetId);
|
||||||
|
saveUserPresets();
|
||||||
|
renderIndicatorPanel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.showPresets = function(indicatorName) {
|
||||||
|
const presets = getPresetsForIndicator(indicatorName);
|
||||||
|
if (presets.length === 0) {
|
||||||
|
alert('No saved presets for this indicator');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = window.open('', '_blank', 'width=400,height=500');
|
||||||
|
|
||||||
|
let htmlContent =
|
||||||
|
'<html><head><title>Presets - ' + indicatorName + '</title><style>' +
|
||||||
|
'body { font-family: sans-serif; padding: 20px; background: #1e222d; color: #d1d4dc; }' +
|
||||||
|
'.preset { padding: 10px; margin: 5px; background: #131722; border-radius: 4px; }' +
|
||||||
|
'.preset:hover { background: #2a2e39; cursor: pointer; }' +
|
||||||
|
'</style></head><body>' +
|
||||||
|
'<h3>' + indicatorName + ' Presets</h3>';
|
||||||
|
|
||||||
|
presets.forEach(p => {
|
||||||
|
htmlContent += '<div class="preset" onclick="opener.applyPresetFromWindow(' + "'" + p.id + "'" + ')">' + p.name + '</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
htmlContent += '</body></html>';
|
||||||
|
|
||||||
|
menu.document.write(htmlContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.applyPresetFromWindow = function(presetId) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === configuringId);
|
||||||
|
if (!indicator) return;
|
||||||
|
applyPreset(indicator.id, presetId);
|
||||||
|
};
|
||||||
|
|
||||||
|
function addIndicator(type) {
|
||||||
|
const IndicatorClass = IR?.[type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const id = `${type}_${nextInstanceId++}`;
|
||||||
|
const instance = new IndicatorClass({ type, params: {}, name: '' });
|
||||||
|
const metadata = instance.getMetadata();
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
_lineType: 'solid',
|
||||||
|
_lineWidth: 2
|
||||||
|
};
|
||||||
|
metadata.plots.forEach((plot, idx) => {
|
||||||
|
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
||||||
|
});
|
||||||
|
metadata.inputs.forEach(input => {
|
||||||
|
params[input.name] = input.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
activeIndicators.push({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name: metadata.name,
|
||||||
|
params,
|
||||||
|
plots: metadata.plots,
|
||||||
|
series: [],
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
// Don't set configuringId so indicators are NOT expanded by default
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
function saveUserPresets() {
|
||||||
|
localStorage.setItem('indicator_presets', JSON.stringify(userPresets));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
|
||||||
|
const results = instance.calculate(candles);
|
||||||
|
indicator.series = [];
|
||||||
|
|
||||||
|
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
||||||
|
const lineWidth = indicator.params._lineWidth || 2;
|
||||||
|
|
||||||
|
const firstNonNull = results?.find(r => r !== null && r !== undefined);
|
||||||
|
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||||
|
|
||||||
|
meta.plots.forEach((plot, plotIdx) => {
|
||||||
|
if (isObjectResult) {
|
||||||
|
const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null);
|
||||||
|
if (!hasData) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||||
|
|
||||||
|
const data = [];
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
let value;
|
||||||
|
if (isObjectResult) {
|
||||||
|
value = results[i]?.[plot.id];
|
||||||
|
} else {
|
||||||
|
value = results[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== null && value !== undefined) {
|
||||||
|
data.push({
|
||||||
|
time: candles[i].time,
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) return;
|
||||||
|
|
||||||
|
let series;
|
||||||
|
let plotLineStyle = lineStyle;
|
||||||
|
if (plot.style === 'dashed') plotLineStyle = LightweightCharts.LineStyle.Dashed;
|
||||||
|
else if (plot.style === 'dotted') plotLineStyle = LightweightCharts.LineStyle.Dotted;
|
||||||
|
else if (plot.style === 'solid') plotLineStyle = LightweightCharts.LineStyle.Solid;
|
||||||
|
|
||||||
|
if (plot.type === 'histogram') {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, {
|
||||||
|
color: plotColor,
|
||||||
|
priceFormat: { type: 'price', precision: 4, minMove: 0.0001 },
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: false
|
||||||
|
}, paneIndex);
|
||||||
|
} else if (plot.type === 'baseline') {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, {
|
||||||
|
baseValue: { type: 'price', price: plot.baseValue || 0 },
|
||||||
|
topLineColor: plot.topLineColor || plotColor,
|
||||||
|
topFillColor1: plot.topFillColor1 || plotColor,
|
||||||
|
topFillColor2: '#00000000',
|
||||||
|
bottomFillColor1: '#00000000',
|
||||||
|
bottomColor: plot.bottomColor || '#00000000',
|
||||||
|
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
|
||||||
|
lineStyle: plotLineStyle,
|
||||||
|
title: plot.title || '',
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: plot.lastValueVisible !== false
|
||||||
|
}, paneIndex);
|
||||||
|
} else {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: plotColor,
|
||||||
|
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
|
||||||
|
lineStyle: plotLineStyle,
|
||||||
|
title: plot.title || '',
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: plot.lastValueVisible !== false
|
||||||
|
}, paneIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
series.setData(data);
|
||||||
|
indicator.series.push(series);
|
||||||
|
|
||||||
|
// Create horizontal band lines for RSI
|
||||||
|
if (meta.name === 'RSI' && indicator.series.length > 0) {
|
||||||
|
const mainSeries = indicator.series[0];
|
||||||
|
const overbought = indicator.params.overbought || 70;
|
||||||
|
const oversold = indicator.params.oversold || 30;
|
||||||
|
|
||||||
|
// Remove existing price lines first
|
||||||
|
while (indicator.bands && indicator.bands.length > 0) {
|
||||||
|
try {
|
||||||
|
indicator.bands.pop();
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
indicator.bands = indicator.bands || [];
|
||||||
|
|
||||||
|
// Create overbought band line
|
||||||
|
indicator.bands.push(mainSeries.createPriceLine({
|
||||||
|
price: overbought,
|
||||||
|
color: '#787B86',
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||||
|
axisLabelVisible: false,
|
||||||
|
title: ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create oversold band line
|
||||||
|
indicator.bands.push(mainSeries.createPriceLine({
|
||||||
|
price: oversold,
|
||||||
|
color: '#787B86',
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||||
|
axisLabelVisible: false,
|
||||||
|
title: ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart drawing
|
||||||
|
export function drawIndicatorsOnChart() {
|
||||||
|
if (!window.dashboard || !window.dashboard.chart) return;
|
||||||
|
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
ind.series?.forEach(s => {
|
||||||
|
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
|
||||||
|
if (!candles || candles.length === 0) return;
|
||||||
|
|
||||||
|
const lineStyleMap = {
|
||||||
|
'solid': LightweightCharts.LineStyle.Solid,
|
||||||
|
'dotted': LightweightCharts.LineStyle.Dotted,
|
||||||
|
'dashed': LightweightCharts.LineStyle.Dashed
|
||||||
|
};
|
||||||
|
|
||||||
|
indicatorPanes.clear();
|
||||||
|
nextPaneIndex = 1;
|
||||||
|
|
||||||
|
const overlayIndicators = [];
|
||||||
|
const paneIndicators = [];
|
||||||
|
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
const IndicatorClass = IR?.[ind.type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type: ind.type, params: ind.params, name: ind.name });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
if (meta.displayMode === 'pane') {
|
||||||
|
paneIndicators.push({ indicator: ind, meta, instance });
|
||||||
|
} else {
|
||||||
|
overlayIndicators.push({ indicator: ind, meta, instance });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPanes = 1 + paneIndicators.length;
|
||||||
|
const mainPaneHeight = paneIndicators.length > 0 ? 60 : 100;
|
||||||
|
const paneHeight = paneIndicators.length > 0 ? Math.floor(40 / paneIndicators.length) : 0;
|
||||||
|
|
||||||
|
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
|
||||||
|
|
||||||
|
overlayIndicators.forEach(({ indicator, meta, instance }) => {
|
||||||
|
if (indicator.visible === false) {
|
||||||
|
indicator.series = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
|
||||||
|
if (indicator.visible === false) {
|
||||||
|
indicator.series = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paneIndex = nextPaneIndex++;
|
||||||
|
indicatorPanes.set(indicator.id, paneIndex);
|
||||||
|
|
||||||
|
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
|
||||||
|
|
||||||
|
const pane = window.dashboard.chart.panes()[paneIndex];
|
||||||
|
if (pane) {
|
||||||
|
pane.setHeight(paneHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export functions for module access
|
||||||
|
export { addIndicator, removeIndicatorById };
|
||||||
|
|
||||||
|
// Legacy compatibility functions
|
||||||
|
window.renderIndicatorList = renderIndicatorPanel;
|
||||||
|
window.toggleIndicator = addIndicator;
|
||||||
|
window.showIndicatorConfig = function(id) {
|
||||||
|
const ind = activeIndicators.find(a => a.id === id);
|
||||||
|
if (ind) configuringId = id;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
window.applyIndicatorConfig = function() {
|
||||||
|
// No-op - config is applied immediately
|
||||||
|
};
|
||||||
@ -214,7 +214,7 @@ function renderPreviewConfig(type) {
|
|||||||
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
|
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
|
||||||
${input.type === 'select' ?
|
${input.type === 'select' ?
|
||||||
`<select class="sim-input" style="font-size: 12px; padding: 6px;" disabled>${input.options.map(o => `<option ${input.default === o ? 'selected' : ''}>${o}</option>`).join('')}</select>` :
|
`<select class="sim-input" style="font-size: 12px; padding: 6px;" disabled>${input.options.map(o => `<option ${input.default === o ? 'selected' : ''}>${o}</option>`).join('')}</select>` :
|
||||||
`<input type="number" class="sim-input" value="${input.default}" style="font-size: 12px; padding: 6px;" disabled>`
|
`<input type="number" class="sim-input" value="${input.default}" ${input.step !== undefined ? `step="${input.step}"` : ''} style="font-size: 12px; padding: 6px;" disabled>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
@ -235,8 +235,19 @@ export function addIndicator(type) {
|
|||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
_lineType: 'solid',
|
_lineType: 'solid',
|
||||||
_lineWidth: 2
|
_lineWidth: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set Hurst-specific defaults
|
||||||
|
if (type === 'hurst') {
|
||||||
|
params.markerBuyShape = 'custom';
|
||||||
|
params.markerSellShape = 'custom';
|
||||||
|
params.markerBuyColor = '#9e9e9e';
|
||||||
|
params.markerSellColor = '#9e9e9e';
|
||||||
|
params.markerBuyCustom = '▲';
|
||||||
|
params.markerSellCustom = '▼';
|
||||||
|
}
|
||||||
|
|
||||||
metadata.plots.forEach((plot, idx) => {
|
metadata.plots.forEach((plot, idx) => {
|
||||||
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
||||||
});
|
});
|
||||||
@ -326,7 +337,7 @@ export function renderIndicatorConfig(indicator) {
|
|||||||
|
|
||||||
<div style="margin-bottom: 8px;">
|
<div style="margin-bottom: 8px;">
|
||||||
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">Line Width</label>
|
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">Line Width</label>
|
||||||
<input type="number" id="config__lineWidth" class="sim-input" value="${indicator.params._lineWidth || 2}" min="1" max="5" style="font-size: 12px; padding: 6px;">
|
<input type="number" id="config__lineWidth" class="sim-input" value="${indicator.params._lineWidth || 1}" min="1" max="5" style="font-size: 12px; padding: 6px;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${meta.inputs.map(input => `
|
${meta.inputs.map(input => `
|
||||||
@ -334,7 +345,7 @@ export function renderIndicatorConfig(indicator) {
|
|||||||
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
|
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
|
||||||
${input.type === 'select' ?
|
${input.type === 'select' ?
|
||||||
`<select id="config_${input.name}" class="sim-input" style="font-size: 12px; padding: 6px;">${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}</select>` :
|
`<select id="config_${input.name}" class="sim-input" style="font-size: 12px; padding: 6px;">${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}</select>` :
|
||||||
`<input type="number" id="config_${input.name}" class="sim-input" value="${indicator.params[input.name]}" ${input.min !== undefined ? `min="${input.min}"` : ''} ${input.max !== undefined ? `max="${input.max}"` : ''} style="font-size: 12px; padding: 6px;">`
|
`<input type="number" id="config_${input.name}" class="sim-input" value="${indicator.params[input.name]}" ${input.min !== undefined ? `min="${input.min}"` : ''} ${input.max !== undefined ? `max="${input.max}"` : ''} ${input.step !== undefined ? `step="${input.step}"` : ''} style="font-size: 12px; padding: 6px;">`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
@ -485,13 +496,20 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
indicator.series = [];
|
indicator.series = [];
|
||||||
|
|
||||||
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
||||||
const lineWidth = indicator.params._lineWidth || 2;
|
const lineWidth = indicator.params._lineWidth || 1;
|
||||||
|
|
||||||
const firstNonNull = results?.find(r => r !== null && r !== undefined);
|
const firstNonNull = results?.find(r => r !== null && r !== undefined);
|
||||||
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||||
|
|
||||||
meta.plots.forEach((plot, plotIdx) => {
|
meta.plots.forEach((plot, plotIdx) => {
|
||||||
if (isObjectResult && typeof firstNonNull[plot.id] === 'undefined') return;
|
if (isObjectResult) {
|
||||||
|
// Find if this specific plot has any non-null data across all results
|
||||||
|
const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null);
|
||||||
|
if (!hasData) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip hidden plots
|
||||||
|
if (plot.visible === false) return;
|
||||||
|
|
||||||
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||||
|
|
||||||
@ -515,6 +533,13 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
if (data.length === 0) return;
|
if (data.length === 0) return;
|
||||||
|
|
||||||
let series;
|
let series;
|
||||||
|
|
||||||
|
// Determine line style for this specific plot
|
||||||
|
let plotLineStyle = lineStyle;
|
||||||
|
if (plot.style === 'dashed') plotLineStyle = LightweightCharts.LineStyle.Dashed;
|
||||||
|
else if (plot.style === 'dotted') plotLineStyle = LightweightCharts.LineStyle.Dotted;
|
||||||
|
else if (plot.style === 'solid') plotLineStyle = LightweightCharts.LineStyle.Solid;
|
||||||
|
|
||||||
if (plot.type === 'histogram') {
|
if (plot.type === 'histogram') {
|
||||||
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, {
|
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, {
|
||||||
color: plotColor,
|
color: plotColor,
|
||||||
@ -526,20 +551,72 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
lastValueVisible: false
|
lastValueVisible: false
|
||||||
}, paneIndex);
|
}, paneIndex);
|
||||||
|
} else if (plot.type === 'baseline') {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, {
|
||||||
|
baseValue: { type: 'price', price: plot.baseValue || 0 },
|
||||||
|
topLineColor: plot.topLineColor || plotColor,
|
||||||
|
topFillColor1: plot.topFillColor1 || plotColor,
|
||||||
|
topFillColor2: plot.topFillColor2 || '#00000000',
|
||||||
|
bottomFillColor1: plot.bottomFillColor1 || '#00000000',
|
||||||
|
bottomColor: plot.bottomColor || '#00000000',
|
||||||
|
lineWidth: plot.width !== undefined ? plot.width : lineWidth,
|
||||||
|
lineStyle: plotLineStyle,
|
||||||
|
title: plot.title || '',
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: plot.lastValueVisible !== false
|
||||||
|
}, paneIndex);
|
||||||
} else {
|
} else {
|
||||||
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
|
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
color: plotColor,
|
color: plotColor,
|
||||||
lineWidth: plot.width || lineWidth,
|
lineWidth: plot.width !== undefined ? plot.width : lineWidth,
|
||||||
lineStyle: lineStyle,
|
lineStyle: plotLineStyle,
|
||||||
title: '',
|
title: plot.title || '',
|
||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
lastValueVisible: true
|
lastValueVisible: plot.lastValueVisible !== false
|
||||||
}, paneIndex);
|
}, paneIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
series.setData(data);
|
series.setData(data);
|
||||||
|
series.plotId = plot.id;
|
||||||
|
|
||||||
|
// Skip hidden plots (visible: false)
|
||||||
|
if (plot.visible === false) {
|
||||||
|
series.applyOptions({ visible: false });
|
||||||
|
}
|
||||||
|
|
||||||
indicator.series.push(series);
|
indicator.series.push(series);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Render gradient zones if available
|
||||||
|
if (meta.gradientZones && indicator.series.length > 0) {
|
||||||
|
// Find the main series to attach zones to
|
||||||
|
let baseSeries = indicator.series[0];
|
||||||
|
|
||||||
|
meta.gradientZones.forEach(zone => {
|
||||||
|
if (zone.from === undefined || zone.to === undefined) return;
|
||||||
|
|
||||||
|
// We use createPriceLine on the series for horizontal bands with custom colors
|
||||||
|
baseSeries.createPriceLine({
|
||||||
|
price: zone.from,
|
||||||
|
color: zone.color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[^)]+\)/, 'rgb($1, $2, $3)'),
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||||
|
axisLabelVisible: false,
|
||||||
|
title: zone.label || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (zone.to !== zone.from) {
|
||||||
|
baseSeries.createPriceLine({
|
||||||
|
price: zone.to,
|
||||||
|
color: zone.color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[^)]+\)/, 'rgb($1, $2, $3)'),
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||||
|
axisLabelVisible: false,
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update the TradingView-style legend overlay on the chart */
|
/** Update the TradingView-style legend overlay on the chart */
|
||||||
|
|||||||
@ -16,8 +16,58 @@ export function toggleSidebar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function restoreSidebarState() {
|
export function restoreSidebarState() {
|
||||||
const collapsed = localStorage.getItem('sidebar_collapsed') === 'true';
|
const collapsed = localStorage.getItem('sidebar_collapsed') !== 'false'; // Default to collapsed
|
||||||
if (collapsed) {
|
const sidebar = document.getElementById('rightSidebar');
|
||||||
document.getElementById('rightSidebar').classList.add('collapsed');
|
if (collapsed && sidebar) {
|
||||||
|
sidebar.classList.add('collapsed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tab Management
|
||||||
|
let activeTab = 'indicators';
|
||||||
|
|
||||||
|
export function initSidebarTabs() {
|
||||||
|
const tabs = document.querySelectorAll('.sidebar-tab');
|
||||||
|
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
switchTab(tab.dataset.tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function switchTab(tabId) {
|
||||||
|
activeTab = tabId;
|
||||||
|
localStorage.setItem('sidebar_active_tab', tabId);
|
||||||
|
|
||||||
|
document.querySelectorAll('.sidebar-tab').forEach(tab => {
|
||||||
|
tab.classList.toggle('active', tab.dataset.tab === tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.sidebar-tab-panel').forEach(panel => {
|
||||||
|
panel.classList.toggle('active', panel.id === `tab-${tabId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tabId === 'indicators') {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.drawIndicatorsOnChart) {
|
||||||
|
window.drawIndicatorsOnChart();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
} else if (tabId === 'strategy') {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.renderStrategyPanel) {
|
||||||
|
window.renderStrategyPanel();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveTab() {
|
||||||
|
return activeTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreSidebarTabState() {
|
||||||
|
const savedTab = localStorage.getItem('sidebar_active_tab') || 'indicators';
|
||||||
|
switchTab(savedTab);
|
||||||
|
}
|
||||||
|
|||||||
228
src/api/dashboard/static/js/ui/signal-markers.js
Normal file
228
src/api/dashboard/static/js/ui/signal-markers.js
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import { IndicatorRegistry } from '../indicators/index.js';
|
||||||
|
|
||||||
|
export function calculateSignalMarkers(candles) {
|
||||||
|
const activeIndicators = window.getActiveIndicators?.() || [];
|
||||||
|
const markers = [];
|
||||||
|
|
||||||
|
if (!candles || candles.length < 2) {
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const indicator of activeIndicators) {
|
||||||
|
if (indicator.params.showMarkers === false || indicator.params.showMarkers === 'false') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[SignalMarkers] Processing indicator:', indicator.type, 'showMarkers:', indicator.params.showMarkers);
|
||||||
|
|
||||||
|
const IndicatorClass = IndicatorRegistry[indicator.type];
|
||||||
|
if (!IndicatorClass) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new IndicatorClass(indicator);
|
||||||
|
const results = instance.calculate(candles);
|
||||||
|
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indicatorMarkers = findCrossoverMarkers(indicator, candles, results);
|
||||||
|
markers.push(...indicatorMarkers);
|
||||||
|
}
|
||||||
|
|
||||||
|
markers.sort((a, b) => a.time - b.time);
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCrossoverMarkers(indicator, candles, results) {
|
||||||
|
const markers = [];
|
||||||
|
const overbought = indicator.params?.overbought || 70;
|
||||||
|
const oversold = indicator.params?.oversold || 30;
|
||||||
|
const indicatorType = indicator.type;
|
||||||
|
|
||||||
|
const buyColor = indicator.params?.markerBuyColor || '#26a69a';
|
||||||
|
const sellColor = indicator.params?.markerSellColor || '#ef5350';
|
||||||
|
const buyShape = indicator.params?.markerBuyShape || 'arrowUp';
|
||||||
|
const sellShape = indicator.params?.markerSellShape || 'arrowDown';
|
||||||
|
const buyCustom = indicator.params?.markerBuyCustom || '◭';
|
||||||
|
const sellCustom = indicator.params?.markerSellCustom || '▼';
|
||||||
|
|
||||||
|
for (let i = 1; i < results.length; i++) {
|
||||||
|
const candle = candles[i];
|
||||||
|
const prevCandle = candles[i - 1];
|
||||||
|
const result = results[i];
|
||||||
|
const prevResult = results[i - 1];
|
||||||
|
|
||||||
|
if (!result || !prevResult) continue;
|
||||||
|
|
||||||
|
if (indicatorType === 'rsi' || indicatorType === 'stoch') {
|
||||||
|
const rsi = result.rsi ?? result;
|
||||||
|
const prevRsi = prevResult.rsi ?? prevResult;
|
||||||
|
|
||||||
|
if (rsi === undefined || prevRsi === undefined) continue;
|
||||||
|
|
||||||
|
if (prevRsi > overbought && rsi <= overbought) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevRsi < oversold && rsi >= oversold) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (indicatorType === 'macd') {
|
||||||
|
const macd = result.macd ?? result.MACD;
|
||||||
|
const signal = result.signal ?? result.signalLine;
|
||||||
|
const prevMacd = prevResult.macd ?? prevResult.MACD;
|
||||||
|
const prevSignal = prevResult.signal ?? prevResult.signalLine;
|
||||||
|
|
||||||
|
if (macd === undefined || signal === undefined || prevMacd === undefined || prevSignal === undefined) continue;
|
||||||
|
|
||||||
|
const macdAbovePrev = prevMacd > prevSignal;
|
||||||
|
const macdAboveNow = macd > signal;
|
||||||
|
|
||||||
|
if (macdAbovePrev && !macdAboveNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!macdAbovePrev && macdAboveNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (indicatorType === 'bb') {
|
||||||
|
const upper = result.upper ?? result.upperBand;
|
||||||
|
const lower = result.lower ?? result.lowerBand;
|
||||||
|
|
||||||
|
if (upper === undefined || lower === undefined) continue;
|
||||||
|
|
||||||
|
const priceAboveUpperPrev = prevCandle.close > (prevResult.upper ?? prevResult.upperBand);
|
||||||
|
const priceAboveUpperNow = candle.close > upper;
|
||||||
|
|
||||||
|
if (priceAboveUpperPrev && !priceAboveUpperNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!priceAboveUpperPrev && priceAboveUpperNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceBelowLowerPrev = prevCandle.close < (prevResult.lower ?? prevResult.lowerBand);
|
||||||
|
const priceBelowLowerNow = candle.close < lower;
|
||||||
|
|
||||||
|
if (priceBelowLowerPrev && !priceBelowLowerNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!priceBelowLowerPrev && priceBelowLowerNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (indicatorType === 'hurst') {
|
||||||
|
const upper = result.upper;
|
||||||
|
const lower = result.lower;
|
||||||
|
const prevUpper = prevResult?.upper;
|
||||||
|
const prevLower = prevResult?.lower;
|
||||||
|
|
||||||
|
if (upper === undefined || lower === undefined ||
|
||||||
|
prevUpper === undefined || prevLower === undefined) continue;
|
||||||
|
|
||||||
|
// BUY: price crosses down below lower band (was above, now below)
|
||||||
|
if (prevCandle.close > prevLower && candle.close < lower) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELL: price crosses down below upper band (was above, now below)
|
||||||
|
if (prevCandle.close > prevUpper && candle.close < upper) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const ma = result.ma ?? result;
|
||||||
|
const prevMa = prevResult.ma ?? prevResult;
|
||||||
|
|
||||||
|
if (ma === undefined || prevMa === undefined) continue;
|
||||||
|
|
||||||
|
const priceAbovePrev = prevCandle.close > prevMa;
|
||||||
|
const priceAboveNow = candle.close > ma;
|
||||||
|
|
||||||
|
if (priceAbovePrev && !priceAboveNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!priceAbovePrev && priceAboveNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
364
src/api/dashboard/static/js/ui/signals-calculator.js
Normal file
364
src/api/dashboard/static/js/ui/signals-calculator.js
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
// Signal Calculator - orchestrates signal calculation using indicator-specific functions
|
||||||
|
// Signal calculation logic is now in each indicator file
|
||||||
|
|
||||||
|
import { IndicatorRegistry, getSignalFunction } from '../indicators/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate signal for an indicator
|
||||||
|
* @param {Object} indicator - Indicator configuration
|
||||||
|
* @param {Array} candles - Candle data array
|
||||||
|
* @param {Object} indicatorValues - Computed indicator values for last candle
|
||||||
|
* @param {Object} prevIndicatorValues - Computed indicator values for previous candle
|
||||||
|
* @returns {Object} Signal object with type, strength, value, reasoning
|
||||||
|
*/
|
||||||
|
function calculateIndicatorSignal(indicator, candles, indicatorValues, prevIndicatorValues) {
|
||||||
|
const signalFunction = getSignalFunction(indicator.type);
|
||||||
|
|
||||||
|
if (!signalFunction) {
|
||||||
|
console.warn('[Signals] No signal function for indicator type:', indicator.type);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCandle = candles[candles.length - 1];
|
||||||
|
const prevCandle = candles[candles.length - 2];
|
||||||
|
|
||||||
|
return signalFunction(indicator, lastCandle, prevCandle, indicatorValues, prevIndicatorValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: 'hold',
|
||||||
|
strength: 0,
|
||||||
|
reasoning: 'No active indicators',
|
||||||
|
buyCount: 0,
|
||||||
|
sellCount: 0,
|
||||||
|
holdCount: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const buySignals = signals.filter(s => s.signal === 'buy');
|
||||||
|
const sellSignals = signals.filter(s => s.signal === 'sell');
|
||||||
|
const holdSignals = signals.filter(s => s.signal === '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 = '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 = 'sell';
|
||||||
|
const avgSellStrength = sellWeight / sellCount;
|
||||||
|
strength = Math.round(avgSellStrength * (sellCount / total));
|
||||||
|
reasoning = `${sellCount} sell signals, ${buyCount} buy, ${holdCount} hold`;
|
||||||
|
} else {
|
||||||
|
summarySignal = '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: summarySignal === 'buy' ? '#26a69a' : summarySignal === 'sell' ? '#ef5350' : '#787b86'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[calculateSummarySignal] Result:', result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate historical crossovers for all indicators based on full candle history
|
||||||
|
* Finds the last time each indicator crossed from BUY to SELL or SELL to BUY
|
||||||
|
*/
|
||||||
|
function calculateHistoricalCrossovers(activeIndicators, candles) {
|
||||||
|
activeIndicators.forEach(indicator => {
|
||||||
|
const indicatorType = indicator.type || indicator.indicatorType;
|
||||||
|
|
||||||
|
// Recalculate indicator values for all candles
|
||||||
|
const IndicatorClass = IndicatorRegistry[indicatorType];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass(indicator);
|
||||||
|
const results = instance.calculate(candles);
|
||||||
|
|
||||||
|
if (!results || results.length === 0) return;
|
||||||
|
|
||||||
|
// Find the most recent crossover by going backwards from the newest candle
|
||||||
|
// candles are sorted oldest first, newest last
|
||||||
|
let lastCrossoverTimestamp = null;
|
||||||
|
let lastSignalType = null;
|
||||||
|
|
||||||
|
// Get indicator-specific parameters
|
||||||
|
const overbought = indicator.params?.overbought || 70;
|
||||||
|
const oversold = indicator.params?.oversold || 30;
|
||||||
|
|
||||||
|
for (let i = candles.length - 1; i > 0; i--) {
|
||||||
|
const candle = candles[i]; // newer candle
|
||||||
|
const prevCandle = candles[i-1]; // older candle
|
||||||
|
|
||||||
|
const result = results[i];
|
||||||
|
const prevResult = results[i-1];
|
||||||
|
|
||||||
|
if (!result || !prevResult) continue;
|
||||||
|
|
||||||
|
// Handle different indicator types
|
||||||
|
if (indicatorType === 'rsi' || indicatorType === 'stoch') {
|
||||||
|
// RSI/Stochastic: check crossing overbought/oversold levels
|
||||||
|
const rsi = result.rsi !== undefined ? result.rsi : result;
|
||||||
|
const prevRsi = prevResult.rsi !== undefined ? prevResult.rsi : prevResult;
|
||||||
|
|
||||||
|
if (rsi === undefined || prevRsi === undefined) continue;
|
||||||
|
|
||||||
|
// SELL: crossed down through overbought (was above, now below)
|
||||||
|
if (prevRsi > overbought && rsi <= overbought) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'sell';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// BUY: crossed up through oversold (was below, now above)
|
||||||
|
if (prevRsi < oversold && rsi >= oversold) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'buy';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (indicatorType === 'hurst') {
|
||||||
|
// Hurst Bands: check price crossing bands
|
||||||
|
const upper = result.upper;
|
||||||
|
const lower = result.lower;
|
||||||
|
const prevUpper = prevResult.upper;
|
||||||
|
const prevLower = prevResult.lower;
|
||||||
|
|
||||||
|
if (upper === undefined || lower === undefined ||
|
||||||
|
prevUpper === undefined || prevLower === undefined) continue;
|
||||||
|
|
||||||
|
// BUY: price crossed down below lower band
|
||||||
|
if (prevCandle.close > prevLower && candle.close < lower) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'buy';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// SELL: price crossed down below upper band
|
||||||
|
if (prevCandle.close > prevUpper && candle.close < upper) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'sell';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// MA-style: check price crossing MA
|
||||||
|
const ma = result.ma !== undefined ? result.ma : result;
|
||||||
|
const prevMa = prevResult.ma !== undefined ? prevResult.ma : prevResult;
|
||||||
|
|
||||||
|
if (ma === undefined || prevMa === undefined) continue;
|
||||||
|
|
||||||
|
// Check crossover: price was on one side of MA, now on the other side
|
||||||
|
const priceAbovePrev = prevCandle.close > prevMa;
|
||||||
|
const priceAboveNow = candle.close > ma;
|
||||||
|
|
||||||
|
// SELL signal: price crossed from above to below MA
|
||||||
|
if (priceAbovePrev && !priceAboveNow) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'sell';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// BUY signal: price crossed from below to above MA
|
||||||
|
if (!priceAbovePrev && priceAboveNow) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'buy';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update the timestamp based on current data
|
||||||
|
// If crossover found use that time, otherwise use last candle time
|
||||||
|
if (lastCrossoverTimestamp) {
|
||||||
|
console.log(`[HistoricalCross] ${indicatorType}: Found ${lastSignalType} crossover at ${new Date(lastCrossoverTimestamp * 1000).toLocaleString()}`);
|
||||||
|
indicator.lastSignalTimestamp = lastCrossoverTimestamp;
|
||||||
|
indicator.lastSignalType = lastSignalType;
|
||||||
|
} else {
|
||||||
|
// No crossover found - use last candle time
|
||||||
|
const lastCandleTime = candles[candles.length - 1]?.time;
|
||||||
|
if (lastCandleTime) {
|
||||||
|
const lastResult = results[results.length - 1];
|
||||||
|
|
||||||
|
if (indicatorType === 'rsi' || indicatorType === 'stoch') {
|
||||||
|
// RSI/Stochastic: use RSI level to determine signal
|
||||||
|
const rsi = lastResult?.rsi !== undefined ? lastResult.rsi : lastResult;
|
||||||
|
if (rsi !== undefined) {
|
||||||
|
indicator.lastSignalType = rsi > overbought ? 'sell' : (rsi < oversold ? 'buy' : null);
|
||||||
|
indicator.lastSignalTimestamp = lastCandleTime;
|
||||||
|
}
|
||||||
|
} else if (indicatorType === 'hurst') {
|
||||||
|
// Hurst Bands: use price vs bands
|
||||||
|
const upper = lastResult?.upper;
|
||||||
|
const lower = lastResult?.lower;
|
||||||
|
const currentPrice = candles[candles.length - 1]?.close;
|
||||||
|
if (upper !== undefined && lower !== undefined && currentPrice !== undefined) {
|
||||||
|
if (currentPrice < lower) {
|
||||||
|
indicator.lastSignalType = 'buy';
|
||||||
|
} else if (currentPrice > upper) {
|
||||||
|
indicator.lastSignalType = 'sell';
|
||||||
|
} else {
|
||||||
|
indicator.lastSignalType = null;
|
||||||
|
}
|
||||||
|
indicator.lastSignalTimestamp = lastCandleTime;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// MA-style: use price vs MA
|
||||||
|
const ma = lastResult?.ma !== undefined ? lastResult.ma : lastResult;
|
||||||
|
if (ma !== undefined) {
|
||||||
|
const isAbove = candles[candles.length - 1].close > ma;
|
||||||
|
indicator.lastSignalType = isAbove ? 'buy' : 'sell';
|
||||||
|
indicator.lastSignalTimestamp = lastCandleTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const signals = [];
|
||||||
|
|
||||||
|
// Calculate crossovers for all indicators based on historical data
|
||||||
|
calculateHistoricalCrossovers(activeIndicators, candles);
|
||||||
|
|
||||||
|
for (const indicator of activeIndicators) {
|
||||||
|
const IndicatorClass = IndicatorRegistry[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;
|
||||||
|
|
||||||
|
if (!results || !meta || results.length !== candles.length) {
|
||||||
|
const instance = new IndicatorClass(indicator);
|
||||||
|
meta = instance.getMetadata();
|
||||||
|
results = instance.calculate(candles);
|
||||||
|
indicator.cachedResults = results;
|
||||||
|
indicator.cachedMeta = meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
console.log('[Signals] No results for indicator:', indicator.type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastResult = results[results.length - 1];
|
||||||
|
const prevResult = results[results.length - 2];
|
||||||
|
if (lastResult === null || lastResult === undefined) {
|
||||||
|
console.log('[Signals] No valid last result for indicator:', indicator.type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let values;
|
||||||
|
let prevValues;
|
||||||
|
if (typeof lastResult === 'object' && lastResult !== null && !Array.isArray(lastResult)) {
|
||||||
|
values = lastResult;
|
||||||
|
prevValues = prevResult;
|
||||||
|
} else if (typeof lastResult === 'number') {
|
||||||
|
values = { ma: lastResult };
|
||||||
|
prevValues = prevResult ? { ma: prevResult } : undefined;
|
||||||
|
} else {
|
||||||
|
console.log('[Signals] Unexpected result type for', indicator.type, ':', typeof lastResult);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signal = calculateIndicatorSignal(indicator, candles, values, prevValues);
|
||||||
|
|
||||||
|
let currentSignal = signal;
|
||||||
|
let lastSignalDate = indicator.lastSignalTimestamp || null;
|
||||||
|
let lastSignalType = indicator.lastSignalType || null;
|
||||||
|
|
||||||
|
if (!currentSignal || !currentSignal.type) {
|
||||||
|
console.log('[Signals] No valid signal for', indicator.type, '- Using last signal if available');
|
||||||
|
|
||||||
|
if (lastSignalType && lastSignalDate) {
|
||||||
|
currentSignal = {
|
||||||
|
type: lastSignalType,
|
||||||
|
strength: 50,
|
||||||
|
value: candles[candles.length - 1]?.close,
|
||||||
|
reasoning: `No crossover (price equals MA)`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log('[Signals] No previous signal available - Skipping');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const currentCandleTimestamp = candles[candles.length - 1].time;
|
||||||
|
|
||||||
|
if (currentSignal.type !== lastSignalType || !lastSignalType) {
|
||||||
|
console.log('[Signals] Signal changed for', indicator.type, ':', lastSignalType, '->', currentSignal.type);
|
||||||
|
lastSignalDate = indicator.lastSignalTimestamp || currentCandleTimestamp;
|
||||||
|
lastSignalType = currentSignal.type;
|
||||||
|
indicator.lastSignalTimestamp = lastSignalDate;
|
||||||
|
indicator.lastSignalType = lastSignalType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signals.push({
|
||||||
|
id: indicator.id,
|
||||||
|
name: meta?.name || indicator.type,
|
||||||
|
label: indicator.type?.toUpperCase(),
|
||||||
|
params: meta?.inputs && meta.inputs.length > 0
|
||||||
|
? indicator.params[meta.inputs[0].name]
|
||||||
|
: null,
|
||||||
|
type: indicator.type,
|
||||||
|
signal: currentSignal.type,
|
||||||
|
strength: Math.round(currentSignal.strength),
|
||||||
|
value: currentSignal.value,
|
||||||
|
reasoning: currentSignal.reasoning,
|
||||||
|
color: currentSignal.type === 'buy' ? '#26a69a' : currentSignal.type === 'sell' ? '#ef5350' : '#787b86',
|
||||||
|
lastSignalDate: lastSignalDate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('[Signals] ========== calculateAllIndicatorSignals END ==========');
|
||||||
|
console.log('[Signals] Total signals calculated:', signals.length);
|
||||||
|
return signals;
|
||||||
|
}
|
||||||
@ -316,18 +316,29 @@ export function showSimulationMarkers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearSimulationMarkers() {
|
export function clearSimulationMarkers() {
|
||||||
if (window.dashboard) {
|
try {
|
||||||
|
if (window.dashboard && window.dashboard.candleSeries && typeof window.dashboard.candleSeries.setMarkers === 'function') {
|
||||||
window.dashboard.candleSeries.setMarkers([]);
|
window.dashboard.candleSeries.setMarkers([]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors clearing markers
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
tradeLineSeries.forEach(series => {
|
tradeLineSeries.forEach(series => {
|
||||||
try {
|
try {
|
||||||
|
if (window.dashboard && window.dashboard.chart) {
|
||||||
window.dashboard.chart.removeSeries(series);
|
window.dashboard.chart.removeSeries(series);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Series might already be removed
|
// Series might already be removed
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tradeLineSeries = [];
|
} catch (e) {
|
||||||
|
// Ignore errors removing series
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tradeLineSeries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearSimulationResults() {
|
export function clearSimulationResults() {
|
||||||
|
|||||||
791
src/api/dashboard/static/js/ui/strategy-panel.js
Normal file
791
src/api/dashboard/static/js/ui/strategy-panel.js
Normal file
@ -0,0 +1,791 @@
|
|||||||
|
import { IndicatorRegistry, getSignalFunction } from '../indicators/index.js';
|
||||||
|
|
||||||
|
let activeIndicators = [];
|
||||||
|
let simulationResults = null;
|
||||||
|
let equitySeries = null;
|
||||||
|
let equityChart = null;
|
||||||
|
let posSeries = null;
|
||||||
|
let posSizeChart = null;
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ping_pong_settings';
|
||||||
|
|
||||||
|
function formatDisplayDate(timestamp) {
|
||||||
|
if (!timestamp) return '';
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDisplayDate(str) {
|
||||||
|
if (!str) return null;
|
||||||
|
const regex = /^(\d{2})\/(\d{2})\/(\d{4})\s(\d{2}):(\d{2})$/;
|
||||||
|
const match = str.trim().match(regex);
|
||||||
|
if (!match) return null;
|
||||||
|
const [_, day, month, year, hours, minutes] = match;
|
||||||
|
return new Date(year, month - 1, day, hours, minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSavedSettings() {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!saved) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
const settings = {
|
||||||
|
startDate: document.getElementById('simStartDate').value,
|
||||||
|
stopDate: document.getElementById('simStopDate').value,
|
||||||
|
contractType: document.getElementById('simContractType').value,
|
||||||
|
direction: document.getElementById('simDirection').value,
|
||||||
|
capital: document.getElementById('simCapital').value,
|
||||||
|
exchangeLeverage: document.getElementById('simExchangeLeverage').value,
|
||||||
|
maxEffectiveLeverage: document.getElementById('simMaxEffectiveLeverage').value,
|
||||||
|
posSize: document.getElementById('simPosSize').value,
|
||||||
|
tp: document.getElementById('simTP').value
|
||||||
|
};
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||||
|
|
||||||
|
const btn = document.getElementById('saveSimSettings');
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = 'Saved!';
|
||||||
|
btn.style.color = '#26a69a';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.style.color = '';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initStrategyPanel() {
|
||||||
|
window.renderStrategyPanel = renderStrategyPanel;
|
||||||
|
renderStrategyPanel();
|
||||||
|
|
||||||
|
// Listen for indicator changes to update the signal selection list
|
||||||
|
const originalAddIndicator = window.addIndicator;
|
||||||
|
window.addIndicator = function(...args) {
|
||||||
|
const res = originalAddIndicator.apply(this, args);
|
||||||
|
setTimeout(renderStrategyPanel, 100);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalRemoveIndicator = window.removeIndicatorById;
|
||||||
|
window.removeIndicatorById = function(...args) {
|
||||||
|
const res = originalRemoveIndicator.apply(this, args);
|
||||||
|
setTimeout(renderStrategyPanel, 100);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderStrategyPanel() {
|
||||||
|
const container = document.getElementById('strategyPanel');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
activeIndicators = window.getActiveIndicators?.() || [];
|
||||||
|
const saved = getSavedSettings();
|
||||||
|
|
||||||
|
// Format initial values for display
|
||||||
|
let startDisplay = saved?.startDate || '01/01/2026 00:00';
|
||||||
|
let stopDisplay = saved?.stopDate || '';
|
||||||
|
|
||||||
|
// If the saved value is in ISO format (from previous version), convert it
|
||||||
|
if (startDisplay.includes('T')) {
|
||||||
|
startDisplay = formatDisplayDate(new Date(startDisplay));
|
||||||
|
}
|
||||||
|
if (stopDisplay.includes('T')) {
|
||||||
|
stopDisplay = formatDisplayDate(new Date(stopDisplay));
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-section-header">
|
||||||
|
<span>⚙️</span> Ping-Pong Strategy
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-section-content">
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Start Date & Time</label>
|
||||||
|
<input type="text" id="simStartDate" class="sim-input" value="${startDisplay}" placeholder="DD/MM/YYYY HH:MM">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Stop Date & Time (Optional)</label>
|
||||||
|
<input type="text" id="simStopDate" class="sim-input" value="${stopDisplay}" placeholder="DD/MM/YYYY HH:MM">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Contract Type</label>
|
||||||
|
<select id="simContractType" class="sim-input">
|
||||||
|
<option value="linear" ${saved?.contractType === 'linear' ? 'selected' : ''}>Linear (USDT-Margined)</option>
|
||||||
|
<option value="inverse" ${saved?.contractType === 'inverse' ? 'selected' : ''}>Inverse (Coin-Margined)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Direction</label>
|
||||||
|
<select id="simDirection" class="sim-input">
|
||||||
|
<option value="long" ${saved?.direction === 'long' ? 'selected' : ''}>Long</option>
|
||||||
|
<option value="short" ${saved?.direction === 'short' ? 'selected' : ''}>Short</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Initial Capital ($)</label>
|
||||||
|
<input type="number" id="simCapital" class="sim-input" value="${saved?.capital || '10000'}" min="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Exchange Leverage (Ping Size Multiplier)</label>
|
||||||
|
<input type="number" id="simExchangeLeverage" class="sim-input" value="${saved?.exchangeLeverage || '1'}" min="1" max="100">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Max Effective Leverage (Total Account Cap)</label>
|
||||||
|
<input type="number" id="simMaxEffectiveLeverage" class="sim-input" value="${saved?.maxEffectiveLeverage || '5'}" min="1" max="100">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Position Size ($ Margin per Ping)</label>
|
||||||
|
<input type="number" id="simPosSize" class="sim-input" value="${saved?.posSize || '10'}" min="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Take Profit (%)</label>
|
||||||
|
<input type="number" id="simTP" class="sim-input" value="${saved?.tp || '15'}" step="0.1" min="0.1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
||||||
|
<label style="margin-bottom: 0;">Open Signal Indicators</label>
|
||||||
|
<button class="action-btn-text" id="saveSimSettings" style="font-size: 10px; color: #00bcd4; background: none; border: none; cursor: pointer; padding: 0;">Save Defaults</button>
|
||||||
|
</div>
|
||||||
|
<div class="indicator-checklist" id="openSignalsList">
|
||||||
|
${renderIndicatorChecklist('open')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Close Signal Indicators (Empty = Accumulation)</label>
|
||||||
|
<div class="indicator-checklist" id="closeSignalsList">
|
||||||
|
${renderIndicatorChecklist('close')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="sim-run-btn" id="runSimulationBtn">Run Simulation</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="simulationResults" class="sim-results" style="display: none;">
|
||||||
|
<!-- Results will be injected here -->
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('runSimulationBtn').addEventListener('click', runSimulation);
|
||||||
|
document.getElementById('saveSimSettings').addEventListener('click', saveSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorChecklist(prefix) {
|
||||||
|
if (activeIndicators.length === 0) {
|
||||||
|
return '<div style="padding: 8px; color: var(--tv-text-secondary); font-size: 11px;">No active indicators on chart</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeIndicators.map(ind => `
|
||||||
|
<label class="checklist-item">
|
||||||
|
<input type="checkbox" data-id="${ind.id}" class="sim-${prefix}-check">
|
||||||
|
<span>${ind.name}</span>
|
||||||
|
</label>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSimulation() {
|
||||||
|
const btn = document.getElementById('runSimulationBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
const originalBtnText = btn.textContent;
|
||||||
|
btn.textContent = 'Preparing Data...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startVal = document.getElementById('simStartDate').value;
|
||||||
|
const stopVal = document.getElementById('simStopDate').value;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
startDate: new Date(startVal).getTime() / 1000,
|
||||||
|
stopDate: stopVal ? new Date(stopVal).getTime() / 1000 : Math.floor(Date.now() / 1000),
|
||||||
|
contractType: document.getElementById('simContractType').value,
|
||||||
|
direction: document.getElementById('simDirection').value,
|
||||||
|
capital: parseFloat(document.getElementById('simCapital').value),
|
||||||
|
exchangeLeverage: parseFloat(document.getElementById('simExchangeLeverage').value),
|
||||||
|
maxEffectiveLeverage: parseFloat(document.getElementById('simMaxEffectiveLeverage').value),
|
||||||
|
posSize: parseFloat(document.getElementById('simPosSize').value),
|
||||||
|
tp: parseFloat(document.getElementById('simTP').value) / 100,
|
||||||
|
openIndicators: Array.from(document.querySelectorAll('.sim-open-check:checked')).map(el => el.dataset.id),
|
||||||
|
closeIndicators: Array.from(document.querySelectorAll('.sim-close-check:checked')).map(el => el.dataset.id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.openIndicators.length === 0) {
|
||||||
|
alert('Please choose at least one indicator for opening positions.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = window.dashboard?.currentInterval || '1d';
|
||||||
|
|
||||||
|
// 1. Ensure data is loaded for the range
|
||||||
|
let allCandles = window.dashboard?.allData?.get(interval) || [];
|
||||||
|
|
||||||
|
const earliestInCache = allCandles.length > 0 ? allCandles[0].time : Infinity;
|
||||||
|
const latestInCache = allCandles.length > 0 ? allCandles[allCandles.length - 1].time : -Infinity;
|
||||||
|
|
||||||
|
if (config.startDate < earliestInCache || config.stopDate > latestInCache) {
|
||||||
|
btn.textContent = 'Fetching from Server...';
|
||||||
|
console.log(`[Simulation] Data gap detected. Range: ${config.startDate}-${config.stopDate}, Cache: ${earliestInCache}-${latestInCache}`);
|
||||||
|
|
||||||
|
const startISO = new Date(config.startDate * 1000).toISOString();
|
||||||
|
const stopISO = new Date(config.stopDate * 1000).toISOString();
|
||||||
|
|
||||||
|
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${interval}&start=${startISO}&end=${stopISO}&limit=10000`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.candles && data.candles.length > 0) {
|
||||||
|
const fetchedCandles = data.candles.reverse().map(c => ({
|
||||||
|
time: Math.floor(new Date(c.time).getTime() / 1000),
|
||||||
|
open: parseFloat(c.open),
|
||||||
|
high: parseFloat(c.high),
|
||||||
|
low: parseFloat(c.low),
|
||||||
|
close: parseFloat(c.close),
|
||||||
|
volume: parseFloat(c.volume || 0)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Merge with existing data
|
||||||
|
allCandles = window.dashboard.mergeData(allCandles, fetchedCandles);
|
||||||
|
window.dashboard.allData.set(interval, allCandles);
|
||||||
|
window.dashboard.candleSeries.setData(allCandles);
|
||||||
|
|
||||||
|
// Recalculate indicators
|
||||||
|
btn.textContent = 'Calculating Indicators...';
|
||||||
|
window.drawIndicatorsOnChart?.();
|
||||||
|
// Wait a bit for indicators to calculate (they usually run in background/setTimeout)
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.textContent = 'Simulating...';
|
||||||
|
|
||||||
|
// Filter candles by the exact range
|
||||||
|
const simCandles = allCandles.filter(c => c.time >= config.startDate && c.time <= config.stopDate);
|
||||||
|
|
||||||
|
if (simCandles.length === 0) {
|
||||||
|
alert('No data available for the selected range.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate indicator signals
|
||||||
|
const indicatorSignals = {};
|
||||||
|
for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) {
|
||||||
|
const ind = activeIndicators.find(a => a.id === indId);
|
||||||
|
if (!ind) continue;
|
||||||
|
|
||||||
|
const signalFunc = getSignalFunction(ind.type);
|
||||||
|
const results = ind.cachedResults;
|
||||||
|
|
||||||
|
if (results && signalFunc) {
|
||||||
|
indicatorSignals[indId] = simCandles.map(candle => {
|
||||||
|
const idx = allCandles.findIndex(c => c.time === candle.time);
|
||||||
|
if (idx < 1) return null;
|
||||||
|
const values = typeof results[idx] === 'object' && results[idx] !== null ? results[idx] : { ma: results[idx] };
|
||||||
|
const prevValues = typeof results[idx-1] === 'object' && results[idx-1] !== null ? results[idx-1] : { ma: results[idx-1] };
|
||||||
|
return signalFunc(ind, allCandles[idx], allCandles[idx-1], values, prevValues);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulation loop
|
||||||
|
const startPrice = simCandles[0].open;
|
||||||
|
let balanceBtc = config.contractType === 'inverse' ? config.capital / startPrice : 0;
|
||||||
|
let balanceUsd = config.contractType === 'linear' ? config.capital : 0;
|
||||||
|
|
||||||
|
let equityData = { usd: [], btc: [] };
|
||||||
|
let totalQty = 0; // Linear: BTC, Inverse: USD Contracts
|
||||||
|
let avgPrice = 0;
|
||||||
|
let avgPriceData = [];
|
||||||
|
let posSizeData = { btc: [], usd: [] };
|
||||||
|
let trades = [];
|
||||||
|
|
||||||
|
const PARTIAL_EXIT_PCT = 0.15;
|
||||||
|
const MIN_POSITION_VALUE_USD = 15;
|
||||||
|
|
||||||
|
for (let i = 0; i < simCandles.length; i++) {
|
||||||
|
const candle = simCandles[i];
|
||||||
|
const price = candle.close;
|
||||||
|
let actionTakenInThisCandle = false;
|
||||||
|
|
||||||
|
// 1. Check TP
|
||||||
|
if (totalQty > 0) {
|
||||||
|
let isTP = false;
|
||||||
|
let exitPrice = price;
|
||||||
|
if (config.direction === 'long') {
|
||||||
|
if (candle.high >= avgPrice * (1 + config.tp)) {
|
||||||
|
isTP = true;
|
||||||
|
exitPrice = avgPrice * (1 + config.tp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (candle.low <= avgPrice * (1 - config.tp)) {
|
||||||
|
isTP = true;
|
||||||
|
exitPrice = avgPrice * (1 - config.tp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTP) {
|
||||||
|
let qtyToClose = totalQty * PARTIAL_EXIT_PCT;
|
||||||
|
let remainingQty = totalQty - qtyToClose;
|
||||||
|
let remainingValueUsd = config.contractType === 'linear' ? remainingQty * exitPrice : remainingQty;
|
||||||
|
let reason = 'TP (Partial)';
|
||||||
|
|
||||||
|
// Minimum size check
|
||||||
|
if (remainingValueUsd < MIN_POSITION_VALUE_USD) {
|
||||||
|
qtyToClose = totalQty;
|
||||||
|
reason = 'TP (Full - Min Size)';
|
||||||
|
}
|
||||||
|
|
||||||
|
let pnl;
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
pnl = config.direction === 'long' ? (exitPrice - avgPrice) * qtyToClose : (avgPrice - exitPrice) * qtyToClose;
|
||||||
|
balanceUsd += pnl;
|
||||||
|
} else {
|
||||||
|
pnl = config.direction === 'long'
|
||||||
|
? qtyToClose * (1/avgPrice - 1/exitPrice)
|
||||||
|
: qtyToClose * (1/exitPrice - 1/avgPrice);
|
||||||
|
balanceBtc += pnl;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalQty -= qtyToClose;
|
||||||
|
trades.push({
|
||||||
|
type: config.direction, recordType: 'exit', time: candle.time,
|
||||||
|
entryPrice: avgPrice, exitPrice: exitPrice, pnl: pnl, reason: reason,
|
||||||
|
currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty,
|
||||||
|
currentQty: config.contractType === 'linear' ? totalQty : totalQty / price
|
||||||
|
});
|
||||||
|
actionTakenInThisCandle = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check Close Signals
|
||||||
|
if (!actionTakenInThisCandle && totalQty > 0 && config.closeIndicators.length > 0) {
|
||||||
|
const hasCloseSignal = config.closeIndicators.some(id => {
|
||||||
|
const sig = indicatorSignals[id][i];
|
||||||
|
if (!sig) return false;
|
||||||
|
return config.direction === 'long' ? sig.type === 'sell' : sig.type === 'buy';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasCloseSignal) {
|
||||||
|
let qtyToClose = totalQty * PARTIAL_EXIT_PCT;
|
||||||
|
let remainingQty = totalQty - qtyToClose;
|
||||||
|
let remainingValueUsd = config.contractType === 'linear' ? remainingQty * price : remainingQty;
|
||||||
|
let reason = 'Signal (Partial)';
|
||||||
|
|
||||||
|
// Minimum size check
|
||||||
|
if (remainingValueUsd < MIN_POSITION_VALUE_USD) {
|
||||||
|
qtyToClose = totalQty;
|
||||||
|
reason = 'Signal (Full - Min Size)';
|
||||||
|
}
|
||||||
|
|
||||||
|
let pnl;
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
pnl = config.direction === 'long' ? (price - avgPrice) * qtyToClose : (avgPrice - price) * qtyToClose;
|
||||||
|
balanceUsd += pnl;
|
||||||
|
} else {
|
||||||
|
pnl = config.direction === 'long'
|
||||||
|
? qtyToClose * (1/avgPrice - 1/price)
|
||||||
|
: qtyToClose * (1/price - 1/avgPrice);
|
||||||
|
balanceBtc += pnl;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalQty -= qtyToClose;
|
||||||
|
trades.push({
|
||||||
|
type: config.direction, recordType: 'exit', time: candle.time,
|
||||||
|
entryPrice: avgPrice, exitPrice: price, pnl: pnl, reason: reason,
|
||||||
|
currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty,
|
||||||
|
currentQty: config.contractType === 'linear' ? totalQty : totalQty / price
|
||||||
|
});
|
||||||
|
actionTakenInThisCandle = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Current Equity for Margin Check
|
||||||
|
let currentEquityBtc, currentEquityUsd;
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
const upnlUsd = totalQty > 0 ? (config.direction === 'long' ? (price - avgPrice) : (avgPrice - price)) * totalQty : 0;
|
||||||
|
currentEquityUsd = balanceUsd + upnlUsd;
|
||||||
|
currentEquityBtc = currentEquityUsd / price;
|
||||||
|
} else {
|
||||||
|
const upnlBtc = totalQty > 0 ? (config.direction === 'long' ? totalQty * (1/avgPrice - 1/price) : totalQty * (1/price - 1/avgPrice)) : 0;
|
||||||
|
currentEquityBtc = balanceBtc + upnlBtc;
|
||||||
|
currentEquityUsd = currentEquityBtc * price;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Open Signals
|
||||||
|
if (!actionTakenInThisCandle) {
|
||||||
|
const hasOpenSignal = config.openIndicators.some(id => {
|
||||||
|
const sig = indicatorSignals[id][i];
|
||||||
|
if (!sig) return false;
|
||||||
|
return config.direction === 'long' ? sig.type === 'buy' : sig.type === 'sell';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasOpenSignal) {
|
||||||
|
const entryValUsd = config.posSize * config.exchangeLeverage;
|
||||||
|
const currentNotionalBtc = config.contractType === 'linear' ? totalQty : totalQty / price;
|
||||||
|
const entryNotionalBtc = entryValUsd / price;
|
||||||
|
|
||||||
|
const projectedEffectiveLeverage = (currentNotionalBtc + entryNotionalBtc) / Math.max(currentEquityBtc, 0.0000001);
|
||||||
|
|
||||||
|
if (projectedEffectiveLeverage <= config.maxEffectiveLeverage) {
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
const entryQty = entryValUsd / price;
|
||||||
|
avgPrice = ((totalQty * avgPrice) + (entryQty * price)) / (totalQty + entryQty);
|
||||||
|
totalQty += entryQty;
|
||||||
|
} else {
|
||||||
|
// Inverse: totalQty is USD contracts
|
||||||
|
avgPrice = (totalQty + entryValUsd) / ((totalQty / avgPrice || 0) + (entryValUsd / price));
|
||||||
|
totalQty += entryValUsd;
|
||||||
|
}
|
||||||
|
|
||||||
|
trades.push({
|
||||||
|
type: config.direction, recordType: 'entry', time: candle.time,
|
||||||
|
entryPrice: price, reason: 'Entry',
|
||||||
|
currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty,
|
||||||
|
currentQty: config.contractType === 'linear' ? totalQty : totalQty / price
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final Equity Recording
|
||||||
|
let finalEquityBtc, finalEquityUsd;
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
const upnl = totalQty > 0 ? (config.direction === 'long' ? (price - avgPrice) : (avgPrice - price)) * totalQty : 0;
|
||||||
|
finalEquityUsd = balanceUsd + upnl;
|
||||||
|
finalEquityBtc = finalEquityUsd / price;
|
||||||
|
} else {
|
||||||
|
const upnl = totalQty > 0 ? (config.direction === 'long' ? totalQty * (1/avgPrice - 1/price) : totalQty * (1/price - 1/avgPrice)) : 0;
|
||||||
|
finalEquityBtc = balanceBtc + upnl;
|
||||||
|
finalEquityUsd = finalEquityBtc * price;
|
||||||
|
}
|
||||||
|
|
||||||
|
equityData.usd.push({ time: candle.time, value: finalEquityUsd });
|
||||||
|
equityData.btc.push({ time: candle.time, value: finalEquityBtc });
|
||||||
|
|
||||||
|
if (totalQty > 0.000001) avgPriceData.push({ time: candle.time, value: avgPrice });
|
||||||
|
posSizeData.btc.push({ time: candle.time, value: config.contractType === 'linear' ? totalQty : totalQty / price });
|
||||||
|
posSizeData.usd.push({ time: candle.time, value: config.contractType === 'linear' ? totalQty * price : totalQty });
|
||||||
|
}
|
||||||
|
|
||||||
|
displayResults(trades, equityData, config, simCandles[simCandles.length-1].close, avgPriceData, posSizeData);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Simulation] Error:', error);
|
||||||
|
alert('Simulation failed.');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Run Simulation';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayResults(trades, equityData, config, endPrice, avgPriceData, posSizeData) {
|
||||||
|
const resultsDiv = document.getElementById('simulationResults');
|
||||||
|
resultsDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// Update main chart with avg price
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.dashboard.setAvgPriceData(avgPriceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryTrades = trades.filter(t => t.recordType === 'entry').length;
|
||||||
|
const exitTrades = trades.filter(t => t.recordType === 'exit').length;
|
||||||
|
const profitableTrades = trades.filter(t => t.recordType === 'exit' && t.pnl > 0).length;
|
||||||
|
const winRate = exitTrades > 0 ? (profitableTrades / exitTrades * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
const startPrice = equityData.usd[0].value / equityData.btc[0].value;
|
||||||
|
const startBtc = config.capital / startPrice;
|
||||||
|
|
||||||
|
const finalUsd = equityData.usd[equityData.usd.length - 1].value;
|
||||||
|
const finalBtc = finalUsd / endPrice;
|
||||||
|
|
||||||
|
const totalPnlUsd = finalUsd - config.capital;
|
||||||
|
const roi = (totalPnlUsd / config.capital * 100).toFixed(2);
|
||||||
|
|
||||||
|
const roiBtc = ((finalBtc - startBtc) / startBtc * 100).toFixed(2);
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = `
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-section-header">Results</div>
|
||||||
|
<div class="sidebar-section-content">
|
||||||
|
<div class="results-summary">
|
||||||
|
<div class="result-stat">
|
||||||
|
<div class="result-stat-value ${totalPnlUsd >= 0 ? 'positive' : 'negative'}">${roi}%</div>
|
||||||
|
<div class="result-stat-label">ROI (USD)</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-stat">
|
||||||
|
<div class="result-stat-value ${parseFloat(roiBtc) >= 0 ? 'positive' : 'negative'}">${roiBtc}%</div>
|
||||||
|
<div class="result-stat-label">ROI (BTC)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-stat-row">
|
||||||
|
<span>Starting Balance</span>
|
||||||
|
<span class="sim-value">$${config.capital.toFixed(0)} / ${startBtc.toFixed(4)} BTC</span>
|
||||||
|
</div>
|
||||||
|
<div class="sim-stat-row">
|
||||||
|
<span>Final Balance</span>
|
||||||
|
<span class="sim-value">$${finalUsd.toFixed(2)} / ${finalBtc.toFixed(4)} BTC</span>
|
||||||
|
</div>
|
||||||
|
<div class="sim-stat-row">
|
||||||
|
<span>Trades (Entry / Exit)</span>
|
||||||
|
<span class="sim-value">${entryTrades} / ${exitTrades}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px;">
|
||||||
|
<span style="font-size: 11px; color: var(--tv-text-secondary);">Equity Chart</span>
|
||||||
|
<div class="chart-toggle-group">
|
||||||
|
<button class="toggle-btn active" data-unit="usd">USD</button>
|
||||||
|
<button class="toggle-btn" data-unit="btc">BTC</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="equity-chart-container" id="equityChart"></div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px;">
|
||||||
|
<span style="font-size: 11px; color: var(--tv-text-secondary);" id="posSizeLabel">Position Size (BTC)</span>
|
||||||
|
<div class="chart-toggle-group">
|
||||||
|
<button class="toggle-btn active" data-unit="usd">USD</button>
|
||||||
|
<button class="toggle-btn" data-unit="btc">BTC</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="equity-chart-container" id="posSizeChart"></div>
|
||||||
|
|
||||||
|
<div class="results-actions">
|
||||||
|
<button class="action-btn secondary" id="toggleTradeMarkers">Show Markers</button>
|
||||||
|
<button class="action-btn secondary" id="clearSim">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create Charts
|
||||||
|
const initCharts = () => {
|
||||||
|
// Equity Chart
|
||||||
|
const equityContainer = document.getElementById('equityChart');
|
||||||
|
if (equityContainer) {
|
||||||
|
equityContainer.innerHTML = '';
|
||||||
|
equityChart = LightweightCharts.createChart(equityContainer, {
|
||||||
|
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
||||||
|
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
|
||||||
|
rightPriceScale: { borderColor: '#2a2e39', autoScale: true },
|
||||||
|
timeScale: {
|
||||||
|
borderColor: '#2a2e39',
|
||||||
|
visible: true,
|
||||||
|
timeVisible: true,
|
||||||
|
secondsVisible: false,
|
||||||
|
tickMarkFormatter: (time, tickMarkType, locale) => {
|
||||||
|
return TimezoneConfig.formatTickMark(time);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
localization: {
|
||||||
|
timeFormatter: (timestamp) => {
|
||||||
|
return TimezoneConfig.formatDate(timestamp * 1000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handleScroll: true,
|
||||||
|
handleScale: true
|
||||||
|
});
|
||||||
|
|
||||||
|
equitySeries = equityChart.addSeries(LightweightCharts.AreaSeries, {
|
||||||
|
lineColor: totalPnlUsd >= 0 ? '#26a69a' : '#ef5350',
|
||||||
|
topColor: totalPnlUsd >= 0 ? 'rgba(38, 166, 154, 0.4)' : 'rgba(239, 83, 80, 0.4)',
|
||||||
|
bottomColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
lineWidth: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
equitySeries.setData(equityData['usd']);
|
||||||
|
equityChart.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pos Size Chart
|
||||||
|
const posSizeContainer = document.getElementById('posSizeChart');
|
||||||
|
if (posSizeContainer) {
|
||||||
|
posSizeContainer.innerHTML = '';
|
||||||
|
posSizeChart = LightweightCharts.createChart(posSizeContainer, {
|
||||||
|
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
||||||
|
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
|
||||||
|
rightPriceScale: { borderColor: '#2a2e39', autoScale: true },
|
||||||
|
timeScale: {
|
||||||
|
borderColor: '#2a2e39',
|
||||||
|
visible: true,
|
||||||
|
timeVisible: true,
|
||||||
|
secondsVisible: false,
|
||||||
|
tickMarkFormatter: (time, tickMarkType, locale) => {
|
||||||
|
return TimezoneConfig.formatTickMark(time);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
localization: {
|
||||||
|
timeFormatter: (timestamp) => {
|
||||||
|
return TimezoneConfig.formatDate(timestamp * 1000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handleScroll: true,
|
||||||
|
handleScale: true
|
||||||
|
});
|
||||||
|
|
||||||
|
posSeries = posSizeChart.addSeries(LightweightCharts.AreaSeries, {
|
||||||
|
lineColor: '#00bcd4',
|
||||||
|
topColor: 'rgba(0, 188, 212, 0.4)',
|
||||||
|
bottomColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
lineWidth: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
posSeries.setData(posSizeData['usd']);
|
||||||
|
posSizeChart.timeScale().fitContent();
|
||||||
|
|
||||||
|
const label = document.getElementById('posSizeLabel');
|
||||||
|
if (label) label.textContent = 'Position Size (USD)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync Time Scales
|
||||||
|
if (equityChart && posSizeChart) {
|
||||||
|
let isSyncing = false;
|
||||||
|
|
||||||
|
const syncCharts = (source, target) => {
|
||||||
|
if (isSyncing) return;
|
||||||
|
isSyncing = true;
|
||||||
|
const range = source.timeScale().getVisibleRange();
|
||||||
|
target.timeScale().setVisibleRange(range);
|
||||||
|
isSyncing = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
equityChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(equityChart, posSizeChart));
|
||||||
|
posSizeChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(posSizeChart, equityChart));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync to Main Chart on Click
|
||||||
|
const syncToMain = (param) => {
|
||||||
|
if (!param.time || !window.dashboard || !window.dashboard.chart) return;
|
||||||
|
|
||||||
|
const timeScale = window.dashboard.chart.timeScale();
|
||||||
|
const currentRange = timeScale.getVisibleRange();
|
||||||
|
if (!currentRange) return;
|
||||||
|
|
||||||
|
// Calculate current width to preserve zoom level
|
||||||
|
const width = currentRange.to - currentRange.from;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
|
||||||
|
timeScale.setVisibleRange({
|
||||||
|
from: param.time - halfWidth,
|
||||||
|
to: param.time + halfWidth
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (equityChart) equityChart.subscribeClick(syncToMain);
|
||||||
|
if (posSizeChart) posSizeChart.subscribeClick(syncToMain);
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(initCharts, 100);
|
||||||
|
|
||||||
|
// Toggle Logic
|
||||||
|
resultsDiv.querySelectorAll('.toggle-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const unit = btn.dataset.unit;
|
||||||
|
|
||||||
|
// Sync all toggle button groups
|
||||||
|
resultsDiv.querySelectorAll(`.toggle-btn`).forEach(b => {
|
||||||
|
if (b.dataset.unit === unit) b.classList.add('active');
|
||||||
|
else b.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (equitySeries) {
|
||||||
|
equitySeries.setData(equityData[unit]);
|
||||||
|
equityChart.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
if (posSeries) {
|
||||||
|
posSeries.setData(posSizeData[unit]);
|
||||||
|
posSizeChart.timeScale().fitContent();
|
||||||
|
|
||||||
|
const label = document.getElementById('posSizeLabel');
|
||||||
|
if (label) label.textContent = `Position Size (${unit.toUpperCase()})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('toggleTradeMarkers').addEventListener('click', () => {
|
||||||
|
toggleSimulationMarkers(trades);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('clearSim').addEventListener('click', () => {
|
||||||
|
resultsDiv.style.display = 'none';
|
||||||
|
clearSimulationMarkers();
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.dashboard.clearAvgPriceData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tradeMarkers = [];
|
||||||
|
|
||||||
|
function toggleSimulationMarkers(trades) {
|
||||||
|
if (tradeMarkers.length > 0) {
|
||||||
|
clearSimulationMarkers();
|
||||||
|
document.getElementById('toggleTradeMarkers').textContent = 'Show Markers';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const markers = [];
|
||||||
|
trades.forEach(t => {
|
||||||
|
const usdVal = t.currentUsd !== undefined ? `$${t.currentUsd.toFixed(0)}` : '0';
|
||||||
|
const qtyVal = t.currentQty !== undefined ? `${t.currentQty.toFixed(4)} BTC` : '0';
|
||||||
|
const sizeStr = ` (${usdVal} / ${qtyVal})`;
|
||||||
|
|
||||||
|
// Entry marker
|
||||||
|
if (t.recordType === 'entry') {
|
||||||
|
markers.push({
|
||||||
|
time: t.time,
|
||||||
|
position: t.type === 'long' ? 'belowBar' : 'aboveBar',
|
||||||
|
color: t.type === 'long' ? '#2962ff' : '#9c27b0',
|
||||||
|
shape: t.type === 'long' ? 'arrowUp' : 'arrowDown',
|
||||||
|
text: `Entry ${t.type.toUpperCase()}${sizeStr}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit marker
|
||||||
|
if (t.recordType === 'exit') {
|
||||||
|
markers.push({
|
||||||
|
time: t.time,
|
||||||
|
position: t.type === 'long' ? 'aboveBar' : 'belowBar',
|
||||||
|
color: t.pnl >= 0 ? '#26a69a' : '#ef5350',
|
||||||
|
shape: t.type === 'long' ? 'arrowDown' : 'arrowUp',
|
||||||
|
text: `Exit ${t.reason}${sizeStr}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort markers by time
|
||||||
|
markers.sort((a, b) => a.time - b.time);
|
||||||
|
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.dashboard.setSimulationMarkers(markers);
|
||||||
|
tradeMarkers = markers;
|
||||||
|
document.getElementById('toggleTradeMarkers').textContent = 'Hide Markers';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSimulationMarkers() {
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.dashboard.clearSimulationMarkers();
|
||||||
|
tradeMarkers = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.clearSimulationResults = function() {
|
||||||
|
const resultsDiv = document.getElementById('simulationResults');
|
||||||
|
if (resultsDiv) resultsDiv.style.display = 'none';
|
||||||
|
clearSimulationMarkers();
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user