updated
This commit is contained in:
parent
bbf5931c52
commit
7bca898286
5
quote_plotter_client/public/img/green.svg
Normal file
5
quote_plotter_client/public/img/green.svg
Normal 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 |
5
quote_plotter_client/public/img/red.svg
Normal file
5
quote_plotter_client/public/img/red.svg
Normal 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 |
@ -1,13 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
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 = () => {
|
||||
return useQuery<RawBrokerSymbol, Error>({
|
||||
queryKey: ['brokers'],
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -17,7 +17,6 @@ export const useBrokerSymbols = () => {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
// console.error('broker and symbol data fetch error:', errorData);
|
||||
throw new Error('Unable to fetch broker and symbols');
|
||||
}
|
||||
@ -38,7 +37,7 @@ function quoteDataLink(params: QueryOptions): string {
|
||||
symbol_b: params.symbolB,
|
||||
},
|
||||
{ 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) => {
|
||||
@ -54,54 +53,11 @@ export const useQuoteData = (params: QueryOptions) => {
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
// console.error('Quote data fetch error:', errorData);
|
||||
throw new Error('Unable to fetch quote data');
|
||||
}
|
||||
const quoteData = await response.json();
|
||||
// console.log('Fetched quote data:', 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;
|
||||
return quoteData as QuoteData;
|
||||
},
|
||||
enabled: !!params.brokerA && !!params.symbolA && !!params.brokerB && !!params.symbolB,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
|
||||
@ -14,6 +14,7 @@ export interface QueryOptions {
|
||||
export interface QuoteRecord {
|
||||
bid_price: number;
|
||||
ask_price: number;
|
||||
direction: string;
|
||||
spread: number;
|
||||
midline: number;
|
||||
timestamp: string;
|
||||
@ -21,22 +22,29 @@ export interface QuoteRecord {
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export interface QuoteData {
|
||||
brokerA: string;
|
||||
symbolA: string;
|
||||
brokerB: string;
|
||||
symbolB: string;
|
||||
records: {
|
||||
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 {
|
||||
brokerA: string;
|
||||
symbolA: string;
|
||||
brokerB: string;
|
||||
symbolB: string;
|
||||
records: QuoteRecordData[];
|
||||
lastDataPoints?: Record<string, { timestamp: string | null; value: number | null }>;
|
||||
markableRecords?: QuoteRecordData[];
|
||||
}
|
||||
|
||||
export interface QueryContextType {
|
||||
|
||||
@ -1,25 +1,145 @@
|
||||
import React, {useRef, useState} from "react";
|
||||
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 { data: quoteData, isLoading: isLoadingQuotes, isError: isErrorQuotes } =
|
||||
useQuoteData(queryOptions);
|
||||
|
||||
const [showMidline, setShowMidline] = useState(false);
|
||||
|
||||
|
||||
// console.log('quote data before render:', quoteData,
|
||||
// 'isLoadingQuotes:', isLoadingQuotes,
|
||||
// 'quotesError:', isErrorQuotes);
|
||||
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>;
|
||||
@ -27,49 +147,214 @@ const ChartComponents: React.FC = () => {
|
||||
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 { 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('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 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;
|
||||
// console.log('minValueA:', minValueA);
|
||||
// console.log('maxValueA:', maxValueA);
|
||||
// console.log('minValueB:', minValueB);
|
||||
// console.log('maxValueB:', maxValueB);
|
||||
//
|
||||
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: datasetSource,
|
||||
source: filteredData,
|
||||
dimensions: ['timestamp', 'askA', 'askB', 'bidA', 'bidB', 'midlineA', 'midlineB'],
|
||||
},
|
||||
tooltip: {
|
||||
@ -86,205 +371,150 @@ const ChartComponents: React.FC = () => {
|
||||
},
|
||||
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})`,
|
||||
`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 (${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,
|
||||
[`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: {
|
||||
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,
|
||||
},
|
||||
],
|
||||
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) => {
|
||||
const date = new Date(value);
|
||||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
|
||||
},
|
||||
},
|
||||
axisLabel: { formatter: (value: number) => new Date(value).toLocaleString() },
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: `${quoteData.brokerA}/${quoteData.symbolA}`,
|
||||
min: minValueA,
|
||||
max: maxValueA,
|
||||
axisLabel: {
|
||||
formatter: (value: number) => value.toFixed(5),
|
||||
},
|
||||
// alignTicks:true,
|
||||
name: `${brokerA}/${symbolA}`,
|
||||
min: adjustedMinValueA,
|
||||
max: adjustedMaxValueA,
|
||||
position: 'left',
|
||||
axisLabel: { formatter: (value: number) => value.toFixed(5) },
|
||||
scale: true,
|
||||
splitNumber: 10,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: `${quoteData.brokerB}/${quoteData.symbolB}`,
|
||||
min: minValueB,
|
||||
max: maxValueB,
|
||||
name: `${brokerB}/${symbolB}`,
|
||||
min: adjustedMinValueB,
|
||||
max: adjustedMaxValueB,
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
formatter: (value: number) => value.toFixed(5),
|
||||
},
|
||||
// alignTicks:true,
|
||||
axisLabel: { formatter: (value: number) => value.toFixed(5) },
|
||||
scale: true,
|
||||
splitNumber: 10,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: `Ask A (${quoteData.brokerA}/${quoteData.symbolA})`,
|
||||
name: `Ask A (${brokerA}/${symbolA})`,
|
||||
type: 'line',
|
||||
data: datasetSource
|
||||
.filter(record => record.askA !== null)
|
||||
.map(record => [record.timestamp, record.askA]),
|
||||
data: filteredData.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,
|
||||
showSymbol: false,
|
||||
connectNulls: false,
|
||||
markLine: marklineAskA,
|
||||
},
|
||||
{
|
||||
name: `Bid A (${quoteData.brokerA}/${quoteData.symbolA})`,
|
||||
name: `Bid A (${brokerA}/${symbolA})`,
|
||||
type: 'line',
|
||||
data: datasetSource
|
||||
.filter(record => record.bidA !== null)
|
||||
.map(record => [record.timestamp, record.bidA]),
|
||||
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: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
showSymbol: false,
|
||||
connectNulls: false,
|
||||
markPoint: markPointDataA,
|
||||
markLine: marklineBidA,
|
||||
},
|
||||
{
|
||||
name: `Ask B (${quoteData.brokerB}/${quoteData.symbolB})`,
|
||||
name: `Midline A (${brokerA}/${symbolA})`,
|
||||
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]),
|
||||
data: filteredData.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,
|
||||
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: `Midline B (${quoteData.brokerB}/${quoteData.symbolB})`,
|
||||
name: `Bid B (${brokerB}/${symbolB})`,
|
||||
type: 'line',
|
||||
data: datasetSource
|
||||
.filter(record => record.midlineB !== null)
|
||||
.map(record => [record.timestamp, record.midlineB]),
|
||||
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: true,
|
||||
symbol: 'square',
|
||||
symbolSize: 4,
|
||||
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">
|
||||
<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">
|
||||
@ -295,7 +525,7 @@ const ChartComponents: React.FC = () => {
|
||||
onChange={() => setShowMidline(false)}
|
||||
className="form-radio text-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
<span>Show Ask/Bid</span>
|
||||
<span>Show Ask/Bid/Spread</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-2 text-gray-700 dark:text-gray-500">
|
||||
<input
|
||||
@ -308,18 +538,16 @@ const ChartComponents: React.FC = () => {
|
||||
<span>Show Midline</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ReactECharts
|
||||
echarts={echarts}
|
||||
ref = {chartRef}
|
||||
ref={chartRef}
|
||||
option={option}
|
||||
style={{ height: 600, width: '100%' }}
|
||||
opts={{ renderer: 'canvas' }}
|
||||
theme="dark"
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user