2025-09-12 18:48:04 +01:00

554 lines
18 KiB
TypeScript

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<ReactECharts>(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 <div>Loading brokers...</div>;
if (isErrorBrokers) return <div>Error loading brokers</div>;
if (isErrorQuotes) return <div>Error loading quote data</div>;
if (!quoteData?.records?.length) return <div>No quote data available</div>;
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()}<br>` +
params
.map((p: any) => `${p.seriesName}: ${p.value[1] != null ? p.value[1].toFixed(5) : 'N/A'}`)
.join('<br>')
);
},
},
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 (
<div className="p-4">
<h2 className="text-2xl font-bold text-gray-800 dark:text-gray-500 mb-4">Quote Chart</h2>
<div className="flex space-x-4 mb-4">
<label className="flex items-center space-x-2 text-gray-700 dark:text-gray-500">
<input
type="radio"
name="chartMode"
checked={!showMidline}
onChange={() => setShowMidline(false)}
className="form-radio text-blue-500 focus:ring-blue-500"
/>
<span>Show Ask/Bid/Spread</span>
</label>
<label className="flex items-center space-x-2 text-gray-700 dark:text-gray-500">
<input
type="radio"
name="chartMode"
checked={showMidline}
onChange={() => setShowMidline(true)}
className="form-radio text-blue-500 focus:ring-blue-500"
/>
<span>Show Midline</span>
</label>
</div>
<ReactECharts
echarts={echarts}
ref={chartRef}
option={option}
style={{ height: 600, width: '100%' }}
opts={{ renderer: 'canvas' }}
theme="dark"
/>
</div>
);
};
export default ChartComponents;