data fixes; indicator=>workspace sync

This commit is contained in:
2026-03-31 20:29:12 -04:00
parent 998f69fa1a
commit cd28e18e52
45 changed files with 1324 additions and 1239 deletions

View File

@@ -5,180 +5,146 @@ 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)
* 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 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',
const PANDAS_TA_TO_TV_NAMES: Record<string, string> = {
// 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 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',
// 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',
// Volume Indicators (3)
'AD': 'Chaikin A/D Line',
'ADOSC': 'Chaikin A/D Oscillator',
'OBV': 'On Balance Volume',
// Volatility (3)
'atr': 'Average True Range',
'kc': 'Keltner Channels',
'donchian': 'Donchian Channels',
// Volatility Indicators (3)
'ATR': 'Average True Range',
'NATR': 'Normalized Average True Range',
'TRANGE': 'True Range',
// 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',
// Price Transform (4)
'AVGPRICE': 'Average Price',
'MEDPRICE': 'Median Price',
'TYPPRICE': 'Typical Price',
'WCLPRICE': 'Weighted Close Price',
// Statistics / Price Transforms (6)
'stdev': 'Standard Deviation',
'linreg': 'Linear Regression Curve',
'slope': 'Linear Regression Slope',
'hl2': 'Median Price',
'hlc3': 'Typical Price',
'ohlc4': 'Average 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',
// Trend (3)
'psar': 'Parabolic SAR',
'vortex': 'Vortex Indicator',
'chop': 'Choppiness Index',
}
// Total: 60 TA-Lib indicators
// Total: 59 indicators
/**
* Custom indicators (implemented in backend, not in TA-Lib)
* Reverse mapping from TradingView display name to pandas-ta function name
*/
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])
const TV_TO_PANDAS_TA_NAMES: Record<string, string> = Object.fromEntries(
Object.entries(PANDAS_TA_TO_TV_NAMES).map(([k, v]) => [v, k])
)
/**
* Convert TA-Lib parameters to TradingView inputs
* Convert pandas-ta parameters to TradingView inputs
*/
function convertTALibParamsToTVInputs(talibName: string, talibParams: Record<string, any>): Record<string, any> {
function convertPandasTaParamsToTVInputs(pandasTaName: string, params: 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
if (pandasTaName === 'bbands') {
tvInputs.length = params.length || 20
tvInputs.mult = params.upper_std ?? params.std ?? 2
tvInputs.source = 'close'
} else if (talibName === 'MACD') {
tvInputs.fastLength = talibParams.fastperiod || 12
tvInputs.slowLength = talibParams.slowperiod || 26
tvInputs.signalLength = talibParams.signalperiod || 9
} else if (pandasTaName === 'macd') {
tvInputs.fastLength = params.fast || 12
tvInputs.slowLength = params.slow || 26
tvInputs.signalLength = params.signal || 9
tvInputs.source = 'close'
} else if (talibName === 'RSI') {
tvInputs.length = talibParams.timeperiod || 14
} 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 (['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 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 parameter conversion
for (const [talibParam, value] of Object.entries(talibParams)) {
const tvParam = paramMapping[talibParam] || talibParam
tvInputs[tvParam] = value
// 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
}
}
@@ -186,68 +152,72 @@ function convertTALibParamsToTVInputs(talibName: string, talibParams: Record<str
}
/**
* Convert TradingView inputs to TA-Lib parameters
* Convert TradingView inputs to pandas-ta 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: {} }
function convertTVInputsToPandasTaParams(tvName: string, tvInputs: Record<string, any>): { pandasTaName: string | null, pandasTaParams: Record<string, any> } {
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 talibParams: Record<string, any> = {}
const pandasTaParams: 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
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 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
// 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 { talibName, talibParams }
return { pandasTaName, pandasTaParams }
}
/**
@@ -319,12 +289,12 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
console.log('[Indicators] Study name extracted:', studyName)
const talibName = TV_TO_BACKEND_NAMES[studyName]
console.log('[Indicators] Backend mapping:', studyName, '->', talibName)
const pandasTaName = TV_TO_PANDAS_TA_NAMES[studyName]
console.log('[Indicators] pandas-ta mapping:', studyName, '->', pandasTaName)
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))
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
}
@@ -357,16 +327,16 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
console.log('[Indicators] TV inputs:', tvInputs)
const { talibParams } = convertTVInputsToTALibParams(studyName, tvInputs)
console.log('[Indicators] Converted TA-Lib params:', talibParams)
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()}`,
talib_name: talibName,
instance_name: `${talibName}_${Date.now()}`,
parameters: talibParams,
pandas_ta_name: pandasTaName,
instance_name: `${pandasTaName}_${Date.now()}`,
parameters: pandasTaParams,
tv_study_id: studyId || tvStudy.id,
tv_indicator_name: studyName,
tv_inputs: tvInputs,
@@ -591,17 +561,17 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
console.log('[Indicators] TV inputs:', tvInputs)
const { talibParams } = convertTVInputsToTALibParams(studyName, tvInputs)
const { pandasTaParams } = convertTVInputsToPandasTaParams(studyName, tvInputs)
console.log('[Indicators] Old params:', existingIndicator.parameters)
console.log('[Indicators] New params:', talibParams)
console.log('[Indicators] New params:', pandasTaParams)
// Update the store with new parameters
if (JSON.stringify(existingIndicator.parameters) !== JSON.stringify(talibParams)) {
if (JSON.stringify(existingIndicator.parameters) !== JSON.stringify(pandasTaParams)) {
console.log('[Indicators] Parameters changed, updating store...')
isUpdatingStore = true
indicatorStore.updateIndicator(existingIndicator.id, {
parameters: talibParams,
parameters: pandasTaParams,
tv_inputs: tvInputs
})
console.log('[Indicators] Indicator updated in store!')
@@ -816,14 +786,14 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
if (!chart) return
const currentSymbol = chartStore.symbol
const tvName = ALL_BACKEND_TO_TV_NAMES[indicator.talib_name]
const tvName = PANDAS_TA_TO_TV_NAMES[indicator.pandas_ta_name]
if (!tvName) {
console.warn('[Indicators] No TradingView mapping for backend indicator:', indicator.talib_name)
console.warn('[Indicators] No TradingView mapping for pandas-ta indicator:', indicator.pandas_ta_name)
return
}
const tvInputs = convertTALibParamsToTVInputs(indicator.talib_name, indicator.parameters)
const tvInputs = convertPandasTaParamsToTVInputs(indicator.pandas_ta_name, indicator.parameters)
console.log(`[Indicators] Creating TradingView study: ${tvName} with inputs:`, tvInputs)
@@ -866,7 +836,7 @@ export function useTradingViewIndicators(tvWidget: IChartingLibraryWidget) {
return
}
const tvInputs = convertTALibParamsToTVInputs(indicator.talib_name, indicator.parameters)
const tvInputs = convertPandasTaParamsToTVInputs(indicator.pandas_ta_name, indicator.parameters)
// Update study inputs
chart.getStudyById(indicator.tv_study_id).applyOverrides(tvInputs)