-
Notifications
You must be signed in to change notification settings - Fork 342
Description
Description:
I am using the TradingView Chart Library in my project.
The historical candle feed is generated randomly on the backend so the chart is not empty.
I also generate a random real-time data feed and send it to the chart via WebSocket.
Problem:
When I am on the 1-minute timeframe, the real-time feed works correctly and shows active price updates.
Switching to the 5-minute timeframe, the real-time price is still visible and updating as expected.
However, if I switch back to the 1-minute timeframe, the real-time data stops updating on the chart.
By inspecting the WebSocket data stream, I confirmed that real-time price updates are still being sent continuously. Despite this, the 1-minute chart does not reflect any real-time movement.
Code:
(Provide your relevant code here)
Environment:
TradingView Charting Library
WebSocket real-time feed
Randomized backend-generated historical candles
Expected Behavior:
Real-time updates should continue to display correctly when switching between timeframes, including back to the 1-minute chart.
Additional Notes:
This seems related to how subscribeBars and resolution changes are handled.
`
<title>TradingView Fixed Exchange & |adj</title> <script type="text/javascript" src="charting_library/charting_library.standalone.js"></script> <script type="text/javascript" src="datafeeds/udf/dist/bundle.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script> <script> async function feedSignRequest(path, body = "", feedTimestamp = Math.floor(Date.now() / 1000)) { try { const feedSignature = await window.chrome.webview.hostObjects.hmacHelper.Sign(path, body, feedTimestamp); return { feedSignature, feedTimestamp }; } catch (err) { console.error("HMAC Sign Error:", err); return { feedSignature: "", feedTimestamp }; } } const originalFetch = window.fetch;
window.fetch = async function (input, init = {}) {
const url = typeof input === "string" ? input : input.url;
const path = new URL(url, window.location.origin).pathname;
const body = init.body || "";
const { feedSignature, feedTimestamp } = await feedSignRequest(path, body);
init.headers = {
...(init.headers || {}),
"X-FeedSignature": feedSignature,
"X-FeedTimestamp": feedTimestamp,
};
return originalFetch(input, init);
};
</script>
<script>
async function wsSignRequest(path, body = "", wsTimestamp = Math.floor(Date.now() / 1000)) {
try {
const wsSignature = await window.chrome.webview.hostObjects.hmacHelper.Sign(path, body, wsTimestamp);
return { wsSignature, wsTimestamp };
} catch (err) {
console.error("HMAC Sign Error:", err);
return { wsSignature: "", wsTimestamp };
}
}
const originalFetch = window.fetch;
window.fetch = async function (input, init = {}) {
const url = typeof input === "string" ? input : input.url;
const path = new URL(url, window.location.origin).pathname;
const body = init.body || "";
const { wsSignature, wsTimestamp } = await wsSignRequest(path, body);
init.headers = {
...(init.headers || {}),
"X-WsSignature": wsSignature,
"X-WsTimestamp": wsTimestamp,
};
return originalFetch(input, init);
};
</script>
<style>
html, body, #tv_chart_container {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
}
</style>
<script>
function initChart() {
let currentExchange = 'DemoExchange';
let currentAdjustment = 'normal';
let myDropdownApiL = null;
let liveWs = null;
let subscriberUID = null;
let lastBarCache = {};
function normalizeSymbol(symbolName) {
let parts = symbolName.split(':').filter(Boolean);
if (parts.length > 2) symbolName = parts[0] + ':' + parts[parts.length - 1];
else if (parts.length === 2) symbolName = parts.join(':');
return symbolName.replace(/\|(1|adj)+$/, '');
}
function buildSymbol(s) {
let symbol = normalizeSymbol(s.symbol);
let exchange = currentExchange;
if (s.exchange) currentExchange = s.exchange;
else if (s.symbol.includes(':')) currentExchange = s.symbol.split(':')[0];
if (!symbol.includes(':') && exchange) symbol = `${exchange}:${symbol}`;
if (currentAdjustment === 'adj' && !symbol.endsWith('|adj')) symbol += '|adj';
else if (currentAdjustment === 'normal') symbol = symbol.replace(/\|adj$/, '');
return symbol;
}
function getBarTime(timestamp, resolution) {
const date = new Date(timestamp * 1000);
if (resolution.includes('D')) {
date.setHours(0, 0, 0, 0);
return Math.floor(date.getTime() / 1000);
} else if (resolution.includes('W')) {
const day = date.getDay();
date.setDate(date.getDate() - day);
date.setHours(0, 0, 0, 0);
return Math.floor(date.getTime() / 1000);
} else {
const minutes = parseInt(resolution) || 1;
const minutesSinceEpoch = Math.floor(timestamp / 60);
const barMinute = Math.floor(minutesSinceEpoch / minutes) * minutes;
return barMinute * 60;
}
}
const baseDatafeed = new Datafeeds.UDFCompatibleDatafeed("https://localhost:5001/api/trading-view/udf");
const customDatafeed = {
onReady: (callback) => baseDatafeed.onReady(callback),
searchSymbols: (userInput, exchange, symbolType, onResult) =>
baseDatafeed.searchSymbols(userInput, exchange, symbolType, onResult),
resolveSymbol: (symbolName, onResolve, onError) =>
baseDatafeed.resolveSymbol(symbolName, onResolve, onError),
getBars: (symbolInfo, resolution, periodParams, onResult, onError) => {
baseDatafeed.getBars(symbolInfo, resolution, periodParams, (bars, meta) => {
if (bars && bars.length > 0) {
const lastBar = bars[bars.length - 1];
const cacheKey = `${symbolInfo.name}_${resolution}`;
lastBarCache[cacheKey] = { ...lastBar };
console.log('📊 Last bar cached:', lastBar);
}
onResult(bars, meta);
}, onError);
},
subscribeBars: (symbolInfo, resolution, onTick, listenerGuid, onResetCacheNeededCallback) => {
console.log('=== subscribeBars called ===');
subscriberUID = listenerGuid;
window.tvOnTick = onTick;
window.currentSymbol = symbolInfo.name;
window.currentResolution = resolution;
if (liveWs && liveWs.readyState === WebSocket.OPEN) {
liveWs.send(JSON.stringify({
type: "subscribe",
symbol: symbolInfo.name,
resolution: resolution
}));
}
},
unsubscribeBars: (listenerGuid) => {
console.log('=== unsubscribeBars called ===');
subscriberUID = null;
window.tvOnTick = null;
window.currentSymbol = null;
window.currentResolution = null;
if (liveWs && liveWs.readyState === WebSocket.OPEN) {
liveWs.send(JSON.stringify({
type: "unsubscribe"
}));
}
}
};
async function connectWebSocket() {
try {
const path = "/ws";
const wsTimestamp = Math.floor(Date.now() / 1000);
const wsSignature = await window.chrome.webview.hostObjects.hmacHelper.Sign(
path,
"",
wsTimestamp
);
const activeSymbol = widget.activeChart().symbol();
const wsUrl = `ws://localhost:5090/ws?timestamp=${wsTimestamp}&signature=${encodeURIComponent(wsSignature)}&symbol=${activeSymbol}`;
console.log("🔗 Connecting to WebSocket with signature...");
liveWs = new WebSocket(wsUrl);
liveWs.onopen = () => {
console.log("✅ Connected to WebSocket server for live data!");
};
liveWs.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log("📊 Received data:", data);
if (!window.tvOnTick || typeof window.tvOnTick !== 'function') {
console.warn("⚠️ tvOnTick callback not found!");
return;
}
const resolution = window.currentResolution || '1';
const symbol = window.currentSymbol;
const cacheKey = `${symbol}_${resolution}`;
const barTime = getBarTime(data.t, resolution);
let lastBar = lastBarCache[cacheKey];
console.log("-------------------------------------");
if (!lastBar || lastBar.time < barTime * 1000) {
const newBar = {
time: barTime * 1000,
open: data.o,
high: data.h,
low: data.l,
close: data.c,
volume: data.v
};
lastBarCache[cacheKey] = { ...newBar };
console.log("🆕 **********************************:", newBar);
alert("******************");
window.tvOnTick(newBar);
} else {
lastBar.high = Math.max(lastBar.high, data.h);
lastBar.low = Math.min(lastBar.low, data.l);
lastBar.close = data.c;
lastBar.volume = data.v;
console.log("🔄 Updating current bar:", lastBar);
window.tvOnTick({ ...lastBar });
}
} catch (err) {
console.error("❌ Invalid WebSocket message:", err);
}
};
liveWs.onclose = () => {
console.log("❌ WebSocket disconnected - Reconnecting in 3s...");
setTimeout(connectWebSocket, 3000);
};
liveWs.onerror = (err) => {
console.error("❌ WebSocket error:", err);
};
} catch (err) {
console.error("❌ Error creating WebSocket signature:", err);
setTimeout(connectWebSocket, 3000);
}
}
connectWebSocket();
const widget = new TradingView.widget({
symbol: 'DemoExchange:AAPL',
interval: '1',
container: "tv_chart_container",
datafeed: customDatafeed,
library_path: "charting_library/",
autosize: true,
locale: "fa",
theme: "light",
enabled_features: ["study_templates", "tick_resolution", "show_exchange_logos","items_favoriting","show_symbol_logos" ],
disabled_features: ["use_localstorage_for_settings"]
});
let chart;
widget.onChartReady(() => {
chart = widget.activeChart();
chart.onSymbolChanged().subscribe(null, function(newSymbol) {
if (!newSymbol || !newSymbol.name) return;
if (newSymbol.exchange) currentExchange = newSymbol.exchange;
else if (newSymbol.name.includes(':')) currentExchange = newSymbol.name.split(':')[0];
let symbolToSend = newSymbol.name.includes(':') ? newSymbol.name : `${currentExchange}:${newSymbol.name}`;
console.log("🔄 Symbol Changed to:", symbolToSend);
lastBarCache = {};
fetch("https://localhost:5001/api/ChartEvents/SymbolChanged", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
symbol_name: symbolToSend,
full_name: newSymbol.full_name,
exchange: currentExchange,
})
})
.then(res => res.json())
.then(data => console.log("✅ Server Response:", data))
.catch(err => console.error("❌ API Error:", err));
if (liveWs && liveWs.readyState === WebSocket.OPEN) {
liveWs.send(JSON.stringify({
type: "symbol_changed",
symbol_name: symbolToSend
}));
}
});
});
}
window.addEventListener("DOMContentLoaded", initChart);
</script>