Improve indicators panel and fix data collector startup
Dashboard: - Add indicator checkboxes with multi-select support - Show color dots for each plot group in indicator list - Config panel always visible with helpful empty state message - Support multi-color indicators (HTS shows Fast/Slow colors) - Add line type selector (solid/dotted/dashed) and line width - Combine SMA/EMA into unified MA indicator with type selector Data Collector: - Fix missing asyncio import in database.py (caused NameError on timeout) - Add startup backfill for all standard intervals on collector restart - Add gap detection + time-based backfill for robustness - Add backfill_gap.py tool for manual gap filling - Optimize custom timeframe generation to only generate missing candles (not all from beginning) Files changed: - src/api/dashboard/static/js/ui/indicators-panel.js - src/api/dashboard/static/js/indicators/index.js, ma_indicator.js - src/api/dashboard/static/js/strategies/config.js - src/api/dashboard/static/js/ui/chart.js - src/api/dashboard/static/js/app.js - src/api/dashboard/static/index.html - src/data_collector/database.py - src/data_collector/main.py - src/data_collector/custom_timeframe_generator.py - src/data_collector/backfill_gap.py (new)
This commit is contained in:
@ -767,15 +767,84 @@
|
||||
.ta-ma-change.positive { color: var(--tv-green); }
|
||||
.ta-ma-change.negative { color: var(--tv-red); }
|
||||
|
||||
.indicator-item.selected {
|
||||
border-color: var(--tv-blue) !important;
|
||||
background: rgba(41, 98, 255, 0.1) !important;
|
||||
.indicator-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.indicator-item:hover {
|
||||
.indicator-checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.indicator-checkbox-item:hover {
|
||||
background: var(--tv-hover);
|
||||
}
|
||||
|
||||
.indicator-checkbox-item.configuring {
|
||||
background: rgba(41, 98, 255, 0.1);
|
||||
border-color: var(--tv-blue);
|
||||
}
|
||||
|
||||
.indicator-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: var(--tv-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.indicator-checkbox-item label {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: var(--tv-text);
|
||||
}
|
||||
|
||||
.indicator-config-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--tv-border);
|
||||
color: var(--tv-text-secondary);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.indicator-config-btn:hover {
|
||||
background: var(--tv-hover);
|
||||
border-color: var(--tv-blue);
|
||||
color: var(--tv-blue);
|
||||
}
|
||||
|
||||
.indicator-config-btn.active {
|
||||
background: var(--tv-blue);
|
||||
border-color: var(--tv-blue);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.indicator-color-dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.ta-level {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@ -22,8 +22,8 @@ import {
|
||||
} from './ui/strategies-panel.js';
|
||||
import {
|
||||
renderIndicatorList,
|
||||
addNewIndicator,
|
||||
selectIndicator,
|
||||
toggleIndicator,
|
||||
showIndicatorConfig,
|
||||
applyIndicatorConfig,
|
||||
removeIndicator,
|
||||
removeIndicatorByIndex,
|
||||
@ -64,6 +64,9 @@ window.loadSavedSimulation = loadSavedSimulation;
|
||||
window.deleteSavedSimulation = deleteSavedSimulation;
|
||||
window.clearSimulationResults = clearSimulationResults;
|
||||
window.updateTimeframeDisplay = updateTimeframeDisplay;
|
||||
window.renderIndicatorList = renderIndicatorList;
|
||||
window.toggleIndicator = toggleIndicator;
|
||||
window.showIndicatorConfig = showIndicatorConfig;
|
||||
|
||||
window.StrategyParams = StrategyParams;
|
||||
window.SimulationStorage = SimulationStorage;
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
export { MA } from './ma.js';
|
||||
export { BaseIndicator } from './base.js';
|
||||
export { HTSIndicator } from './hts.js';
|
||||
export { SMAIndicator } from './sma.js';
|
||||
export { EMAIndicator } from './ema.js';
|
||||
export { MAIndicator } from './ma_indicator.js';
|
||||
export { RSIIndicator } from './rsi.js';
|
||||
export { BollingerBandsIndicator } from './bb.js';
|
||||
export { MACDIndicator } from './macd.js';
|
||||
@ -10,8 +9,7 @@ export { StochasticIndicator } from './stoch.js';
|
||||
export { ATRIndicator } from './atr.js';
|
||||
|
||||
import { HTSIndicator } from './hts.js';
|
||||
import { SMAIndicator } from './sma.js';
|
||||
import { EMAIndicator } from './ema.js';
|
||||
import { MAIndicator } from './ma_indicator.js';
|
||||
import { RSIIndicator } from './rsi.js';
|
||||
import { BollingerBandsIndicator } from './bb.js';
|
||||
import { MACDIndicator } from './macd.js';
|
||||
@ -20,8 +18,7 @@ import { ATRIndicator } from './atr.js';
|
||||
|
||||
export const IndicatorRegistry = {
|
||||
hts: HTSIndicator,
|
||||
sma: SMAIndicator,
|
||||
ema: EMAIndicator,
|
||||
ma: MAIndicator,
|
||||
rsi: RSIIndicator,
|
||||
bb: BollingerBandsIndicator,
|
||||
macd: MACDIndicator,
|
||||
|
||||
21
src/api/dashboard/static/js/indicators/ma_indicator.js
Normal file
21
src/api/dashboard/static/js/indicators/ma_indicator.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { MA } from './ma.js';
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class MAIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 44;
|
||||
const maType = this.params.maType || 'SMA';
|
||||
return MA.get(maType, candles, period, 'close');
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'MA',
|
||||
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: 'value', color: '#2962ff', title: 'MA' }]
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -6,8 +6,7 @@ export const StrategyParams = {
|
||||
|
||||
export const AVAILABLE_INDICATORS = [
|
||||
{ type: 'hts', name: 'HTS Trend System', description: 'Fast/Slow MAs of High/Low prices' },
|
||||
{ type: 'sma', name: 'SMA', description: 'Simple Moving Average' },
|
||||
{ type: 'ema', name: 'EMA', description: 'Exponential Moving Average' },
|
||||
{ type: 'ma', name: 'MA', description: 'Moving Average (SMA/EMA/RMA/WMA/VWMA)' },
|
||||
{ type: 'rsi', name: 'RSI', description: 'Relative Strength Index' },
|
||||
{ type: 'bb', name: 'Bollinger Bands', description: 'Volatility bands' },
|
||||
{ type: 'macd', name: 'MACD', description: 'Moving Average Convergence Divergence' },
|
||||
|
||||
@ -22,6 +22,7 @@ export class TradingDashboard {
|
||||
|
||||
setInterval(() => {
|
||||
this.loadNewData();
|
||||
this.loadStats();
|
||||
if (new Date().getSeconds() < 15) this.loadTA();
|
||||
}, 10000);
|
||||
}
|
||||
@ -281,7 +282,10 @@ export class TradingDashboard {
|
||||
}
|
||||
|
||||
async loadInitialData() {
|
||||
await this.loadData(1000, true);
|
||||
await Promise.all([
|
||||
this.loadData(1000, true),
|
||||
this.loadStats()
|
||||
]);
|
||||
this.hasInitialLoad = true;
|
||||
}
|
||||
|
||||
@ -508,26 +512,33 @@ export class TradingDashboard {
|
||||
|
||||
<div class="ta-section">
|
||||
<div class="ta-section-title">Indicators</div>
|
||||
<div id="indicatorList" style="display: flex; flex-direction: column; gap: 6px; margin-top: 8px;"></div>
|
||||
<button class="ta-btn" onclick="addNewIndicator()" style="width: 100%; margin-top: 12px; font-size: 11px;">
|
||||
+ Add Indicator
|
||||
</button>
|
||||
<div id="indicatorList" class="indicator-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="ta-section" id="indicatorConfigPanel">
|
||||
<div class="ta-section-title">Configuration</div>
|
||||
<div id="configForm" style="margin-top: 8px;"></div>
|
||||
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
||||
<div style="display: flex; gap: 8px; margin-top: 12px;" id="configButtons">
|
||||
<button class="ta-btn" onclick="applyIndicatorConfig()" style="flex: 1; font-size: 11px; background: var(--tv-blue); color: white; border: none;">Apply</button>
|
||||
<button class="ta-btn" onclick="removeIndicator()" style="flex: 1; font-size: 11px; border-color: var(--tv-red); color: var(--tv-red);">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
window.renderIndicatorList?.();
|
||||
}
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/stats?symbol=BTC');
|
||||
this.statsData = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateStats(candle) {
|
||||
const price = candle.close;
|
||||
const change = ((price - candle.open) / candle.open * 100);
|
||||
const isUp = candle.close >= candle.open;
|
||||
|
||||
if (this.currentPriceLine) {
|
||||
@ -538,11 +549,15 @@ export class TradingDashboard {
|
||||
}
|
||||
|
||||
document.getElementById('currentPrice').textContent = price.toFixed(2);
|
||||
document.getElementById('currentPrice').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
|
||||
document.getElementById('priceChange').textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
|
||||
document.getElementById('priceChange').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
|
||||
document.getElementById('dailyHigh').textContent = candle.high.toFixed(2);
|
||||
document.getElementById('dailyLow').textContent = candle.low.toFixed(2);
|
||||
|
||||
if (this.statsData) {
|
||||
const change = this.statsData.change_24h;
|
||||
document.getElementById('currentPrice').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
|
||||
document.getElementById('priceChange').textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
|
||||
document.getElementById('priceChange').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
|
||||
document.getElementById('dailyHigh').textContent = this.statsData.high_24h.toFixed(2);
|
||||
document.getElementById('dailyLow').textContent = this.statsData.low_24h.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
switchTimeframe(interval) {
|
||||
|
||||
@ -2,7 +2,40 @@ import { AVAILABLE_INDICATORS } from '../strategies/config.js';
|
||||
import { IndicatorRegistry as IR } from '../indicators/index.js';
|
||||
|
||||
let activeIndicators = [];
|
||||
let selectedIndicatorIndex = -1;
|
||||
let configuringIndex = -1;
|
||||
|
||||
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 getPlotGroupName(plotId) {
|
||||
if (plotId.toLowerCase().includes('fast')) return 'Fast';
|
||||
if (plotId.toLowerCase().includes('slow')) return 'Slow';
|
||||
if (plotId.toLowerCase().includes('upper')) return 'Upper';
|
||||
if (plotId.toLowerCase().includes('lower')) return 'Lower';
|
||||
if (plotId.toLowerCase().includes('middle') || plotId.toLowerCase().includes('basis')) return 'Middle';
|
||||
if (plotId.toLowerCase().includes('signal')) return 'Signal';
|
||||
if (plotId.toLowerCase().includes('histogram')) return 'Histogram';
|
||||
if (plotId.toLowerCase().includes('k')) return '%K';
|
||||
if (plotId.toLowerCase().includes('d')) return '%D';
|
||||
return plotId;
|
||||
}
|
||||
|
||||
function groupPlotsByColor(plots) {
|
||||
const groups = {};
|
||||
plots.forEach((plot, idx) => {
|
||||
const groupName = getPlotGroupName(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 getActiveIndicators() {
|
||||
return activeIndicators;
|
||||
@ -16,109 +49,221 @@ export function renderIndicatorList() {
|
||||
const container = document.getElementById('indicatorList');
|
||||
if (!container) return;
|
||||
|
||||
if (activeIndicators.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 10px; font-size: 12px;">No indicators added</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = activeIndicators.map((ind, idx) => `
|
||||
<div class="indicator-item ${idx === selectedIndicatorIndex ? 'selected' : ''}"
|
||||
onclick="selectIndicator(${idx})"
|
||||
style="display: flex; align-items: center; justify-content: space-between; padding: 8px; background: var(--tv-bg); border-radius: 4px; cursor: pointer; border: 1px solid ${idx === selectedIndicatorIndex ? 'var(--tv-blue)' : 'var(--tv-border)'}">
|
||||
<div>
|
||||
<div style="font-size: 12px; font-weight: 600;">${ind.name}</div>
|
||||
<div style="font-size: 10px; color: var(--tv-text-secondary);">${ind.params.short || ind.params.period || ind.params.fast || 'N/A'}</div>
|
||||
container.innerHTML = AVAILABLE_INDICATORS.map((ind, idx) => {
|
||||
const activeIdx = activeIndicators.findIndex(a => a.type === ind.type);
|
||||
const isActive = activeIdx >= 0;
|
||||
const isConfiguring = activeIdx === configuringIndex;
|
||||
|
||||
let colorDots = '';
|
||||
if (isActive) {
|
||||
const indicator = activeIndicators[activeIdx];
|
||||
const plotGroups = groupPlotsByColor(indicator.plots || []);
|
||||
colorDots = plotGroups.map(group => {
|
||||
const firstIdx = group.indices[0];
|
||||
const color = indicator.params[`_color_${firstIdx}`] || '#2962ff';
|
||||
return `<span class="indicator-color-dot" style="background: ${color};"></span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="indicator-checkbox-item ${isConfiguring ? 'configuring' : ''}"
|
||||
onclick="toggleIndicator('${ind.type}')"
|
||||
title="${ind.description}">
|
||||
<input type="checkbox"
|
||||
id="ind_${ind.type}"
|
||||
${isActive ? 'checked' : ''}
|
||||
onclick="event.stopPropagation(); toggleIndicator('${ind.type}')"
|
||||
class="indicator-checkbox">
|
||||
<label for="ind_${ind.type}" onclick="event.stopPropagation(); toggleIndicator('${ind.type}')" style="flex: 1;">
|
||||
${ind.name}
|
||||
${colorDots}
|
||||
</label>
|
||||
${isActive ? `<button class="indicator-config-btn ${isConfiguring ? 'active' : ''}"
|
||||
onclick="event.stopPropagation(); showIndicatorConfig(${activeIdx})"
|
||||
title="Configure ${ind.name}">⚙</button>` : ''}
|
||||
</div>
|
||||
<button onclick="event.stopPropagation(); removeIndicatorByIndex(${idx})" style="background: none; border: none; color: var(--tv-red); cursor: pointer; font-size: 14px;">×</button>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
updateConfigPanel();
|
||||
}
|
||||
|
||||
function updateConfigPanel() {
|
||||
const configPanel = document.getElementById('indicatorConfigPanel');
|
||||
if (selectedIndicatorIndex >= 0) {
|
||||
configPanel.style.display = 'block';
|
||||
renderIndicatorConfig(selectedIndicatorIndex);
|
||||
const configButtons = document.getElementById('configButtons');
|
||||
if (!configPanel) return;
|
||||
|
||||
configPanel.style.display = 'block';
|
||||
|
||||
if (configuringIndex >= 0 && configuringIndex < activeIndicators.length) {
|
||||
renderIndicatorConfig(configuringIndex);
|
||||
if (configButtons) configButtons.style.display = 'flex';
|
||||
} else {
|
||||
configPanel.style.display = 'none';
|
||||
const container = document.getElementById('configForm');
|
||||
if (container) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px; font-size: 12px;">Select an active indicator to configure its settings</div>';
|
||||
}
|
||||
if (configButtons) configButtons.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export function addNewIndicator() {
|
||||
const type = prompt('Enter indicator type:\n' + AVAILABLE_INDICATORS.map(i => `${i.type}: ${i.name}`).join('\n'));
|
||||
if (!type) return;
|
||||
export function toggleIndicator(type) {
|
||||
const existingIdx = activeIndicators.findIndex(a => a.type === type);
|
||||
|
||||
const indicatorDef = AVAILABLE_INDICATORS.find(i => i.type === type.toLowerCase());
|
||||
if (!indicatorDef) {
|
||||
alert('Unknown indicator type');
|
||||
return;
|
||||
if (existingIdx >= 0) {
|
||||
activeIndicators[existingIdx].series?.forEach(s => {
|
||||
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||
});
|
||||
activeIndicators.splice(existingIdx, 1);
|
||||
if (configuringIndex >= activeIndicators.length) {
|
||||
configuringIndex = -1;
|
||||
} else if (configuringIndex === existingIdx) {
|
||||
configuringIndex = -1;
|
||||
} else if (configuringIndex > existingIdx) {
|
||||
configuringIndex--;
|
||||
}
|
||||
} else {
|
||||
const indicatorDef = AVAILABLE_INDICATORS.find(i => i.type === type);
|
||||
if (!indicatorDef) return;
|
||||
|
||||
const IndicatorClass = IR?.[type];
|
||||
if (!IndicatorClass) return;
|
||||
|
||||
const instance = new IndicatorClass({ type, params: {}, name: indicatorDef.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({
|
||||
type: type,
|
||||
name: metadata.name,
|
||||
params: params,
|
||||
plots: metadata.plots,
|
||||
series: []
|
||||
});
|
||||
|
||||
configuringIndex = activeIndicators.length - 1;
|
||||
}
|
||||
|
||||
const IndicatorClass = IR?.[type.toLowerCase()];
|
||||
if (!IndicatorClass) {
|
||||
alert('Indicator class not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = new IndicatorClass({ type: type.toLowerCase(), params: {}, name: indicatorDef.name });
|
||||
const metadata = instance.getMetadata();
|
||||
|
||||
const params = {};
|
||||
metadata.inputs.forEach(input => {
|
||||
params[input.name] = input.default;
|
||||
});
|
||||
|
||||
activeIndicators.push({
|
||||
type: type.toLowerCase(),
|
||||
name: metadata.name,
|
||||
params: params,
|
||||
plots: metadata.plots,
|
||||
series: []
|
||||
});
|
||||
|
||||
selectedIndicatorIndex = activeIndicators.length - 1;
|
||||
renderIndicatorList();
|
||||
drawIndicatorsOnChart();
|
||||
}
|
||||
|
||||
export function selectIndicator(index) {
|
||||
selectedIndicatorIndex = index;
|
||||
export function showIndicatorConfig(index) {
|
||||
if (configuringIndex === index) {
|
||||
configuringIndex = -1;
|
||||
} else {
|
||||
configuringIndex = index;
|
||||
}
|
||||
renderIndicatorList();
|
||||
}
|
||||
|
||||
export function showIndicatorConfigByType(type) {
|
||||
const idx = activeIndicators.findIndex(a => a.type === type);
|
||||
if (idx >= 0) {
|
||||
configuringIndex = idx;
|
||||
renderIndicatorList();
|
||||
}
|
||||
}
|
||||
|
||||
export function renderIndicatorConfig(index) {
|
||||
const container = document.getElementById('configForm');
|
||||
if (!container) return;
|
||||
|
||||
const indicator = activeIndicators[index];
|
||||
if (!indicator || !indicator.plots) {
|
||||
if (!indicator) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const IndicatorClass = IR?.[indicator.type];
|
||||
if (!IndicatorClass) return;
|
||||
if (!IndicatorClass) {
|
||||
container.innerHTML = '<div style="color: var(--tv-red);">Error loading indicator</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||
const meta = instance.getMetadata();
|
||||
|
||||
container.innerHTML = meta.inputs.map(input => `
|
||||
const plotGroups = groupPlotsByColor(meta.plots);
|
||||
|
||||
const colorInputs = plotGroups.map(group => {
|
||||
const firstIdx = group.indices[0];
|
||||
const color = indicator.params[`_color_${firstIdx}`] || meta.plots[firstIdx].color || '#2962ff';
|
||||
return `
|
||||
<div style="margin-bottom: 8px;">
|
||||
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${group.name} Color</label>
|
||||
<input type="color" id="config__color_${firstIdx}" value="${color}" style="width: 100%; height: 28px; border: 1px solid var(--tv-border); border-radius: 4px; cursor: pointer; background: var(--tv-bg);">
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="font-size: 11px; color: var(--tv-blue); margin-bottom: 8px; font-weight: 600;">${indicator.name}</div>
|
||||
|
||||
${colorInputs}
|
||||
|
||||
<div style="margin-bottom: 8px;">
|
||||
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
|
||||
${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>` :
|
||||
`<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;">`
|
||||
}
|
||||
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">Line Type</label>
|
||||
<select id="config__lineType" class="sim-input" style="font-size: 12px; padding: 6px;">
|
||||
${LINE_TYPES.map(lt => `<option value="${lt}" ${indicator.params._lineType === lt ? 'selected' : ''}>${lt.charAt(0).toUpperCase() + lt.slice(1)}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
<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>
|
||||
<input type="number" id="config__lineWidth" class="sim-input" value="${indicator.params._lineWidth || 2}" min="1" max="5" style="font-size: 12px; padding: 6px;">
|
||||
</div>
|
||||
|
||||
${meta.inputs.map(input => `
|
||||
<div style="margin-bottom: 8px;">
|
||||
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
|
||||
${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>` :
|
||||
`<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;">`
|
||||
}
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
export function applyIndicatorConfig() {
|
||||
if (selectedIndicatorIndex < 0) return;
|
||||
if (configuringIndex < 0 || configuringIndex >= activeIndicators.length) return;
|
||||
|
||||
const indicator = activeIndicators[selectedIndicatorIndex];
|
||||
const indicator = activeIndicators[configuringIndex];
|
||||
const IndicatorClass = IR?.[indicator.type];
|
||||
if (!IndicatorClass) return;
|
||||
|
||||
const instance = new IndicatorClass({ type: indicator.type, params: {}, name: indicator.name });
|
||||
const meta = instance.getMetadata();
|
||||
|
||||
const plotGroups = groupPlotsByColor(meta.plots);
|
||||
plotGroups.forEach(group => {
|
||||
const firstIdx = group.indices[0];
|
||||
const colorEl = document.getElementById(`config__color_${firstIdx}`);
|
||||
if (colorEl) {
|
||||
const color = colorEl.value;
|
||||
group.indices.forEach(idx => {
|
||||
indicator.params[`_color_${idx}`] = color;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const lineTypeEl = document.getElementById('config__lineType');
|
||||
const lineWidthEl = document.getElementById('config__lineWidth');
|
||||
|
||||
if (lineTypeEl) indicator.params._lineType = lineTypeEl.value;
|
||||
if (lineWidthEl) indicator.params._lineWidth = parseInt(lineWidthEl.value);
|
||||
|
||||
meta.inputs.forEach(input => {
|
||||
const el = document.getElementById(`config_${input.name}`);
|
||||
if (el) {
|
||||
@ -131,18 +276,33 @@ export function applyIndicatorConfig() {
|
||||
}
|
||||
|
||||
export function removeIndicator() {
|
||||
if (selectedIndicatorIndex < 0) return;
|
||||
activeIndicators.splice(selectedIndicatorIndex, 1);
|
||||
selectedIndicatorIndex = -1;
|
||||
if (configuringIndex < 0 || configuringIndex >= activeIndicators.length) return;
|
||||
|
||||
activeIndicators[configuringIndex].series?.forEach(s => {
|
||||
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||
});
|
||||
|
||||
activeIndicators.splice(configuringIndex, 1);
|
||||
configuringIndex = -1;
|
||||
|
||||
renderIndicatorList();
|
||||
drawIndicatorsOnChart();
|
||||
}
|
||||
|
||||
export function removeIndicatorByIndex(index) {
|
||||
if (index < 0 || index >= activeIndicators.length) return;
|
||||
|
||||
activeIndicators[index].series?.forEach(s => {
|
||||
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||
});
|
||||
|
||||
activeIndicators.splice(index, 1);
|
||||
if (selectedIndicatorIndex >= activeIndicators.length) {
|
||||
selectedIndicatorIndex = activeIndicators.length - 1;
|
||||
if (configuringIndex === index) {
|
||||
configuringIndex = -1;
|
||||
} else if (configuringIndex > index) {
|
||||
configuringIndex--;
|
||||
}
|
||||
|
||||
renderIndicatorList();
|
||||
drawIndicatorsOnChart();
|
||||
}
|
||||
@ -159,6 +319,8 @@ export function drawIndicatorsOnChart() {
|
||||
const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
|
||||
if (!candles || candles.length === 0) return;
|
||||
|
||||
const lineStyleMap = { 'solid': 0, 'dotted': 1, 'dashed': 2 };
|
||||
|
||||
activeIndicators.forEach((indicator, idx) => {
|
||||
const IndicatorClass = IR?.[indicator.type];
|
||||
if (!IndicatorClass) return;
|
||||
@ -173,30 +335,53 @@ export function drawIndicatorsOnChart() {
|
||||
const meta = instance.getMetadata();
|
||||
indicator.series = [];
|
||||
|
||||
meta.plots.forEach(plot => {
|
||||
if (results[0] && typeof results[0][plot.id] === 'undefined') return;
|
||||
const lineStyle = lineStyleMap[indicator.params._lineType] || 0;
|
||||
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 && typeof firstNonNull[plot.id] === 'undefined') return;
|
||||
|
||||
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||
|
||||
const lineSeries = window.dashboard.chart.addLineSeries({
|
||||
color: plot.color || '#2962ff',
|
||||
lineWidth: plot.width || 1,
|
||||
color: plotColor,
|
||||
lineWidth: plot.width || lineWidth,
|
||||
lineStyle: lineStyle,
|
||||
title: plot.title,
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: true
|
||||
});
|
||||
|
||||
const data = candles.map((c, i) => ({
|
||||
time: c.time,
|
||||
value: results[i]?.[plot.id] ?? null
|
||||
})).filter(d => d.value !== null && d.value !== undefined);
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
lineSeries.setData(data);
|
||||
indicator.series.push(lineSeries);
|
||||
if (data.length > 0) {
|
||||
lineSeries.setData(data);
|
||||
indicator.series.push(lineSeries);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.addNewIndicator = addNewIndicator;
|
||||
window.selectIndicator = selectIndicator;
|
||||
window.toggleIndicator = toggleIndicator;
|
||||
window.showIndicatorConfig = showIndicatorConfig;
|
||||
window.applyIndicatorConfig = applyIndicatorConfig;
|
||||
window.removeIndicator = removeIndicator;
|
||||
window.removeIndicatorByIndex = removeIndicatorByIndex;
|
||||
|
||||
Reference in New Issue
Block a user