Local changes: Updated model training, removed debug instrumentation, and configuration improvements
This commit is contained in:
253
frontend/src/components/SpreadChart.tsx
Normal file
253
frontend/src/components/SpreadChart.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user