auth
This commit is contained in:
289
web/src/composables/useTradingViewDatafeed.ts
Normal file
289
web/src/composables/useTradingViewDatafeed.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* TradingView Datafeed implementation using WebSocket backend
|
||||
*
|
||||
* This adapter translates TradingView's Datafeed API calls into
|
||||
* WebSocket messages that match the backend protocol defined in
|
||||
* backend/src/datasource/websocket_protocol.py
|
||||
*
|
||||
* Uses the shared WebSocket connection for all communication.
|
||||
*/
|
||||
|
||||
import { wsManager, type MessageHandler } from './useWebSocket'
|
||||
import type {
|
||||
IBasicDataFeed,
|
||||
DatafeedConfiguration,
|
||||
LibrarySymbolInfo,
|
||||
Bar,
|
||||
SearchSymbolResultItem,
|
||||
HistoryMetadata
|
||||
} from '../types/tradingview'
|
||||
|
||||
interface PendingRequest {
|
||||
resolve: (value: any) => void
|
||||
reject: (reason: any) => void
|
||||
timeout: number
|
||||
}
|
||||
|
||||
interface Subscription {
|
||||
symbolInfo: LibrarySymbolInfo
|
||||
resolution: string
|
||||
onTick: (bar: Bar) => void
|
||||
subscriptionId: string
|
||||
}
|
||||
|
||||
export class WebSocketDatafeed implements IBasicDataFeed {
|
||||
private pendingRequests: Map<string, PendingRequest> = new Map()
|
||||
private subscriptions: Map<string, Subscription> = new Map()
|
||||
private requestTimeout = 10000 // 10 seconds
|
||||
private configuration: DatafeedConfiguration | null = null
|
||||
private messageHandler: MessageHandler
|
||||
|
||||
constructor() {
|
||||
// Use the shared WebSocket connection (managed by App.vue authentication)
|
||||
// Register message handler for datafeed messages
|
||||
this.messageHandler = this.handleMessage.bind(this)
|
||||
wsManager.addHandler(this.messageHandler)
|
||||
}
|
||||
|
||||
private generateRequestId(): string {
|
||||
return `tv_${Date.now()}_${Math.random().toString(36).substring(7)}`
|
||||
}
|
||||
|
||||
private sendRequest<T>(message: any): Promise<T> {
|
||||
const requestId = message.request_id || this.generateRequestId()
|
||||
message.request_id = requestId
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
this.pendingRequests.delete(requestId)
|
||||
reject(new Error('Request timeout'))
|
||||
}, this.requestTimeout)
|
||||
|
||||
this.pendingRequests.set(requestId, { resolve, reject, timeout })
|
||||
wsManager.send(message)
|
||||
})
|
||||
}
|
||||
|
||||
private handleMessage(message: any): void {
|
||||
console.log('[TradingView Datafeed] Received message:', message)
|
||||
|
||||
// Handle responses to pending requests
|
||||
if (message.request_id && this.pendingRequests.has(message.request_id)) {
|
||||
console.log('[TradingView Datafeed] Found pending request for:', message.request_id)
|
||||
const pending = this.pendingRequests.get(message.request_id)!
|
||||
this.pendingRequests.delete(message.request_id)
|
||||
clearTimeout(pending.timeout)
|
||||
|
||||
if (message.type === 'error') {
|
||||
console.log('[TradingView Datafeed] Resolving with error:', message.error_message)
|
||||
pending.reject(new Error(message.error_message || 'Unknown error'))
|
||||
} else {
|
||||
console.log('[TradingView Datafeed] Resolving with response')
|
||||
pending.resolve(message)
|
||||
}
|
||||
} else if (message.request_id) {
|
||||
console.log('[TradingView Datafeed] No pending request found for:', message.request_id)
|
||||
}
|
||||
|
||||
// Handle real-time bar updates
|
||||
if (message.type === 'bar_update') {
|
||||
const subscription = this.subscriptions.get(message.subscription_id)
|
||||
if (subscription && message.bar) {
|
||||
const bar: Bar = {
|
||||
time: message.bar.time * 1000, // Convert to milliseconds
|
||||
open: parseFloat(message.bar.data.open),
|
||||
high: parseFloat(message.bar.data.high),
|
||||
low: parseFloat(message.bar.data.low),
|
||||
close: parseFloat(message.bar.data.close),
|
||||
volume: parseFloat(message.bar.data.volume)
|
||||
}
|
||||
subscription.onTick(bar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReady(callback: (configuration: DatafeedConfiguration) => void): void {
|
||||
// TradingView requires callback to be called asynchronously
|
||||
if (this.configuration) {
|
||||
setTimeout(() => callback(this.configuration!), 0)
|
||||
return
|
||||
}
|
||||
|
||||
this.sendRequest<any>({
|
||||
type: 'get_config'
|
||||
})
|
||||
.then((response) => {
|
||||
this.configuration = response.config
|
||||
setTimeout(() => callback(this.configuration!), 0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get datafeed configuration:', error)
|
||||
// Provide default configuration
|
||||
const defaultConfig: DatafeedConfiguration = {
|
||||
supported_resolutions: ['1', '5', '15', '30', '60', '240', '1D', '1W'],
|
||||
supports_search: true,
|
||||
supports_group_request: false,
|
||||
supports_marks: false,
|
||||
supports_timescale_marks: false,
|
||||
supports_time: false
|
||||
}
|
||||
this.configuration = defaultConfig
|
||||
setTimeout(() => callback(defaultConfig), 0)
|
||||
})
|
||||
}
|
||||
|
||||
searchSymbols(
|
||||
userInput: string,
|
||||
exchange: string,
|
||||
symbolType: string,
|
||||
onResult: (items: SearchSymbolResultItem[]) => void
|
||||
): void {
|
||||
this.sendRequest<any>({
|
||||
type: 'search_symbols',
|
||||
query: userInput,
|
||||
exchange: exchange || undefined,
|
||||
symbol_type: symbolType || undefined,
|
||||
limit: 30
|
||||
})
|
||||
.then((response) => {
|
||||
onResult(response.results || [])
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Symbol search failed:', error)
|
||||
onResult([])
|
||||
})
|
||||
}
|
||||
|
||||
resolveSymbol(
|
||||
symbolName: string,
|
||||
onResolve: (symbolInfo: LibrarySymbolInfo) => void,
|
||||
onError: (reason: string) => void
|
||||
): void {
|
||||
this.sendRequest<any>({
|
||||
type: 'resolve_symbol',
|
||||
symbol: symbolName
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.symbol_info) {
|
||||
console.log('[TradingView Datafeed] Resolved symbol info:', response.symbol_info)
|
||||
onResolve(response.symbol_info)
|
||||
} else {
|
||||
onError('Symbol not found')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to resolve symbol:', error)
|
||||
onError(error instanceof Error ? error.message : 'Unknown error')
|
||||
})
|
||||
}
|
||||
|
||||
getBars(
|
||||
symbolInfo: LibrarySymbolInfo,
|
||||
resolution: string,
|
||||
periodParams: {
|
||||
from: number
|
||||
to: number
|
||||
firstDataRequest: boolean
|
||||
countBack?: number
|
||||
},
|
||||
onResult: (bars: Bar[], meta: HistoryMetadata) => void,
|
||||
onError: (reason: string) => void
|
||||
): void {
|
||||
this.sendRequest<any>({
|
||||
type: 'get_bars',
|
||||
symbol: symbolInfo.ticker || symbolInfo.name,
|
||||
resolution: resolution,
|
||||
from_time: periodParams.from,
|
||||
to_time: periodParams.to,
|
||||
countback: periodParams.countBack
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.history) {
|
||||
const bars: Bar[] = (response.history.bars || []).map((bar: any) => ({
|
||||
time: bar.time * 1000, // Convert to milliseconds
|
||||
open: parseFloat(bar.data.open),
|
||||
high: parseFloat(bar.data.high),
|
||||
low: parseFloat(bar.data.low),
|
||||
close: parseFloat(bar.data.close),
|
||||
volume: parseFloat(bar.data.volume)
|
||||
}))
|
||||
|
||||
const meta: HistoryMetadata = {
|
||||
noData: bars.length === 0,
|
||||
nextTime: response.history.next_time
|
||||
}
|
||||
|
||||
onResult(bars, meta)
|
||||
} else {
|
||||
onResult([], { noData: true })
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get bars:', error)
|
||||
onError(error instanceof Error ? error.message : 'Unknown error')
|
||||
})
|
||||
}
|
||||
|
||||
subscribeBars(
|
||||
symbolInfo: LibrarySymbolInfo,
|
||||
resolution: string,
|
||||
onTick: (bar: Bar) => void,
|
||||
listenerGuid: string,
|
||||
onResetCacheNeededCallback: () => void
|
||||
): void {
|
||||
this.sendRequest<any>({
|
||||
type: 'subscribe_bars',
|
||||
symbol: symbolInfo.ticker || symbolInfo.name,
|
||||
resolution: resolution,
|
||||
subscription_id: listenerGuid
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.success) {
|
||||
this.subscriptions.set(listenerGuid, {
|
||||
symbolInfo,
|
||||
resolution,
|
||||
onTick,
|
||||
subscriptionId: listenerGuid
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to subscribe to bars:', error)
|
||||
})
|
||||
}
|
||||
|
||||
unsubscribeBars(listenerGuid: string): void {
|
||||
this.sendRequest<any>({
|
||||
type: 'unsubscribe_bars',
|
||||
subscription_id: listenerGuid
|
||||
})
|
||||
.then(() => {
|
||||
this.subscriptions.delete(listenerGuid)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to unsubscribe from bars:', error)
|
||||
})
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
// Clean up all subscriptions
|
||||
for (const [guid] of this.subscriptions) {
|
||||
this.unsubscribeBars(guid)
|
||||
}
|
||||
this.subscriptions.clear()
|
||||
|
||||
// Clear pending requests
|
||||
for (const [requestId, pending] of this.pendingRequests) {
|
||||
clearTimeout(pending.timeout)
|
||||
pending.reject(new Error('Datafeed destroyed'))
|
||||
}
|
||||
this.pendingRequests.clear()
|
||||
|
||||
// Remove message handler from shared WebSocket
|
||||
wsManager.removeHandler(this.messageHandler)
|
||||
}
|
||||
}
|
||||
|
||||
export function createTradingViewDatafeed(): IBasicDataFeed {
|
||||
return new WebSocketDatafeed()
|
||||
}
|
||||
Reference in New Issue
Block a user