custom indicators fixed
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user