Setup views: primary-target column, floor-target prune, liveness cutoff

Three follow-ups to the gate probability floor (8f41143):

- Signals table shows the starred primary target (shared primaryTarget
  helper) instead of an independently computed max-probability best,
  so Overview, Signals and ticker details agree by construction.
- Targets pinned at the 3% probability clamp floor collapse to the
  nearest one (enhance_trade_setup + backtest candidates in parity):
  floor-pinned levels are indistinguishable to the model, so farther
  ones were duplicate 3% rows inviting lottery headlines.
- get_trade_setups only returns setups re-emitted within
  LIVE_SETUP_MAX_AGE_DAYS (3): an older latest row means the daily
  scan no longer confirms the setup, and such rows otherwise surface
  forever on Overview/Signals/ticker/alerts. History endpoints keep
  full history.

Backtest on the Jul-3 snapshot is metric-identical to the gate-floor
run on all qualified stats (1089 qualified, Sharpe 2.02, CAGR +49.6%,
DD -15.8%): the prune only removes noise the gate already rejected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 10:06:34 +02:00
co-authored by Claude Fable 5
parent 8f411435ee
commit fdc49d0e28
8 changed files with 170 additions and 42 deletions
+10 -7
View File
@@ -1,9 +1,10 @@
import { Link } from 'react-router-dom';
import type { TradeSetup } from '../../lib/types';
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' | 'best_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' | 'detected_at';
export type SortDirection = 'asc' | 'desc';
interface TradeTableProps {
@@ -21,7 +22,7 @@ const columns: { key: SortColumn; label: string }[] = [
{ key: 'entry_price', label: 'Entry' },
{ key: 'stop_loss', label: 'Stop Loss' },
{ key: 'target', label: 'Target' },
{ key: 'best_target_probability', label: 'Best Target' },
{ key: 'primary_target_probability', label: 'Primary Target' },
{ key: 'risk_amount', label: 'Risk $' },
{ key: 'reward_amount', label: 'Reward $' },
{ key: 'rr_ratio', label: 'R:R' },
@@ -65,10 +66,12 @@ function riskLevelClass(riskLevel: TradeSetup['risk_level']) {
return 'text-gray-400';
}
function bestTargetText(trade: TradeSetup) {
if (!trade.targets || trade.targets.length === 0) return '—';
const best = [...trade.targets].sort((a, b) => b.probability - a.probability)[0];
return `${formatPrice(best.price)} (${best.probability.toFixed(0)}%)`;
// The starred primary — the same target the Overview and ticker details
// headline, so every view agrees on which target a setup is "about".
function primaryTargetText(trade: TradeSetup) {
const primary = primaryTarget(trade);
if (!primary) return '—';
return `${formatPrice(primary.price)} (${primary.probability.toFixed(0)}%)`;
}
export function TradeTable({ trades, sortColumn, sortDirection, onSort }: TradeTableProps) {
@@ -121,7 +124,7 @@ export function TradeTable({ trades, sortColumn, sortDirection, onSort }: TradeT
<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">{bestTargetText(trade)}</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">{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>
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useActivation } from '../../hooks/useActivation';
import { useTrades } from '../../hooks/useTrades';
import { qualifiesSetup, activationSummary } from '../../lib/qualification';
import { qualifiesSetup, activationSummary, primaryTargetProbability } from '../../lib/qualification';
import { TradeTable, type SortColumn, type SortDirection, computeTradeAnalysis } from '../scanner/TradeTable';
import { SkeletonTable } from '../ui/Skeleton';
import { useToast } from '../ui/Toast';
@@ -42,8 +42,8 @@ function getComputedValue(trade: TradeSetup, column: SortColumn): number {
case 'stop_pct': return analysis.stop_pct;
case 'target_pct': return analysis.target_pct;
case 'confidence_score': return trade.confidence_score ?? -1;
case 'best_target_probability':
return trade.targets?.length ? Math.max(...trade.targets.map((t) => t.probability)) : -1;
case 'primary_target_probability':
return primaryTargetProbability(trade) ?? -1;
case 'risk_level':
if (trade.risk_level === 'Low') return 1;
if (trade.risk_level === 'Medium') return 2;
@@ -78,7 +78,7 @@ function sortTrades(
case 'stop_pct':
case 'target_pct':
case 'confidence_score':
case 'best_target_probability':
case 'primary_target_probability':
case 'risk_level':
cmp = getComputedValue(a, column) - getComputedValue(b, column);
break;
+9 -6
View File
@@ -1,4 +1,4 @@
import type { ActivationConfig, TradeSetup } from './types';
import type { ActivationConfig, TradeSetup, TradeTarget } from './types';
const HIGH_CONVICTION_ACTIONS = new Set(['LONG_HIGH', 'SHORT_HIGH']);
@@ -16,15 +16,18 @@ function actionDirection(action: TradeSetup['recommended_action']): 'long' | 'sh
return 'neutral';
}
export function bestTargetProbability(setup: TradeSetup): number {
return setup.targets?.length ? Math.max(...setup.targets.map((t) => t.probability)) : 0;
/** The starred primary target (the one the headline R:R refers to), falling
* back to the most likely target when no star is stored. */
export function primaryTarget(setup: TradeSetup): TradeTarget | null {
const starred = setup.targets?.find((t) => t.is_primary);
if (starred) return starred;
if (!setup.targets?.length) return null;
return [...setup.targets].sort((a, b) => b.probability - a.probability)[0];
}
/** Probability of the starred primary target (the one the headline R:R refers to). */
export function primaryTargetProbability(setup: TradeSetup): number | null {
const primary = setup.targets?.find((t) => t.is_primary);
if (primary) return primary.probability;
return setup.targets?.length ? bestTargetProbability(setup) : null;
return primaryTarget(setup)?.probability ?? null;
}
/** R:R recomputed from the current price (0 if no reward/risk left). */