done for the day

This commit is contained in:
crowetic 2025-06-06 20:45:57 -07:00
parent 84ab5946e7
commit fd23b2e5a6
2 changed files with 96 additions and 87 deletions

View File

@ -4,7 +4,7 @@ import QortCandlestickChart, {
} from './components/QortCandlestickChart'; } from './components/QortCandlestickChart';
import { import {
aggregateCandles, aggregateCandles,
aggregateDailyCandles, // aggregateDailyCandles,
Trade, Trade,
} from './utils/qortTrades'; } from './utils/qortTrades';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@ -66,57 +66,47 @@ const App: React.FC = () => {
const [isFiltering, setIsFiltering] = useState(false); const [isFiltering, setIsFiltering] = useState(false);
const [isUpdating, setIsUpdating] = useState<Record<string, boolean>>({}); const [isUpdating, setIsUpdating] = useState<Record<string, boolean>>({});
// function filterOutliersPercentile(
// trades: Trade[],
// lower = 0.01,
// upper = 0.99
// ): Trade[] {
// if (trades.length < 10) return trades;
// // Compute prices
// const prices = trades
// .map((t) => parseFloat(t.foreignAmount) / parseFloat(t.qortAmount))
// .filter((x) => isFinite(x) && x > 0);
// // Sort prices
// const sorted = [...prices].sort((a, b) => a - b);
// const lowerIdx = Math.floor(sorted.length * lower);
// const upperIdx = Math.ceil(sorted.length * upper) - 1;
// const min = sorted[lowerIdx];
// const max = sorted[upperIdx];
// // Filter trades within percentile range
// return trades.filter((t) => {
// const price = parseFloat(t.foreignAmount) / parseFloat(t.qortAmount);
// return price >= min && price <= max;
// });
// }
function getLatestTradeTimestamp(trades: Trade[]): number { function getLatestTradeTimestamp(trades: Trade[]): number {
if (!trades || !trades.length) return 0; if (!trades || !trades.length) return 0;
return Math.max(...trades.map((t) => t.tradeTimestamp)); return Math.max(...trades.map((t) => t.tradeTimestamp));
} }
function fastPercentileFilter(trades: Trade[], lower = 0.01, upper = 0.99) { // function fastPercentileFilter(trades: Trade[], lower = 0.01, upper = 0.99) {
// 1. Extract price array (one pass) // // 1. Extract price array (one pass)
const prices = []; // const prices = [];
const validTrades = []; // const validTrades = [];
for (const t of trades) { // for (const t of trades) {
const qort = parseFloat(t.qortAmount); // const qort = parseFloat(t.qortAmount);
const price = parseFloat(t.foreignAmount) / qort; // const price = parseFloat(t.foreignAmount) / qort;
if (isFinite(price) && price > 0) { // if (isFinite(price) && price > 0) {
prices.push(price); // prices.push(price);
validTrades.push({ trade: t, price }); // validTrades.push({ trade: t, price });
} // }
} // }
// 2. Get percentiles (sort once) // // 2. Get percentiles (sort once)
prices.sort((a, b) => a - b); // prices.sort((a, b) => a - b);
const min = prices[Math.floor(prices.length * lower)]; // const min = prices[Math.floor(prices.length * lower)];
const max = prices[Math.ceil(prices.length * upper) - 1]; // const max = prices[Math.ceil(prices.length * upper) - 1];
// 3. Filter in single pass // // 3. Filter in single pass
return validTrades // return validTrades
.filter(({ price }) => price >= min && price <= max) // .filter(({ price }) => price >= min && price <= max)
.map(({ trade }) => trade); // .map(({ trade }) => trade);
// }
function fastPercentileFilter(trades: Trade[], lower = 0.002, upper = 0.998) {
if (trades.length < 200) return trades;
const prices = trades
.map((t) => parseFloat(t.foreignAmount) / parseFloat(t.qortAmount))
.filter((x) => isFinite(x) && x > 0);
const sorted = [...prices].sort((a, b) => a - b);
const lowerIdx = Math.floor(sorted.length * lower);
const upperIdx = Math.ceil(sorted.length * upper) - 1;
const min = sorted[lowerIdx];
const max = sorted[upperIdx];
return trades.filter((t) => {
const price = parseFloat(t.foreignAmount) / parseFloat(t.qortAmount);
return price >= min && price <= max;
});
} }
// --- LocalStorage LOAD on mount --- // --- LocalStorage LOAD on mount ---
@ -146,13 +136,15 @@ const App: React.FC = () => {
console.log('Filter effect skipped - waiting for cacheLoaded'); console.log('Filter effect skipped - waiting for cacheLoaded');
return; return;
} }
setIsFiltering(true); setIsFiltering(true);
setTimeout(() => { setTimeout(() => {
// --- Determine minTimestamp --- // --- Determine minTimestamp ---
const now = new Date(); const now = new Date();
const periodObj = PERIODS.find((p) => p.label === period); const periodObj = PERIODS.find((p) => p.label === period);
let minTimestamp = 0; let minTimestamp = 0;
let useDaily = false; // let useDaily = false;
if (periodObj) { if (periodObj) {
if ('days' in periodObj && periodObj.days !== undefined) { if ('days' in periodObj && periodObj.days !== undefined) {
now.setDate(now.getDate() - periodObj.days); now.setDate(now.getDate() - periodObj.days);
@ -164,12 +156,12 @@ const App: React.FC = () => {
) { ) {
now.setMonth(now.getMonth() - periodObj.months); now.setMonth(now.getMonth() - periodObj.months);
minTimestamp = now.getTime(); minTimestamp = now.getTime();
// For 1M or more, use daily candles // For 1M or more, use daily candles----------------------------------------------DISABLED FORCING DAILY CANDLES, AND MODIFIED FILTERING.
if (periodObj.months >= 1) useDaily = true; // if (periodObj.months >= 6) useDaily = false;
} else if ('months' in periodObj && periodObj.months === null) { } else if ('months' in periodObj && periodObj.months === null) {
// 'All' // 'All'
minTimestamp = 0; minTimestamp = 0;
useDaily = true; // useDaily = false;
} }
} }
// --- Filter trades --- // --- Filter trades ---
@ -177,14 +169,16 @@ const App: React.FC = () => {
let filtered = minTimestamp let filtered = minTimestamp
? trades.filter((t) => t.tradeTimestamp >= minTimestamp) ? trades.filter((t) => t.tradeTimestamp >= minTimestamp)
: trades; : trades;
filtered = fastPercentileFilter(filtered, 0.01, 0.99); filtered = fastPercentileFilter(filtered, 0.00005, 0.995);
// --- Aggregate --- // // --- Aggregate ---
if (useDaily) { // if (useDaily) {
setCandles(aggregateDailyCandles(filtered)); // setCandles(aggregateDailyCandles(filtered));
} else { // } else {
setCandles(aggregateCandles(filtered, interval)); // setCandles(aggregateCandles(filtered, interval));
} // }
setCandles(aggregateCandles(filtered, interval));
setIsFiltering(false); setIsFiltering(false);
}, 10); }, 10);
}, [interval, period, selectedChain, allChainTrades, cacheLoaded]); }, [interval, period, selectedChain, allChainTrades, cacheLoaded]);
@ -300,10 +294,11 @@ const App: React.FC = () => {
total={null} total={null}
chain={selectedChain} chain={selectedChain}
/> />
<Container maxWidth="xl" disableGutters> <Container maxWidth={false} disableGutters>
<Box <Box
sx={{ sx={{
minHeight: '100vh', minHeight: '100vh',
width: '100vw',
background: theme.palette.background.default, background: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
p: { xs: 1, md: 3 }, p: { xs: 1, md: 3 },
@ -313,11 +308,11 @@ const App: React.FC = () => {
<Paper <Paper
elevation={5} elevation={5}
sx={{ sx={{
p: { xs: 1, md: 4 }, width: '100%',
maxWidth: 1800, margin: '36px 0 0 0', // Remove 'auto' to allow full width
margin: '36px auto 0 auto',
background: theme.palette.background.paper, background: theme.palette.background.paper,
boxShadow: theme.shadows[6], boxShadow: theme.shadows[6],
p: { xs: 1, md: 4 },
}} }}
> >
{/* Action Button */} {/* Action Button */}
@ -449,13 +444,13 @@ const App: React.FC = () => {
</Button> </Button>
))} ))}
</Box> </Box>
{/* Chart */}
<Box <Box
sx={{ sx={{
width: '100%', width: '100%',
maxWidth: 1800, height: { xs: 320, md: 520, lg: '60vh' }, // adapt for screen
height: { xs: 320, md: 520 },
mx: 'auto', mx: 'auto',
minHeight: 240,
position: 'relative',
}} }}
> >
<QortCandlestickChart <QortCandlestickChart

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import Chart from 'react-apexcharts'; import Chart from 'react-apexcharts';
import { ApexOptions } from 'apexcharts'; import { ApexOptions } from 'apexcharts';
import { Box, useTheme } from '@mui/material';
export interface Candle { export interface Candle {
x: number; // timestamp x: number; // timestamp
@ -39,11 +40,12 @@ const QortCandlestickChart: React.FC<Props> = ({
}) => { }) => {
const smaData = showSMA ? calculateSMA(candles, 7) : []; const smaData = showSMA ? calculateSMA(candles, 7) : [];
const intervalLabel = interval === 24 * 60 * 60 * 1000 ? '1d' : '1h'; const intervalLabel = interval === 24 * 60 * 60 * 1000 ? '1d' : '1h';
// const [yMin, setYMin] = useState(undefined); const theme = useTheme();
// const [yMax, setYMax] = useState(undefined);
const options: ApexOptions = { const options: ApexOptions = {
chart: { chart: {
type: 'candlestick', type: 'candlestick',
width: '100%',
height: '100%',
background: background, background: background,
toolbar: { toolbar: {
show: true, show: true,
@ -66,25 +68,29 @@ const QortCandlestickChart: React.FC<Props> = ({
title: { title: {
text: `QORT/${pairLabel} Price (${intervalLabel} Candles) (${themeMode === 'dark' ? 'Dark' : 'Light'} Theme)`, text: `QORT/${pairLabel} Price (${intervalLabel} Candles) (${themeMode === 'dark' ? 'Dark' : 'Light'} Theme)`,
align: 'center', align: 'center',
style: { color: textColor, fontWeight: 700, fontSize: '1.11rem' }, style: {
color: theme.palette.text.primary,
fontWeight: 700,
fontSize: '1.11rem',
},
offsetY: 8, //adjust if necessary offsetY: 8, //adjust if necessary
}, },
xaxis: { xaxis: {
type: 'datetime', type: 'datetime',
labels: { style: { colors: textColor } }, labels: { style: { colors: theme.palette.text.primary } },
axisBorder: { color: textColor }, axisBorder: { color: textColor },
axisTicks: { color: textColor }, axisTicks: { color: textColor },
tooltip: { enabled: true }, tooltip: { enabled: true },
}, },
yaxis: { yaxis: {
tooltip: { enabled: true }, tooltip: { enabled: true },
labels: { style: { colors: textColor } }, labels: { style: { colors: theme.palette.text.primary } },
axisBorder: { color: textColor }, axisBorder: { color: textColor },
axisTicks: { color: textColor }, axisTicks: { color: textColor },
}, },
theme: { mode: themeMode }, theme: { mode: themeMode },
legend: { legend: {
labels: { colors: textColor }, labels: { colors: theme.palette.text.primary },
show: showSMA && smaData.length > 0, show: showSMA && smaData.length > 0,
}, },
grid: { grid: {
@ -93,21 +99,14 @@ const QortCandlestickChart: React.FC<Props> = ({
tooltip: { tooltip: {
theme: themeMode, theme: themeMode,
}, },
responsive: [ // plotOptions: {
{ // candlestick: {
breakpoint: 800, // // Width can be a number (pixels) or a string (percentage)
options: { // // e.g., width: 8 (pixels) or width: '80%' (of grid slot)
chart: { height: 320 }, // // @ts-expect-error: width is supported at runtime even if not in types
title: { style: { fontSize: '1rem' } }, // width: '100%',
}, // },
}, // },
{
breakpoint: 1200,
options: {
chart: { height: 400 },
},
},
],
}; };
const series = [ const series = [
@ -118,7 +117,22 @@ const QortCandlestickChart: React.FC<Props> = ({
]; ];
return ( return (
<Chart options={options} series={series} type="candlestick" height={420} /> <Box
sx={{
width: '100%',
height: { xs: 280, sm: 420, md: 540, lg: '60vh' },
minHeight: 240,
maxWidth: '100vw',
}}
>
<Chart
options={options}
series={series}
type="candlestick"
width="100%"
height="100%"
/>
</Box>
); );
}; };