import { watch } from 'vue' import type { IChartingLibraryWidget, IStudyApi } from '../types/tradingview' import { useIndicatorStore } from '../stores/indicators' import { useIndicatorTypesStore } from '../stores/indicatorTypes' import { useChartStore } from '../stores/chart' import type { IndicatorInstance } from '../stores/indicators' /** * Mapping between pandas-ta indicator function names and TradingView indicator display names. * Only includes indicators present in BOTH systems (inner join). * Keys are lowercase pandas-ta function names; values are TradingView display strings. */ const PANDAS_TA_TO_TV_NAMES: Record = { // Overlap / Moving Averages (17) 'sma': 'Moving Average', 'ema': 'Moving Average Exponential', 'wma': 'Moving Average Weighted', 'dema': 'Double EMA', 'tema': 'Triple EMA', 'trima': 'Triangular Moving Average', 'kama': 'Moving Average Adaptive', 't3': 'T3', 'hma': 'Hull Moving Average', 'alma': 'Arnaud Legoux Moving Average', 'midpoint': 'Midpoint', 'midprice': 'Midprice', 'supertrend': 'SuperTrend', 'ichimoku': 'Ichimoku Cloud', 'vwap': 'VWAP', 'vwma': 'VWMA', 'bbands': 'Bollinger Bands', // Momentum (22) 'rsi': 'Relative Strength Index', 'macd': 'MACD', 'stoch': 'Stochastic', 'stochrsi':'Stochastic RSI', 'cci': 'Commodity Channel Index', 'willr': 'Williams %R', 'mom': 'Momentum', 'roc': 'Rate Of Change', 'trix': 'TRIX', 'cmo': 'Chande Momentum Oscillator', 'adx': 'Average Directional Index', 'aroon': 'Aroon', 'ao': 'Awesome Oscillator', 'bop': 'Balance of Power', 'uo': 'Ultimate Oscillator', 'apo': 'Price Oscillator', 'mfi': 'Money Flow Index', 'coppock': 'Coppock Curve', 'dpo': 'Detrended Price Oscillator', 'fisher': 'Fisher Transform', 'rvgi': 'Relative Vigor Index', 'kst': 'Know Sure Thing', // Volatility (3) 'atr': 'Average True Range', 'kc': 'Keltner Channels', 'donchian': 'Donchian Channels', // Volume (8) 'obv': 'On Balance Volume', 'ad': 'Accumulation/Distribution', 'adosc': 'Chaikin Oscillator', 'cmf': 'Chaikin Money Flow', 'eom': 'Ease of Movement', 'efi': "Elder's Force Index", 'kvo': 'Klinger Oscillator', 'pvt': 'Price Volume Trend', // Statistics / Price Transforms (6) 'stdev': 'Standard Deviation', 'linreg': 'Linear Regression Curve', 'slope': 'Linear Regression Slope', 'hl2': 'Median Price', 'hlc3': 'Typical Price', 'ohlc4': 'Average Price', // Trend (3) 'psar': 'Parabolic SAR', 'vortex': 'Vortex Indicator', 'chop': 'Choppiness Index', } // Total: 59 indicators /** * Reverse mapping from TradingView display name to pandas-ta function name */ const TV_TO_PANDAS_TA_NAMES: Record = Object.fromEntries( Object.entries(PANDAS_TA_TO_TV_NAMES).map(([k, v]) => [v, k]) ) /** * Convert pandas-ta parameters to TradingView inputs */ function convertPandasTaParamsToTVInputs(pandasTaName: string, params: Record): Record { const tvInputs: Record = {} if (pandasTaName === 'bbands') { tvInputs.length = params.length || 20 tvInputs.mult = params.upper_std ?? params.std ?? 2 tvInputs.source = 'close' } else if (pandasTaName === 'macd') { tvInputs.fastLength = params.fast || 12 tvInputs.slowLength = params.slow || 26 tvInputs.signalLength = params.signal || 9 tvInputs.source = 'close' } else if (pandasTaName === 'stoch') { tvInputs.kPeriod = params.k || 14 tvInputs.dPeriod = params.d || 3 tvInputs.smoothK = params.smooth_k || 3 } else if (pandasTaName === 'stochrsi') { tvInputs.length = params.length || 14 tvInputs.rsiLength = params.rsi_length || 14 tvInputs.kPeriod = params.k || 3 tvInputs.dPeriod = params.d || 3 } else if (pandasTaName === 'psar') { tvInputs.start = params.af0 || 0.02 tvInputs.increment = params.af || 0.02 tvInputs.max = params.max_af || 0.2 } else if (pandasTaName === 'ichimoku') { tvInputs.conversionPeriod = params.tenkan || 9 tvInputs.basePeriod = params.kijun || 26 tvInputs.laggingSpanPeriod = params.senkou || 52 } else if (pandasTaName === 'vwap') { tvInputs.anchor = params.anchor || 'D' } else if (['apo', 'ppo'].includes(pandasTaName)) { tvInputs.fastLength = params.fast || 12 tvInputs.slowLength = params.slow || 26 tvInputs.source = 'close' } else if (pandasTaName === 'uo') { tvInputs.fast = params.fast || 7 tvInputs.medium = params.medium || 14 tvInputs.slow = params.slow || 28 } else if (pandasTaName === 'adosc') { tvInputs.fastLength = params.fast || 3 tvInputs.slowLength = params.slow || 10 } else if (pandasTaName === 'kc') { tvInputs.length = params.length || 20 tvInputs.mult = params.scalar || 2 } else { // Generic: pass length through; skip source if (params.length !== undefined) tvInputs.length = params.length for (const [k, v] of Object.entries(params)) { if (k === 'length' || k === 'source') continue tvInputs[k] = v } } return tvInputs } /** * Convert TradingView inputs to pandas-ta parameters */ function convertTVInputsToPandasTaParams(tvName: string, tvInputs: Record): { pandasTaName: string | null, pandasTaParams: Record } { const pandasTaName = TV_TO_PANDAS_TA_NAMES[tvName] || null if (!pandasTaName) { console.warn('[Indicators] No pandas-ta mapping for TradingView indicator:', tvName) return { pandasTaName: null, pandasTaParams: {} } } const pandasTaParams: Record = {} if (pandasTaName === 'bbands') { pandasTaParams.length = tvInputs.in_0 ?? tvInputs.length ?? 20 const std = tvInputs.in_1 ?? tvInputs.mult ?? 2 pandasTaParams.upper_std = std pandasTaParams.lower_std = std } else if (pandasTaName === 'macd') { pandasTaParams.fast = tvInputs.fastLength ?? 12 pandasTaParams.slow = tvInputs.slowLength ?? 26 pandasTaParams.signal = tvInputs.signalLength ?? 9 } else if (pandasTaName === 'stoch') { pandasTaParams.k = tvInputs.kPeriod ?? 14 pandasTaParams.d = tvInputs.dPeriod ?? 3 pandasTaParams.smooth_k = tvInputs.smoothK ?? 3 } else if (pandasTaName === 'stochrsi') { pandasTaParams.length = tvInputs.length ?? 14 pandasTaParams.rsi_length = tvInputs.rsiLength ?? 14 pandasTaParams.k = tvInputs.kPeriod ?? 3 pandasTaParams.d = tvInputs.dPeriod ?? 3 } else if (pandasTaName === 'psar') { pandasTaParams.af0 = tvInputs.start ?? 0.02 pandasTaParams.af = tvInputs.increment ?? 0.02 pandasTaParams.max_af = tvInputs.max ?? 0.2 } else if (pandasTaName === 'ichimoku') { pandasTaParams.tenkan = tvInputs.conversionPeriod ?? 9 pandasTaParams.kijun = tvInputs.basePeriod ?? 26 pandasTaParams.senkou = tvInputs.laggingSpanPeriod ?? 52 } else if (pandasTaName === 'vwap') { pandasTaParams.anchor = tvInputs.anchor ?? 'D' } else if (['apo', 'ppo'].includes(pandasTaName)) { pandasTaParams.fast = tvInputs.fastLength ?? 12 pandasTaParams.slow = tvInputs.slowLength ?? 26 } else if (pandasTaName === 'uo') { pandasTaParams.fast = tvInputs.fast ?? 7 pandasTaParams.medium = tvInputs.medium ?? 14 pandasTaParams.slow = tvInputs.slow ?? 28 } else if (pandasTaName === 'adosc') { pandasTaParams.fast = tvInputs.fastLength ?? 3 pandasTaParams.slow = tvInputs.slowLength ?? 10 } else if (pandasTaName === 'kc') { pandasTaParams.length = tvInputs.length ?? 20 pandasTaParams.scalar = tvInputs.mult ?? 2 } else { // Generic: extract length; skip source for (const [k, v] of Object.entries(tvInputs)) { if (k === 'source') continue pandasTaParams[k] = v } // Normalise TV in_0 → length for simple single-period indicators if (tvInputs.in_0 !== undefined && pandasTaParams.length === undefined) { pandasTaParams.length = tvInputs.in_0 delete pandasTaParams.in_0 } } return { pandasTaName, pandasTaParams } } /** * Setup bidirectional synchronization between TradingView indicators and IndicatorStore */ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) { const indicatorStore = useIndicatorStore() const chartStore = useChartStore() let isApplyingTVUpdate = false // Prevent circular updates when TV modifies indicators let isUpdatingStore = false // Prevent circular updates when we update the store from TV let isChartReady = false // Track if chart API is ready let isUnmounting = false // Set during cleanup to suppress study_event: remove from widget destruction /** * Convert TradingView study to our IndicatorInstance format */ function convertTVStudyToIndicator(tvStudy: any, symbol: string, studyId?: string): IndicatorInstance | null { try { console.log('[Indicators] convertTVStudyToIndicator called with:', { tvStudy, symbol, studyId }) console.log('[Indicators] Study object keys:', Object.keys(tvStudy)) console.log('[Indicators] Study object type:', typeof tvStudy) // Expose study object to window for debugging ;(window as any).__lastTVStudy = tvStudy console.log('[Indicators] Study object exposed as window.__lastTVStudy for debugging') // Study object has methods - name() gives the indicator type, title() gives display name let studyName = 'Unknown' // Try tvStudy.study().name() first if (typeof tvStudy.study === 'function') { try { const innerStudy = tvStudy.study() if (innerStudy && typeof innerStudy.name === 'function') { studyName = innerStudy.name() console.log('[Indicators] Called tvStudy.study().name():', studyName) } } catch (e) { console.log('[Indicators] Error calling study().name():', e) } } // Try name() directly if that didn't work if (studyName === 'Unknown' && typeof tvStudy.name === 'function') { try { studyName = tvStudy.name() console.log('[Indicators] Called study.name():', studyName) } catch (e) { console.log('[Indicators] Error calling name():', e) } } // Fallback to title() if name() didn't work if (studyName === 'Unknown' && typeof tvStudy.title === 'function') { try { studyName = tvStudy.title() console.log('[Indicators] Called study.title():', studyName) } catch (e) { console.log('[Indicators] Error calling title():', e) } } // Fallback: metaInfo.name is the registered study name (e.g. dxo_ind_custom_trendflex) if (studyName === 'Unknown' || (!studyName.startsWith('dxo_ind_') && !TV_TO_PANDAS_TA_NAMES[studyName])) { const metaName = tvStudy._study?.name?.() || tvStudy._metaInfo?.name || tvStudy.metaInfo?.()?.name if (metaName && typeof metaName === 'string') { console.log('[Indicators] Overriding studyName with metaInfo.name:', metaName) studyName = metaName } } // Last resort: if still unresolved, check all type descriptions if (!studyName.startsWith('dxo_ind_') && !TV_TO_PANDAS_TA_NAMES[studyName]) { const indicatorTypesStore = useIndicatorTypesStore() const title = typeof tvStudy.title === 'function' ? tvStudy.title() : null const desc = tvStudy._metaInfo?.description || tvStudy._metaInfo?.shortDescription // Also match against studyName itself (TV returns display name from study().name()) const matchByDesc = Object.values(indicatorTypesStore.types).find( t => t.display_name === studyName || t.display_name === title || t.display_name === desc ) if (matchByDesc) { studyName = `dxo_ind_${matchByDesc.pandas_ta_name}` console.log('[Indicators] Resolved studyName by description match:', studyName) } } console.log('[Indicators] Study name extracted:', studyName) // Handle our named custom indicator studies (dxo_ind_ prefix) if (studyName.startsWith('dxo_ind_')) { const pandasTaNameFromStudy = studyName.slice('dxo_ind_'.length) const indicatorTypesStore = useIndicatorTypesStore() const typeEntry = indicatorTypesStore.types[pandasTaNameFromStudy] if (!typeEntry) { console.log('[Indicators] Custom indicator type not found in store:', pandasTaNameFromStudy) return null } const defaultParams = Object.fromEntries( Object.entries(typeEntry.metadata.parameters).map(([k, v]) => [k, v.default]) ) const now = Math.floor(Date.now() / 1000) return { id: studyId || `ind_${Date.now()}`, pandas_ta_name: pandasTaNameFromStudy, instance_name: typeEntry.display_name, parameters: defaultParams, tv_study_id: studyId, tv_indicator_name: studyName, visible: true, pane: typeEntry.metadata.pane, symbol, created_at: now, modified_at: now, custom_metadata: typeEntry.metadata, } as IndicatorInstance } const pandasTaName = TV_TO_PANDAS_TA_NAMES[studyName] console.log('[Indicators] pandas-ta mapping:', studyName, '->', pandasTaName) if (!pandasTaName) { console.log('[Indicators] TradingView study not mapped to pandas-ta:', studyName) console.log('[Indicators] Available mappings:', Object.keys(TV_TO_PANDAS_TA_NAMES).slice(0, 10)) return null } // Extract inputs from study - try calling getInputValues() if it's a method let tvInputs: any = {} if (typeof tvStudy.getInputValues === 'function') { try { const inputsArray = tvStudy.getInputValues() console.log('[Indicators] Called study.getInputValues():', inputsArray) // Convert array of {id, value} objects to a simple object if (Array.isArray(inputsArray)) { tvInputs = {} for (const input of inputsArray) { if (input.id && input.value !== undefined) { tvInputs[input.id] = input.value } } console.log('[Indicators] Converted array inputs to object:', tvInputs) } else { tvInputs = inputsArray } } catch (e) { console.log('[Indicators] Error calling getInputValues():', e) } } else { tvInputs = tvStudy.inputs || tvStudy._inputs || {} } console.log('[Indicators] TV inputs:', tvInputs) const { pandasTaParams } = convertTVInputsToPandasTaParams(studyName, tvInputs) console.log('[Indicators] Converted pandas-ta params:', pandasTaParams) const now = Math.floor(Date.now() / 1000) const indicator = { id: studyId || tvStudy.id || `ind_${Date.now()}`, pandas_ta_name: pandasTaName, instance_name: `${pandasTaName}_${Date.now()}`, parameters: pandasTaParams, tv_study_id: studyId || tvStudy.id, tv_indicator_name: studyName, tv_inputs: tvInputs, visible: true, pane: 'chart', symbol, created_at: now, modified_at: now } console.log('[Indicators] Created indicator object:', indicator) return indicator } catch (error) { console.error('[Indicators] Error converting TV study:', error) return null } } /** * Setup listeners for TradingView indicator events */ function setupIndicatorListeners() { try { const chart = tvWidget.activeChart() if (!chart) { console.warn('[Indicators] Active chart not available yet') return () => {} } isChartReady = true // Apply any indicators that arrived before chart was ready (e.g. from workspace sync on page load) const pendingIndicators = Object.values(indicatorStore.indicators).filter(ind => !ind.tv_study_id) if (pendingIndicators.length > 0) { console.log('[Indicators] Chart ready, applying', pendingIndicators.length, 'pending indicators from store') isApplyingTVUpdate = true ;(async () => { try { for (const indicator of pendingIndicators) { await createTVStudy(indicator) } } finally { isApplyingTVUpdate = false } })() } console.log('[Indicators] Setting up indicator event subscriptions') console.log('[Indicators] Chart ready, widget:', tvWidget) // Subscribe to 'study_event' - fires for specific study instance events with ID // Callback signature: (studyId: string, eventType: string) tvWidget.subscribe('study_event', (studyId: any, eventType?: any) => { console.log('[Indicators] ===== STUDY_EVENT FIRED =====') console.log('[Indicators] Arg 1 (studyId):', studyId) console.log('[Indicators] Arg 2 (eventType):', eventType) if (isApplyingTVUpdate) { console.log('[Indicators] Ignoring study_event - isApplyingTVUpdate is true') return } const currentSymbol = chartStore.symbol // If studyId is actually an object, extract the real values let actualStudyId = studyId let actualEventType = eventType if (typeof studyId === 'object' && studyId !== null) { actualStudyId = studyId.id || studyId.studyId actualEventType = studyId.type || studyId.event || eventType } console.log('[Indicators] Parsed - Study ID:', actualStudyId, 'Event type:', actualEventType) if (!actualStudyId) { console.warn('[Indicators] No study ID found, cannot process event') return } // Filter out events we don't care about if (actualEventType && !['create', 'remove'].includes(actualEventType)) { console.log('[Indicators] Ignoring event type:', actualEventType) return } // If we have an event type, handle it specifically if (actualEventType === 'create') { console.log('[Indicators] New indicator created with ID:', actualStudyId) // Wait for TradingView to finish initializing the study setTimeout(() => { try { // Guard against feedback loop: if we created this study from the store // (via createTVStudy or registerCustomStudy), the store entry already has // tv_study_id set by the time this setTimeout fires. Skip in that case. const alreadyTracked = Object.values(indicatorStore.indicators).find( ind => ind.tv_study_id === actualStudyId ) if (alreadyTracked) { console.log('[Indicators] Study already tracked in store, skipping create event:', alreadyTracked.id) return } const study = chart.getStudyById(actualStudyId) if (study) { console.log('[Indicators] Retrieved new study after timeout') const indicator = convertTVStudyToIndicator(study, currentSymbol, actualStudyId) if (indicator) { console.log('[Indicators] Adding new indicator to store:', indicator) isUpdatingStore = true indicatorStore.addIndicator(indicator) console.log('[Indicators] Indicator added! Store now has:', Object.keys(indicatorStore.indicators).length, 'indicators') isUpdatingStore = false } else { console.warn('[Indicators] Conversion returned null - indicator not supported') } } } catch (error) { console.error('[Indicators] Error handling create event:', error) } }, 0) } else if (actualEventType === 'remove') { console.log('[Indicators] Indicator removed with ID:', actualStudyId) if (isUnmounting) { console.log('[Indicators] Ignoring study remove during widget destruction') return } const existingIndicator = Object.values(indicatorStore.indicators).find( ind => ind.tv_study_id === actualStudyId ) if (existingIndicator) { console.log('[Indicators] Removing indicator from store:', existingIndicator.id) isUpdatingStore = true indicatorStore.removeIndicator(existingIndicator.id) console.log('[Indicators] Indicator removed!') isUpdatingStore = false } else { console.log('[Indicators] Indicator not found in store for removal') } } else { // No event type provided, just a study ID - check if it exists console.log('[Indicators] Event fired with study ID but no type, checking if study exists...') try { const study = chart.getStudyById(actualStudyId) if (study) { console.log('[Indicators] Study exists - likely created or modified') const existingIndicator = Object.values(indicatorStore.indicators).find( ind => ind.tv_study_id === actualStudyId ) if (!existingIndicator) { // New indicator console.log('[Indicators] New indicator detected, adding to store...') const indicator = convertTVStudyToIndicator(study, currentSymbol, actualStudyId) if (indicator) { isUpdatingStore = true indicatorStore.addIndicator(indicator) console.log('[Indicators] Indicator added!') isUpdatingStore = false } } else { console.log('[Indicators] Indicator already exists in store') } } } catch (error) { // Study doesn't exist - it was removed console.log('[Indicators] Study does not exist - checking for removal...') const existingIndicator = Object.values(indicatorStore.indicators).find( ind => ind.tv_study_id === actualStudyId ) if (existingIndicator) { console.log('[Indicators] Removing indicator from store') isUpdatingStore = true indicatorStore.removeIndicator(existingIndicator.id) isUpdatingStore = false } } } console.log('[Indicators] ===== STUDY_EVENT COMPLETE =====') }) // Subscribe to 'study_properties_changed' - fires when indicator settings are modified // This is the key event for detecting when users edit indicator parameters // Callback signature: (studyId: string) tvWidget.subscribe('study_properties_changed', (studyId: any) => { console.log('[Indicators] ===== STUDY_PROPERTIES_CHANGED FIRED =====') console.log('[Indicators] Study ID:', studyId) if (isApplyingTVUpdate) { console.log('[Indicators] Ignoring properties change - isApplyingTVUpdate is true') return } const currentSymbol = chartStore.symbol if (studyId && typeof studyId === 'string') { console.log('[Indicators] Study properties changed for ID:', studyId) // Wait for TradingView to finish applying the changes setTimeout(() => { try { const study = chart.getStudyById(studyId) if (study) { console.log('[Indicators] Retrieved updated study') // Find existing indicator in store const existingIndicator = Object.values(indicatorStore.indicators).find( ind => ind.tv_study_id === studyId ) if (existingIndicator) { console.log('[Indicators] Found existing indicator:', existingIndicator.id) // Custom indicators expose param_* inputs that map to their parameters. // Extract those and update the store (useCustomIndicators will re-fetch data). if (existingIndicator.pandas_ta_name.startsWith('custom_')) { if (typeof study.getInputValues === 'function') { const inputsArray = study.getInputValues() if (Array.isArray(inputsArray)) { const newParams: Record = {} for (const input of inputsArray) { if (input.id && input.id.startsWith('param_')) { newParams[input.id.slice(6)] = input.value } } if (Object.keys(newParams).length > 0) { const hasChanged = Object.entries(newParams).some( ([k, v]) => existingIndicator.parameters[k] !== v ) if (hasChanged) { console.log('[Indicators] Custom indicator params changed:', newParams) isUpdatingStore = true indicatorStore.updateIndicator(existingIndicator.id, { parameters: newParams }) isUpdatingStore = false } } } } return } // Get the study name using study().name() let studyName = 'Unknown' if (typeof study.study === 'function') { const innerStudy = study.study() if (innerStudy && typeof innerStudy.name === 'function') { studyName = innerStudy.name() } } console.log('[Indicators] Study name:', studyName) // Get inputs - try calling getInputValues() if available let tvInputs: any = {} if (typeof study.getInputValues === 'function') { const inputsArray = study.getInputValues() // Convert array of {id, value} objects to a simple object if (Array.isArray(inputsArray)) { tvInputs = {} for (const input of inputsArray) { if (input.id && input.value !== undefined) { tvInputs[input.id] = input.value } } console.log('[Indicators] Converted inputs array to object:', tvInputs) } else { tvInputs = inputsArray } } console.log('[Indicators] TV inputs:', tvInputs) const { pandasTaParams } = convertTVInputsToPandasTaParams(studyName, tvInputs) console.log('[Indicators] Old params:', existingIndicator.parameters) console.log('[Indicators] New params:', pandasTaParams) // Update the store with new parameters if (JSON.stringify(existingIndicator.parameters) !== JSON.stringify(pandasTaParams)) { console.log('[Indicators] Parameters changed, updating store...') isUpdatingStore = true indicatorStore.updateIndicator(existingIndicator.id, { parameters: pandasTaParams, tv_inputs: tvInputs }) console.log('[Indicators] Indicator updated in store!') isUpdatingStore = false } else { console.log('[Indicators] Parameters unchanged (might be visual properties only)') } } else { console.log('[Indicators] No existing indicator found for properties change, ignoring') } } } catch (error) { console.error('[Indicators] Error handling study_properties_changed:', error) } }, 0) } else { console.log('[Indicators] study_properties_changed with no study ID, ignoring') } console.log('[Indicators] ===== STUDY_PROPERTIES_CHANGED COMPLETE =====') }) // Suppress study_event: remove during page unload (TV fires removes for all studies // as part of its own cleanup, which would otherwise wipe the persistence store) const onBeforeUnload = () => { isUnmounting = true } window.addEventListener('beforeunload', onBeforeUnload) // Cleanup function return () => { console.log('[Indicators] Cleaning up event subscriptions') isUnmounting = true window.removeEventListener('beforeunload', onBeforeUnload) try { tvWidget.unsubscribe('study_event') tvWidget.unsubscribe('study_properties_changed') } catch (error) { console.error('[Indicators] Error during cleanup:', error) } } } catch (error) { console.error('[Indicators] Error setting up indicator listeners:', error) return () => {} } } /** * Setup watchers for IndicatorStore changes to apply to TradingView */ function setupStoreWatchers() { watch( () => indicatorStore.indicators, async (newIndicators, oldIndicators) => { if (isUpdatingStore || !isChartReady) return isApplyingTVUpdate = true try { // Check if widget is ready before accessing activeChart if (!tvWidget || typeof tvWidget.activeChart !== 'function') { console.log('[Indicators] Widget not ready, skipping store update') isApplyingTVUpdate = false return } const chart = tvWidget.activeChart() if (!chart) { console.log('[Indicators] Chart not available, skipping store update') isApplyingTVUpdate = false return } const currentSymbol = chartStore.symbol // Find added indicators for (const [id, indicator] of Object.entries(newIndicators)) { if (indicator.symbol && indicator.symbol !== currentSymbol) continue if (!oldIndicators || !oldIndicators[id]) { // Indicator was added to store await createTVStudy(indicator) } else { // Check if indicator exists in TradingView let study = null if (indicator.tv_study_id) { try { study = chart.getStudyById(indicator.tv_study_id) } catch (e) { // Study doesn't exist } } if (!study) { // Indicator is in store but not in TradingView - create it await createTVStudy(indicator) } else if (JSON.stringify(indicator) !== JSON.stringify(oldIndicators[id])) { // Indicator was modified updateTVStudy(indicator) } } } // Find removed indicators if (oldIndicators) { for (const [id, indicator] of Object.entries(oldIndicators)) { if (indicator.symbol && indicator.symbol !== currentSymbol) continue if (!newIndicators[id]) { removeTVStudy(indicator) } } } } catch (error) { console.error('[Indicators] Error applying store changes:', error) } finally { isApplyingTVUpdate = false } }, { deep: true } ) } /** * Create an indicator in TradingView */ async function createTVStudy(indicator: IndicatorInstance) { if (!isChartReady) return // Custom indicators (pandas_ta_name starts with "custom_") are handled by // useCustomIndicators — they use TV createCustomStudy, not createStudy. if (indicator.pandas_ta_name.startsWith('custom_')) return try { const chart = tvWidget.activeChart() if (!chart) return const currentSymbol = chartStore.symbol const tvName = PANDAS_TA_TO_TV_NAMES[indicator.pandas_ta_name] if (!tvName) { console.warn('[Indicators] No TradingView mapping for pandas-ta indicator:', indicator.pandas_ta_name) return } const tvInputs = convertPandasTaParamsToTVInputs(indicator.pandas_ta_name, indicator.parameters) console.log(`[Indicators] Creating TradingView study: ${tvName} with inputs:`, tvInputs) const studyId = await chart.createStudy(tvName, false, false, tvInputs) // If TradingView assigned a different ID, update the store if (studyId && studyId !== indicator.tv_study_id) { isUpdatingStore = true await new Promise(resolve => setTimeout(resolve, 0)) try { indicatorStore.updateIndicator(indicator.id, { tv_study_id: studyId, symbol: indicator.symbol || currentSymbol }) } finally { await new Promise(resolve => setTimeout(resolve, 0)) isUpdatingStore = false } } } catch (error) { console.error('[Indicators] Error creating TV study:', error) } } /** * Update an indicator in TradingView */ function updateTVStudy(indicator: IndicatorInstance) { if (!isChartReady || !indicator.tv_study_id) return try { const chart = tvWidget.activeChart() if (!chart) return const study = chart.getStudyById(indicator.tv_study_id) if (!study) { console.warn('[Indicators] Study not found in TV, creating instead:', indicator.id) createTVStudy(indicator) return } const tvInputs = convertPandasTaParamsToTVInputs(indicator.pandas_ta_name, indicator.parameters) // Update study inputs chart.getStudyById(indicator.tv_study_id).applyOverrides(tvInputs) console.log('[Indicators] Updated study in TV:', indicator.id) } catch (error) { console.error('[Indicators] Error updating TV study:', error) } } /** * Remove an indicator from TradingView */ function removeTVStudy(indicator: IndicatorInstance) { if (!isChartReady || !indicator.tv_study_id) return try { const chart = tvWidget.activeChart() if (!chart) return chart.removeStudy(indicator.tv_study_id) console.log('[Indicators] Removed study from TV:', indicator.id) } catch (error) { console.error('[Indicators] Error removing TV study:', error) } } // Initialize const cleanupIndicatorListeners = setupIndicatorListeners() setupStoreWatchers() // Return cleanup function return () => { cleanupIndicatorListeners() } }