technical indc

This commit is contained in:
Adrian Brzeziński 2022-12-05 18:19:59 +01:00
parent 72844038db
commit 2ec44f1742
6 changed files with 511 additions and 171 deletions

View File

@ -23,7 +23,7 @@ function Modal({
className="relative z-50 overflow-y-auto" className="relative z-50 overflow-y-auto"
> >
<div <div
className={`fixed inset-0 backdrop-blur-sm backdrop-brightness-75 ${ className={`fixed inset-0 bg-black opacity-50 ${
disableOutsideClose ? 'pointer-events-none' : '' disableOutsideClose ? 'pointer-events-none' : ''
}`} }`}
aria-hidden="true" aria-hidden="true"

View File

@ -16,7 +16,11 @@ import { useWallet } from '@solana/wallet-adapter-react'
import TradeOnboardingTour from '@components/tours/TradeOnboardingTour' import TradeOnboardingTour from '@components/tours/TradeOnboardingTour'
import FavoriteMarketsBar from './FavoriteMarketsBar' import FavoriteMarketsBar from './FavoriteMarketsBar'
const TradingViewChart = dynamic(() => import('./TradingViewChart'), { //const TradingViewChart = dynamic(() => import('./TradingViewChart'), {
// ssr: false,
//})
const TradingViewChartKline = dynamic(() => import('./TradingViewChartKline'), {
ssr: false, ssr: false,
}) })
@ -179,7 +183,7 @@ const TradeAdvancedPage = () => {
className="h-full border border-x-0 border-th-bkg-3" className="h-full border border-x-0 border-th-bkg-3"
> >
<div className={`relative h-full overflow-auto`}> <div className={`relative h-full overflow-auto`}>
<TradingViewChart /> <TradingViewChartKline />
</div> </div>
</div> </div>
<div key="balances"> <div key="balances">

View File

@ -1,182 +1,194 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useMemo, useState } from 'react'
import { useTheme } from 'next-themes'
import {
widget,
ChartingLibraryWidgetOptions,
IChartingLibraryWidget,
ResolutionString,
} from '@public/charting_library'
import mangoStore from '@store/mangoStore' import mangoStore from '@store/mangoStore'
import { CHART_DATA_FEED } from 'utils/constants' import { useViewport } from 'hooks/useViewport'
import { init, dispose } from 'klinecharts' import { CHART_DATA_FEED, DEFAULT_MARKET_NAME } from 'utils/constants'
import axios from 'axios' import { breakpoints } from 'utils/theme'
import { COLORS } from 'styles/colors'
const ONE_HOUR_MINS = 60 export interface ChartContainerProps {
container: ChartingLibraryWidgetOptions['container']
const RES_NAME_TO_RES_VAL: { symbol: ChartingLibraryWidgetOptions['symbol']
[key: string]: { interval: ChartingLibraryWidgetOptions['interval']
val: string datafeedUrl: string
seconds: number libraryPath: ChartingLibraryWidgetOptions['library_path']
} chartsStorageUrl: ChartingLibraryWidgetOptions['charts_storage_url']
} = { chartsStorageApiVersion: ChartingLibraryWidgetOptions['charts_storage_api_version']
'1m': { val: '1', seconds: 60 }, clientId: ChartingLibraryWidgetOptions['client_id']
'5m': { val: '5', seconds: 5 * 60 }, userId: ChartingLibraryWidgetOptions['user_id']
'30m': { fullscreen: ChartingLibraryWidgetOptions['fullscreen']
val: `${ONE_HOUR_MINS / 2}`, autosize: ChartingLibraryWidgetOptions['autosize']
seconds: (ONE_HOUR_MINS / 2) * 60, studiesOverrides: ChartingLibraryWidgetOptions['studies_overrides']
}, theme: string
'1H': { val: `${ONE_HOUR_MINS}`, seconds: ONE_HOUR_MINS * 60 },
'2H': { val: `${2 * ONE_HOUR_MINS}`, seconds: ONE_HOUR_MINS * 2 * 60 },
'4H': { val: `${4 * ONE_HOUR_MINS}`, seconds: ONE_HOUR_MINS * 4 * 60 },
'1D': { val: '1D', seconds: 24 * ONE_HOUR_MINS * 60 },
}
type BASE_CHART_QUERY = {
resolution: string
symbol: string
to: number
}
type CHART_QUERY = BASE_CHART_QUERY & {
from: number
}
type HISTORY = {
c: string[]
h: string[]
l: string[]
o: string[]
t: number[]
v: string[]
} }
const TradingViewChart = () => { const TradingViewChart = () => {
//const { theme } = useTheme() const { theme } = useTheme()
const { width } = useViewport()
const [chartReady, setChartReady] = useState(false)
const selectedMarketName = mangoStore((s) => s.selectedMarket.current?.name) const selectedMarketName = mangoStore((s) => s.selectedMarket.current?.name)
const [resolution, setResultion] = useState(RES_NAME_TO_RES_VAL['1H']) const isMobile = width ? width < breakpoints.sm : false
const [chart, setChart] = useState<klinecharts.Chart | null>(null)
const [baseChartQuery, setQuery] = useState<BASE_CHART_QUERY | null>(null) const defaultProps = useMemo(
const clearTimerRef = useRef<NodeJS.Timeout | null>(null) () => ({
const fetchData = async (baseQuery: BASE_CHART_QUERY, from: number) => { symbol: DEFAULT_MARKET_NAME,
const query: CHART_QUERY = { interval: '60' as ResolutionString,
...baseQuery, theme: 'Dark',
from, container: 'tv_chart_container',
} datafeedUrl: CHART_DATA_FEED,
const response = await axios.get(`${CHART_DATA_FEED}/history`, { libraryPath: '/charting_library/',
params: query, fullscreen: false,
}) autosize: true,
const newData = response.data as HISTORY studiesOverrides: {
const dataSize = newData.t.length 'volume.volume.color.0': COLORS.DOWN[theme],
const dataList = [] 'volume.volume.color.1': COLORS.UP[theme],
for (let i = 0; i < dataSize; i++) { 'volume.precision': 4,
const kLineModel = { },
open: parseFloat(newData.o[i]), }),
low: parseFloat(newData.l[i]), [theme]
high: parseFloat(newData.h[i]), )
close: parseFloat(newData.c[i]),
volume: parseFloat(newData.v[i]), const tvWidgetRef = useRef<IChartingLibraryWidget | null>(null)
timestamp: newData.t[i] * 1000,
} let chartStyleOverrides = {
dataList.push(kLineModel) 'paneProperties.background': 'rgba(0,0,0,0)',
} 'paneProperties.backgroundType': 'solid',
return dataList 'paneProperties.legendProperties.showBackground': false,
'paneProperties.vertGridProperties.color': 'rgba(0,0,0,0)',
'paneProperties.horzGridProperties.color': 'rgba(0,0,0,0)',
'paneProperties.legendProperties.showStudyTitles': false,
'scalesProperties.showStudyLastValue': false,
'scalesProperties.fontSize': 11,
} }
function updateData( const mainSeriesProperties = [
kLineChart: klinecharts.Chart, 'candleStyle',
baseQuery: BASE_CHART_QUERY 'hollowCandleStyle',
) { 'haStyle',
if (clearTimerRef.current) { 'barStyle',
clearInterval(clearTimerRef.current) ]
}
clearTimerRef.current = setTimeout(async () => { mainSeriesProperties.forEach((prop) => {
if (kLineChart) { chartStyleOverrides = {
const from = baseQuery.to - resolution.seconds ...chartStyleOverrides,
const newData = (await fetchData(baseQuery!, from))[0] [`mainSeriesProperties.${prop}.barColorsOnPrevClose`]: true,
newData.timestamp += 10000 [`mainSeriesProperties.${prop}.drawWick`]: true,
kLineChart.updateData(newData) [`mainSeriesProperties.${prop}.drawBorder`]: true,
updateData(kLineChart, baseQuery) [`mainSeriesProperties.${prop}.upColor`]: COLORS.UP[theme],
} [`mainSeriesProperties.${prop}.downColor`]: COLORS.DOWN[theme],
}, 10000) [`mainSeriesProperties.${prop}.borderColor`]: COLORS.UP[theme],
[`mainSeriesProperties.${prop}.borderUpColor`]: COLORS.UP[theme],
[`mainSeriesProperties.${prop}.borderDownColor`]: COLORS.DOWN[theme],
[`mainSeriesProperties.${prop}.wickUpColor`]: COLORS.UP[theme],
[`mainSeriesProperties.${prop}.wickDownColor`]: COLORS.DOWN[theme],
} }
})
useEffect(() => { useEffect(() => {
const fetchFreshData = async () => { if (tvWidgetRef.current && chartReady && selectedMarketName) {
const from = Math.floor(Date.now() / 1000) - 60 * 60 * 24 * 365 tvWidgetRef.current.setSymbol(
const data = await fetchData(baseChartQuery!, from) selectedMarketName!,
chart?.applyNewData(data) tvWidgetRef.current.activeChart().resolution(),
updateData(chart!, baseChartQuery!) () => {
return
} }
if (chart && baseChartQuery) { )
fetchFreshData()
} }
}, [baseChartQuery]) }, [selectedMarketName, chartReady])
useEffect(() => { useEffect(() => {
if (selectedMarketName && resolution) { if (window) {
setQuery({ const widgetOptions: ChartingLibraryWidgetOptions = {
resolution: resolution.val, // debug: true,
symbol: selectedMarketName, symbol: defaultProps.symbol,
to: Math.floor(Date.now() / 1000), // BEWARE: no trailing slash is expected in feed URL
}) // tslint:disable-next-line:no-any
} datafeed: new (window as any).Datafeeds.UDFCompatibleDatafeed(
}, [selectedMarketName, resolution]) defaultProps.datafeedUrl
),
interval:
defaultProps.interval as ChartingLibraryWidgetOptions['interval'],
container:
defaultProps.container as ChartingLibraryWidgetOptions['container'],
library_path: defaultProps.libraryPath as string,
locale: 'en',
enabled_features: ['hide_left_toolbar_by_default'],
disabled_features: [
'use_localstorage_for_settings',
'timeframes_toolbar',
isMobile ? 'left_toolbar' : '',
'show_logo_on_all_charts',
'caption_buttons_text_if_possible',
'header_settings',
// 'header_chart_type',
'header_compare',
'compare_symbol',
'header_screenshot',
// 'header_widget_dom_node',
// 'header_widget',
'header_saveload',
'header_undo_redo',
'header_interval_dialog_button',
'show_interval_dialog_on_key_press',
'header_symbol_search',
'popup_hints',
],
fullscreen: defaultProps.fullscreen,
autosize: defaultProps.autosize,
studies_overrides: defaultProps.studiesOverrides,
theme:
theme === 'Light' || theme === 'Banana' || theme === 'Lychee'
? 'Light'
: 'Dark',
custom_css_url: '/styles/tradingview.css',
loading_screen: {
backgroundColor:
theme === 'Dark'
? COLORS.BKG1.Dark
: theme === 'Light'
? COLORS.BKG1.Light
: theme === 'Mango Classic'
? COLORS.BKG1['Mango Classic']
: theme === 'Medium'
? COLORS.BKG1.Medium
: theme === 'Avocado'
? COLORS.BKG1.Avocado
: theme === 'Blueberry'
? COLORS.BKG1.Blueberry
: theme === 'Banana'
? COLORS.BKG1.Banana
: theme === 'Lychee'
? COLORS.BKG1.Lychee
: theme === 'Olive'
? COLORS.BKG1.Olive
: COLORS.BKG1['High Contrast'],
},
overrides: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
useEffect(() => { ...chartStyleOverrides,
const initKline = async () => { },
const style = getComputedStyle(document.body) }
const gridColor = style.getPropertyValue('--bkg-3')
const kLineChart = init('update-k-line')
kLineChart.setStyleOptions({ const tvWidget = new widget(widgetOptions)
grid: { tvWidgetRef.current = tvWidget
show: true,
horizontal: { tvWidgetRef.current.onChartReady(function () {
color: gridColor, setChartReady(true)
},
vertical: {
color: gridColor,
},
},
candle: {
tooltip: {
labels: ['T: ', 'O: ', 'C: ', 'H: ', 'L: ', 'V: '],
},
},
xAxis: {
axisLine: {
show: true,
color: gridColor,
size: 1,
},
},
yAxis: {
axisLine: {
show: true,
color: gridColor,
size: 1,
},
},
}) })
setChart(kLineChart) //eslint-disable-next-line
} }
initKline() }, [theme, isMobile, defaultProps])
return () => {
dispose('update-k-line')
}
}, [])
return ( return (
<> <div id={defaultProps.container as string} className="tradingview-chart" />
<div className="flex">
{Object.keys(RES_NAME_TO_RES_VAL).map((key) => (
<div
className="cursor-pointer py-1 px-2"
key={key}
onClick={() => setResultion(RES_NAME_TO_RES_VAL[key])}
>
{key}
</div>
))}
</div>
<div
style={{ height: 'calc(100% - 30px)' }}
id="update-k-line"
className="k-line-chart"
/>
</>
) )
} }

View File

@ -0,0 +1,324 @@
import { useEffect, useRef, useState } from 'react'
import mangoStore from '@store/mangoStore'
import { CHART_DATA_FEED } from 'utils/constants'
import klinecharts, { init, dispose } from 'klinecharts'
import axios from 'axios'
import { useViewport } from 'hooks/useViewport'
import usePrevious from '@components/shared/usePrevious'
import Modal from '@components/shared/Modal'
import Switch from '@components/forms/Switch'
const ONE_HOUR_MINS = 60
const ONE_MINUTE_SECONDS = 60
const ONE_HOUR_SECONDS = ONE_HOUR_MINS * ONE_MINUTE_SECONDS
const ONE_DAY_SECONDS = ONE_HOUR_SECONDS * 24
type BASE_CHART_QUERY = {
resolution: string
symbol: string
to: number
}
type CHART_QUERY = BASE_CHART_QUERY & {
from: number
}
type HISTORY = {
c: string[]
h: string[]
l: string[]
o: string[]
t: number[]
v: string[]
}
//Translate values that api accepts to chart seconds
const RES_NAME_TO_RES_VAL: {
[key: string]: {
val: string
seconds: number
}
} = {
'1m': { val: '1', seconds: ONE_MINUTE_SECONDS },
'5m': { val: '5', seconds: 5 * ONE_MINUTE_SECONDS },
'30m': {
val: `${ONE_HOUR_MINS / 2}`,
seconds: (ONE_HOUR_MINS / 2) * ONE_MINUTE_SECONDS,
},
'1H': { val: `${ONE_HOUR_MINS}`, seconds: ONE_HOUR_SECONDS },
'2H': { val: `${2 * ONE_HOUR_MINS}`, seconds: ONE_HOUR_SECONDS * 2 },
'4H': { val: `${4 * ONE_HOUR_MINS}`, seconds: ONE_HOUR_SECONDS * 4 },
'1D': { val: '1D', seconds: 24 * ONE_HOUR_SECONDS },
}
const mainTechnicalIndicatorTypes = [
'MA',
'EMA',
'SAR',
'BOLL',
'SMA',
'BBI',
'TRIX',
]
const subTechnicalIndicatorTypes = [
'VOL',
'MACD',
'RSI',
'KDJ',
'OBV',
'CCI',
'WR',
'DMI',
'MTM',
'EMV',
]
const TradingViewChartKline = () => {
const { width } = useViewport()
const prevWidth = usePrevious(width)
const selectedMarketName = mangoStore((s) => s.selectedMarket.current?.name)
const [isTechnicalModalOpen, setIsTechnicalModalOpen] = useState(false)
const [mainTechnicalIndicators, setMainTechnicalIndicators] = useState<
string[]
>([])
const [resolution, setResultion] = useState(RES_NAME_TO_RES_VAL['1H'])
const [chart, setChart] = useState<klinecharts.Chart | null>(null)
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 {
const query: CHART_QUERY = {
...baseQuery,
from,
}
const response = await axios.get(`${CHART_DATA_FEED}/history`, {
params: query,
})
const newData = response.data as HISTORY
const dataSize = newData.t.length
const dataList = []
for (let i = 0; i < dataSize; i++) {
const kLineModel = {
open: parseFloat(newData.o[i]),
low: parseFloat(newData.l[i]),
high: parseFloat(newData.h[i]),
close: parseFloat(newData.c[i]),
volume: parseFloat(newData.v[i]),
timestamp: newData.t[i] * 1000,
}
dataList.push(kLineModel)
}
return dataList
} catch (e) {
console.log(e)
return []
}
}
function updateData(
kLineChart: klinecharts.Chart,
baseQuery: BASE_CHART_QUERY
) {
if (clearTimerRef.current) {
clearInterval(clearTimerRef.current)
}
clearTimerRef.current = setTimeout(async () => {
if (kLineChart) {
const from = baseQuery.to - resolution.seconds
const newData = (await fetchData(baseQuery!, from))[0]
if (newData) {
newData.timestamp += 10000
kLineChart.updateData(newData)
updateData(kLineChart, baseQuery)
}
}
}, 10000)
}
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!)
}
}
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 fresh data
useEffect(() => {
if (chart && baseChartQuery) {
fetchFreshData(14)
//add callback to fetch more data when zoom out
chart.loadMore(() => {
fetchFreshData(365)
})
}
}, [baseChartQuery])
//change query based on market and resolution
useEffect(() => {
if (selectedMarketName && resolution) {
setQuery({
resolution: resolution.val,
symbol: selectedMarketName,
to: Math.floor(Date.now() / 1000),
})
}
}, [selectedMarketName, resolution])
//init chart without data
useEffect(() => {
const initKline = async () => {
const style = getComputedStyle(document.body)
const gridColor = style.getPropertyValue('--bkg-3')
const kLineChart = init('update-k-line')
kLineChart!.setStyleOptions({
grid: {
show: true,
horizontal: {
style: 'solid',
color: gridColor,
},
vertical: {
style: 'solid',
color: gridColor,
},
},
candle: {
tooltip: {
labels: ['T: ', 'O: ', 'C: ', 'H: ', 'L: ', 'V: '],
},
},
xAxis: {
axisLine: {
show: true,
color: gridColor,
size: 1,
},
},
yAxis: {
axisLine: {
show: true,
color: gridColor,
size: 1,
},
},
})
setChart(kLineChart)
}
initKline()
return () => {
dispose('update-k-line')
}
}, [])
return (
<>
<div className="flex">
{Object.keys(RES_NAME_TO_RES_VAL).map((key) => (
<div
className={`cursor-pointer py-1 px-2 ${
resolution === RES_NAME_TO_RES_VAL[key] ? 'text-th-active' : ''
}`}
key={key}
onClick={() => setResultion(RES_NAME_TO_RES_VAL[key])}
>
{key}
</div>
))}
<div
className="cursor-pointer py-1 px-2 "
onClick={() => setIsTechnicalModalOpen(true)}
>
Indicator
</div>
</div>
<div
style={{ height: 'calc(100% - 30px)', width: '100%' }}
id="update-k-line"
className="k-line-chart"
/>
<Modal
isOpen={isTechnicalModalOpen}
onClose={() => setIsTechnicalModalOpen(false)}
>
<div className="flex max-h-96 flex-col overflow-auto text-left">
<h2 className="pb-4">Main Indicator</h2>
{mainTechnicalIndicatorTypes.map((type) => {
return (
<IndicatorSwitch
key={type}
type={type}
chart={chart}
mainTechnicalIndicators={mainTechnicalIndicators}
setMainTechnicalIndicators={setMainTechnicalIndicators}
></IndicatorSwitch>
)
})}
<h2 className="pb-4">Sub Indicator</h2>
{subTechnicalIndicatorTypes.map((type) => {
return (
<IndicatorSwitch
key={type}
type={type}
chart={chart}
mainTechnicalIndicators={mainTechnicalIndicators}
setMainTechnicalIndicators={setMainTechnicalIndicators}
></IndicatorSwitch>
)
})}
</div>
</Modal>
</>
)
}
const IndicatorSwitch = ({
type,
mainTechnicalIndicators,
chart,
setMainTechnicalIndicators,
}: {
type: string
mainTechnicalIndicators: string[]
chart: klinecharts.Chart | null
setMainTechnicalIndicators: (indicators: string[]) => void
}) => {
return (
<div
className="flex justify-between border-t border-th-bkg-3 p-4 text-th-fgd-4"
key={type}
>
{type}
<Switch
checked={!!mainTechnicalIndicators.find((x) => x === type)}
onChange={(check) => {
let newInidicatorsArray = [...mainTechnicalIndicators]
if (check) {
newInidicatorsArray.push(type)
chart?.createTechnicalIndicator(type, true, {
id: 'candle_pane',
})
} else {
newInidicatorsArray = newInidicatorsArray.filter((x) => x !== type)
chart?.removeTechnicalIndicator('candle_pane', type)
}
setMainTechnicalIndicators(newInidicatorsArray)
}}
/>
</div>
)
}
export default TradingViewChartKline

