Require aligned action for qualified setups
This commit is contained in:
@@ -2,6 +2,13 @@ import type { ActivationConfig, TradeSetup } from './types';
|
||||
|
||||
const HIGH_CONVICTION_ACTIONS = new Set(['LONG_HIGH', 'SHORT_HIGH']);
|
||||
|
||||
function actionDirection(action: TradeSetup['recommended_action']): 'long' | 'short' | 'neutral' {
|
||||
if (!action || action === 'NEUTRAL') return 'neutral';
|
||||
if (action.startsWith('LONG')) return 'long';
|
||||
if (action.startsWith('SHORT')) return 'short';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
export function bestTargetProbability(setup: TradeSetup): number {
|
||||
return setup.targets?.length ? Math.max(...setup.targets.map((t) => t.probability)) : 0;
|
||||
}
|
||||
@@ -42,8 +49,11 @@ export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boo
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// NEUTRAL = "no clear setup" — not actionable, so by default it doesn't qualify.
|
||||
if (config.exclude_neutral && (setup.recommended_action ?? 'NEUTRAL') === 'NEUTRAL') return false;
|
||||
// NEUTRAL = "no clear setup"; an opposite action means this setup is counter-bias.
|
||||
if (config.exclude_neutral) {
|
||||
const actionDir = actionDirection(setup.recommended_action);
|
||||
if (actionDir === 'neutral' || actionDir !== setup.direction) return false;
|
||||
}
|
||||
if (config.require_high_conviction && !HIGH_CONVICTION_ACTIONS.has(setup.recommended_action ?? '')) {
|
||||
return false;
|
||||
}
|
||||
@@ -53,9 +63,9 @@ export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boo
|
||||
|
||||
/**
|
||||
* Symbol of the current single 'top pick' — the #1 row the dashboard highlights:
|
||||
* the highest residual 12-1 momentum percentile among qualified setups (or among all
|
||||
* setups when none qualify). Returns null when there are no setups. Keep in step
|
||||
* with the Top Setups ranking in DashboardPage.
|
||||
* 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.
|
||||
*/
|
||||
export function topPickSymbol(
|
||||
trades: TradeSetup[] | undefined,
|
||||
@@ -64,8 +74,7 @@ export function topPickSymbol(
|
||||
const all = trades ?? [];
|
||||
if (all.length === 0) return null;
|
||||
const qualified = activation ? all.filter((t) => qualifiesSetup(t, activation)) : [];
|
||||
const pool = qualified.length > 0 ? qualified : all;
|
||||
const top = [...pool].sort(
|
||||
const top = [...qualified].sort(
|
||||
(a, b) => (b.momentum_percentile ?? -Infinity) - (a.momentum_percentile ?? -Infinity),
|
||||
)[0];
|
||||
return top?.symbol ?? null;
|
||||
|
||||
@@ -76,15 +76,12 @@ export default function DashboardPage() {
|
||||
[trades.data, activation.data],
|
||||
);
|
||||
|
||||
// Show qualified setups first; fall back to the full list when none qualify.
|
||||
// Rank by residual 12-1 momentum percentile so the strongest names sit at the top.
|
||||
const showingQualified = qualifiedSetups.length > 0;
|
||||
// Rank only actionable/qualified setups by residual 12-1 momentum percentile.
|
||||
const topSetups: TradeSetup[] = useMemo(() => {
|
||||
const pool = showingQualified ? qualifiedSetups : trades.data ?? [];
|
||||
return [...pool]
|
||||
return [...qualifiedSetups]
|
||||
.sort((a, b) => (b.momentum_percentile ?? -Infinity) - (a.momentum_percentile ?? -Infinity))
|
||||
.slice(0, 5);
|
||||
}, [showingQualified, qualifiedSetups, trades.data]);
|
||||
}, [qualifiedSetups]);
|
||||
|
||||
const topWatchlist = useMemo(
|
||||
() =>
|
||||
@@ -197,12 +194,12 @@ export default function DashboardPage() {
|
||||
<div className="xl:col-span-3">
|
||||
<Section
|
||||
title="Top Setups"
|
||||
hint={showingQualified ? 'ranked by expected value' : 'none qualified — showing all'}
|
||||
hint="qualified and ranked by residual momentum"
|
||||
>
|
||||
{trades.isLoading && <SkeletonTable rows={5} cols={5} />}
|
||||
{trades.isError && <Callout variant="error">Failed to load setups</Callout>}
|
||||
{trades.data && topSetups.length === 0 && (
|
||||
<Callout variant="empty">No active setups. Run the scanner from the Signals page.</Callout>
|
||||
<Callout variant="empty">No qualified actionable setups right now.</Callout>
|
||||
)}
|
||||
{topSetups.length > 0 && (
|
||||
<div className="glass overflow-x-auto">
|
||||
|
||||
Reference in New Issue
Block a user