feat: require gate reset before post-stop reentry
This commit is contained in:
@@ -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&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>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user