feat: require gate reset before post-stop reentry

This commit is contained in:
2026-07-17 19:30:40 +02:00
parent 1a6f82bf6d
commit 5155d00d9e
18 changed files with 577 additions and 278 deletions
@@ -373,8 +373,8 @@ export function BacktestPanel() {
<p className="text-[11px] text-gray-500">
Avg hold {fmtDays(monitorRun.avg_hold_days)} · Best {fmtR(monitorRun.best_trade_r)} / Worst{' '}
{fmtR(monitorRun.worst_trade_r)} · Avg P&amp;L per trade {fmtMoney(monitorRun.avg_trade_pnl)}
{monitorRun.reentry_lockdown_sessions ? (
<> · Re-entry lockdown {monitorRun.reentry_lockdown_sessions} market sessions after initial stop</>
{monitorRun.reentry_policy === 'gate_reset' ? (
<> · Re-entry after gate failure and fresh qualification</>
) : null}
</p>
@@ -67,14 +67,13 @@ function entryDrift(setup: TradeSetup, currentPrice?: number) {
}
type NotActionableState =
| { kind: 'lockdown'; remainingSessions: number }
| { kind: 'gate-reset' }
| { kind: 'invalidated' }
| null;
function notActionableState(setup: TradeSetup, currentPrice?: number) {
const remainingSessions = setup.reentry_lockdown_remaining_sessions ?? 0;
if (remainingSessions > 0) {
return { kind: 'lockdown', remainingSessions } satisfies NotActionableState;
if (setup.reentry_gate_reset_required) {
return { kind: 'gate-reset' } satisfies NotActionableState;
}
if (currentPrice == null) return null;
if (entryDrift(setup, currentPrice)?.status !== 'invalidated') return null;
@@ -271,20 +270,17 @@ function SetupCard({ setup, action, currentPrice, risk, regime, exitPolicy, sele
};
const inactiveState = notActionableState(setup, currentPrice);
if (inactiveState?.kind === 'lockdown') {
const remaining = inactiveState.remainingSessions;
if (inactiveState?.kind === 'gate-reset') {
return (
<div data-direction={setup.direction} className="rounded-xl border border-amber-400/20 bg-amber-400/[0.04] p-4">
<div className="flex flex-wrap items-center gap-2">
<DirTag direction={setup.direction} />
<span className="num text-[10px] uppercase tracking-[0.16em] text-amber-300">post-stop lockdown</span>
<span className="num ml-auto text-xs text-gray-500">
{remaining} market session{remaining === 1 ? '' : 's'} remaining
</span>
<span className="num text-[10px] uppercase tracking-[0.16em] text-amber-300">awaiting gate reset</span>
<span className="num ml-auto text-xs text-gray-500">re-entry paused</span>
</div>
<p className="mt-2 text-[11.5px] leading-relaxed text-gray-400">
This setup remains visible for context but cannot be marked as taken. Once the lockdown expires,
the scanner recalculates the normal gate before it can become actionable again.
This setup remains visible for context but cannot be marked as taken. The ticker must first fail
the production gate; only a later fresh qualification can become actionable again.
</p>
</div>
);
@@ -639,11 +635,11 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPric
<div className="min-w-0">
{preferredInactive ? (
<span className="text-sm font-semibold text-gray-400">
{preferredInactive.kind === 'lockdown' ? (
{preferredInactive.kind === 'gate-reset' ? (
<>
Re-entry paused{' '}
<span className="font-normal text-gray-500">
({preferredInactive.remainingSessions} market session{preferredInactive.remainingSessions === 1 ? '' : 's'} remaining after stop)
(waiting for the gate to fail before a fresh qualification)
</span>
</>
) : (
+3 -4
View File
@@ -43,7 +43,7 @@ export function liveRiskReward(setup: TradeSetup, currentPrice: number): number
* app/services/qualification.py — keep the two in sync.
*/
export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boolean {
if ((setup.reentry_lockdown_remaining_sessions ?? 0) > 0) return false;
if (setup.reentry_gate_reset_required) return false;
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.
@@ -80,9 +80,8 @@ export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boo
* qualifiesSetup rule-for-rule (keep the order in sync).
*/
export function disqualifyReason(setup: TradeSetup, config: ActivationConfig): string | null {
const lockdownRemaining = setup.reentry_lockdown_remaining_sessions ?? 0;
if (lockdownRemaining > 0) {
return `post-stop lockdown · ${lockdownRemaining} session${lockdownRemaining === 1 ? '' : 's'} remaining`;
if (setup.reentry_gate_reset_required) {
return 'post-stop gate reset required';
}
if (setup.rr_ratio < config.min_rr) {
return `R:R ${setup.rr_ratio.toFixed(1)} below gate ${config.min_rr.toFixed(1)}`;
+6 -6
View File
@@ -144,7 +144,7 @@ export interface TradeSetup {
momentum_percentile?: number | null;
strategy_rank?: number | null;
volatility_percentile?: number | null;
reentry_lockdown_remaining_sessions?: number | null;
reentry_gate_reset_required?: boolean;
context_as_of?: TradeSetupContextAsOf | null;
recommendation_summary?: RecommendationSummary;
}
@@ -356,10 +356,10 @@ export interface BacktestPortfolioMonitorRun extends BacktestPortfolioPolicy {
label: string;
description: string;
is_production: boolean;
comparison_arm?: 'live_no_lockdown' | 'live_lockdown_5' | null;
comparison_arm?: 'live_immediate' | 'live_gate_reset' | null;
entry_variant: string;
exit_policy: string;
reentry_lockdown_sessions?: number;
reentry_policy?: 'immediate' | 'gate_reset';
lookback: string;
lookback_label: string;
}
@@ -371,8 +371,8 @@ export interface BacktestPortfolioMonitor {
label: string;
description: string;
is_production: boolean;
comparison_arm?: 'live_no_lockdown' | 'live_lockdown_5' | null;
reentry_lockdown_sessions?: number;
comparison_arm?: 'live_immediate' | 'live_gate_reset' | null;
reentry_policy?: 'immediate' | 'gate_reset';
}[];
lookbacks: { lookback: string; label: string }[];
runs: BacktestPortfolioMonitorRun[];
@@ -415,7 +415,7 @@ export interface BacktestReport {
target_model?: 'production_gtl' | 'structural_sr';
target_model_label?: string;
is_production_target_model?: boolean;
production_reentry_lockdown_sessions?: number;
production_reentry_policy?: 'gate_reset';
};
overall_qualified: BacktestBucket;
overall_all: BacktestBucket;