indicator integration
This commit is contained in:
@@ -8,6 +8,7 @@ import LoginScreen from './components/LoginScreen.vue'
|
||||
import { useOrderStore } from './stores/orders'
|
||||
import { useChartStore } from './stores/chart'
|
||||
import { useShapeStore } from './stores/shapes'
|
||||
import { useIndicatorStore } from './stores/indicators'
|
||||
import { useStateSync } from './composables/useStateSync'
|
||||
import { wsManager } from './composables/useWebSocket'
|
||||
|
||||
@@ -65,10 +66,12 @@ const handleAuthenticate = async (
|
||||
const orderStore = useOrderStore()
|
||||
const chartStore = useChartStore()
|
||||
const shapeStore = useShapeStore()
|
||||
const indicatorStore = useIndicatorStore()
|
||||
const stateSync = useStateSync({
|
||||
OrderStore: orderStore,
|
||||
ChartStore: chartStore,
|
||||
ShapeStore: shapeStore
|
||||
ShapeStore: shapeStore,
|
||||
IndicatorStore: indicatorStore
|
||||
})
|
||||
stateSyncCleanup = stateSync.cleanup
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import Card from 'primevue/card'
|
||||
import { createTradingViewDatafeed } from '../composables/useTradingViewDatafeed'
|
||||
import { useTradingViewShapes } from '../composables/useTradingViewShapes'
|
||||
import { useTradingViewIndicators } from '../composables/useTradingViewIndicators'
|
||||
import { useChartStore } from '../stores/chart'
|
||||
import type { IChartingLibraryWidget } from '../types/tradingview'
|
||||
|
||||
@@ -12,6 +13,7 @@ let tvWidget: IChartingLibraryWidget | null = null
|
||||
let datafeed: any = null
|
||||
let isUpdatingFromChart = false // Flag to prevent circular updates
|
||||
let shapeCleanup: (() => void) | null = null // Cleanup function for shape sync
|
||||
let indicatorCleanup: (() => void) | null = null // Cleanup function for indicator sync
|
||||
|
||||
onMounted(() => {
|
||||
if (!chartContainer.value) return
|
||||
@@ -39,6 +41,91 @@ onMounted(() => {
|
||||
'symbol_search_hot_key'
|
||||
],
|
||||
enabled_features: ['study_templates'],
|
||||
// Restrict indicators to only those supported by both TA-Lib and TradingView
|
||||
studies_access: {
|
||||
type: 'white',
|
||||
tools: [
|
||||
// Overlap Studies (14)
|
||||
{ name: 'Moving Average' },
|
||||
{ name: 'Moving Average Exponential' },
|
||||
{ name: 'Weighted Moving Average' },
|
||||
{ name: 'DEMA' },
|
||||
{ name: 'TEMA' },
|
||||
{ name: 'Triangular Moving Average' },
|
||||
{ name: 'KAMA' },
|
||||
{ name: 'MESA Adaptive Moving Average' },
|
||||
{ name: 'T3' },
|
||||
{ name: 'Bollinger Bands' },
|
||||
{ name: 'Midpoint' },
|
||||
{ name: 'Midprice' },
|
||||
{ name: 'Parabolic SAR' },
|
||||
{ name: 'Hilbert Transform - Instantaneous Trendline' },
|
||||
// Momentum Indicators (21)
|
||||
{ name: 'Relative Strength Index' },
|
||||
{ name: 'Momentum' },
|
||||
{ name: 'Rate of Change' },
|
||||
{ name: 'TRIX' },
|
||||
{ name: 'Chande Momentum Oscillator' },
|
||||
{ name: 'Directional Movement Index' },
|
||||
{ name: 'Average Directional Movement Index' },
|
||||
{ name: 'Average Directional Movement Index Rating' },
|
||||
{ name: 'Absolute Price Oscillator' },
|
||||
{ name: 'Percentage Price Oscillator' },
|
||||
{ name: 'MACD' },
|
||||
{ name: 'Money Flow Index' },
|
||||
{ name: 'Stochastic' },
|
||||
{ name: 'Stochastic Fast' },
|
||||
{ name: 'Stochastic RSI' },
|
||||
{ name: 'Williams %R' },
|
||||
{ name: 'Commodity Channel Index' },
|
||||
{ name: 'Aroon' },
|
||||
{ name: 'Aroon Oscillator' },
|
||||
{ name: 'Balance Of Power' },
|
||||
{ name: 'Ultimate Oscillator' },
|
||||
// Volume Indicators (3)
|
||||
{ name: 'Chaikin A/D Line' },
|
||||
{ name: 'Chaikin A/D Oscillator' },
|
||||
{ name: 'On Balance Volume' },
|
||||
// Volatility Indicators (3)
|
||||
{ name: 'Average True Range' },
|
||||
{ name: 'Normalized Average True Range' },
|
||||
{ name: 'True Range' },
|
||||
// Price Transform (4)
|
||||
{ name: 'Average Price' },
|
||||
{ name: 'Median Price' },
|
||||
{ name: 'Typical Price' },
|
||||
{ name: 'Weighted Close Price' },
|
||||
// Cycle Indicators (5)
|
||||
{ name: 'Hilbert Transform - Dominant Cycle Period' },
|
||||
{ name: 'Hilbert Transform - Dominant Cycle Phase' },
|
||||
{ name: 'Hilbert Transform - Phasor Components' },
|
||||
{ name: 'Hilbert Transform - SineWave' },
|
||||
{ name: 'Hilbert Transform - Trend vs Cycle Mode' },
|
||||
// Statistic Functions (9)
|
||||
{ name: 'Beta' },
|
||||
{ name: 'Pearson\'s Correlation Coefficient' },
|
||||
{ name: 'Linear Regression' },
|
||||
{ name: 'Linear Regression Angle' },
|
||||
{ name: 'Linear Regression Intercept' },
|
||||
{ name: 'Linear Regression Slope' },
|
||||
{ name: 'Standard Deviation' },
|
||||
{ name: 'Time Series Forecast' },
|
||||
{ name: 'Variance' },
|
||||
// Custom Indicators (12)
|
||||
{ name: 'VWAP' },
|
||||
{ name: 'VWMA' },
|
||||
{ name: 'Hull Moving Average' },
|
||||
{ name: 'SuperTrend' },
|
||||
{ name: 'Donchian Channels' },
|
||||
{ name: 'Keltner Channels' },
|
||||
{ name: 'Chaikin Money Flow' },
|
||||
{ name: 'Vortex Indicator' },
|
||||
{ name: 'Awesome Oscillator' },
|
||||
{ name: 'Accelerator Oscillator' },
|
||||
{ name: 'Choppiness Index' },
|
||||
{ name: 'Mass Index' }
|
||||
]
|
||||
},
|
||||
fullscreen: false,
|
||||
autosize: true,
|
||||
theme: 'Dark',
|
||||
@@ -54,6 +141,7 @@ onMounted(() => {
|
||||
// Setup shape synchronization
|
||||
if (tvWidget) {
|
||||
shapeCleanup = useTradingViewShapes(tvWidget)
|
||||
indicatorCleanup = useTradingViewIndicators(tvWidget)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -177,6 +265,12 @@ onBeforeUnmount(() => {
|
||||
shapeCleanup = null
|
||||
}
|
||||
|
||||
// Cleanup indicator synchronization
|
||||
if (indicatorCleanup) {
|
||||
indicatorCleanup()
|
||||
indicatorCleanup = null
|
||||
}
|
||||
|
||||
if (tvWidget) {
|
||||
tvWidget.remove()
|
||||
tvWidget = null
|
||||
|
||||
@@ -21,6 +21,7 @@ const newPassword = ref('')
|
||||
const confirmNewPassword = ref('')
|
||||
const isLoading = ref(false)
|
||||
const isChangingPassword = ref(false)
|
||||
const passwordInput = ref<InstanceType<typeof Password> | null>(null)
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
if (!password.value || isLoading.value) return false
|
||||
@@ -69,9 +70,11 @@ const togglePasswordChange = () => {
|
||||
|
||||
onMounted(() => {
|
||||
// Focus on the password input when component mounts
|
||||
const passwordInput = document.querySelector('#password input') as HTMLInputElement
|
||||
if (passwordInput) {
|
||||
passwordInput.focus()
|
||||
if (passwordInput.value?.$el) {
|
||||
const inputElement = passwordInput.value.$el.querySelector('input') as HTMLInputElement
|
||||
if (inputElement) {
|
||||
inputElement.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -105,6 +108,7 @@ onMounted(() => {
|
||||
<div class="form-field">
|
||||
<label for="password">{{ isChangingPassword ? 'Current Password' : 'Password' }}</label>
|
||||
<Password
|
||||
ref="passwordInput"
|
||||
id="password"
|
||||
v-model="password"
|
||||
:feedback="needsConfirmation"
|
||||
|
||||
@@ -100,6 +100,12 @@ export function useStateSync(stores: Record<string, Store>) {
|
||||
patch: patch
|
||||
};
|
||||
sendJson(msg);
|
||||
|
||||
// Optimistically increment sequence number after sending
|
||||
// This ensures the next patch we send has the correct base sequence
|
||||
currentSeqs[storeName] = seq + 1;
|
||||
saveStoredSeqs(currentSeqs);
|
||||
console.log(`[StateSync] Incremented seq for "${storeName}" to ${seq + 1} after sending patch`);
|
||||
};
|
||||
|
||||
// Set up watchers for each store to send patches on changes
|
||||
|
||||
905
web/src/composables/useTradingViewIndicators.ts
Normal file
905
web/src/composables/useTradingViewIndicators.ts
Normal file
@@ -0,0 +1,905 @@
|
||||
import { watch } from 'vue'
|
||||
import type { IChartingLibraryWidget, IStudyApi } from '../types/tradingview'
|
||||
import { useIndicatorStore } from '../stores/indicators'
|
||||
import { useChartStore } from '../stores/chart'
|
||||
import type { IndicatorInstance } from '../stores/indicators'
|
||||
|
||||
/**
|
||||
* Mapping between TA-Lib indicator names and TradingView indicator names
|
||||
* Only includes indicators that are present in BOTH systems (inner join)
|
||||
*/
|
||||
const TALIB_TO_TV_NAMES: Record<string, string> = {
|
||||
// Overlap Studies (14)
|
||||
'SMA': 'Moving Average',
|
||||
'EMA': 'Moving Average Exponential',
|
||||
'WMA': 'Weighted Moving Average',
|
||||
'DEMA': 'DEMA',
|
||||
'TEMA': 'TEMA',
|
||||
'TRIMA': 'Triangular Moving Average',
|
||||
'KAMA': 'KAMA',
|
||||
'MAMA': 'MESA Adaptive Moving Average',
|
||||
'T3': 'T3',
|
||||
'BBANDS': 'Bollinger Bands',
|
||||
'MIDPOINT': 'Midpoint',
|
||||
'MIDPRICE': 'Midprice',
|
||||
'SAR': 'Parabolic SAR',
|
||||
'HT_TRENDLINE': 'Hilbert Transform - Instantaneous Trendline',
|
||||
|
||||
// Momentum Indicators (21)
|
||||
'RSI': 'Relative Strength Index',
|
||||
'MOM': 'Momentum',
|
||||
'ROC': 'Rate of Change',
|
||||
'TRIX': 'TRIX',
|
||||
'CMO': 'Chande Momentum Oscillator',
|
||||
'DX': 'Directional Movement Index',
|
||||
'ADX': 'Average Directional Movement Index',
|
||||
'ADXR': 'Average Directional Movement Index Rating',
|
||||
'APO': 'Absolute Price Oscillator',
|
||||
'PPO': 'Percentage Price Oscillator',
|
||||
'MACD': 'MACD',
|
||||
'MFI': 'Money Flow Index',
|
||||
'STOCH': 'Stochastic',
|
||||
'STOCHF': 'Stochastic Fast',
|
||||
'STOCHRSI': 'Stochastic RSI',
|
||||
'WILLR': 'Williams %R',
|
||||
'CCI': 'Commodity Channel Index',
|
||||
'AROON': 'Aroon',
|
||||
'AROONOSC': 'Aroon Oscillator',
|
||||
'BOP': 'Balance Of Power',
|
||||
'ULTOSC': 'Ultimate Oscillator',
|
||||
|
||||
// Volume Indicators (3)
|
||||
'AD': 'Chaikin A/D Line',
|
||||
'ADOSC': 'Chaikin A/D Oscillator',
|
||||
'OBV': 'On Balance Volume',
|
||||
|
||||
// Volatility Indicators (3)
|
||||
'ATR': 'Average True Range',
|
||||
'NATR': 'Normalized Average True Range',
|
||||
'TRANGE': 'True Range',
|
||||
|
||||
// Price Transform (4)
|
||||
'AVGPRICE': 'Average Price',
|
||||
'MEDPRICE': 'Median Price',
|
||||
'TYPPRICE': 'Typical Price',
|
||||
'WCLPRICE': 'Weighted Close Price',
|
||||
|
||||
// Cycle Indicators (5)
|
||||
'HT_DCPERIOD': 'Hilbert Transform - Dominant Cycle Period',
|
||||
'HT_DCPHASE': 'Hilbert Transform - Dominant Cycle Phase',
|
||||
'HT_PHASOR': 'Hilbert Transform - Phasor Components',
|
||||
'HT_SINE': 'Hilbert Transform - SineWave',
|
||||
'HT_TRENDMODE': 'Hilbert Transform - Trend vs Cycle Mode',
|
||||
|
||||
// Statistic Functions (9)
|
||||
'BETA': 'Beta',
|
||||
'CORREL': 'Pearson\'s Correlation Coefficient',
|
||||
'LINEARREG': 'Linear Regression',
|
||||
'LINEARREG_ANGLE': 'Linear Regression Angle',
|
||||
'LINEARREG_INTERCEPT': 'Linear Regression Intercept',
|
||||
'LINEARREG_SLOPE': 'Linear Regression Slope',
|
||||
'STDDEV': 'Standard Deviation',
|
||||
'TSF': 'Time Series Forecast',
|
||||
'VAR': 'Variance',
|
||||
}
|
||||
|
||||
// Total: 60 TA-Lib indicators
|
||||
|
||||
/**
|
||||
* Custom indicators (implemented in backend, not in TA-Lib)
|
||||
*/
|
||||
const CUSTOM_TO_TV_NAMES: Record<string, string> = {
|
||||
'VWAP': 'VWAP',
|
||||
'VWMA': 'VWMA',
|
||||
'HMA': 'Hull Moving Average',
|
||||
'SUPERTREND': 'SuperTrend',
|
||||
'DONCHIAN': 'Donchian Channels',
|
||||
'KELTNER': 'Keltner Channels',
|
||||
'CMF': 'Chaikin Money Flow',
|
||||
'VORTEX': 'Vortex Indicator',
|
||||
'AO': 'Awesome Oscillator',
|
||||
'AC': 'Accelerator Oscillator',
|
||||
'CHOP': 'Choppiness Index',
|
||||
'MASS': 'Mass Index',
|
||||
}
|
||||
|
||||
// Combined mapping (TA-Lib + Custom)
|
||||
const ALL_BACKEND_TO_TV_NAMES: Record<string, string> = {
|
||||
...TALIB_TO_TV_NAMES,
|
||||
...CUSTOM_TO_TV_NAMES
|
||||
}
|
||||
|
||||
// Total: 72 indicators (60 TA-Lib + 12 Custom)
|
||||
|
||||
/**
|
||||
* Reverse mapping from TradingView to backend
|
||||
*/
|
||||
const TV_TO_TALIB_NAMES: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(TALIB_TO_TV_NAMES).map(([k, v]) => [v, k])
|
||||
)
|
||||
|
||||
const TV_TO_CUSTOM_NAMES: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(CUSTOM_TO_TV_NAMES).map(([k, v]) => [v, k])
|
||||
)
|
||||
|
||||
const TV_TO_BACKEND_NAMES: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(ALL_BACKEND_TO_TV_NAMES).map(([k, v]) => [v, k])
|
||||
)
|
||||
|
||||
/**
|
||||
* Convert TA-Lib parameters to TradingView inputs
|
||||
*/
|
||||
function convertTALibParamsToTVInputs(talibName: string, talibParams: Record<string, any>): Record<string, any> {
|
||||
const tvInputs: Record<string, any> = {}
|
||||
|
||||
// Common parameter mappings
|
||||
const paramMapping: Record<string, string> = {
|
||||
'timeperiod': 'length',
|
||||
'fastperiod': 'fastLength',
|
||||
'slowperiod': 'slowLength',
|
||||
'signalperiod': 'signalLength',
|
||||
'nbdevup': 'mult',
|
||||
'nbdevdn': 'mult',
|
||||
'fastlimit': 'fastLimit',
|
||||
'slowlimit': 'slowLimit',
|
||||
'acceleration': 'start',
|
||||
'maximum': 'increment',
|
||||
'fastk_period': 'kPeriod',
|
||||
'slowk_period': 'kPeriod',
|
||||
'slowd_period': 'dPeriod',
|
||||
'fastd_period': 'dPeriod',
|
||||
}
|
||||
|
||||
// Special handling for specific indicators
|
||||
if (talibName === 'BBANDS') {
|
||||
tvInputs.length = talibParams.timeperiod || 20
|
||||
tvInputs.mult = talibParams.nbdevup || 2
|
||||
tvInputs.source = 'close'
|
||||
} else if (talibName === 'MACD') {
|
||||
tvInputs.fastLength = talibParams.fastperiod || 12
|
||||
tvInputs.slowLength = talibParams.slowperiod || 26
|
||||
tvInputs.signalLength = talibParams.signalperiod || 9
|
||||
tvInputs.source = 'close'
|
||||
} else if (talibName === 'RSI') {
|
||||
tvInputs.length = talibParams.timeperiod || 14
|
||||
tvInputs.source = 'close'
|
||||
} else if (['SMA', 'EMA', 'WMA', 'DEMA', 'TEMA', 'TRIMA'].includes(talibName)) {
|
||||
tvInputs.length = talibParams.timeperiod || 14
|
||||
tvInputs.source = 'close'
|
||||
} else if (talibName === 'STOCH') {
|
||||
tvInputs.kPeriod = talibParams.fastk_period || 14
|
||||
tvInputs.dPeriod = talibParams.slowd_period || 3
|
||||
tvInputs.smoothK = talibParams.slowk_period || 3
|
||||
} else if (talibName === 'ATR') {
|
||||
tvInputs.length = talibParams.timeperiod || 14
|
||||
} else if (talibName === 'CCI') {
|
||||
tvInputs.length = talibParams.timeperiod || 20
|
||||
} else {
|
||||
// Generic parameter conversion
|
||||
for (const [talibParam, value] of Object.entries(talibParams)) {
|
||||
const tvParam = paramMapping[talibParam] || talibParam
|
||||
tvInputs[tvParam] = value
|
||||
}
|
||||
}
|
||||
|
||||
return tvInputs
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert TradingView inputs to TA-Lib parameters
|
||||
*/
|
||||
function convertTVInputsToTALibParams(tvName: string, tvInputs: Record<string, any>): { talibName: string | null, talibParams: Record<string, any> } {
|
||||
const talibName = TV_TO_BACKEND_NAMES[tvName] || null
|
||||
if (!talibName) {
|
||||
console.warn('[Indicators] No backend mapping for TradingView indicator:', tvName)
|
||||
return { talibName: null, talibParams: {} }
|
||||
}
|
||||
|
||||
const talibParams: Record<string, any> = {}
|
||||
|
||||
// Reverse parameter mappings
|
||||
const reverseMapping: Record<string, string> = {
|
||||
'length': 'timeperiod',
|
||||
'fastLength': 'fastperiod',
|
||||
'slowLength': 'slowperiod',
|
||||
'signalLength': 'signalperiod',
|
||||
'mult': 'nbdevup',
|
||||
'fastLimit': 'fastlimit',
|
||||
'slowLimit': 'slowlimit',
|
||||
'start': 'acceleration',
|
||||
'increment': 'maximum',
|
||||
'kPeriod': 'fastk_period',
|
||||
'dPeriod': 'slowd_period',
|
||||
'smoothK': 'slowk_period',
|
||||
}
|
||||
|
||||
// Special handling for specific indicators
|
||||
if (talibName === 'BBANDS') {
|
||||
// TradingView uses in_0 for length, in_1 for multiplier
|
||||
talibParams.timeperiod = tvInputs.in_0 || tvInputs.length || 20
|
||||
talibParams.nbdevup = tvInputs.in_1 || tvInputs.mult || 2
|
||||
talibParams.nbdevdn = tvInputs.in_1 || tvInputs.mult || 2
|
||||
talibParams.matype = 0 // SMA
|
||||
} else if (talibName === 'MACD') {
|
||||
talibParams.fastperiod = tvInputs.fastLength || 12
|
||||
talibParams.slowperiod = tvInputs.slowLength || 26
|
||||
talibParams.signalperiod = tvInputs.signalLength || 9
|
||||
} else if (talibName === 'RSI') {
|
||||
talibParams.timeperiod = tvInputs.in_0 || tvInputs.length || 14
|
||||
} else if (['SMA', 'EMA', 'WMA', 'DEMA', 'TEMA', 'TRIMA'].includes(talibName)) {
|
||||
talibParams.timeperiod = tvInputs.in_0 || tvInputs.length || 14
|
||||
} else if (talibName === 'STOCH') {
|
||||
talibParams.fastk_period = tvInputs.kPeriod || 14
|
||||
talibParams.slowd_period = tvInputs.dPeriod || 3
|
||||
talibParams.slowk_period = tvInputs.smoothK || 3
|
||||
talibParams.slowk_matype = 0 // SMA
|
||||
talibParams.slowd_matype = 0 // SMA
|
||||
} else if (talibName === 'ATR') {
|
||||
talibParams.timeperiod = tvInputs.length || 14
|
||||
} else if (talibName === 'CCI') {
|
||||
talibParams.timeperiod = tvInputs.length || 20
|
||||
} else {
|
||||
// Generic parameter conversion
|
||||
for (const [tvParam, value] of Object.entries(tvInputs)) {
|
||||
if (tvParam === 'source') continue // Skip source parameter
|
||||
const talibParam = reverseMapping[tvParam] || tvParam
|
||||
talibParams[talibParam] = value
|
||||
}
|
||||
}
|
||||
|
||||
return { talibName, talibParams }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback to property access
|
||||
if (studyName === 'Unknown') {
|
||||
studyName = tvStudy._study?.name?.() ||
|
||||
tvStudy._metaInfo?.description ||
|
||||
tvStudy._metaInfo?.shortDescription || 'Unknown'
|
||||
}
|
||||
|
||||
console.log('[Indicators] Study name extracted:', studyName)
|
||||
|
||||
const talibName = TV_TO_BACKEND_NAMES[studyName]
|
||||
console.log('[Indicators] Backend mapping:', studyName, '->', talibName)
|
||||
|
||||
if (!talibName) {
|
||||
console.log('[Indicators] TradingView study not mapped to backend:', studyName)
|
||||
console.log('[Indicators] Available mappings:', Object.keys(TV_TO_BACKEND_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 { talibParams } = convertTVInputsToTALibParams(studyName, tvInputs)
|
||||
console.log('[Indicators] Converted TA-Lib params:', talibParams)
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
|
||||
const indicator = {
|
||||
id: studyId || tvStudy.id || `ind_${Date.now()}`,
|
||||
talib_name: talibName,
|
||||
instance_name: `${talibName}_${Date.now()}`,
|
||||
parameters: talibParams,
|
||||
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
|
||||
|
||||
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.chart_state.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 {
|
||||
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)
|
||||
|
||||
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.chart_state.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)
|
||||
|
||||
// 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 { talibParams } = convertTVInputsToTALibParams(studyName, tvInputs)
|
||||
|
||||
console.log('[Indicators] Old params:', existingIndicator.parameters)
|
||||
console.log('[Indicators] New params:', talibParams)
|
||||
|
||||
// Update the store with new parameters
|
||||
if (JSON.stringify(existingIndicator.parameters) !== JSON.stringify(talibParams)) {
|
||||
console.log('[Indicators] Parameters changed, updating store...')
|
||||
isUpdatingStore = true
|
||||
indicatorStore.updateIndicator(existingIndicator.id, {
|
||||
parameters: talibParams,
|
||||
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, doing full sync...')
|
||||
// Might be a new indicator, do full sync
|
||||
if (!isUpdatingStore && !isApplyingTVUpdate) {
|
||||
syncIndicatorsFromTV()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Indicators] Error handling study_properties_changed:', error)
|
||||
}
|
||||
}, 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 COMPLETE =====')
|
||||
})
|
||||
|
||||
// Initial sync on data load
|
||||
const dataLoadedSubscription = chart.onDataLoaded().subscribe(null, () => {
|
||||
console.log('[Indicators] Chart data loaded, performing initial sync')
|
||||
syncIndicatorsFromTV()
|
||||
})
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
console.log('[Indicators] Cleaning up event subscriptions')
|
||||
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)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Indicators] Error setting up indicator listeners:', error)
|
||||
return () => {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan TradingView for indicators and sync to store
|
||||
*/
|
||||
function syncIndicatorsFromTV() {
|
||||
if (!isChartReady) return
|
||||
|
||||
try {
|
||||
const chart = tvWidget.activeChart()
|
||||
if (!chart) return
|
||||
|
||||
const currentSymbol = chartStore.chart_state.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
|
||||
*/
|
||||
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.chart_state.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
|
||||
|
||||
try {
|
||||
const chart = tvWidget.activeChart()
|
||||
if (!chart) return
|
||||
|
||||
const currentSymbol = chartStore.chart_state.symbol
|
||||
const tvName = ALL_BACKEND_TO_TV_NAMES[indicator.talib_name]
|
||||
|
||||
if (!tvName) {
|
||||
console.warn('[Indicators] No TradingView mapping for backend indicator:', indicator.talib_name)
|
||||
return
|
||||
}
|
||||
|
||||
const tvInputs = convertTALibParamsToTVInputs(indicator.talib_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 = convertTALibParamsToTVInputs(indicator.talib_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()
|
||||
}
|
||||
}
|
||||
64
web/src/stores/indicators.ts
Normal file
64
web/src/stores/indicators.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface IndicatorInstance {
|
||||
id: string
|
||||
talib_name: string
|
||||
instance_name: string
|
||||
parameters: Record<string, any>
|
||||
tv_study_id?: string
|
||||
tv_indicator_name?: string
|
||||
tv_inputs?: Record<string, any>
|
||||
visible: boolean
|
||||
pane: string
|
||||
symbol?: string
|
||||
created_at?: number
|
||||
modified_at?: number
|
||||
original_id?: string
|
||||
}
|
||||
|
||||
export const useIndicatorStore = defineStore('IndicatorStore', () => {
|
||||
const indicators = ref<Record<string, IndicatorInstance>>({})
|
||||
|
||||
// Helper methods
|
||||
const addIndicator = (indicator: IndicatorInstance) => {
|
||||
indicators.value[indicator.id] = indicator
|
||||
}
|
||||
|
||||
const updateIndicator = (id: string, updates: Partial<IndicatorInstance>) => {
|
||||
if (indicators.value[id]) {
|
||||
const updated = {
|
||||
...indicators.value[id],
|
||||
...updates,
|
||||
modified_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
indicators.value[id] = updated
|
||||
}
|
||||
}
|
||||
|
||||
const removeIndicator = (id: string) => {
|
||||
delete indicators.value[id]
|
||||
}
|
||||
|
||||
const getIndicator = (id: string): IndicatorInstance | undefined => {
|
||||
return indicators.value[id]
|
||||
}
|
||||
|
||||
const getAllIndicators = (): IndicatorInstance[] => {
|
||||
return Object.values(indicators.value)
|
||||
}
|
||||
|
||||
const getIndicatorsBySymbol = (symbol: string): IndicatorInstance[] => {
|
||||
return Object.values(indicators.value).filter(ind => ind.symbol === symbol)
|
||||
}
|
||||
|
||||
return {
|
||||
indicators,
|
||||
addIndicator,
|
||||
updateIndicator,
|
||||
removeIndicator,
|
||||
getIndicator,
|
||||
getAllIndicators,
|
||||
getIndicatorsBySymbol
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user