Files
crypto_trader/frontend/src/components/SpreadChart.tsx

254 lines
10 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import {
Box,
Paper,
Typography,
Grid,
Chip,
Tooltip,
CircularProgress,
} from '@mui/material'
import {
TrendingUp,
TrendingDown,
SwapHoriz,
} from '@mui/icons-material'
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
ResponsiveContainer,
ReferenceLine,
} from 'recharts'
import { marketDataApi } from '../api/marketData'
interface SpreadChartProps {
primarySymbol: string
secondarySymbol: string
lookbackPeriod?: number
zScoreThreshold?: number
}
export default function SpreadChart({
primarySymbol,
secondarySymbol,
lookbackPeriod = 20,
zScoreThreshold = 2.0,
}: SpreadChartProps) {
// Fetch spread data from backend
const { data: spreadResponse, isLoading, error } = useQuery({
queryKey: ['spread-data', primarySymbol, secondarySymbol, lookbackPeriod],
queryFn: () => marketDataApi.getSpreadData(
primarySymbol,
secondarySymbol,
'1h',
lookbackPeriod + 30
),
refetchInterval: 60000, // Refresh every minute
staleTime: 30000,
})
const spreadData = spreadResponse?.data ?? []
const currentZScore = spreadResponse?.currentZScore ?? 0
const currentSpread = spreadResponse?.currentSpread ?? 0
// Determine signal state
const getSignalState = () => {
if (currentZScore > zScoreThreshold) {
return { label: `Short Spread (Sell ${primarySymbol})`, color: 'error' as const, icon: <TrendingDown /> }
} else if (currentZScore < -zScoreThreshold) {
return { label: `Long Spread (Buy ${primarySymbol})`, color: 'success' as const, icon: <TrendingUp /> }
}
return { label: 'Neutral (No Signal)', color: 'default' as const, icon: <SwapHoriz /> }
}
const signalState = getSignalState()
if (isLoading) {
return (
<Paper sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: 400 }}>
<CircularProgress />
<Typography sx={{ ml: 2 }}>Loading spread data...</Typography>
</Paper>
)
}
if (error) {
return (
<Paper sx={{ p: 2, minHeight: 400 }}>
<Typography color="error">Failed to load spread data: {(error as Error).message}</Typography>
</Paper>
)
}
return (
<Paper sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box>
<Typography variant="h6">
Pairs Trading: {primarySymbol} / {secondarySymbol}
</Typography>
<Typography variant="body2" color="text.secondary">
Statistical Arbitrage - Spread Analysis
</Typography>
</Box>
<Chip
icon={signalState.icon}
label={signalState.label}
color={signalState.color}
variant="outlined"
/>
</Box>
{/* Key Metrics */}
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={4}>
<Paper elevation={0} sx={{ p: 1.5, bgcolor: 'background.default', textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary">Current Spread</Typography>
<Typography variant="h5">{currentSpread?.toFixed(4) ?? 'N/A'}</Typography>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper
elevation={0}
sx={{
p: 1.5,
bgcolor: Math.abs(currentZScore) > zScoreThreshold
? (currentZScore > 0 ? 'error.dark' : 'success.dark')
: 'background.default',
textAlign: 'center',
transition: 'background-color 0.3s',
}}
>
<Typography variant="caption" color="text.secondary">Z-Score</Typography>
<Typography
variant="h5"
sx={{
color: Math.abs(currentZScore) > zScoreThreshold ? 'white' : 'inherit'
}}
>
{currentZScore?.toFixed(2) ?? 'N/A'}
</Typography>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper elevation={0} sx={{ p: 1.5, bgcolor: 'background.default', textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary">Threshold</Typography>
<Typography variant="h5">±{zScoreThreshold}</Typography>
</Paper>
</Grid>
</Grid>
{/* Z-Score Visual Gauge */}
<Box sx={{ mb: 3, px: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" color="success.main">-{zScoreThreshold} (Buy)</Typography>
<Typography variant="caption" color="text.secondary">0 (Neutral)</Typography>
<Typography variant="caption" color="error.main">+{zScoreThreshold} (Sell)</Typography>
</Box>
<Tooltip title={`Current Z-Score: ${currentZScore?.toFixed(2) ?? 'N/A'}`}>
<Box sx={{ position: 'relative', height: 20, bgcolor: 'background.default', borderRadius: 1 }}>
{/* Threshold zones */}
<Box sx={{
position: 'absolute',
left: 0,
width: `${(1 - zScoreThreshold / 4) * 50}%`,
height: '100%',
bgcolor: 'success.main',
opacity: 0.2,
borderRadius: '4px 0 0 4px'
}} />
<Box sx={{
position: 'absolute',
right: 0,
width: `${(1 - zScoreThreshold / 4) * 50}%`,
height: '100%',
bgcolor: 'error.main',
opacity: 0.2,
borderRadius: '0 4px 4px 0'
}} />
{/* Current position indicator */}
<Box sx={{
position: 'absolute',
left: `${Math.min(100, Math.max(0, (currentZScore + 4) / 8 * 100))}%`,
transform: 'translateX(-50%)',
width: 4,
height: '100%',
bgcolor: Math.abs(currentZScore) > zScoreThreshold
? (currentZScore > 0 ? 'error.main' : 'success.main')
: 'primary.main',
borderRadius: 1,
}} />
</Box>
</Tooltip>
</Box>
{/* Spread Chart */}
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
Spread History (Ratio: {primarySymbol} / {secondarySymbol})
</Typography>
<Box sx={{ height: 200 }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={spreadData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
dataKey="timestamp"
tickFormatter={(t) => new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
stroke="rgba(255,255,255,0.5)"
/>
<YAxis stroke="rgba(255,255,255,0.5)" domain={['auto', 'auto']} />
<RechartsTooltip
contentStyle={{ backgroundColor: '#1e1e1e', border: '1px solid #333' }}
labelFormatter={(t) => new Date(t).toLocaleString()}
/>
<Line
type="monotone"
dataKey="spread"
stroke="#8884d8"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</Box>
{/* Z-Score Chart */}
<Typography variant="subtitle2" color="text.secondary" gutterBottom sx={{ mt: 2 }}>
Z-Score History
</Typography>
<Box sx={{ height: 150 }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={spreadData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
dataKey="timestamp"
tickFormatter={(t) => new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
stroke="rgba(255,255,255,0.5)"
/>
<YAxis stroke="rgba(255,255,255,0.5)" domain={[-4, 4]} />
<RechartsTooltip
contentStyle={{ backgroundColor: '#1e1e1e', border: '1px solid #333' }}
labelFormatter={(t) => new Date(t).toLocaleString()}
/>
{/* Threshold lines */}
<ReferenceLine y={zScoreThreshold} stroke="#f44336" strokeDasharray="5 5" />
<ReferenceLine y={-zScoreThreshold} stroke="#4caf50" strokeDasharray="5 5" />
<ReferenceLine y={0} stroke="rgba(255,255,255,0.3)" />
<Line
type="monotone"
dataKey="zScore"
stroke="#82ca9d"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</Box>
</Paper>
)
}