265 lines
12 KiB
TypeScript
265 lines
12 KiB
TypeScript
import './SymbolCard.css';
|
||
import { PriceChart } from './PriceChart';
|
||
import { Badge, ProductStatusBadge } from './ui/Primitives';
|
||
|
||
interface SymbolCardProps {
|
||
symbol: string;
|
||
data: {
|
||
price: number;
|
||
change24h: number;
|
||
changeToday: number;
|
||
session: string;
|
||
volatility: string;
|
||
signal: string;
|
||
signalTime?: number;
|
||
tradingMode?: 'Paper' | 'Live' | 'Alerts';
|
||
activePosition?: {
|
||
side: 'BUY' | 'SELL';
|
||
entryPrice: number;
|
||
size: number;
|
||
stopLoss: number;
|
||
takeProfit: number;
|
||
unrealizedPnl?: number;
|
||
unrealizedPnlPercent?: number;
|
||
marketValue?: number;
|
||
} | null;
|
||
priceHistory: Array<{ timestamp: number; price: number }>;
|
||
rules: {
|
||
[ruleName: string]: {
|
||
passed: boolean;
|
||
reason: string;
|
||
isPending?: boolean;
|
||
isSkipped?: boolean;
|
||
metadata?: any;
|
||
};
|
||
};
|
||
profileSignals?: {
|
||
[profileId: string]: {
|
||
profileName?: string;
|
||
signal: string;
|
||
passed: boolean;
|
||
reason?: string;
|
||
execution?: {
|
||
status: 'EXECUTED' | 'BLOCKED' | 'SKIPPED';
|
||
code: string;
|
||
reason: string;
|
||
orderId?: string;
|
||
};
|
||
rules?: {
|
||
[ruleName: string]: {
|
||
passed: boolean;
|
||
reason: string;
|
||
metadata?: any;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
indicators: {
|
||
ema20_1h?: number;
|
||
ema20_15m?: number;
|
||
ema50_4h?: number;
|
||
ema200_4h?: number;
|
||
rsi_1h?: number;
|
||
rsi_15m?: number;
|
||
};
|
||
};
|
||
}
|
||
|
||
const ruleDisplayNames: { [key: string]: string } = {
|
||
'TrendBiasRule': 'Trend',
|
||
'SessionRule': 'Session',
|
||
'ZoneRule': 'Zone',
|
||
'MomentumRule': 'Momentum',
|
||
'EntryTriggerRule': 'Entry',
|
||
'AIAnalysisRule': 'AI',
|
||
'RiskManagementRule': 'Risk'
|
||
};
|
||
|
||
export const SymbolCard = ({ symbol, data }: SymbolCardProps) => {
|
||
const profileSignalEntries = Object.entries(data.profileSignals || {});
|
||
|
||
return (
|
||
<div className="symbol-card">
|
||
<div className="symbol-header">
|
||
<div className="title-area">
|
||
<h2>{symbol}</h2>
|
||
<div className="session-info">
|
||
<ProductStatusBadge status={data.tradingMode}>{data.tradingMode}</ProductStatusBadge>
|
||
<Badge variant="neutral" size="sm">{data.session}</Badge>
|
||
<span className={`vol-label ${data.volatility.toLowerCase()}`}>
|
||
{data.volatility} Vol
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="price-info">
|
||
<div className="price">${data.price.toLocaleString()}</div>
|
||
<div className="change-grid">
|
||
<div className={`change-item ${data.changeToday >= 0 ? 'up' : 'down'}`}>
|
||
Today: {data.changeToday >= 0 ? '+' : ''}{data.changeToday.toFixed(2)}%
|
||
</div>
|
||
<div className={`change-item ${data.change24h >= 0 ? 'up' : 'down'}`}>
|
||
24h: {data.change24h >= 0 ? '+' : ''}{data.change24h.toFixed(2)}%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="signal-status">
|
||
<ProductStatusBadge status={data.signal}>{data.signal || 'NONE'}</ProductStatusBadge>
|
||
{data.signalTime && (
|
||
<span className="signal-time">
|
||
{Math.floor((Date.now() - data.signalTime) / 60000)}m ago
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{profileSignalEntries.length > 0 && (
|
||
<div className="rules-section">
|
||
<h3>Profile Signals</h3>
|
||
<div className="rules-grid">
|
||
{profileSignalEntries.map(([profileId, profileSignal]) => {
|
||
const executionState = profileSignal.execution?.status;
|
||
const executionClass = executionState === 'EXECUTED'
|
||
? 'executed'
|
||
: executionState === 'BLOCKED'
|
||
? 'blocked'
|
||
: 'skipped';
|
||
return (
|
||
<div key={profileId} className="rule-container">
|
||
<Badge
|
||
className="rule-status"
|
||
variant={profileSignal.signal === 'BUY' ? 'success' : profileSignal.signal === 'SELL' ? 'danger' : profileSignal.signal === 'MIXED' ? 'warning' : 'neutral'}
|
||
title={profileSignal.reason || profileSignal.signal}
|
||
>
|
||
<span className="rule-name">
|
||
{(profileSignal.profileName || profileId).slice(0, 18)}
|
||
</span>
|
||
<span className="rule-icon">{profileSignal.signal}</span>
|
||
</Badge>
|
||
{profileSignal.execution && (
|
||
<div className={`profile-execution ${executionClass}`} title={profileSignal.execution.reason}>
|
||
<div className="profile-execution-head">
|
||
<span>{profileSignal.execution.status}</span>
|
||
<code>{profileSignal.execution.code}</code>
|
||
</div>
|
||
<div className="profile-execution-reason">{profileSignal.execution.reason}</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{data.activePosition && (
|
||
<div className={`active-position-panel ${data.activePosition.side.toLowerCase()}`}>
|
||
<div className="pos-header">
|
||
<span className="pos-label">ACTIVE {data.activePosition.side}</span>
|
||
<span className="pos-size">{data.activePosition.size.toFixed(4)} Units</span>
|
||
</div>
|
||
<div className="pos-grid">
|
||
<div className="pos-item">
|
||
<span className="label">Entry</span>
|
||
<span className="value">${data.activePosition.entryPrice.toLocaleString()}</span>
|
||
</div>
|
||
<div className={`pos-item pnl ${data.activePosition.unrealizedPnl! >= 0 ? 'up' : 'down'}`}>
|
||
<span className="label">P/L ($)</span>
|
||
<span className="value">
|
||
{data.activePosition.unrealizedPnl! >= 0 ? '+' : ''}
|
||
{data.activePosition.unrealizedPnl!.toFixed(2)}
|
||
</span>
|
||
</div>
|
||
<div className={`pos-item pnl-percent ${data.activePosition.unrealizedPnlPercent! >= 0 ? 'up' : 'down'}`}>
|
||
<span className="label">Return</span>
|
||
<span className="value">
|
||
{data.activePosition.unrealizedPnlPercent! >= 0 ? '+' : ''}
|
||
{data.activePosition.unrealizedPnlPercent!.toFixed(2)}%
|
||
</span>
|
||
</div>
|
||
<div className="pos-item">
|
||
<span className="label">Value</span>
|
||
<span className="value">${data.activePosition.marketValue?.toLocaleString()}</span>
|
||
</div>
|
||
<div className="pos-item sl">
|
||
<span className="label">SL</span>
|
||
<span className="value">${data.activePosition.stopLoss.toLocaleString()}</span>
|
||
</div>
|
||
<div className="pos-item tp">
|
||
<span className="label">TP</span>
|
||
<span className="value">${data.activePosition.takeProfit.toLocaleString()}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<PriceChart data={data.priceHistory || []} currentPrice={data.price} />
|
||
|
||
<div className="rules-section">
|
||
<h3>Rules</h3>
|
||
<div className="rules-grid">
|
||
{Object.entries(data.rules).map(([ruleName, ruleData]) => {
|
||
const isAI = ruleName === 'AIAnalysisRule';
|
||
const aiData = isAI ? ruleData.metadata : null;
|
||
|
||
return (
|
||
<div key={ruleName} className="rule-container">
|
||
<Badge
|
||
className="rule-status"
|
||
variant={ruleData.isSkipped ? 'neutral' : (ruleData.isPending ? 'warning' : (ruleData.passed ? 'success' : 'danger'))}
|
||
title={ruleData.reason}
|
||
>
|
||
<span className="rule-icon">
|
||
{ruleData.isSkipped ? '➖' : (ruleData.isPending ? '⏳' : (ruleData.passed ? '✓' : '✗'))}
|
||
</span>
|
||
<span className="rule-name">{ruleDisplayNames[ruleName] || ruleName}</span>
|
||
</Badge>
|
||
{isAI && aiData && (
|
||
<div className={`ai-details ${ruleData.passed ? 'passed' : 'failed'}`}>
|
||
{aiData.confidence !== undefined && (
|
||
<div className="ai-confidence">Confidence: {aiData.confidence}%</div>
|
||
)}
|
||
{aiData.reasoning && (
|
||
<div className="ai-reasoning">{aiData.reasoning}</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="indicators-section">
|
||
<h3>Indicators</h3>
|
||
<div className="indicators-grid">
|
||
{data.indicators.rsi_15m !== undefined && (
|
||
<div className="indicator">
|
||
<span className="indicator-label">RSI 15m:</span>
|
||
<span className="indicator-value">{data.indicators.rsi_15m.toFixed(1)}</span>
|
||
</div>
|
||
)}
|
||
{data.indicators.rsi_1h !== undefined && (
|
||
<div className="indicator">
|
||
<span className="indicator-label">RSI 1h:</span>
|
||
<span className="indicator-value">{data.indicators.rsi_1h.toFixed(1)}</span>
|
||
</div>
|
||
)}
|
||
{data.indicators.ema20_15m !== undefined && (
|
||
<div className="indicator">
|
||
<span className="indicator-label">EMA20 15m:</span>
|
||
<span className="indicator-value">${data.indicators.ema20_15m.toLocaleString()}</span>
|
||
</div>
|
||
)}
|
||
{data.indicators.ema20_1h !== undefined && (
|
||
<div className="indicator">
|
||
<span className="indicator-label">EMA20 1h:</span>
|
||
<span className="indicator-value">${data.indicators.ema20_1h.toLocaleString()}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|