This commit is contained in:
sammanme 2025-09-12 18:48:04 +01:00
parent bbf5931c52
commit 7bca898286
5 changed files with 466 additions and 264 deletions

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(254.59340659340654 254.59340659340654) rotate(180) scale(2.81 2.81)">
<path d="M 46.969 89.104 c -1.041 1.194 -2.897 1.194 -3.937 0 L 13.299 54.989 c -0.932 -1.072 -0.171 -2.743 1.25 -2.743 h 14.249 V 1.91 c 0 -1.055 0.855 -1.91 1.91 -1.91 h 28.584 c 1.055 0 1.91 0.855 1.91 1.91 v 50.336 h 14.249 c 1.421 0 2.182 1.671 1.25 2.743 L 46.969 89.104 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(101,214,41); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 974 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)">
<path d="M 46.969 89.104 c -1.041 1.194 -2.897 1.194 -3.937 0 L 13.299 54.989 c -0.932 -1.072 -0.171 -2.743 1.25 -2.743 h 14.249 V 1.91 c 0 -1.055 0.855 -1.91 1.91 -1.91 h 28.584 c 1.055 0 1.91 0.855 1.91 1.91 v 50.336 h 14.249 c 1.421 0 2.182 1.671 1.25 2.743 L 46.969 89.104 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(214,41,41); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 961 B

View File

