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:
@@ -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;
|
||||
|
||||
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user