mango-v4-ui/components/trade/TradingViewChartKline.tsx

545 lines
16 KiB
TypeScript
Raw Normal View History

2022-12-06 08:41:57 -08:00
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
2022-12-05 09:19:59 -08:00
import mangoStore from '@store/mangoStore'
import klinecharts, { init, dispose } 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'
2022-12-05 15:54:47 -08:00
import {
BASE_CHART_QUERY,
CHART_QUERY,
2022-12-29 10:43:14 -08:00
DEFAULT_MAIN_INDICATORS,
DEFAULT_SUB_INDICATOR,
2022-12-05 15:54:47 -08:00
mainTechnicalIndicatorTypes,
MAIN_INDICATOR_CLASS,
ONE_DAY_SECONDS,
RES_NAME_TO_RES_VAL,
subTechnicalIndicatorTypes,
} from 'utils/kLineChart'
2022-12-29 10:43:14 -08:00
import Loading from '@components/shared/Loading'
2022-12-06 13:58:49 -08:00
import clsx from 'clsx'
2022-12-26 02:13:07 -08:00
import { useTheme } from 'next-themes'
import { COLORS } from 'styles/colors'
import { IconButton } from '@components/shared/Button'
import { ArrowsPointingOutIcon, XMarkIcon } from '@heroicons/react/20/solid'
2023-01-02 11:50:03 -08:00
import { queryBars } from 'apis/birdeye/datafeed'
2022-12-05 09:19:59 -08:00
2022-12-08 06:11:09 -08:00
const UPDATE_INTERVAL = 10000
2022-12-06 08:41:57 -08:00
type Props = {
setIsFullView?: Dispatch<SetStateAction<boolean>>
isFullView?: boolean
}
const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
2022-12-05 09:19:59 -08:00
const { width } = useViewport()
2022-12-26 02:13:07 -08:00
const { theme } = useTheme()
2022-12-05 09:19:59 -08:00
const prevWidth = usePrevious(width)
2022-12-21 09:31:25 -08:00
const selectedMarket = mangoStore((s) => s.selectedMarket.current)
const selectedMarketName = selectedMarket?.name
2022-12-05 09:19:59 -08:00
const [isTechnicalModalOpen, setIsTechnicalModalOpen] = useState(false)
const [mainTechnicalIndicators, setMainTechnicalIndicators] = useState<
string[]
>([])
2022-12-05 15:54:47 -08:00
const [subTechnicalIndicators, setSubTechnicalIndicators] = useState<{
//indicatorName: class
[indicatorName: string]: string
}>({})
2022-12-29 10:43:14 -08:00
const [isLoading, setIsLoading] = useState(false)
2022-12-06 08:41:57 -08:00
const [resolution, setResolution] = useState(RES_NAME_TO_RES_VAL['1H'])
2022-12-05 09:19:59 -08:00
const [chart, setChart] = useState<klinecharts.Chart | null>(null)
2022-12-29 10:43:14 -08:00
const previousChart = usePrevious(chart)
2022-12-05 09:19:59 -08:00
const [baseChartQuery, setQuery] = useState<BASE_CHART_QUERY | null>(null)
const clearTimerRef = useRef<NodeJS.Timeout | null>(null)
const fetchData = async (baseQuery: BASE_CHART_QUERY, from: number) => {
try {
2022-12-29 10:43:14 -08:00
setIsLoading(true)
2022-12-05 09:19:59 -08:00
const query: CHART_QUERY = {
...baseQuery,
2022-12-21 03:21:04 -08:00
time_from: from,
2022-12-05 09:19:59 -08:00
}
2023-01-02 11:50:03 -08:00
const response = await queryBars(query.address, query.type, {
firstDataRequest: false,
from: query.time_from,
to: query.time_to,
2022-12-05 09:19:59 -08:00
})
2023-01-02 11:50:03 -08:00
const dataSize = response.length
2022-12-05 09:19:59 -08:00
const dataList = []
for (let i = 0; i < dataSize; i++) {
2023-01-02 11:50:03 -08:00
const row = response[i]
2022-12-05 09:19:59 -08:00
const kLineModel = {
2023-01-02 11:50:03 -08:00
...row,
2022-12-05 09:19:59 -08:00
}
dataList.push(kLineModel)
}
2022-12-29 10:43:14 -08:00
setIsLoading(false)
2022-12-05 09:19:59 -08:00
return dataList
} catch (e) {
2022-12-29 10:43:14 -08:00
setIsLoading(false)
2022-12-05 09:19:59 -08:00
console.log(e)
return []
}
}
2022-12-08 06:11:09 -08:00
//update data every 10 secs
2022-12-05 09:19:59 -08:00
function updateData(
kLineChart: klinecharts.Chart,
baseQuery: BASE_CHART_QUERY
) {
if (clearTimerRef.current) {
clearInterval(clearTimerRef.current)
}
clearTimerRef.current = setTimeout(async () => {
if (kLineChart) {
2022-12-21 03:21:04 -08:00
const from = baseQuery.time_to - resolution.seconds
2022-12-05 09:19:59 -08:00
const newData = (await fetchData(baseQuery!, from))[0]
if (newData) {
2022-12-08 06:11:09 -08:00
newData.timestamp += UPDATE_INTERVAL
2022-12-05 09:19:59 -08:00
kLineChart.updateData(newData)
updateData(kLineChart, baseQuery)
}
}
2022-12-08 06:11:09 -08:00
}, UPDATE_INTERVAL)
2022-12-05 09:19:59 -08:00
}
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
updateData(chart, baseChartQuery!)
}
}
2022-12-08 06:11:09 -08:00
//size change
2022-12-05 09:19:59 -08:00
useEffect(() => {
if (width !== prevWidth && chart) {
//wait for event que to be empty
//to have current width
setTimeout(() => {
2022-12-05 15:54:47 -08:00
chart?.resize()
2022-12-05 09:19:59 -08:00
}, 0)
}
}, [width])
2022-12-08 06:11:09 -08:00
//when base query change we refetch with fresh data
2022-12-05 09:19:59 -08:00
useEffect(() => {
if (chart && baseChartQuery) {
fetchFreshData(14)
//add callback to fetch more data when zoom out
chart.loadMore(() => {
2022-12-21 09:31:25 -08:00
try {
fetchFreshData(365)
} catch (e) {
console.log('Error fetching new data')
}
chart.loadMore(() => null)
2022-12-05 09:19:59 -08:00
})
}
}, [baseChartQuery])
//change query based on market and resolution
useEffect(() => {
if (selectedMarketName && resolution) {
setQuery({
2022-12-21 03:21:04 -08:00
type: resolution.val,
address: '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6',
time_to: Math.floor(Date.now() / 1000),
2022-12-05 09:19:59 -08:00
})
}
}, [selectedMarketName, resolution])
2022-12-26 02:13:07 -08:00
// init default technical indicators after init of chart
2022-12-29 10:43:14 -08:00
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])
2022-12-08 06:11:09 -08:00
2022-12-05 09:19:59 -08:00
//init chart without data
useEffect(() => {
const initKline = async () => {
const kLineChart = init('update-k-line')
2022-12-05 15:54:47 -08:00
kLineChart.setStyleOptions({
2022-12-05 09:19:59 -08:00
grid: {
2022-12-26 02:13:07 -08:00
show: false,
2022-12-05 09:19:59 -08:00
},
candle: {
2022-12-26 02:13:07 -08:00
bar: {
upColor: COLORS.UP[theme],
downColor: COLORS.DOWN[theme],
},
2022-12-05 09:19:59 -08:00
tooltip: {
2022-12-26 02:13:07 -08:00
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,
},
},
2022-12-05 09:19:59 -08:00
},
},
xAxis: {
axisLine: {
show: true,
2022-12-26 02:13:07 -08:00
color: COLORS.BKG4[theme],
size: 1,
},
tickLine: {
show: true,
2022-12-05 09:19:59 -08:00
size: 1,
2022-12-26 02:13:07 -08:00
length: 3,
color: COLORS.BKG4[theme],
},
tickText: {
show: true,
color: COLORS.FGD4[theme],
family: 'TT Mono',
weight: 'normal',
size: 10,
2022-12-05 09:19:59 -08:00
},
},
yAxis: {
axisLine: {
show: true,
2022-12-26 02:13:07 -08:00
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: {
2022-12-05 09:19:59 -08:00
size: 1,
2022-12-26 02:13:07 -08:00
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,
},
2022-12-05 09:19:59 -08:00
},
},
2022-12-05 15:54:47 -08:00
separator: {
size: 2,
2022-12-26 02:13:07 -08:00
color: COLORS.BKG4[theme],
2022-12-05 15:54:47 -08:00
},
2022-12-05 09:19:59 -08:00
})
setChart(kLineChart)
}
initKline()
return () => {
dispose('update-k-line')
}
}, [])
2022-12-08 06:11:09 -08:00
2022-12-05 09:19:59 -08:00
return (
2022-12-06 08:41:57 -08:00
<div
2022-12-06 13:58:49 -08:00
className={clsx(
2023-01-18 18:42:29 -08:00
'h-full w-full',
2022-12-26 02:13:07 -08:00
isFullView
2023-01-18 18:42:29 -08:00
? 'fixed left-0 top-0 right-0 bottom-0 z-40 bg-th-bkg-1 text-th-fgd-1'
2022-12-26 02:13:07 -08:00
: ''
2022-12-06 13:58:49 -08:00
)}
2022-12-06 08:41:57 -08:00
>
2022-12-26 02:13:07 -08:00
<div className="mb-1 flex w-full items-center justify-between border-b border-th-bkg-3 p-1 text-th-fgd-2">
<div className="flex text-th-fgd-3">
{Object.keys(RES_NAME_TO_RES_VAL).map((key) => (
<button
className={clsx(
'default-transition px-2 md:hover:text-th-fgd-2',
resolution === RES_NAME_TO_RES_VAL[key] && 'text-th-active'
)}
key={key}
onClick={() => setResolution(RES_NAME_TO_RES_VAL[key])}
>
{key}
</button>
))}
<button
className="default-transition px-2 md:hover:text-th-fgd-2"
onClick={() => setIsTechnicalModalOpen(true)}
2022-12-05 09:19:59 -08:00
>
2022-12-26 02:13:07 -08:00
Indicator
</button>
2022-12-29 10:43:14 -08:00
<div className="px-2">
{isLoading && <Loading className="w-4"></Loading>}
</div>
2022-12-05 09:19:59 -08:00
</div>
2022-12-26 02:13:07 -08:00
{setIsFullView ? (
<IconButton
className="text-th-fgd-3"
size="small"
hideBg
onClick={() => setIsFullView(!isFullView)}
>
{isFullView ? (
<XMarkIcon className="h-5 w-5" />
) : (
<ArrowsPointingOutIcon className="h-5 w-5" />
)}
</IconButton>
) : null}
2022-12-05 09:19:59 -08:00
</div>
<div
2022-12-26 02:13:07 -08:00
style={{ height: 'calc(100% - 48px)', width: '100%' }}
2022-12-05 09:19:59 -08:00
id="update-k-line"
className="k-line-chart"
/>
<Modal
isOpen={isTechnicalModalOpen}
onClose={() => setIsTechnicalModalOpen(false)}
>
<div className="hide-scroll flex max-h-96 flex-col overflow-auto text-left">
2022-12-26 02:13:07 -08:00
<h2 className="py-4 text-base">Main Indicator</h2>
2022-12-05 09:19:59 -08:00
{mainTechnicalIndicatorTypes.map((type) => {
return (
<IndicatorSwitch
key={type}
type={type}
2022-12-05 15:54:47 -08:00
checked={!!mainTechnicalIndicators.find((x) => 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),
])
}
}}
2022-12-05 09:19:59 -08:00
></IndicatorSwitch>
)
})}
2022-12-26 02:13:07 -08:00
<h2 className="py-4 text-base">Bottom Indicator</h2>
2022-12-05 09:19:59 -08:00
{subTechnicalIndicatorTypes.map((type) => {
return (
<IndicatorSwitch
key={type}
type={type}
2022-12-05 15:54:47 -08:00
checked={
!!Object.keys(subTechnicalIndicators).find((x) => 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)
}
}}
2022-12-05 09:19:59 -08:00
></IndicatorSwitch>
)
})}
</div>
</Modal>
2022-12-05 15:54:47 -08:00
</div>
2022-12-05 09:19:59 -08:00
)
}
const IndicatorSwitch = ({
type,
2022-12-05 15:54:47 -08:00
onChange,
checked,
2022-12-05 09:19:59 -08:00
}: {
type: string
2022-12-05 15:54:47 -08:00
onChange: (checked: boolean) => void
checked: boolean
2022-12-05 09:19:59 -08:00
}) => {
return (
<div
className="flex justify-between border-t border-th-bkg-3 p-4 text-th-fgd-4"
key={type}
>
{type}
2022-12-05 15:54:47 -08:00
<Switch checked={checked} onChange={onChange} />
2022-12-05 09:19:59 -08:00
</div>
)
}
export default TradingViewChartKline