|
|
"""Domain models for the Morning Momentum Intraday Backtester.
|
|
|
|
|
|
All models use Pydantic for validation and serialization.
|
|
|
No dependencies on the existing backtest system.
|
|
|
"""
|
|
|
from __future__ import annotations
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
# ── Strategy Parameters ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
class StrategyParams(BaseModel):
|
|
|
"""Core strategy parameters controlling when to enter/exit."""
|
|
|
|
|
|
compound_returns: bool = False
|
|
|
"""When True, position sizing scales with current equity (compounding).
|
|
|
When False, position sizing uses min(initial_capital, current_equity) (simple returns,
|
|
|
capped at actual equity to avoid trading money that doesn't exist after drawdowns).
|
|
|
Momentum research defaults to simple returns to avoid late-period overweighting.
|
|
|
Ignored when daily_budget_reset is True."""
|
|
|
|
|
|
daily_budget_reset: bool = False
|
|
|
"""Research-only mode: every day resets sizing_capital to initial_capital,
|
|
|
ignoring prior-day PnL entirely (no compounding, no drawdown cap).
|
|
|
Useful for isolating strategy alpha from capital-path effects.
|
|
|
When True, takes precedence over compound_returns."""
|
|
|
|
|
|
entry_minutes_after_open: int = 30
|
|
|
"""Minutes after 9:30 AM ET to evaluate morning gainers and enter trades."""
|
|
|
|
|
|
confirmation_minutes_after_entry: int = 0
|
|
|
"""Optional extra confirmation delay after the primary entry time.
|
|
|
Example: 5 means evaluate leaders at +10min but only enter at +15min if
|
|
|
the confirmation rule is still satisfied."""
|
|
|
|
|
|
min_confirmation_return_pct: float | None = None
|
|
|
"""Minimum return between the primary entry bar close and the confirmation
|
|
|
bar close. Example: 0.0 = no fade allowed; 0.002 = require +0.2% follow-through.
|
|
|
Ignored when confirmation_minutes_after_entry <= 0."""
|
|
|
|
|
|
exit_minutes_before_close: int = 30
|
|
|
"""Minutes before 4:00 PM ET to force-close all positions."""
|
|
|
|
|
|
stop_loss_pct: float | None = -0.02
|
|
|
"""Fixed stop-loss threshold (e.g. -0.02 = -2%). None to disable."""
|
|
|
|
|
|
trailing_stop_pct: float | None = None
|
|
|
"""Trailing stop: if set, ratchet stop up as price rises. e.g. -0.03 = trail 3% below peak.
|
|
|
When both stop_loss_pct and trailing_stop_pct are set, trailing_stop_pct is used."""
|
|
|
|
|
|
atr_stop_multiplier: float | None = None
|
|
|
"""Catastrophic stop distance in ATR(14) units from the actual entry price.
|
|
|
Example: 0.5 means stop at entry - 0.5 x ATR. When set, this overrides
|
|
|
stop_loss_pct as the initial stop anchor."""
|
|
|
|
|
|
opening_range_stop_multiplier: float | None = None
|
|
|
"""Catastrophic stop distance in opening-range-width units from the actual
|
|
|
entry price. Example: 1.0 means stop at entry - 1.0 x opening range width.
|
|
|
Used as an alternative to ATR when the opening range itself is the better
|
|
|
volatility proxy."""
|
|
|
|
|
|
trailing_activation_gain_pct: float | None = None
|
|
|
"""Optional delayed trailing activation threshold.
|
|
|
When set, trailing_stop_pct does not turn on until peak return from entry
|
|
|
reaches this gain threshold. Before activation, only the catastrophic /
|
|
|
fixed stop is active."""
|
|
|
|
|
|
overextended_trailing_gain_pct: float | None = None
|
|
|
"""If set together with overextended_trailing_stop_pct, trades whose morning gain
|
|
|
at entry is at least this large use the alternate trailing stop instead of the
|
|
|
baseline trailing_stop_pct."""
|
|
|
|
|
|
overextended_trailing_stop_pct: float | None = None
|
|
|
"""Alternate trailing stop for overextended morning leaders.
|
|
|
Example: base trail -0.075 with overextended trail -0.065 tightens risk only for
|
|
|
names already up sharply by entry time."""
|
|
|
|
|
|
min_morning_gain_pct: float = 0.01
|
|
|
"""Minimum gain from open to entry time for a stock to qualify (e.g. 0.01 = 1%)."""
|
|
|
|
|
|
max_morning_gain_pct: float | None = None
|
|
|
"""Maximum morning gain allowed (e.g. 0.10 = 10%). Filters out extreme gap-ups
|
|
|
that tend to mean-revert quickly. None = no cap."""
|
|
|
|
|
|
min_entry_volume: int | None = None
|
|
|
"""Minimum cumulative volume by entry time (shares). Filters illiquid stocks.
|
|
|
E.g. 50000 = must have traded 50K shares in first 30 minutes."""
|
|
|
|
|
|
min_entry_dollar_volume: float | None = None
|
|
|
"""Minimum cumulative dollar volume by entry time.
|
|
|
Helps reject low-priced names that pass a raw share-volume filter but still
|
|
|
trade too little notional size for reliable execution."""
|
|
|
|
|
|
ticker_cooldown_days: int = 0
|
|
|
"""Blackout period after trading a ticker (calendar days).
|
|
|
E.g. 5 = same ticker can't be selected again within 5 days. 0 = disabled."""
|
|
|
|
|
|
top_n: int = 3
|
|
|
"""Number of top gainers to buy each day (equal-weight allocation)."""
|
|
|
|
|
|
min_positions_to_trade: int = 1
|
|
|
"""Minimum number of qualified picks required to trade the day at all.
|
|
|
Event-driven leader strategies often degrade when only one or two names pass.
|
|
|
Use this to explicitly allow no-trade days instead of forcing sparse baskets."""
|
|
|
|
|
|
max_positions_per_sector: int | None = None
|
|
|
"""Optional basket diversification cap.
|
|
|
When set, at most this many positions may be opened from the same sector
|
|
|
in the day's momentum basket. Unknown sectors are left uncapped."""
|
|
|
|
|
|
full_size_positions_threshold: int | None = None
|
|
|
"""When set, sparse days scale down total deployed capital instead of always
|
|
|
using the full daily book. Example: threshold=4 means 1-3 position days are
|
|
|
sized below 100% of the daily budget, while 4+ position days use full size."""
|
|
|
|
|
|
sparse_day_size_floor: float = 1.0
|
|
|
"""Minimum day-level size scaler when full_size_positions_threshold is active.
|
|
|
0.5 means even a 1-position day still deploys 50% of the normal daily budget."""
|
|
|
|
|
|
initial_capital: float = 10_000.0
|
|
|
"""Starting capital in USD."""
|
|
|
|
|
|
slippage_bps: float = 5.0
|
|
|
"""One-way slippage in basis points (applied to both entry and exit fills)."""
|
|
|
|
|
|
market_regime_spy_threshold: float | None = None
|
|
|
"""Skip trading if SPY's morning return (open to entry time) is below this threshold.
|
|
|
E.g. -0.005 = skip if SPY is down more than -0.5% by entry time. None = disabled."""
|
|
|
|
|
|
market_regime_gap_threshold: float | None = None
|
|
|
"""Skip trading if regime ticker's opening gap versus prior close is below this threshold.
|
|
|
This is a day-level guard that remains available even when the regime ticker is not
|
|
|
part of the intraday candidate set."""
|
|
|
|
|
|
market_regime_gap_ticker: str = "SPY"
|
|
|
"""Ticker used for the day-level opening-gap regime check. Default 'SPY'."""
|
|
|
|
|
|
min_candidate_breadth: float | None = None
|
|
|
"""Skip the day if fewer than this fraction of intraday candidates opened above
|
|
|
their prior close. None = disabled."""
|
|
|
|
|
|
min_gap_pct: float | None = None
|
|
|
"""Minimum opening gap versus prior close. None = disabled."""
|
|
|
|
|
|
max_gap_pct: float | None = None
|
|
|
"""Maximum opening gap versus prior close. None = disabled."""
|
|
|
|
|
|
min_volume_ratio_14d: float | None = None
|
|
|
"""Minimum cumulative volume by entry time divided by 14-day average daily volume.
|
|
|
Helps reject low-attention names that are up but not truly in play."""
|
|
|
|
|
|
min_ret_5d: float | None = None
|
|
|
"""Minimum prior 5-day return. None = disabled."""
|
|
|
|
|
|
min_entropy_20d: float | None = None
|
|
|
"""Minimum allowed entropy(20d). None = disabled."""
|
|
|
|
|
|
max_entropy_20d: float | None = None
|
|
|
"""Maximum allowed entropy(20d). None = disabled."""
|
|
|
|
|
|
max_vix: float | None = None
|
|
|
"""Skip the whole day if VIX closes above this threshold. None = disabled."""
|
|
|
|
|
|
vix_size_scale_low: float | None = None
|
|
|
"""VIX level where size scaling starts. None = disabled."""
|
|
|
|
|
|
vix_size_scale_high: float | None = None
|
|
|
"""VIX level where the day-size scaler reaches vix_size_scale_min."""
|
|
|
|
|
|
vix_size_scale_min: float = 1.0
|
|
|
"""Minimum position-size scaler once VIX reaches vix_size_scale_high."""
|
|
|
|
|
|
regime_size_scale_low: float | None = None
|
|
|
"""Regime gap at which the day-size scaler bottoms out. None = disabled."""
|
|
|
|
|
|
regime_size_scale_high: float | None = None
|
|
|
"""Regime gap at which the day-size scaler returns to 1.0."""
|
|
|
|
|
|
regime_size_scale_min: float = 1.0
|
|
|
"""Minimum regime day-size scaler once regime_size_scale_low is breached."""
|
|
|
|
|
|
regime_skip_below: float | None = None
|
|
|
"""Hard skip floor below regime_size_scale_low. None = no extra skip."""
|
|
|
|
|
|
breadth_size_scale_low: float | None = None
|
|
|
"""Breadth ratio at which the day-size scaler bottoms out. None = disabled."""
|
|
|
|
|
|
breadth_size_scale_high: float | None = None
|
|
|
"""Breadth ratio at which the day-size scaler returns to 1.0."""
|
|
|
|
|
|
breadth_size_scale_min: float = 1.0
|
|
|
"""Minimum breadth day-size scaler once breadth_size_scale_low is breached."""
|
|
|
|
|
|
breadth_skip_below: float | None = None
|
|
|
"""Hard skip floor on candidate breadth. None = no extra skip."""
|
|
|
|
|
|
sector_concentration_scale_low: float | None = None
|
|
|
"""Sector concentration ratio where basket-level size scaling starts.
|
|
|
|
|
|
Concentration is measured as max_sector_count / selected_count using only
|
|
|
selected picks with known sectors.
|
|
|
"""
|
|
|
|
|
|
sector_concentration_scale_high: float | None = None
|
|
|
"""Sector concentration ratio where the basket-level sector scaler reaches
|
|
|
sector_concentration_scale_min."""
|
|
|
|
|
|
sector_concentration_scale_min: float = 1.0
|
|
|
"""Minimum basket-level sector-concentration scaler."""
|
|
|
|
|
|
soft_day_scaler_threshold: float = 1.0
|
|
|
"""Days whose combined regime/breadth scaler falls below this threshold are treated
|
|
|
as soft days for extra basket throttles."""
|
|
|
|
|
|
soft_day_max_trades: int | None = None
|
|
|
"""Maximum number of trades allowed on soft days. None = no extra cap."""
|
|
|
|
|
|
soft_day_sparse_max_trades: int | None = None
|
|
|
"""Optional extra scaler for sparse baskets on soft days.
|
|
|
|
|
|
When set, the soft-day sparse defense only considers days whose final
|
|
|
selected basket size is at or below this count.
|
|
|
"""
|
|
|
|
|
|
soft_day_sparse_require_no_event: bool = False
|
|
|
"""When True, do not apply the soft-day sparse scaler if the basket already
|
|
|
contains a supported event-qualified name."""
|
|
|
|
|
|
soft_day_sparse_exempt_largecap: bool = False
|
|
|
"""When True, do not apply the soft-day sparse scaler when the basket
|
|
|
includes a liquid large-cap candidate."""
|
|
|
|
|
|
soft_day_sparse_exempt_moderate_gap_liquid: bool = False
|
|
|
"""When True, do not apply the soft-day sparse scaler when the basket
|
|
|
includes a moderate-gap liquid follow-through candidate."""
|
|
|
|
|
|
soft_day_sparse_scale: float = 1.0
|
|
|
"""Extra day-size scaler applied to sparse soft-day baskets."""
|
|
|
|
|
|
event_sleeve_soft_day_max_trades: int | None = None
|
|
|
"""When set, only enable the soft-day event sleeve if the pre-event basket
|
|
|
has at most this many selected names."""
|
|
|
|
|
|
event_sleeve_soft_day_max_avg_quality: float | None = None
|
|
|
"""When set, only enable the soft-day event sleeve if the pre-event basket's
|
|
|
average quality score is at or below this threshold."""
|
|
|
|
|
|
event_sleeve_soft_day_require_no_existing_event: bool = False
|
|
|
"""When True, only enable the soft-day event sleeve if the pre-event basket
|
|
|
does not already contain an event-qualified pick."""
|
|
|
|
|
|
tail_risk_day_max_trades: int | None = None
|
|
|
"""Optional meta-layer for sparse, high-extension basket risk.
|
|
|
|
|
|
When set, the tail-risk defense only considers days whose final basket size
|
|
|
is at or below this count.
|
|
|
"""
|
|
|
|
|
|
tail_risk_day_min_max_gain_pct: float | None = None
|
|
|
"""Minimum maximum morning gain required to trigger the tail-risk day defense."""
|
|
|
|
|
|
tail_risk_day_max_avg_quality: float | None = None
|
|
|
"""Maximum average basket quality allowed to trigger the tail-risk day defense."""
|
|
|
|
|
|
tail_risk_day_max_support_score: float | None = None
|
|
|
"""Maximum same-day support score allowed to trigger the tail-risk defense.
|
|
|
|
|
|
Support blends prior liquidity, entry-time liquidity, and same-day
|
|
|
catalyst/attention proxies. Lower values indicate thin, weakly supported
|
|
|
moves more prone to failure on sparse single-name days.
|
|
|
"""
|
|
|
|
|
|
tail_risk_day_min_max_entropy_20d: float | None = None
|
|
|
"""Minimum highest selected entropy_20d required to trigger the tail-risk defense."""
|
|
|
|
|
|
tail_risk_day_min_max_confirmation_return_pct: float | None = None
|
|
|
"""Minimum highest selected confirmation return required to trigger the tail-risk defense."""
|
|
|
|
|
|
tail_risk_day_require_no_event: bool = False
|
|
|
"""When True, the tail-risk day defense only triggers if no selected pick has
|
|
|
an event-qualified catalyst."""
|
|
|
|
|
|
tail_risk_day_event_exemption_min_support_score: float | None = None
|
|
|
"""Minimum support score required for an event-qualified pick to exempt the
|
|
|
day from tail-risk defense.
|
|
|
|
|
|
This prevents weak catalysts on thin, single-name days from disabling the
|
|
|
sparse-day defense merely because an event flag exists.
|
|
|
"""
|
|
|
|
|
|
tail_risk_day_exempt_largecap: bool = False
|
|
|
"""When True, skip the tail-risk day defense whenever the selected basket
|
|
|
contains a liquid large-cap candidate."""
|
|
|
|
|
|
tail_risk_day_scale: float = 1.0
|
|
|
"""Minimum extra day-size scaler applied when the tail-risk defense triggers."""
|
|
|
|
|
|
low_momentum_single_name_max_gain_pct: float | None = None
|
|
|
"""Scale sparse single-name days when the only pick has weak morning gain.
|
|
|
|
|
|
This catches low-conviction continuation attempts that are not high-extension
|
|
|
tail-risk days but still concentrate the full day budget in one marginal name.
|
|
|
"""
|
|
|
|
|
|
low_momentum_single_name_require_no_event: bool = False
|
|
|
"""When True, do not apply the low-momentum single-name scaler if the pick
|
|
|
has a supported event-qualified catalyst."""
|
|
|
|
|
|
low_momentum_single_name_exempt_largecap: bool = False
|
|
|
"""When True, do not apply the low-momentum single-name scaler to liquid
|
|
|
large-cap candidates."""
|
|
|
|
|
|
low_momentum_single_name_scale: float = 1.0
|
|
|
"""Extra day-size scaler applied to low-momentum single-name days."""
|
|
|
|
|
|
basket_quality_relative_floor: float | None = None
|
|
|
"""Optional dynamic floor applied after basket selection.
|
|
|
|
|
|
When set, keep only picks whose day-level quality score is at least this
|
|
|
fraction of the best selected pick's score. This turns a fixed top-N basket
|
|
|
into an adaptive basket that can shrink on weak tail names.
|
|
|
"""
|
|
|
|
|
|
basket_quality_min_count: int = 0
|
|
|
"""Minimum number of picks to keep even when basket_quality_relative_floor prunes
|
|
|
weak tail names. 0 means no forced minimum beyond surviving the floor."""
|
|
|
|
|
|
basket_quality_prune_blend_only: bool = False
|
|
|
"""When True, apply the basket quality floor only to 'blend' tail fills and keep
|
|
|
sleeve-forced picks intact. This is useful when the goal is to stop overfilling
|
|
|
weak residual names without undoing the intended sleeve diversification."""
|
|
|
|
|
|
rolling_loss_days: int | None = None
|
|
|
"""Lookback window for a self-referential rolling loss pause. None = disabled."""
|
|
|
|
|
|
rolling_loss_threshold: float | None = None
|
|
|
"""Pause trading when the strategy's rolling return over rolling_loss_days drops
|
|
|
below this threshold. None = disabled."""
|
|
|
|
|
|
entropy_size_scale_low: float | None = None
|
|
|
"""Entropy level where trade-size scaling starts. None = disabled."""
|
|
|
|
|
|
entropy_size_scale_high: float | None = None
|
|
|
"""Entropy level where the trade-size scaler reaches entropy_size_scale_min."""
|
|
|
|
|
|
entropy_size_scale_min: float = 1.0
|
|
|
"""Minimum per-trade size scaler once entropy reaches entropy_size_scale_high."""
|
|
|
|
|
|
use_five_sleeves: bool = False
|
|
|
"""When True, build the daily basket from five sleeve rankings instead of one raw top-N list.
|
|
|
Sleeves: core gain, gap, volume surprise, low entropy, and prior trend."""
|
|
|
|
|
|
momentum_selection_mode: str = "standard"
|
|
|
"""How to build the execution basket from eligible momentum candidates.
|
|
|
|
|
|
- standard: existing sleeve/blend selection path
|
|
|
- liquid_continuation: prioritize moderate-gap liquid, liquid large-cap,
|
|
|
and sector breadth-confirmed continuation names in the core basket
|
|
|
"""
|
|
|
|
|
|
five_sleeve_force_count: int = 5
|
|
|
"""How many sleeve-specific picks to force before the weighted blend fill starts.
|
|
|
5 preserves the original behavior of taking one pick from each sleeve.
|
|
|
Lower values let the weighted blend dominate sooner and reduce quota-style overfitting."""
|
|
|
|
|
|
five_sleeve_core_weight: float = 0.40
|
|
|
"""Blend weight for the core momentum sleeve (morning gain)."""
|
|
|
|
|
|
five_sleeve_gap_weight: float = 0.20
|
|
|
"""Blend weight for the opening-gap sleeve."""
|
|
|
|
|
|
five_sleeve_volume_weight: float = 0.25
|
|
|
"""Blend weight for the volume-surprise sleeve."""
|
|
|
|
|
|
five_sleeve_entropy_weight: float = 0.05
|
|
|
"""Blend weight for the low-entropy sleeve."""
|
|
|
|
|
|
five_sleeve_trend_weight: float = 0.10
|
|
|
"""Blend weight for the prior-trend sleeve."""
|
|
|
|
|
|
use_event_sleeve: bool = False
|
|
|
"""When True, allow a dedicated same-day filing catalyst sleeve.
|
|
|
|
|
|
This sleeve only changes basket selection among already-tradable
|
|
|
intraday names; it does not bypass the base execution filters.
|
|
|
"""
|
|
|
|
|
|
event_weight: float = 0.0
|
|
|
"""Blend weight for the same-day filing catalyst sleeve."""
|
|
|
|
|
|
event_min_score: float | None = None
|
|
|
"""Minimum same-day filing event score required for event-sleeve eligibility."""
|
|
|
|
|
|
event_sleeve_soft_day_only: bool = False
|
|
|
"""Enable the event sleeve only on soft days.
|
|
|
|
|
|
Soft-day detection reuses the existing regime/breadth/sector scaler
|
|
|
machinery so event-driven substitutions only happen when the normal
|
|
|
basket looks weak.
|
|
|
"""
|
|
|
|
|
|
use_slow_ignite_sleeve: bool = False
|
|
|
"""When True, allow one extra sleeve for slower-starting but still high-attention leaders.
|
|
|
This is intended for names that are not yet above the primary morning-gain floor by the
|
|
|
standard entry time, but are showing strong confirmation, liquidity, and prior trend."""
|
|
|
|
|
|
slow_ignite_weight: float = 0.0
|
|
|
"""Blend weight for the slow-ignite sleeve. Ignored when use_slow_ignite_sleeve is False."""
|
|
|
|
|
|
slow_ignite_min_gain_pct: float | None = None
|
|
|
"""Lower gain floor for slow-ignite candidates. Typically below min_morning_gain_pct."""
|
|
|
|
|
|
slow_ignite_max_gain_pct: float | None = None
|
|
|
"""Upper gain cap for slow-ignite candidates. Keeps the sleeve focused on slower starters."""
|
|
|
|
|
|
slow_ignite_min_entry_dollar_volume: float | None = None
|
|
|
"""Minimum entry-time dollar volume required for slow-ignite candidates."""
|
|
|
|
|
|
slow_ignite_min_volume_ratio_14d: float | None = None
|
|
|
"""Minimum entry-time volume ratio required for slow-ignite candidates."""
|
|
|
|
|
|
slow_ignite_min_ret_5d: float | None = None
|
|
|
"""Minimum prior 5-day return required for slow-ignite candidates."""
|
|
|
|
|
|
slow_ignite_max_entropy_20d: float | None = None
|
|
|
"""Maximum entropy allowed for slow-ignite candidates."""
|
|
|
|
|
|
use_liquid_largecap_sleeve: bool = False
|
|
|
"""When True, allow a dedicated sleeve for liquid large-cap leaders.
|
|
|
This is designed for slower large-cap continuation names that may not rank highly
|
|
|
in the small/midcap-oriented momentum sleeves despite strong notional liquidity."""
|
|
|
|
|
|
liquid_largecap_weight: float = 0.0
|
|
|
"""Blend weight for the liquid large-cap sleeve. Ignored when disabled."""
|
|
|
|
|
|
liquid_largecap_min_gain_pct: float | None = None
|
|
|
"""Lower gain floor for liquid large-cap candidates."""
|
|
|
|
|
|
liquid_largecap_max_gain_pct: float | None = None
|
|
|
"""Upper gain cap for liquid large-cap candidates."""
|
|
|
|
|
|
liquid_largecap_min_confirmation_return_pct: float | None = None
|
|
|
"""Minimum follow-through required for liquid large-cap candidates."""
|
|
|
|
|
|
liquid_largecap_min_entry_dollar_volume: float | None = None
|
|
|
"""Minimum entry-time dollar volume required for liquid large-cap candidates."""
|
|
|
|
|
|
liquid_largecap_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum prior 30-day average daily dollar volume required for liquid large-cap candidates."""
|
|
|
|
|
|
liquid_largecap_max_entropy_20d: float | None = None
|
|
|
"""Maximum entropy allowed for liquid large-cap candidates."""
|
|
|
|
|
|
use_moderate_gap_liquid_sleeve: bool = False
|
|
|
"""When True, enable a dedicated sleeve for moderate-gap liquid follow-through names.
|
|
|
|
|
|
This targets names that are too moderate to win the raw gap/momentum rank,
|
|
|
but have enough same-day follow-through and institutional liquidity to be
|
|
|
distinct from thin small-cap attention spikes.
|
|
|
"""
|
|
|
|
|
|
moderate_gap_liquid_weight: float = 0.0
|
|
|
"""Blend weight for the moderate-gap liquid follow-through sleeve."""
|
|
|
|
|
|
moderate_gap_liquid_min_gap_pct: float | None = None
|
|
|
"""Minimum opening gap for moderate-gap liquid candidates."""
|
|
|
|
|
|
moderate_gap_liquid_max_gap_pct: float | None = None
|
|
|
"""Maximum opening gap for moderate-gap liquid candidates."""
|
|
|
|
|
|
moderate_gap_liquid_min_gain_pct: float | None = None
|
|
|
"""Minimum entry-time gain for moderate-gap liquid candidates."""
|
|
|
|
|
|
moderate_gap_liquid_max_gain_pct: float | None = None
|
|
|
"""Maximum entry-time gain for moderate-gap liquid candidates."""
|
|
|
|
|
|
moderate_gap_liquid_min_confirmation_return_pct: float | None = None
|
|
|
"""Minimum confirmation-bar follow-through for moderate-gap liquid candidates."""
|
|
|
|
|
|
moderate_gap_liquid_min_entry_dollar_volume: float | None = None
|
|
|
"""Minimum entry-time cumulative dollar volume for moderate-gap liquid candidates."""
|
|
|
|
|
|
moderate_gap_liquid_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum prior 30-day average daily dollar volume for moderate-gap liquid candidates."""
|
|
|
|
|
|
moderate_gap_liquid_max_avg_dollar_vol_30d: float | None = None
|
|
|
"""Optional upper bound on prior 30-day dollar volume.
|
|
|
|
|
|
This keeps the sleeve from duplicating the liquid-largecap sleeve when the
|
|
|
intended target is mid/liquid follow-through names such as TER/CRDO/CDNS/CPNG.
|
|
|
"""
|
|
|
|
|
|
moderate_gap_liquid_min_volume_ratio_14d: float | None = None
|
|
|
"""Minimum entry-time volume ratio for moderate-gap liquid candidates."""
|
|
|
|
|
|
moderate_gap_liquid_min_atr_pct: float | None = None
|
|
|
"""Minimum ATR(14) divided by today's open for moderate-gap liquid candidates."""
|
|
|
|
|
|
moderate_gap_liquid_max_entropy_20d: float | None = None
|
|
|
"""Maximum entropy allowed for moderate-gap liquid candidates."""
|
|
|
|
|
|
use_sector_thrust_sleeve: bool = False
|
|
|
"""When True, enable a sector breadth-confirmed thrust sleeve.
|
|
|
|
|
|
This is a PEAD-style synthetic breadth idea adapted to intraday momentum:
|
|
|
the sleeve only boosts names whose own early trend is supported by multiple
|
|
|
same-sector leaders showing synchronous confirmation and liquidity.
|
|
|
"""
|
|
|
|
|
|
sector_thrust_weight: float = 0.0
|
|
|
"""Blend weight for the sector breadth-confirmed thrust sleeve."""
|
|
|
|
|
|
sector_thrust_min_members: int = 2
|
|
|
"""Minimum number of same-sector names that must pass the thrust gate."""
|
|
|
|
|
|
sector_thrust_min_gain_pct: float | None = None
|
|
|
"""Minimum morning gain required for a ticker to contribute to sector thrust."""
|
|
|
|
|
|
sector_thrust_min_confirmation_return_pct: float | None = None
|
|
|
"""Minimum confirmation return required for sector thrust contributors."""
|
|
|
|
|
|
sector_thrust_min_entry_dollar_volume: float | None = None
|
|
|
"""Minimum entry-time dollar volume required for sector thrust contributors."""
|
|
|
|
|
|
sector_thrust_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum prior 30-day average dollar volume required for sector thrust contributors."""
|
|
|
|
|
|
sector_thrust_min_sector_avg_confirmation_return_pct: float | None = None
|
|
|
"""Minimum average confirmation return across same-sector contributors."""
|
|
|
|
|
|
sector_thrust_min_sector_total_entry_dollar_volume: float | None = None
|
|
|
"""Minimum total entry-time dollar volume across same-sector contributors."""
|
|
|
|
|
|
use_liquid_cluster_engine: bool = False
|
|
|
"""When True, enable a separate post-allocation liquid-cluster stock engine.
|
|
|
|
|
|
Unlike sector_thrust, this does not change the main basket rank. It uses a
|
|
|
reserved fraction of the day budget to add a small number of liquid,
|
|
|
same-sector follow-through names after the core basket is selected.
|
|
|
"""
|
|
|
|
|
|
liquid_cluster_capital_fraction: float = 0.0
|
|
|
"""Fraction of the day budget reserved for the liquid-cluster engine."""
|
|
|
|
|
|
liquid_cluster_max_positions: int = 0
|
|
|
"""Maximum number of liquid-cluster stock positions to add."""
|
|
|
|
|
|
liquid_cluster_max_positions_per_sector: int = 1
|
|
|
"""Maximum number of liquid-cluster stock picks per sector."""
|
|
|
|
|
|
liquid_cluster_min_members: int = 2
|
|
|
"""Minimum number of same-sector names required to activate a cluster."""
|
|
|
|
|
|
liquid_cluster_min_gain_pct: float | None = None
|
|
|
"""Minimum morning gain required for a name to contribute to a liquid cluster."""
|
|
|
|
|
|
liquid_cluster_max_gain_pct: float | None = None
|
|
|
"""Maximum morning gain allowed for liquid-cluster contributors."""
|
|
|
|
|
|
liquid_cluster_min_confirmation_return_pct: float | None = None
|
|
|
"""Minimum confirmation return required for liquid-cluster contributors."""
|
|
|
|
|
|
liquid_cluster_min_entry_dollar_volume: float | None = None
|
|
|
"""Minimum entry-time dollar volume required for liquid-cluster contributors."""
|
|
|
|
|
|
liquid_cluster_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum prior 30-day average dollar volume required for liquid-cluster contributors."""
|
|
|
|
|
|
liquid_cluster_max_avg_dollar_vol_30d: float | None = None
|
|
|
"""Optional upper bound on prior 30-day average dollar volume for cluster contributors."""
|
|
|
|
|
|
liquid_cluster_min_volume_ratio_14d: float | None = None
|
|
|
"""Minimum entry-time volume ratio required for liquid-cluster contributors."""
|
|
|
|
|
|
liquid_cluster_max_entropy_20d: float | None = None
|
|
|
"""Maximum entropy allowed for liquid-cluster contributors."""
|
|
|
|
|
|
liquid_cluster_min_sector_avg_confirmation_return_pct: float | None = None
|
|
|
"""Minimum average confirmation return across the activated liquid cluster."""
|
|
|
|
|
|
liquid_cluster_min_sector_total_entry_dollar_volume: float | None = None
|
|
|
"""Minimum combined entry-time dollar volume across the activated liquid cluster."""
|
|
|
|
|
|
liquid_cluster_require_special_liquidity_gate: bool = False
|
|
|
"""When True, contributors must already qualify as moderate-gap liquid or liquid large-cap."""
|
|
|
|
|
|
use_event_day_liquid_sleeve: bool = False
|
|
|
"""When True, activate a separate post-allocation liquid continuation sleeve on event-backed days.
|
|
|
|
|
|
This engine does not alter the core basket rank. It reserves a small slice
|
|
|
of the day budget to add liquid continuation names only when at least one
|
|
|
approved same-day event is also visible in the morning tape.
|
|
|
"""
|
|
|
|
|
|
event_day_liquid_capital_fraction: float = 0.0
|
|
|
"""Fraction of the day budget reserved for the event-day liquid sleeve."""
|
|
|
|
|
|
event_day_liquid_max_positions: int = 0
|
|
|
"""Maximum number of event-day liquid continuation names to add."""
|
|
|
|
|
|
event_day_liquid_soft_day_only: bool = False
|
|
|
"""Only activate the event-day liquid sleeve on soft days."""
|
|
|
|
|
|
event_day_liquid_min_event_names: int = 1
|
|
|
"""Minimum number of event-backed morning names required to activate the sleeve."""
|
|
|
|
|
|
event_day_liquid_allowed_event_types: list[str] = Field(default_factory=list)
|
|
|
"""Optional event types used only for event-day sleeve activation.
|
|
|
|
|
|
When empty, activation reuses the filtered event state already applied to
|
|
|
the core momentum strategy. When set, activation can see a broader set of
|
|
|
raw filing types without contaminating the core event sleeves.
|
|
|
"""
|
|
|
|
|
|
event_day_liquid_min_event_score: float | None = None
|
|
|
"""Minimum same-day event score required for activation contributors."""
|
|
|
|
|
|
event_day_liquid_min_event_support_score: float | None = None
|
|
|
"""Minimum support score required for activation contributors."""
|
|
|
|
|
|
event_day_liquid_min_total_event_entry_dollar_volume: float | None = None
|
|
|
"""Minimum combined entry-time dollar volume across activation contributors."""
|
|
|
|
|
|
event_day_liquid_min_gain_pct: float | None = None
|
|
|
"""Minimum morning gain required for added liquid continuation names."""
|
|
|
|
|
|
event_day_liquid_max_gain_pct: float | None = None
|
|
|
"""Maximum morning gain allowed for added liquid continuation names."""
|
|
|
|
|
|
event_day_liquid_min_confirmation_return_pct: float | None = None
|
|
|
"""Minimum confirmation return required for added liquid continuation names."""
|
|
|
|
|
|
event_day_liquid_min_entry_dollar_volume: float | None = None
|
|
|
"""Minimum entry-time dollar volume required for added liquid continuation names."""
|
|
|
|
|
|
event_day_liquid_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum prior 30-day average dollar volume required for added liquid names."""
|
|
|
|
|
|
event_day_liquid_max_entropy_20d: float | None = None
|
|
|
"""Maximum 20-day entropy allowed for added liquid continuation names."""
|
|
|
|
|
|
event_day_liquid_min_support_score: float | None = None
|
|
|
"""Minimum blended support score required for added liquid continuation names."""
|
|
|
|
|
|
use_sector_etf_sleeve: bool = False
|
|
|
"""When True, allow a post-allocation sector ETF proxy sleeve.
|
|
|
|
|
|
This sleeve uses the same activated liquid-cluster sectors, but deploys a
|
|
|
reserved capital slice into sector ETFs instead of additional single-name
|
|
|
positions.
|
|
|
"""
|
|
|
|
|
|
sector_etf_capital_fraction: float = 0.0
|
|
|
"""Fraction of the day budget reserved for the sector ETF sleeve."""
|
|
|
|
|
|
sector_etf_max_positions: int = 1
|
|
|
"""Maximum number of sector ETF proxy positions to add."""
|
|
|
|
|
|
sector_etf_min_sector_score: float | None = None
|
|
|
"""Minimum liquid-cluster sector score required for ETF sleeve activation."""
|
|
|
|
|
|
use_gap_reclaim_sleeve: bool = False
|
|
|
"""Enable a high-gap reclaim sleeve for early flushes that stabilize below the open."""
|
|
|
|
|
|
gap_reclaim_weight: float = 0.0
|
|
|
"""Blend weight for the high-gap reclaim sleeve."""
|
|
|
|
|
|
gap_reclaim_min_gap_pct: float | None = None
|
|
|
"""Minimum opening gap required for a high-gap reclaim candidate."""
|
|
|
|
|
|
gap_reclaim_min_gain_pct: float | None = None
|
|
|
"""Minimum allowed entry-time gain for the reclaim sleeve (can be negative)."""
|
|
|
|
|
|
gap_reclaim_max_gain_pct: float | None = None
|
|
|
"""Maximum allowed entry-time gain for the reclaim sleeve."""
|
|
|
|
|
|
gap_reclaim_min_confirmation_return_pct: float | None = None
|
|
|
"""Minimum confirmation-bar return required for the reclaim sleeve."""
|
|
|
|
|
|
gap_reclaim_min_entry_dollar_volume: float | None = None
|
|
|
"""Minimum entry-time cumulative dollar volume required for the reclaim sleeve."""
|
|
|
|
|
|
gap_reclaim_min_recovery_from_opening_low_pct: float | None = None
|
|
|
"""Minimum rebound from the opening-range low required for the reclaim sleeve."""
|
|
|
|
|
|
fallback_liquid_largecap_slots: int = 0
|
|
|
"""Number of liquid large-cap fallback seats available after regular selection.
|
|
|
|
|
|
This does not change the main basket on normal days. It only allows a small
|
|
|
number of highly liquid large-cap names to fill otherwise sparse baskets.
|
|
|
"""
|
|
|
|
|
|
fallback_liquid_largecap_trigger_below: int = 0
|
|
|
"""Enable the liquid large-cap fallback only when regular picks are below this count.
|
|
|
|
|
|
Example: 2 means "only consider fallback seats when the main selection
|
|
|
found fewer than 2 names."
|
|
|
"""
|
|
|
|
|
|
candidate_source_mode: str = "daily_gap"
|
|
|
"""How to build the candidate universe before the final basket is selected.
|
|
|
|
|
|
- daily_gap: existing point-in-time opening-gap shortlist from daily bars
|
|
|
- intraday_first: build a broader daily seed list, fetch intraday for that
|
|
|
seed, then rank the final candidate shortlist using same-day entry-time
|
|
|
information only (still lookahead-free).
|
|
|
"""
|
|
|
|
|
|
candidate_seed_threshold: float = 0.0
|
|
|
"""Opening-gap threshold used only for the broader seed list when
|
|
|
candidate_source_mode='intraday_first'. Lower values widen the intraday
|
|
|
fetch universe without using same-day highs/closes."""
|
|
|
|
|
|
candidate_seed_max_per_day: int = 150
|
|
|
"""Maximum seed shortlist size per day when candidate_source_mode is
|
|
|
'intraday_first'. This bounds intraday fetch cost before the final
|
|
|
entry-time rerank."""
|
|
|
|
|
|
candidate_seed_liquid_overlay_slots: int = 0
|
|
|
"""Optional number of extra prior-day liquid large-cap seeds to add per day.
|
|
|
|
|
|
This is designed for names like TSLA/AVGO/NVDA that may not clear the main
|
|
|
opening-gap seed threshold but still deserve intraday-first evaluation
|
|
|
because of exceptional prior-day liquidity.
|
|
|
"""
|
|
|
|
|
|
candidate_seed_liquid_min_gap_pct: float | None = None
|
|
|
"""Minimum opening gap for the liquid overlay seed list.
|
|
|
|
|
|
Uses today's open vs prior close only, so it remains lookahead-free.
|
|
|
"""
|
|
|
|
|
|
candidate_seed_liquid_max_gap_pct: float | None = None
|
|
|
"""Maximum opening gap for the liquid overlay seed list."""
|
|
|
|
|
|
candidate_seed_liquid_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum prior 30-day average dollar volume required for overlay seeds."""
|
|
|
|
|
|
candidate_seed_liquid_min_ret_5d: float | None = None
|
|
|
"""Minimum prior 5-day return required for overlay seeds."""
|
|
|
|
|
|
candidate_seed_liquid_max_entropy_20d: float | None = None
|
|
|
"""Maximum prior 20-day entropy allowed for overlay seeds."""
|
|
|
|
|
|
candidate_seed_leader_overlay_slots: int = 0
|
|
|
"""Optional number of extra liquid trend-leader seeds to add per day.
|
|
|
|
|
|
Unlike the liquid gap overlay, this path is meant to catch strong same-day
|
|
|
continuation names that did not gap enough to enter the main seed list but
|
|
|
already have exceptional prior trend, volatility, and liquidity.
|
|
|
"""
|
|
|
|
|
|
candidate_seed_leader_min_gap_pct: float | None = None
|
|
|
"""Minimum opening gap allowed for the trend-leader overlay."""
|
|
|
|
|
|
candidate_seed_leader_max_gap_pct: float | None = None
|
|
|
"""Maximum opening gap allowed for the trend-leader overlay."""
|
|
|
|
|
|
candidate_seed_leader_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum prior 30-day average dollar volume for trend-leader overlay seeds."""
|
|
|
|
|
|
candidate_seed_leader_min_ret_5d: float | None = None
|
|
|
"""Minimum prior 5-day return required for trend-leader overlay seeds."""
|
|
|
|
|
|
candidate_seed_leader_min_atr_pct: float | None = None
|
|
|
"""Minimum ATR/open ratio required for trend-leader overlay seeds."""
|
|
|
|
|
|
candidate_seed_leader_max_entropy_20d: float | None = None
|
|
|
"""Maximum prior 20-day entropy allowed for trend-leader overlay seeds."""
|
|
|
|
|
|
candidate_seed_moderate_liquid_overlay_slots: int = 0
|
|
|
"""Number of extra moderate-gap liquid follow-through seeds to append per day.
|
|
|
|
|
|
This is intentionally separate from the base candidate rank. It widens the
|
|
|
intraday fetch set only for a bounded sleeve-specific profile, rather than
|
|
|
diluting the main gapper seed list.
|
|
|
"""
|
|
|
|
|
|
candidate_seed_moderate_liquid_min_gap_pct: float | None = None
|
|
|
"""Minimum opening gap for moderate-liquid overlay seeds."""
|
|
|
|
|
|
candidate_seed_moderate_liquid_max_gap_pct: float | None = None
|
|
|
"""Maximum opening gap for moderate-liquid overlay seeds."""
|
|
|
|
|
|
candidate_seed_moderate_liquid_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum prior 30-day average dollar volume for moderate-liquid overlay seeds."""
|
|
|
|
|
|
candidate_seed_moderate_liquid_max_avg_dollar_vol_30d: float | None = None
|
|
|
"""Optional upper bound on prior 30-day dollar volume for moderate-liquid seeds."""
|
|
|
|
|
|
candidate_seed_moderate_liquid_min_ret_5d: float | None = None
|
|
|
"""Minimum prior 5-day return for moderate-liquid overlay seeds."""
|
|
|
|
|
|
candidate_seed_moderate_liquid_max_entropy_20d: float | None = None
|
|
|
"""Maximum prior 20-day entropy for moderate-liquid overlay seeds."""
|
|
|
|
|
|
candidate_seed_event_overlay_slots: int = 0
|
|
|
"""Number of actual filing-catalyst names to force into the seed shortlist.
|
|
|
|
|
|
This is a candidate-stage overlay, not a final-rank weight. It exists to
|
|
|
make sure same-day catalyst names are present in the intraday fetch set
|
|
|
even when pure gap/ret/entropy seed ranking would miss them.
|
|
|
"""
|
|
|
|
|
|
candidate_seed_event_min_score: float | None = None
|
|
|
"""Minimum same-day filing event score required for the event overlay."""
|
|
|
|
|
|
candidate_seed_event_min_gap_pct: float | None = None
|
|
|
"""Minimum opening gap for event-overlay names."""
|
|
|
|
|
|
candidate_seed_event_max_gap_pct: float | None = None
|
|
|
"""Maximum opening gap for event-overlay names."""
|
|
|
|
|
|
candidate_seed_event_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum prior 30-day average dollar volume for event-overlay names."""
|
|
|
|
|
|
candidate_seed_event_min_ret_5d: float | None = None
|
|
|
"""Minimum prior 5-day return for event-overlay names."""
|
|
|
|
|
|
candidate_seed_event_max_entropy_20d: float | None = None
|
|
|
"""Maximum prior 20-day entropy allowed for event-overlay names."""
|
|
|
|
|
|
candidate_final_max_per_day: int = 30
|
|
|
"""Final candidate shortlist size per day after intraday-first reranking."""
|
|
|
|
|
|
candidate_intraday_rank_mode: str = "sleeves"
|
|
|
"""Final intraday-first shortlist ranking mode.
|
|
|
|
|
|
- sleeves: reuse the same five-sleeve / top-N basket logic used by the
|
|
|
execution engine, but at a wider candidate cutoff.
|
|
|
- weighted: rank entry-time candidates with a weighted quality score using
|
|
|
only same-day information known by the entry/confirmation bar.
|
|
|
"""
|
|
|
|
|
|
candidate_intraday_weight_gain: float = 0.0
|
|
|
"""Weighted-mode contribution from same-day morning gain at entry."""
|
|
|
|
|
|
candidate_intraday_weight_confirmation: float = 0.0
|
|
|
"""Weighted-mode contribution from confirmation-bar follow-through."""
|
|
|
|
|
|
candidate_intraday_weight_volume_ratio: float = 0.0
|
|
|
"""Weighted-mode contribution from entry-time volume ratio vs 14-day ADV."""
|
|
|
|
|
|
candidate_intraday_weight_entry_dollar_volume: float = 0.0
|
|
|
"""Weighted-mode contribution from entry-time cumulative dollar volume."""
|
|
|
|
|
|
candidate_intraday_weight_avg_dollar_vol_30d: float = 0.0
|
|
|
"""Weighted-mode contribution from prior 30-day average dollar volume."""
|
|
|
|
|
|
candidate_intraday_weight_support_score: float = 0.0
|
|
|
"""Weighted-mode contribution from same-day blended support score."""
|
|
|
|
|
|
candidate_intraday_weight_liquid_largecap: float = 0.0
|
|
|
"""Weighted-mode contribution from qualifying as a liquid large-cap name."""
|
|
|
|
|
|
candidate_intraday_weight_moderate_gap_liquid: float = 0.0
|
|
|
"""Weighted-mode contribution from qualifying as a moderate-gap liquid name."""
|
|
|
|
|
|
candidate_intraday_weight_gap: float = 0.0
|
|
|
"""Weighted-mode contribution from opening gap vs prior close."""
|
|
|
|
|
|
candidate_intraday_weight_ret_5d: float = 0.0
|
|
|
"""Weighted-mode contribution from prior 5-day return."""
|
|
|
|
|
|
candidate_intraday_weight_low_entropy: float = 0.0
|
|
|
"""Weighted-mode contribution from lower 20-day entropy."""
|
|
|
|
|
|
candidate_intraday_weight_sector_thrust: float = 0.0
|
|
|
"""Weighted-mode contribution from sector breadth-confirmed thrust."""
|
|
|
|
|
|
candidate_intraday_weight_event_score: float = 0.0
|
|
|
"""Weighted-mode contribution from same-day filing/event score."""
|
|
|
|
|
|
candidate_intraday_weight_attention_wiki: float = 0.0
|
|
|
"""Weighted-mode contribution from same-day wiki attention."""
|
|
|
|
|
|
candidate_intraday_weight_attention_news: float = 0.0
|
|
|
"""Weighted-mode contribution from same-day news/article attention."""
|
|
|
|
|
|
candidate_intraday_event_reserve_slots: int = 0
|
|
|
"""Number of same-day actual catalyst names to reserve inside the final shortlist.
|
|
|
|
|
|
Unlike candidate_seed_event_overlay_slots, this does not expand the
|
|
|
shortlist. It replaces weak tail picks inside candidate_final_max_per_day.
|
|
|
"""
|
|
|
|
|
|
candidate_intraday_event_reserve_min_score: float | None = None
|
|
|
"""Minimum same-day filing event score required for reserve-slot eligibility."""
|
|
|
|
|
|
candidate_intraday_event_reserve_soft_day_only: bool = False
|
|
|
"""Apply the final-shortlist catalyst reserve only on soft days."""
|
|
|
|
|
|
candidate_intraday_moderate_liquid_reserve_slots: int = 0
|
|
|
"""Number of moderate-gap liquid names to reserve inside the final intraday shortlist.
|
|
|
|
|
|
This reserve is applied after normal intraday ranking and replaces weak
|
|
|
non-event tail names. It ensures the downstream sleeve can see qualified
|
|
|
moderate-liquid follow-through names without changing the base rank formula.
|
|
|
"""
|
|
|
|
|
|
candidate_intraday_moderate_liquid_reserve_trigger_below: int = 0
|
|
|
"""Only apply moderate-liquid final reserve when the base shortlist would
|
|
|
produce fewer than this many execution picks. 0 means always allow reserve
|
|
|
when candidate_intraday_moderate_liquid_reserve_slots > 0.
|
|
|
"""
|
|
|
|
|
|
recent_live_scan_days: int = 0
|
|
|
"""When > 0, very recent backtests (window length <= this many trading days and ending
|
|
|
within this many calendar days of the latest completed backtest date) bypass the static
|
|
|
universe + daily pre-screen path and instead use a broad screener universe with
|
|
|
intraday-first candidate generation. Designed for same-day / recent sanity checks where
|
|
|
names like current Yahoo top gainers may not exist in the static research universe."""
|
|
|
|
|
|
recent_live_scan_min_price: float = 2.0
|
|
|
"""Minimum price for the recent live screener universe."""
|
|
|
|
|
|
recent_live_scan_avg_volume_min: int = 200_000
|
|
|
"""Minimum 3-month average volume for the recent live screener universe."""
|
|
|
|
|
|
recent_live_scan_market_cap_min: float = 100_000_000.0
|
|
|
"""Minimum market cap for the recent live screener universe."""
|
|
|
|
|
|
recent_live_scan_max_candidates_per_day: int = 150
|
|
|
"""Maximum daily shortlist size produced by the intraday-first recent scan."""
|
|
|
|
|
|
recent_live_scan_top_n: int | None = None
|
|
|
"""Optional top-N override used only for recent live-scan windows."""
|
|
|
|
|
|
recent_live_scan_min_morning_gain_pct: float | None = None
|
|
|
"""Optional morning-gain floor override used only for recent live-scan windows."""
|
|
|
|
|
|
recent_live_scan_max_morning_gain_pct: float | None = None
|
|
|
"""Optional morning-gain cap override used only for recent live-scan windows."""
|
|
|
|
|
|
recent_live_scan_min_confirmation_return_pct: float | None = None
|
|
|
"""Optional confirmation-return override used only for recent live-scan windows."""
|
|
|
|
|
|
recent_live_scan_min_entry_dollar_volume: float | None = None
|
|
|
"""Optional entry dollar-volume override used only for recent live-scan windows."""
|
|
|
|
|
|
recent_live_scan_max_gap_pct: float | None = None
|
|
|
"""Optional opening-gap cap override used only for recent live-scan windows."""
|
|
|
|
|
|
recent_live_scan_max_entropy_20d: float | None = None
|
|
|
"""Optional entropy cap override used only for recent live-scan windows."""
|
|
|
|
|
|
recent_live_scan_use_slow_ignite_sleeve: bool | None = None
|
|
|
"""Optional recent-window override for enabling the slow-ignite sleeve."""
|
|
|
|
|
|
recent_live_scan_slow_ignite_weight: float | None = None
|
|
|
"""Optional recent-window override for slow-ignite sleeve weight."""
|
|
|
|
|
|
recent_live_scan_slow_ignite_min_gain_pct: float | None = None
|
|
|
"""Optional recent-window override for slow-ignite minimum gain."""
|
|
|
|
|
|
recent_live_scan_slow_ignite_max_gain_pct: float | None = None
|
|
|
"""Optional recent-window override for slow-ignite maximum gain."""
|
|
|
|
|
|
recent_live_scan_slow_ignite_min_entry_dollar_volume: float | None = None
|
|
|
"""Optional recent-window override for slow-ignite minimum entry dollar volume."""
|
|
|
|
|
|
recent_live_scan_slow_ignite_max_entropy_20d: float | None = None
|
|
|
"""Optional recent-window override for slow-ignite maximum entropy."""
|
|
|
|
|
|
recent_live_scan_use_liquid_largecap_sleeve: bool | None = None
|
|
|
"""Optional recent-window override for enabling the liquid large-cap sleeve."""
|
|
|
|
|
|
recent_live_scan_liquid_largecap_weight: float | None = None
|
|
|
"""Optional recent-window override for liquid large-cap sleeve weight."""
|
|
|
|
|
|
recent_live_scan_liquid_largecap_min_gain_pct: float | None = None
|
|
|
"""Optional recent-window override for liquid large-cap minimum gain."""
|
|
|
|
|
|
recent_live_scan_liquid_largecap_max_gain_pct: float | None = None
|
|
|
"""Optional recent-window override for liquid large-cap maximum gain."""
|
|
|
|
|
|
recent_live_scan_liquid_largecap_min_confirmation_return_pct: float | None = None
|
|
|
"""Optional recent-window override for liquid large-cap confirmation return."""
|
|
|
|
|
|
recent_live_scan_liquid_largecap_min_entry_dollar_volume: float | None = None
|
|
|
"""Optional recent-window override for liquid large-cap minimum entry dollar volume."""
|
|
|
|
|
|
recent_live_scan_liquid_largecap_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Optional recent-window override for liquid large-cap minimum average dollar volume."""
|
|
|
|
|
|
recent_live_scan_liquid_largecap_max_entropy_20d: float | None = None
|
|
|
"""Optional recent-window override for liquid large-cap maximum entropy."""
|
|
|
|
|
|
candidate_require_event_flag: bool = False
|
|
|
"""When True, the daily candidate shortlist only includes names with a same-day
|
|
|
filing-based catalyst flag. This is stricter than using attention proxies alone
|
|
|
and is intended for event-driven research variants."""
|
|
|
|
|
|
candidate_min_event_score: float | None = None
|
|
|
"""Minimum same-day filing event score required at the candidate stage.
|
|
|
Ignored when no same-day event features are present."""
|
|
|
|
|
|
candidate_allowed_event_types: list[str] = Field(default_factory=list)
|
|
|
"""Optional same-day filing event types allowed at the candidate stage.
|
|
|
|
|
|
When non-empty, candidate-stage catalyst gates only treat these filing
|
|
|
types as valid. This lets momentum variants use actual catalysts such as
|
|
|
earnings/material events while excluding weaker attention-like filings.
|
|
|
"""
|
|
|
|
|
|
candidate_weight_event_score: float = 0.0
|
|
|
"""Ranking weight for same-day filing event score in momentum candidate selection."""
|
|
|
|
|
|
candidate_weight_attention_wiki: float = 0.0
|
|
|
"""Ranking weight for same-day Wikipedia attention in momentum candidate selection."""
|
|
|
|
|
|
candidate_weight_attention_news: float = 0.0
|
|
|
"""Ranking weight for same-day news/article attention in momentum candidate selection."""
|
|
|
|
|
|
candidate_min_attention_wiki_spike_10d: float | None = None
|
|
|
"""Minimum same-day wiki spike required at the candidate stage."""
|
|
|
|
|
|
candidate_min_attention_article_count_3d: int | None = None
|
|
|
"""Minimum same-day 3-day article count required at the candidate stage."""
|
|
|
|
|
|
candidate_min_attention_us_article_count_3d: int | None = None
|
|
|
"""Minimum same-day 3-day US article count required at the candidate stage."""
|
|
|
|
|
|
candidate_min_attention_resolver_confidence: float | None = None
|
|
|
"""Minimum same-day entity resolver confidence required at the candidate stage."""
|
|
|
|
|
|
|
|
|
class ORBStrategyParams(BaseModel):
|
|
|
"""Parameters for the Opening Range Breakout (ORB) strategy."""
|
|
|
|
|
|
engine_family: str = "quality_breakout"
|
|
|
"""Candidate engine family: classic_breakout | quality_breakout | compression_breakout | gainers_leader | leader_followthrough | stocks_in_play_dual_regime.
|
|
|
quality_breakout is the backward-compatible default because it supports the
|
|
|
existing body-ratio / momentum extensions while leaving them disabled at 0 weight."""
|
|
|
|
|
|
live_readiness: str = "live_ready"
|
|
|
"""Research classification for the signal family: live_ready | research_only."""
|
|
|
|
|
|
# ORB window
|
|
|
orb_minutes: int = 5
|
|
|
"""Duration of the opening range in minutes. 5 = first 5-min candle (9:30–9:35 ET)."""
|
|
|
|
|
|
sim_bar_minutes: int = 5
|
|
|
"""Bar interval for breakout detection and stop management after the ORB candle.
|
|
|
5 = use raw 5-min bars (default). 30 = aggregate to 30-min bars (more realistic, fewer whipsaws).
|
|
|
The ORB candle itself always uses the first 5-min bar regardless of this setting."""
|
|
|
|
|
|
# Entry
|
|
|
entry_direction: str = "long_only"
|
|
|
"""Entry direction filter: 'long_only' (bullish candle only), 'candle' (both), 'both' (always)."""
|
|
|
|
|
|
order_timeout_minutes: int = 45
|
|
|
"""Cancel unfilled breakout order after this many minutes from open. Default = 45min = 10:15 ET."""
|
|
|
|
|
|
# Universe quality filters (applied during ORB pre-screening)
|
|
|
min_price: float = 10.0
|
|
|
"""Minimum stock price. $10 is the ORB paper's practical minimum."""
|
|
|
|
|
|
min_avg_dollar_volume: float = 25_000_000.0
|
|
|
"""Minimum 30-day average daily dollar volume ($25M). Ensures sufficient liquidity."""
|
|
|
|
|
|
min_atr_14: float = 0.50
|
|
|
"""Minimum ATR(14) in dollars ($0.50). Ensures sufficient intraday range to trade."""
|
|
|
|
|
|
min_atr_pct: float | None = None
|
|
|
"""Minimum ATR(14) as a fraction of prev_close (e.g. 0.04 = 4%). Filters out
|
|
|
low-volatility names where the ORB setup lacks explosive follow-through potential.
|
|
|
None disables (legacy behavior)."""
|
|
|
|
|
|
max_atr_pct: float | None = None
|
|
|
"""Maximum ATR(14) as a fraction of prev_close (e.g. 0.10 = 10%). Caps extreme-volatility
|
|
|
names that have large individual losses (MSTR, crypto stocks, micro-caps). None disables."""
|
|
|
|
|
|
# RVOL-based candidate selection
|
|
|
min_rvol: float | None = 1.0
|
|
|
"""Minimum approximate RVOL at open. RVOL = first_5min_vol / (avg_daily_vol / 78).
|
|
|
Note: this is an approximation — actual morning volume is 2–3× uniform rate,
|
|
|
so calibrate relative to that systematic bias. None disables the filter."""
|
|
|
|
|
|
max_candidates: int = 20
|
|
|
"""Maximum candidates to pass to intraday fetch and simulate per day."""
|
|
|
|
|
|
max_candidates_per_sector: int | None = None
|
|
|
"""Optional diversification cap after ranking.
|
|
|
Example: 2 = at most two names from the same sector in the day's final ORB list.
|
|
|
None disables the cap."""
|
|
|
|
|
|
min_candidates_to_trade: int = 3
|
|
|
"""Skip the day entirely if fewer than this many candidates pass all filters."""
|
|
|
|
|
|
# Composite ranking weights
|
|
|
weight_rvol: float = 0.60
|
|
|
"""RVOL weight in composite ranking score (50% from paper + 10% from spread, which is unavailable)."""
|
|
|
|
|
|
weight_gap: float = 0.25
|
|
|
"""Gap% weight (proxy for premarket activity, which is unavailable)."""
|
|
|
|
|
|
weight_dollar_vol: float = 0.15
|
|
|
"""First-5-min dollar volume weight."""
|
|
|
|
|
|
weight_premarket_dollar_vol: float = 0.0
|
|
|
"""Premarket dollar-volume weight. Serves as a same-day catalyst / attention proxy
|
|
|
when dedicated news data is unavailable."""
|
|
|
|
|
|
weight_body_ratio: float = 0.0
|
|
|
"""ORB candle directional conviction: (close-open)/(high-low) for longs, reversed for shorts.
|
|
|
High value = first candle decisively moved in the breakout direction."""
|
|
|
|
|
|
weight_close_location: float = 0.0
|
|
|
"""First ORB candle close location within its range: (close-low)/(high-low).
|
|
|
leader_followthrough typically rewards closes that finish near the candle high,
|
|
|
even if the opening bar is slightly red (red-to-green reclaim behavior)."""
|
|
|
|
|
|
weight_momentum: float = 0.0
|
|
|
"""5-day prior price momentum weight. Positive = stock already trending in breakout direction."""
|
|
|
|
|
|
weight_entropy: float = 0.0
|
|
|
"""Entropy(20d) ranking weight. compression_breakout typically rewards lower entropy."""
|
|
|
|
|
|
weight_atr_ratio: float = 0.0
|
|
|
"""Recent ATR(10) / ATR(60) ranking weight."""
|
|
|
|
|
|
weight_obv_slope: float = 0.0
|
|
|
"""OBV accumulation slope (20d) ranking weight. Positive OBV = smart-money accumulation pre-breakout."""
|
|
|
|
|
|
weight_obv_slope_5: float = 0.0
|
|
|
"""OBV accumulation slope (5d) ranking weight. Short-term accumulation signal, orthogonal to 20d slope."""
|
|
|
|
|
|
weight_gap_zscore: float = 0.0
|
|
|
"""Opening-gap z-score ranking weight relative to prior 20 sessions."""
|
|
|
|
|
|
weight_event_catalyst: float = 0.0
|
|
|
"""Same-day catalyst weight from actual filing events.
|
|
|
Used by stocks_in_play_dual_regime to reward names with a concrete event
|
|
|
instead of relying only on attention proxies."""
|
|
|
|
|
|
prior_event_lookback_days: int = 0
|
|
|
"""Calendar days to look back for prior earnings/guidance events in DB.
|
|
|
0=off (default, V24 parity). 7=V46. When >0 AND weight_event_catalyst>0,
|
|
|
uses DB events table path instead of Oracle REST API for event_flag/event_score.
|
|
|
Marks each trading day within this window after an earnings_release or
|
|
|
guidance_update event as event_flag=True, event_score=1.0."""
|
|
|
|
|
|
weight_attention_wiki: float = 0.0
|
|
|
"""Wikipedia attention weight for actual stocks-in-play ranking."""
|
|
|
|
|
|
weight_attention_news: float = 0.0
|
|
|
"""News/article attention weight for actual stocks-in-play ranking."""
|
|
|
|
|
|
min_body_ratio: float = 0.0
|
|
|
"""Minimum ORB candle body/range conviction. 0 disables the filter."""
|
|
|
|
|
|
min_close_location: float = 0.0
|
|
|
"""Minimum ORB candle close-location filter for leader_followthrough.
|
|
|
Example: 0.50 means the candle must close in the upper half of its range."""
|
|
|
|
|
|
max_close_location_short: float = 1.0
|
|
|
"""Maximum ORB candle close-location filter for short setups.
|
|
|
Example: 0.40 means a failed-ORB short must close in the lower 40% of the
|
|
|
opening range. 1.0 disables the filter."""
|
|
|
|
|
|
allow_doji_breakout: bool = False
|
|
|
"""When True, doji first bars are still allowed to trade via ORB high/low breakout.
|
|
|
Useful for gainers-style leader chasing or leader followthrough setups where the
|
|
|
opening 5-min candle can pause before a strong trend day. Default False to preserve
|
|
|
classic ORB behavior."""
|
|
|
|
|
|
allow_red_to_green_breakout: bool = False
|
|
|
"""When True, gainers_leader / leader_followthrough may trade long ORB-high breakouts
|
|
|
even if the first ORB candle closes red. This is meant for leader-followthrough days
|
|
|
where a strong name briefly dips after the open before reclaiming the ORB high."""
|
|
|
|
|
|
require_event_flag: bool = False
|
|
|
"""Require a same-day filing-based catalyst flag for candidate inclusion.
|
|
|
Designed for stocks_in_play_dual_regime, where attention alone is not enough."""
|
|
|
|
|
|
allowed_event_types: list[str] = Field(default_factory=list)
|
|
|
"""Optional whitelist of filing event types that count as catalysts.
|
|
|
Empty list means any same-day filing event is accepted."""
|
|
|
|
|
|
allow_failed_orb_short: bool = False
|
|
|
"""Allow failed gap-up ORB shorts in dual-regime mode."""
|
|
|
|
|
|
require_vwap_confirmation: bool = False
|
|
|
"""Require the ORB candle close to confirm against VWAP:
|
|
|
longs must close above VWAP, failed-ORB shorts must close below VWAP."""
|
|
|
|
|
|
attention_min_wiki_spike_10d: float | None = None
|
|
|
"""Minimum wiki spike to accept a stocks-in-play candidate."""
|
|
|
|
|
|
attention_min_wiki_zscore_20d: float | None = None
|
|
|
"""Minimum wiki z-score to accept a stocks-in-play candidate."""
|
|
|
|
|
|
attention_min_article_count_3d: int | None = None
|
|
|
"""Minimum article count to accept a stocks-in-play candidate."""
|
|
|
|
|
|
attention_min_us_article_count_3d: int | None = None
|
|
|
"""Minimum US article count to accept a stocks-in-play candidate."""
|
|
|
|
|
|
attention_min_resolver_confidence: float | None = None
|
|
|
"""Minimum entity-resolution confidence for attention data usage."""
|
|
|
|
|
|
min_sector_relative_strength: float | None = None
|
|
|
"""Minimum ORB return minus sector ORB return for continuation longs.
|
|
|
Positive values force the name to outperform its own sector in the opening range."""
|
|
|
|
|
|
min_entropy: float | None = None
|
|
|
"""Minimum allowed entropy_20d. None disables the lower bound."""
|
|
|
|
|
|
max_entropy: float | None = None
|
|
|
"""Maximum allowed entropy_20d. None disables the upper bound."""
|
|
|
|
|
|
compression_ratio_max: float | None = None
|
|
|
"""Maximum allowed recent range compression ratio (10d / 60d). Lower = tighter setup.
|
|
|
None disables the filter."""
|
|
|
|
|
|
max_gap_zscore_20d: float | None = None
|
|
|
"""Maximum allowed gap z-score (relative to prior 20 sessions). Rejects anomalous gap-up days
|
|
|
where short-sellers are already leaning against the name. Low gap_zscore = routine gap = better ORB.
|
|
|
None disables the filter. Gainers_leader only."""
|
|
|
|
|
|
min_obv_slope_20d: float | None = None
|
|
|
"""Minimum allowed OBV slope (20d). 0.0 = require positive accumulation (net up-volume days).
|
|
|
Negative OBV slope = institutional distribution — filters those out when set.
|
|
|
None disables the filter. Gainers_leader only."""
|
|
|
|
|
|
# ATR-based stop management
|
|
|
atr_stop_multiplier: float = 0.10
|
|
|
"""Initial stop distance = ATR(14) × this multiplier. Paper uses 10% (0.10)."""
|
|
|
|
|
|
breakeven_at_r: float = 1.0
|
|
|
"""Move stop to breakeven (entry price) when trade reaches this R-multiple."""
|
|
|
|
|
|
trailing_at_r: float = 2.0
|
|
|
"""Activate trailing stop (using recent bar lows) when trade reaches this R-multiple."""
|
|
|
|
|
|
# Risk-based position sizing
|
|
|
risk_per_trade_pct: float = 0.0025
|
|
|
"""Risk dollars per trade = equity × this. 0.0025 = 0.25% per trade."""
|
|
|
|
|
|
max_position_pct: float = 0.20
|
|
|
"""Maximum single position as fraction of equity. 0.20 = 20%."""
|
|
|
|
|
|
daily_max_loss_pct: float = 0.0125
|
|
|
"""Stop trading for the day if cumulative loss exceeds this. 0.0125 = 1.25%."""
|
|
|
|
|
|
max_stops_per_day: int = 3
|
|
|
"""Stop trading for the day after this many full-R stop losses."""
|
|
|
|
|
|
# Exit
|
|
|
exit_minutes_before_close: int = 5
|
|
|
"""Minutes before 4:00 PM ET to force-close. Default 5 = 15:55 ET."""
|
|
|
|
|
|
# Execution
|
|
|
slippage_bps: float = 5.0
|
|
|
"""One-way slippage in basis points (applied to both entry and exit fills)."""
|
|
|
|
|
|
initial_capital: float = 10_000.0
|
|
|
"""Starting capital in USD."""
|
|
|
|
|
|
ticker_cooldown_days: int = 0
|
|
|
"""Blackout period after trading a ticker (same as momentum strategy). 0 = disabled."""
|
|
|
|
|
|
settlement_days: int = 0
|
|
|
"""Cash account settlement delay (trading days).
|
|
|
0 = disabled (all equity always available — original behavior, allows over-deployment).
|
|
|
1 = T+1 (sale proceeds settle next trading day; also enforces within-day settled-cash cap).
|
|
|
2 = T+2 (legacy US rule pre-May 2024).
|
|
|
GFV context: unsettled proceeds can buy but not same-day sell (ORB always exits same day,
|
|
|
so only settled cash is usable)."""
|
|
|
|
|
|
max_gap_pct: float | None = None
|
|
|
"""Maximum opening gap (open vs prev_close) allowed for ORB candidates.
|
|
|
Stocks that gap >10% at open are over-extended and prone to reversal — they have a low
|
|
|
ORB breakout continuation rate. None = no cap (allow any gap). E.g. 0.10 = 10% cap."""
|
|
|
|
|
|
min_abs_gap_pct: float | None = None
|
|
|
"""Minimum absolute opening gap required to treat the session as catalyst-like.
|
|
|
Useful proxy when explicit same-day news labels are unavailable. None = disabled."""
|
|
|
|
|
|
min_premarket_dollar_vol: float | None = None
|
|
|
"""Minimum premarket dollar volume (04:00-09:30 ET) required for candidate inclusion.
|
|
|
Acts as a same-day attention/liquidity filter. None = disabled."""
|
|
|
|
|
|
small_gap_attention_override_premarket_dollar_vol: float | None = None
|
|
|
"""For gainers_leader / leader_followthrough: allow candidates below min_abs_gap_pct when premarket
|
|
|
dollar volume is exceptionally high. This is meant for mega-cap / leader followthrough
|
|
|
days that do not gap much but clearly dominate premarket attention."""
|
|
|
|
|
|
small_gap_attention_override_rvol: float | None = None
|
|
|
"""Optional RVOL floor paired with small_gap_attention_override_premarket_dollar_vol.
|
|
|
When set, small-gap candidates must also show at least this opening-range RVOL to
|
|
|
bypass min_abs_gap_pct."""
|
|
|
|
|
|
max_small_gap_attention_candidates: int | None = None
|
|
|
"""Optional daily cap on candidates admitted via the small-gap attention override.
|
|
|
Useful to keep exceptional followthrough names from diluting the core gap-driven
|
|
|
gainers basket. None disables the cap."""
|
|
|
|
|
|
# Market regime
|
|
|
market_regime_spy_threshold: float | None = None
|
|
|
"""Skip trading if regime ticker's opening gap is below this threshold. None = disabled.
|
|
|
E.g. -0.005 = skip if regime ticker gaps down >0.5% at open."""
|
|
|
|
|
|
market_regime_ticker: str = "SPY"
|
|
|
"""Ticker used for the gap-based market regime check. Default 'SPY'.
|
|
|
IWM often works better for ORB (mid/small-cap universe matches ORB candidates).
|
|
|
Only used when market_regime_spy_threshold is not None."""
|
|
|
|
|
|
min_candidate_breadth: float | None = None
|
|
|
"""Skip day if fewer than this fraction of intraday tickers opened above prev close.
|
|
|
E.g. 0.30 = skip if <30% of day's candidates gapped up.
|
|
|
Sweep result: 0.30 gives Sharpe 19.86 (vs 18.31 no filter), 0.50 gives 20.27.
|
|
|
This is more robust than single-ETF regime checks because it measures the actual
|
|
|
candidate pool's sentiment. None = disabled."""
|
|
|
|
|
|
compound_returns: bool = True
|
|
|
"""When True (default), position sizing scales with current equity (compounding).
|
|
|
When False, position sizing always uses initial_capital (simple/단리 mode).
|
|
|
Simple mode prevents late-period bias where larger equity dominates the return metric.
|
|
|
Ignored when daily_budget_reset is True."""
|
|
|
|
|
|
daily_budget_reset: bool = False
|
|
|
"""Research-only mode: every day resets sizing_capital to initial_capital,
|
|
|
ignoring prior-day PnL entirely (no compounding, no drawdown cap).
|
|
|
Useful for isolating strategy alpha from capital-path effects.
|
|
|
When True, takes precedence over compound_returns."""
|
|
|
|
|
|
trailing_stop_atr_multiplier: float = 0.0
|
|
|
"""ATR-based trailing stop distance from peak price. 0 = disabled (use swing-low mode).
|
|
|
When > 0: trailing_stop = peak_price - atr * this_value. Bar-size independent.
|
|
|
E.g. 1.5 = trail 1.5×ATR(14) below the running peak. Activates at trailing_at_r.
|
|
|
Swing-low mode (0.0) ties trailing sensitivity to sim_bar_minutes — ATR mode removes that dependency."""
|
|
|
|
|
|
trailing_tighten_at_r: float | None = None
|
|
|
"""Two-stage trailing: when R reaches this level, switch to trailing_stop_atr_multiplier_tight.
|
|
|
None = single-stage trailing (no tightening). Requires trailing_stop_atr_multiplier > 0.
|
|
|
E.g. trailing_at_r=1.5 (wide trail) + trailing_tighten_at_r=3.0 (tight trail)."""
|
|
|
|
|
|
trailing_stop_atr_multiplier_tight: float = 0.0
|
|
|
"""ATR multiplier for the tighter second-stage trailing stop (used when trailing_tighten_at_r is hit).
|
|
|
0.0 = fall back to trailing_stop_atr_multiplier (effectively disables tightening)."""
|
|
|
|
|
|
max_simultaneous_entries: int | None = None
|
|
|
"""Maximum trades that can enter on the same bar timestamp. None = unlimited.
|
|
|
Prevents the 9:35 burst where all candidates break out simultaneously, overwhelming
|
|
|
the kill switch and creating uncontrolled correlated risk. Top-ranked candidates are taken first."""
|
|
|
|
|
|
partial_exit_at_r: float | None = None
|
|
|
"""Lock in partial profits when R-multiple reaches this level. None = disabled.
|
|
|
E.g. 1.0 = sell partial_exit_pct of the position at 1R, let remainder run."""
|
|
|
|
|
|
partial_exit_pct: float = 0.50
|
|
|
"""Fraction of position to exit at partial_exit_at_r. 0.50 = sell half the position."""
|
|
|
|
|
|
market_regime_spy_trend_days: int | None = None
|
|
|
"""Multi-day SPY trend filter: look back this many trading days for cumulative return.
|
|
|
None = disabled. Requires market_regime_spy_trend_threshold to also be set.
|
|
|
E.g. 5 = look at SPY's 5-day cumulative return ending yesterday."""
|
|
|
|
|
|
market_regime_spy_trend_threshold: float | None = None
|
|
|
"""Skip day if SPY's cumulative return over market_regime_spy_trend_days is below this.
|
|
|
E.g. -0.03 = skip if SPY down >3% over the past N days.
|
|
|
Protects against sustained bear-market weeks (single-day gap filter misses these)."""
|
|
|
|
|
|
rolling_loss_days: int | None = None
|
|
|
"""Self-referential rolling loss filter: look back this many trading days for strategy P&L.
|
|
|
None = disabled. Requires rolling_loss_threshold to also be set.
|
|
|
E.g. 5 = compute strategy's cumulative return over past 5 trading days."""
|
|
|
|
|
|
rolling_loss_threshold: float | None = None
|
|
|
"""Skip day if strategy's own rolling return (past rolling_loss_days) is below this.
|
|
|
E.g. -0.05 = pause trading if strategy lost >5% of initial capital in past 5 days.
|
|
|
Protects against cascading losses in regimes where the strategy stops working."""
|
|
|
|
|
|
# VIX regime filter and position size scaling
|
|
|
max_vix: float | None = None
|
|
|
"""Skip the whole day if VIX closes above this threshold. None = disabled.
|
|
|
E.g. 30.0 = skip days when VIX > 30 (high fear regime)."""
|
|
|
|
|
|
vix_size_scale_low: float | None = None
|
|
|
"""VIX level where position size scaling starts (scaler = 1.0 below this). None = disabled."""
|
|
|
|
|
|
vix_size_scale_high: float | None = None
|
|
|
"""VIX level where the position-size scaler reaches vix_size_scale_min."""
|
|
|
|
|
|
vix_size_scale_min: float = 1.0
|
|
|
"""Minimum position-size scaler once VIX reaches vix_size_scale_high.
|
|
|
E.g. 0.50 = halve position sizes when VIX is at or above vix_size_scale_high."""
|
|
|
|
|
|
# V20: soft regime/breadth scalers (all default to V19 binary-skip behavior)
|
|
|
regime_size_scale_low: float | None = None
|
|
|
"""QQQ gap at which regime scaler bottoms out. None = V19 binary skip."""
|
|
|
regime_size_scale_high: float | None = None
|
|
|
"""QQQ gap at which regime scaler = 1.0. Binary skip fires when gap < market_regime_spy_threshold."""
|
|
|
regime_size_scale_min: float = 1.0
|
|
|
"""Minimum regime scaler. 1.0 = V19 behavior."""
|
|
|
regime_skip_below: float | None = None
|
|
|
"""Hard skip floor below regime_size_scale_low. None = no extra skip."""
|
|
|
|
|
|
breadth_size_scale_low: float | None = None
|
|
|
"""Breadth ratio at which breadth scaler bottoms out. None = V19 binary skip."""
|
|
|
breadth_size_scale_high: float | None = None
|
|
|
"""Breadth ratio at which breadth scaler = 1.0."""
|
|
|
breadth_size_scale_min: float = 1.0
|
|
|
"""Minimum breadth scaler. 1.0 = V19 behavior."""
|
|
|
breadth_skip_below: float | None = None
|
|
|
"""Hard skip floor on breadth. None = no extra skip."""
|
|
|
|
|
|
# V20: regime-adaptive stops
|
|
|
soft_day_scaler_threshold: float = 1.0
|
|
|
"""combined_scaler (regime*breadth) below this triggers soft-day stop adjustments."""
|
|
|
atr_stop_multiplier_weak: float | None = None
|
|
|
"""Replaces atr_stop_multiplier on soft days. None = no change."""
|
|
|
breakeven_at_r_weak: float | None = None
|
|
|
"""Replaces breakeven_at_r on soft days. None = no change."""
|
|
|
|
|
|
# V20: soft-day selection bar
|
|
|
soft_day_max_trades: int | None = None
|
|
|
"""Max trades per soft day (top-N by rank). None = no cap."""
|
|
|
soft_day_min_score_pct: float | None = None
|
|
|
"""Min composite score rank_pct on soft days. None = no filter."""
|
|
|
|
|
|
# Breakout volume confirmation
|
|
|
min_breakout_rel_vol: float | None = None
|
|
|
"""Minimum relative volume on the breakout bar vs average post-ORB bar volume.
|
|
|
Filters low-conviction breakouts where price touches the level on thin volume.
|
|
|
None = disabled (any volume accepted). E.g. 1.5 = breakout bar must have 1.5× avg bar volume."""
|
|
|
|
|
|
# Time-decay trailing stop tightening
|
|
|
time_decay_start_minutes: int | None = None
|
|
|
"""Minutes after market open (9:30 ET) to start tightening the trailing stop.
|
|
|
None = disabled. E.g. 180 = start tightening at 12:30 PM ET."""
|
|
|
|
|
|
time_decay_factor: float = 0.5
|
|
|
"""By close, the trailing ATR multiplier shrinks to this fraction of its base value.
|
|
|
E.g. 0.5 = trail width halves linearly from time_decay_start_minutes to close."""
|
|
|
|
|
|
# Running VWAP trailing exit
|
|
|
vwap_exit_mode: str = "none"
|
|
|
"""VWAP-based exit mode:
|
|
|
- 'none': disabled (default)
|
|
|
- 'exit': exit when bar close crosses below running VWAP (longs) or above (shorts)
|
|
|
- 'floor': use VWAP - buffer as trailing stop floor (can't trail above VWAP for longs)
|
|
|
Running VWAP is computed from cumulative (typical_price × volume) / cumulative(volume)
|
|
|
starting from market open."""
|
|
|
|
|
|
vwap_exit_buffer_atr: float = 0.0
|
|
|
"""Buffer below VWAP (in ATR units) for 'floor' mode.
|
|
|
E.g. 0.3 = trailing stop can't go below VWAP - 0.3×ATR.
|
|
|
For 'exit' mode: exit only when close < VWAP - buffer×ATR (allows noise)."""
|
|
|
|
|
|
vwap_exit_after_r: float = 0.0
|
|
|
"""Only activate VWAP exit after reaching this R-multiple.
|
|
|
0.0 = active from entry. 1.0 = only after trade reaches 1R.
|
|
|
Prevents premature VWAP exits on initial pullbacks after breakout."""
|
|
|
|
|
|
# Score-based position sizing
|
|
|
score_sizing_multiplier: float | None = None
|
|
|
"""Scale risk_per_trade_pct by candidate rank. Top candidate gets this multiplier,
|
|
|
bottom gets 1.0x (linear interpolation). None = disabled (equal sizing).
|
|
|
E.g. 2.0 = top pick risks 2× base, bottom pick risks 1×. Requires score rank
|
|
|
to be passed from simulate_orb_day."""
|
|
|
|
|
|
# Confirmation bar requirement
|
|
|
require_confirmation_bar: bool = False
|
|
|
"""After breakout, require the NEXT bar to close above entry price (long) or
|
|
|
below (short) to confirm. If the confirmation bar fails, skip the trade.
|
|
|
Filters false breakouts where price barely touches the level and reverses."""
|
|
|
|
|
|
# Gap fill protection
|
|
|
exit_on_gap_fill: bool = False
|
|
|
"""Exit immediately if price drops below prev_close (long) or rises above (short).
|
|
|
A gap fill means the original catalyst is being rejected by the market.
|
|
|
Uses bar close for the check (not intra-bar low)."""
|
|
|
|
|
|
# Max hold time
|
|
|
max_hold_minutes: int | None = None
|
|
|
"""Maximum minutes to hold a position. None = hold until exit_minutes_before_close.
|
|
|
E.g. 120 = exit 2 hours after entry regardless of profit/loss.
|
|
|
Useful for capturing morning momentum without afternoon reversal risk."""
|
|
|
|
|
|
# Bar close confirmation entry
|
|
|
entry_on_bar_close: bool = False
|
|
|
"""Require breakout bar's CLOSE to be above breakout level (long) or below (short),
|
|
|
not just the bar's HIGH/LOW. Enter at the bar's close price.
|
|
|
Filters wick-only breakouts where price barely touches the ORB level and reverses.
|
|
|
The trader waits for the 5-min bar to complete, then enters at the close price.
|
|
|
Same-bar stop is skipped (trader was not in position during the bar)."""
|
|
|
|
|
|
# ── Pyramiding (add to winners) ──
|
|
|
pyramid_at_r: float | None = None
|
|
|
"""Add to winning position when R-multiple reaches this level. None = disabled.
|
|
|
E.g. 1.0 = add pyramid_add_pct of original position when trade reaches 1R.
|
|
|
Stop is moved to at least breakeven on the blended cost after adding."""
|
|
|
|
|
|
pyramid_add_pct: float = 0.50
|
|
|
"""Fraction of original position size to add at each pyramid level.
|
|
|
0.50 = add 50% of original shares (100 shares → add 50 → 150 total)."""
|
|
|
|
|
|
pyramid_max_adds: int = 1
|
|
|
"""Maximum number of pyramid additions per trade. 1 = single add-on.
|
|
|
Each subsequent add triggers at pyramid_at_r + n * pyramid_at_r (staggered)."""
|
|
|
|
|
|
# ── Re-entry after stop-out ──
|
|
|
reentry_after_stop: bool = False
|
|
|
"""Allow re-entry on a ticker that was stopped out earlier in the same day.
|
|
|
The ticker must re-break the ORB level with volume >= reentry_min_volume_ratio
|
|
|
times the original breakout volume. Simulates the 'shakeout then real move' pattern."""
|
|
|
|
|
|
reentry_min_volume_ratio: float = 1.5
|
|
|
"""Minimum volume ratio (vs original breakout bar) required for re-entry.
|
|
|
1.5 = re-breakout bar must have 50% more volume than original breakout bar."""
|
|
|
|
|
|
reentry_max_per_ticker: int = 1
|
|
|
"""Maximum re-entries allowed per ticker per day."""
|
|
|
|
|
|
# ── Portfolio deployment cap ──
|
|
|
max_total_deployment_pct: float | None = None
|
|
|
"""Maximum total capital deployed across all concurrent positions as fraction of equity.
|
|
|
None = no limit (original behavior). E.g. 0.80 = never deploy more than 80% of equity.
|
|
|
Prevents over-concentration when settlement_days=0 allows unlimited deployment."""
|
|
|
|
|
|
# ── Drawdown governor ──
|
|
|
drawdown_governor_threshold: float | None = None
|
|
|
"""Enable drawdown governor when equity drops this fraction below peak.
|
|
|
None = disabled. E.g. 0.05 = start reducing sizing when equity is 5% below peak.
|
|
|
Linearly scales sizing from 1.0 at threshold to drawdown_governor_min_scale at 2× threshold."""
|
|
|
|
|
|
drawdown_governor_min_scale: float = 0.30
|
|
|
"""Minimum sizing scale at maximum drawdown governor activation.
|
|
|
0.30 = reduce position sizes to 30% of normal at 2× drawdown_governor_threshold."""
|
|
|
|
|
|
# ── Streak-based sizing ──
|
|
|
streak_sizing_win_bonus: float | None = None
|
|
|
"""Bonus sizing multiplier per consecutive win in recent trade history.
|
|
|
None = disabled. E.g. 0.15 = add 15% sizing per consecutive win.
|
|
|
3 consecutive wins → 1.0 + 3*0.15 = 1.45x sizing.
|
|
|
Computed at start of each day from previous days' trade outcomes."""
|
|
|
|
|
|
streak_sizing_loss_penalty: float | None = None
|
|
|
"""Reduce sizing per consecutive loss. None = no penalty (only reward wins).
|
|
|
E.g. 0.10 = subtract 10% per consecutive loss.
|
|
|
2 consecutive losses → 1.0 - 2*0.10 = 0.80x sizing."""
|
|
|
|
|
|
streak_sizing_max: float = 2.0
|
|
|
"""Cap on streak-based sizing multiplier. Prevents excessive leverage on long streaks."""
|
|
|
|
|
|
streak_sizing_min: float = 0.50
|
|
|
"""Floor on streak-based sizing multiplier. Prevents sizing from going too low."""
|
|
|
|
|
|
# ── Rolling performance sizing ──
|
|
|
rolling_wr_sizing_window: int | None = None
|
|
|
"""Window of recent trades for rolling win-rate sizing bonus.
|
|
|
None = disabled. E.g. 15 = compute WR over last 15 trades.
|
|
|
Applied AFTER streak sizing (multiplicative)."""
|
|
|
|
|
|
rolling_wr_sizing_threshold: float = 0.55
|
|
|
"""WR above this threshold triggers the bonus multiplier.
|
|
|
E.g. 0.55 = if rolling WR > 55%, apply rolling_wr_sizing_bonus."""
|
|
|
|
|
|
rolling_wr_sizing_bonus: float = 0.30
|
|
|
"""Bonus multiplier when rolling WR exceeds threshold.
|
|
|
E.g. 0.30 = size 1.30x when rolling WR is above threshold."""
|
|
|
|
|
|
rolling_wr_sizing_penalty_threshold: float | None = None
|
|
|
"""WR below this triggers a sizing reduction. None = no penalty.
|
|
|
E.g. 0.40 = if rolling WR < 40%, reduce sizing by rolling_wr_sizing_penalty."""
|
|
|
|
|
|
rolling_wr_sizing_penalty: float = 0.20
|
|
|
"""Penalty reduction when rolling WR is below penalty threshold.
|
|
|
E.g. 0.20 = size 0.80x when rolling WR is below penalty threshold."""
|
|
|
|
|
|
# ── Gap-adaptive trailing ──
|
|
|
gap_trail_wide_threshold: float | None = None
|
|
|
"""Gap% above which trailing uses wider ATR multiplier. None = disabled.
|
|
|
E.g. 0.05 = gaps > 5% get wider trailing (strong catalyst = longer trend).
|
|
|
Uses gap_trail_wide_atr_multiplier instead of trailing_stop_atr_multiplier."""
|
|
|
|
|
|
gap_trail_wide_atr_multiplier: float = 1.2
|
|
|
"""Trailing ATR multiplier for large-gap stocks (gap > gap_trail_wide_threshold).
|
|
|
Wider trail lets strong catalyst stocks run further before stopping out."""
|
|
|
|
|
|
gap_trail_tight_atr_multiplier: float | None = None
|
|
|
"""Optional tighter trailing for small-gap stocks (gap <= gap_trail_wide_threshold).
|
|
|
None = use default trailing_stop_atr_multiplier. E.g. 0.5 = tight trail for small gaps."""
|
|
|
|
|
|
# ── Pullback continuation entry ──
|
|
|
pullback_entry: bool = False
|
|
|
"""Enable pullback continuation entry mode. Instead of entering immediately on
|
|
|
ORB breakout, wait for a pullback after breakout and enter on continuation.
|
|
|
Filters false breakouts and gives better entry prices with tighter stops."""
|
|
|
|
|
|
pullback_max_bars: int = 6
|
|
|
"""Maximum bars to wait for pullback-continuation pattern after initial breakout.
|
|
|
If no valid pullback+continuation within this window, skip the trade."""
|
|
|
|
|
|
pullback_min_retracement_pct: float = 0.30
|
|
|
"""Minimum retracement of the breakout move to qualify as a pullback.
|
|
|
0.30 = price must pull back at least 30% of (post-breakout peak - breakout level)."""
|
|
|
|
|
|
pullback_stop_at_low: bool = True
|
|
|
"""Set stop at the pullback low instead of ATR-based stop.
|
|
|
Gives naturally tighter stops based on actual price structure."""
|
|
|
|
|
|
# ── orb_pullback_v1 extended pullback controls ──
|
|
|
pullback_impulse_window_end_min: int | None = None
|
|
|
"""Minutes from market open (9:30 ET) by which the post-breakout impulse peak
|
|
|
must form. E.g. 25 = peak must occur by 9:55 ET. None disables (legacy behavior)."""
|
|
|
|
|
|
pullback_impulse_min_move_atr: float | None = None
|
|
|
"""Minimum impulse size from breakout level to peak as a multiple of ATR(14).
|
|
|
E.g. 0.5 = peak must be at least 0.5 × ATR above breakout level. None disables."""
|
|
|
|
|
|
pullback_depth_max_pct: float | None = None
|
|
|
"""Maximum pullback depth as a fraction of the impulse move. Works in conjunction
|
|
|
with pullback_min_retracement_pct. E.g. 0.50 = pullback must retrace at most 50%
|
|
|
of (peak - breakout). None disables (only min floor applied)."""
|
|
|
|
|
|
pullback_volume_contraction_ratio: float | None = None
|
|
|
"""Require pullback-phase average bar volume < impulse-phase average × this ratio.
|
|
|
E.g. 0.7 = pullback must occur on 70% or less of impulse volume. None disables."""
|
|
|
|
|
|
pullback_vwap_floor: bool = False
|
|
|
"""If True, abort the pullback setup if any bar during pullback breaches below
|
|
|
the running session VWAP by more than pullback_vwap_floor_tolerance_pct."""
|
|
|
|
|
|
pullback_vwap_floor_tolerance_pct: float = 0.003
|
|
|
"""Tolerance below running VWAP before pullback_vwap_floor fires. 0.003 = -0.3%."""
|
|
|
|
|
|
pullback_stop_mode: str = "atr"
|
|
|
"""Stop distance mode after pullback continuation entry.
|
|
|
'atr' = use ATR × atr_stop_multiplier (or pullback_low if pullback_stop_at_low=True),
|
|
|
'pullback_low' = always use pullback extreme as stop,
|
|
|
'vwap_lower' = use running VWAP − pullback_stop_vwap_buffer_pct as stop."""
|
|
|
|
|
|
pullback_stop_vwap_buffer_pct: float = 0.002
|
|
|
"""Buffer below running VWAP for 'vwap_lower' stop mode. 0.002 = −0.2%."""
|
|
|
|
|
|
pullback_reclaim_confirm_rel_vol: float | None = None
|
|
|
"""Minimum relative volume on the reclaim/continuation bar. Computed as
|
|
|
bar_volume / (avg_post_orb_bar_vol). E.g. 1.2 = bar must have 1.2× average
|
|
|
post-ORB volume. None disables."""
|
|
|
|
|
|
# ── vwap_reclaim_v1 engine ──
|
|
|
vwap_reclaim_window_start_min: int = 30
|
|
|
"""Minutes from market open (9:30 ET) when to start scanning for VWAP reclaim entries.
|
|
|
30 = 10:00 ET. Only used by engine_family: vwap_reclaim_v1."""
|
|
|
|
|
|
vwap_reclaim_window_end_min: int = 120
|
|
|
"""Minutes from market open when to stop accepting new VWAP reclaim entries.
|
|
|
120 = 11:30 ET. Only used by engine_family: vwap_reclaim_v1."""
|
|
|
|
|
|
vwap_reclaim_require_prior_dip: bool = False
|
|
|
"""If True, require that at least one bar before the reclaim window had close < running VWAP
|
|
|
(for long). Selects only true VWAP reclaim setups (failed ORB then recovered), not stocks
|
|
|
that drifted above VWAP all morning. Creates orthogonality with V23 (V23 winners never dip)."""
|
|
|
|
|
|
vwap_reclaim_min_clearance_pct: float = 0.0
|
|
|
"""Minimum % that entry bar's close must be above the running VWAP (for long).
|
|
|
E.g. 0.003 = close must be at least 0.3% above VWAP. Filters marginal reclaims."""
|
|
|
|
|
|
vwap_reclaim_stop_mode: str = "atr"
|
|
|
"""Stop distance mode for vwap_reclaim_v1 entries.
|
|
|
'atr' = ATR × atr_stop_multiplier (default),
|
|
|
'vwap' = distance from entry_price to VWAP − buffer (structural floor stop)."""
|
|
|
|
|
|
vwap_reclaim_stop_vwap_buffer_pct: float = 0.002
|
|
|
"""Buffer below VWAP for vwap stop mode. 0.002 = stop at VWAP × (1 - 0.2%)."""
|
|
|
|
|
|
# ── Profit target ──
|
|
|
profit_target_r: float | None = None
|
|
|
"""Exit at market when R-multiple reaches this level. None = disabled.
|
|
|
E.g. 3.0 = exit when trade reaches 3R profit. Locks in gains before
|
|
|
trailing stop gives back profits."""
|
|
|
|
|
|
# ── Fixed dollar exits ──
|
|
|
fixed_profit_dollars: float | None = None
|
|
|
"""Exit when trade P&L reaches this profit in dollars. Overrides ATR-based profit target. None = disabled."""
|
|
|
|
|
|
fixed_loss_dollars: float | None = None
|
|
|
"""Exit when trade loss reaches this amount in dollars (positive = max loss allowed). Overrides ATR stop. None = disabled."""
|
|
|
|
|
|
# ── ORB range quality filter ──
|
|
|
orb_range_atr_min: float | None = None
|
|
|
"""Minimum ORB candle range as fraction of ATR(14). None = disabled.
|
|
|
Filters stocks with too-narrow opening ranges (likely noise).
|
|
|
E.g. 0.3 = ORB range must be at least 30% of ATR."""
|
|
|
|
|
|
orb_range_atr_max: float | None = None
|
|
|
"""Maximum ORB candle range as fraction of ATR(14). None = disabled.
|
|
|
Filters stocks whose opening range already consumed the day's move.
|
|
|
E.g. 1.5 = ORB range must be at most 150% of ATR."""
|
|
|
|
|
|
# ── SPY intraday guard ──
|
|
|
spy_intraday_guard_pct: float | None = None
|
|
|
"""Tighten trailing stop when SPY drops this % from its open intraday. None = disabled.
|
|
|
E.g. -0.005 = if SPY drops 0.5% from open, tighten trail.
|
|
|
Applied during Phase 2 exit management."""
|
|
|
|
|
|
spy_intraday_guard_tighten: float = 0.5
|
|
|
"""Factor to multiply trailing ATR multiplier when SPY guard triggers.
|
|
|
0.5 = trail becomes 50% tighter (e.g., 0.8 ATR → 0.4 ATR)."""
|
|
|
|
|
|
# ── Single-trade loss cap ──
|
|
|
single_trade_loss_cap_pct: float | None = None
|
|
|
"""Maximum loss allowed from a single trade as a fraction of initial_capital.
|
|
|
None = disabled (default; streak/governor boosts apply without loss ceiling).
|
|
|
When set, sizing_capital is clamped after all boosts (drawdown governor, streak,
|
|
|
rolling WR) so that risk_per_trade_pct × sizing_capital ≤ this cap × initial_capital.
|
|
|
E.g. 0.05 with risk_per_trade_pct=0.05 → max single-trade risk = $500 on $10k initial,
|
|
|
regardless of streak multiplier.
|
|
|
Fixes the structural misalignment where streak_sizing_max=2.5 allows a single -1R
|
|
|
trade to exceed daily_max_loss_pct when both are computed on different capital bases."""
|
|
|
|
|
|
# ── Dual-trigger momentum path (hybrid) ──
|
|
|
dual_trigger_enabled: bool = False
|
|
|
"""Enable momentum-confirmation as an alternate entry trigger alongside ORB breakout.
|
|
|
When True, for each candidate compute both the ORB breakout time and the 09:45
|
|
|
momentum confirmation time; use whichever fires first within the entry window.
|
|
|
Post-entry management (ATR stop, BE, trail, tighten) is identical regardless of
|
|
|
which trigger fired. min_breakout_rel_vol gate applies only on the ORB path."""
|
|
|
|
|
|
momo_entry_minutes_after_open: int = 10
|
|
|
"""Minutes after market open for the first momentum evaluation bar (09:40 close)."""
|
|
|
|
|
|
momo_confirmation_minutes_after_entry: int = 5
|
|
|
"""Minutes after momo_entry_minutes_after_open for the confirmation close (09:45)."""
|
|
|
|
|
|
momo_confirm_window_minutes: int = 55
|
|
|
"""Latest allowable momentum confirm, measured as minutes after ORB end (09:35).
|
|
|
09:35 + 55 min = 10:30 ET. Confirmation signals after this are ignored."""
|
|
|
|
|
|
momo_min_confirmation_return_pct: float = 0.005
|
|
|
"""Minimum return from 09:40 close to 09:45 close to count as momentum confirmed."""
|
|
|
|
|
|
momo_min_morning_gain_pct: float = 0.015
|
|
|
"""Minimum gain from open to 09:45 close for momentum confirm trigger."""
|
|
|
|
|
|
momo_max_morning_gain_pct: float = 0.06
|
|
|
"""Maximum gain from open to 09:45 close (rejects over-extended names)."""
|
|
|
|
|
|
# ── Candidate overlay (Leader + Liquid) for ORB path ──
|
|
|
candidate_seed_leader_overlay_slots: int = 0
|
|
|
"""Number of Leader overlay slots appended after ORB pre-screen.
|
|
|
Leader overlay targets trend leaders with high ret_5d + low entropy + high ATR,
|
|
|
even when their opening gap is muted (captured via momo confirm trigger).
|
|
|
0 = disabled."""
|
|
|
|
|
|
candidate_seed_leader_min_gap_pct: float | None = None
|
|
|
"""Minimum gap for Leader overlay candidates (can be negative for flat-open leaders)."""
|
|
|
|
|
|
candidate_seed_leader_max_gap_pct: float | None = None
|
|
|
"""Maximum gap for Leader overlay candidates."""
|
|
|
|
|
|
candidate_seed_leader_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum 30-day avg dollar volume for Leader overlay."""
|
|
|
|
|
|
candidate_seed_leader_min_ret_5d: float | None = None
|
|
|
"""Minimum 5-day prior return for Leader overlay (e.g. 0.15 = +15%)."""
|
|
|
|
|
|
candidate_seed_leader_min_atr_pct: float | None = None
|
|
|
"""Minimum ATR/open for Leader overlay (e.g. 0.06 = 6%)."""
|
|
|
|
|
|
candidate_seed_leader_max_entropy_20d: float | None = None
|
|
|
"""Maximum entropy_20d for Leader overlay (e.g. 0.75)."""
|
|
|
|
|
|
candidate_seed_liquid_overlay_slots: int = 0
|
|
|
"""Number of Liquid overlay slots appended after ORB pre-screen.
|
|
|
Targets highly liquid names with moderate gaps that ORB pre-screen may miss.
|
|
|
0 = disabled."""
|
|
|
|
|
|
candidate_seed_liquid_min_gap_pct: float | None = None
|
|
|
"""Minimum gap for Liquid overlay candidates."""
|
|
|
|
|
|
candidate_seed_liquid_max_gap_pct: float | None = None
|
|
|
"""Maximum gap for Liquid overlay candidates."""
|
|
|
|
|
|
candidate_seed_liquid_min_avg_dollar_vol_30d: float | None = None
|
|
|
"""Minimum 30-day avg dollar volume for Liquid overlay."""
|
|
|
|
|
|
candidate_seed_liquid_min_ret_5d: float | None = None
|
|
|
"""Minimum 5-day prior return for Liquid overlay."""
|
|
|
|
|
|
candidate_seed_liquid_max_entropy_20d: float | None = None
|
|
|
"""Maximum entropy_20d for Liquid overlay."""
|
|
|
|
|
|
# ── Entropy-based per-candidate size scaler (from momentum strategy) ──
|
|
|
entropy_size_scale_low: float | None = None
|
|
|
"""Entropy level below which the per-candidate size scaler is 1.0 (no reduction).
|
|
|
None = disabled (entropy scaler inactive for ORB path)."""
|
|
|
|
|
|
entropy_size_scale_high: float | None = None
|
|
|
"""Entropy level at or above which size scaler = entropy_size_scale_min."""
|
|
|
|
|
|
entropy_size_scale_min: float = 0.6
|
|
|
"""Minimum size scaler at entropy_size_scale_high (e.g. 0.6 = 60% of normal size)."""
|
|
|
|
|
|
|
|
|
class UniverseParams(BaseModel):
|
|
|
"""Parameters controlling which stocks to scan."""
|
|
|
|
|
|
source: str = "sp500"
|
|
|
"""Universe source: 'sp500', 'nasdaq100', 'broad', 'midlarge', 'largecap', 'midcap',
|
|
|
'smallmid', 'yaml', or 'screener'."""
|
|
|
|
|
|
symbols_file: str | None = None
|
|
|
"""Path to YAML symbols file (required if source='yaml')."""
|
|
|
|
|
|
market_cap_min: float | None = None
|
|
|
"""Minimum market cap filter (USD). Overrides screener default when set."""
|
|
|
|
|
|
avg_volume_min: int | None = None
|
|
|
"""Minimum 3-month average daily volume filter."""
|
|
|
|
|
|
sector_exclude: list[str] = Field(default_factory=list)
|
|
|
"""Sectors to exclude (e.g. ['Energy', 'Utilities']). Not applied for index sources."""
|
|
|
|
|
|
min_price: float = 5.0
|
|
|
"""Minimum stock price. Filters out very cheap stocks."""
|
|
|
|
|
|
|
|
|
class BacktestParams(BaseModel):
|
|
|
"""Backtest period and pre-screening parameters."""
|
|
|
|
|
|
start_date: str | None = None
|
|
|
"""Backtest start date (YYYY-MM-DD). None = auto (today - lookback_trading_days)."""
|
|
|
|
|
|
end_date: str | None = None
|
|
|
"""Backtest end date (YYYY-MM-DD). None = today."""
|
|
|
|
|
|
lookback_trading_days: int = 200
|
|
|
"""Number of trading days to backtest when start_date is None."""
|
|
|
|
|
|
pre_screen_threshold: float = 0.015
|
|
|
"""Phase 1 pre-screening threshold: (today_open - prev_close) / prev_close >= this.
|
|
|
Uses only open-time information plus prior-day data (no lookahead)."""
|
|
|
|
|
|
|
|
|
class CacheParams(BaseModel):
|
|
|
"""Intraday data disk cache configuration."""
|
|
|
|
|
|
enabled: bool = True
|
|
|
"""Whether to use the disk cache for intraday bars."""
|
|
|
|
|
|
dir: str = "data/cache/intraday"
|
|
|
"""Root directory for Parquet cache files."""
|
|
|
|
|
|
|
|
|
class OutputParams(BaseModel):
|
|
|
"""Output and reporting configuration."""
|
|
|
|
|
|
dir: str = "runs/intraday"
|
|
|
"""Directory for writing result JSON files."""
|
|
|
|
|
|
verbose: bool = False
|
|
|
"""Show detailed per-day output during simulation."""
|
|
|
|
|
|
|
|
|
class IntradayConfig(BaseModel):
|
|
|
"""Full configuration for one intraday backtest run.
|
|
|
|
|
|
Maps 1:1 to the YAML config file format.
|
|
|
"""
|
|
|
|
|
|
strategy_mode: str = "momentum"
|
|
|
"""Strategy to use: 'momentum' (morning gainers) or 'orb' (opening range breakout)."""
|
|
|
|
|
|
strategy: StrategyParams = Field(default_factory=StrategyParams)
|
|
|
"""Momentum strategy parameters (used when strategy_mode='momentum')."""
|
|
|
|
|
|
orb_strategy: ORBStrategyParams | None = None
|
|
|
"""ORB strategy parameters (used when strategy_mode='orb'). None = use defaults."""
|
|
|
|
|
|
universe: UniverseParams = Field(default_factory=UniverseParams)
|
|
|
backtest: BacktestParams = Field(default_factory=BacktestParams)
|
|
|
cache: CacheParams = Field(default_factory=CacheParams)
|
|
|
output: OutputParams = Field(default_factory=OutputParams)
|
|
|
|
|
|
|
|
|
# ── Trade Results ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
class IntradayTrade(BaseModel):
|
|
|
"""One completed intraday trade."""
|
|
|
|
|
|
date: str
|
|
|
"""Trading date (YYYY-MM-DD)."""
|
|
|
|
|
|
ticker: str
|
|
|
"""Stock symbol."""
|
|
|
|
|
|
entry_price: float
|
|
|
"""Fill price at entry (after slippage)."""
|
|
|
|
|
|
exit_price: float
|
|
|
"""Fill price at exit (after slippage)."""
|
|
|
|
|
|
entry_time: str
|
|
|
"""Entry bar timestamp (ISO 8601, ET)."""
|
|
|
|
|
|
exit_time: str
|
|
|
"""Exit bar timestamp (ISO 8601, ET)."""
|
|
|
|
|
|
shares: float
|
|
|
"""Number of shares held."""
|
|
|
|
|
|
pnl: float
|
|
|
"""Dollar P&L (after slippage costs)."""
|
|
|
|
|
|
pnl_pct: float
|
|
|
"""Percentage P&L: (exit_price - entry_price) / entry_price (before slippage adj)."""
|
|
|
|
|
|
exit_reason: str
|
|
|
"""How the trade was closed: 'close' or 'stop_loss'."""
|
|
|
|
|
|
morning_gain_pct: float = 0.0
|
|
|
"""Stock's gain from open to entry time (momentum signal). 0.0 for ORB trades."""
|
|
|
|
|
|
slippage_cost: float = 0.0
|
|
|
"""Total slippage cost in USD (entry + exit)."""
|
|
|
|
|
|
trade_sleeve: str | None = None
|
|
|
"""Selection sleeve label for momentum strategies. None for ORB trades."""
|
|
|
|
|
|
gap_pct: float | None = None
|
|
|
"""Opening gap used by momentum candidate selection. None when unavailable."""
|
|
|
|
|
|
confirmation_return_pct: float | None = None
|
|
|
"""Return from primary entry bar to confirmation bar for momentum confirmation."""
|
|
|
|
|
|
entry_dollar_volume: float | None = None
|
|
|
"""Cumulative dollar volume through the momentum entry/confirmation bar."""
|
|
|
|
|
|
avg_dollar_vol_30d: float | None = None
|
|
|
"""Prior 30-day average dollar volume used by liquidity/support gates."""
|
|
|
|
|
|
entropy_20d: float | None = None
|
|
|
"""Prior 20-day entropy feature used by candidate and size scaling."""
|
|
|
|
|
|
ret_5d: float | None = None
|
|
|
"""Prior 5-day return feature used by leader/continuation gates."""
|
|
|
|
|
|
event_score: float | None = None
|
|
|
"""Same-day filing/event score when available."""
|
|
|
|
|
|
support_score: float | None = None
|
|
|
"""Blended liquidity/attention/catalyst support score used by tail defense."""
|
|
|
|
|
|
is_liquid_largecap: bool | None = None
|
|
|
"""True when the trade qualified through the liquid large-cap sleeve/gate."""
|
|
|
|
|
|
is_moderate_gap_liquid: bool | None = None
|
|
|
"""True when the trade qualified through the moderate-gap liquid sleeve/gate."""
|
|
|
|
|
|
is_sector_thrust: bool | None = None
|
|
|
"""True when the trade qualified through the sector breadth-confirmed thrust sleeve/gate."""
|
|
|
|
|
|
sector_thrust_member_count: int | None = None
|
|
|
"""Number of same-sector names supporting the trade's sector-thrust state."""
|
|
|
|
|
|
sector_thrust_total_entry_dollar_volume: float | None = None
|
|
|
"""Combined entry-time dollar volume across supporting same-sector names."""
|
|
|
|
|
|
is_liquid_cluster: bool | None = None
|
|
|
"""True when the trade qualified through the separate liquid-cluster engine."""
|
|
|
|
|
|
liquid_cluster_member_count: int | None = None
|
|
|
"""Number of same-sector names supporting the liquid-cluster trade."""
|
|
|
|
|
|
liquid_cluster_total_entry_dollar_volume: float | None = None
|
|
|
"""Combined entry-time dollar volume across the liquid cluster."""
|
|
|
|
|
|
liquid_cluster_sector: str | None = None
|
|
|
"""Resolved sector label used by the liquid-cluster engine / ETF sleeve."""
|
|
|
|
|
|
liquid_cluster_sector_score: float | None = None
|
|
|
"""Sector-level cluster score used for post-allocation overlays."""
|
|
|
|
|
|
sector_proxy_ticker: str | None = None
|
|
|
"""Mapped sector ETF proxy ticker when the trade comes from ETF sleeve logic."""
|
|
|
|
|
|
# ORB-specific fields (optional, None for momentum trades)
|
|
|
orb_direction: str | None = None
|
|
|
"""ORB trade direction: 'long' or 'short'. None for momentum trades."""
|
|
|
|
|
|
rvol: float | None = None
|
|
|
"""Approximate RVOL at entry time. None for momentum trades."""
|
|
|
|
|
|
atr_at_entry: float | None = None
|
|
|
"""ATR(14) value used for stop sizing. None for momentum trades."""
|
|
|
|
|
|
r_multiple_at_exit: float | None = None
|
|
|
"""Final R-multiple at exit: (exit_price - entry_price) / initial_risk. None for momentum."""
|
|
|
|
|
|
stop_level_at_exit: str | None = None
|
|
|
"""Stop level active when the trade exited: 'initial', 'breakeven', or 'trailing'.
|
|
|
None for momentum trades. Helps diagnose whether winners were protected before exiting."""
|
|
|
|
|
|
partial_exit_r: float | None = None
|
|
|
"""R-multiple at which the partial exit fired, if partial_exit_at_r was set. None otherwise."""
|
|
|
|
|
|
pyramid_adds: int = 0
|
|
|
"""Number of pyramid additions executed during this trade. 0 = no pyramiding."""
|
|
|
|
|
|
pyramid_pnl: float = 0.0
|
|
|
"""Dollar P&L contributed by pyramid add-on shares. 0.0 = no pyramid or no pyramid PnL."""
|
|
|
|
|
|
is_reentry: bool = False
|
|
|
"""True if this trade is a re-entry after a prior stop-out on the same ticker same day."""
|
|
|
|
|
|
trigger_type: str = "orb"
|
|
|
"""Entry trigger: 'orb' (ORB breakout) or 'momentum_confirm' (09:45 momentum gate).
|
|
|
Always 'orb' for non-hybrid strategies."""
|
|
|
|
|
|
total_capital_deployed: float = 0.0
|
|
|
"""Total capital deployed including pyramid additions.
|
|
|
Computed as original_shares * entry_price + sum(pyramid_shares * pyramid_entry).
|
|
|
Used for accurate portfolio deployment tracking."""
|
|
|
|
|
|
|
|
|
class DayResult(BaseModel):
|
|
|
"""Simulation result for one trading day."""
|
|
|
|
|
|
date: str
|
|
|
trades: list[IntradayTrade] = Field(default_factory=list)
|
|
|
daily_pnl: float = 0.0
|
|
|
daily_return_pct: float = 0.0
|
|
|
candidates_found: int = 0
|
|
|
"""Number of stocks that met the morning gain threshold."""
|
|
|
|
|
|
# Settlement / GFV tracking (ORB-only; 0 when settlement_days=0 or momentum)
|
|
|
capital_deployed: float = 0.0
|
|
|
"""Total capital deployed in positions this day (sum of shares × entry_price)."""
|
|
|
available_cash_start: float = 0.0
|
|
|
"""Settled cash available at start of this trading day (before any trades)."""
|
|
|
skipped_insufficient_cash: int = 0
|
|
|
"""Candidates skipped because available settled cash was exhausted."""
|
|
|
|
|
|
# Diagnostic fields (populated by simulate_orb_day / run_orb_simulation_with_state)
|
|
|
skip_reason: str | None = None
|
|
|
"""Why this day had no trades. One of: 'vix_gate', 'market_regime', 'breadth',
|
|
|
'rolling_loss', 'spy_trend', 'no_candidates', 'below_min_candidates'. None = traded."""
|
|
|
candidate_filter_stats: dict | None = None
|
|
|
"""Per-filter drop counts from compute_orb_candidates: {gap, rvol, atr, dolvol, dir,
|
|
|
no_bars, late, price}. Present on all days (traded and skipped-after-candidates)."""
|
|
|
|
|
|
# V20 diagnostics
|
|
|
regime_scaler: float | None = None
|
|
|
"""Regime size scaler for this day (1.0 = full size or V19 path)."""
|
|
|
breadth_scaler: float | None = None
|
|
|
"""Breadth size scaler for this day (1.0 = full size or V19 path)."""
|
|
|
sector_scaler: float | None = None
|
|
|
"""Basket sector-concentration scaler for this day (1.0 = no extra concentration penalty)."""
|
|
|
tail_risk_scaler: float | None = None
|
|
|
"""Extra meta-layer scaler for sparse high-extension tail-risk days."""
|
|
|
soft_day_sparse_scaler: float | None = None
|
|
|
"""Extra meta-layer scaler for sparse soft-day baskets lacking supportive sleeves."""
|
|
|
is_soft_day: bool = False
|
|
|
"""True when combined_scaler < soft_day_scaler_threshold (soft-regime day)."""
|
|
|
event_day_liquid_active: bool = False
|
|
|
"""True when the event-day liquid sleeve activation gate passed for the day."""
|
|
|
event_day_liquid_event_count: int | None = None
|
|
|
"""Number of morning event contributors that qualified the event-day liquid gate."""
|
|
|
event_day_liquid_total_event_entry_dollar_volume: float | None = None
|
|
|
"""Combined entry-time dollar volume across event-day liquid activation contributors."""
|
|
|
|
|
|
|
|
|
# ── Aggregate Metrics ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
class IntradayMetrics(BaseModel):
|
|
|
"""Summary performance metrics for a complete backtest run."""
|
|
|
|
|
|
# Identity
|
|
|
run_id: str = ""
|
|
|
params_hash: str = ""
|
|
|
|
|
|
# Period
|
|
|
start_date: str = ""
|
|
|
end_date: str = ""
|
|
|
trading_days: int = 0
|
|
|
days_with_trades: int = 0
|
|
|
|
|
|
# Trade counts
|
|
|
total_trades: int = 0
|
|
|
stop_loss_exits: int = 0
|
|
|
|
|
|
# Trade-level metrics
|
|
|
win_rate: float | None = None
|
|
|
avg_win_pct: float | None = None
|
|
|
avg_loss_pct: float | None = None
|
|
|
profit_factor: float | None = None
|
|
|
expectancy_pct: float | None = None
|
|
|
|
|
|
# Return metrics
|
|
|
total_return_pct: float | None = None
|
|
|
annualized_return_pct: float | None = None
|
|
|
avg_daily_return_pct: float | None = None
|
|
|
|
|
|
# Risk metrics
|
|
|
max_drawdown_pct: float | None = None
|
|
|
sharpe_ratio: float | None = None
|
|
|
sortino_ratio: float | None = None
|
|
|
calmar_ratio: float | None = None
|
|
|
loss_day_rate: float | None = None
|
|
|
"""Fraction of trading days with negative PnL."""
|
|
|
avg_loss_day_pct: float | None = None
|
|
|
"""Average return across negative-PnL days only."""
|
|
|
tail_loss_20_pct: float | None = None
|
|
|
"""Average return of the worst 20% of losing days."""
|
|
|
worst_day_return_pct: float | None = None
|
|
|
"""Worst single-day return."""
|
|
|
loss_containment_score: float | None = None
|
|
|
"""0-100 score favoring strategies that lose small amounts on bad days."""
|
|
|
|
|
|
# Intraday-specific
|
|
|
avg_hold_minutes: float | None = None
|
|
|
stop_loss_exit_pct: float | None = None
|
|
|
"""Fraction of trades exited via stop loss."""
|
|
|
|
|
|
# Capital
|
|
|
initial_capital: float = 10_000.0
|
|
|
final_equity: float = 0.0
|
|
|
|
|
|
|
|
|
class SweepResult(BaseModel):
|
|
|
"""One parameter combination result from a grid sweep."""
|
|
|
|
|
|
params: dict[str, Any]
|
|
|
metrics: IntradayMetrics
|