React frontend with: - Authentication UI - Trading dashboard - ML signals display - Portfolio management Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
204 lines
6.9 KiB
TypeScript
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;
|