tradingview / charting-library-tutorial

This tutorial explains step by step how to connect your data to the Charting Library
MIT License
397 stars 306 forks source link

Could not use drawing tools #63

Closed akshay7892 closed 2 years ago

akshay7892 commented 2 years ago

Hello, A big thank you for creating such a amazing product and making it available to us. I'm a newbie in the world of JS so I'm writing this to the best of knowledge, so please be patient.

Version of TAC = CL v21.066 (internal id 132c7060 @ 2022-04-01T09:59:44.647Z)

I followed the charting-library-tutorial and used those five files i.e. index.html, main.js , datafeed.js, helper.js and streaming.js with some minor modifications like change in url structure of api endpoint, adding volume along with OHLC values to datafeed from feched data, updating timezone and exchange info etc. and it was enough to display data from backend on the chart. I did not altered the main library in any manner as I dont have that level of understanding of the JS. However, now I cant use any of the drawing tools. If I use those five files provided in the tutorial repo, everything works fine but if I use the files that I have modified drawing tools don't work anymore. Also, to be precise to moved those five files in the "charting_library_clonned_data" folder and updated filepaths in main.js and index.html.

Please refer below screenshots and index.html, main.js , datafeed.js, helper.js and streaming.js codes.

I have uploaded TAC.py on this repo file which requires fastapi with uvicorn and if you run it using uvicorn TAC:app --port 8000 --reload it will respond to the exchange and historical data request made by the library . Hope this will help. Please let me know if you need anything. Also, I have uploaded a json for exchange and OHLCV data of 1min for 2000 candles if you want to test it on the repo . History endpoint should be /{d_fysymbol}/{Resolution}/{Timestamp}/{Limit} i.e "/NSE-SBIN-EQ/1/1650707789/2000" and exchange end point should be /exchange.

You can find these files also on this repo.

TAC_BTC-USD_2

I might be completely wrong but looks like it has something to do with those two drawing_events 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".

TAC_SBIN-1

TAC_SBIN-2

<--! index.html-->

<--! index.html-->
<!DOCTYPE HTML>
<html>
    <head>
        <title>TradingView Charting Library example</title>
        <script
            type="text/javascript"
            src="charting_library/charting_library.js">
        </script>
        <script src="https://cdn.socket.io/4.4.1/socket.io.min.js" integrity="sha384-fKnu0iswBIqkjxrhQCTZ7qlLHOFEgNkRmK2vaO/LbTZSXdJfAu6ewRBdwHPhBo/H" crossorigin="anonymous"></script>
        <!-- Custom datafeed module. -->
        <script type="module" src="src/main.js"></script>
    </head>
    <body style="margin:0px;">
        <div id="tv_chart_container">
            <!-- This div will contain the Charting Library widget. -->
        </div>
    </body>
</html>

main.js

//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

//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

//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

//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;
            }
        }
    }
}
romfrancois commented 2 years ago

Closing this one off since a discussion is already started here