Files
signal-platform/frontend/src/lib/exitPlan.ts
T
dennisthiessenandClaude Opus 5 f22313deaf
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m13s
Deploy / deploy (push) Successful in 37s
chore: remove dead frontend code and one unused service helper
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>
2026-08-08 17:24:04 +02:00

136 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}