Please refer below screenshots and index.html, main.js , datafeed.js, helper.js and streaming.js codes.
I might be completely wrong but looks like it has something to do with those two drawing_event
s with same ids in above screenshot. When I try to draw a trendline onSelectedLineToolChanged
along with other mouse events are getting triggered but drawing_event
isn't in the below image. Also, I could tell that trend line is being drawn on the chart by looking at light blue selected region both on time scale and price scale, as well as the enabled "Remove Drawings" option in the right click menu, but it does not show up in the "object tree". Also, tv-floating-toolbar tv-grouped-floating-toolbar ui-draggable
div isn't in the "tv-chart-container".
//main.js
// Datafeed implementation, will be added later//
import Datafeed from './datafeed.js';
window.tvWidget = new TradingView.widget({
symbol: 'NSE:SBIN-EQ', // default symbol
interval: '1', // default interval
fullscreen: true, // displays the chart in the fullscreen mode
container: 'tv_chart_container',
timezone: 'Asia/Kolkata',
datafeed: Datafeed,
library_path: 'charting_library/',
debug: true,
});
//helpers.js
// Make requests to CryptoCompare API
export async function makeApiRequest(path) {
try {
const response = await fetch(`http://localhost:8000/${path}`);
return response.json();
} catch (error) {
throw new Error(`LocalHost request error: ${error.status}`);
}
}
// Generate a symbol ID from a pair of the coins
export function generateSymbol(exchange, fromSymbol, toSymbol) {
const short = `${fromSymbol}`;
const type = `${toSymbol}`;
const med = `${fromSymbol}-${toSymbol}`;
return {
short,
med,
type,
full: `${exchange}:${med}`,
};
}
export function decodeQueryParam(p) {
return decodeURIComponent(p.replace(/\+/g, ' '));
}
export function parseFullSymbol(fullSymbol) {
const match = fullSymbol.match(/^(\w+):(\w+)\-(\w+)$/);
if (!match) {
return null;
}
return {
exchange: match[1],
fromSymbol: match[2],
toSymbol: match[3],
fysymbol : `${match[1]}-${match[2]}-${match[3]}`,
};
}
//datafeed.js
import {
makeApiRequest,
generateSymbol,
parseFullSymbol,
decodeQueryParam,
} from './helpers.js';
import {
subscribeOnStream,
unsubscribeFromStream,
} from './streaming.js';
const lastBarsCache = new Map();
const configurationData = {
supported_resolutions: ['1','5','10','15','30','45','60','1D', '1W', '1M'],
//supported_resolutions: ['1'],
exchanges: [{
value: 'NSE',
name: 'NSE',
desc: 'National Stock Exchange',
},],
symbols_types: [{
// `symbolType` argument for the `searchSymbols` method, if a user selects this symbol type
name: 'EQ',
value: 'EQ',
// `symbolType` argument for the `searchSymbols` method, if a user selects this symbol type
},
{
// `symbolType` argument for the `searchSymbols` method, if a user selects this symbol type
name: 'Index',
value: 'Index',
},
],
};
async function getAllSymbols() {
const data = await makeApiRequest('exchange');
let allSymbols = [];
for (const exchange of configurationData.exchanges) {
const pairs = data.Data[exchange.value].pairs;
for (const leftPairPart of Object.keys(pairs)) {
const symbols = pairs[leftPairPart].map(rightPairPart => {
const symbol = generateSymbol(exchange.value, leftPairPart, rightPairPart);
return {
symbol: symbol.short,
full_name: symbol.full,
description: symbol.short,
exchange: exchange.value,
type: symbol.type,
};
});
allSymbols = [...allSymbols, ...symbols];
}
}
return allSymbols;
}
export default {
onReady: (callback) => {
console.log('[onReady]: Method call');
setTimeout(() => callback(configurationData));
},
searchSymbols: async (
userInput,
exchange,
symbolType,
onResultReadyCallback,
) => {
console.log('[searchSymbols]: Method call');
const symbols = await getAllSymbols();
console.log('symbols',symbols);
const newSymbols = symbols.filter(symbol => {
const isExchangeValid = exchange === '' || symbol.exchange === exchange;
const isFullSymbolContainsInput = symbol.full_name
.toLowerCase()
.indexOf(userInput.toLowerCase()) !== -1;
return isExchangeValid && isFullSymbolContainsInput;
});
onResultReadyCallback(newSymbols);
},
resolveSymbol: async (
symbolName,
onSymbolResolvedCallback,
onResolveErrorCallback,
) => {
console.log('[resolveSymbol]: Method call', symbolName);
const symbols = await getAllSymbols();
const symbolItem = symbols.find(({
full_name,
}) => full_name === symbolName);
if (!symbolItem) {
console.log('[resolveSymbol]: Cannot resolve symbol', symbolName);
onResolveErrorCallback('cannot resolve symbol');
return;
}
const symbolInfo = {
ticker: symbolItem.full_name,
name: symbolItem.symbol,
full_name : symbolItem.full_name,
description: symbolItem.description,
type: symbolItem.type,
session: '0915-1530',
timezone: 'Asia/Kolkata',
exchange: symbolItem.exchange,
minmov: 1,
pricescale: 100,
has_intraday: true,
intraday_multipliers: ['1','15'],
has_daily: true,
daily_multipliers : ['1'],
has_no_volume: false,
has_weekly_and_monthly: false,
visible_plots_set : "ohlcv",
original_currency_code : "INR",
supported_resolutions: ['1','5','10','15','30','45','60','1D', '1W', '1M'],
volume_precision: 2,
data_status: 'streaming',
};
console.log('[resolveSymbol]: Symbol resolved', symbolName);
onSymbolResolvedCallback(symbolInfo);
},
getBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => {
const { from, to, firstDataRequest } = periodParams;
console.log('[getBars]: Method call', symbolInfo, resolution, from, to);
const parsedSymbol = parseFullSymbol(symbolInfo.ticker);
console.log("parsedSymbol=",parsedSymbol);
const urlParameters = {
//e: parsedSymbol.exchange,
//fsym: parsedSymbol.fromSymbol,
//tsym: parsedSymbol.toSymbol,
fys : parsedSymbol.fysymbol,
//fromTs:from,
rr: resolution,
toTs: to,
//countBack : countBack,
limit: 2000,
};
const query = Object.keys(urlParameters)
.map(name => `${encodeURIComponent(urlParameters[name])}`)
.join('/');
try {
const data = await makeApiRequest(`${query}`);
console.log(`[getBars]: returned `,data);
if (data.Response && data.Response === 'Error' || data.Data.length === 0) {
// "noData" should be set if there is no data in the requested period.
onHistoryCallback([], {
noData: true,
});
return;
}
let bars = [];
data.Data.forEach(bar => {
//console.log('Data from HistAPI',data.Data);
if (bar.time < to) {
bars = [...bars, {
time: bar.time * 1000,
low: bar.low,
high: bar.high,
open: bar.open,
close: bar.close,
volume: bar.volume,
}];
}
//console.log("bar.close",bar.close,"bar.volume",bar.volume);
});
console.log('firstDataRequest',firstDataRequest);
if (firstDataRequest) {
lastBarsCache.set(symbolInfo.full_name, {
...bars[bars.length - 1],
});
console.log('firstDataRequest',firstDataRequest);
}
console.log(`[getBars]: returned ${bars.length} bar(s)`);
onHistoryCallback(bars, {
noData: false,
});
} catch (error) {
console.log('[getBars]: Get error', error);
onErrorCallback(error);
}
},
subscribeBars: (
symbolInfo,
resolution,
onRealtimeCallback,
subscribeUID,
onResetCacheNeededCallback,
) => {
console.log('[subscribeBars]: Method call with subscribeUID:', subscribeUID);
subscribeOnStream(
symbolInfo,
resolution,
onRealtimeCallback,
subscribeUID,
onResetCacheNeededCallback,
lastBarsCache.get(symbolInfo.full_name),
);
},
unsubscribeBars: (subscriberUID) => {
console.log('[unsubscribeBars]: Method call with subscriberUID:', subscriberUID);
unsubscribeFromStream(subscriberUID);
},
};
//streaming.js
import { parseFullSymbol } from './helpers.js';
//const socket = io('wss://streamer.cryptocompare.com');
//const socket = io();
//{transports: ['websocket', 'polling', 'flashsocket']}
//);
//const socket = io(/ws/)
const socket = io('http://localhost:9000', {
transports: ['websocket', 'polling', 'flashsocket']
});
const channelToSubscription = new Map();
console.log('ChannelToSubscription=',channelToSubscription ),
socket.on('connect', () => {
console.log('[socket] Connected');
});
socket.on('disconnect', (reason) => {
console.log('[socket] Disconnected:', reason);
});
socket.on('error', (error) => {
console.log('[socket] Error:', error);
});
socket.on('m', data => {
console.log('[socket] Message:', data);
const [
eventTypeStr,
exchange,
fromSymbol,
toSymbol,
,
,
tradeTimeStr,
,
tradePriceStr,
] = data.split('~');
if (parseInt(eventTypeStr) !== 0) {
// skip all non-TRADE events
return;
}
const tradePrice = parseFloat(tradePriceStr);
const tradeTime = parseInt(tradeTimeStr);
const channelString = `${exchange}-${fromSymbol}-${toSymbol}`;
const subscriptionItem = channelToSubscription.get(channelString);
if (subscriptionItem === undefined) {
return;
}
const lastDailyBar = subscriptionItem.lastDailyBar;
const nextDailyBarTime = getNextDailyBarTime(lastDailyBar.time);
let bar;
if (tradeTime >= nextDailyBarTime) {
bar = {
time: nextDailyBarTime,
open: tradePrice,
high: tradePrice,
low: tradePrice,
close: tradePrice,
};
console.log('[socket] Generate new bar', bar);
} else {
bar = {
...lastDailyBar,
high: Math.max(lastDailyBar.high, tradePrice),
low: Math.min(lastDailyBar.low, tradePrice),
close: tradePrice,
};
console.log('[socket] Update the latest bar by price', tradePrice);
}
subscriptionItem.lastDailyBar = bar;
// send data to every subscriber of that symbol
subscriptionItem.handlers.forEach(handler => handler.callback(bar));
});
function getNextDailyBarTime(barTime) {
const date = new Date(barTime * 1000);
date.setDate(date.getDate() + 1);
return date.getTime() / 1000;
}
export function subscribeOnStream(
symbolInfo,
resolution,
onRealtimeCallback,
subscribeUID,
onResetCacheNeededCallback,
lastDailyBar,
) {
const parsedSymbol = parseFullSymbol(symbolInfo.full_name);
const channelString = `${parsedSymbol.exchange}-${parsedSymbol.fromSymbol}-${parsedSymbol.toSymbol}`;
console.log("chaneelString=",channelString)
const handler = {
id: subscribeUID,
callback: onRealtimeCallback,
};
let subscriptionItem = channelToSubscription.get(channelString);
if (subscriptionItem) {
// already subscribed to the channel, use the existing subscription
subscriptionItem.handlers.push(handler);
return;
}
subscriptionItem = {
subscribeUID,
resolution,
lastDailyBar,
handlers: [handler],
};
channelToSubscription.set(channelString, subscriptionItem);
console.log('[subscribeBars]: Subscribe to streaming. Channel:', channelString);
socket.emit('SubAdd', { subs: [channelString] });
}
export function unsubscribeFromStream(subscriberUID) {
// find a subscription with id === subscriberUID
for (const channelString of channelToSubscription.keys()) {
const subscriptionItem = channelToSubscription.get(channelString);
const handlerIndex = subscriptionItem.handlers
.findIndex(handler => handler.id === subscriberUID);
if (handlerIndex !== -1) {
// remove from handlers
subscriptionItem.handlers.splice(handlerIndex, 1);
if (subscriptionItem.handlers.length === 0) {
// unsubscribe from the channel, if it was the last handler
console.log('[unsubscribeBars]: Unsubscribe from streaming. Channel:', channelString);
socket.emit('SubRemove', { subs: [channelString] });
channelToSubscription.delete(channelString);
break;
}
}
}
}