import { Dispatch, SetStateAction, useEffect, useState } from 'react' import mangoStore from '@store/mangoStore' import klinecharts, { init, dispose, KLineData } from 'klinecharts' import { useViewport } from 'hooks/useViewport' import usePrevious from '@components/shared/usePrevious' import Modal from '@components/shared/Modal' import Switch from '@components/forms/Switch' import { BASE_CHART_QUERY, CHART_QUERY, DEFAULT_MAIN_INDICATORS, DEFAULT_SUB_INDICATOR, mainTechnicalIndicatorTypes, MAIN_INDICATOR_CLASS, ONE_DAY_SECONDS, RES_NAME_TO_RES_VAL, subTechnicalIndicatorTypes, } from 'utils/kLineChart' import Loading from '@components/shared/Loading' import clsx from 'clsx' import { useTheme } from 'next-themes' import { COLORS } from 'styles/colors' import { IconButton } from '@components/shared/Button' import { ArrowsPointingOutIcon, XMarkIcon } from '@heroicons/react/20/solid' import { queryBars } from 'apis/birdeye/datafeed' import { getNextBarTime, parseResolution, socketUrl, } from 'apis/birdeye/helpers' type Props = { setIsFullView?: Dispatch> isFullView?: boolean } const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => { const { theme } = useTheme() const styles = { grid: { show: false, }, candle: { bar: { upColor: COLORS.UP[theme], downColor: COLORS.DOWN[theme], }, tooltip: { labels: ['', 'O:', 'C:', 'H:', 'L:', 'V:'], text: { size: 12, family: 'TT Mono', weight: 'normal', color: COLORS.FGD4[theme], marginLeft: 8, marginTop: 6, marginRight: 8, marginBottom: 0, }, }, priceMark: { show: true, high: { show: true, color: COLORS.FGD4[theme], textMargin: 5, textSize: 10, textFamily: 'TT Mono', textWeight: 'normal', }, low: { show: true, color: COLORS.FGD4[theme], textMargin: 5, textSize: 10, textFamily: 'TT Mono', textWeight: 'normal', }, last: { show: true, upColor: COLORS.BKG4[theme], downColor: COLORS.BKG4[theme], noChangeColor: COLORS.BKG4[theme], line: { show: true, // 'solid'|'dash' style: 'dash', dashValue: [4, 4], size: 1, }, text: { show: true, size: 10, paddingLeft: 2, paddingTop: 2, paddingRight: 2, paddingBottom: 2, color: '#FFFFFF', family: 'TT Mono', weight: 'normal', borderRadius: 2, }, }, }, }, xAxis: { axisLine: { show: true, color: COLORS.BKG4[theme], size: 1, }, tickLine: { show: true, size: 1, length: 3, color: COLORS.BKG4[theme], }, tickText: { show: true, color: COLORS.FGD4[theme], family: 'TT Mono', weight: 'normal', size: 10, }, }, yAxis: { axisLine: { show: true, color: COLORS.BKG4[theme], size: 1, }, tickLine: { show: true, size: 1, length: 3, color: COLORS.BKG4[theme], }, tickText: { show: true, color: COLORS.FGD4[theme], family: 'TT Mono', weight: 'normal', size: 10, }, }, crosshair: { show: true, horizontal: { show: true, line: { show: true, style: 'dash', dashValue: [4, 2], size: 1, color: COLORS.FGD4[theme], }, text: { show: true, color: '#FFFFFF', size: 10, family: 'TT Mono', weight: 'normal', paddingLeft: 2, paddingRight: 2, paddingTop: 2, paddingBottom: 2, borderSize: 1, borderColor: COLORS.FGD4[theme], borderRadius: 2, backgroundColor: COLORS.FGD4[theme], }, }, vertical: { show: true, line: { show: true, style: 'dash', dashValue: [4, 2], size: 1, color: COLORS.FGD4[theme], }, text: { show: true, color: '#FFFFFF', size: 10, family: 'TT Mono', weight: 'normal', paddingLeft: 2, paddingRight: 2, paddingTop: 2, paddingBottom: 2, borderSize: 1, borderColor: COLORS.FGD4[theme], borderRadius: 2, backgroundColor: COLORS.FGD4[theme], }, }, }, technicalIndicator: { margin: { top: 0.2, bottom: 0.1, }, bar: { upColor: COLORS.UP[theme], downColor: COLORS.DOWN[theme], noChangeColor: '#888888', }, line: { size: 1, colors: ['#FF9600', '#9D65C9', '#2196F3', '#E11D74', '#01C5C4'], }, circle: { upColor: '#26A69A', downColor: '#EF5350', noChangeColor: '#888888', }, lastValueMark: { show: false, text: { show: false, color: '#ffffff', size: 12, family: 'Helvetica Neue', weight: 'normal', paddingLeft: 3, paddingTop: 2, paddingRight: 3, paddingBottom: 2, borderRadius: 2, }, }, tooltip: { // 'always' | 'follow_cross' | 'none' showRule: 'always', // 'standard' | 'rect' showType: 'standard', showName: true, showParams: true, defaultValue: 'n/a', text: { size: 12, family: 'TT Mono', weight: 'normal', color: COLORS.FGD4[theme], marginTop: 6, marginRight: 8, marginBottom: 0, marginLeft: 8, }, }, }, separator: { size: 2, color: COLORS.BKG4[theme], }, } const socket = new WebSocket(socketUrl, 'echo-protocol') const unsub_msg = { type: 'UNSUBSCRIBE_PRICE', } const { width } = useViewport() const prevWidth = usePrevious(width) const selectedMarket = mangoStore((s) => s.selectedMarket.current) const [socketConnected, setSocketConnected] = useState(false) const selectedMarketName = selectedMarket?.name const [isTechnicalModalOpen, setIsTechnicalModalOpen] = useState(false) const [mainTechnicalIndicators, setMainTechnicalIndicators] = useState< string[] >([]) const [subTechnicalIndicators, setSubTechnicalIndicators] = useState<{ //indicatorName: class [indicatorName: string]: string }>({}) const [isLoading, setIsLoading] = useState(false) const [resolution, setResolution] = useState(RES_NAME_TO_RES_VAL['1H']) const [chart, setChart] = useState(null) const previousChart = usePrevious(chart) const [baseChartQuery, setQuery] = useState(null) const fetchData = async ( baseQuery: BASE_CHART_QUERY, from: number, to?: number ) => { try { setIsLoading(true) const query: CHART_QUERY = { ...baseQuery, time_from: from, time_to: to ? to : baseQuery.time_to, } const response = await queryBars(query.address, query.type, { firstDataRequest: false, from: query.time_from, to: query.time_to, }) const dataSize = response.length const dataList = [] for (let i = 0; i < dataSize; i++) { const row = response[i] const kLineModel = { ...row, } dataList.push(kLineModel) } setIsLoading(false) return dataList } catch (e) { setIsLoading(false) console.error(e) return [] } } //update data every 10 secs function setupSocket( kLineChart: klinecharts.Chart, baseQuery: BASE_CHART_QUERY ) { // Connection opened socket.addEventListener('open', (_event) => { console.log('[socket] Kline Connected') }) socket.addEventListener('message', (msg) => { const data = JSON.parse(msg.data) if (data.type === 'WELLCOME') { setSocketConnected(true) socket.send(JSON.stringify(unsub_msg)) const msg = { type: 'SUBSCRIBE_PRICE', data: { chartType: parseResolution(baseQuery.type), address: baseQuery.address, currency: 'pair', }, } socket.send(JSON.stringify(msg)) } if (data.type === 'PRICE_DATA') { const dataList = kLineChart.getDataList() const lastItem = dataList[dataList.length - 1] const currTime = data.data.unixTime * 1000 if (!dataList.length) { return } const lastBar: KLineData & { time: number } = { ...lastItem, time: lastItem.timestamp, } const resolution = parseResolution(baseQuery.type) const nextBarTime = getNextBarTime(lastBar, resolution) let bar: KLineData if (currTime >= nextBarTime) { bar = { timestamp: nextBarTime, open: data.data.o, high: data.data.h, low: data.data.l, close: data.data.c, volume: data.data.v, } } else { bar = { ...lastBar, high: Math.max(lastBar.high, data.data.h), low: Math.min(lastBar.low, data.data.l), close: data.data.c, volume: data.data.v, } } kLineChart.updateData(bar) } }) } const fetchFreshData = async (daysToSubtractFromToday: number) => { const from = Math.floor(Date.now() / 1000) - ONE_DAY_SECONDS * daysToSubtractFromToday const data = await fetchData(baseChartQuery!, from) if (chart) { chart.applyNewData(data) //after we fetch fresh data start to update data every x seconds setupSocket(chart, baseChartQuery!) } } //size change useEffect(() => { if (width !== prevWidth && chart) { //wait for event que to be empty //to have current width setTimeout(() => { chart?.resize() }, 0) } }, [width]) //when base query change we refetch with fresh data useEffect(() => { if (chart && baseChartQuery) { //becuase bird eye send onlu 1k records at one time //we query for lower amounts of days at the start const halfDayThreshold = ['1', '3'] const twoDaysThreshold = ['5', '15', '30'] const daysToSub = halfDayThreshold.includes(baseChartQuery.type) ? 0.5 : twoDaysThreshold.includes(baseChartQuery.type) ? 2 : 5 fetchFreshData(daysToSub) //add callback to fetch more data when zoom out chart.loadMore(async (timestamp: number) => { try { const unixTime = timestamp / 1000 const from = unixTime - ONE_DAY_SECONDS * daysToSub const data = await fetchData(baseChartQuery!, from, unixTime) chart.applyMoreData(data) } catch (e) { console.error('Error fetching new data') } }) } }, [baseChartQuery]) //change query based on market and resolution useEffect(() => { if (selectedMarketName && resolution) { setQuery({ type: resolution.val, address: '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6', time_to: Math.floor(Date.now() / 1000), }) } }, [selectedMarketName, resolution]) // init default technical indicators after init of chart useEffect(() => { if (chart !== null && previousChart === null) { if (DEFAULT_SUB_INDICATOR) { const subId = chart.createTechnicalIndicator( DEFAULT_SUB_INDICATOR, true ) setSubTechnicalIndicators({ [DEFAULT_SUB_INDICATOR]: subId }) } if (DEFAULT_MAIN_INDICATORS?.length) { for (const type of DEFAULT_MAIN_INDICATORS) { chart?.createTechnicalIndicator(type, true, { id: MAIN_INDICATOR_CLASS, }) } setMainTechnicalIndicators(DEFAULT_MAIN_INDICATORS) } } }, [chart !== null]) //init chart without data useEffect(() => { const initKline = async () => { const kLineChart = init('update-k-line') kLineChart.setStyleOptions({ ...styles }) setChart(kLineChart) } initKline() return () => { dispose('update-k-line') if (socketConnected) { console.log('[socket] kline disconnected') socket.send(JSON.stringify(unsub_msg)) socket.close() } } }, []) return (
{Object.keys(RES_NAME_TO_RES_VAL).map((key) => ( ))}
{isLoading && }
{setIsFullView ? ( setIsFullView(!isFullView)} > {isFullView ? ( ) : ( )} ) : null}
setIsTechnicalModalOpen(false)} >

Main Indicator

{mainTechnicalIndicatorTypes.map((type) => { return ( x === type)} onChange={(check) => { if (check) { chart?.createTechnicalIndicator(type, true, { id: MAIN_INDICATOR_CLASS, }) setMainTechnicalIndicators([ ...mainTechnicalIndicators, type, ]) } else { chart?.removeTechnicalIndicator(MAIN_INDICATOR_CLASS, type) setMainTechnicalIndicators([ ...mainTechnicalIndicators.filter((x) => x !== type), ]) } }} > ) })}

Bottom Indicator

{subTechnicalIndicatorTypes.map((type) => { return ( x === type) } onChange={(check) => { if (check) { const subId = chart?.createTechnicalIndicator(type, true) setSubTechnicalIndicators({ ...subTechnicalIndicators, [type]: subId!, }) } else { chart?.removeTechnicalIndicator( subTechnicalIndicators[type], type ) const newItems = { ...subTechnicalIndicators } delete newItems[type] // or whichever key you want setSubTechnicalIndicators(newItems) } }} > ) })}
) } const IndicatorSwitch = ({ type, onChange, checked, }: { type: string onChange: (checked: boolean) => void checked: boolean }) => { return (
{type}
) } export default TradingViewChartKline