trading-platform-frontend-v2/src/modules/ml/components/PredictionCard.tsx
rckrdmrd 5b53c2539a feat: Initial commit - Trading Platform Frontend
React frontend with:
- Authentication UI
- Trading dashboard
- ML signals display
- Portfolio management

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 04:30:39 -06:00

204 lines
6.9 KiB
TypeScript

/**
* PredictionCard Component
* Displays ML prediction signal details in a card format
*/
import React from 'react';
import {
ArrowTrendingUpIcon,
ArrowTrendingDownIcon,
ShieldCheckIcon,
ClockIcon,
ChartBarIcon,
BoltIcon,
} from '@heroicons/react/24/solid';
import type { MLSignal } from '../../../services/mlService';
import { AMDPhaseIndicator } from './AMDPhaseIndicator';
interface PredictionCardProps {
signal: MLSignal;
onExecuteTrade?: (signal: MLSignal) => void;
showExecuteButton?: boolean;
className?: string;
}
export const PredictionCard: React.FC<PredictionCardProps> = ({
signal,
onExecuteTrade,
showExecuteButton = true,
className = '',
}) => {
// Calculate signal age
const getSignalAge = () => {
const created = new Date(signal.created_at);
const now = new Date();
const diffMs = now.getTime() - created.getTime();
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
};
// Check if signal is still valid
const isValid = new Date(signal.valid_until) > new Date();
// Calculate potential profit/loss percentages
const calculatePnLPercentages = () => {
const entryPrice = signal.entry_price;
const profitPercent = ((signal.take_profit - entryPrice) / entryPrice) * 100;
const lossPercent = ((entryPrice - signal.stop_loss) / entryPrice) * 100;
return {
profit: Math.abs(profitPercent),
loss: Math.abs(lossPercent),
};
};
const pnl = calculatePnLPercentages();
// Get confidence color
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.7) return 'text-green-400';
if (confidence >= 0.5) return 'text-yellow-400';
return 'text-red-400';
};
return (
<div className={`card p-5 ${!isValid ? 'opacity-60' : ''} ${className}`}>
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div
className={`p-2.5 rounded-lg ${
signal.direction === 'long' ? 'bg-green-500/20' : 'bg-red-500/20'
}`}
>
{signal.direction === 'long' ? (
<ArrowTrendingUpIcon className="w-5 h-5 text-green-400" />
) : (
<ArrowTrendingDownIcon className="w-5 h-5 text-red-400" />
)}
</div>
<div>
<h3 className="text-lg font-bold text-white">{signal.symbol}</h3>
<p className="text-xs text-gray-400">{getSignalAge()}</p>
</div>
</div>
{/* Direction and Confidence Badge */}
<div className="text-right">
<div
className={`inline-flex items-center px-3 py-1 rounded-full font-bold text-sm mb-1 ${
signal.direction === 'long'
? 'bg-green-500/20 text-green-400'
: 'bg-red-500/20 text-red-400'
}`}
>
{signal.direction.toUpperCase()}
</div>
<div className={`text-lg font-bold ${getConfidenceColor(signal.confidence_score)}`}>
{Math.round(signal.confidence_score * 100)}%
</div>
</div>
</div>
{/* AMD Phase Indicator (compact) */}
<div className="mb-4">
<AMDPhaseIndicator
phase={signal.amd_phase as any}
confidence={signal.confidence_score}
compact={true}
/>
</div>
{/* Price Levels */}
<div className="grid grid-cols-3 gap-2 mb-4">
<div className="text-center p-3 bg-gray-800 rounded-lg">
<p className="text-xs text-gray-400 mb-1">Entry</p>
<p className="font-mono font-bold text-white text-sm">
${signal.entry_price.toFixed(2)}
</p>
</div>
<div className="text-center p-3 bg-red-900/30 border border-red-800 rounded-lg">
<p className="text-xs text-red-400 mb-1">Stop Loss</p>
<p className="font-mono font-bold text-red-400 text-sm">
${signal.stop_loss.toFixed(2)}
</p>
<p className="text-xs text-red-300 mt-1">-{pnl.loss.toFixed(1)}%</p>
</div>
<div className="text-center p-3 bg-green-900/30 border border-green-800 rounded-lg">
<p className="text-xs text-green-400 mb-1">Take Profit</p>
<p className="font-mono font-bold text-green-400 text-sm">
${signal.take_profit.toFixed(2)}
</p>
<p className="text-xs text-green-300 mt-1">+{pnl.profit.toFixed(1)}%</p>
</div>
</div>
{/* Metrics Row */}
<div className="grid grid-cols-3 gap-3 mb-4 text-sm">
<div className="flex items-center gap-1.5 text-gray-400">
<ShieldCheckIcon className="w-4 h-4" />
<div>
<p className="text-xs text-gray-500">R:R</p>
<p className="text-white font-bold">{signal.risk_reward_ratio.toFixed(1)}</p>
</div>
</div>
<div className="flex items-center gap-1.5 text-gray-400">
<ChartBarIcon className="w-4 h-4" />
<div>
<p className="text-xs text-gray-500">P(TP)</p>
<p className="text-white font-bold">{Math.round(signal.prob_tp_first * 100)}%</p>
</div>
</div>
<div className="flex items-center gap-1.5 text-gray-400">
<BoltIcon className="w-4 h-4" />
<div>
<p className="text-xs text-gray-500">Vol</p>
<p className="text-white font-bold uppercase text-xs">
{signal.volatility_regime.substring(0, 3)}
</p>
</div>
</div>
</div>
{/* Valid Until */}
<div className="flex items-center gap-2 mb-4 p-2 bg-gray-800 rounded">
<ClockIcon className={`w-4 h-4 ${isValid ? 'text-blue-400' : 'text-red-400'}`} />
<div className="flex-1">
<p className="text-xs text-gray-400">
{isValid ? 'Valid until' : 'Expired at'}
</p>
<p className="text-sm text-white font-medium">
{new Date(signal.valid_until).toLocaleString()}
</p>
</div>
{!isValid && (
<span className="text-xs px-2 py-1 bg-red-900/30 text-red-400 rounded">
EXPIRED
</span>
)}
</div>
{/* Execute Trade Button */}
{showExecuteButton && isValid && onExecuteTrade && (
<button
onClick={() => onExecuteTrade(signal)}
className={`w-full py-3 rounded-lg font-semibold transition-colors ${
signal.direction === 'long'
? 'bg-green-600 hover:bg-green-700 text-white'
: 'bg-red-600 hover:bg-red-700 text-white'
}`}
>
Execute {signal.direction === 'long' ? 'Buy' : 'Sell'} Order
</button>
)}
</div>
);
};
export default PredictionCard;