326 lines
10 KiB
TypeScript
326 lines
10 KiB
TypeScript
|
|
import React, {useRef, useState} from "react";
|
||
|
|
import { useQuoteData } from "../QueryOptions/apiQueryOptions";
|
||
|
|
import { useQueryContext } from "../context/querycontext";
|
||
|
|
import * as echarts from 'echarts';
|
||
|
|
import ReactECharts from 'echarts-for-react';
|
||
|
|
|
||
|
|
|
||
|
|
const ChartComponents: React.FC = () => {
|
||
|
|
const chartRef = useRef<ReactECharts>(null);
|
||
|
|
const { queryOptions, isLoadingBrokers, isErrorBrokers } =
|
||
|
|
useQueryContext();
|
||
|
|
|
||
|
|
|
||
|
|
const { data: quoteData, isLoading: isLoadingQuotes, isError: isErrorQuotes } =
|
||
|
|
useQuoteData(queryOptions);
|
||
|
|
|
||
|
|
const [showMidline, setShowMidline] = useState(false);
|
||
|
|
|
||
|
|
|
||
|
|
// console.log('quote data before render:', quoteData,
|
||
|
|
// 'isLoadingQuotes:', isLoadingQuotes,
|
||
|
|
// 'quotesError:', isErrorQuotes);
|
||
|
|
|
||
|
|
|
||
|
|
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 datasetSource = quoteData.records.filter(record => record.askA !== null || record.midlineA
|
||
|
|
|| record.bidA !== null || record.askB !== null || record.bidB !== null || record.midlineB)
|
||
|
|
.map(record => ({
|
||
|
|
timestamp: record.timestamp,
|
||
|
|
askA: record.askA,
|
||
|
|
askB: record.askB,
|
||
|
|
bidA: record.bidA,
|
||
|
|
bidB: record.bidB,
|
||
|
|
midlineA: record.midlineA,
|
||
|
|
midlineB: record.midlineB,
|
||
|
|
}));
|
||
|
|
// console.log('datasetSource:', datasetSource);
|
||
|
|
|
||
|
|
const valuesA = datasetSource
|
||
|
|
.flatMap(record => [record.askA, record.bidA, record.midlineA])
|
||
|
|
.filter((value): value is number => value !== null);
|
||
|
|
const valuesB = datasetSource
|
||
|
|
.flatMap(record => [record.askB, record.bidB, record.midlineB])
|
||
|
|
.filter((value): value is number => value !== null);
|
||
|
|
const buffer = 0.00001;
|
||
|
|
// console.log('ValueA:', valuesA);
|
||
|
|
// console.log('ValueB:', valuesB);
|
||
|
|
|
||
|
|
// console.log('askA values:', datasetSource.map(r => r.askA).filter(v => v !== null));
|
||
|
|
// console.log('askB values:', datasetSource.map(r => r.askB).filter(v => v !== null));
|
||
|
|
// console.log('bidA values:', datasetSource.map(r => r.bidA).filter(v => v !== null));
|
||
|
|
// console.log('bidB values:', datasetSource.map(r => r.bidB).filter(v => v !== null));
|
||
|
|
|
||
|
|
const minValueA = valuesA.length ? Math.min(...valuesA) - buffer : 0;
|
||
|
|
const maxValueA = valuesA.length ? Math.max(...valuesA) + buffer : 100;
|
||
|
|
const minValueB = valuesB.length ? Math.min(...valuesB) - buffer : 0;
|
||
|
|
const maxValueB = valuesB.length ? Math.max(...valuesB) + buffer : 100;
|
||
|
|
// console.log('minValueA:', minValueA);
|
||
|
|
// console.log('maxValueA:', maxValueA);
|
||
|
|
// console.log('minValueB:', minValueB);
|
||
|
|
// console.log('maxValueB:', maxValueB);
|
||
|
|
//
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
const option = {
|
||
|
|
dataset: {
|
||
|
|
source: datasetSource,
|
||
|
|
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 (${quoteData.brokerA}/${quoteData.symbolA})`,
|
||
|
|
`Bid A (${quoteData.brokerA}/${quoteData.symbolA})`,
|
||
|
|
`Midline A (${quoteData.brokerA}/${quoteData.symbolA})`,
|
||
|
|
`Ask B (${quoteData.brokerB}/${quoteData.symbolB})`,
|
||
|
|
`Bid B (${quoteData.brokerB}/${quoteData.symbolB})`,
|
||
|
|
`Midline B (${quoteData.brokerB}/${quoteData.symbolB})`,
|
||
|
|
],
|
||
|
|
selected: {
|
||
|
|
[`Ask A (${quoteData.brokerA}/${quoteData.symbolA})`]: !showMidline,
|
||
|
|
[`Ask B (${quoteData.brokerB}/${quoteData.symbolB})`]: !showMidline,
|
||
|
|
[`Bid A (${quoteData.brokerA}/${quoteData.symbolA})`]: !showMidline,
|
||
|
|
[`Bid B (${quoteData.brokerB}/${quoteData.symbolB})`]: !showMidline,
|
||
|
|
[`Midline A (${quoteData.brokerA}/${quoteData.symbolA})`]: showMidline,
|
||
|
|
[`Midline B (${quoteData.brokerB}/${quoteData.symbolB})`]: showMidline,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
grid: {
|
||
|
|
left: '5%',
|
||
|
|
right: '5%',
|
||
|
|
bottom: '15%',
|
||
|
|
containLabel: true,
|
||
|
|
},
|
||
|
|
toolbox: {
|
||
|
|
feature: {
|
||
|
|
saveAsImage: {},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
dataZoom: [
|
||
|
|
{
|
||
|
|
type: 'slider',
|
||
|
|
xAxisIndex: 0,
|
||
|
|
start: 80,
|
||
|
|
end: 100,
|
||
|
|
labelFormatter: (value: number) => {
|
||
|
|
const date = new Date(value);
|
||
|
|
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
type: 'inside',
|
||
|
|
xAxisIndex: 0,
|
||
|
|
start: 80,
|
||
|
|
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,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
xAxis: {
|
||
|
|
type: 'time',
|
||
|
|
name: 'Timestamp',
|
||
|
|
axisLabel: {
|
||
|
|
formatter: (value: number) => {
|
||
|
|
const date = new Date(value);
|
||
|
|
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
yAxis: [
|
||
|
|
{
|
||
|
|
type: 'value',
|
||
|
|
name: `${quoteData.brokerA}/${quoteData.symbolA}`,
|
||
|
|
min: minValueA,
|
||
|
|
max: maxValueA,
|
||
|
|
axisLabel: {
|
||
|
|
formatter: (value: number) => value.toFixed(5),
|
||
|
|
},
|
||
|
|
// alignTicks:true,
|
||
|
|
scale: true,
|
||
|
|
splitNumber: 10,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
type: 'value',
|
||
|
|
name: `${quoteData.brokerB}/${quoteData.symbolB}`,
|
||
|
|
min: minValueB,
|
||
|
|
max: maxValueB,
|
||
|
|
position: 'right',
|
||
|
|
axisLabel: {
|
||
|
|
formatter: (value: number) => value.toFixed(5),
|
||
|
|
},
|
||
|
|
// alignTicks:true,
|
||
|
|
scale: true,
|
||
|
|
splitNumber: 10,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
series: [
|
||
|
|
{
|
||
|
|
name: `Ask A (${quoteData.brokerA}/${quoteData.symbolA})`,
|
||
|
|
type: 'line',
|
||
|
|
data: datasetSource
|
||
|
|
.filter(record => record.askA !== null)
|
||
|
|
.map(record => [record.timestamp, record.askA]),
|
||
|
|
yAxisIndex: 0,
|
||
|
|
lineStyle: { color: '#007bff', width: 2 },
|
||
|
|
itemStyle: { color: '#007bff' },
|
||
|
|
showSymbol: true,
|
||
|
|
symbol: 'circle',
|
||
|
|
symbolSize: 4,
|
||
|
|
connectNulls: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: `Bid A (${quoteData.brokerA}/${quoteData.symbolA})`,
|
||
|
|
type: 'line',
|
||
|
|
data: datasetSource
|
||
|
|
.filter(record => record.bidA !== null)
|
||
|
|
.map(record => [record.timestamp, record.bidA]),
|
||
|
|
yAxisIndex: 0,
|
||
|
|
lineStyle: { color: '#00b7eb', type: 'dashed', width: 2 },
|
||
|
|
itemStyle: { color: '#00b7eb' },
|
||
|
|
showSymbol: true,
|
||
|
|
symbol: 'circle',
|
||
|
|
symbolSize: 4,
|
||
|
|
connectNulls: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: `Ask B (${quoteData.brokerB}/${quoteData.symbolB})`,
|
||
|
|
type: 'line',
|
||
|
|
data: datasetSource
|
||
|
|
.filter(record => record.askB !== null)
|
||
|
|
.map(record => [record.timestamp, record.askB]),
|
||
|
|
yAxisIndex: 1,
|
||
|
|
lineStyle: { color: '#ff4500', width: 2 },
|
||
|
|
itemStyle: { color: '#ff4500' },
|
||
|
|
showSymbol: true,
|
||
|
|
symbol: 'square',
|
||
|
|
symbolSize: 4,
|
||
|
|
connectNulls: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: `Bid B (${quoteData.brokerB}/${quoteData.symbolB})`,
|
||
|
|
type: 'line',
|
||
|
|
data: datasetSource
|
||
|
|
.filter(record => record.bidB !== null)
|
||
|
|
.map(record => [record.timestamp, record.bidB]),
|
||
|
|
yAxisIndex: 1,
|
||
|
|
lineStyle: { color: '#ff8c00', type: 'dashed', width: 2 },
|
||
|
|
itemStyle: { color: '#ff8c00' },
|
||
|
|
showSymbol: true,
|
||
|
|
symbol: 'square',
|
||
|
|
symbolSize: 4,
|
||
|
|
connectNulls: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: `Midline A (${quoteData.brokerA}/${quoteData.symbolA})`,
|
||
|
|
type: 'line',
|
||
|
|
data: datasetSource
|
||
|
|
.filter(record => record.midlineA !== null)
|
||
|
|
.map(record => [record.timestamp, record.midlineA]),
|
||
|
|
yAxisIndex: 0,
|
||
|
|
lineStyle: { color: '#28a745', width: 2 },
|
||
|
|
itemStyle: { color: '#28a745' },
|
||
|
|
showSymbol: true,
|
||
|
|
symbol: 'circle',
|
||
|
|
symbolSize: 4,
|
||
|
|
connectNulls: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: `Midline B (${quoteData.brokerB}/${quoteData.symbolB})`,
|
||
|
|
type: 'line',
|
||
|
|
data: datasetSource
|
||
|
|
.filter(record => record.midlineB !== null)
|
||
|
|
.map(record => [record.timestamp, record.midlineB]),
|
||
|
|
yAxisIndex: 1,
|
||
|
|
lineStyle: { color: '#9932cc', width: 2 },
|
||
|
|
itemStyle: { color: '#9932cc' },
|
||
|
|
showSymbol: true,
|
||
|
|
symbol: 'square',
|
||
|
|
symbolSize: 4,
|
||
|
|
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</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;
|