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

@@ -1,21 +1,21 @@
/* web/src/assets/theme.css */
:root {
--p-primary-color: #00d4aa; /* teal accent */
--p-primary-contrast-color: #0a0e1a;
--p-surface-0: #0a0e1a; /* deepest background */
--p-surface-50: #0f1629;
--p-surface-100: #161e35;
--p-surface-200: #1e2a45;
--p-surface-300: #263452;
--p-surface-400: #34446a;
--p-surface-700: #8892a4;
--p-surface-800: #aab4c5;
--p-surface-900: #cdd6e8;
--p-primary-color: #26A69A; /* TV green */
--p-primary-contrast-color: #131722;
--p-surface-0: #131722; /* TV bg */
--p-surface-50: #1a1e2b;
--p-surface-100: #1e222d;
--p-surface-200: #2A2E39; /* TV grid */
--p-surface-300: #363b4a;
--p-surface-400: #434857;
--p-surface-700: #787B86; /* TV subtext */
--p-surface-800: #B2B5BE;
--p-surface-900: #D3D4DC; /* TV text */
/* Semantic trading colors */
--color-bull: #26a69a;
--color-bear: #ef5350;
--color-neutral: #8892a4;
--color-bull: #26A69A;
--color-bear: #EF5350;
--color-neutral: #787B86;
}
html, body, #app {

View File

