fix: align production defaults and close review parity gaps
Ship greenfield min_rr=2.0 and conf=0, read-only Structural S/R, indicator cache invalidation, and UI/gate language that treats GTL as screening not exit. Align strategy_rank missing-vol fallback live vs backtest, single-source PRIMARY_TARGET_MIN_RR, expand prod parity tests, and drop dead FE clients.
This commit is contained in:
@@ -56,12 +56,6 @@ export function updateSetting(key: string, value: string) {
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
export function updateRegistration(enabled: boolean) {
|
||||
return apiClient
|
||||
.put<{ message: string }>('admin/settings/registration', { enabled })
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
export function getRecommendationSettings() {
|
||||
return apiClient
|
||||
.get<RecommendationConfig>('admin/settings/recommendations')
|
||||
|
||||
@@ -14,7 +14,3 @@ export function list(params?: TradeListParams) {
|
||||
export function bySymbol(symbol: string) {
|
||||
return apiClient.get<TradeSetup[]>(`trades/${symbol.toUpperCase()}`).then((r) => r.data);
|
||||
}
|
||||
|
||||
export function history(symbol: string) {
|
||||
return apiClient.get<TradeSetup[]>(`trades/${symbol.toUpperCase()}/history`).then((r) => r.data);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ import type { ActivationConfig } from '../../lib/types';
|
||||
import { useActivationSettings, useUpdateActivationSettings } from '../../hooks/useAdmin';
|
||||
import { SkeletonTable } from '../ui/Skeleton';
|
||||
|
||||
/** Mirrors app.services.admin_service.ACTIVATION_DEFAULTS — keep in sync. */
|
||||
const DEFAULTS: ActivationConfig = {
|
||||
min_momentum_percentile: 80,
|
||||
min_rr: 1.2,
|
||||
min_confidence: 55,
|
||||
min_rr: 2.0,
|
||||
min_confidence: 0,
|
||||
require_high_conviction: false,
|
||||
exclude_conflicts: false,
|
||||
exclude_neutral: true,
|
||||
|
||||
@@ -39,7 +39,7 @@ export function RBar({ r, max = 1.6 }: { r: number | null; max?: number }) {
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* PriceRail — stop → entry → now → target laid out spatially */
|
||||
/* PriceRail — stop → entry → now → gate level laid out spatially */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export function PriceRail({
|
||||
@@ -69,7 +69,7 @@ export function PriceRail({
|
||||
const progressWidth = current != null ? Math.abs(pct(current) - pct(entry)) : 0;
|
||||
return (
|
||||
<div className="hz-rail" role="img" aria-label={
|
||||
`Stop ${fmt(stop)}, entry ${fmt(entry)}, now ${current != null ? fmt(current) : 'unknown'}, target ${fmt(target)}`
|
||||
`Stop ${fmt(stop)}, entry ${fmt(entry)}, now ${current != null ? fmt(current) : 'unknown'}, gate ${fmt(target)}`
|
||||
}>
|
||||
<div className="hz-rail-track" />
|
||||
<div
|
||||
@@ -111,10 +111,10 @@ export function PriceRail({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="hz-rail-mark" style={{ left: `${pct(target)}%` }}>
|
||||
<div className="hz-rail-mark" style={{ left: `${pct(target)}%` }} title="Gate level — screening only, not a take-profit">
|
||||
<span className="hz-rail-ring" />
|
||||
<span className="hz-rail-label">
|
||||
<em>target</em>
|
||||
<em>gate</em>
|
||||
<b>{fmt(target)}</b>
|
||||
{rTarget != null && <i>+{fmt(rTarget, 1)}R</i>}
|
||||
</span>
|
||||
|
||||
@@ -4,7 +4,25 @@ import { formatPrice, formatPercent, formatDateTime } from '../../lib/format';
|
||||
import { primaryTarget } from '../../lib/qualification';
|
||||
import { recommendationActionDirection, recommendationActionLabel } from '../../lib/recommendation';
|
||||
|
||||
export type SortColumn = 'symbol' | 'direction' | 'recommended_action' | 'confidence_score' | 'entry_price' | 'stop_loss' | 'target' | 'primary_target_probability' | 'risk_amount' | 'reward_amount' | 'rr_ratio' | 'stop_pct' | 'target_pct' | 'risk_level' | 'composite_score' | 'detected_at';
|
||||
export type SortColumn =
|
||||
| 'symbol'
|
||||
| 'direction'
|
||||
| 'recommended_action'
|
||||
| 'confidence_score'
|
||||
| 'entry_price'
|
||||
| 'stop_loss'
|
||||
| 'target'
|
||||
| 'primary_target_probability'
|
||||
| 'risk_amount'
|
||||
| 'reward_amount'
|
||||
| 'rr_ratio'
|
||||
| 'stop_pct'
|
||||
| 'target_pct'
|
||||
| 'risk_level'
|
||||
| 'composite_score'
|
||||
| 'strategy_rank'
|
||||
| 'momentum_percentile'
|
||||
| 'detected_at';
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
interface TradeTableProps {
|
||||
@@ -16,20 +34,22 @@ interface TradeTableProps {
|
||||
|
||||
const columns: { key: SortColumn; label: string }[] = [
|
||||
{ key: 'symbol', label: 'Symbol' },
|
||||
{ key: 'strategy_rank', label: 'Prod rank' },
|
||||
{ key: 'momentum_percentile', label: 'Mom %ile' },
|
||||
{ key: 'recommended_action', label: 'Recommended Action' },
|
||||
{ key: 'confidence_score', label: 'Confidence' },
|
||||
{ key: 'direction', label: 'Direction' },
|
||||
{ key: 'entry_price', label: 'Entry' },
|
||||
{ key: 'stop_loss', label: 'Stop Loss' },
|
||||
{ key: 'target', label: 'Target' },
|
||||
{ key: 'primary_target_probability', label: 'Primary Target' },
|
||||
{ key: 'target', label: 'Gate level' },
|
||||
{ key: 'primary_target_probability', label: 'Gate reach' },
|
||||
{ key: 'risk_amount', label: 'Risk $' },
|
||||
{ key: 'reward_amount', label: 'Reward $' },
|
||||
{ key: 'rr_ratio', label: 'R:R' },
|
||||
{ key: 'rr_ratio', label: 'Gate R:R' },
|
||||
{ key: 'stop_pct', label: '% to Stop' },
|
||||
{ key: 'target_pct', label: '% to Target' },
|
||||
{ key: 'target_pct', label: '% to gate' },
|
||||
{ key: 'risk_level', label: 'Risk' },
|
||||
{ key: 'composite_score', label: 'Score' },
|
||||
{ key: 'composite_score', label: 'Composite' },
|
||||
{ key: 'detected_at', label: 'Detected' },
|
||||
];
|
||||
|
||||
@@ -105,6 +125,12 @@ export function TradeTable({ trades, sortColumn, sortDirection, onSort }: TradeT
|
||||
{trade.symbol}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200" title="80% residual momentum + 20% vol — production book order">
|
||||
{trade.strategy_rank != null ? trade.strategy_rank.toFixed(1) : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200" title="Residual 12-1 momentum percentile (activation gate)">
|
||||
{trade.momentum_percentile != null ? trade.momentum_percentile.toFixed(0) : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3.5">
|
||||
<div className="space-y-0.5">
|
||||
<span className="text-xs font-semibold text-blue-300">{recommendationActionLabel(trade.recommended_action)}</span>
|
||||
@@ -123,15 +149,21 @@ export function TradeTable({ trades, sortColumn, sortDirection, onSort }: TradeT
|
||||
</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200">{formatPrice(trade.entry_price)}</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200">{formatPrice(trade.stop_loss)}</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200">{formatPrice(trade.target)}</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200">{primaryTargetText(trade)}</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200" title="Gate Target Ladder level — screening only, not an exit">
|
||||
{formatPrice(trade.target)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200" title="Reach probability for the gate level before stop">
|
||||
{primaryTargetText(trade)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200">{formatPrice(analysis.risk_amount)}</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200">{formatPrice(analysis.reward_amount)}</td>
|
||||
<td className={`px-4 py-3.5 font-mono font-semibold ${rrColorClass(trade.rr_ratio)}`}>{trade.rr_ratio.toFixed(2)}</td>
|
||||
<td className={`px-4 py-3.5 font-mono font-semibold ${rrColorClass(trade.rr_ratio)}`} title="Gate R:R — not the live trail exit">
|
||||
{trade.rr_ratio.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200">{formatPercent(analysis.stop_pct)}</td>
|
||||
<td className="px-4 py-3.5 font-mono text-gray-200">{formatPercent(analysis.target_pct)}</td>
|
||||
<td className={`px-4 py-3.5 font-semibold ${riskLevelClass(trade.risk_level)}`}>{trade.risk_level ?? '—'}</td>
|
||||
<td className="px-4 py-3.5">
|
||||
<td className="px-4 py-3.5" title="Display quality only — does not select trades">
|
||||
<span className={`font-semibold ${trade.composite_score > 70 ? 'text-emerald-400' : trade.composite_score >= 40 ? 'text-amber-400' : 'text-red-400'}`}>
|
||||
{Math.round(trade.composite_score)}
|
||||
</span>
|
||||
|
||||
@@ -44,6 +44,10 @@ function getComputedValue(trade: TradeSetup, column: SortColumn): number {
|
||||
case 'confidence_score': return trade.confidence_score ?? -1;
|
||||
case 'primary_target_probability':
|
||||
return primaryTargetProbability(trade) ?? -1;
|
||||
case 'strategy_rank':
|
||||
return trade.strategy_rank ?? trade.momentum_percentile ?? -1;
|
||||
case 'momentum_percentile':
|
||||
return trade.momentum_percentile ?? -1;
|
||||
case 'risk_level':
|
||||
if (trade.risk_level === 'Low') return 1;
|
||||
if (trade.risk_level === 'Medium') return 2;
|
||||
@@ -79,6 +83,8 @@ function sortTrades(
|
||||
case 'target_pct':
|
||||
case 'confidence_score':
|
||||
case 'primary_target_probability':
|
||||
case 'strategy_rank':
|
||||
case 'momentum_percentile':
|
||||
case 'risk_level':
|
||||
cmp = getComputedValue(a, column) - getComputedValue(b, column);
|
||||
break;
|
||||
@@ -108,7 +114,8 @@ export function SetupsPanel() {
|
||||
const [minConfidence, setMinConfidence] = useState(0);
|
||||
const [directionFilter, setDirectionFilter] = useState<DirectionFilter>('both');
|
||||
const [actionFilter, setActionFilter] = useState<ActionFilter>('all');
|
||||
const [sortColumn, setSortColumn] = useState<SortColumn>('rr_ratio');
|
||||
// Production book orders by 80/20 strategy_rank, not raw R:R.
|
||||
const [sortColumn, setSortColumn] = useState<SortColumn>('strategy_rank');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||
|
||||
// Keep the Min R:R / Min Confidence inputs showing the *effective* floor: when
|
||||
@@ -244,10 +251,11 @@ export function SetupsPanel() {
|
||||
|
||||
<Disclosure summary="How the scanner works & action glossary">
|
||||
<p className="mb-3 text-xs text-gray-400">
|
||||
The scanner identifies asymmetric risk-reward trade setups by analyzing S/R levels as
|
||||
price targets and using ATR-based stops to define risk. Click{' '}
|
||||
<span className="font-medium text-gray-300">Run Scanner</span> to scan all tickers now,
|
||||
or wait for the scheduled run.
|
||||
The scanner builds long setups with a 1.5× ATR stop and a Gate Target Ladder proposal used
|
||||
only for R:R / reach-probability screening — not as a take-profit. Structural chart S/R is
|
||||
separate. Live exit is the ATR trail / max hold. Click{' '}
|
||||
<span className="font-medium text-gray-300">Run Scanner</span> to scan all tickers now, or
|
||||
wait for the scheduled run.
|
||||
</p>
|
||||
<div className="grid gap-1 md:grid-cols-2">
|
||||
{RECOMMENDATION_ACTION_GLOSSARY.map((item) => (
|
||||
|
||||
@@ -109,19 +109,19 @@ export function TrackRecordPanel() {
|
||||
<Disclosure summary="Track-record maintenance">
|
||||
<div className="space-y-4 pt-1">
|
||||
<p className="max-w-2xl text-xs text-gray-500">
|
||||
The live check replays every setup against the daily bars after detection: target before stop =
|
||||
win, stop first = loss (both in one bar counts conservatively as a loss), neither within 30
|
||||
trading days = expired at 0R. Only setups whose full window has elapsed count; younger ones are
|
||||
still maturing (near stops resolve fast, far targets need time, so early numbers skew negative).
|
||||
The evaluator scores <span className="text-gray-300">all</span> setups — qualified or not, so
|
||||
unqualified ones stay a control group — and runs nightly.
|
||||
<span className="text-amber-300/90">Diagnostic only — not production P&L.</span>{' '}
|
||||
Grades gate-level touch vs stop (the rejected take-profit model). Production exits are
|
||||
initial stop / ATR trail / max hold — see paper trades and the portfolio monitor above.
|
||||
Target before stop = win, stop first = loss (same-bar both = loss), neither in 30 trading
|
||||
days = expired at 0R. Only matured windows count. Scores{' '}
|
||||
<span className="text-gray-300">all</span> setups as a control group; runs nightly.
|
||||
</p>
|
||||
|
||||
{/* Diagnostic, not strategy validation: live target/stop outcomes vs the backtest's target/stop model. */}
|
||||
<div className="glass-sm space-y-2 p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-2">
|
||||
<div className="flex flex-wrap items-baseline gap-x-5 gap-y-1">
|
||||
<span className="text-sm text-gray-300">Setup-outcome pipeline check</span>
|
||||
<span className="text-sm text-gray-300">Gate barrier pipeline check</span>
|
||||
<span className="text-sm text-gray-400">
|
||||
Live <span className={`num font-semibold ${rColor(liveAvgR)}`}>{fmtR(liveAvgR)}</span>
|
||||
</span>
|
||||
@@ -129,7 +129,7 @@ export function TrackRecordPanel() {
|
||||
Backtest <span className={`num font-semibold ${rColor(btAvgR)}`}>{fmtR(btAvgR)}</span>
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{liveN} matured{perf ? ` · ${perf.maturing} maturing` : ''} · qualified target/stop
|
||||
{liveN} matured{perf ? ` · ${perf.maturing} maturing` : ''} · not ATR-trail book
|
||||
</span>
|
||||
</div>
|
||||
<StatusChip status={status} />
|
||||
|
||||
@@ -203,6 +203,31 @@ function SetupCard({ setup, action, currentPrice, risk, regime, exitPolicy, sele
|
||||
selectedPrice?: number | null;
|
||||
onSelectPrice?: (price: number) => void;
|
||||
}) {
|
||||
// Hooks must run unconditionally (Rules of Hooks) even when setup is missing.
|
||||
const createTrade = useCreatePaperTrade();
|
||||
const [taking, setTaking] = useState(false);
|
||||
const [takeShares, setTakeShares] = useState(0);
|
||||
const [takeEntry, setTakeEntry] = useState(0);
|
||||
const [takeTarget, setTakeTarget] = useState(0);
|
||||
const [internalSel, setInternalSel] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!setup) return;
|
||||
const next = positionSize(risk.accountSize, risk.riskPct, setup.entry_price, setup.stop_loss);
|
||||
setTakeShares(next?.shares ?? 0);
|
||||
setTakeEntry(currentPrice ?? setup.entry_price);
|
||||
setTakeTarget(setup.target);
|
||||
}, [setup, currentPrice, risk.accountSize, risk.riskPct]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!taking) return;
|
||||
const onKey = (e: globalThis.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setTaking(false);
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [taking]);
|
||||
|
||||
if (!setup) {
|
||||
return (
|
||||
<div className="rounded-xl border border-white/[0.07] p-4 text-xs text-gray-500">
|
||||
@@ -222,16 +247,9 @@ function SetupCard({ setup, action, currentPrice, risk, regime, exitPolicy, sele
|
||||
const exitPlan = deriveExitPlan(setup, exitPolicy);
|
||||
const honorsTarget = exitPlan?.honorsTarget ?? false;
|
||||
|
||||
const createTrade = useCreatePaperTrade();
|
||||
const [taking, setTaking] = useState(false);
|
||||
const [takeShares, setTakeShares] = useState<number>(sizing?.shares ?? 0);
|
||||
const [takeEntry, setTakeEntry] = useState<number>(currentPrice ?? setup.entry_price);
|
||||
const [takeTarget, setTakeTarget] = useState<number>(setup.target);
|
||||
|
||||
// Target choice from the ladder drives the rail, the chips, and the take
|
||||
// flow — the scanner's primary is just the default. Controlled by the page
|
||||
// when provided (so the candlestick overlay follows), else local.
|
||||
const [internalSel, setInternalSel] = useState<number | null>(null);
|
||||
const selPrice = selectedPrice !== undefined ? selectedPrice : internalSel;
|
||||
const selectTargetPrice = (p: number) => {
|
||||
if (onSelectPrice) onSelectPrice(p);
|
||||
@@ -242,16 +260,6 @@ function SetupCard({ setup, action, currentPrice, risk, regime, exitPolicy, sele
|
||||
const activeRR = selected?.rr_ratio ?? setup.rr_ratio;
|
||||
const activeProb = selected?.probability ?? prob;
|
||||
|
||||
// Close the take dialog on Escape.
|
||||
useEffect(() => {
|
||||
if (!taking) return;
|
||||
const onKey = (e: globalThis.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setTaking(false);
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [taking]);
|
||||
|
||||
const confirmTake = () => {
|
||||
createTrade.mutate(
|
||||
{
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ReferenceLine } from 'recharts';
|
||||
import type { SRLevel } from '../../lib/types';
|
||||
import { formatPrice } from '../../lib/format';
|
||||
|
||||
interface SROverlayProps {
|
||||
levels: SRLevel[];
|
||||
}
|
||||
|
||||
export function SROverlay({ levels }: SROverlayProps) {
|
||||
return (
|
||||
<>
|
||||
{levels.map((level) => {
|
||||
const isSupport = level.type === 'support';
|
||||
return (
|
||||
<ReferenceLine
|
||||
key={level.id}
|
||||
y={level.price_level}
|
||||
stroke={isSupport ? '#22c55e' : '#ef4444'}
|
||||
strokeDasharray="6 3"
|
||||
strokeWidth={1.5}
|
||||
label={{
|
||||
value: formatPrice(level.price_level),
|
||||
position: 'right',
|
||||
fill: isSupport ? '#22c55e' : '#ef4444',
|
||||
fontSize: 11,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -118,9 +118,9 @@ export function disqualifyReason(setup: TradeSetup, config: ActivationConfig): s
|
||||
|
||||
/**
|
||||
* Symbol of the current single 'top pick' — the #1 row the dashboard highlights:
|
||||
* the highest residual 12-1 momentum percentile among qualified setups. Returns
|
||||
* null when there are no actionable setups. Keep in step with the Top Setups
|
||||
* ranking in DashboardPage.
|
||||
* highest production strategy_rank (80/20 mom/vol) among qualified setups,
|
||||
* falling back to residual momentum percentile. Returns null when there are no
|
||||
* actionable setups. Keep in step with the Top Setups ranking in DashboardPage.
|
||||
*/
|
||||
export function topPickSymbol(
|
||||
trades: TradeSetup[] | undefined,
|
||||
|
||||
Reference in New Issue
Block a user