Big refactoring
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import type { TradeSetup } from '../../lib/types';
|
||||
import { formatPrice, formatPercent } from '../../lib/format';
|
||||
import { recommendationActionDirection, recommendationActionLabel } from '../../lib/recommendation';
|
||||
|
||||
interface RecommendationPanelProps {
|
||||
symbol: string;
|
||||
longSetup?: TradeSetup;
|
||||
shortSetup?: TradeSetup;
|
||||
}
|
||||
|
||||
function riskClass(risk: TradeSetup['risk_level']) {
|
||||
if (risk === 'Low') return 'text-emerald-400';
|
||||
if (risk === 'Medium') return 'text-amber-400';
|
||||
if (risk === 'High') return 'text-red-400';
|
||||
return 'text-gray-400';
|
||||
}
|
||||
|
||||
function isRecommended(setup: TradeSetup | undefined, action: TradeSetup['recommended_action'] | undefined) {
|
||||
if (!setup || !action) return false;
|
||||
if (setup.direction === 'long') return action.startsWith('LONG');
|
||||
return action.startsWith('SHORT');
|
||||
}
|
||||
|
||||
function TargetTable({ setup }: { setup: TradeSetup }) {
|
||||
if (!setup.targets || setup.targets.length === 0) {
|
||||
return <p className="text-xs text-gray-500">No target probabilities available.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-left text-gray-500 border-b border-white/[0.06]">
|
||||
<th className="py-2 pr-3">Classification</th>
|
||||
<th className="py-2 pr-3">Price</th>
|
||||
<th className="py-2 pr-3">Distance</th>
|
||||
<th className="py-2 pr-3">R:R</th>
|
||||
<th className="py-2">Probability</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{setup.targets.map((target) => (
|
||||
<tr key={`${setup.id}-${target.sr_level_id}-${target.price}`} className="border-b border-white/[0.04]">
|
||||
<td className="py-2 pr-3 text-gray-300">{target.classification}</td>
|
||||
<td className="py-2 pr-3 font-mono text-gray-200">{formatPrice(target.price)}</td>
|
||||
<td className="py-2 pr-3 font-mono text-gray-200">{formatPercent((target.distance_from_entry / setup.entry_price) * 100)}</td>
|
||||
<td className="py-2 pr-3 font-mono text-gray-200">{target.rr_ratio.toFixed(2)}</td>
|
||||
<td className="py-2 font-mono text-gray-200">{target.probability.toFixed(1)}%</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupCard({ setup, action }: { setup?: TradeSetup; action?: TradeSetup['recommended_action'] }) {
|
||||
if (!setup) {
|
||||
return (
|
||||
<div className="glass-sm p-4 text-xs text-gray-500">
|
||||
Setup unavailable for this direction.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const recommended = isRecommended(setup, action);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-direction={setup.direction}
|
||||
className={`glass-sm p-4 space-y-3 ${recommended ? 'border border-emerald-500/40' : 'opacity-80'}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className={`text-sm font-semibold ${setup.direction === 'long' ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{setup.direction.toUpperCase()}
|
||||
</h4>
|
||||
<span className="text-xs text-gray-300">{setup.confidence_score?.toFixed(1) ?? '—'}%</span>
|
||||
</div>
|
||||
|
||||
{!recommended && recommendationActionDirection(action ?? null) !== 'neutral' && (
|
||||
<p className="text-[11px] text-amber-400">Alternative setup (ticker bias currently favors the opposite direction).</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="text-gray-500">Entry</div><div className="font-mono text-gray-200">{formatPrice(setup.entry_price)}</div>
|
||||
<div className="text-gray-500">Stop</div><div className="font-mono text-gray-200">{formatPrice(setup.stop_loss)}</div>
|
||||
<div className="text-gray-500">Primary Target</div><div className="font-mono text-gray-200">{formatPrice(setup.target)}</div>
|
||||
<div className="text-gray-500">R:R</div><div className="font-mono text-gray-200">{setup.rr_ratio.toFixed(2)}</div>
|
||||
</div>
|
||||
|
||||
<TargetTable setup={setup} />
|
||||
|
||||
{setup.conflict_flags.length > 0 && (
|
||||
<div className="rounded border border-amber-500/30 bg-amber-500/10 p-2 text-[11px] text-amber-300">
|
||||
{setup.conflict_flags.join(' • ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RecommendationPanel({ symbol, longSetup, shortSetup }: RecommendationPanelProps) {
|
||||
const summary = longSetup?.recommendation_summary ?? shortSetup?.recommendation_summary;
|
||||
const action = (summary?.action ?? 'NEUTRAL') as TradeSetup['recommended_action'];
|
||||
const preferredDirection = recommendationActionDirection(action);
|
||||
|
||||
const preferredSetup =
|
||||
preferredDirection === 'long'
|
||||
? longSetup
|
||||
: preferredDirection === 'short'
|
||||
? shortSetup
|
||||
: undefined;
|
||||
|
||||
const alternativeSetup =
|
||||
preferredDirection === 'long'
|
||||
? shortSetup
|
||||
: preferredDirection === 'short'
|
||||
? longSetup
|
||||
: undefined;
|
||||
|
||||
if (!longSetup && !shortSetup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="mb-3 text-xs font-medium uppercase tracking-widest text-gray-500">Recommendation</h2>
|
||||
<div className="glass p-5 space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<span className="text-sm font-semibold text-indigo-300">{recommendationActionLabel(action)}</span>
|
||||
<span className={`text-sm font-semibold ${riskClass(summary?.risk_level ?? null)}`}>
|
||||
Risk: {summary?.risk_level ?? '—'}
|
||||
</span>
|
||||
<span className="text-sm text-gray-300">Composite: {summary?.composite_score?.toFixed(1) ?? '—'}</span>
|
||||
<span className="text-xs text-gray-500">{symbol.toUpperCase()}</span>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500">Recommended Action is the ticker-level bias. The preferred setup is shown first; the opposite side is available under Alternative scenario.</p>
|
||||
|
||||
{summary?.reasoning && (
|
||||
<p className="text-sm text-gray-300">{summary.reasoning}</p>
|
||||
)}
|
||||
|
||||
{preferredDirection !== 'neutral' && preferredSetup ? (
|
||||
<div className="space-y-3">
|
||||
<SetupCard setup={preferredSetup} action={action} />
|
||||
|
||||
{alternativeSetup && (
|
||||
<details className="glass-sm p-3">
|
||||
<summary className="cursor-pointer text-xs font-medium text-gray-300">
|
||||
Alternative scenario ({alternativeSetup.direction.toUpperCase()})
|
||||
</summary>
|
||||
<div className="mt-3">
|
||||
<SetupCard setup={alternativeSetup} action={action} />
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<SetupCard setup={longSetup} action={action} />
|
||||
<SetupCard setup={shortSetup} action={action} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user