diff --git a/src/api/dashboard/static/js/indicators/hurst.js b/src/api/dashboard/static/js/indicators/hurst.js index 2e511db..b8cb4dc 100644 --- a/src/api/dashboard/static/js/indicators/hurst.js +++ b/src/api/dashboard/static/js/indicators/hurst.js @@ -4,14 +4,12 @@ const SIGNAL_TYPES = { BUY: 'buy', - SELL: 'sell', - HOLD: 'hold' + SELL: 'sell' }; const SIGNAL_COLORS = { - buy: '#26a69a', - hold: '#787b86', - sell: '#ef5350' + buy: '#9e9e9e', + sell: '#9e9e9e' }; class BaseIndicator { @@ -30,62 +28,30 @@ class BaseIndicator { } } -// Calculate ATR (Average True Range) -function calculateATR(candles, period) { - const atr = new Array(candles.length).fill(null); - - for (let i = 1; i < candles.length; i++) { - const high = candles[i].high; - const low = candles[i].low; - const prevClose = candles[i - 1].close; - - const tr = Math.max( - high - low, - Math.abs(high - prevClose), - Math.abs(low - prevClose) - ); - - if (i >= period) { - let sum = 0; - for (let j = 0; j < period; j++) { - const idx = i - j; - const h = candles[idx].high; - const l = candles[idx].low; - const pc = candles[idx - 1]?.close || h; - sum += Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc)); - } - atr[i] = sum / period; - } else { - let sum = 0; - for (let j = 1; j <= i; j++) { - const h = candles[j].high; - const l = candles[j].low; - const pc = candles[j - 1].close; - sum += Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc)); - } - atr[i] = sum / (i + 1); - } - } - - return atr; -} - // Calculate RMA (Rolling Moving Average - Wilder's method) -function calculateRMA(values, period) { - const rma = new Array(values.length).fill(null); - +// Recreates Pine Script's ta.rma() exactly +function calculateRMA(sourceArray, length) { + const rma = new Array(sourceArray.length).fill(null); let sum = 0; - for (let i = 0; i < period && i < values.length; i++) { - sum += values[i]; - if (i === period - 1) { - rma[i] = sum / period; - } - } + const alpha = 1 / length; - const alpha = 1 / period; - for (let i = period; i < values.length; i++) { - if (rma[i - 1] !== null) { - rma[i] = rma[i - 1] + alpha * (values[i] - rma[i - 1]); + // 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; } } @@ -128,36 +94,58 @@ export class HurstBandsIndicator extends BaseIndicator { 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 = this.params.period || 30; + const mcl_t = this.params.period || 30; const mcm = this.params.multiplier || 1.8; - const results = new Array(candles.length).fill(null); + 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 = calculateATR(candles, mcl); + const atr = calculateRMA(trArray, mcl); - const mcl_2 = Math.floor(mcl / 2); - - for (let i = mcl_2; i < candles.length; i++) { - const ma_val = ma_mcl[i]; - const atr_val = atr[i]; + for (let i = 0; i < candles.length; i++) { + const src = closes[i]; + const mcm_off = mcm * (atr[i] || 0); - if (ma_val === null || atr_val === null) continue; + const historicalIndex = i - mcl_2; + const historical_ma = historicalIndex >= 0 ? ma_mcl[historicalIndex] : null; - const displacedIdx = i - mcl_2; - const displacedRMA = displacedIdx >= 0 ? ma_mcl[displacedIdx] : null; - - if (displacedRMA === null) continue; - - const mcm_off = mcm * atr_val; + const centerLine = (historical_ma === null || historical_ma === undefined || isNaN(historical_ma)) ? src : historical_ma; results[i] = { - upper: displacedRMA + mcm_off, - lower: displacedRMA - mcm_off + upper: centerLine + mcm_off, + lower: centerLine - mcm_off }; } @@ -169,16 +157,19 @@ export class HurstBandsIndicator extends BaseIndicator { name: 'Hurst Bands', description: 'Cyclic price channels based on Hurst theory', inputs: [ - { name: 'period', label: 'Period (mcl)', type: 'number', default: 30, min: 5, max: 200 }, + { 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: '#ff6b6b', title: 'Upper' }, - { id: 'lower', color: '#4caf50', title: 'Lower' } + { 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 }; +export { calculateHurstSignal }; \ No newline at end of file diff --git a/src/api/dashboard/static/js/ui/indicators-panel-new.js b/src/api/dashboard/static/js/ui/indicators-panel-new.js index ad81d94..c30713d 100644 --- a/src/api/dashboard/static/js/ui/indicators-panel-new.js +++ b/src/api/dashboard/static/js/ui/indicators-panel-new.js @@ -15,7 +15,16 @@ let indicatorPanes = new Map(); let nextPaneIndex = 1; // Presets storage -let userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}'); +let userPresets; +try { + userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}'); + if (!userPresets || typeof userPresets !== 'object') { + userPresets = { presets: [] }; + } +} catch (e) { + console.warn('Failed to parse presets:', e); + userPresets = { presets: [] }; +} // Categories const CATEGORIES = [ @@ -589,35 +598,78 @@ function getPresetsForIndicator(indicatorName) { } window.savePreset = function(id) { + console.log('[savePreset] Attempting to save preset for id:', id); const indicator = activeIndicators.find(a => a.id === id); - if (!indicator) return; + if (!indicator) { + console.error('[savePreset] Indicator not found for id:', id); + return; + } const presetName = prompt('Enter preset name:'); if (!presetName) return; const IndicatorClass = IR?.[indicator.type]; - if (!IndicatorClass) return; + if (!IndicatorClass) { + console.error('[savePreset] Indicator class not found for type:', indicator.type); + 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!`); + try { + 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: {} + }; + + // Save standard inputs + if (meta.inputs && Array.isArray(meta.inputs)) { + meta.inputs.forEach(input => { + preset.values[input.name] = indicator.params[input.name]; + }); + } + + // Save visual settings (line width, type, colors) + preset.values._lineWidth = indicator.params._lineWidth; + preset.values._lineType = indicator.params._lineType; + + // Save colors for each plot + if (meta.plots && Array.isArray(meta.plots)) { + meta.plots.forEach((plot, idx) => { + const colorKey = `_color_${idx}`; + if (indicator.params[colorKey]) { + preset.values[colorKey] = indicator.params[colorKey]; + } + }); + } + + // Save marker settings + const markerKeys = [ + 'showMarkers', + 'markerBuyShape', 'markerBuyColor', 'markerBuyCustom', + 'markerSellShape', 'markerSellColor', 'markerSellCustom' + ]; + markerKeys.forEach(key => { + if (indicator.params[key] !== undefined) { + preset.values[key] = indicator.params[key]; + } + }); + + if (!userPresets) userPresets = { presets: [] }; + if (!userPresets.presets) userPresets.presets = []; + + userPresets.presets.push(preset); + saveUserPresets(); + renderIndicatorPanel(); + + alert(`Preset "${presetName}" saved!`); + } catch (error) { + console.error('[savePreset] Error saving preset:', error); + alert('Error saving preset. See console for details.'); + } }; window.applyPreset = function(id, presetId) { @@ -688,15 +740,27 @@ function addIndicator(type) { const params = { _lineType: 'solid', - _lineWidth: 2, + _lineWidth: 1, showMarkers: true, - markerBuyShape: 'arrowUp', - markerBuyColor: '#26a69a', - markerBuyCustom: '◭', - markerSellShape: 'arrowDown', - markerSellColor: '#ef5350', + markerBuyShape: 'custom', + markerBuyColor: '#9e9e9e', + markerBuyCustom: '▲', + markerSellShape: 'custom', + markerSellColor: '#9e9e9e', markerSellCustom: '▼' }; + + // Override with Hurst-specific defaults + if (type === 'hurst') { + params._lineWidth = 1; + params.markerBuyShape = 'custom'; + params.markerSellShape = 'custom'; + params.markerBuyColor = '#9e9e9e'; + params.markerSellColor = '#9e9e9e'; + params.markerBuyCustom = '▲'; + params.markerSellCustom = '▼'; + } + metadata.plots.forEach((plot, idx) => { params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx); }); @@ -751,7 +815,7 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li indicator.series = []; 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 isObjectResult = firstNonNull && typeof firstNonNull === 'object'; diff --git a/src/api/dashboard/static/js/ui/indicators-panel.js b/src/api/dashboard/static/js/ui/indicators-panel.js index 9880884..e103d68 100644 --- a/src/api/dashboard/static/js/ui/indicators-panel.js +++ b/src/api/dashboard/static/js/ui/indicators-panel.js @@ -235,8 +235,19 @@ export function addIndicator(type) { const params = { _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) => { params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx); }); @@ -326,7 +337,7 @@ export function renderIndicatorConfig(indicator) {