data fixes; indicator=>workspace sync
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
23
web/src/utils.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user