Drop over-progressed setups via live R:R; refresh trades on fetch
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 25s

Answers "why does a too-far-progressed setup still show": setups are only
recalculated by the scheduled R:R scan and manual fetch; at creation
entry == current price (0% progress), so over-progression is a
between-scans drift effect and must be judged at read time.

- /trades now attaches current_price (latest close per ticker).
- Qualification drops setups whose R:R recomputed from the current price
  falls below min_rr — i.e. price already ran toward target (reward
  consumed) or through the stop. Reuses the existing min_rr threshold
  instead of a separate progress %; far cleaner (a 3:1 is already ~1:1
  by 33% progress). Skipped for historical setups (no current_price).
- Fix: useFetchSymbolData now invalidates the trades queries, so a fetch/
  recompute actually refreshes confidence/setups in the UI (was the cause
  of the stale 100% confidence lingering after recompute).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 14:02:10 +02:00
parent a32f09c8ba
commit da83f027e1
7 changed files with 97 additions and 4 deletions
+3
View File
@@ -40,6 +40,9 @@ export function useFetchSymbolData(options: UseFetchSymbolDataOptions = {}) {
queryClient.invalidateQueries({ queryKey: ['fundamentals', symbol] });
queryClient.invalidateQueries({ queryKey: ['sr-levels', symbol] });
queryClient.invalidateQueries({ queryKey: ['scores', symbol] });
// Fetch re-runs the scanner → setups/confidence change. Refresh both the
// per-ticker trades (['trades', symbol]) and the Overview list (['trades']).
queryClient.invalidateQueries({ queryKey: ['trades'] });
if (invalidatePipelineReadiness) {
queryClient.invalidateQueries({ queryKey: ['admin', 'pipeline-readiness'] });
+13
View File
@@ -13,12 +13,25 @@ export function primaryTargetProbability(setup: TradeSetup): number | null {
return setup.targets?.length ? bestTargetProbability(setup) : null;
}
/** R:R recomputed from the current price (0 if no reward/risk left). */
export function liveRiskReward(setup: TradeSetup, currentPrice: number): number {
const reward = setup.direction === 'long' ? setup.target - currentPrice : currentPrice - setup.target;
const risk = setup.direction === 'long' ? currentPrice - setup.stop_loss : setup.stop_loss - currentPrice;
if (reward <= 0 || risk <= 0) return 0;
return reward / risk;
}
/**
* Whether a setup clears the activation gate. Mirrors the backend predicate in
* app/services/qualification.py — keep the two in sync.
*/
export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boolean {
if (setup.rr_ratio < config.min_rr) return false;
// Live R:R from current price — drops setups whose price has already run
// toward target (reward consumed) or through the stop.
if (setup.current_price != null && liveRiskReward(setup, setup.current_price) < config.min_rr) {
return false;
}
if ((setup.confidence_score ?? 0) < config.min_confidence) return false;
if (config.require_high_conviction && !HIGH_CONVICTION_ACTIONS.has(setup.recommended_action ?? '')) {
return false;
+1
View File
@@ -130,6 +130,7 @@ export interface TradeSetup {
actual_outcome: string | null;
outcome_date: string | null;
evaluated_at: string | null;
current_price: number | null;
recommendation_summary?: RecommendationSummary;
}