2025-09-12 18:48:04 +01:00
|
|
|
import React, {useRef, useState, useEffect} from "react";
|
2025-08-19 17:04:19 +01:00
|
|
|
import { useQuoteData } from "../QueryOptions/apiQueryOptions";
|
|
|
|
|
import { useQueryContext } from "../context/querycontext";
|
|
|
|
|
import * as echarts from 'echarts';
|
|
|
|
|
import ReactECharts from 'echarts-for-react';
|
2025-09-12 18:48:04 +01:00
|
|
|
import { type QuoteData } from "../QueryOptions/dataTypes";
|
2025-08-19 17:04:19 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
const ChartComponents: React.FC = () => {
|
2025-09-12 18:48:04 +01:00
|
|
|
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 };
|
2025-08-19 17:04:19 +01:00
|
|
|
|
|
|
|
|
|
2025-09-12 18:48:04 +01:00
|
|
|
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,
|
|
|
|
|
});
|
2025-08-19 17:04:19 +01:00
|
|
|
|
2025-09-12 18:48:04 +01:00
|
|
|
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]);
|
2025-08-19 17:04:19 +01:00
|
|
|
|
|
|
|
|
|
2025-09-12 18:48:04 +01:00
|
|
|
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;
|
2025-08-19 17:04:19 +01:00
|
|
|
|
|
|
|
|
|
2025-09-12 18:48:04 +01:00
|
|
|
|
|
|
|
|
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 = {
|
2025-08-19 17:04:19 +01:00
|
|
|
dataset: {
|
2025-09-12 18:48:04 +01:00
|
|
|
source: filteredData,
|
2025-08-19 17:04:19 +01:00
|
|
|
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: [
|
2025-09-12 18:48:04 +01:00
|
|
|
`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})`,
|
2025-08-19 17:04:19 +01:00
|
|
|
],
|
|
|
|
|
selected: {
|
2025-09-12 18:48:04 +01:00
|
|
|
[`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,
|
2025-08-19 17:04:19 +01:00
|
|
|
},
|
|
|
|
|
},
|
2025-09-12 18:48:04 +01:00
|
|
|
grid: { left: '5%', right: '5%', bottom: '15%', containLabel: true },
|
|
|
|
|
toolbox: { feature: { dataZoom: {}, saveAsImage: {}, brush: { type: ['rect', 'polygon', 'clear'] } } },
|
|
|
|
|
dataZoom,
|
2025-08-19 17:04:19 +01:00
|
|
|
xAxis: {
|
|
|
|
|
type: 'time',
|
|
|
|
|
name: 'Timestamp',
|
2025-09-12 18:48:04 +01:00
|
|
|
axisLabel: { formatter: (value: number) => new Date(value).toLocaleString() },
|
2025-08-19 17:04:19 +01:00
|
|
|
},
|
|
|
|
|
yAxis: [
|
|
|
|
|
{
|
|
|
|
|
type: 'value',
|
2025-09-12 18:48:04 +01:00
|
|
|
name: `${brokerA}/${symbolA}`,
|
|
|
|
|
min: adjustedMinValueA,
|
|
|
|
|
max: adjustedMaxValueA,
|
|
|
|
|
position: 'left',
|
|
|
|
|
axisLabel: { formatter: (value: number) => value.toFixed(5) },
|
2025-08-19 17:04:19 +01:00
|
|
|
scale: true,
|
|
|
|
|
splitNumber: 10,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'value',
|
2025-09-12 18:48:04 +01:00
|
|
|
name: `${brokerB}/${symbolB}`,
|
|
|
|
|
min: adjustedMinValueB,
|
|
|
|
|
max: adjustedMaxValueB,
|
2025-08-19 17:04:19 +01:00
|
|
|
position: 'right',
|
2025-09-12 18:48:04 +01:00
|
|
|
axisLabel: { formatter: (value: number) => value.toFixed(5) },
|
2025-08-19 17:04:19 +01:00
|
|
|
scale: true,
|
|
|
|
|
splitNumber: 10,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
series: [
|
|
|
|
|
{
|
2025-09-12 18:48:04 +01:00
|
|
|
name: `Ask A (${brokerA}/${symbolA})`,
|
2025-08-19 17:04:19 +01:00
|
|
|
type: 'line',
|
2025-09-12 18:48:04 +01:00
|
|
|
data: filteredData.filter(record => record.askA !== null).map(record => [record.timestamp, record.askA]),
|
2025-08-19 17:04:19 +01:00
|
|
|
yAxisIndex: 0,
|
|
|
|
|
lineStyle: { color: '#007bff', width: 2 },
|
|
|
|
|
itemStyle: { color: '#007bff' },
|
2025-09-12 18:48:04 +01:00
|
|
|
showSymbol: false,
|
2025-08-19 17:04:19 +01:00
|
|
|
connectNulls: false,
|
2025-09-12 18:48:04 +01:00
|
|
|
markLine: marklineAskA,
|
2025-08-19 17:04:19 +01:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-12 18:48:04 +01:00
|
|
|
name: `Bid A (${brokerA}/${symbolA})`,
|
2025-08-19 17:04:19 +01:00
|
|
|
type: 'line',
|
2025-09-12 18:48:04 +01:00
|
|
|
data: filteredData.filter(record => record.bidA !== null).map(record => [record.timestamp, record.bidA]),
|
2025-08-19 17:04:19 +01:00
|
|
|
yAxisIndex: 0,
|
|
|
|
|
lineStyle: { color: '#00b7eb', type: 'dashed', width: 2 },
|
|
|
|
|
itemStyle: { color: '#00b7eb' },
|
2025-09-12 18:48:04 +01:00
|
|
|
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,
|
2025-08-19 17:04:19 +01:00
|
|
|
connectNulls: false,
|
2025-09-12 18:48:04 +01:00
|
|
|
|
2025-08-19 17:04:19 +01:00
|
|
|
},
|
2025-09-12 18:48:04 +01:00
|
|
|
// {
|
|
|
|
|
// 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,
|
|
|
|
|
// },
|
2025-08-19 17:04:19 +01:00
|
|
|
{
|
2025-09-12 18:48:04 +01:00
|
|
|
name: `Ask B (${brokerB}/${symbolB})`,
|
2025-08-19 17:04:19 +01:00
|
|
|
type: 'line',
|
2025-09-12 18:48:04 +01:00
|
|
|
data: filteredData.filter(record => record.askB !== null).map(record => [record.timestamp, record.askB]),
|
2025-08-19 17:04:19 +01:00
|
|
|
yAxisIndex: 1,
|
|
|
|
|
lineStyle: { color: '#ff4500', width: 2 },
|
|
|
|
|
itemStyle: { color: '#ff4500' },
|
2025-09-12 18:48:04 +01:00
|
|
|
showSymbol: false,
|
2025-08-19 17:04:19 +01:00
|
|
|
connectNulls: false,
|
2025-09-12 18:48:04 +01:00
|
|
|
markLine: marklineAskB,
|
2025-08-19 17:04:19 +01:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-12 18:48:04 +01:00
|
|
|
name: `Bid B (${brokerB}/${symbolB})`,
|
2025-08-19 17:04:19 +01:00
|
|
|
type: 'line',
|
2025-09-12 18:48:04 +01:00
|
|
|
data: filteredData.filter(record => record.bidB !== null).map(record => [record.timestamp, record.bidB]),
|
2025-08-19 17:04:19 +01:00
|
|
|
yAxisIndex: 1,
|
|
|
|
|
lineStyle: { color: '#ff8c00', type: 'dashed', width: 2 },
|
|
|
|
|
itemStyle: { color: '#ff8c00' },
|
2025-09-12 18:48:04 +01:00
|
|
|
showSymbol: false,
|
2025-08-19 17:04:19 +01:00
|
|
|
connectNulls: false,
|
2025-09-12 18:48:04 +01:00
|
|
|
markPoint: markPointDataB,
|
|
|
|
|
maekLine: marklineBidB,
|
2025-08-19 17:04:19 +01:00
|
|
|
},
|
|
|
|
|
{
|
2025-09-12 18:48:04 +01:00
|
|
|
name: `Midline B (${brokerB}/${symbolB})`,
|
2025-08-19 17:04:19 +01:00
|
|
|
type: 'line',
|
2025-09-12 18:48:04 +01:00
|
|
|
data: filteredData.filter(record => record.midlineB !== null).map(record => [record.timestamp, record.midlineB]),
|
2025-08-19 17:04:19 +01:00
|
|
|
yAxisIndex: 1,
|
|
|
|
|
lineStyle: { color: '#9932cc', width: 2 },
|
|
|
|
|
itemStyle: { color: '#9932cc' },
|
2025-09-12 18:48:04 +01:00
|
|
|
showSymbol: false,
|
2025-08-19 17:04:19 +01:00
|
|
|
connectNulls: false,
|
|
|
|
|
},
|
2025-09-12 18:48:04 +01:00
|
|
|
// {
|
|
|
|
|
// 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,
|
|
|
|
|
// },
|
2025-08-19 17:04:19 +01:00
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-09-12 18:48:04 +01:00
|
|
|
<div className="p-4">
|
2025-08-19 17:04:19 +01:00
|
|
|
<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"
|
|
|
|
|
/>
|
2025-09-12 18:48:04 +01:00
|
|
|
<span>Show Ask/Bid/Spread</span>
|
2025-08-19 17:04:19 +01:00
|
|
|
</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}
|
2025-09-12 18:48:04 +01:00
|
|
|
ref={chartRef}
|
2025-08-19 17:04:19 +01:00
|
|
|
option={option}
|
|
|
|
|
style={{ height: 600, width: '100%' }}
|
|
|
|
|
opts={{ renderer: 'canvas' }}
|
|
|
|
|
theme="dark"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-09-12 18:48:04 +01:00
|
|
|
);
|
2025-08-19 17:04:19 +01:00
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default ChartComponents;
|