Scan of every module and exported symbol, with each candidate verified by hand
rather than trusted from the scan.
Deleted outright:
frontend/src/lib/fundamentals.ts (112 lines, 12 exports) — imported by
nothing, including FundamentalsPanel, which reads backend values. It mirrors
scoring_service._compute_fundamental_score, so it is the same *kind* of
thing as lib/qualification.ts — but nothing consumes it, so it mirrored
nothing and could drift out of sync unnoticed.
Skeleton.SkeletonLine, paperTrades.getEquityCurve, regime.regimeColor
breadth_service.compute_breadth_today — self-described "thin wrapper, for
future live use"; that future did not arrive.
Kept, but unexported — used inside their own module, so the dead part was the
public surface, not the code: Button.Spinner, exitPlan.SETUP_STOP_ATR_MULTIPLIER,
client.ApiError.
Three things the scan flagged that are NOT dead, recorded so the next sweep does
not re-raise them:
RegimeChart.tsx — lazy(() => import(...)) in RegimePage, so it looks orphaned
to any importer-graph scan. Deleting it would break the risk page.
qualification.ts MIN_TARGET_PROBABILITY / liveRiskReward — that file is a live
mirror of app/services/qualification.py used in five places, and the
constant is exported to document the backend value it tracks.
ssl_bootstrap.ssl_status — called from an inline python snippet inside
scripts/run_tier1_macbook.sh, invisible to a .py-only search.
No orphaned backend modules across app/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
4.6 KiB
TypeScript
136 lines
4.6 KiB
TypeScript
/**
|
||
* What actually closes a trade.
|
||
*
|
||
* The setup's `target` is NOT an exit under the production policy: it is a
|
||
* screening artifact — the headline Gate Target Ladder proposal, used to
|
||
* compute the R:R and
|
||
* probability that admit the setup through the activation gate. The live exit
|
||
* (`paper_trade_service.resolve_open_trades`) never reads it; `atr_trailing`
|
||
* closes on the initial stop, a trailing stop, or the max hold.
|
||
*
|
||
* This module derives the real plan so the UI can show it instead of implying
|
||
* a take-profit that will never fire. See docs/research/sr-levels-and-exits.md.
|
||
*/
|
||
import type { ExitPolicy, TradeSetup } from './types';
|
||
|
||
/**
|
||
* Stop width used when the scanner builds a setup: stop = entry ∓ 1.5 × ATR
|
||
* (`rr_scanner_service.scan_symbol`, and `backtest_service.ATR_MULTIPLIER`).
|
||
* Lets us recover ATR from a setup without another round trip:
|
||
* ATR = |entry − stop| / 1.5
|
||
* Guarded by test_prod_strategy_parity.py so a backend change can't silently
|
||
* desync this.
|
||
*/
|
||
const SETUP_STOP_ATR_MULTIPLIER = 1.5;
|
||
|
||
export interface ExitPlan {
|
||
mode: ExitPolicy['mode'];
|
||
/** Does the setup's target actually close the trade? Only when mode === 'target'. */
|
||
honorsTarget: boolean;
|
||
/** Distance from entry to the initial stop, i.e. 1R per share. */
|
||
riskPerShare: number;
|
||
initialStop: number;
|
||
/** Trailing-stop width in price, once the trail is active (atr_trailing only). */
|
||
trailWidth: number | null;
|
||
/**
|
||
* Price the trade must reach before the trailing stop rises above the initial
|
||
* stop and takes over. Below this, the initial stop is what's protecting you.
|
||
*/
|
||
trailTakesOverAt: number | null;
|
||
/** Trail width expressed in R — the intuitive "how much give-back". */
|
||
trailWidthR: number | null;
|
||
maxHoldDays: number;
|
||
headline: string;
|
||
}
|
||
|
||
/**
|
||
* Derive the real exit plan for a setup under the live policy.
|
||
* Returns null when the setup has no usable risk distance.
|
||
*/
|
||
export function deriveExitPlan(setup: TradeSetup, policy?: ExitPolicy): ExitPlan | null {
|
||
const isLong = setup.direction === 'long';
|
||
const riskPerShare = Math.abs(setup.entry_price - setup.stop_loss);
|
||
if (!(riskPerShare > 0)) return null;
|
||
|
||
// Fall back to the shipped defaults when the policy hasn't loaded yet, so the
|
||
// card never renders a blank or (worse) a target-based plan.
|
||
const mode = policy?.mode ?? 'atr_trailing';
|
||
const maxHoldDays = policy?.hold_days ?? 30;
|
||
const atrMultiplier = policy?.atr_multiplier ?? 3;
|
||
|
||
const atr = riskPerShare / SETUP_STOP_ATR_MULTIPLIER;
|
||
|
||
if (mode === 'atr_trailing') {
|
||
const trailWidth = atrMultiplier * atr;
|
||
// The trail only bites once it clears the initial stop:
|
||
// highestClose − trailWidth > stop ⇔ highestClose > entry ± (trailWidth − R)
|
||
const takeoverOffset = trailWidth - riskPerShare;
|
||
const trailTakesOverAt = isLong
|
||
? setup.entry_price + takeoverOffset
|
||
: setup.entry_price - takeoverOffset;
|
||
return {
|
||
mode,
|
||
honorsTarget: false,
|
||
riskPerShare,
|
||
initialStop: setup.stop_loss,
|
||
trailWidth,
|
||
trailTakesOverAt,
|
||
trailWidthR: trailWidth / riskPerShare,
|
||
maxHoldDays,
|
||
headline: `${atrMultiplier}× ATR trailing stop · max ${maxHoldDays} trading days`,
|
||
};
|
||
}
|
||
|
||
if (mode === 'trailing') {
|
||
const trailWidth = (setup.entry_price * (policy?.trailing_pct ?? 12)) / 100;
|
||
return {
|
||
mode,
|
||
honorsTarget: false,
|
||
riskPerShare,
|
||
initialStop: setup.stop_loss,
|
||
trailWidth,
|
||
trailTakesOverAt: null,
|
||
trailWidthR: trailWidth / riskPerShare,
|
||
maxHoldDays,
|
||
headline: `${policy?.trailing_pct ?? 12}% trailing stop · max ${maxHoldDays} trading days`,
|
||
};
|
||
}
|
||
|
||
if (mode === 'target') {
|
||
return {
|
||
mode,
|
||
honorsTarget: true,
|
||
riskPerShare,
|
||
initialStop: setup.stop_loss,
|
||
trailWidth: null,
|
||
trailTakesOverAt: null,
|
||
trailWidthR: null,
|
||
maxHoldDays,
|
||
headline: 'Take profit at the selected level, or exit at the stop',
|
||
};
|
||
}
|
||
|
||
// 'time'
|
||
return {
|
||
mode,
|
||
honorsTarget: false,
|
||
riskPerShare,
|
||
initialStop: setup.stop_loss,
|
||
trailWidth: null,
|
||
trailTakesOverAt: null,
|
||
trailWidthR: null,
|
||
maxHoldDays,
|
||
headline: `Hold to the stop or ${maxHoldDays} trading days — no target, no trail`,
|
||
};
|
||
}
|
||
|
||
/** How far price has run from the scan entry, in R. Sign is direction-aware. */
|
||
export function driftInR(setup: TradeSetup, currentPrice: number): number | null {
|
||
const risk = Math.abs(setup.entry_price - setup.stop_loss);
|
||
if (!(risk > 0)) return null;
|
||
const moved = setup.direction === 'long'
|
||
? currentPrice - setup.entry_price
|
||
: setup.entry_price - currentPrice;
|
||
return moved / risk;
|
||
}
|