import React, {useRef, useState, useEffect} from "react"; import { useQuoteData } from "../QueryOptions/apiQueryOptions"; import { useQueryContext } from "../context/querycontext"; import * as echarts from 'echarts'; import ReactECharts from 'echarts-for-react'; import { type QuoteData } from "../QueryOptions/dataTypes"; const ChartComponents: React.FC = () => { const chartRef = useRef(null); const { queryOptions, isLoadingBrokers, isErrorBrokers } = useQueryContext(); const {data: quoteData, isError: isErrorQuotes } = useQuoteData(queryOptions) as { data: QuoteData | undefined; isLoading: boolean; isError: boolean }; const [showMidline, setShowMidline] = useState(false); const [latestValues, setLatestValues] = useState({ lastTimestampA: null as number | null, lastTimestampB: null as number | null, lastAskA: null as number | null, lastBidA: null as number | null, lastAskB: null as number | null, lastBidB: null as number | null, lastMidlineA: null as number | null, lastMidlineB: null as number | null, }); useEffect(() => { if (quoteData?.records) { const newLatestValues = { askA: null as number | null, bidA: null as number | null, midlineA: null as number | null, spreadA: null as number | null, timestampA: null as string | null, askB: null as number | null, bidB: null as number | null, midlineB: null as number | null, spreadB: null as number | null, timestampB: null as string | null, }; for (const record of quoteData.records) { if (newLatestValues.askA === null && record.askA !== null) { newLatestValues.askA = record.askA; newLatestValues.bidA = record.bidA; newLatestValues.midlineA = record.midlineA; newLatestValues.spreadA = record.spreadA; newLatestValues.timestampA = record.timestamp; // console.log('Updated A in filteredData at:', record.timestamp, 'with askA:', record.askA); } if (newLatestValues.askB === null && record.askB !== null) { newLatestValues.askB = record.askB; newLatestValues.bidB = record.bidB; newLatestValues.midlineB = record.midlineB; newLatestValues.spreadB = record.spreadB; newLatestValues.timestampB = record.timestamp; // console.log('Updated B in filteredData at:', record.timestamp, 'with askB:', record.askB); } } const lastTimestampA = newLatestValues.timestampA ? new Date(newLatestValues.timestampA).getTime() : (quoteData.records[0]?.timestamp ? new Date(quoteData.records[0].timestamp).getTime() : null); const lastTimestampB = newLatestValues.timestampB ? new Date(newLatestValues.timestampB).getTime() : (quoteData.records[0]?.timestamp ? new Date(quoteData.records[0].timestamp).getTime() : null); setLatestValues({ lastTimestampA, lastTimestampB, lastAskA: newLatestValues.askA, lastBidA: newLatestValues.bidA, lastAskB: newLatestValues.askB, lastBidB: newLatestValues.bidB, lastMidlineA: newLatestValues.midlineA, lastMidlineB: newLatestValues.midlineB, }); const chartInstance = chartRef.current?.getEchartsInstance(); if (chartInstance) { const timestamps = quoteData.records.map(record => new Date(record.timestamp).getTime()); if (timestamps.length > 0) { const maxTimestamp = Math.max(...timestamps); const minTimestamp = Math.min(...timestamps); const range = maxTimestamp - minTimestamp; const last2Percent = minTimestamp + range * 0.98; chartInstance.setOption({ dataZoom: [ { startValue: last2Percent, endValue: maxTimestamp }, { type: 'inside', startValue: last2Percent, endValue: maxTimestamp }, ], }, true); const updateMarkLine = () => { if (newLatestValues.askA) { const option = chartInstance.getOption(); option.series = (option.series as any[]).map((series: any) => { if (series.name === `Ask A (${brokerA}/${symbolA})`) { return { ...series, markLine: { data: [ { yAxis: newLatestValues.askA, lineStyle: { type: 'dashed', color: '#007bff', width: 1, }, label: { show: true, position: 'insideEndTop', color: 'white', fontWeight: 'bold', fontSize: 12, formatter: () => newLatestValues.askA!.toFixed(5), }, silent: true, animation: false, clip: false, }, ], }, }; } return series; }); chartInstance.setOption(option); } }; updateMarkLine(); chartInstance.on('dataZoom', updateMarkLine); return () => { chartInstance.off('dataZoom', updateMarkLine); }; } } } }, [quoteData]); if (isLoadingBrokers) return
Loading brokers...
; if (isErrorBrokers) return
Error loading brokers
; if (isErrorQuotes) return
Error loading quote data
; if (!quoteData?.records?.length) return
No quote data available
; const { records: filteredData, brokerA, symbolA, brokerB, symbolB } = quoteData; const valuesA = filteredData .map(records => [records.askA, records.bidA, records.midlineA]) .flat() .filter((v): v is number => v !== null); const valuesB = filteredData .map(records => [records.askB, records.bidB, records.midlineB]) .flat() .filter((v): v is number => v !== null); const buffer = 0.00001; const minValueA = valuesA.length ? Math.min(...valuesA) - buffer : 0; const maxValueA = valuesA.length ? Math.max(...valuesA) + buffer : 100; const adjustedMinValueA = Math.min(minValueA, latestValues.lastAskA || minValueA) - buffer; const adjustedMaxValueA = Math.max(maxValueA, latestValues.lastAskA || maxValueA) + buffer; const minValueB = valuesB.length ? Math.min(...valuesB) - buffer : 0; const maxValueB = valuesB.length ? Math.max(...valuesB) + buffer : 100; const adjustedMinValueB = Math.min(minValueB, latestValues.lastAskB || minValueB) - buffer; const adjustedMaxValueB = Math.max(maxValueB, latestValues.lastAskB || maxValueB) + buffer; const { // lastTimestampA, // lastTimestampB, lastAskA, lastBidA, lastAskB, lastBidB, // lastMidlineA, // lastMidlineB, } = latestValues; // const firstTimestamp = filteredData.at(0)?.timestamp; // console.log(`Last Timestamp A: ${lastTimestampA} last Ask A: ${lastAskA} to First Timestamp ${firstTimestamp}`); // console.log(`Last Ask A: ${lastAskA}`); // console.log('Last Bid A:', lastBidA); // console.log('Last Timestamp B:', lastTimestampB); // console.log('Last Ask B:', lastAskB); // console.log('Last Bid B:', lastBidB); // console.log('Last Midline A:', lastMidlineA); // console.log('Last Midline B:', lastMidlineB); // console.log('MarkLine Data for Ask A:', lastAskA ? [{ yAxis: lastAskA }] : 'Invalid (null)'); const typeDataA = filteredData.filter(record => record.directionA === 'buy' || record.directionA === 'sell'); const typeDataB = filteredData.filter(record => record.directionB === 'buy' || record.directionB === 'sell'); const typeDataPointBidA = typeDataA.map(record => ({ coord: [record.timestamp, record.directionA === 'buy' ? record.askA : record.bidA], name: record.directionA, itemStyle: { color: record.directionA === 'buy' ? '#00ff00' : '#ff0000' }, symbolRotate: record.directionA === 'buy' ? 0 : 180, symbolOffset: record.directionA === 'buy' ? [0, 10] : [0, -10], })); const typeDataPointBidB = typeDataB.map(record => ({ coord: [record.timestamp, record.directionB === 'buy' ? record.askB : record.bidB], name: record.directionB, itemStyle: { color: record.directionB === 'buy' ? '#00ff00' : '#ff0000' }, symbolRotate: record.directionB === 'buy' ? 0 : 180, symbolOffset: record.directionB === 'buy' ? [0, 10] : [0, -10], })); const markPointDataA = { symbol: 'arrow', symbolSize: 15, label: { show: true, formatter: (param: any) => param.data.name === 'buy' ? 'Buy' : 'Sell', fontSize: 10, color: 'white' }, data: typeDataPointBidA }; const markPointDataB = { symbol: 'arrow', symbolSize: 15, label: { show: true, formatter: (param: any) => param.data.name === 'buy' ? 'Buy' : 'Sell', fontSize: 10, color: 'white' }, data: typeDataPointBidB }; const dataZoom = [ { type: 'slider', xAxisIndex: 0, start: 0, end: 100, labelFormatter: (value: number) => new Date(value).toLocaleTimeString() }, { type: 'inside', xAxisIndex: 0, start: 0, end: 100 }, { type: 'slider', yAxisIndex: 0, start: 0, end: 100, labelFormatter: (value: number) => value.toFixed(5) }, { type: 'inside', yAxisIndex: 0, start: 0, end: 100 }, { type: 'slider', yAxisIndex: 1, start: 0, end: 100, labelFormatter: (value: number) => value.toFixed(5) }, { type: 'inside', yAxisIndex: 1, start: 0, end: 100 }, ]; // console.log(echarts.version) const marklineAskA = { symbol: 'none', lineStyle: { type: 'dashed', color: '#007bff', }, label: { show: true, position: 'insideEndTop', color: 'white', fontWeight: 'bold', fontSize: 12, formatter: () => lastAskA!.toFixed(5) }, data: [ [ { coord: ['min', lastAskA] }, { coord: ['max', lastAskA] } ] ], silent: true, animation: false, clip: false }; const marklineAskB = { symbol: 'none', lineStyle: { type: 'dashed', color: '#ff4500', }, label: { show: true, position: 'insideEndTop', color: 'white', fontWeight: 'bold', fontSize: 12, formatter: () => lastAskB!.toFixed(5) }, data: [ [ { coord: ['min', lastAskB] }, { coord: ['max', lastAskB] } ] ], silent: true, animation: false, clip: false }; const marklineBidA = { symbol: 'none', lineStyle: { type: 'dashed', color: '#00b7eb', }, label: { show: true, position: 'insideEndTop', color: 'white', fontWeight: 'bold', fontSize: 12, formatter: () => lastBidA!.toFixed(5) }, data: [ [ { coord: ['min', lastBidA] }, { coord: ['max', lastBidA] } ] ], silent: true, animation: false, clip: false }; const marklineBidB = { symbol: 'none', lineStyle: { type: 'dashed', color: '#ff8c00', }, label: { show: true, position: 'insideEndTop', color: 'white', fontWeight: 'bold', fontSize: 12, formatter: () => lastBidB!.toFixed(5) }, data: [ [ { coord: ['min', lastBidB] }, { coord: ['max', lastBidB] } ] ], silent: true, animation: false, clip: false }; const option = { dataset: { source: filteredData, dimensions: ['timestamp', 'askA', 'askB', 'bidA', 'bidB', 'midlineA', 'midlineB'], }, tooltip: { trigger: 'axis', formatter: (params: any) => { const date = new Date(params[0].value[0]); return ( `Time: ${date.toLocaleTimeString()}
` + params .map((p: any) => `${p.seriesName}: ${p.value[1] != null ? p.value[1].toFixed(5) : 'N/A'}`) .join('
') ); }, }, legend: { data: [ `Ask A (${brokerA}/${symbolA})`, `Bid A (${brokerA}/${symbolA})`, `Midline A (${brokerA}/${symbolA})`, // `Spread A (${brokerA}/${symbolA})`, `Ask B (${brokerB}/${symbolB})`, `Bid B (${brokerB}/${symbolB})`, `Midline B (${brokerB}/${symbolB})`, // `Spread B (${brokerB}/${symbolB})`, ], selected: { [`Ask A (${brokerA}/${symbolA})`]: !showMidline, [`Bid A (${brokerA}/${symbolA})`]: !showMidline, [`Ask B (${brokerB}/${symbolB})`]: !showMidline, [`Bid B (${brokerB}/${symbolB})`]: !showMidline, [`Midline A (${brokerA}/${symbolA})`]: showMidline, [`Midline B (${brokerB}/${symbolB})`]: showMidline, // [`Spread A (${brokerA}/${symbolA})`]: showMidline, // [`Spread B (${brokerB}/${symbolB})`]: showMidline, }, }, grid: { left: '5%', right: '5%', bottom: '15%', containLabel: true }, toolbox: { feature: { dataZoom: {}, saveAsImage: {}, brush: { type: ['rect', 'polygon', 'clear'] } } }, dataZoom, xAxis: { type: 'time', name: 'Timestamp', axisLabel: { formatter: (value: number) => new Date(value).toLocaleString() }, }, yAxis: [ { type: 'value', name: `${brokerA}/${symbolA}`, min: adjustedMinValueA, max: adjustedMaxValueA, position: 'left', axisLabel: { formatter: (value: number) => value.toFixed(5) }, scale: true, splitNumber: 10, }, { type: 'value', name: `${brokerB}/${symbolB}`, min: adjustedMinValueB, max: adjustedMaxValueB, position: 'right', axisLabel: { formatter: (value: number) => value.toFixed(5) }, scale: true, splitNumber: 10, }, ], series: [ { name: `Ask A (${brokerA}/${symbolA})`, type: 'line', data: filteredData.filter(record => record.askA !== null).map(record => [record.timestamp, record.askA]), yAxisIndex: 0, lineStyle: { color: '#007bff', width: 2 }, itemStyle: { color: '#007bff' }, showSymbol: false, connectNulls: false, markLine: marklineAskA, }, { name: `Bid A (${brokerA}/${symbolA})`, type: 'line', data: filteredData.filter(record => record.bidA !== null).map(record => [record.timestamp, record.bidA]), yAxisIndex: 0, lineStyle: { color: '#00b7eb', type: 'dashed', width: 2 }, itemStyle: { color: '#00b7eb' }, showSymbol: false, connectNulls: false, markPoint: markPointDataA, markLine: marklineBidA, }, { name: `Midline A (${brokerA}/${symbolA})`, type: 'line', data: filteredData.filter(record => record.midlineA !== null).map(record => [record.timestamp, record.midlineA]), yAxisIndex: 0, lineStyle: { color: '#28a745', width: 2 }, itemStyle: { color: '#28a745' }, showSymbol: false, connectNulls: false, }, // { // name: `Spread A (${brokerA}/${symbolA})`, // type: 'line', // data: filteredData.filter(record => record.spreadA !== null).map(record => [record.timestamp, record.spreadA]), // yAxisIndex: 0, // lineStyle: { color: '#6f42c1', width: 2 }, // itemStyle: { color: '#6f42c1' }, // showSymbol: false, // connectNulls: false, // }, { name: `Ask B (${brokerB}/${symbolB})`, type: 'line', data: filteredData.filter(record => record.askB !== null).map(record => [record.timestamp, record.askB]), yAxisIndex: 1, lineStyle: { color: '#ff4500', width: 2 }, itemStyle: { color: '#ff4500' }, showSymbol: false, connectNulls: false, markLine: marklineAskB, }, { name: `Bid B (${brokerB}/${symbolB})`, type: 'line', data: filteredData.filter(record => record.bidB !== null).map(record => [record.timestamp, record.bidB]), yAxisIndex: 1, lineStyle: { color: '#ff8c00', type: 'dashed', width: 2 }, itemStyle: { color: '#ff8c00' }, showSymbol: false, connectNulls: false, markPoint: markPointDataB, maekLine: marklineBidB, }, { name: `Midline B (${brokerB}/${symbolB})`, type: 'line', data: filteredData.filter(record => record.midlineB !== null).map(record => [record.timestamp, record.midlineB]), yAxisIndex: 1, lineStyle: { color: '#9932cc', width: 2 }, itemStyle: { color: '#9932cc' }, showSymbol: false, connectNulls: false, }, // { // name: `Spread B (${brokerB}/${symbolB})`, // type: 'line', // data: filteredData.filter(record => record.spreadB !== null).map(record => [record.timestamp, record.spreadB]), // yAxisIndex: 1, // lineStyle: { color: '#dc3545', width: 2 }, // itemStyle: { color: '#dc3545' }, // showSymbol: false, // connectNulls: false, // }, ], }; return (

Quote Chart

); }; export default ChartComponents;