@@ -6,6 +6,14 @@ import { useTradingViewShapes } from '../composables/useTradingViewShapes'
import { useTradingViewIndicators } from '../composables/useTradingViewIndicators'
import { useChartStore } from '../stores/chart'
import type { IChartingLibraryWidget } from '../types/tradingview'
import { intervalToSeconds } from '../utils'
// Convert seconds to TradingView interval string
function secondsToInterval(seconds: number): string {
if (seconds % 86400 === 0) return `${seconds / 86400}D`
if (seconds % 3600 === 0) return `${seconds / 3600}H`
return `${seconds / 60}` // plain number = minutes
}
const chartContainer = ref<HTMLDivElement | null>(null)
const chartStore = useChartStore()
@@ -31,7 +39,7 @@ onMounted(() => {
tvWidget = new window.TradingView.widget({
symbol: chartStore.symbol, // Use symbol from store
datafeed: datafeed,
interval: chartStore.period as any,
interval: secondsToInterval(chartStore.period) as any,
container: chartContainer.value!,
library_path: '/charting_library/',
locale: 'en',
@@ -190,9 +198,10 @@ function setupChartListeners() {
// Listen for period changes
chart.onIntervalChanged().subscribe(null, (interval: string) => {
console.log('[ChartView] Period changed to:', interval)
const seconds = intervalToSeconds(interval)
console.log('[ChartView] Period changed to:', interval, `(${seconds}s)`)
isUpdatingFromChart = true
chartStore.period = interval
chartStore.period = seconds
isUpdatingFromChart = false
})
@@ -244,10 +253,11 @@ function setupStoreWatchers() {
(newPeriod) => {
if (isUpdatingFromChart) return
console.log('[ChartView] Store period changed externally to:', newPeriod)
if (chart.resolution() !== newPeriod) {
chart.setResolution(newPeriod, () => {
console.log('[ChartView] Chart period updated to:', newPeriod)
const tvInterval = secondsToInterval(newPeriod)
console.log('[ChartView] Store period changed externally to:', newPeriod, `-> ${tvInterval}`)
if (chart.resolution() !== tvInterval) {
chart.setResolution(tvInterval, () => {
console.log('[ChartView] Chart period updated to:', tvInterval)
})
}
}

View File

@@ -36,10 +36,35 @@ const rooms = computed(() => [{
// Streaming state
let currentStreamingMessageId: string | null = null
let toolCallMessageId: string | null = null
let lastSentMessageId: string | null = null
let streamingBuffer = ''
const isAgentProcessing = ref(false)
const toolCallStatus = ref<string | null>(null)
const addToolCallBubble = (label: string) => {
removeToolCallBubble()
toolCallMessageId = `tool-call-${Date.now()}`
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
messages.value = [...messages.value, {
_id: toolCallMessageId,
content: `${label}`,
senderId: AGENT_ID,
timestamp,
date: new Date().toLocaleDateString(),
saved: false,
distributed: false,
seen: false,
files: [],
toolCall: true
}]
}
const removeToolCallBubble = () => {
if (toolCallMessageId) {
messages.value = messages.value.filter(m => m._id !== toolCallMessageId)
toolCallMessageId = null
}
}
// Generate message ID
const generateMessageId = () => `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
@@ -52,7 +77,7 @@ const handleMessage = (data: WebSocketMessage) => {
console.log('[ChatPanel] Received message:', data)
if (data.type === 'agent_tool_call') {
toolCallStatus.value = data.label ?? data.toolName ?? null
addToolCallBubble(data.label ?? data.toolName ?? 'Tool call...')
return
}
@@ -99,12 +124,13 @@ const handleMessage = (data: WebSocketMessage) => {
if (!currentStreamingMessageId) {
console.log('[ChatPanel] Starting new streaming message')
// Remove any ephemeral tool-call bubble before starting the real response
removeToolCallBubble()
// Set up streaming state and mark user message as seen
isAgentProcessing.value = true
currentStreamingMessageId = generateMessageId()
streamingBuffer = data.content
streamingImages.value = []
toolCallStatus.value = null
// Mark the last sent user message as seen (double-checkmark)
if (lastSentMessageId) {
@@ -205,7 +231,7 @@ const handleMessage = (data: WebSocketMessage) => {
streamingBuffer = ''
streamingImages.value = []
isAgentProcessing.value = false
toolCallStatus.value = null
removeToolCallBubble()
}
}
}
@@ -221,7 +247,7 @@ const stopAgent = () => {
}
wsManager.send(wsMessage)
isAgentProcessing.value = false
toolCallStatus.value = null
removeToolCallBubble()
lastSentMessageId = null
}
@@ -336,34 +362,137 @@ const chatTheme = 'dark'
// Styles to match TradingView dark theme
const chatStyles = computed(() => JSON.stringify({
general: {
color: '#d1d4dc',
colorSpinner: '#2962ff',
borderStyle: '1px solid #2a2e39'
color: '#D3D4DC',
colorButtonClear: '#D3D4DC',
colorButton: '#131722',
backgroundColorButton: '#26A69A',
backgroundInput: '#131722',
colorPlaceholder: '#787B86',
colorCaret: '#D3D4DC',
colorSpinner: '#26A69A',
borderStyle: '1px solid #2A2E39',
backgroundScrollIcon: '#2A2E39'
},
container: {
background: '#131722'
border: 'none',
borderRadius: '0',
boxShadow: 'none'
},
header: {
background: '#1e222d',
colorRoomName: '#d1d4dc',
colorRoomInfo: '#787b86'
background: '#2A2E39',
colorRoomName: '#D3D4DC',
colorRoomInfo: '#787B86',
position: 'absolute',
width: '100%'
},
footer: {
background: '#1e222d',
borderStyleInput: '1px solid #2a2e39',
backgroundInput: '#1e222d',
colorInput: '#d1d4dc',
colorPlaceholder: '#787b86',
colorIcons: '#787b86'
background: '#2A2E39',
borderStyleInput: '1px solid #2A2E39',
borderInputSelected: '#26A69A',
backgroundReply: '#2A2E39',
backgroundTagActive: '#2A2E39',
backgroundTag: '#1E222D'
},
content: {
background: '#131722'
},
sidemenu: {
background: '#131722',
backgroundHover: '#1E222D',
backgroundActive: '#2A2E39',
colorActive: '#D3D4DC',
borderColorSearch: '#2A2E39'
},
dropdown: {
background: '#2A2E39',
backgroundHover: '#363B4A'
},
message: {
background: '#1e222d',
backgroundMe: '#2962ff',
color: '#d1d4dc',
colorMe: '#ffffff'
background: '#1E222D',
backgroundMe: '#26A69A',
color: '#D3D4DC',
colorStarted: '#787B86',
backgroundDeleted: '#131722',
backgroundSelected: '#2A2E39',
colorDeleted: '#787B86',
colorUsername: '#787B86',
colorTimestamp: '#787B86',
backgroundDate: 'rgba(0, 0, 0, 0.3)',
colorDate: '#787B86',
backgroundSystem: 'rgba(0, 0, 0, 0.3)',
colorSystem: '#787B86',
backgroundMedia: 'rgba(0, 0, 0, 0.18)',
backgroundReply: 'rgba(0, 0, 0, 0.18)',
colorReplyUsername: '#D3D4DC',
colorReply: '#B2B5BE',
colorTag: '#26A69A',
backgroundImage: '#2A2E39',
colorNewMessages: '#26A69A',
backgroundScrollCounter: '#26A69A',
colorScrollCounter: '#131722',
backgroundReaction: 'none',
borderStyleReaction: 'none',
backgroundReactionHover: '#2A2E39',
borderStyleReactionHover: 'none',
colorReactionCounter: '#D3D4DC',
backgroundReactionMe: '#26A69A',
borderStyleReactionMe: 'none',
backgroundReactionHoverMe: '#26A69A',
borderStyleReactionHoverMe: 'none',
colorReactionCounterMe: '#131722',
backgroundAudioRecord: '#EF5350',
backgroundAudioLine: 'rgba(255, 255, 255, 0.15)',
backgroundAudioProgress: '#26A69A',
backgroundAudioProgressSelector: '#26A69A',
colorFileExtension: '#787B86'
},
markdown: {
background: 'rgba(42, 46, 57, 0.8)',
border: 'rgba(55, 60, 74, 0.9)',
color: '#26A69A',
colorMulti: '#D3D4DC'
},
room: {
colorUsername: '#D3D4DC',
colorMessage: '#787B86',
colorTimestamp: '#787B86',
colorStateOnline: '#26A69A',
colorStateOffline: '#787B86',
backgroundCounterBadge: '#26A69A',
colorCounterBadge: '#131722'
},
emoji: {
background: '#2A2E39'
},
icons: {
search: '#787B86',
add: '#D3D4DC',
toggle: '#D3D4DC',
menu: '#D3D4DC',
close: '#787B86',
closeImage: '#D3D4DC',
file: '#26A69A',
paperclip: '#787B86',
closeOutline: '#D3D4DC',
closePreview: '#D3D4DC',
send: '#26A69A',
sendDisabled: '#787B86',
emoji: '#787B86',
emojiReaction: '#787B86',
document: '#26A69A',
pencil: '#787B86',
checkmark: '#787B86',
checkmarkSeen: '#26A69A',
eye: '#D3D4DC',
dropdownMessage: '#D3D4DC',
dropdownMessageBackground: 'rgba(0, 0, 0, 0.3)',
dropdownRoom: '#D3D4DC',
dropdownScroll: '#2A2E39',
microphone: '#787B86',
audioPlay: '#26A69A',
audioPause: '#26A69A',
audioCancel: '#EF5350',
audioConfirm: '#26A69A'
}
}))
@@ -429,7 +558,6 @@ onUnmounted(() => {
<!-- Stop button overlay -->
<div v-if="isAgentProcessing" class="stop-button-container">
<div v-if="toolCallStatus" class="tool-call-status">{{ toolCallStatus }}</div>
<Button
icon="pi pi-stop-circle"
label="Stop"
@@ -459,12 +587,12 @@ onUnmounted(() => {
justify-content: center;
gap: 1rem;
background: #131722;
color: #787b86;
color: #787B86;
}
.workspace-loading-spinner {
font-size: 2rem;
color: #787b86;
color: #787B86;
}
.workspace-loading-message {
@@ -485,6 +613,7 @@ onUnmounted(() => {
max-width: 80% !important;
}
.chat-header-custom {
display: flex;
justify-content: space-between;
@@ -504,21 +633,10 @@ onUnmounted(() => {
.stop-button-container {
position: absolute;
bottom: 80px;
right: 20px;
left: 20px;
z-index: 1000;
}
.tool-call-status {
background: rgba(30, 34, 45, 0.92);
color: #787b86;
font-size: 0.75rem;
padding: 4px 10px;
border-radius: 4px;
margin-bottom: 6px;
text-align: center;
border: 1px solid #2a2e39;
}
.stop-button {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: pulse 2s infinite;

View File

@@ -9,6 +9,7 @@
*/
import { wsManager, type MessageHandler } from './useWebSocket'
import { intervalToSeconds } from '../utils'
import type {
IBasicDataFeed,
DatafeedConfiguration,
@@ -241,14 +242,39 @@ export class WebSocketDatafeed implements IBasicDataFeed {
console.log('[TradingView Datafeed] Raw bar sample:', response.history.bars?.[0])
console.log('[TradingView Datafeed] Denominators:', denoms)
const bars: Bar[] = (response.history.bars || []).map((bar: any) => ({
time: bar.time * 1000, // Convert to milliseconds
open: parseFloat(bar.open) / denoms.tick,
high: parseFloat(bar.high) / denoms.tick,
low: parseFloat(bar.low) / denoms.tick,
close: parseFloat(bar.close) / denoms.tick,
volume: parseFloat(bar.volume) / denoms.base
}))
const rawBars: any[] = response.history.bars || []
// Parse bars, preserving null OHLC for gap bars (no trades that period)
const parsedBars: Bar[] = rawBars.map((bar: any) => {
if (bar.open === null || bar.close === null) {
return { time: bar.time * 1000, open: null, high: null, low: null, close: null }
}
return {
time: bar.time * 1000,
open: parseFloat(bar.open) / denoms.tick,
high: parseFloat(bar.high) / denoms.tick,
low: parseFloat(bar.low) / denoms.tick,
close: parseFloat(bar.close) / denoms.tick,
volume: parseFloat(bar.volume) / denoms.base
}
})
parsedBars.sort((a, b) => a.time - b.time)
// Fill any gaps between returned bars with null bars so TradingView
// receives a contiguous array of the correct length.
const periodMs = intervalToSeconds(resolution) * 1000
const bars: Bar[] = []
for (let i = 0; i < parsedBars.length; i++) {
if (i > 0) {
const prev = parsedBars[i - 1].time
const curr = parsedBars[i].time
for (let t = prev + periodMs; t < curr; t += periodMs) {
bars.push({ time: t, open: null, high: null, low: null, close: null })
}
}
bars.push(parsedBars[i])
}
console.log('[TradingView Datafeed] Scaled bar sample:', bars[0])

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)

View File

@@ -144,37 +144,6 @@ function convertTVShapeToShape(tvShape: any, symbol: string, shapeId?: string, s
}
}
/**
* Convert interval string to seconds
*/
function intervalToSeconds(interval: string): number {
// Handle plain numbers (TradingView uses integers for sub-hour intervals in minutes)
const numericInterval = parseInt(interval)
if (!isNaN(numericInterval) && interval === numericInterval.toString()) {
return numericInterval * 60 // Plain number means minutes
}
// Handle formatted intervals like "15M", "1H", "1D"
const match = interval.match(/^(\d+)([SMHDW])$/)
if (!match) {
console.warn('[TradingView Shapes] Unknown interval format:', interval)
return 60 // Default to 1 minute
}
const value = parseInt(match[1])
const unit = match[2]
const multipliers: Record<string, number> = {
'S': 1,
'M': 60,
'H': 3600,
'D': 86400,
'W': 604800
}
return value * (multipliers[unit] || 60)
}
/**
* Canonicalize timestamp to candle boundary
* TradingView requires timestamps to align exactly with candle start times
@@ -532,9 +501,8 @@ export function useTradingViewShapes(tvWidget: IChartingLibraryWidget) {
const currentSymbol = chartStore.symbol
// Get current chart period and convert to seconds for timestamp canonicalization
const period = chartStore.period
const intervalSeconds = intervalToSeconds(period)
// period is already stored in seconds
const intervalSeconds = chartStore.period
// Convert points to TradingView format and canonicalize timestamps to candle boundaries
const tvPoints = shape.points.map(p => {

View File

@@ -5,7 +5,7 @@ export interface ChartState {
symbol: string
start_time: number | null
end_time: number | null
period: string
period: number
selected_shapes: string[]
}
@@ -13,7 +13,7 @@ export const useChartStore = defineStore('chartState', () => {
const symbol = ref<string>('BINANCE:BTC/USDT')
const start_time = ref<number | null>(null)
const end_time = ref<number | null>(null)
const period = ref<string>('15')
const period = ref<number>(900) // seconds; default 15 minutes
const selected_shapes = ref<string[]>([])
return { symbol, start_time, end_time, period, selected_shapes }

View File

@@ -3,7 +3,7 @@ import { ref } from 'vue'
export interface IndicatorInstance {
id: string
talib_name: string
pandas_ta_name: string
instance_name: string
parameters: Record<string, any>
tv_study_id?: string

23
web/src/utils.ts Normal file
View File

@@ -0,0 +1,23 @@
/**
* Convert a TradingView interval string to seconds.
* Plain numbers are minutes (TradingView convention), suffixed forms use the suffix.
* Examples: "15" → 900, "1D" → 86400, "1W" → 604800
*/
export function intervalToSeconds(interval: string): number {
const numericInterval = parseInt(interval)
if (!isNaN(numericInterval) && interval === numericInterval.toString()) {
return numericInterval * 60 // plain number = minutes
}
const match = interval.match(/^(\d+)([SMHDW])$/)
if (match) {
const value = parseInt(match[1])
switch (match[2]) {
case 'S': return value
case 'M': return value * 60
case 'H': return value * 3600
case 'D': return value * 86400
case 'W': return value * 604800
}
}
return 60 // fallback: 1 minute
}