add position-size calculator to the recommendation panel
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 34s
Deploy / deploy (push) Successful in 23s

Risk-based sizing on each setup card: shares = floor((account × risk%) /
|entry − stop|), with position value and dollars-at-risk. Account size and
per-trade risk % are editable inline and persisted in localStorage. Flags when
a position would exceed the account (needs margin). Frontend-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 11:26:55 +02:00
parent ff48e4a3ff
commit 1951531453
4 changed files with 144 additions and 6 deletions
@@ -1,6 +1,8 @@
import type { TradeSetup } from '../../lib/types';
import { formatPrice, formatPercent } from '../../lib/format';
import { recommendationActionDirection, recommendationActionLabel } from '../../lib/recommendation';
import { useRiskSettings, type RiskSettings } from '../../hooks/useRiskSettings';
import { positionSize } from '../../lib/position';
interface RecommendationPanelProps {
symbol: string;
@@ -83,7 +85,7 @@ function TargetTable({ setup }: { setup: TradeSetup }) {
);
}
function SetupCard({ setup, action, currentPrice }: { setup?: TradeSetup; action?: TradeSetup['recommended_action']; currentPrice?: number }) {
function SetupCard({ setup, action, currentPrice, risk }: { setup?: TradeSetup; action?: TradeSetup['recommended_action']; currentPrice?: number; risk: RiskSettings }) {
if (!setup) {
return (
<div className="glass-sm p-4 text-xs text-gray-500">
@@ -94,6 +96,7 @@ function SetupCard({ setup, action, currentPrice }: { setup?: TradeSetup; action
const recommended = isRecommended(setup, action);
const drift = entryDrift(setup, currentPrice);
const sizing = positionSize(risk.accountSize, risk.riskPct, setup.entry_price, setup.stop_loss);
return (
<div
@@ -132,6 +135,33 @@ function SetupCard({ setup, action, currentPrice }: { setup?: TradeSetup; action
<div className="text-gray-500">R:R</div><div className="font-mono text-gray-200">{setup.rr_ratio.toFixed(2)}</div>
</div>
{sizing ? (
<div className="rounded border border-white/[0.06] bg-white/[0.02] p-2.5 text-xs">
<p className="mb-1.5 text-[10px] uppercase tracking-wider text-gray-500">
Position size · {risk.riskPct}% of {formatPrice(risk.accountSize)}
</p>
<div className="grid grid-cols-3 gap-2 text-center">
<div>
<div className="font-mono text-sm text-gray-100">{sizing.shares}</div>
<div className="text-[10px] text-gray-500">shares</div>
</div>
<div>
<div className={`font-mono text-sm ${sizing.exceedsAccount ? 'text-amber-400' : 'text-gray-100'}`}>{formatPrice(sizing.positionValue)}</div>
<div className="text-[10px] text-gray-500">position</div>
</div>
<div>
<div className="font-mono text-sm text-gray-100">{formatPrice(sizing.dollarRisk)}</div>
<div className="text-[10px] text-gray-500">at risk</div>
</div>
</div>
{sizing.exceedsAccount && (
<p className="mt-1.5 text-[10px] text-amber-400">Position exceeds account needs margin.</p>
)}
</div>
) : (
<p className="text-[11px] text-gray-600">Set account size below to size this trade.</p>
)}
<TargetTable setup={setup} />
{setup.conflict_flags.length > 0 && (
@@ -143,7 +173,39 @@ function SetupCard({ setup, action, currentPrice }: { setup?: TradeSetup; action
);
}
function RiskSettingsBar({ risk, update }: { risk: RiskSettings; update: (p: Partial<RiskSettings>) => void }) {
return (
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs">
<span className="text-gray-500">Risk settings:</span>
<label className="flex items-center gap-1.5">
<span className="text-gray-500">Account $</span>
<input
type="number"
min={0}
value={risk.accountSize}
onChange={(e) => update({ accountSize: Number(e.target.value) })}
className="w-28 input-glass px-2 py-1 font-mono"
/>
</label>
<label className="flex items-center gap-1.5">
<span className="text-gray-500">Risk %</span>
<input
type="number"
min={0}
max={100}
step={0.25}
value={risk.riskPct}
onChange={(e) => update({ riskPct: Number(e.target.value) })}
className="w-20 input-glass px-2 py-1 font-mono"
/>
</label>
<span className="text-[10px] text-gray-600">saved in this browser</span>
</div>
);
}
export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPrice }: RecommendationPanelProps) {
const { settings: risk, update: updateRisk } = useRiskSettings();
const summary = longSetup?.recommendation_summary ?? shortSetup?.recommendation_summary;
const action = (summary?.action ?? 'NEUTRAL') as TradeSetup['recommended_action'];
const preferredDirection = recommendationActionDirection(action);
@@ -185,9 +247,11 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPric
<p className="text-sm text-gray-300">{summary.reasoning}</p>
)}
<RiskSettingsBar risk={risk} update={updateRisk} />
{preferredDirection !== 'neutral' && preferredSetup ? (
<div className="space-y-3">
<SetupCard setup={preferredSetup} action={action} currentPrice={currentPrice} />
<SetupCard setup={preferredSetup} action={action} currentPrice={currentPrice} risk={risk} />
{alternativeSetup && (
<details className="glass-sm p-3">
@@ -195,15 +259,15 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPric
Alternative scenario ({alternativeSetup.direction.toUpperCase()})
</summary>
<div className="mt-3">
<SetupCard setup={alternativeSetup} action={action} currentPrice={currentPrice} />
<SetupCard setup={alternativeSetup} action={action} currentPrice={currentPrice} risk={risk} />
</div>
</details>
)}
</div>
) : (
<div className="grid gap-4 lg:grid-cols-2">
<SetupCard setup={longSetup} action={action} currentPrice={currentPrice} />
<SetupCard setup={shortSetup} action={action} currentPrice={currentPrice} />
<SetupCard setup={longSetup} action={action} currentPrice={currentPrice} risk={risk} />
<SetupCard setup={shortSetup} action={action} currentPrice={currentPrice} risk={risk} />
</div>
)}
</div>