custom indicators fixed

This commit is contained in:
2026-04-09 17:00:43 -04:00
parent a70dcd954f
commit fd431516cc
17 changed files with 778 additions and 440 deletions

View File

@@ -1,6 +1,7 @@
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'
@@ -230,6 +231,7 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
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
@@ -280,15 +282,63 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
}
}
// Final fallback to property access
if (studyName === 'Unknown') {
studyName = tvStudy._study?.name?.() ||
tvStudy._metaInfo?.description ||
tvStudy._metaInfo?.shortDescription || 'Unknown'
// 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)
@@ -429,6 +479,17 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
// 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')
@@ -452,6 +513,11 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
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
)
@@ -545,6 +611,35 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
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<string, any> = {}
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') {
@@ -596,11 +691,7 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
console.log('[Indicators] Parameters unchanged (might be visual properties only)')
}
} else {
console.log('[Indicators] No existing indicator found, doing full sync...')
// Might be a new indicator, do full sync
if (!isUpdatingStore && !isApplyingTVUpdate) {
syncIndicatorsFromTV()
}
console.log('[Indicators] No existing indicator found for properties change, ignoring')
}
}
} catch (error) {
@@ -608,35 +699,25 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
}
}, 0)
} else {
console.log('[Indicators] No study ID in event, doing full sync...')
// Fallback to full sync
setTimeout(() => {
if (!isUpdatingStore && !isApplyingTVUpdate) {
syncIndicatorsFromTV()
}
}, 100)
console.log('[Indicators] study_properties_changed with no study ID, ignoring')
}
console.log('[Indicators] ===== STUDY_PROPERTIES_CHANGED COMPLETE =====')
})
// Initial sync on data load
const dataLoadedSubscription = chart.onDataLoaded().subscribe(null, () => {
console.log('[Indicators] Chart data loaded, performing initial sync')
syncIndicatorsFromTV()
})
// 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 {
// Unsubscribe from all widget events
tvWidget.unsubscribe('study_event')
tvWidget.unsubscribe('study_properties_changed')
if (dataLoadedSubscription && typeof dataLoadedSubscription.unsubscribe === 'function') {
dataLoadedSubscription.unsubscribe()
}
} catch (error) {
console.error('[Indicators] Error during cleanup:', error)
}
@@ -647,74 +728,6 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
}
}
/**
* Scan TradingView for indicators and sync to store
*/
function syncIndicatorsFromTV() {
if (!isChartReady) return
try {
const chart = tvWidget.activeChart()
if (!chart) return
const currentSymbol = chartStore.symbol
const allStudies = chart.getAllStudies()
if (!allStudies) return
const seenStudyIds = new Set<string>()
isUpdatingStore = true
try {
for (const studyInfo of allStudies) {
seenStudyIds.add(studyInfo.id)
// getAllStudies() returns simple objects {id, name}
// We need to get the full study object using getStudyById()
try {
const tvStudy = chart.getStudyById(studyInfo.id)
if (!tvStudy) continue
const indicator = convertTVStudyToIndicator(tvStudy, currentSymbol, studyInfo.id)
if (!indicator) continue
const existingIndicator = Object.values(indicatorStore.indicators).find(
ind => ind.tv_study_id === studyInfo.id
)
if (!existingIndicator) {
console.log('[Indicators] New indicator detected:', indicator)
indicatorStore.addIndicator(indicator)
} else if (JSON.stringify(existingIndicator.parameters) !== JSON.stringify(indicator.parameters)) {
console.log('[Indicators] Indicator parameters changed:', indicator.id)
indicatorStore.updateIndicator(existingIndicator.id, {
parameters: indicator.parameters,
tv_inputs: indicator.tv_inputs
})
}
} catch (err) {
console.warn('[Indicators] Could not get study details for:', studyInfo.id, err)
}
}
// Check for deleted indicators
const allStoreIndicators = indicatorStore.getAllIndicators()
for (const storeIndicator of allStoreIndicators) {
if (storeIndicator.symbol === currentSymbol &&
storeIndicator.tv_study_id &&
!seenStudyIds.has(storeIndicator.tv_study_id)) {
console.log('[Indicators] Indicator deleted:', storeIndicator.id)
indicatorStore.removeIndicator(storeIndicator.id)
}
}
} finally {
isUpdatingStore = false
}
} catch (error) {
console.error('[Indicators] Error syncing indicators from TV:', error)
}
}
/**
* Setup watchers for IndicatorStore changes to apply to TradingView
*/