|
|
|
|
@@ -0,0 +1,735 @@
|
|
|
|
|
import { watch } from 'vue'
|
|
|
|
|
import type { IChartingLibraryWidget } from '../types/tradingview'
|
|
|
|
|
import { useShapeStore } from '../stores/shapes'
|
|
|
|
|
import { useChartStore } from '../stores/chart'
|
|
|
|
|
import type { Shape, ControlPoint } from '../stores/shapes'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map TradingView internal tool names (LineToolXXX) to TradingView's native shape type names
|
|
|
|
|
* Used when converting user-drawn shapes to our Shape format
|
|
|
|
|
*/
|
|
|
|
|
const TV_TOOL_TO_SHAPE_TYPE: Record<string, string> = {
|
|
|
|
|
'LineToolTrendLine': 'trend_line',
|
|
|
|
|
'LineToolHorzLine': 'horizontal_line',
|
|
|
|
|
'LineToolVertLine': 'vertical_line',
|
|
|
|
|
'LineToolRectangle': 'rectangle',
|
|
|
|
|
'LineToolFibRetracement': 'fib_retracement',
|
|
|
|
|
'LineToolFibExtension': 'fib_trend_ext',
|
|
|
|
|
'LineToolParallelChannel': 'parallel_channel',
|
|
|
|
|
'LineToolArrow': 'arrow',
|
|
|
|
|
'LineToolText': 'text',
|
|
|
|
|
'LineToolCallout': 'text',
|
|
|
|
|
'LineToolCircle': 'circle',
|
|
|
|
|
'LineToolPath': 'path',
|
|
|
|
|
'LineToolPitchfork': 'pitchfork',
|
|
|
|
|
'LineToolGannFan': 'gannbox_fan',
|
|
|
|
|
'LineToolHeadAndShoulders': 'head_and_shoulders'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map line styles between our format and TradingView format
|
|
|
|
|
*/
|
|
|
|
|
const LINE_STYLE_TO_TV: Record<string, number> = {
|
|
|
|
|
'solid': 0,
|
|
|
|
|
'dashed': 1,
|
|
|
|
|
'dotted': 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const TV_TO_LINE_STYLE: Record<number, string> = {
|
|
|
|
|
0: 'solid',
|
|
|
|
|
1: 'dashed',
|
|
|
|
|
2: 'dotted'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convert TradingView shape to our Shape format
|
|
|
|
|
*/
|
|
|
|
|
function convertTVShapeToShape(tvShape: any, symbol: string, shapeId?: string, shapeName?: string): Shape {
|
|
|
|
|
// Get shape ID - try multiple ways
|
|
|
|
|
let id = shapeId || tvShape.id
|
|
|
|
|
if (!id && typeof tvShape.getId === 'function') {
|
|
|
|
|
try {
|
|
|
|
|
id = tvShape.getId()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[TradingView Shapes] Error calling getId:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get shape name/type - use provided name first, then try multiple properties
|
|
|
|
|
if (!shapeName) {
|
|
|
|
|
shapeName = tvShape.name
|
|
|
|
|
}
|
|
|
|
|
if (!shapeName && typeof tvShape.getName === 'function') {
|
|
|
|
|
try {
|
|
|
|
|
shapeName = tvShape.getName()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[TradingView Shapes] Error calling getName:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback to checking shape type or tool name
|
|
|
|
|
if (!shapeName) {
|
|
|
|
|
shapeName = tvShape.toolName || tvShape.type || 'unknown'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const standardType = TV_TOOL_TO_SHAPE_TYPE[shapeName] || shapeName
|
|
|
|
|
|
|
|
|
|
// Extract control points from TradingView shape
|
|
|
|
|
const points: ControlPoint[] = []
|
|
|
|
|
|
|
|
|
|
// Try multiple ways to get points from the shape
|
|
|
|
|
let pointsData = tvShape.points || []
|
|
|
|
|
|
|
|
|
|
// If getPoints method exists, use it
|
|
|
|
|
if (typeof tvShape.getPoints === 'function') {
|
|
|
|
|
try {
|
|
|
|
|
pointsData = tvShape.getPoints() || []
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[TradingView Shapes] Error calling getPoints:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert points to our format
|
|
|
|
|
if (Array.isArray(pointsData)) {
|
|
|
|
|
for (const point of pointsData) {
|
|
|
|
|
if (point && typeof point === 'object') {
|
|
|
|
|
const controlPoint: ControlPoint = {
|
|
|
|
|
time: Math.floor(point.time),
|
|
|
|
|
price: point.price
|
|
|
|
|
}
|
|
|
|
|
// Only include channel if it's actually set
|
|
|
|
|
if (point.channel !== undefined && point.channel !== null) {
|
|
|
|
|
controlPoint.channel = point.channel
|
|
|
|
|
}
|
|
|
|
|
points.push(controlPoint)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[TradingView Shapes] Converting shape ${id} (${shapeName}), found ${points.length} points`, points)
|
|
|
|
|
|
|
|
|
|
// Extract properties
|
|
|
|
|
let properties = tvShape.properties || {}
|
|
|
|
|
|
|
|
|
|
// If getProperties method exists, use it
|
|
|
|
|
if (typeof tvShape.getProperties === 'function') {
|
|
|
|
|
try {
|
|
|
|
|
properties = tvShape.getProperties() || properties
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[TradingView Shapes] Error calling getProperties:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const color = properties.linecolor || '#2962FF'
|
|
|
|
|
const lineWidth = properties.linewidth || 1
|
|
|
|
|
const lineStyle = TV_TO_LINE_STYLE[properties.linestyle] || 'solid'
|
|
|
|
|
|
|
|
|
|
const now = Math.floor(Date.now() / 1000)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
type: standardType,
|
|
|
|
|
points,
|
|
|
|
|
color,
|
|
|
|
|
line_width: lineWidth,
|
|
|
|
|
line_style: lineStyle,
|
|
|
|
|
properties: {
|
|
|
|
|
...properties,
|
|
|
|
|
// Store original TradingView shape name for reference
|
|
|
|
|
tv_shape_name: shapeName
|
|
|
|
|
},
|
|
|
|
|
symbol,
|
|
|
|
|
created_at: now,
|
|
|
|
|
modified_at: now
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
function canonicalizeTimestamp(timestamp: number, intervalSeconds: number): number {
|
|
|
|
|
// Round down to nearest candle boundary
|
|
|
|
|
return Math.floor(timestamp / intervalSeconds) * intervalSeconds
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup bidirectional synchronization between TradingView shapes and ShapeStore
|
|
|
|
|
*/
|
|
|
|
|
export function useTradingViewShapes(tvWidget: IChartingLibraryWidget) {
|
|
|
|
|
const shapeStore = useShapeStore()
|
|
|
|
|
const chartStore = useChartStore()
|
|
|
|
|
|
|
|
|
|
let isApplyingTVUpdate = false // Prevent circular updates when TV modifies shapes
|
|
|
|
|
let isUpdatingStore = false // Prevent circular updates when we update the store from TV
|
|
|
|
|
let isChartReady = false // Track if chart API is ready
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle drawing events from TradingView
|
|
|
|
|
*/
|
|
|
|
|
function handleDrawingEvent(shapeId: string, eventType: string) {
|
|
|
|
|
if (isApplyingTVUpdate) return
|
|
|
|
|
|
|
|
|
|
console.log('[TradingView Shapes] Drawing event:', eventType, shapeId)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const chart = tvWidget.activeChart()
|
|
|
|
|
if (!chart) return
|
|
|
|
|
|
|
|
|
|
const currentSymbol = chartStore.chart_state.symbol
|
|
|
|
|
|
|
|
|
|
if (eventType === 'remove') {
|
|
|
|
|
isUpdatingStore = true
|
|
|
|
|
shapeStore.removeShape(shapeId)
|
|
|
|
|
isUpdatingStore = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// First get the shape name from getAllShapes (which has id and name)
|
|
|
|
|
let shapeName: string | undefined
|
|
|
|
|
const allShapes = chart.getAllShapes()
|
|
|
|
|
if (allShapes) {
|
|
|
|
|
const shapeInfo = allShapes.find((s: any) => s.id === shapeId)
|
|
|
|
|
if (shapeInfo) {
|
|
|
|
|
shapeName = shapeInfo.name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Then get the actual shape entity using getShapeById (which has methods)
|
|
|
|
|
let tvShape
|
|
|
|
|
try {
|
|
|
|
|
tvShape = chart.getShapeById(shapeId)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('[TradingView Shapes] Could not get shape:', shapeId)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!tvShape) return
|
|
|
|
|
|
|
|
|
|
// Debug: log on first encounter
|
|
|
|
|
if (eventType === 'create') {
|
|
|
|
|
console.log('[TradingView Shapes] Shape entity methods:', Object.keys(tvShape))
|
|
|
|
|
console.log('[TradingView Shapes] Shape name from getAllShapes:', shapeName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (eventType === 'create') {
|
|
|
|
|
// Get points immediately - don't create shape without them
|
|
|
|
|
const points = tvShape.getPoints()
|
|
|
|
|
const controlPoints = points && points.length > 0 ? points.map(p => {
|
|
|
|
|
const cp: ControlPoint = {
|
|
|
|
|
time: Math.floor(p.time),
|
|
|
|
|
price: p.price
|
|
|
|
|
}
|
|
|
|
|
if (p.channel !== undefined && p.channel !== null) {
|
|
|
|
|
cp.channel = p.channel
|
|
|
|
|
}
|
|
|
|
|
return cp
|
|
|
|
|
}) : []
|
|
|
|
|
|
|
|
|
|
// Create shape with complete data
|
|
|
|
|
const shape = convertTVShapeToShape(tvShape, currentSymbol, shapeId, shapeName)
|
|
|
|
|
// Override points with the ones we just fetched
|
|
|
|
|
shape.points = controlPoints
|
|
|
|
|
|
|
|
|
|
console.log('[TradingView Shapes] Creating shape:', shapeId, shape.type, 'with', shape.points.length, 'points')
|
|
|
|
|
isUpdatingStore = true
|
|
|
|
|
shapeStore.addShape(shape)
|
|
|
|
|
isUpdatingStore = false
|
|
|
|
|
} else if (eventType === 'points_changed') {
|
|
|
|
|
// Points updated - get latest points and create NEW array
|
|
|
|
|
const points = tvShape.getPoints()
|
|
|
|
|
if (points) {
|
|
|
|
|
// Create a completely new array to ensure it's treated as a replacement
|
|
|
|
|
const controlPoints = Array.from(points.map(p => {
|
|
|
|
|
const cp: ControlPoint = {
|
|
|
|
|
time: Math.floor(p.time),
|
|
|
|
|
price: p.price
|
|
|
|
|
}
|
|
|
|
|
if (p.channel !== undefined && p.channel !== null) {
|
|
|
|
|
cp.channel = p.channel
|
|
|
|
|
}
|
|
|
|
|
return cp
|
|
|
|
|
}))
|
|
|
|
|
console.log('[TradingView Shapes] Points changed:', shapeId, 'now has', controlPoints.length, 'points')
|
|
|
|
|
isUpdatingStore = true
|
|
|
|
|
shapeStore.updateShape(shapeId, { points: controlPoints })
|
|
|
|
|
isUpdatingStore = false
|
|
|
|
|
}
|
|
|
|
|
} else if (eventType === 'properties_changed') {
|
|
|
|
|
// Properties updated - get latest properties
|
|
|
|
|
const properties = tvShape.getProperties()
|
|
|
|
|
if (properties) {
|
|
|
|
|
const color = properties.linecolor || '#2962FF'
|
|
|
|
|
const lineWidth = properties.linewidth || 1
|
|
|
|
|
const lineStyle = TV_TO_LINE_STYLE[properties.linestyle] || 'solid'
|
|
|
|
|
console.log('[TradingView Shapes] Properties changed:', shapeId)
|
|
|
|
|
isUpdatingStore = true
|
|
|
|
|
shapeStore.updateShape(shapeId, {
|
|
|
|
|
color,
|
|
|
|
|
line_width: lineWidth,
|
|
|
|
|
line_style: lineStyle,
|
|
|
|
|
properties
|
|
|
|
|
})
|
|
|
|
|
isUpdatingStore = false
|
|
|
|
|
}
|
|
|
|
|
} else if (eventType === 'move') {
|
|
|
|
|
// Shape moved - update points with NEW array
|
|
|
|
|
const points = tvShape.getPoints()
|
|
|
|
|
if (points) {
|
|
|
|
|
const controlPoints = Array.from(points.map(p => {
|
|
|
|
|
const cp: ControlPoint = {
|
|
|
|
|
time: Math.floor(p.time),
|
|
|
|
|
price: p.price
|
|
|
|
|
}
|
|
|
|
|
if (p.channel !== undefined && p.channel !== null) {
|
|
|
|
|
cp.channel = p.channel
|
|
|
|
|
}
|
|
|
|
|
return cp
|
|
|
|
|
}))
|
|
|
|
|
console.log('[TradingView Shapes] Shape moved:', shapeId)
|
|
|
|
|
isUpdatingStore = true
|
|
|
|
|
shapeStore.updateShape(shapeId, { points: controlPoints })
|
|
|
|
|
isUpdatingStore = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[TradingView Shapes] Error handling drawing event:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup listeners for TradingView shape events
|
|
|
|
|
*/
|
|
|
|
|
function setupShapeListeners() {
|
|
|
|
|
try {
|
|
|
|
|
const chart = tvWidget.activeChart()
|
|
|
|
|
if (!chart) {
|
|
|
|
|
console.warn('[TradingView Shapes] Active chart not available yet')
|
|
|
|
|
return () => {}
|
|
|
|
|
}
|
|
|
|
|
isChartReady = true
|
|
|
|
|
|
|
|
|
|
console.log('[TradingView Shapes] Setting up drawing event subscriptions')
|
|
|
|
|
|
|
|
|
|
// Subscribe to the drawing event which handles all shape events
|
|
|
|
|
const unsubscribe = tvWidget.subscribe('drawing_event', (shapeId: string, eventType: string) => {
|
|
|
|
|
handleDrawingEvent(shapeId, eventType)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Initial sync on data load
|
|
|
|
|
const dataLoadedSubscription = chart.onDataLoaded().subscribe(null, () => {
|
|
|
|
|
console.log('[TradingView Shapes] Chart data loaded, performing initial sync')
|
|
|
|
|
syncShapesFromTV()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Cleanup function to unsubscribe
|
|
|
|
|
return () => {
|
|
|
|
|
console.log('[TradingView Shapes] Cleaning up event subscriptions')
|
|
|
|
|
try {
|
|
|
|
|
if (typeof unsubscribe === 'function') {
|
|
|
|
|
unsubscribe()
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('[TradingView Shapes] unsubscribe is not a function:', typeof unsubscribe)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dataLoadedSubscription && typeof dataLoadedSubscription.unsubscribe === 'function') {
|
|
|
|
|
dataLoadedSubscription.unsubscribe()
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[TradingView Shapes] Error during cleanup:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[TradingView Shapes] Error setting up shape listeners:', error)
|
|
|
|
|
return () => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Scan TradingView for shapes and sync to store
|
|
|
|
|
*/
|
|
|
|
|
function syncShapesFromTV() {
|
|
|
|
|
if (!isChartReady) {
|
|
|
|
|
return // Skip if chart isn't ready yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const chart = tvWidget.activeChart()
|
|
|
|
|
if (!chart) {
|
|
|
|
|
return // Chart not available
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const allShapes = chart.getAllShapes()
|
|
|
|
|
|
|
|
|
|
if (!allShapes) return
|
|
|
|
|
|
|
|
|
|
const currentSymbol = chartStore.chart_state.symbol
|
|
|
|
|
|
|
|
|
|
// Track which shape IDs we've seen
|
|
|
|
|
const seenIds = new Set<string>()
|
|
|
|
|
|
|
|
|
|
isUpdatingStore = true
|
|
|
|
|
try {
|
|
|
|
|
for (const tvShape of allShapes) {
|
|
|
|
|
seenIds.add(tvShape.id)
|
|
|
|
|
|
|
|
|
|
const existingShape = shapeStore.getShape(tvShape.id)
|
|
|
|
|
const newShape = convertTVShapeToShape(tvShape, currentSymbol)
|
|
|
|
|
|
|
|
|
|
if (!existingShape) {
|
|
|
|
|
// New shape - add it
|
|
|
|
|
console.log('[TradingView Shapes] New shape detected:', tvShape.id, tvShape.name, 'with', newShape.points.length, 'points')
|
|
|
|
|
shapeStore.addShape(newShape)
|
|
|
|
|
} else {
|
|
|
|
|
// Check if shape has changed
|
|
|
|
|
const pointsChanged = JSON.stringify(existingShape.points) !== JSON.stringify(newShape.points)
|
|
|
|
|
const hasChanged =
|
|
|
|
|
pointsChanged ||
|
|
|
|
|
existingShape.color !== newShape.color ||
|
|
|
|
|
existingShape.line_width !== newShape.line_width ||
|
|
|
|
|
existingShape.line_style !== newShape.line_style
|
|
|
|
|
|
|
|
|
|
if (hasChanged) {
|
|
|
|
|
if (pointsChanged) {
|
|
|
|
|
console.log('[TradingView Shapes] Shape points changed:', tvShape.id,
|
|
|
|
|
'from', existingShape.points.length, 'to', newShape.points.length, 'points')
|
|
|
|
|
} else {
|
|
|
|
|
console.log('[TradingView Shapes] Shape properties modified:', tvShape.id)
|
|
|
|
|
}
|
|
|
|
|
shapeStore.updateShape(tvShape.id, {
|
|
|
|
|
points: newShape.points,
|
|
|
|
|
color: newShape.color,
|
|
|
|
|
line_width: newShape.line_width,
|
|
|
|
|
line_style: newShape.line_style,
|
|
|
|
|
properties: newShape.properties
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for deleted shapes
|
|
|
|
|
const allStoreShapes = shapeStore.getAllShapes()
|
|
|
|
|
for (const storeShape of allStoreShapes) {
|
|
|
|
|
if (storeShape.symbol === currentSymbol && !seenIds.has(storeShape.id)) {
|
|
|
|
|
console.log('[TradingView Shapes] Shape deleted:', storeShape.id)
|
|
|
|
|
shapeStore.removeShape(storeShape.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
isUpdatingStore = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[TradingView Shapes] Error syncing shapes from TV:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup watchers for ShapeStore changes to apply to TradingView
|
|
|
|
|
*/
|
|
|
|
|
function setupStoreWatchers() {
|
|
|
|
|
// Watch for shape store changes and apply to TradingView
|
|
|
|
|
watch(
|
|
|
|
|
() => shapeStore.shapes,
|
|
|
|
|
async (newShapes, oldShapes) => {
|
|
|
|
|
if (isUpdatingStore || !isChartReady) return
|
|
|
|
|
|
|
|
|
|
isApplyingTVUpdate = true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const chart = tvWidget.activeChart()
|
|
|
|
|
if (!chart) return
|
|
|
|
|
|
|
|
|
|
const currentSymbol = chartStore.chart_state.symbol
|
|
|
|
|
|
|
|
|
|
// Find added shapes
|
|
|
|
|
for (const [id, shape] of Object.entries(newShapes)) {
|
|
|
|
|
// Only apply shapes for current symbol (or shapes with null symbol)
|
|
|
|
|
if (shape.symbol && shape.symbol !== currentSymbol) continue
|
|
|
|
|
|
|
|
|
|
if (!oldShapes || !oldShapes[id]) {
|
|
|
|
|
// Shape was added to store
|
|
|
|
|
await createTVShape(shape)
|
|
|
|
|
} else {
|
|
|
|
|
// Check if shape exists in TradingView
|
|
|
|
|
let tvShape = null
|
|
|
|
|
try {
|
|
|
|
|
tvShape = chart.getShapeById(id)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Shape doesn't exist in TV
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!tvShape) {
|
|
|
|
|
// Shape is in store but not in TradingView - create it
|
|
|
|
|
await createTVShape(shape)
|
|
|
|
|
} else if (JSON.stringify(shape) !== JSON.stringify(oldShapes[id])) {
|
|
|
|
|
// Shape was modified
|
|
|
|
|
updateTVShape(shape)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find removed shapes
|
|
|
|
|
if (oldShapes) {
|
|
|
|
|
for (const [id, shape] of Object.entries(oldShapes)) {
|
|
|
|
|
if (shape.symbol && shape.symbol !== currentSymbol) continue
|
|
|
|
|
|
|
|
|
|
if (!newShapes[id]) {
|
|
|
|
|
removeTVShape(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[TradingView Shapes] Error applying store changes:', error)
|
|
|
|
|
} finally {
|
|
|
|
|
isApplyingTVUpdate = false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ deep: true }
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a shape in TradingView
|
|
|
|
|
*/
|
|
|
|
|
async function createTVShape(shape: Shape) {
|
|
|
|
|
if (!isChartReady) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const chart = tvWidget.activeChart()
|
|
|
|
|
if (!chart) return
|
|
|
|
|
|
|
|
|
|
const currentSymbol = chartStore.chart_state.symbol
|
|
|
|
|
|
|
|
|
|
// Get current chart interval and convert to seconds for timestamp canonicalization
|
|
|
|
|
const interval = chartStore.chart_state.interval
|
|
|
|
|
const intervalSeconds = intervalToSeconds(interval)
|
|
|
|
|
|
|
|
|
|
// Convert points to TradingView format and canonicalize timestamps to candle boundaries
|
|
|
|
|
const tvPoints = shape.points.map(p => {
|
|
|
|
|
const canonicalTime = canonicalizeTimestamp(p.time, intervalSeconds)
|
|
|
|
|
const point: any = {
|
|
|
|
|
time: canonicalTime,
|
|
|
|
|
price: p.price
|
|
|
|
|
}
|
|
|
|
|
// Only include channel if it has a value (for parallel channels, etc.)
|
|
|
|
|
if (p.channel != null) {
|
|
|
|
|
point.channel = p.channel
|
|
|
|
|
}
|
|
|
|
|
return point
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Build overrides - merge color/width/style with properties
|
|
|
|
|
const overrides: any = {
|
|
|
|
|
...shape.properties
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shape.color) {
|
|
|
|
|
overrides.linecolor = shape.color
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shape.line_width) {
|
|
|
|
|
overrides.linewidth = shape.line_width
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shape.line_style) {
|
|
|
|
|
overrides.linestyle = LINE_STYLE_TO_TV[shape.line_style] || 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
|
shape: shape.type,
|
|
|
|
|
zOrder: 'top',
|
|
|
|
|
lock: false,
|
|
|
|
|
disableSelection: false,
|
|
|
|
|
disableSave: false,
|
|
|
|
|
disableUndo: false,
|
|
|
|
|
overrides
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[TradingView Shapes] Creating shape type="${shape.type}" with ${tvPoints.length} points:`, tvPoints)
|
|
|
|
|
|
|
|
|
|
// Create the shape - use createShape for single points, createMultipointShape for multiple
|
|
|
|
|
let entityIdPromise
|
|
|
|
|
if (tvPoints.length === 1) {
|
|
|
|
|
console.log(`[TradingView Shapes] Using createShape() for single point`)
|
|
|
|
|
entityIdPromise = chart.createShape(tvPoints[0], options)
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`[TradingView Shapes] Using createMultipointShape() for ${tvPoints.length} points`)
|
|
|
|
|
entityIdPromise = chart.createMultipointShape(tvPoints, options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const entityId = await entityIdPromise
|
|
|
|
|
|
|
|
|
|
// If TradingView assigned a different ID, update the store
|
|
|
|
|
if (entityId && entityId !== shape.id) {
|
|
|
|
|
isUpdatingStore = true
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
shapeStore.removeShape(shape.id)
|
|
|
|
|
|
|
|
|
|
const updatedShape: Shape = {
|
|
|
|
|
...shape,
|
|
|
|
|
id: entityId,
|
|
|
|
|
original_id: shape.original_id || shape.id,
|
|
|
|
|
symbol: shape.symbol || currentSymbol
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shapeStore.addShape(updatedShape)
|
|
|
|
|
} finally {
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
|
|
|
isUpdatingStore = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bring shape to front to ensure visibility
|
|
|
|
|
try {
|
|
|
|
|
const createdShape = chart.getShapeById(entityId)
|
|
|
|
|
if (createdShape && typeof createdShape.bringToFront === 'function') {
|
|
|
|
|
createdShape.bringToFront()
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Ignore errors
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[TradingView Shapes] Error creating shape:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update a shape in TradingView
|
|
|
|
|
*/
|
|
|
|
|
function updateTVShape(shape: Shape) {
|
|
|
|
|
if (!isChartReady) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const chart = tvWidget.activeChart()
|
|
|
|
|
if (!chart) return
|
|
|
|
|
|
|
|
|
|
const tvShape = chart.getShapeById(shape.id)
|
|
|
|
|
|
|
|
|
|
if (!tvShape) {
|
|
|
|
|
console.warn('[TradingView Shapes] Shape not found in TV, creating instead:', shape.id)
|
|
|
|
|
createTVShape(shape)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update points
|
|
|
|
|
const tvPoints = shape.points.map(p => {
|
|
|
|
|
const point: { time: number; price: number; channel?: string } = {
|
|
|
|
|
time: p.time,
|
|
|
|
|
price: p.price
|
|
|
|
|
}
|
|
|
|
|
if (p.channel) {
|
|
|
|
|
point.channel = p.channel
|
|
|
|
|
}
|
|
|
|
|
return point
|
|
|
|
|
})
|
|
|
|
|
tvShape.setPoints(tvPoints)
|
|
|
|
|
|
|
|
|
|
// Update properties
|
|
|
|
|
tvShape.setProperties({
|
|
|
|
|
linecolor: shape.color || '#2962FF',
|
|
|
|
|
linewidth: shape.line_width || 1,
|
|
|
|
|
linestyle: LINE_STYLE_TO_TV[shape.line_style || 'solid'] || 0,
|
|
|
|
|
...shape.properties
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
console.log('[TradingView Shapes] Updated shape in TV:', shape.id)
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[TradingView Shapes] Error updating shape in TV:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove a shape from TradingView
|
|
|
|
|
*/
|
|
|
|
|
function removeTVShape(shapeId: string) {
|
|
|
|
|
if (!isChartReady) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const chart = tvWidget.activeChart()
|
|
|
|
|
if (!chart) return
|
|
|
|
|
|
|
|
|
|
const tvShape = chart.getShapeById(shapeId)
|
|
|
|
|
|
|
|
|
|
if (tvShape) {
|
|
|
|
|
chart.removeEntity(tvShape)
|
|
|
|
|
console.log('[TradingView Shapes] Removed shape from TV:', shapeId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[TradingView Shapes] Error removing shape from TV:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup selection tracking
|
|
|
|
|
*/
|
|
|
|
|
function setupSelectionTracking() {
|
|
|
|
|
if (!isChartReady) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const chart = tvWidget.activeChart()
|
|
|
|
|
if (!chart) return
|
|
|
|
|
|
|
|
|
|
chart.onSelectionChanged().subscribe(null, () => {
|
|
|
|
|
const selectedObjects = chart.selection()
|
|
|
|
|
|
|
|
|
|
// Filter for shape/drawing objects only
|
|
|
|
|
const selectedShapeIds = selectedObjects
|
|
|
|
|
.filter((obj: any) => obj.type === 'study' || obj.name?.startsWith('LineTool'))
|
|
|
|
|
.map((obj: any) => obj.id)
|
|
|
|
|
|
|
|
|
|
console.log('[TradingView Shapes] Selection changed:', selectedShapeIds)
|
|
|
|
|
chartStore.chart_state.selected_shapes = selectedShapeIds
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('[TradingView Shapes] Selection tracking not available:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize
|
|
|
|
|
const cleanupShapeListeners = setupShapeListeners()
|
|
|
|
|
setupStoreWatchers()
|
|
|
|
|
setupSelectionTracking()
|
|
|
|
|
|
|
|
|
|
// Return cleanup function
|
|
|
|
|
return () => {
|
|
|
|
|
cleanupShapeListeners()
|
|
|
|
|
}
|
|
|
|
|
}
|