View File

@ -33,7 +33,7 @@
"howler": "^2.2.3", "howler": "^2.2.3",
"html-react-parser": "^3.0.4", "html-react-parser": "^3.0.4",
"immer": "^9.0.12", "immer": "^9.0.12",
"klinecharts": "^8.6.3", "klinecharts": "8.5.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"next": "^13.0.0", "next": "^13.0.0",
"next-i18next": "^11.1.1", "next-i18next": "^11.1.1",

View File

@ -4435,10 +4435,10 @@ keyvaluestorage-interface@^1.0.0:
resolved "https://registry.yarnpkg.com/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz#13ebdf71f5284ad54be94bd1ad9ed79adad515ff" resolved "https://registry.yarnpkg.com/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz#13ebdf71f5284ad54be94bd1ad9ed79adad515ff"
integrity sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g== integrity sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==
klinecharts@^8.6.3: klinecharts@8.5.0:
version "8.6.3" version "8.5.0"
resolved "https://registry.yarnpkg.com/klinecharts/-/klinecharts-8.6.3.tgz#9ff2c40e31d86ca0600abc5fb8bf546c61daf130" resolved "https://registry.yarnpkg.com/klinecharts/-/klinecharts-8.5.0.tgz#48337c79c76100738e307267cd4d3c43578ce195"
integrity sha512-hGDtWiMNywEDneZFmt+vZ6tOYutCDWV5FPBcXcn7L8kGwe73Q5yJayk8UzP9pIQSBWyxswWIySKh/BVFA6GhuQ== integrity sha512-7F34LO3N1F/2qK4xlDSFQ4UXFE1Skvo754lXHfOdI4KtkV0Fo65lJzdpShTCdB6r1BzWxIfBOHufzRjTPfrLHA==
language-subtag-registry@~0.3.2: language-subtag-registry@~0.3.2:
version "0.3.21" version "0.3.21"