@ -1,13 +1,13 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import qs from 'query-string'; import qs from 'query-string';
import { type RawBrokerSymbol, type QueryOptions, type QuoteData, type QuoteRecord } from "./dataTypes"; import { type RawBrokerSymbol, type QueryOptions, type QuoteData } from "./dataTypes";
export const useBrokerSymbols = () => { export const useBrokerSymbols = () => {
return useQuery<RawBrokerSymbol, Error>({ return useQuery<RawBrokerSymbol, Error>({
queryKey: ['brokers'], queryKey: ['brokers'],
queryFn: async(): Promise<RawBrokerSymbol> => { queryFn: async(): Promise<RawBrokerSymbol> => {
const response = await fetch('http://localhost:8000/api/quotes/brokers&symbols', { const response = await fetch('http://localhost:8000/api/brokers&symbols', {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -17,7 +17,6 @@ export const useBrokerSymbols = () => {
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
// console.error('broker and symbol data fetch error:', errorData); // console.error('broker and symbol data fetch error:', errorData);
throw new Error('Unable to fetch broker and symbols'); throw new Error('Unable to fetch broker and symbols');
} }
@ -38,7 +37,7 @@ function quoteDataLink(params: QueryOptions): string {
symbol_b: params.symbolB, symbol_b: params.symbolB,
}, },
{ skipEmptyString: true, skipNull: true }); { skipEmptyString: true, skipNull: true });
return `http://localhost:8000/api/quotes/api/data?${query}`; return `http://localhost:8000/api/data?${query}`;
} }
export const useQuoteData = (params: QueryOptions) => { export const useQuoteData = (params: QueryOptions) => {
@ -54,54 +53,11 @@ export const useQuoteData = (params: QueryOptions) => {
}, },
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
// console.error('Quote data fetch error:', errorData); // console.error('Quote data fetch error:', errorData);
throw new Error('Unable to fetch quote data'); throw new Error('Unable to fetch quote data');
} }
const quoteData = await response.json(); const quoteData = await response.json();
// console.log('Fetched quote data:', quoteData); return quoteData as QuoteData;
const records = quoteData.data || [];
// console.log('Records:', records);
const recordsA = records.filter(
(record: QuoteRecord) => record.broker === params.brokerA && record.symbol
=== params.symbolA
);
const recordsB = records.filter(
(record: QuoteRecord) => record.broker === params.brokerB && record.symbol
=== params.symbolB
);
// console.log('recordsA:', recordsA, 'recordsB:', recordsB);
if (!recordsA || !recordsB) {
throw new Error('No matching records found for the selected brokers and symbols');
}
const timestamps = [...new Set([...recordsA, ...recordsB].map(record => record.timestamp))
].sort();
const quoteDataResult: QuoteData = {
brokerA: params.brokerA,
brokerB: params.brokerB,
symbolA: params.symbolA,
symbolB: params.symbolB,
records: timestamps.map(timestamp => {
const recordA = recordsA.find((record: QuoteRecord) =>
record.timestamp === timestamp) || {};
const recordB = recordsB.find((record: QuoteRecord) =>
record.timestamp === timestamp) || {};
return {
askA: recordA.ask_price ?? null,
bidA: recordA.bid_price ?? null,
askB: recordB.ask_price ?? null,
bidB: recordB.bid_price ?? null,
spreadA: recordA.spread ?? null,
spreadB: recordB.spread ?? null,
midlineA: recordA.midline ?? null,
midlineB: recordB.midline ?? null,
timestamp,
};
})
};
// console.log('Processed quote data:', quoteDataResult);
return quoteDataResult;
}, },
enabled: !!params.brokerA && !!params.symbolA && !!params.brokerB && !!params.symbolB, enabled: !!params.brokerA && !!params.symbolA && !!params.brokerB && !!params.symbolB,
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,

View File

@ -14,6 +14,7 @@ export interface QueryOptions {
export interface QuoteRecord { export interface QuoteRecord {
bid_price: number; bid_price: number;
ask_price: number; ask_price: number;
direction: string;
spread: number; spread: number;
midline: number; midline: number;
timestamp: string; timestamp: string;
@ -21,22 +22,29 @@ export interface QuoteRecord {
symbol: string; symbol: string;
} }
export interface QuoteRecordData {
askA: number | null;
askB: number | null;
bidA: number | null;
bidB: number | null;
directionA?: string;
directionB?: string;
spreadA: number | null;
spreadB: number | null;
midlineA: number | null;
midlineB: number | null;
timestamp: string;
}
export interface QuoteData { export interface QuoteData {
brokerA: string; brokerA: string;
symbolA: string; symbolA: string;
brokerB: string; brokerB: string;
symbolB: string; symbolB: string;
records: { records: QuoteRecordData[];
askA: number | null; lastDataPoints?: Record<string, { timestamp: string | null; value: number | null }>;
askB: number | null; markableRecords?: QuoteRecordData[];
bidA: number | null;
bidB: number | null;
spreadA: number | null;
spreadB: number | null;
midlineA: number | null;
midlineB: number | null;
timestamp: string;
}[];
} }
export interface QueryContextType { export interface QueryContextType {

View File

@ -1,75 +1,360 @@
import React, {useRef, useState} from "react"; import React, {useRef, useState, useEffect} from "react";
import { useQuoteData } from "../QueryOptions/apiQueryOptions"; import { useQuoteData } from "../QueryOptions/apiQueryOptions";
import { useQueryContext } from "../context/querycontext"; import { useQueryContext } from "../context/querycontext";
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import { type QuoteData } from "../QueryOptions/dataTypes";
const ChartComponents: React.FC = () => { const ChartComponents: React.FC = () => {
const chartRef = useRef<ReactECharts>(null); const chartRef = useRef<ReactECharts>(null);
const { queryOptions, isLoadingBrokers, isErrorBrokers } = const { queryOptions, isLoadingBrokers, isErrorBrokers } =
useQueryContext(); useQueryContext();
const {data: quoteData, isError: isErrorQuotes } =
useQuoteData(queryOptions) as { data: QuoteData | undefined; isLoading: boolean; isError: boolean };
const { data: quoteData, isLoading: isLoadingQuotes, isError: isErrorQuotes } = const [showMidline, setShowMidline] = useState(false);
useQuoteData(queryOptions);
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,
});
const [showMidline, setShowMidline] = useState(false); 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;
// 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 const minValueA = valuesA.length ? Math.min(...valuesA) - buffer : 0;
|| record.bidA !== null || record.askB !== null || record.bidB !== null || record.midlineB) const maxValueA = valuesA.length ? Math.max(...valuesA) + buffer : 100;
.map(record => ({ const adjustedMinValueA = Math.min(minValueA, latestValues.lastAskA || minValueA) - buffer;
timestamp: record.timestamp, const adjustedMaxValueA = Math.max(maxValueA, latestValues.lastAskA || maxValueA) + buffer;
askA: record.askA, const minValueB = valuesB.length ? Math.min(...valuesB) - buffer : 0;
askB: record.askB, const maxValueB = valuesB.length ? Math.max(...valuesB) + buffer : 100;
bidA: record.bidA, const adjustedMinValueB = Math.min(minValueB, latestValues.lastAskB || minValueB) - buffer;
bidB: record.bidB, const adjustedMaxValueB = Math.max(maxValueB, latestValues.lastAskB || maxValueB) + buffer;
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 {
const option = { // 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: { dataset: {
source: datasetSource, source: filteredData,
dimensions: ['timestamp', 'askA', 'askB', 'bidA', 'bidB', 'midlineA', 'midlineB'], dimensions: ['timestamp', 'askA', 'askB', 'bidA', 'bidB', 'midlineA', 'midlineB'],
}, },
tooltip: { tooltip: {
@ -86,205 +371,150 @@ const ChartComponents: React.FC = () => {
}, },
legend: { legend: {
data: [ data: [
`Ask A (${quoteData.brokerA}/${quoteData.symbolA})`, `Ask A (${brokerA}/${symbolA})`,
`Bid A (${quoteData.brokerA}/${quoteData.symbolA})`, `Bid A (${brokerA}/${symbolA})`,
`Midline A (${quoteData.brokerA}/${quoteData.symbolA})`, `Midline A (${brokerA}/${symbolA})`,
`Ask B (${quoteData.brokerB}/${quoteData.symbolB})`, // `Spread A (${brokerA}/${symbolA})`,
`Bid B (${quoteData.brokerB}/${quoteData.symbolB})`, `Ask B (${brokerB}/${symbolB})`,
`Midline B (${quoteData.brokerB}/${quoteData.symbolB})`, `Bid B (${brokerB}/${symbolB})`,
`Midline B (${brokerB}/${symbolB})`,
// `Spread B (${brokerB}/${symbolB})`,
], ],
selected: { selected: {
[`Ask A (${quoteData.brokerA}/${quoteData.symbolA})`]: !showMidline, [`Ask A (${brokerA}/${symbolA})`]: !showMidline,
[`Ask B (${quoteData.brokerB}/${quoteData.symbolB})`]: !showMidline, [`Bid A (${brokerA}/${symbolA})`]: !showMidline,
[`Bid A (${quoteData.brokerA}/${quoteData.symbolA})`]: !showMidline, [`Ask B (${brokerB}/${symbolB})`]: !showMidline,
[`Bid B (${quoteData.brokerB}/${quoteData.symbolB})`]: !showMidline, [`Bid B (${brokerB}/${symbolB})`]: !showMidline,
[`Midline A (${quoteData.brokerA}/${quoteData.symbolA})`]: showMidline, [`Midline A (${brokerA}/${symbolA})`]: showMidline,
[`Midline B (${quoteData.brokerB}/${quoteData.symbolB})`]: showMidline, [`Midline B (${brokerB}/${symbolB})`]: showMidline,
// [`Spread A (${brokerA}/${symbolA})`]: showMidline,
// [`Spread B (${brokerB}/${symbolB})`]: showMidline,
}, },
}, },
grid: { grid: { left: '5%', right: '5%', bottom: '15%', containLabel: true },
left: '5%', toolbox: { feature: { dataZoom: {}, saveAsImage: {}, brush: { type: ['rect', 'polygon', 'clear'] } } },
right: '5%', dataZoom,
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: { xAxis: {
type: 'time', type: 'time',
name: 'Timestamp', name: 'Timestamp',
axisLabel: { axisLabel: { formatter: (value: number) => new Date(value).toLocaleString() },
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: [ yAxis: [
{ {
type: 'value', type: 'value',
name: `${quoteData.brokerA}/${quoteData.symbolA}`, name: `${brokerA}/${symbolA}`,
min: minValueA, min: adjustedMinValueA,
max: maxValueA, max: adjustedMaxValueA,
axisLabel: { position: 'left',
formatter: (value: number) => value.toFixed(5), axisLabel: { formatter: (value: number) => value.toFixed(5) },
},
// alignTicks:true,
scale: true, scale: true,
splitNumber: 10, splitNumber: 10,
}, },
{ {
type: 'value', type: 'value',
name: `${quoteData.brokerB}/${quoteData.symbolB}`, name: `${brokerB}/${symbolB}`,
min: minValueB, min: adjustedMinValueB,
max: maxValueB, max: adjustedMaxValueB,
position: 'right', position: 'right',
axisLabel: { axisLabel: { formatter: (value: number) => value.toFixed(5) },
formatter: (value: number) => value.toFixed(5),
},
// alignTicks:true,
scale: true, scale: true,
splitNumber: 10, splitNumber: 10,
}, },
], ],
series: [ series: [
{ {
name: `Ask A (${quoteData.brokerA}/${quoteData.symbolA})`, name: `Ask A (${brokerA}/${symbolA})`,
type: 'line', type: 'line',
data: datasetSource data: filteredData.filter(record => record.askA !== null).map(record => [record.timestamp, record.askA]),
.filter(record => record.askA !== null)
.map(record => [record.timestamp, record.askA]),
yAxisIndex: 0, yAxisIndex: 0,
lineStyle: { color: '#007bff', width: 2 }, lineStyle: { color: '#007bff', width: 2 },
itemStyle: { color: '#007bff' }, itemStyle: { color: '#007bff' },
showSymbol: true, showSymbol: false,
symbol: 'circle',
symbolSize: 4,
connectNulls: false, connectNulls: false,
markLine: marklineAskA,
}, },
{ {
name: `Bid A (${quoteData.brokerA}/${quoteData.symbolA})`, name: `Bid A (${brokerA}/${symbolA})`,
type: 'line', type: 'line',
data: datasetSource data: filteredData.filter(record => record.bidA !== null).map(record => [record.timestamp, record.bidA]),
.filter(record => record.bidA !== null)
.map(record => [record.timestamp, record.bidA]),
yAxisIndex: 0, yAxisIndex: 0,
lineStyle: { color: '#00b7eb', type: 'dashed', width: 2 }, lineStyle: { color: '#00b7eb', type: 'dashed', width: 2 },
itemStyle: { color: '#00b7eb' }, itemStyle: { color: '#00b7eb' },
showSymbol: true, showSymbol: false,
symbol: 'circle',
symbolSize: 4,
connectNulls: false, connectNulls: false,
markPoint: markPointDataA,
markLine: marklineBidA,
}, },
{ {
name: `Ask B (${quoteData.brokerB}/${quoteData.symbolB})`, name: `Midline A (${brokerA}/${symbolA})`,
type: 'line', type: 'line',
data: datasetSource data: filteredData.filter(record => record.midlineA !== null).map(record => [record.timestamp, record.midlineA]),
.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, yAxisIndex: 0,
lineStyle: { color: '#28a745', width: 2 }, lineStyle: { color: '#28a745', width: 2 },
itemStyle: { color: '#28a745' }, itemStyle: { color: '#28a745' },
showSymbol: true, showSymbol: false,
symbol: 'circle',
symbolSize: 4,
connectNulls: 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: `Midline B (${quoteData.brokerB}/${quoteData.symbolB})`, name: `Bid B (${brokerB}/${symbolB})`,
type: 'line', type: 'line',
data: datasetSource data: filteredData.filter(record => record.bidB !== null).map(record => [record.timestamp, record.bidB]),
.filter(record => record.midlineB !== null) yAxisIndex: 1,
.map(record => [record.timestamp, record.midlineB]), 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, yAxisIndex: 1,
lineStyle: { color: '#9932cc', width: 2 }, lineStyle: { color: '#9932cc', width: 2 },
itemStyle: { color: '#9932cc' }, itemStyle: { color: '#9932cc' },
showSymbol: true, showSymbol: false,
symbol: 'square',
symbolSize: 4,
connectNulls: 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 ( return (
<div className= "p-4"> <div className="p-4">
<h2 className="text-2xl font-bold text-gray-800 dark:text-gray-500 mb-4">Quote Chart</h2> <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"> <div className="flex space-x-4 mb-4">
<label className="flex items-center space-x-2 text-gray-700 dark:text-gray-500"> <label className="flex items-center space-x-2 text-gray-700 dark:text-gray-500">
@ -295,7 +525,7 @@ const ChartComponents: React.FC = () => {
onChange={() => setShowMidline(false)} onChange={() => setShowMidline(false)}
className="form-radio text-blue-500 focus:ring-blue-500" className="form-radio text-blue-500 focus:ring-blue-500"
/> />
<span>Show Ask/Bid</span> <span>Show Ask/Bid/Spread</span>
</label> </label>
<label className="flex items-center space-x-2 text-gray-700 dark:text-gray-500"> <label className="flex items-center space-x-2 text-gray-700 dark:text-gray-500">
<input <input
@ -308,18 +538,16 @@ const ChartComponents: React.FC = () => {
<span>Show Midline</span> <span>Show Midline</span>
</label> </label>
</div> </div>
<ReactECharts <ReactECharts
echarts={echarts} echarts={echarts}
ref = {chartRef} ref={chartRef}
option={option} option={option}
style={{ height: 600, width: '100%' }} style={{ height: 600, width: '100%' }}
opts={{ renderer: 'canvas' }} opts={{ renderer: 'canvas' }}
theme="dark" theme="dark"
/> />
</div> </div>
) );
}; };