diff --git a/frontend/src/components/ticker/RecommendationPanel.tsx b/frontend/src/components/ticker/RecommendationPanel.tsx
index f610a6b..b2d4ced 100644
--- a/frontend/src/components/ticker/RecommendationPanel.tsx
+++ b/frontend/src/components/ticker/RecommendationPanel.tsx
@@ -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 (
@@ -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 (
R:R
{setup.rr_ratio.toFixed(2)}
+ {sizing ? (
+
+
+ Position size · {risk.riskPct}% of {formatPrice(risk.accountSize)}
+
+
+
+
{sizing.shares}
+
shares
+
+
+
{formatPrice(sizing.positionValue)}
+
position
+
+
+
{formatPrice(sizing.dollarRisk)}
+
at risk
+
+
+ {sizing.exceedsAccount && (
+
Position exceeds account — needs margin.
+ )}
+
+ ) : (
+ Set account size below to size this trade.
+ )}
+
{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) => void }) {
+ return (
+
+ Risk settings:
+
+
+ saved in this browser
+
+ );
+}
+
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
{summary.reasoning}
)}
+
+
{preferredDirection !== 'neutral' && preferredSetup ? (
-
+
{alternativeSetup && (
@@ -195,15 +259,15 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPric
Alternative scenario ({alternativeSetup.direction.toUpperCase()})
-
+
)}
) : (
-
-
+
+
)}
diff --git a/frontend/src/hooks/useRiskSettings.ts b/frontend/src/hooks/useRiskSettings.ts
new file mode 100644
index 0000000..9c35ebd
--- /dev/null
+++ b/frontend/src/hooks/useRiskSettings.ts
@@ -0,0 +1,37 @@
+import { useCallback, useEffect, useState } from 'react';
+
+export interface RiskSettings {
+ accountSize: number;
+ riskPct: number;
+}
+
+const STORAGE_KEY = 'risk-settings';
+const DEFAULTS: RiskSettings = { accountSize: 10000, riskPct: 1 };
+
+/** Account size + per-trade risk %, persisted in localStorage (per browser). */
+export function useRiskSettings() {
+ const [settings, setSettings] = useState(() => {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) return { ...DEFAULTS, ...JSON.parse(raw) };
+ } catch {
+ /* ignore malformed storage */
+ }
+ return DEFAULTS;
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
+ } catch {
+ /* ignore quota/availability errors */
+ }
+ }, [settings]);
+
+ const update = useCallback(
+ (patch: Partial) => setSettings((s) => ({ ...s, ...patch })),
+ [],
+ );
+
+ return { settings, update };
+}
diff --git a/frontend/src/lib/position.ts b/frontend/src/lib/position.ts
new file mode 100644
index 0000000..7d4e3f6
--- /dev/null
+++ b/frontend/src/lib/position.ts
@@ -0,0 +1,37 @@
+export interface PositionSize {
+ shares: number;
+ riskPerShare: number;
+ dollarRisk: number;
+ positionValue: number;
+ /** Position value exceeds the account → needs margin / not affordable in cash. */
+ exceedsAccount: boolean;
+}
+
+/**
+ * Risk-based position sizing. Risk a fixed % of the account per trade; the stop
+ * distance sets how many shares that budget buys:
+ * shares = floor((account × risk%) / |entry − stop|)
+ * Returns null when inputs are unusable (no account, no risk, zero stop width).
+ */
+export function positionSize(
+ accountSize: number,
+ riskPct: number,
+ entry: number,
+ stop: number,
+): PositionSize | null {
+ const riskPerShare = Math.abs(entry - stop);
+ if (!(accountSize > 0) || !(riskPct > 0) || !(riskPerShare > 0) || !(entry > 0)) {
+ return null;
+ }
+ const budget = accountSize * (riskPct / 100);
+ const shares = Math.floor(budget / riskPerShare);
+ const dollarRisk = shares * riskPerShare;
+ const positionValue = shares * entry;
+ return {
+ shares,
+ riskPerShare,
+ dollarRisk,
+ positionValue,
+ exceedsAccount: positionValue > accountSize,
+ };
+}
diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo
index ec12067..70aa034 100644
--- a/frontend/tsconfig.tsbuildinfo
+++ b/frontend/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/activation.ts","./src/api/admin.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/fundamentals.ts","./src/api/health.ts","./src/api/indicators.ts","./src/api/ingestion.ts","./src/api/jobs.ts","./src/api/ohlcv.ts","./src/api/performance.ts","./src/api/scores.ts","./src/api/sentiment.ts","./src/api/sr-levels.ts","./src/api/tickers.ts","./src/api/trades.ts","./src/api/watchlist.ts","./src/components/admin/activationsettings.tsx","./src/components/admin/alertsettings.tsx","./src/components/admin/datacleanup.tsx","./src/components/admin/jobcontrols.tsx","./src/components/admin/pipelinereadinesspanel.tsx","./src/components/admin/recommendationsettings.tsx","./src/components/admin/sentimentprovidersettings.tsx","./src/components/admin/settingsform.tsx","./src/components/admin/tickermanagement.tsx","./src/components/admin/tickeruniversebootstrap.tsx","./src/components/admin/usertable.tsx","./src/components/auth/protectedroute.tsx","./src/components/charts/candlestickchart.tsx","./src/components/layout/appshell.tsx","./src/components/layout/mobilenav.tsx","./src/components/layout/sidebar.tsx","./src/components/rankings/rankingstable.tsx","./src/components/rankings/weightsform.tsx","./src/components/scanner/tradetable.tsx","./src/components/signals/setupspanel.tsx","./src/components/signals/trackrecordpanel.tsx","./src/components/ticker/dimensionbreakdownpanel.tsx","./src/components/ticker/fundamentalspanel.tsx","./src/components/ticker/indicatorselector.tsx","./src/components/ticker/recommendationpanel.tsx","./src/components/ticker/sroverlay.tsx","./src/components/ticker/sentimentpanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/callout.tsx","./src/components/ui/confirmdialog.tsx","./src/components/ui/disclosure.tsx","./src/components/ui/dropdown.tsx","./src/components/ui/field.tsx","./src/components/ui/pageheader.tsx","./src/components/ui/scorecard.tsx","./src/components/ui/section.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/watchlist/addtickerform.tsx","./src/components/watchlist/watchlisttable.tsx","./src/hooks/useactivation.ts","./src/hooks/useadmin.ts","./src/hooks/useauth.ts","./src/hooks/usefetchsymboldata.ts","./src/hooks/useperformance.ts","./src/hooks/usescores.ts","./src/hooks/usetickerdetail.ts","./src/hooks/usetickers.ts","./src/hooks/usetrades.ts","./src/hooks/usewatchlist.ts","./src/lib/format.ts","./src/lib/ingestionstatus.ts","./src/lib/qualification.ts","./src/lib/recommendation.ts","./src/lib/types.ts","./src/pages/adminpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/loginpage.tsx","./src/pages/marketpage.tsx","./src/pages/registerpage.tsx","./src/pages/signalspage.tsx","./src/pages/tickerdetailpage.tsx","./src/stores/authstore.ts"],"version":"5.6.3"}
\ No newline at end of file
+{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/activation.ts","./src/api/admin.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/fundamentals.ts","./src/api/health.ts","./src/api/indicators.ts","./src/api/ingestion.ts","./src/api/jobs.ts","./src/api/ohlcv.ts","./src/api/performance.ts","./src/api/scores.ts","./src/api/sentiment.ts","./src/api/sr-levels.ts","./src/api/tickers.ts","./src/api/trades.ts","./src/api/watchlist.ts","./src/components/admin/activationsettings.tsx","./src/components/admin/alertsettings.tsx","./src/components/admin/datacleanup.tsx","./src/components/admin/jobcontrols.tsx","./src/components/admin/pipelinereadinesspanel.tsx","./src/components/admin/recommendationsettings.tsx","./src/components/admin/sentimentprovidersettings.tsx","./src/components/admin/settingsform.tsx","./src/components/admin/tickermanagement.tsx","./src/components/admin/tickeruniversebootstrap.tsx","./src/components/admin/usertable.tsx","./src/components/auth/protectedroute.tsx","./src/components/charts/candlestickchart.tsx","./src/components/layout/appshell.tsx","./src/components/layout/mobilenav.tsx","./src/components/layout/sidebar.tsx","./src/components/rankings/rankingstable.tsx","./src/components/rankings/weightsform.tsx","./src/components/scanner/tradetable.tsx","./src/components/signals/setupspanel.tsx","./src/components/signals/trackrecordpanel.tsx","./src/components/ticker/dimensionbreakdownpanel.tsx","./src/components/ticker/fundamentalspanel.tsx","./src/components/ticker/indicatorselector.tsx","./src/components/ticker/recommendationpanel.tsx","./src/components/ticker/sroverlay.tsx","./src/components/ticker/sentimentpanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/callout.tsx","./src/components/ui/confirmdialog.tsx","./src/components/ui/disclosure.tsx","./src/components/ui/dropdown.tsx","./src/components/ui/field.tsx","./src/components/ui/pageheader.tsx","./src/components/ui/scorecard.tsx","./src/components/ui/section.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/watchlist/addtickerform.tsx","./src/components/watchlist/watchlisttable.tsx","./src/hooks/useactivation.ts","./src/hooks/useadmin.ts","./src/hooks/useauth.ts","./src/hooks/usefetchsymboldata.ts","./src/hooks/useperformance.ts","./src/hooks/userisksettings.ts","./src/hooks/usescores.ts","./src/hooks/usetickerdetail.ts","./src/hooks/usetickers.ts","./src/hooks/usetrades.ts","./src/hooks/usewatchlist.ts","./src/lib/format.ts","./src/lib/ingestionstatus.ts","./src/lib/position.ts","./src/lib/qualification.ts","./src/lib/recommendation.ts","./src/lib/types.ts","./src/pages/adminpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/loginpage.tsx","./src/pages/marketpage.tsx","./src/pages/registerpage.tsx","./src/pages/signalspage.tsx","./src/pages/tickerdetailpage.tsx","./src/stores/authstore.ts"],"version":"5.6.3"}
\ No newline at end of file