Clean up superseded configs and commit accumulated R&D infrastructure

Key changes:
- Delete superseded strategy configs: orb_gainers safe_v2-v9, orb_pullback, vwap_reclaim, hypergap, leader_safe
- Add V46 prior_event_types param to domain.py + run.py event type wiring
- Major simulator.py enhancements: sector thrust sleeve, sector proxy mapping, helper functions
- Improve screener.py with better scoring/filtering
- Add new test coverage: test_simulator.py (776 lines) + test_screener.py (313 lines)
- Add V24.1 research candidate configs (w002/w003/w004/entrycap/losscap010 variants)
- Add leader momentum research configs and sweep files
- Update configs/snapshots/registry.json with new strategy registrations
- Add docs/leader_intraday_momentum_workflow.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent 92840b857a
commit 9095b376d9

@ -585,12 +585,15 @@ async def build_momentum_research_context(
if print_progress:
print(f"\n Intraday loaded: {len(all_intraday)} days")
ticker_sectors = await _load_ticker_sectors_with_oracle(tickers, client)
if _momentum_uses_historical_intraday_first(config.strategy):
candidates = momentum_intraday_first_candidates(
all_intraday,
trading_days,
config.strategy,
daily_enrichment=daily_enrichment,
ticker_sectors=ticker_sectors,
max_per_day=config.strategy.candidate_final_max_per_day,
)
else:
@ -609,7 +612,7 @@ async def build_momentum_research_context(
context = MomentumResearchContext(
config=config,
tickers=tickers,
ticker_sectors=await _load_ticker_sectors_with_oracle(tickers, client),
ticker_sectors=ticker_sectors,
trading_days=trading_days,
daily_bars=daily_bars,
all_intraday=all_intraday,
@ -680,6 +683,7 @@ def simulate_momentum_params(
trading_days,
strategy,
daily_enrichment=context.daily_enrichment,
ticker_sectors=context.ticker_sectors,
max_per_day=strategy.candidate_final_max_per_day,
)
else:

@ -1669,9 +1669,11 @@ async def run(config: IntradayConfig, refresh_cache: bool = False) -> tuple:
_prior_lookback = int(getattr(orb_params, "prior_event_lookback_days", 0) or 0)
if _prior_lookback > 0:
all_tickers = list({t for day_tickers in candidates.values() for t in day_tickers})
print(f" Prefetching prior-event features from DB (D-{_prior_lookback}) for {len(all_tickers)} tickers...")
_prior_event_types = tuple(getattr(orb_params, "prior_event_types", None) or ["earnings_release", "guidance_update"])
print(f" Prefetching prior-event features from DB (D-{_prior_lookback}, types={_prior_event_types}) for {len(all_tickers)} tickers...")
event_features = await _prefetch_prior_event_features_db(
all_tickers, trading_days, lookback_calendar_days=_prior_lookback
all_tickers, trading_days, lookback_calendar_days=_prior_lookback,
event_types=_prior_event_types,
)
print(f" Prior-event coverage: {len(event_features)} tickers with events")
else:
@ -2168,9 +2170,11 @@ async def run_with_sweep(config: IntradayConfig, sweep_path: str) -> None:
_prior_lookback_sw = int(getattr(orb_params, "prior_event_lookback_days", 0) or 0)
if _prior_lookback_sw > 0:
all_tickers_sw = list({t for day_tickers in candidates.values() for t in day_tickers})
print(f" Prefetching prior-event features from DB (D-{_prior_lookback_sw}) for {len(all_tickers_sw)} tickers...")
_prior_event_types_sw = tuple(getattr(orb_params, "prior_event_types", None) or ["earnings_release", "guidance_update"])
print(f" Prefetching prior-event features from DB (D-{_prior_lookback_sw}, types={_prior_event_types_sw}) for {len(all_tickers_sw)} tickers...")
event_features = await _prefetch_prior_event_features_db(
all_tickers_sw, trading_days, lookback_calendar_days=_prior_lookback_sw
all_tickers_sw, trading_days, lookback_calendar_days=_prior_lookback_sw,
event_types=_prior_event_types_sw,
)
print(f" Prior-event coverage: {len(event_features)} tickers with events")
else:

@ -95,6 +95,7 @@ def run_sweep(
momentum_enrichment: dict | None = None,
vix_by_day: dict[str, float] | None = None,
ticker_sectors: dict[str, str] | None = None,
sector_proxy_intraday_by_day: dict[str, dict[str, list[dict]]] | None = None,
) -> list[SweepResult]:
"""Run simulation for each parameter combination.
@ -132,6 +133,7 @@ def run_sweep(
daily_enrichment=momentum_enrichment,
vix_by_day=vix_by_day,
ticker_sectors=ticker_sectors,
sector_proxy_intraday_by_day=sector_proxy_intraday_by_day,
)
metrics = compute_metrics(day_results, config, run_id=f"sw{i:04d}")

@ -524,6 +524,8 @@ def run_backtest(
universe_profile = "midwide-liquid-long-v1"
elif "smallcap" in snapshot_id:
universe_profile = "smallcap-liquid-long-v1"
elif "broad" in snapshot_id:
universe_profile = "broad-liquid-long-v1"
if console:
console.print(f"\n[bold yellow]Snapshot '{snapshot_id}' is stale — refreshing...[/]")

@ -5,7 +5,9 @@ import argparse
import asyncio
import datetime as dt
import uuid
from pathlib import Path
import yaml
from sqlalchemy import select
from libs.common.config import get_settings
@ -19,12 +21,26 @@ from libs.oracle_client import FilingsService, make_oracle_client
logger = get_logger(__name__)
def _load_symbols_from_yaml(path: str) -> list[str]:
"""Load ticker list from a symbols YAML (supports list or dict with 'symbols' key)."""
raw = yaml.safe_load(Path(path).read_text())
if isinstance(raw, list):
items = raw
elif isinstance(raw, dict):
items = raw.get("symbols", [])
else:
items = []
return sorted({str(s).upper() for s in items if s})
async def poll_filings(
run_id: str,
start_date: str | None = None,
end_date: str | None = None,
symbols: list[str] | None = None,
) -> dict[str, int]:
settings = get_settings()
if symbols is None:
symbols = settings.get_symbols()
app_config = settings.get_app_config()
form_types = ",".join(app_config.get("pipeline", {}).get("form_types", ["8-K", "6-K"]))
@ -58,6 +74,9 @@ async def poll_filings(
ticker_to_symbol = {s.ticker: s.symbol_id for s in symbol_result.scalars().all()}
for ticker in symbols:
last_exc: Exception | None = None
response = None
for attempt in range(3):
try:
response = await svc.search_filings(
ticker,
@ -65,6 +84,26 @@ async def poll_filings(
start_date=effective_start,
end_date=end_date,
)
last_exc = None
break
except Exception as exc:
last_exc = exc
wait = 2 ** attempt # 1s, 2s, 4s
logger.warning(
"poll_retry",
ticker=ticker,
attempt=attempt + 1,
wait=wait,
error=str(exc) or repr(exc),
exc_type=type(exc).__name__,
)
await asyncio.sleep(wait)
if last_exc is not None:
logger.error("poll_error", ticker=ticker, error=str(last_exc) or repr(last_exc), exc_type=type(last_exc).__name__)
stats["errors"] += 1
continue
stats["seen"] += len(response.filings)
for filing in response.filings:
@ -115,10 +154,6 @@ async def poll_filings(
filing_date=filing.filing_date,
)
except Exception as exc:
logger.error("poll_error", ticker=ticker, error=str(exc) or repr(exc), exc_type=type(exc).__name__)
stats["errors"] += 1
# Update job record
job.status = "succeeded" if stats["errors"] == 0 else "partial"
job.finished_at_utc = dt.datetime.now(tz=dt.UTC)
@ -146,13 +181,26 @@ def main() -> None:
metavar="YYYY-MM-DD",
help="End date for filing search (default: today)",
)
parser.add_argument(
"--symbols-file",
default=None,
help="Override symbols YAML (default: settings.symbols_file). "
"Use for one-off backfills against a wider universe (e.g. broad snapshot).",
)
args = parser.parse_args()
settings = get_settings()
configure_logging(settings.log_level)
bind_job_run_id(args.run_id)
asyncio.run(poll_filings(args.run_id, start_date=args.start_date, end_date=args.end_date))
override_symbols = _load_symbols_from_yaml(args.symbols_file) if args.symbols_file else None
asyncio.run(poll_filings(
args.run_id,
start_date=args.start_date,
end_date=args.end_date,
symbols=override_symbols,
))
if __name__ == "__main__":

@ -1,119 +0,0 @@
_meta:
id: 38
name: "Hypergap Failure V1"
status: aborted
aborted_date: "2026-04-21"
aborted_reason: >
3 tests all failed. Test 1 (quality filters + regime gate): -33%, WR ~27%.
Test 2 (quality filters, no regime): -59%, WR ~25%.
Test 3 (inverted quality - no rvol, no premarket_vol): -59.16%, WR 42.9%, DD -59.16%.
Structural R/R problem: avg_win 3.55% < avg_loss 4.10%. Need WR ≥ 54% to break even at
this R/R — unachievable. High-quality stocks fail hard but rarely; low-quality stocks fail
often but with small moves. Neither profile yields positive expectancy on gap-failure shorts.
Root cause: gap-up short positions have inherently adverse asymmetry (stocks rocket up when
wrong, drift down slowly when right). No filter combination overcomes this.
description: >
Phase 3 / diagnostic: extreme-gap stocks (≥6%) that fail to hold the ORB.
Hypothesis: V23's portfolio-level correlation (~0.40) with any long-momentum engine is
regime-driven (both long-momentum, both triggered by QQQ-positive days). The only way to
break regime correlation is to be directionally orthogonal.
Gap failure = stock gaps up ≥6%, but ORB candle is bearish (sold off in first 5 min).
Entry: short when price breaks below ORB low. On days when V23's stocks are succeeding
(trend), these stocks should not be bearish-ORB (so no trades). On days when market
reverses (V23 losing), gap stocks are more likely to fail → shorts enter → anti-correlation.
Gate: WR ≥ 42% (shorts tolerate lower WR than longs due to asymmetric payout),
total_return ≥ 0%, max_dd ≥ -20%.
strategy_mode: orb
orb_strategy:
engine_family: hypergap_failure_v1
live_readiness: research_only
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: short_only # only trade bearish ORB candles (gap failure)
order_timeout_minutes: 45
allow_doji_breakout: false
allow_red_to_green_breakout: false
# === Candidate filters: extreme gap pool (≥6%), same quality bars as V23 ===
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: null # inverted: allow low-rvol retail stocks (test #3: invert quality)
min_abs_gap_pct: 0.06 # extreme gap: ≥6% (gap failure more likely above this threshold)
min_premarket_dollar_vol: null # inverted: allow low-premarket-vol retail stocks
max_candidates: 20
max_candidates_per_sector: 3
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: null # no cap
min_candidate_breadth: null # no breadth gate — operate on any breadth day
market_regime_spy_threshold: null # no QQQ regime gate — need to find own signal first
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: null # no rolling loss kill — diagnostic mode
max_simultaneous_entries: 3
min_breakout_rel_vol: null
# === Scoring weights (same as V23) ===
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
# === Stop / exit (conservative start for diagnostic) ===
atr_stop_multiplier: 1.0 # wider stop for shorts (gap stocks can be volatile)
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.05
max_stops_per_day: 5
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.025
drawdown_governor_min_scale: 0.30
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe:
source: midlarge
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday_orb
verbose: false

@ -0,0 +1,109 @@
_meta:
name: Leader Intraday Momentum Actual Catalyst Liquid
description: Separate actual-catalyst liquid-leader continuation engine. Uses only filing-backed catalyst candidates of selected event types, seeds a broad intraday-first shortlist from same-day events, then ranks and trades the liquid leaders showing early continuation.
id: 26
strategy_mode: momentum
strategy:
compound_returns: false
entry_minutes_after_open: 10
confirmation_minutes_after_entry: 5
min_confirmation_return_pct: 0.003
exit_minutes_before_close: 10
stop_loss_pct: null
trailing_stop_pct: -0.08
overextended_trailing_gain_pct: 0.05
overextended_trailing_stop_pct: -0.07
min_gap_pct: 0.0
min_morning_gain_pct: 0.008
max_morning_gain_pct: 0.06
max_gap_pct: 0.12
min_volume_ratio_14d: 0.02
min_entry_volume: 100000
min_entry_dollar_volume: 12000000
ticker_cooldown_days: 0
top_n: 5
max_positions_per_sector: 2
use_five_sleeves: true
five_sleeve_force_count: 3
use_event_sleeve: true
event_weight: 0.20
event_min_score: 1.0
event_sleeve_soft_day_only: false
use_liquid_largecap_sleeve: true
liquid_largecap_weight: 0.15
liquid_largecap_min_gain_pct: 0.004
liquid_largecap_max_gain_pct: 0.03
liquid_largecap_min_confirmation_return_pct: 0.001
liquid_largecap_min_entry_dollar_volume: 50000000
liquid_largecap_min_avg_dollar_vol_30d: 1000000000
liquid_largecap_max_entropy_20d: 0.85
use_moderate_gap_liquid_sleeve: true
moderate_gap_liquid_weight: 0.10
moderate_gap_liquid_min_gap_pct: 0.003
moderate_gap_liquid_max_gap_pct: 0.04
moderate_gap_liquid_min_gain_pct: 0.008
moderate_gap_liquid_max_gain_pct: 0.04
moderate_gap_liquid_min_confirmation_return_pct: 0.002
moderate_gap_liquid_min_entry_dollar_volume: 20000000
moderate_gap_liquid_min_avg_dollar_vol_30d: 250000000
moderate_gap_liquid_max_avg_dollar_vol_30d: 3000000000
moderate_gap_liquid_min_volume_ratio_14d: 0.02
moderate_gap_liquid_max_entropy_20d: 0.84
max_entropy_20d: 0.85
entropy_size_scale_low: 0.78
entropy_size_scale_high: 0.85
entropy_size_scale_min: 0.7
sector_concentration_scale_low: 0.4
sector_concentration_scale_high: 0.67
sector_concentration_scale_min: 0.85
max_vix: 35.0
initial_capital: 10000.0
slippage_bps: 5.0
candidate_source_mode: intraday_first
candidate_seed_threshold: 0.0
candidate_seed_max_per_day: 80
candidate_seed_liquid_overlay_slots: 0
candidate_seed_leader_overlay_slots: 0
candidate_seed_moderate_liquid_overlay_slots: 0
candidate_seed_event_overlay_slots: 20
candidate_seed_event_min_score: 1.0
candidate_seed_event_min_gap_pct: 0.0
candidate_seed_event_max_gap_pct: 0.12
candidate_seed_event_min_avg_dollar_vol_30d: 150000000
candidate_seed_event_min_ret_5d: 0.0
candidate_seed_event_max_entropy_20d: 0.85
candidate_final_max_per_day: 12
candidate_intraday_rank_mode: weighted
candidate_intraday_weight_gain: 0.10
candidate_intraday_weight_confirmation: 0.25
candidate_intraday_weight_volume_ratio: 0.15
candidate_intraday_weight_entry_dollar_volume: 0.20
candidate_intraday_weight_avg_dollar_vol_30d: 0.15
candidate_intraday_weight_gap: 0.05
candidate_intraday_weight_low_entropy: 0.05
candidate_intraday_weight_event_score: 0.20
candidate_intraday_event_reserve_slots: 2
candidate_intraday_event_reserve_min_score: 1.0
candidate_intraday_event_reserve_soft_day_only: false
candidate_intraday_moderate_liquid_reserve_slots: 0
candidate_require_event_flag: true
candidate_min_event_score: 1.0
candidate_allowed_event_types:
- earnings_release
- guidance_update
- material_contract
- other_material_event
universe:
source: broad
min_price: 10.0
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
pre_screen_threshold: 0.02
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday
verbose: false

@ -0,0 +1,185 @@
_meta:
name: Leader Intraday Momentum Event Day Liquid Hybrid
description: Flagship high-WR intraday-first basket plus a separate multi-event liquid overlay. The core basket stays identical to the flagship strategy; only on broad same-day filing clusters does the strategy reserve a small extra budget slice for liquid continuation names outside the base basket.
id: 27
strategy_mode: momentum
strategy:
compound_returns: false
entry_minutes_after_open: 10
confirmation_minutes_after_entry: 5
min_confirmation_return_pct: 0.005
exit_minutes_before_close: 10
stop_loss_pct: null
trailing_stop_pct: -0.07
overextended_trailing_gain_pct: 0.04
overextended_trailing_stop_pct: -0.065
min_morning_gain_pct: 0.015
max_morning_gain_pct: 0.06
max_gap_pct: 0.055
min_volume_ratio_14d: 0.04
min_entry_volume: 125000
min_entry_dollar_volume: 1500000
ticker_cooldown_days: 0
top_n: 9
max_positions_per_sector: 2
use_five_sleeves: true
five_sleeve_force_count: 4
use_event_sleeve: true
event_weight: 0.12
event_min_score: 1.0
event_sleeve_soft_day_only: true
soft_day_sparse_max_trades: 2
soft_day_sparse_require_no_event: true
soft_day_sparse_exempt_largecap: true
soft_day_sparse_exempt_moderate_gap_liquid: true
soft_day_sparse_scale: 0.7
tail_risk_day_max_trades: 3
tail_risk_day_min_max_gain_pct: 0.025
tail_risk_day_max_support_score: 1.0
tail_risk_day_min_max_confirmation_return_pct: 0.01
tail_risk_day_require_no_event: true
tail_risk_day_event_exemption_min_support_score: 0.35
tail_risk_day_exempt_largecap: true
tail_risk_day_scale: 0.55
low_momentum_single_name_max_gain_pct: 0.025
low_momentum_single_name_require_no_event: true
low_momentum_single_name_exempt_largecap: true
low_momentum_single_name_scale: 0.55
max_entropy_20d: 0.86
entropy_size_scale_low: 0.78
entropy_size_scale_high: 0.86
entropy_size_scale_min: 0.6
sector_concentration_scale_low: 0.4
sector_concentration_scale_high: 0.67
sector_concentration_scale_min: 0.8
max_vix: 30.0
recent_live_scan_days: 0
recent_live_scan_min_price: 2.0
recent_live_scan_avg_volume_min: 200000
recent_live_scan_market_cap_min: 100000000.0
recent_live_scan_max_candidates_per_day: 150
recent_live_scan_top_n: 6
recent_live_scan_min_morning_gain_pct: 0.005
recent_live_scan_max_morning_gain_pct: 0.05
recent_live_scan_min_confirmation_return_pct: 0.0005
recent_live_scan_min_entry_dollar_volume: 50000000.0
recent_live_scan_max_gap_pct: 0.04
recent_live_scan_max_entropy_20d: 0.9
recent_live_scan_use_slow_ignite_sleeve: true
recent_live_scan_slow_ignite_weight: 0.30
recent_live_scan_slow_ignite_min_gain_pct: 0.003
recent_live_scan_slow_ignite_max_gain_pct: 0.015
recent_live_scan_slow_ignite_min_entry_dollar_volume: 50000000.0
recent_live_scan_slow_ignite_max_entropy_20d: 0.9
recent_live_scan_use_liquid_largecap_sleeve: true
recent_live_scan_liquid_largecap_weight: 0.35
recent_live_scan_liquid_largecap_min_gain_pct: 0.004
recent_live_scan_liquid_largecap_max_gain_pct: 0.02
recent_live_scan_liquid_largecap_min_confirmation_return_pct: 0.0005
recent_live_scan_liquid_largecap_min_entry_dollar_volume: 50000000.0
recent_live_scan_liquid_largecap_min_avg_dollar_vol_30d: 500000000.0
recent_live_scan_liquid_largecap_max_entropy_20d: 0.9
initial_capital: 10000.0
slippage_bps: 5.0
candidate_source_mode: intraday_first
candidate_seed_threshold: 0.0075
candidate_seed_max_per_day: 150
candidate_seed_liquid_overlay_slots: 3
candidate_seed_liquid_min_gap_pct: 0.005
candidate_seed_liquid_max_gap_pct: 0.025
candidate_seed_liquid_min_avg_dollar_vol_30d: 5000000000.0
candidate_seed_liquid_min_ret_5d: 0.0
candidate_seed_liquid_max_entropy_20d: 0.87
candidate_seed_leader_overlay_slots: 1
candidate_seed_leader_min_gap_pct: -0.025
candidate_seed_leader_max_gap_pct: 0.01
candidate_seed_leader_min_avg_dollar_vol_30d: 500000000.0
candidate_seed_leader_min_ret_5d: 0.15
candidate_seed_leader_min_atr_pct: 0.06
candidate_seed_leader_max_entropy_20d: 0.75
candidate_seed_moderate_liquid_overlay_slots: 20
candidate_seed_moderate_liquid_min_gap_pct: 0.005
candidate_seed_moderate_liquid_max_gap_pct: 0.025
candidate_seed_moderate_liquid_min_avg_dollar_vol_30d: 250000000.0
candidate_seed_moderate_liquid_max_avg_dollar_vol_30d: 2000000000.0
candidate_seed_moderate_liquid_max_entropy_20d: 0.86
candidate_seed_event_overlay_slots: 0
candidate_seed_event_min_score: null
candidate_seed_event_min_gap_pct: null
candidate_seed_event_max_gap_pct: null
candidate_seed_event_min_avg_dollar_vol_30d: null
candidate_seed_event_min_ret_5d: null
candidate_seed_event_max_entropy_20d: null
candidate_final_max_per_day: 14
candidate_intraday_rank_mode: weighted
candidate_intraday_weight_gain: 0.10
candidate_intraday_weight_confirmation: 0.45
candidate_intraday_weight_volume_ratio: 0.20
candidate_intraday_weight_entry_dollar_volume: 0.15
candidate_intraday_weight_gap: 0.05
candidate_intraday_weight_low_entropy: 0.05
candidate_intraday_weight_avg_dollar_vol_30d: 0.08
candidate_intraday_weight_event_score: 0.0
candidate_intraday_event_reserve_slots: 0
candidate_intraday_event_reserve_min_score: null
candidate_intraday_event_reserve_soft_day_only: false
candidate_intraday_moderate_liquid_reserve_slots: 1
candidate_intraday_moderate_liquid_reserve_trigger_below: 2
candidate_allowed_event_types: []
use_event_day_liquid_sleeve: true
event_day_liquid_capital_fraction: 0.12
event_day_liquid_max_positions: 2
event_day_liquid_allowed_event_types:
- earnings_release
- guidance_update
- material_contract
- other_material_event
- management_change
- unknown
event_day_liquid_min_event_names: 2
event_day_liquid_min_event_score: 1.0
event_day_liquid_min_event_support_score: 0.15
event_day_liquid_min_total_event_entry_dollar_volume: 100000000.0
event_day_liquid_min_gain_pct: 0.004
event_day_liquid_max_gain_pct: 0.04
event_day_liquid_min_confirmation_return_pct: 0.0005
event_day_liquid_min_entry_dollar_volume: 25000000.0
event_day_liquid_min_avg_dollar_vol_30d: 250000000.0
event_day_liquid_max_entropy_20d: 0.88
event_day_liquid_min_support_score: 0.35
use_liquid_largecap_sleeve: true
liquid_largecap_weight: 0.05
use_moderate_gap_liquid_sleeve: true
moderate_gap_liquid_weight: 0.0
moderate_gap_liquid_min_gap_pct: 0.005
moderate_gap_liquid_max_gap_pct: 0.025
moderate_gap_liquid_min_gain_pct: 0.015
moderate_gap_liquid_max_gain_pct: 0.04
moderate_gap_liquid_min_confirmation_return_pct: 0.005
moderate_gap_liquid_min_entry_dollar_volume: 40000000.0
moderate_gap_liquid_min_avg_dollar_vol_30d: 250000000.0
moderate_gap_liquid_max_avg_dollar_vol_30d: 2000000000.0
moderate_gap_liquid_min_volume_ratio_14d: 0.04
moderate_gap_liquid_max_entropy_20d: 0.86
fallback_liquid_largecap_slots: 1
fallback_liquid_largecap_trigger_below: 2
liquid_largecap_min_gain_pct: 0.004
liquid_largecap_max_gain_pct: 0.015
liquid_largecap_min_confirmation_return_pct: 0.0005
liquid_largecap_min_entry_dollar_volume: 50000000.0
liquid_largecap_min_avg_dollar_vol_30d: 2000000000.0
liquid_largecap_max_entropy_20d: 0.87
universe:
source: broad
min_price: 10.0
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
pre_screen_threshold: 0.02
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday
verbose: false

@ -1,48 +0,0 @@
_meta:
name: Leader Intraday Momentum High WR
description: More selective high-win-rate variant of the safe momentum basket. Uses a 5-minute confirmation that requires +0.45% follow-through, a 2.0M entry dollar-volume floor, a tighter 6.0% morning overextension cap, a 5.5% opening-gap cap, an 8.0% base trailing stop, and a slightly tighter 7.5% trail once a name is already up 4%+ at entry. Intended for users who prioritize win rate over basket breadth.
id: 20
strategy_mode: momentum
strategy:
compound_returns: false
entry_minutes_after_open: 10
confirmation_minutes_after_entry: 5
min_confirmation_return_pct: 0.0045
exit_minutes_before_close: 10
stop_loss_pct: null
trailing_stop_pct: -0.08
overextended_trailing_gain_pct: 0.04
overextended_trailing_stop_pct: -0.075
min_morning_gain_pct: 0.015
max_morning_gain_pct: 0.06
max_gap_pct: 0.055
min_entry_volume: 125000
min_entry_dollar_volume: 2000000
ticker_cooldown_days: 4
top_n: 8
max_positions_per_sector: 2
use_five_sleeves: true
max_entropy_20d: 0.86
max_vix: 30.0
recent_live_scan_days: 0
recent_live_scan_min_price: 2.0
recent_live_scan_avg_volume_min: 200000
recent_live_scan_market_cap_min: 100000000.0
recent_live_scan_max_candidates_per_day: 150
initial_capital: 10000.0
slippage_bps: 5.0
market_regime_spy_threshold: null
universe:
source: broad
min_price: 10.0
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
pre_screen_threshold: 0.02
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday
verbose: false

@ -1,6 +1,6 @@
_meta:
name: Leader Intraday Momentum High WR Intraday First
description: Corrected high-win-rate momentum variant that replaces the pure daily-gap candidate source with a historical intraday-first shortlist. It uses a 1.5% seed gap, reranks the shortlist at entry time using confirmation, volume-ratio, dollar-volume, gap, and low-entropy quality, trims exposure on the noisiest high-entropy names, only on soft days lets one event sleeve substitute a catalyst-backed name into the basket, adds a bounded moderate-gap liquid follow-through reserve for names that broad scan catches but the raw gap rank misses, and applies a no-event sparse basket tail defense. Intended to improve corrected Q1 and weak 2025 quarter robustness without reintroducing lookahead.
description: Corrected high-win-rate momentum variant that replaces the pure daily-gap candidate source with a historical intraday-first shortlist. It uses a 1.5% seed gap, reranks the shortlist at entry time using confirmation, volume-ratio, dollar-volume, gap, and low-entropy quality, trims exposure on the noisiest high-entropy names, only on soft days lets one event sleeve substitute a catalyst-backed name into the basket, adds a bounded moderate-gap liquid follow-through reserve for names that broad scan catches but the raw gap rank misses, scales down sparse soft-day baskets that lack supportive event or liquid-follow-through structure, and now reserves a small extra budget slice on broad same-day filing clusters to add liquid continuation names outside the base basket.
id: 25
strategy_mode: momentum
strategy:
@ -28,14 +28,24 @@ strategy:
event_weight: 0.12
event_min_score: 1.0
event_sleeve_soft_day_only: true
soft_day_sparse_max_trades: 2
soft_day_sparse_require_no_event: true
soft_day_sparse_exempt_largecap: true
soft_day_sparse_exempt_moderate_gap_liquid: true
soft_day_sparse_scale: 0.7
tail_risk_day_max_trades: 3
tail_risk_day_min_max_gain_pct: 0.025
tail_risk_day_max_support_score: 1.0
tail_risk_day_min_max_entropy_20d: null
tail_risk_day_min_max_confirmation_return_pct: 0.01
tail_risk_day_require_no_event: true
tail_risk_day_event_exemption_min_support_score: 0.35
tail_risk_day_exempt_largecap: true
tail_risk_day_scale: 0.55
low_momentum_single_name_max_gain_pct: 0.025
low_momentum_single_name_require_no_event: true
low_momentum_single_name_exempt_largecap: true
low_momentum_single_name_scale: 0.55
max_entropy_20d: 0.86
entropy_size_scale_low: 0.78
entropy_size_scale_high: 0.86
@ -106,6 +116,27 @@ strategy:
candidate_intraday_weight_avg_dollar_vol_30d: 0.08
candidate_intraday_moderate_liquid_reserve_slots: 1
candidate_intraday_moderate_liquid_reserve_trigger_below: 2
use_event_day_liquid_sleeve: true
event_day_liquid_capital_fraction: 0.12
event_day_liquid_max_positions: 2
event_day_liquid_allowed_event_types:
- earnings_release
- guidance_update
- material_contract
- other_material_event
- management_change
- unknown
event_day_liquid_min_event_names: 2
event_day_liquid_min_event_score: 1.0
event_day_liquid_min_event_support_score: 0.15
event_day_liquid_min_total_event_entry_dollar_volume: 100000000.0
event_day_liquid_min_gain_pct: 0.004
event_day_liquid_max_gain_pct: 0.04
event_day_liquid_min_confirmation_return_pct: 0.0005
event_day_liquid_min_entry_dollar_volume: 25000000.0
event_day_liquid_min_avg_dollar_vol_30d: 250000000.0
event_day_liquid_max_entropy_20d: 0.88
event_day_liquid_min_support_score: 0.35
use_liquid_largecap_sleeve: true
liquid_largecap_weight: 0.05
use_moderate_gap_liquid_sleeve: true

@ -1,7 +1,7 @@
_meta:
name: Leader Intraday Momentum Defended
description: Intraday-first leader momentum with layered downside defense. It keeps the corrected broad-universe candidate engine, then scales down weak market/breadth days and also shrinks thin, weak-support single-name breakout days to improve loss containment without changing the core entry logic.
id: 31
name: Leader Intraday Momentum High WR Intraday First Cluster Overlay Base
description: Experimental base config for evaluating post-allocation liquid-cluster and sector-ETF breadth overlays on top of the High WR Intraday First flagship. This is not the official production strategy; it exists to fetch the wider metadata/proxy set needed for overlay research.
id: 26
strategy_mode: momentum
strategy:
compound_returns: false
@ -10,9 +10,9 @@ strategy:
min_confirmation_return_pct: 0.005
exit_minutes_before_close: 10
stop_loss_pct: null
trailing_stop_pct: -0.08
trailing_stop_pct: -0.07
overextended_trailing_gain_pct: 0.04
overextended_trailing_stop_pct: -0.075
overextended_trailing_stop_pct: -0.065
min_morning_gain_pct: 0.015
max_morning_gain_pct: 0.06
max_gap_pct: 0.055
@ -24,32 +24,36 @@ strategy:
max_positions_per_sector: 2
use_five_sleeves: true
five_sleeve_force_count: 4
tail_risk_day_max_trades: 1
tail_risk_day_min_max_gain_pct: 0.03
tail_risk_day_max_support_score: 0.25
tail_risk_day_min_max_entropy_20d: 0.79
use_event_sleeve: true
event_weight: 0.12
event_min_score: 1.0
event_sleeve_soft_day_only: true
soft_day_sparse_max_trades: 2
soft_day_sparse_require_no_event: true
soft_day_sparse_exempt_largecap: true
soft_day_sparse_exempt_moderate_gap_liquid: true
soft_day_sparse_scale: 0.7
tail_risk_day_max_trades: 3
tail_risk_day_min_max_gain_pct: 0.025
tail_risk_day_max_support_score: 1.0
tail_risk_day_min_max_entropy_20d: null
tail_risk_day_min_max_confirmation_return_pct: 0.01
tail_risk_day_require_no_event: true
tail_risk_day_event_exemption_min_support_score: 0.35
tail_risk_day_exempt_largecap: true
tail_risk_day_scale: 0.55
low_momentum_single_name_max_gain_pct: 0.025
low_momentum_single_name_require_no_event: true
low_momentum_single_name_exempt_largecap: true
low_momentum_single_name_scale: 0.55
max_entropy_20d: 0.86
entropy_size_scale_low: 0.78
entropy_size_scale_high: 0.86
entropy_size_scale_min: 0.6
sector_concentration_scale_low: 0.4
sector_concentration_scale_high: 0.67
sector_concentration_scale_min: 0.8
max_vix: 30.0
market_regime_spy_threshold: null
market_regime_gap_threshold: -0.015
market_regime_gap_ticker: SPY
regime_size_scale_low: -0.015
regime_size_scale_high: 0.002
regime_size_scale_min: 0.65
min_candidate_breadth: 0.4
breadth_size_scale_low: 0.4
breadth_size_scale_high: 0.62
breadth_size_scale_min: 0.7
soft_day_scaler_threshold: 0.88
soft_day_max_trades: 5
rolling_loss_days: null
rolling_loss_threshold: null
recent_live_scan_days: 0
recent_live_scan_min_price: 2.0
recent_live_scan_avg_volume_min: 200000
@ -78,6 +82,7 @@ strategy:
recent_live_scan_liquid_largecap_max_entropy_20d: 0.9
initial_capital: 10000.0
slippage_bps: 5.0
market_regime_spy_threshold: null
candidate_source_mode: intraday_first
candidate_seed_threshold: 0.0075
candidate_seed_max_per_day: 150
@ -94,6 +99,12 @@ strategy:
candidate_seed_leader_min_ret_5d: 0.15
candidate_seed_leader_min_atr_pct: 0.06
candidate_seed_leader_max_entropy_20d: 0.75
candidate_seed_moderate_liquid_overlay_slots: 20
candidate_seed_moderate_liquid_min_gap_pct: 0.005
candidate_seed_moderate_liquid_max_gap_pct: 0.025
candidate_seed_moderate_liquid_min_avg_dollar_vol_30d: 250000000.0
candidate_seed_moderate_liquid_max_avg_dollar_vol_30d: 2000000000.0
candidate_seed_moderate_liquid_max_entropy_20d: 0.86
candidate_final_max_per_day: 14
candidate_intraday_rank_mode: weighted
candidate_intraday_weight_gain: 0.10
@ -103,8 +114,22 @@ strategy:
candidate_intraday_weight_gap: 0.05
candidate_intraday_weight_low_entropy: 0.05
candidate_intraday_weight_avg_dollar_vol_30d: 0.08
candidate_intraday_moderate_liquid_reserve_slots: 1
candidate_intraday_moderate_liquid_reserve_trigger_below: 2
use_liquid_largecap_sleeve: true
liquid_largecap_weight: 0.05
use_moderate_gap_liquid_sleeve: true
moderate_gap_liquid_weight: 0.0
moderate_gap_liquid_min_gap_pct: 0.005
moderate_gap_liquid_max_gap_pct: 0.025
moderate_gap_liquid_min_gain_pct: 0.015
moderate_gap_liquid_max_gain_pct: 0.04
moderate_gap_liquid_min_confirmation_return_pct: 0.005
moderate_gap_liquid_min_entry_dollar_volume: 40000000.0
moderate_gap_liquid_min_avg_dollar_vol_30d: 250000000.0
moderate_gap_liquid_max_avg_dollar_vol_30d: 2000000000.0
moderate_gap_liquid_min_volume_ratio_14d: 0.04
moderate_gap_liquid_max_entropy_20d: 0.86
fallback_liquid_largecap_slots: 1
fallback_liquid_largecap_trigger_below: 2
liquid_largecap_min_gain_pct: 0.004
@ -113,6 +138,26 @@ strategy:
liquid_largecap_min_entry_dollar_volume: 50000000.0
liquid_largecap_min_avg_dollar_vol_30d: 2000000000.0
liquid_largecap_max_entropy_20d: 0.87
use_liquid_cluster_engine: true
liquid_cluster_capital_fraction: 0.15
liquid_cluster_max_positions: 1
liquid_cluster_max_positions_per_sector: 1
liquid_cluster_min_members: 2
liquid_cluster_min_gain_pct: 0.015
liquid_cluster_max_gain_pct: 0.04
liquid_cluster_min_confirmation_return_pct: 0.005
liquid_cluster_min_entry_dollar_volume: 40000000.0
liquid_cluster_min_avg_dollar_vol_30d: 250000000.0
liquid_cluster_max_avg_dollar_vol_30d: 2000000000.0
liquid_cluster_min_volume_ratio_14d: 0.04
liquid_cluster_max_entropy_20d: 0.86
liquid_cluster_min_sector_avg_confirmation_return_pct: 0.005
liquid_cluster_min_sector_total_entry_dollar_volume: 100000000.0
liquid_cluster_require_special_liquidity_gate: true
use_sector_etf_sleeve: true
sector_etf_capital_fraction: 0.10
sector_etf_max_positions: 1
sector_etf_min_sector_score: 0.20
universe:
source: broad
min_price: 10.0

@ -0,0 +1,117 @@
_meta:
name: Leader Intraday Momentum Liquid Continuation Core
description: Separate liquid-continuation core engine. Instead of treating liquid follow-through as an overlay, this strategy makes moderate-gap liquid names, liquid large-cap leaders, and sector breadth-confirmed continuation the primary basket selection path.
id: 28
strategy_mode: momentum
strategy:
compound_returns: false
entry_minutes_after_open: 10
confirmation_minutes_after_entry: 5
min_confirmation_return_pct: 0.003
exit_minutes_before_close: 10
stop_loss_pct: null
trailing_stop_pct: -0.075
overextended_trailing_gain_pct: 0.04
overextended_trailing_stop_pct: -0.06
min_gap_pct: 0.0
min_morning_gain_pct: 0.006
max_morning_gain_pct: 0.04
max_gap_pct: 0.05
min_volume_ratio_14d: 0.02
min_entry_volume: 100000
min_entry_dollar_volume: 20000000
ticker_cooldown_days: 0
top_n: 4
max_positions_per_sector: 2
use_five_sleeves: false
momentum_selection_mode: liquid_continuation
soft_day_sparse_max_trades: 2
soft_day_sparse_exempt_largecap: true
soft_day_sparse_exempt_moderate_gap_liquid: true
soft_day_sparse_scale: 0.8
tail_risk_day_max_trades: 3
tail_risk_day_min_max_gain_pct: 0.02
tail_risk_day_min_max_confirmation_return_pct: 0.006
tail_risk_day_exempt_largecap: true
tail_risk_day_scale: 0.65
max_entropy_20d: 0.88
entropy_size_scale_low: 0.80
entropy_size_scale_high: 0.88
entropy_size_scale_min: 0.7
sector_concentration_scale_low: 0.4
sector_concentration_scale_high: 0.67
sector_concentration_scale_min: 0.85
max_vix: 35.0
initial_capital: 10000.0
slippage_bps: 5.0
market_regime_spy_threshold: null
candidate_source_mode: intraday_first
candidate_seed_threshold: 0.0
candidate_seed_max_per_day: 180
candidate_seed_liquid_overlay_slots: 10
candidate_seed_liquid_min_gap_pct: -0.015
candidate_seed_liquid_max_gap_pct: 0.03
candidate_seed_liquid_min_avg_dollar_vol_30d: 2000000000.0
candidate_seed_liquid_min_ret_5d: 0.0
candidate_seed_liquid_max_entropy_20d: 0.88
candidate_seed_leader_overlay_slots: 6
candidate_seed_leader_min_gap_pct: -0.02
candidate_seed_leader_max_gap_pct: 0.03
candidate_seed_leader_min_avg_dollar_vol_30d: 500000000.0
candidate_seed_leader_min_ret_5d: 0.10
candidate_seed_leader_min_atr_pct: 0.04
candidate_seed_leader_max_entropy_20d: 0.82
candidate_seed_moderate_liquid_overlay_slots: 40
candidate_seed_moderate_liquid_min_gap_pct: 0.002
candidate_seed_moderate_liquid_max_gap_pct: 0.04
candidate_seed_moderate_liquid_min_avg_dollar_vol_30d: 250000000.0
candidate_seed_moderate_liquid_max_avg_dollar_vol_30d: 4000000000.0
candidate_seed_moderate_liquid_min_ret_5d: 0.0
candidate_seed_moderate_liquid_max_entropy_20d: 0.88
candidate_final_max_per_day: 14
candidate_intraday_rank_mode: liquid_continuation
candidate_intraday_moderate_liquid_reserve_slots: 2
candidate_intraday_moderate_liquid_reserve_trigger_below: 3
use_liquid_largecap_sleeve: true
liquid_largecap_weight: 0.0
liquid_largecap_min_gain_pct: 0.006
liquid_largecap_max_gain_pct: 0.025
liquid_largecap_min_confirmation_return_pct: 0.003
liquid_largecap_min_entry_dollar_volume: 60000000.0
liquid_largecap_min_avg_dollar_vol_30d: 2000000000.0
liquid_largecap_max_entropy_20d: 0.90
use_moderate_gap_liquid_sleeve: true
moderate_gap_liquid_weight: 0.0
moderate_gap_liquid_min_gap_pct: 0.005
moderate_gap_liquid_max_gap_pct: 0.035
moderate_gap_liquid_min_gain_pct: 0.01
moderate_gap_liquid_max_gain_pct: 0.04
moderate_gap_liquid_min_confirmation_return_pct: 0.003
moderate_gap_liquid_min_entry_dollar_volume: 40000000.0
moderate_gap_liquid_min_avg_dollar_vol_30d: 250000000.0
moderate_gap_liquid_max_avg_dollar_vol_30d: 4000000000.0
moderate_gap_liquid_min_volume_ratio_14d: 0.04
moderate_gap_liquid_max_entropy_20d: 0.88
use_sector_thrust_sleeve: true
sector_thrust_weight: 0.0
sector_thrust_min_members: 2
sector_thrust_min_gain_pct: 0.01
sector_thrust_min_confirmation_return_pct: 0.003
sector_thrust_min_entry_dollar_volume: 40000000.0
sector_thrust_min_avg_dollar_vol_30d: 250000000.0
sector_thrust_min_sector_avg_confirmation_return_pct: 0.003
sector_thrust_min_sector_total_entry_dollar_volume: 150000000.0
universe:
source: broad
min_price: 10.0
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
pre_screen_threshold: 0.02
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday
verbose: false

@ -1,101 +0,0 @@
_meta:
name: Leader Intraday Momentum Safe
description: Safer intraday-first leader basket derived from the current high-win-rate engine. It keeps the liquid-largecap-aware candidate stack, same-day confirmation rerank, and five-sleeve blend, but scales sparse 1-5 position days below full size to reduce trap-day drawdowns without reverting to the stale pre-intraday-first safe rules.
id: 19
strategy_mode: momentum
strategy:
compound_returns: false
entry_minutes_after_open: 10
confirmation_minutes_after_entry: 5
min_confirmation_return_pct: 0.005
exit_minutes_before_close: 10
stop_loss_pct: null
trailing_stop_pct: -0.08
overextended_trailing_gain_pct: 0.04
overextended_trailing_stop_pct: -0.075
min_morning_gain_pct: 0.015
max_morning_gain_pct: 0.06
max_gap_pct: 0.055
min_volume_ratio_14d: 0.04
min_entry_volume: 125000
min_entry_dollar_volume: 1500000
ticker_cooldown_days: 0
top_n: 9
full_size_positions_threshold: 6
sparse_day_size_floor: 0.5
max_positions_per_sector: 2
use_five_sleeves: true
five_sleeve_force_count: 4
max_entropy_20d: 0.86
max_vix: 30.0
recent_live_scan_days: 0
recent_live_scan_min_price: 2.0
recent_live_scan_avg_volume_min: 200000
recent_live_scan_market_cap_min: 100000000.0
recent_live_scan_max_candidates_per_day: 150
recent_live_scan_top_n: 6
recent_live_scan_min_morning_gain_pct: 0.005
recent_live_scan_max_morning_gain_pct: 0.05
recent_live_scan_min_confirmation_return_pct: 0.0005
recent_live_scan_min_entry_dollar_volume: 50000000.0
recent_live_scan_max_gap_pct: 0.04
recent_live_scan_max_entropy_20d: 0.9
recent_live_scan_use_slow_ignite_sleeve: true
recent_live_scan_slow_ignite_weight: 0.30
recent_live_scan_slow_ignite_min_gain_pct: 0.003
recent_live_scan_slow_ignite_max_gain_pct: 0.015
recent_live_scan_slow_ignite_min_entry_dollar_volume: 50000000.0
recent_live_scan_slow_ignite_max_entropy_20d: 0.9
recent_live_scan_use_liquid_largecap_sleeve: true
recent_live_scan_liquid_largecap_weight: 0.35
recent_live_scan_liquid_largecap_min_gain_pct: 0.004
recent_live_scan_liquid_largecap_max_gain_pct: 0.02
recent_live_scan_liquid_largecap_min_confirmation_return_pct: 0.0005
recent_live_scan_liquid_largecap_min_entry_dollar_volume: 50000000.0
recent_live_scan_liquid_largecap_min_avg_dollar_vol_30d: 500000000.0
recent_live_scan_liquid_largecap_max_entropy_20d: 0.9
initial_capital: 10000.0
slippage_bps: 5.0
market_regime_spy_threshold: null
candidate_source_mode: intraday_first
candidate_seed_threshold: 0.0075
candidate_seed_max_per_day: 150
candidate_seed_liquid_overlay_slots: 3
candidate_seed_liquid_min_gap_pct: 0.005
candidate_seed_liquid_max_gap_pct: 0.025
candidate_seed_liquid_min_avg_dollar_vol_30d: 5000000000.0
candidate_seed_liquid_min_ret_5d: 0.0
candidate_seed_liquid_max_entropy_20d: 0.87
candidate_final_max_per_day: 12
candidate_intraday_rank_mode: weighted
candidate_intraday_weight_gain: 0.10
candidate_intraday_weight_confirmation: 0.45
candidate_intraday_weight_volume_ratio: 0.20
candidate_intraday_weight_entry_dollar_volume: 0.15
candidate_intraday_weight_gap: 0.05
candidate_intraday_weight_low_entropy: 0.05
candidate_intraday_weight_avg_dollar_vol_30d: 0.08
use_liquid_largecap_sleeve: true
liquid_largecap_weight: 0.05
fallback_liquid_largecap_slots: 1
fallback_liquid_largecap_trigger_below: 2
liquid_largecap_min_gain_pct: 0.004
liquid_largecap_max_gain_pct: 0.015
liquid_largecap_min_confirmation_return_pct: 0.0005
liquid_largecap_min_entry_dollar_volume: 50000000.0
liquid_largecap_min_avg_dollar_vol_30d: 2000000000.0
liquid_largecap_max_entropy_20d: 0.87
universe:
source: broad
min_price: 10.0
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
pre_screen_threshold: 0.02
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday
verbose: false

@ -1,116 +0,0 @@
_meta:
id: 29
name: "ORB Gainers V23 Safe"
status: experimental
parent: orb_gainers_v23
description: >
V23 파생 전략 — "안전 투자자" 버전. 수익률을 희생해서 손실을 최소화하는 것이 목표.
V23 대비 5가지 방향으로 보수화:
1. 레짐 필터 강화: QQQ 갭 0.15% → 0.30% (더 강한 상승 장세만 진입)
2. 진입 품질 상향: min_rvol 1.5→2.0, min_candidate_breadth 0.60→0.70
3. 포지션 크기 축소: risk_per_trade 5%→3%, max_simultaneous 3→2
4. 손실 governor 강화: rolling_loss -7%→-3%, drawdown_governor 2.5%→1.5%
5. 일일 손실 컷: daily_max_loss 5%→3%, max_stops_per_day 5→3
streak_sizing 비활성화 (승리 시 포지션 키우지 않음 — 안전 우선)
strategy_mode: orb
orb_strategy:
engine_family: gainers_leader
live_readiness: experimental
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
# === SAFE CHANGE: higher rvol requirement (was 1.5) ===
min_rvol: 2.0
# === SAFE CHANGE: slightly higher gap floor (was 0.02) ===
min_abs_gap_pct: 0.025
min_premarket_dollar_vol: 1500000
max_candidates: 20
max_candidates_per_sector: 3
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
# === SAFE CHANGE: higher breadth requirement (was 0.60) ===
min_candidate_breadth: 0.70
# === SAFE CHANGE: stronger QQQ regime required (was 0.0015 = 0.15%) ===
market_regime_spy_threshold: 0.003
market_regime_ticker: QQQ
rolling_loss_days: 7
# === SAFE CHANGE: stop much sooner on bad streaks (was -0.07) ===
rolling_loss_threshold: -0.03
# === SAFE CHANGE: max 2 simultaneous positions (was 3) ===
max_simultaneous_entries: 2
min_breakout_rel_vol: 1.2
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
# === SAFE CHANGE: smaller position risk (was 0.05) ===
risk_per_trade_pct: 0.03
max_position_pct: 0.70
# === SAFE CHANGE: cut daily losses sooner (was 0.05) ===
daily_max_loss_pct: 0.03
# === SAFE CHANGE: stop the day after 3 stops (was 5) ===
max_stops_per_day: 3
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
# === SAFE CHANGE: tighter portfolio DD governor (was 0.025) ===
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.30
# === SAFE CHANGE: no streak sizing boost (was bonus=0.70, max=2.5) ===
streak_sizing_win_bonus: 0.0
streak_sizing_max: 1.0
universe:
source: midlarge
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday_orb
verbose: false

@ -1,124 +0,0 @@
_meta:
id: 30
name: "ORB Gainers V23 Safe v2"
status: validated
parent: orb_gainers_v23
description: >
V23 파생 전략 — "안전 투자자" v2. v1(+5.16%, DD -13.49%)보다 DD를 줄이는 것이 목표.
핵심 발견 (v1 분석):
- V23 손실일의 QQQ 갭: +0.3%~+3.4% → QQQ 임계값 강화는 효과 없음
- 손실은 QQQ 방향이 아닌 개별 종목 실패에서 발생
- DD는 손실 클러스터(Oct/Sep 2025)에서 집중 발생
v2 접근법:
1. Rolling loss governor 강화: 손실 직후 즉시 거래 중단 (-2% threshold)
2. Partial exit 활성화: 1R(0.75ATR) 도달시 50% 이익 실현 → 많은 거래를 "무조건 수익"으로
3. 포지션 축소: risk 5%→2% (손실 기회당 절대액 감소)
4. 동시 포지션: 3→2 (손실 클러스터링 방지)
5. QQQ 레짐: 유지 (효과 없음이 증명됨 — 더 강화해도 소용없음)
6. Streak sizing 비활성화 (안전 우선)
200d 검증 결과 (2025-07-03 → 2026-04-20):
- 수익: +36.67% (V23 +109.32% 대비)
- Max DD: -11.54% (고점 대비, 그러나 시작 자본 이하 0일!)
- Sharpe: 2.24
- 시작 자본($10k) 이하: 0일 (최저점 $10,017 on 2025-07-09)
- 최악의 하루: -$344 (V23 -$981 대비)
- 거래일: 49/200, 거래: 154건
strategy_mode: orb
orb_strategy:
engine_family: gainers_leader
live_readiness: experimental
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
max_candidates: 20
max_candidates_per_sector: 3
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 5
# === KEY CHANGE: stop IMMEDIATELY after $200 loss (was -7%) ===
rolling_loss_threshold: -0.02
# === CHANGE: max 2 simultaneous (was 3) ===
max_simultaneous_entries: 2
min_breakout_rel_vol: 1.2
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
# === KEY CHANGE: lock in 50% at 1R (was disabled at 99R) ===
partial_exit_at_r: 1.0
partial_exit_pct: 0.50
# === CHANGE: smaller per-trade risk (was 0.05) ===
risk_per_trade_pct: 0.02
max_position_pct: 0.70
# === CHANGE: tighter daily loss cut (was 0.05) ===
daily_max_loss_pct: 0.02
max_stops_per_day: 3
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
# === CHANGE: tighter portfolio governor (was 0.025) ===
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50
# === CHANGE: no streak sizing (was bonus=0.70, max=2.5) ===
streak_sizing_win_bonus: 0.0
streak_sizing_max: 1.0
universe:
source: midlarge
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday_orb
verbose: false

@ -1,112 +0,0 @@
_meta:
id: 31
name: "ORB Gainers V23 Safe v3"
status: experimental
parent: orb_gainers_v23_safe_v2
description: >
V23 Safe v3 — "안전 투자자" 최적화.
v2 분석 결과 (200d):
- +36.67%, DD -11.54%, 시작 자본 이하: 0일 (최저 $10,017)
- Max DD 원인: Nov-Dec 2025 손실 클러스터
11/21(-$321) 후 rolling window(5일)가 만료되어 12월에 다시 거래 시작
→ COHR, TSLA, LITE, VST, CYTK 등 연속 손실
v3 변경:
1. rolling_loss_days: 5→10 (손실 기억 기간 연장 → Nov 손실 후 Dec 재진입 방지)
2. rolling_loss_threshold: -0.02→-0.015 (더 빠른 중단: $150 누적 손실시 정지)
3. 나머지는 v2 동일 (partial_exit@1R, risk=2%, max_entries=2)
목표: 고점 대비 DD를 -8% 이하로 줄이면서 시작 자본 이하 0일 유지
strategy_mode: orb
orb_strategy:
engine_family: gainers_leader
live_readiness: experimental
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
max_candidates: 20
max_candidates_per_sector: 3
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
# === KEY CHANGE: longer loss memory (was 5) ===
rolling_loss_days: 10
# === KEY CHANGE: stop sooner — $150 loss triggers pause (was -0.02) ===
rolling_loss_threshold: -0.015
max_simultaneous_entries: 2
min_breakout_rel_vol: 1.2
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
# 50% partial exit at 1R (lock in gains early)
partial_exit_at_r: 1.0
partial_exit_pct: 0.50
risk_per_trade_pct: 0.02
max_position_pct: 0.70
daily_max_loss_pct: 0.02
max_stops_per_day: 3
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50
streak_sizing_win_bonus: 0.0
streak_sizing_max: 1.0
universe:
source: midlarge
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday_orb
verbose: false

@ -1,107 +0,0 @@
_meta:
id: 37
name: "ORB Gainers V23 Safe v9"
status: validated_200d_only
parent: orb_gainers_v23_safe_v8
description: >
V23 Safe v9 — v8 + streak_sizing_win_bonus: 0.70 (V23 level streak sizing).
VALIDATED champion of the Safe family on 200d window ONLY (2026-04-21).
주의: 400d에서는 V23이 모든 지표에서 완전히 우월 — +146% vs +101%, DD -13.7% vs -17.2%.
v9는 200d 단기 보수적 대안으로만 유효. 실전 배포 기준은 V23.
Safe v8 결과: +82.36%, DD -7.72%, Sharpe 3.14 — V23 Sharpe(3.01)보다 높고 DD는 5pp 낮음.
단, 수익은 V23(+109.32%)보다 27pp 낮음. 차이 원인: V23의 streak sizing(win_bonus=0.70).
V23에서 streak_sizing은 핵심 수익 증폭기 (V19→V21 승진에 기여).
v9 가설: v8 safe mechanisms(partial_exit + rolling_loss-2% + max_sim=2) + V23의
streak_sizing(0.70) = +100%+ 수익 AND DD < V23 -12.91%?
200d 결과 (2025-07-03→2026-04-20): +101.25%, DD -7.62%, WR 58.06%, Sharpe 3.34
worst_day -$324, trade_days 46/200, 124 trades.
400d 결과 (2024-09-13→2026-04-20): +101.01%, DD -17.20%, WR 57.08%, Sharpe 1.99
profit_factor 1.77, worst_day -6.11%, 226 trades, 93 trade days.
400d gate: DD -17.20% ≤ -18% ✓ AND return +101% ≥ +90% ✓ → PROMOTED.
V23 대비 (200d): DD -5.29pp 개선 (-7.62% vs -12.91%); Sharpe +0.33 우위;
수익은 -8pp 낮음 (-101.25% vs +109.32%).
*** 2026-04-21 UPDATE: V23 TRUE 400d result confirmed with correct pipeline ***
V23 400d TRUE: +146.09%, DD -13.66%, Sharpe 2.33 (vs v9: +101.01%, DD -17.20%, Sharpe 1.99)
V23 STRICTLY DOMINATES Safe v9 on 400d in return (+45pp), DD (+3.5pp better), and Sharpe.
"Risk-adjusted superior" claim is ONLY valid on 200d window. On 400d, V23 is also safer.
V23 is the absolute champion. v9 remains valid as 200d conservative alternative only.
strategy_mode: orb
orb_strategy:
engine_family: gainers_leader
live_readiness: experimental
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
max_candidates: 20
max_candidates_per_sector: 3
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: -0.02
max_simultaneous_entries: 2
min_breakout_rel_vol: 1.2
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 1.0
partial_exit_pct: 0.50
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.02
max_stops_per_day: 3
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50
# === KEY CHANGE: enable streak sizing (V23 level) ===
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe:
source: midlarge
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday_orb
verbose: false

@ -1,16 +1,13 @@
_meta:
id: 34
name: "ORB Gainers V23 Safe v6"
status: experimental
parent: orb_gainers_v23_safe_v4
id: 105
name: "ORB Gainers V24.1 Candidate Entry Cap"
status: research
live_readiness: experimental
parent: orb_gainers_v24_quality_overlay
description: >
V23 Safe v6 — v4 + risk_per_trade_pct 2%→3%.
Safe v5 (atr×0.50) 실패: +39.11% 수익이지만 DD -13.81% (v4 -11.01% 대비 +2.8pp 악화), worst_day -$509.
Hypothesis: atr 조정이 아닌 per-trade risk 증가가 더 효율적.
v6 가설: 2%→3% risk 증가 시 partial_exit + rolling_loss + 2-simultaneous 안전장치가
DD를 V23 기준(-12.91%) 이하로 유지하면서 수익을 +50~55%로 끌어올릴 수 있는가?
Narrow candidate for V24.1. Keeps V24 unchanged except reducing
max_simultaneous_entries from 3 to 2 to lower correlated 09:35-10:15
burst risk without altering signal ranking or stop logic.
strategy_mode: orb
@ -19,14 +16,18 @@ orb_strategy:
live_readiness: experimental
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
@ -35,42 +36,51 @@ orb_strategy:
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: -0.02
rolling_loss_threshold: -0.07
max_simultaneous_entries: 2
min_breakout_rel_vol: 1.2
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
weight_obv_slope: 0.05
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 1.0
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
# === KEY CHANGE: 2%→3% per-trade risk ===
risk_per_trade_pct: 0.03
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.02
max_stops_per_day: 3
daily_max_loss_pct: 0.05
max_stops_per_day: 5
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50
streak_sizing_win_bonus: 0.0
streak_sizing_max: 1.0
drawdown_governor_threshold: 0.025
drawdown_governor_min_scale: 0.30
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe:
source: midlarge

@ -0,0 +1,108 @@
_meta:
id: 106
name: "ORB Gainers V24.1 Candidate Loss Cap 10%"
status: research
live_readiness: experimental
parent: orb_gainers_v24_quality_overlay
description: >
Narrow candidate for V24.1. Keeps V24 signal logic unchanged and adds
single_trade_loss_cap_pct: 0.10 to trim only the most aggressive
streak-sized exposures.
Rationale:
- V24 keeps streak_sizing_win_bonus: 0.70, streak_sizing_max: 2.5
- With risk_per_trade_pct: 0.05, a fully boosted trade can risk 12.5%
of initial capital, which is structurally misaligned with
daily_max_loss_pct: 0.05
- single_trade_loss_cap_pct: 0.10 caps only the extreme tail
(2.5x -> 2.0x max effective sizing) while preserving normal-day behavior
strategy_mode: orb
orb_strategy:
engine_family: gainers_leader
live_readiness: experimental
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
max_candidates: 20
max_candidates_per_sector: 3
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: -0.07
max_simultaneous_entries: 3
min_breakout_rel_vol: 1.2
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
weight_obv_slope: 0.05
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.05
max_stops_per_day: 5
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.025
drawdown_governor_min_scale: 0.30
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
single_trade_loss_cap_pct: 0.10
universe:
source: midlarge
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday_orb
verbose: false

@ -1,17 +1,13 @@
_meta:
id: 36
name: "ORB Gainers V23 Safe v8"
status: experimental
parent: orb_gainers_v23_safe_v7
id: 101
name: "ORB Gainers V24.1 Candidate W0.02"
status: research
live_readiness: experimental
parent: orb_gainers_v24_quality_overlay
description: >
V23 Safe v8 — v7 + risk_per_trade_pct 4%→5% (V23 level).
Risk sweep trend (2026-04-21):
2%→+36.78% DD-11.01% SR2.18 / 3%→+63.84% DD-8.90% SR2.79 / 4%→+75.63% DD-8.61% SR3.02
모든 메트릭이 단조 개선! V23(5% risk)은 +109.32% DD-12.91% SR3.01.
v8 가설: V23 risk(5%) + safe mechanisms(partial_exit/rolling_loss-2%/max_sim=2)이 V23보다
높은 Sharpe와 낮은 DD로 유사한 수익을 낼 수 있는가? (V23에서 aggressive 파라미터만 제거)
Research candidate for V24.1. Retains the V24 OBV overlay but reduces
weight_obv_slope from 0.05 to 0.02 to test whether the current engine
prefers a lighter accumulation bias.
strategy_mode: orb
@ -20,14 +16,18 @@ orb_strategy:
live_readiness: experimental
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
@ -36,42 +36,51 @@ orb_strategy:
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: -0.02
max_simultaneous_entries: 2
rolling_loss_threshold: -0.07
max_simultaneous_entries: 3
min_breakout_rel_vol: 1.2
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
weight_obv_slope: 0.02
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 1.0
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
# === KEY CHANGE: 4%→5% per-trade risk (V23 level) ===
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.02
max_stops_per_day: 3
daily_max_loss_pct: 0.05
max_stops_per_day: 5
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50
streak_sizing_win_bonus: 0.0
streak_sizing_max: 1.0
drawdown_governor_threshold: 0.025
drawdown_governor_min_scale: 0.30
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe:
source: midlarge

@ -1,21 +1,13 @@
_meta:
id: 32
name: "ORB Gainers V23 Safe v4"
status: validated
parent: orb_gainers_v23_safe_v2
id: 102
name: "ORB Gainers V24.1 Candidate W0.03"
status: research
live_readiness: experimental
parent: orb_gainers_v24_quality_overlay
description: >
V23 Safe v4 — v2 + rolling_loss_days 5→7.
v2 max DD 원인: Nov 21 손실 후 rolling window 5일 만료로 Dec 2에 재진입 허용.
7일 window로 Nov 손실이 Dec 2까지 기억됨 → Dec 재진입 방지.
200d 검증 결과 (2025-07-03 → 2026-04-20):
- 수익: +36.78% (v2 +36.67% 대비 +0.11pp)
- Max DD: -11.01% (v2 -11.54% 대비 개선)
- Win Rate: 56.77%
- Sharpe: 2.18 (v2 2.24 대비 미소 하락)
- 최악의 하루: -$339 (v2 -$344 대비)
- 거래일: 48/200, 거래: 155건
Research candidate for V24.1. Retains the V24 OBV overlay but reduces
weight_obv_slope from 0.05 to 0.03 to test whether the current engine
prefers a lighter accumulation bias.
strategy_mode: orb
@ -24,14 +16,18 @@ orb_strategy:
live_readiness: experimental
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
@ -40,41 +36,51 @@ orb_strategy:
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: -0.02
max_simultaneous_entries: 2
rolling_loss_threshold: -0.07
max_simultaneous_entries: 3
min_breakout_rel_vol: 1.2
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
weight_obv_slope: 0.03
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 1.0
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
risk_per_trade_pct: 0.02
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.02
max_stops_per_day: 3
daily_max_loss_pct: 0.05
max_stops_per_day: 5
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50
streak_sizing_win_bonus: 0.0
streak_sizing_max: 1.0
drawdown_governor_threshold: 0.025
drawdown_governor_min_scale: 0.30
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe:
source: midlarge

@ -1,18 +1,13 @@
_meta:
id: 35
name: "ORB Gainers V23 Safe v7"
status: experimental
parent: orb_gainers_v23_safe_v6
id: 103
name: "ORB Gainers V24.1 Candidate W0.04"
status: research
live_readiness: experimental
parent: orb_gainers_v24_quality_overlay
description: >
V23 Safe v7 — v6 + risk_per_trade_pct 3%→4%.
Safe v6 breakthrough (2026-04-21): risk 2%→3% dramatically improved ALL metrics:
+63.84% return (+27pp vs v4), DD -8.90% (better than v4's -11.01%), WR 57.46%, Sharpe 2.79.
Counterintuitive: higher risk → better DD%. Cause: larger wins elevate peak equity faster,
same absolute $ drawdowns = smaller % DD.
v7 가설: risk 3%→4% 시 수익 추가 향상 (≥+85%) 하면서 DD는 V23(-12.91%) 이하 유지?
V23 risk=5%일 때 +109.32%이므로, 4%는 중간 지점 탐색.
Research candidate for V24.1. Retains the V24 OBV overlay but reduces
weight_obv_slope from 0.05 to 0.04 to test whether the current engine
prefers a lighter accumulation bias.
strategy_mode: orb
@ -21,14 +16,18 @@ orb_strategy:
live_readiness: experimental
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
@ -37,42 +36,51 @@ orb_strategy:
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: -0.02
max_simultaneous_entries: 2
rolling_loss_threshold: -0.07
max_simultaneous_entries: 3
min_breakout_rel_vol: 1.2
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
weight_obv_slope: 0.04
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 1.0
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
# === KEY CHANGE: 3%→4% per-trade risk ===
risk_per_trade_pct: 0.04
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.02
max_stops_per_day: 3
daily_max_loss_pct: 0.05
max_stops_per_day: 5
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50
streak_sizing_win_bonus: 0.0
streak_sizing_max: 1.0
drawdown_governor_threshold: 0.025
drawdown_governor_min_scale: 0.30
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe:
source: midlarge

@ -1,124 +0,0 @@
_meta:
id: 29
name: "ORB Gainers V24 LossCap"
description: >
[DOCUMENTED FAILURE — NOT PROMOTED]
V23 → V24 via single change: single_trade_loss_cap_pct=0.05
Result: +92.6% (200d), WR 58.0%, Sharpe 3.00, DD -9.0% — return -55.5pp vs V23.
Also tested cap=0.10 (-33pp) and streak_max=2.0 (-36pp). All failed.
Root cause: streak sizing amplifies wins AND losses symmetrically.
Capping losses also caps wins proportionally → unavoidable trade-off.
V23 HIMS -4.63% loss is a designed -1R at streak×2.4 — not a fixable bug.
Original hypothesis: streak_sizing_max=2.5 creates structural misalignment where a single -1R trade
can consume 2.5× daily_max_loss_pct worth of capital (e.g. 2.4× streak → $1,200 loss on
$10k initial, while daily_max_loss_pct=0.05 intent is $500 max).
Fix: after all sizing boosts (governor + streak + rolling WR), clamp sizing_capital so
that risk_per_trade_pct × sizing_capital ≤ single_trade_loss_cap_pct × initial_capital.
With risk_per_trade_pct=0.05 and cap=0.05: max sizing = $10,000 = initial_capital.
Example (2026-04-17 HIMS):
Without cap: streak 2.4× → sizing $24k → risk $1,200 → loss -4.63% of portfolio
With cap: sizing clamped to $10k → risk $500 → loss ~-1.92% of portfolio
Trade-off: streak bonus is capped for loss protection, but also for wins (smaller positions
on winning streaks). Net effect on WR and return is the test hypothesis.
Validation:
V23 200d TRUE BASELINE: +109.32%, WR 58.1%, DD -12.91%, Sharpe 3.01, 160 trades
Gates (200d): return ≥ +104%, single max loss ≤ $500, 2026-04-17 daily ≤ -2.5%
Gates (400d): return ≥ +88%, WR ≥ 52%, DD ≤ -24%
strategy_mode: orb
orb_strategy:
engine_family: gainers_leader
live_readiness: live_ready
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
max_candidates: 20
max_candidates_per_sector: 3
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: -0.07
max_simultaneous_entries: 3
min_breakout_rel_vol: 1.2
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.05
max_stops_per_day: 5
exit_minutes_before_close: 5
# === CHANGE: cap single-trade loss at 5% of initial_capital (= $500 on $10k) ===
# Prevents streak boost from amplifying -1R losses beyond daily_max_loss intent.
single_trade_loss_cap_pct: 0.05
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.025
drawdown_governor_min_scale: 0.30
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe:
source: midlarge
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday_orb
verbose: false

@ -1,166 +0,0 @@
_meta:
id: 35
name: "ORB Pullback V1"
status: documented_failure
description: >
[DOCUMENTED FAILURE — NOT PROMOTED]
Phase 1 attempt: V23's gainers_leader candidate pool + pullback continuation entry.
Diagnostic result (200d):
- Base pullback (no quality gates): 133 trades, WR 25.6%, return -10.14%
- All quality filters (impulse_min, depth_max, vol_contraction, vwap_floor): negative selection
Adding each filter either left WR unchanged or DECREASED it (min 13.3%)
- V23 immediate-entry same pool: WR 61.5% (+36pp gap)
Root cause: V23's candidate pool selects stocks that immediately continue after breakout.
Waiting for a pullback negatively selects against V23's edge — catches the stocks
that stall (typically failing breakouts). All 5 quality filters showed negative selection;
this is NOT a tunable parameter problem but a structural incompatibility.
Conclusion: orb_pullback_v1 on V23 candidates = negative-EV. Pivoting to vwap_reclaim_v1.
Original Phase 1 multi-engine hypothesis:
Engine: orb_pullback_v1 (independent engine_family, not a V23 variant).
Same candidate universe and scoring as V23 (gainers_leader candidate pool).
Different entry: instead of immediate ORB breakout, waits for:
1. Post-breakout impulse peak within 9:40-9:55 ET window
2. Pullback of 25-50% of impulse move (with volume contraction)
3. VWAP floor check (pullback can't breach VWAP by >0.3%)
4. Continuation bar: green + above pullback extreme
Stop: pullback_low (structural) rather than pure ATR.
Gates (standalone 200d):
trades >= 50, WR >= 50%, total_return >= 0%, max_dd >= -20%
Portfolio gates (combined with V23, 600d):
trade_overlap <= 20%, daily_pnl_corr <= 0.30,
combined_600d_dd improvement >= 5pp vs V23 standalone (-51.25%)
Evaluation: not standalone — portfolio contribution to V23 is the target metric.
Run via apps/intraday_bt/portfolio_report.py for combined analysis.
Initial capital intentionally lower ($4000) for composite sleeve weighting (40%
of a hypothetical $10k combined portfolio). For standalone comparison use $10000.
strategy_mode: orb
orb_strategy:
engine_family: orb_pullback_v1
live_readiness: research_only
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 45
allow_doji_breakout: true
allow_red_to_green_breakout: true
# === Candidate filters identical to V23 (gainers_leader pool) ===
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
max_candidates: 20
max_candidates_per_sector: 3
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: -0.07
max_simultaneous_entries: 3
min_breakout_rel_vol: 1.2
# === Scoring weights identical to V23 ===
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
# === Stop / exit parameters (base ATR same as V23) ===
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.05
max_stops_per_day: 5
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.025
drawdown_governor_min_scale: 0.30
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
# === Pullback entry — core engine feature ===
pullback_entry: true
pullback_max_bars: 8
pullback_min_retracement_pct: 0.25
pullback_stop_at_low: true
# === Extended pullback controls (orb_pullback_v1) ===
# Impulse peak must form by 9:55 ET (25 min from open)
pullback_impulse_window_end_min: 25
# Impulse must move at least 0.4× ATR above breakout level
pullback_impulse_min_move_atr: 0.4
# Pullback depth: 25% to 60% of impulse move
pullback_depth_max_pct: 0.60
# Pullback phase must have lower avg volume than impulse phase (70% threshold)
pullback_volume_contraction_ratio: 0.70
# Abort if pullback penetrates VWAP by more than 0.3%
pullback_vwap_floor: true
pullback_vwap_floor_tolerance_pct: 0.003
# Stop: structural pullback low (not VWAP — cleaner for initial testing)
pullback_stop_mode: pullback_low
pullback_stop_vwap_buffer_pct: 0.002
# Reclaim bar must have 1.2× average post-ORB bar volume
pullback_reclaim_confirm_rel_vol: 1.2
universe:
source: midlarge
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday_orb
verbose: false

@ -1,123 +0,0 @@
_meta:
id: 36
name: "VWAP Reclaim V1"
status: aborted
aborted_date: "2026-04-21"
aborted_reason: >
Same gainers pool as V23 → 57.3% trade overlap (fails ≤20% gate). Orthogonal high-gap
variant (id:37) reduced overlap to 16.5% but daily PnL corr=0.394 (fails ≤0.30 gate).
Root cause: correlation is regime-driven (both long-momentum, both triggered by same
QQQ-positive days) — not fixable by any stock-selection filter. VWAP stop mode broke
position sizing (entry ≈ VWAP → stop_distance ≈ 0 → overleverage → WR 16%). Baseline
+13.82%/WR 48% does not beat V23 (+95.67%/WR 58%). Not a valid diversifier.
description: >
Phase 2 / diagnostic pass: V23's gainers_leader candidate pool + minimal VWAP reclaim entry.
Engine: vwap_reclaim_v1 — same pre-market candidates as V23, but instead of entering
on the 9:30-9:35 ORB breakout, scans from 10:00 ET (30 min from open) for the first
bar that closes above the running session VWAP.
Hypothesis: catalyst stocks spend the first 20-30 min in price discovery.
A VWAP close-above in the 10:00-11:30 window signals committed direction.
Diagnostic purpose: determine if V23's candidate pool structurally supports
a late-morning entry (vs. negative selection like orb_pullback_v1 showed).
Gate: base WR ≥ 45% (vs. pullback's 25.6%). If fails → wrong pool.
This config uses zero quality gates (no tightness, no base, no vol filter) —
purely "first bar closing above VWAP in [10:00, 11:30]".
strategy_mode: orb
orb_strategy:
engine_family: vwap_reclaim_v1
live_readiness: research_only
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 120 # not used for entry, but sets the timeout context
allow_doji_breakout: true
allow_red_to_green_breakout: true
# === Candidate filters identical to V23 (gainers_leader pool) ===
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000
max_candidates: 20
max_candidates_per_sector: 3
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: 0.04
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: -0.07
max_simultaneous_entries: 3
min_breakout_rel_vol: null # disabled — VWAP reclaim bar is late-morning, not ORB
# === Scoring weights identical to V23 ===
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
# === Stop / exit parameters (base ATR same as V23) ===
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.05
max_stops_per_day: 5
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.025
drawdown_governor_min_scale: 0.30
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
# === VWAP reclaim window ===
vwap_reclaim_window_start_min: 30 # 10:00 ET
vwap_reclaim_window_end_min: 120 # 11:30 ET
universe:
source: midlarge
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday_orb
verbose: false

@ -1,119 +0,0 @@
_meta:
id: 37
name: "VWAP Reclaim V1 High-Gap"
status: aborted
aborted_date: "2026-04-21"
aborted_reason: >
Orthogonal gap pool (≥4%) reduced trade overlap to 16.5% (passes ≤20%) but daily PnL
corr=0.394 (fails ≤0.30). Correlation is purely regime-driven — both engines are
long-momentum triggered by QQQ-positive days. Worst-20% day combined PnL is WORSE than
V23 standalone (amplifies drawdowns). Baseline +13.82%/WR 48% fails G1 (WR<50%) and G3
(corr). Phase 3 must use a directionally different approach to break regime correlation.
description: >
Diagnostic pass 2: high-gap pool (gap ≥ 4%) + VWAP reclaim entry.
Problem with same-pool vwap_reclaim_v1: 57% trade overlap with V23 (same stocks, same days).
Hypothesis: V23 uses max_gap_pct=0.04 (2-4% gap). Gap > 4% stocks are orthogonal by
construction — V23 never touches them. These higher-gap stocks often exhibit genuine
price discovery (initial dump then reclaim) rather than immediate continuation.
Gate: base WR ≥ 45% AND portfolio overlap ≤ 30%.
strategy_mode: orb
orb_strategy:
engine_family: vwap_reclaim_v1
live_readiness: research_only
orb_minutes: 5
sim_bar_minutes: 5
entry_direction: long_only
order_timeout_minutes: 120 # not used for entry, but sets the timeout context
allow_doji_breakout: true
allow_red_to_green_breakout: true
# === Candidate filters identical to V23 (gainers_leader pool) ===
min_price: 10.0
min_avg_dollar_volume: 25000000
min_atr_14: 0.50
min_atr_pct: 0.04
min_rvol: 1.5
min_abs_gap_pct: 0.04 # high-gap pool: ≥4% gap (orthogonal to V23's 2-4% range)
min_premarket_dollar_vol: 1500000
max_candidates: 20
max_candidates_per_sector: 3
min_candidates_to_trade: 1
ticker_cooldown_days: 0
max_gap_pct: null # no upper cap — allow all high-gap stocks
min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ
rolling_loss_days: 7
rolling_loss_threshold: -0.07
max_simultaneous_entries: 3
min_breakout_rel_vol: null # disabled — VWAP reclaim bar is late-morning, not ORB
# === Scoring weights identical to V23 ===
weight_rvol: 0.35
weight_gap: 0.20
weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0
weight_momentum: 0.15
# === Stop / exit parameters (base ATR same as V23) ===
atr_stop_multiplier: 0.75
breakeven_at_r: 1.0
trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 99.0
partial_exit_pct: 0.50
risk_per_trade_pct: 0.05
max_position_pct: 0.70
daily_max_loss_pct: 0.05
max_stops_per_day: 5
exit_minutes_before_close: 5
slippage_bps: 5.0
initial_capital: 10000
compound_returns: false
daily_budget_reset: true
settlement_days: 1
drawdown_governor_threshold: 0.025
drawdown_governor_min_scale: 0.30
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
# === VWAP reclaim window ===
vwap_reclaim_window_start_min: 30 # 10:00 ET
vwap_reclaim_window_end_min: 120 # 11:30 ET
vwap_reclaim_require_prior_dip: false
vwap_reclaim_min_clearance_pct: 0.0 # no clearance filter — enter on first close above VWAP
vwap_reclaim_stop_mode: vwap # structural stop: distance to VWAP floor
vwap_reclaim_stop_vwap_buffer_pct: 0.002 # stop at VWAP × (1 - 0.2%)
universe:
source: midlarge
backtest:
start_date: null
end_date: null
lookback_trading_days: 200
cache:
enabled: true
dir: data/cache/intraday
output:
dir: runs/intraday_orb
verbose: false

@ -0,0 +1,21 @@
sweep:
use_liquid_cluster_engine: [false, true]
use_sector_etf_sleeve: [false, true]
liquid_cluster_capital_fraction: [0.15]
liquid_cluster_max_positions: [1]
liquid_cluster_max_positions_per_sector: [1]
liquid_cluster_min_members: [2]
liquid_cluster_min_gain_pct: [0.015]
liquid_cluster_max_gain_pct: [0.04]
liquid_cluster_min_confirmation_return_pct: [0.005]
liquid_cluster_min_entry_dollar_volume: [40000000.0]
liquid_cluster_min_avg_dollar_vol_30d: [250000000.0]
liquid_cluster_max_avg_dollar_vol_30d: [2000000000.0]
liquid_cluster_min_volume_ratio_14d: [0.04]
liquid_cluster_max_entropy_20d: [0.86]
liquid_cluster_min_sector_avg_confirmation_return_pct: [0.005]
liquid_cluster_min_sector_total_entry_dollar_volume: [100000000.0]
liquid_cluster_require_special_liquidity_gate: [true]
sector_etf_capital_fraction: [0.10]
sector_etf_max_positions: [1]
sector_etf_min_sector_score: [0.20]

@ -0,0 +1,7 @@
sweep:
max_gap_zscore_20d:
- null
- 1.5
- 2.0
- 2.5
- 3.0

@ -191,6 +191,101 @@
"prior_catalyst_type_diversity_60d"
]
},
"broad-liquid-long-v1_bucketfix_full_audit_canonical": {
"purpose": "broad_universe_experiment",
"refresh_policy": "auto_full_rebuild",
"universe_profile": "broad-liquid-long-v1",
"label_version": "label-2.0.0",
"start_date": "2022-03-01",
"end_date": null,
"feature_versions": [
"market_v1",
"event_v1",
"text_v1",
"earnings_surprise_v1"
],
"feature_sets": [
"base",
"earnings_history",
"peer_surprise",
"catalyst_persistence",
"tier2",
"tier3",
"technical",
"macro",
"prior_drift"
],
"enrichment_steps": [
"base_export",
"earnings_history_enrich",
"peer_surprise_enrich",
"catalyst_persistence_enrich",
"tier2_enrich",
"tier3_enrich",
"technical_enrich",
"macro_enrich",
"prior_drift_enrich"
],
"expected_feature_columns": [
"pre_event_hurst_60d",
"pre_event_entropy_60d",
"pre_event_short_ratio",
"pre_event_sector_momentum_20d",
"pre_event_ou_theta_60d",
"pre_event_gravitational_pull",
"pre_event_market_temperature",
"pre_event_volatility_20d",
"pre_event_rsi_14",
"pre_event_bb_position",
"pre_event_obv_slope_20d",
"macro_vix",
"macro_hy_spread",
"macro_t10y2y",
"prior_event_fwd5d",
"lm_positive_pct",
"lm_negative_pct",
"lm_net_sentiment",
"lm_uncertainty_pct",
"lm_word_count",
"reported_eps",
"estimated_eps",
"earnings_surprise_pct",
"earnings_beat",
"sue_lag_1_pct",
"sue_lag_2_pct",
"sue_lag_3_pct",
"sue_lag_4_pct",
"sue_lag_5_pct",
"sue_lag_6_pct",
"sue_lag_7_pct",
"sue_lag_8_pct",
"sue_lag_9_pct",
"sue_lag_10_pct",
"sue_lag_11_pct",
"sue_lag_12_pct",
"sue_hist_mean_4q",
"sue_hist_mean_8q",
"sue_hist_mean_12q",
"sue_hist_pos_rate_4q",
"sue_hist_pos_rate_12q",
"sue_hist_latest_pct",
"sue_hist_streak_pos",
"sector",
"peer_sector_event_count_365d",
"peer_sector_surprise_median_365d",
"peer_sector_surprise_mean_365d",
"peer_sector_surprise_pos_rate_365d",
"peer_relative_surprise_pct_365d",
"peer_sector_sue_hist_mean_4q_median_365d",
"peer_sector_sue_hist_mean_4q_mean_365d",
"peer_relative_sue_hist_mean_4q_365d",
"peer_sector_sue_hist_pos_rate_4q_mean_365d",
"prior_catalyst_count_20d",
"prior_catalyst_count_60d",
"prior_catalyst_type_diversity_20d",
"prior_catalyst_type_diversity_60d"
]
},
"midlarge-liquid-long-v1-oot-2020-2021_canonical": {
"purpose": "oot",
"refresh_policy": "manual_only",

@ -0,0 +1,174 @@
# Leader Intraday Momentum Workflow
이 문서는 `Leader Intraday Momentum High WR Intraday First` 전략을 개발하고
검증하는 운영 기준이다. 기존 `orb_gainers`와 달리 이 전략은 ORB breakout보다
`장 초반 top leader follow-through`를 직접 매매하는 momentum 전략이다.
## Source Of Truth
정식 전략 파일:
- [leader_intraday_momentum_high_wr_intraday_first.yaml](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml)
공식 검증 경로:
- [apps/intraday_bt/run.py](/Users/yirugi/mycloud/personal/workspace/fithia2/apps/intraday_bt/run.py)
연구 helper나 momentum snapshot 결과는 빠른 탐색용이다. 최종 채택 판단은 반드시
아래 공식 CLI와 웹사이트가 사용하는 동일 경로로 재검증한다.
```bash
python -u -m apps.intraday_bt.run \
--config configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml \
--start 2026-01-02 \
--end 2026-03-31 \
--daily-budget-reset \
--no-compound-returns
```
전략 개발 검증은 `daily_budget_reset + no compound`를 기본으로 한다. 이는 날짜별
edge를 보기 위한 연구 모드이며, 후반 구간의 복리/계좌 규모 효과로 과적합되는
문제를 줄인다. 실전 계좌 결과는 별도로 simple/compound 모드에서 확인한다.
## Current Engine
핵심 구조:
- Universe는 정식 `broad` 3408개 티커를 사용한다.
- Daily seed는 look-ahead 없이 당일 open 이전/entry 시점까지 알 수 있는 feature만 쓴다.
- Intraday-first shortlist를 만든 뒤, 10분 entry와 5분 confirmation으로 재랭킹한다.
- Five-sleeve selection으로 `core`, `volume`, `gap`, `trend`, `blend`를 분리한다.
- Same-day filing catalyst는 additive alpha가 아니라 weak tail exemption 판단에 쓴다.
- Moderate-gap liquid reserve는 sparse day에서만 broad scan이 잡는 liquid follow-through 후보를 보강한다.
- Multi-event liquid overlay는 core basket을 건드리지 않고, broad same-day filing cluster가 확인된 날에만 별도 budget으로 liquid continuation names를 추가한다.
- 손실 방어는 개별 티커 블랙리스트가 아니라 구조 조건만 사용한다.
현재 방어 레이어:
- `weak-event tail defense`: event flag가 있어도 support score가 낮으면 sparse-day 방어 예외로 보지 않는다.
- `low-momentum single-name defense`: single-name day에서 유일한 후보의 morning gain이 낮고 supported event/largecap도 아니면 day budget을 줄인다.
- `soft-day sparse defense`: soft day인데 basket이 1~2개뿐이고 event / liquid large-cap / moderate-gap liquid support가 없으면 day budget을 추가로 줄인다.
- `trailing_stop_pct: -0.07`: hard take-profit 없이 intraday trailing stop으로 큰 downside를 제한한다.
- `loss_containment_score`: WR/DD와 별도로 손실일 평균과 tail loss를 직접 보는 보조 지표다.
## Official Validation Windows
과적합 방지를 위해 2026 Q1만 보지 않고 2025년 분기별 official backtest를 같이 본다.
```bash
python -u -m apps.intraday_bt.run --config configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml --start 2025-01-02 --end 2025-03-31 --daily-budget-reset --no-compound-returns
python -u -m apps.intraday_bt.run --config configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml --start 2025-04-01 --end 2025-06-30 --daily-budget-reset --no-compound-returns
python -u -m apps.intraday_bt.run --config configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml --start 2025-07-01 --end 2025-09-30 --daily-budget-reset --no-compound-returns
python -u -m apps.intraday_bt.run --config configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml --start 2025-10-01 --end 2025-12-31 --daily-budget-reset --no-compound-returns
python -u -m apps.intraday_bt.run --config configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml --start 2026-01-02 --end 2026-03-31 --daily-budget-reset --no-compound-returns
```
Latest official results as of 2026-04-21 after promoting the multi-event liquid overlay into the flagship:
| Window | Result file | Return | WR | Max DD | LC | Trades |
|---|---|---:|---:|---:|---:|---:|
| 2025 Q1 | `runs/intraday/intraday_20260421_220246_34b0dff0.json` | +6.56% | 55.1% | -10.93% | 66.11 | 89 |
| 2025 Q2 | `runs/intraday/intraday_20260421_223436_79a89cb1.json` | +23.23% | 60.0% | -4.31% | 66.80 | 60 |
| 2025 Q3 | `runs/intraday/intraday_20260421_224448_ccd2985b.json` | +6.92% | 49.6% | -6.81% | 64.38 | 135 |
| 2025 Q4 | `runs/intraday/intraday_20260421_222253_6f5a98e0.json` | -5.24% | 41.8% | -15.47% | 65.33 | 122 |
| 2026 Q1 | `runs/intraday/intraday_20260421_221923_2ea8b7f2.json` | +16.14% | 55.4% | -6.16% | 66.33 | 148 |
해석:
- 2025 Q4는 아직 음수라서 전략의 약점 구간이다.
- 다만 multi-event overlay는 2025 Q1/Q2/Q3/Q4 official 창에서는 아예 발화하지 않아, 약한 분기들을 추가로 오염시키지는 않았다.
- 2026 Q1 holdout에서는 2026-01-02 한 번의 broad filing cluster에서만 발화했고, 그 날 `APLD`, `BMNR` 두 개를 추가해 flagship 대비 수익률과 WR을 끌어올렸다.
- 다음 개선은 개별 종목을 외우는 방식이 아니라 Q4 같은 weak regime을 더 잘 감지하는 meta-layer여야 한다.
- `sector thrust` breadth engine은 2026-04-21에 코드로 추가해 실험했지만, 정식 전략에 full enable하면 2026 Q1 holdout이 약 `+21.92% -> +15.98%`까지 악화돼 아직 승격하지 않았다.
## Actual Catalyst Branches
2026-04-21에 `actual catalyst + liquid leader continuation` 방향도 분리 검증했다.
전략 파일:
- `configs/intraday/strategies/leader_intraday_momentum_actual_catalyst_liquid.yaml`
- `configs/intraday/strategies/leader_intraday_momentum_event_day_liquid_hybrid.yaml`
Q1 결과:
| Variant | Result file | Return | WR | Max DD | Trades | Notes |
|---|---|---:|---:|---:|---:|---|
| strict event-only | `runs/intraday/intraday_20260421_205738_1f70d7db.json` | +3.21% | 63.6% | -7.87% | 11 | actual filing catalyst만 거래해서 너무 sparse했다 |
| hybrid event reserve | `runs/intraday/intraday_20260421_210703_5bb723e7.json` | +15.65% | 54.8% | -6.14% | 146 | baseline보다 아주 미세하게 개선됐지만 구조 변화는 작았다 |
| baseline flagship | `runs/intraday/intraday_20260421_211021_30f71c80.json` | +15.59% | 54.8% | -6.19% | 146 | 비교 기준 |
해석:
- strict event-only는 방향성은 맞아도 메인 엔진으로 쓰기엔 너무 희소하다.
- hybrid는 `candidate_allowed_event_types`를 통해 `earnings_release`, `guidance_update`, `material_contract`, `other_material_event`만 event로 인정하게 했지만, Q1 기준으로 baseline 대비 개선폭은 `+0.06%` 수준에 그쳤다.
- 즉, actual catalyst를 reserve/overlay로만 넣는 것만으로는 아직 획기적 변화가 없었다.
- 다음 구조 개선은 `event issuer 자체`를 더 사는 것이 아니라, `event day에 broad scan에서 잡힌 liquid continuation names를 별도 engine/sleeve로 어떻게 승격할지` 쪽이 더 유망하다.
## Multi-Event Liquid Overlay
2026-04-21 최종 승격안은 strict event reserve가 아니라 `overlay-only` 구조였다.
핵심 규칙:
- core basket은 [leader_intraday_momentum_high_wr_intraday_first.yaml](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml)과 동일하게 유지한다.
- filing data는 core rank에 섞지 않는다.
- 대신 `earnings_release`, `guidance_update`, `material_contract`, `other_material_event`, `management_change`, `unknown` 중에서 **2개 이상** same-day contributor가 동시에 보이고, 합산 entry dollar volume이 **$100M 이상**일 때만 overlay를 켠다.
- overlay가 켜진 날에만 day budget의 12%를 써서, base basket 밖의 liquid continuation names를 최대 2개 추가한다.
최종 해석:
- 2025 official 창에서는 overlay가 발화하지 않았고 결과도 거의 그대로 유지됐다.
- 2026 Q1에서는 2026-01-02 하루만 발화했고, `APLD`, `BMNR` 두 개가 추가됐다.
- 이 한 번의 broad event cluster가 `+16.14% / WR 55.4% / DD -6.16%`를 만들었고, 직전 flagship 비교치 `+15.59% / WR 54.8% / DD -6.19%`보다 좋아졌다.
- 즉, 이 overlay는 “매일 조금씩 손대는 additive factor”가 아니라, **희소하지만 설명 가능한 broad event cluster day에만 붙는 post-allocation sleeve**로 이해해야 한다.
## Liquid Continuation Core Experiment
`moderate-gap liquid / liquid large-cap / sector thrust`를 overlay가 아니라
**full core basket engine**으로 승격한 실험도 별도로 진행했다.
- 전략 파일: [leader_intraday_momentum_liquid_continuation_core.yaml](/Users/yirugi/mycloud/personal/workspace/fithia2/configs/intraday/strategies/leader_intraday_momentum_liquid_continuation_core.yaml)
- 코드 변경:
- `momentum_selection_mode: liquid_continuation`
- `candidate_intraday_rank_mode: liquid_continuation`
- same-day `support_score`, `is_liquid_largecap`, `is_moderate_gap_liquid`를 candidate weighted rank에 추가
결과:
- 1차 broad version: [intraday_20260421_235041_f5aeac8a.json](/Users/yirugi/mycloud/personal/workspace/fithia2/runs/intraday/intraday_20260421_235041_f5aeac8a.json)
- `2026 Q1: -6.98%`, `WR 47.7%`, `DD -13.52%`
- stricter version: [intraday_20260421_235327_04e5bda8.json](/Users/yirugi/mycloud/personal/workspace/fithia2/runs/intraday/intraday_20260421_235327_04e5bda8.json)
- `2026 Q1: -0.43%`, `WR 46.8%`, `DD -11.56%`
- walk-forward spot check: [intraday_20260421_235647_93d29530.json](/Users/yirugi/mycloud/personal/workspace/fithia2/runs/intraday/intraday_20260421_235647_93d29530.json)
- `2025 Q1: -2.31%`, `WR 41.8%`, `DD -9.23%`
결론:
- 이 엔진은 실제로 `special liquidity` 이름만 중심으로 고르도록 동작했지만,
**flagship을 대체할 full core engine으로는 아직 edge가 없다.**
- 특히 moderate-gap liquid 정의를 core로 올리면 거래 수는 줄어도 분기 성과가
baseline보다 지속적으로 나빠졌다.
- 따라서 현재 판단은 `liquid continuation`을 full replacement로 승격하지 말고,
**tail replacement / reserve slot / rare-day sleeve** 쪽에만 제한적으로 쓰는 편이 낫다.
## Required Checks Before Keeping A Change
변경을 유지하려면 최소한 아래를 확인한다.
- Unit tests pass:
```bash
pytest -q tests/unit/intraday/test_simulator.py tests/unit/intraday/test_run_helpers.py tests/unit/intraday/test_screener.py tests/unit/intraday/test_metrics.py
```
- 2026 Q1 official result가 무너지지 않는다.
- 2025 Q1/Q2/Q3/Q4 중 한 분기만 좋아지고 다른 분기들이 크게 악화되지 않는다.
- 결과 JSON의 trade diagnostics로 변경이 어떤 구조에 적용됐는지 설명 가능해야 한다.
## Known Bottleneck
분기별 official 검증에서 `Fetching momentum filing catalysts` 단계가 가장 느리다.
현재는 캐시가 있어도 90% 이후 일부 ticker 조회가 오래 걸린다. 전략 검증 자체는
정상 완료되지만, 다음 인프라 개선은 event 조회 범위 축소나 캐시 hit 판정 개선이
우선이다.

@ -39,6 +39,7 @@ _UNIVERSE_PROFILE_MIDLARGE_LIQUID_LONG_V1 = "midlarge-liquid-long-v1"
_UNIVERSE_PROFILE_MIDPLUS_LIQUID_LONG_V1 = "midplus-liquid-long-v1"
_UNIVERSE_PROFILE_MIDWIDE_LIQUID_LONG_V1 = "midwide-liquid-long-v1"
_UNIVERSE_PROFILE_SMALLCAP_LIQUID_LONG_V1 = "smallcap-liquid-long-v1"
_UNIVERSE_PROFILE_BROAD_LIQUID_LONG_V1 = "broad-liquid-long-v1"
_UNIVERSE_PROFILES: dict[str, dict[str, Any]] = {
_UNIVERSE_PROFILE_MIDLARGE_LIQUID_LONG_V1: {
@ -70,6 +71,13 @@ _UNIVERSE_PROFILES: dict[str, dict[str, Any]] = {
"exchange": "NYSE,NASDAQ,AMEX",
"exclude_types": "ETF,FUND,ADR,SPAC",
},
_UNIVERSE_PROFILE_BROAD_LIQUID_LONG_V1: {
"market_cap_min": 300_000_000,
"price_min": 5,
"avg_dollar_volume_20d_min": 3_000_000,
"exchange": "NYSE,NASDAQ,AMEX",
"exclude_types": "ETF,FUND,ADR,SPAC",
},
}

@ -10,6 +10,7 @@ from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from uuid import uuid4
import pyarrow as pa
import pyarrow.parquet as pq
@ -197,7 +198,7 @@ class IntradayCache:
_INTRADAY_NEGATIVE_REASON_KEY: reason.encode(),
})
table = pa.Table.from_pylist([], schema=schema)
tmp = p.with_suffix(".tmp")
tmp = p.with_suffix(f".{uuid4().hex}.tmp")
try:
pq.write_table(table, str(tmp), compression="snappy")
os.replace(str(tmp), str(p))

@ -1142,8 +1142,15 @@ class ORBStrategyParams(BaseModel):
"""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."""
Marks each trading day within this window after qualifying events as
event_flag=True, event_score=1.0. Event types controlled by prior_event_types."""
prior_event_types: list[str] = Field(
default_factory=lambda: ["earnings_release", "guidance_update"]
)
"""DB event types to include in prior_event_lookback_days signal.
Default matches V46 (earnings_release + guidance_update).
Set to ['earnings_release'] for earnings-only variant."""
weight_attention_wiki: float = 0.0
"""Wikipedia attention weight for actual stocks-in-play ranking."""

@ -778,6 +778,9 @@ def write_results(
"sector_scaler": r.sector_scaler,
"tail_risk_scaler": r.tail_risk_scaler,
"is_soft_day": r.is_soft_day,
"event_day_liquid_active": r.event_day_liquid_active,
"event_day_liquid_event_count": r.event_day_liquid_event_count,
"event_day_liquid_total_event_entry_dollar_volume": r.event_day_liquid_total_event_entry_dollar_volume,
}
for r in day_results
],

@ -619,6 +619,7 @@ def momentum_pre_screen_candidates(
require_event_flag = bool(getattr(strategy, "candidate_require_event_flag", False)) if strategy else False
min_event_score = getattr(strategy, "candidate_min_event_score", None) if strategy else None
allowed_event_types = _momentum_allowed_event_types(strategy)
min_wiki_spike = getattr(strategy, "candidate_min_attention_wiki_spike_10d", None) if strategy else None
min_article_count = (
getattr(strategy, "candidate_min_attention_article_count_3d", None)
@ -651,8 +652,7 @@ def momentum_pre_screen_candidates(
if gap_pct is None or gap_pct < threshold:
continue
event_flag = bool(info.get("event_flag"))
event_score = float(info.get("event_score") or 0.0)
event_flag, event_score = _momentum_effective_event_state(info, allowed_event_types)
wiki_spike = float(info.get("attention_wiki_spike_10d") or 0.0)
article_count = int(info.get("attention_article_count_3d") or 0)
us_article_count = int(info.get("attention_us_article_count_3d") or 0)
@ -707,6 +707,46 @@ def momentum_pre_screen_candidates(
return result
def _momentum_allowed_event_types(strategy) -> set[str]:
if strategy is None:
return set()
return {
str(value).strip().lower()
for value in getattr(strategy, "candidate_allowed_event_types", [])
if str(value).strip()
}
def _momentum_event_types_pass(
info: dict,
allowed_event_types: set[str],
) -> bool:
if not allowed_event_types:
return True
raw_event_types = info.get("event_types") or []
event_types = {
str(value).strip().lower()
for value in raw_event_types
if str(value).strip()
}
if not event_types:
return False
return any(event_type in allowed_event_types for event_type in event_types)
def _momentum_effective_event_state(
info: dict,
allowed_event_types: set[str],
) -> tuple[bool, float]:
event_flag = bool(info.get("event_flag"))
event_score = float(info.get("event_score") or 0.0)
if not event_flag:
return False, 0.0
if allowed_event_types and not _momentum_event_types_pass(info, allowed_event_types):
return False, 0.0
return True, event_score
def _momentum_candidate_signal_passes(
info: dict,
strategy,
@ -716,13 +756,13 @@ def _momentum_candidate_signal_passes(
return True
require_event_flag = bool(getattr(strategy, "candidate_require_event_flag", False))
min_event_score = getattr(strategy, "candidate_min_event_score", None)
allowed_event_types = _momentum_allowed_event_types(strategy)
min_wiki_spike = getattr(strategy, "candidate_min_attention_wiki_spike_10d", None)
min_article_count = getattr(strategy, "candidate_min_attention_article_count_3d", None)
min_us_article_count = getattr(strategy, "candidate_min_attention_us_article_count_3d", None)
min_resolver_conf = getattr(strategy, "candidate_min_attention_resolver_confidence", None)
event_flag = bool(info.get("event_flag"))
event_score = float(info.get("event_score") or 0.0)
event_flag, event_score = _momentum_effective_event_state(info, allowed_event_types)
wiki_spike = float(info.get("attention_wiki_spike_10d") or 0.0)
article_count = int(info.get("attention_article_count_3d") or 0)
us_article_count = int(info.get("attention_us_article_count_3d") or 0)
@ -749,6 +789,7 @@ def _momentum_intraday_weighted_score(
strategy,
) -> float:
"""Weighted same-day candidate score for intraday-first ranking."""
from libs.intraday.simulator import _same_day_support_score
def _clip_unit(value: float | None, cap: float) -> float:
if value is None or cap <= 0:
@ -785,6 +826,7 @@ def _momentum_intraday_weighted_score(
if entropy_20d is not None
else 0.0
)
support_score = _same_day_support_score(info)
event_score = float(daily_info.get("event_score") or 0.0)
wiki_spike = float(daily_info.get("attention_wiki_spike_10d") or 0.0)
@ -811,6 +853,13 @@ def _momentum_intraday_weighted_score(
score += float(getattr(strategy, "candidate_intraday_weight_avg_dollar_vol_30d", 0.0) or 0.0) * _prior_dollar_vol_score(
avg_dollar_vol_30d
)
score += float(getattr(strategy, "candidate_intraday_weight_support_score", 0.0) or 0.0) * support_score
score += float(getattr(strategy, "candidate_intraday_weight_liquid_largecap", 0.0) or 0.0) * (
1.0 if info.get("is_liquid_largecap") else 0.0
)
score += float(getattr(strategy, "candidate_intraday_weight_moderate_gap_liquid", 0.0) or 0.0) * (
1.0 if info.get("is_moderate_gap_liquid") else 0.0
)
score += float(getattr(strategy, "candidate_intraday_weight_gap", 0.0) or 0.0) * _clip_unit(
gap_pct, 0.10
)
@ -818,6 +867,10 @@ def _momentum_intraday_weighted_score(
ret_5d, 0.20
)
score += float(getattr(strategy, "candidate_intraday_weight_low_entropy", 0.0) or 0.0) * low_entropy
score += float(getattr(strategy, "candidate_intraday_weight_sector_thrust", 0.0) or 0.0) * _clip_unit(
float(info.get("sector_thrust_score") or 0.0),
1.0,
)
score += float(getattr(strategy, "candidate_intraday_weight_event_score", 0.0) or 0.0) * _clip_unit(
event_score, 3.0
)
@ -830,12 +883,34 @@ def _momentum_intraday_weighted_score(
return score
def _momentum_intraday_liquid_continuation_score(
info: dict,
) -> tuple[float, float, float, float, float, float, float, float, float]:
from libs.intraday.simulator import _same_day_support_score
entropy_20d = info.get("entropy_20d")
return (
1.0 if info.get("is_moderate_gap_liquid") else 0.0,
1.0 if info.get("is_liquid_largecap") else 0.0,
1.0 if info.get("is_sector_thrust") else 0.0,
_same_day_support_score(info),
float(info.get("confirmation_return_pct") or 0.0),
float(info.get("entry_dollar_volume") or 0.0),
float(info.get("avg_dollar_vol_30d") or 0.0),
float(info.get("gain_pct") or 0.0),
-(float(entropy_20d) if entropy_20d is not None else 1.0),
)
def _momentum_intraday_event_reserve_eligible(
daily_info: dict,
strategy,
) -> bool:
if not bool(daily_info.get("event_flag")):
return False
allowed_event_types = _momentum_allowed_event_types(strategy)
if not _momentum_event_types_pass(daily_info, allowed_event_types):
return False
min_score = getattr(strategy, "candidate_intraday_event_reserve_min_score", None)
if min_score is None:
return True
@ -1070,6 +1145,7 @@ def momentum_intraday_first_candidates(
strategy,
*,
daily_enrichment: dict[str, dict[str, dict]] | None = None,
ticker_sectors: dict[str, str] | None = None,
max_per_day: int | None = None,
) -> dict[str, list[str]]:
"""Build the final momentum shortlist from entry-time intraday information.
@ -1078,7 +1154,11 @@ def momentum_intraday_first_candidates(
already bounded the intraday fetch set. The final ranking uses only
information known by the entry / confirmation bar of the same day.
"""
from libs.intraday.simulator import _select_momentum_sleeves, compute_morning_gains
from libs.intraday.simulator import (
_annotate_sector_thrust_features,
_select_momentum_sleeves,
compute_morning_gains,
)
if max_per_day is None:
max_per_day = max(1, int(getattr(strategy, "candidate_final_max_per_day", 30) or 30))
@ -1108,6 +1188,11 @@ def momentum_intraday_first_candidates(
day,
daily_features_by_ticker=daily_info_by_ticker,
)
gains = _annotate_sector_thrust_features(
gains,
shortlist_strategy,
ticker_sectors,
)
if not gains:
continue
filtered_gains = {
@ -1165,7 +1250,45 @@ def momentum_intraday_first_candidates(
max_per_day=max_per_day,
)
continue
picks = _select_momentum_sleeves(rankable_gains, shortlist_strategy, ticker_sectors=None)
if rank_mode == "liquid_continuation":
rankable_liquid_gains = {
ticker: info
for ticker, info in rankable_gains.items()
if (
info.get("is_moderate_gap_liquid")
or info.get("is_liquid_largecap")
or info.get("is_sector_thrust")
)
}
if not rankable_liquid_gains:
continue
ranked = sorted(
rankable_liquid_gains.items(),
key=lambda item: _momentum_intraday_liquid_continuation_score(item[1]),
reverse=True,
)
if ranked:
ranked_tickers = _apply_momentum_intraday_event_reserve(
[ticker for ticker, _info in ranked],
filtered_gains,
daily_info_by_ticker,
strategy,
day_bars=day_bars,
max_per_day=max_per_day,
)
result[day] = _apply_momentum_intraday_moderate_liquid_reserve(
ranked_tickers,
filtered_gains,
daily_info_by_ticker,
strategy,
max_per_day=max_per_day,
)
continue
picks = _select_momentum_sleeves(
rankable_gains,
shortlist_strategy,
ticker_sectors=ticker_sectors,
)
if picks:
ranked_tickers = _apply_momentum_intraday_event_reserve(
[ticker for ticker, _sleeve in picks],

File diff suppressed because it is too large Load Diff

@ -17,9 +17,12 @@ from apps.intraday_bt.run import (
_normalize_candidate_map,
_momentum_intraday_seed_candidates,
_momentum_strategy_uses_candidate_stage_catalyst,
_momentum_strategy_uses_daily_enrichment,
_momentum_strategy_requires_regime_ticker_daily,
_momentum_strategy_uses_attention,
_momentum_strategy_uses_catalyst,
_momentum_strategy_uses_sector_labels,
_momentum_strategy_uses_sector_proxies,
_retain_recent_intraday_shortlist,
_recent_intraday_first_candidates,
_strategy_for_recent_live_scan,
@ -275,13 +278,26 @@ def test_momentum_strategy_uses_seed_event_overlay_for_candidate_stage_catalyst(
assert _momentum_strategy_uses_candidate_stage_catalyst(strategy) is True
def test_momentum_strategy_uses_candidate_event_type_filter_for_fetch_activation() -> None:
strategy = StrategyParams(candidate_allowed_event_types=["earnings_release"])
assert _momentum_strategy_uses_catalyst(strategy) is True
assert _momentum_strategy_uses_candidate_stage_catalyst(strategy) is True
def test_momentum_strategy_uses_event_reserve_and_event_sleeve_for_fetch_activation() -> None:
reserve_strategy = StrategyParams(candidate_intraday_event_reserve_slots=1)
sleeve_strategy = StrategyParams(use_event_sleeve=True, event_weight=0.1)
event_day_liquid_strategy = StrategyParams(
use_event_day_liquid_sleeve=True,
event_day_liquid_capital_fraction=0.1,
event_day_liquid_max_positions=1,
)
assert _momentum_strategy_uses_catalyst(reserve_strategy) is True
assert _momentum_strategy_uses_candidate_stage_catalyst(reserve_strategy) is False
assert _momentum_strategy_uses_catalyst(sleeve_strategy) is True
assert _momentum_strategy_uses_catalyst(event_day_liquid_strategy) is True
def test_momentum_strategy_uses_intraday_attention_weight_for_fetch_activation() -> None:
@ -296,6 +312,21 @@ def test_momentum_strategy_requires_regime_ticker_daily_for_gap_meta_layer() ->
assert _momentum_strategy_requires_regime_ticker_daily(strategy) is True
def test_momentum_strategy_uses_sector_metadata_and_proxy_fetch_for_overlay_engines() -> None:
cluster_strategy = StrategyParams(use_liquid_cluster_engine=True)
etf_strategy = StrategyParams(
use_sector_etf_sleeve=True,
sector_etf_capital_fraction=0.2,
sector_etf_max_positions=1,
)
assert _momentum_strategy_uses_daily_enrichment(cluster_strategy) is True
assert _momentum_strategy_uses_sector_labels(cluster_strategy) is True
assert _momentum_strategy_uses_sector_proxies(cluster_strategy) is False
assert _momentum_strategy_uses_sector_labels(etf_strategy) is True
assert _momentum_strategy_uses_sector_proxies(etf_strategy) is True
def test_momentum_intraday_seed_candidates_only_apply_signal_filters_in_final_pass() -> None:
daily_bars = {
"AAA": [
@ -620,7 +651,9 @@ def test_retain_recent_intraday_shortlist_preserves_intraday_candidates() -> Non
def test_momentum_strategy_defaults_to_simple_returns_and_cli_can_override() -> None:
config = load_config("configs/intraday/strategies/leader_intraday_momentum_high_wr.yaml")
config = load_config(
"configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml"
)
assert config.strategy.compound_returns is False

@ -184,6 +184,112 @@ def test_momentum_pre_screen_candidates_can_require_event_and_attention() -> Non
assert result == {"2026-01-05": ["AAA"]}
def test_momentum_pre_screen_candidates_can_filter_event_types() -> None:
daily_bars = {
"AAA": [
{"date": "2026-01-02", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.0, "volume": 1_000},
{"date": "2026-01-05", "open": 10.3, "high": 10.8, "low": 10.2, "close": 10.6, "volume": 2_000},
],
"BBB": [
{"date": "2026-01-02", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.0, "volume": 1_000},
{"date": "2026-01-05", "open": 10.4, "high": 10.9, "low": 10.3, "close": 10.7, "volume": 2_000},
],
}
enrichment = {
"AAA": {
"2026-01-05": {
"gap_pct": 0.03,
"ret_5d": 0.03,
"entropy_20d": 0.60,
"avg_dollar_vol_30d": 20_000_000.0,
"atr_14": 1.0,
"event_flag": True,
"event_score": 1.0,
"event_types": ["earnings_release"],
}
},
"BBB": {
"2026-01-05": {
"gap_pct": 0.04,
"ret_5d": 0.04,
"entropy_20d": 0.50,
"avg_dollar_vol_30d": 25_000_000.0,
"atr_14": 1.2,
"event_flag": True,
"event_score": 1.0,
"event_types": ["management_change"],
}
},
}
strategy = StrategyParams(
candidate_require_event_flag=True,
candidate_allowed_event_types=["earnings_release"],
)
result = momentum_pre_screen_candidates(
daily_bars,
["2026-01-05"],
enrichment,
threshold=0.02,
max_per_day=5,
strategy=strategy,
)
assert result == {"2026-01-05": ["AAA"]}
def test_momentum_pre_screen_candidates_event_type_filter_does_not_block_non_event_names() -> None:
daily_bars = {
"AAA": [
{"date": "2026-01-02", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.0, "volume": 1_000},
{"date": "2026-01-05", "open": 10.3, "high": 10.8, "low": 10.2, "close": 10.6, "volume": 2_000},
],
"BBB": [
{"date": "2026-01-02", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.0, "volume": 1_000},
{"date": "2026-01-05", "open": 10.4, "high": 10.9, "low": 10.3, "close": 10.7, "volume": 2_000},
],
}
enrichment = {
"AAA": {
"2026-01-05": {
"gap_pct": 0.03,
"ret_5d": 0.03,
"entropy_20d": 0.60,
"avg_dollar_vol_30d": 20_000_000.0,
"atr_14": 1.0,
"event_flag": True,
"event_score": 1.0,
"event_types": ["management_change"],
}
},
"BBB": {
"2026-01-05": {
"gap_pct": 0.04,
"ret_5d": 0.04,
"entropy_20d": 0.50,
"avg_dollar_vol_30d": 25_000_000.0,
"atr_14": 1.2,
"event_flag": False,
"event_score": 0.0,
}
},
}
strategy = StrategyParams(
candidate_allowed_event_types=["earnings_release"],
)
result = momentum_pre_screen_candidates(
daily_bars,
["2026-01-05"],
enrichment,
threshold=0.02,
max_per_day=5,
strategy=strategy,
)
assert result == {"2026-01-05": ["BBB", "AAA"]}
def test_momentum_intraday_first_candidates_uses_entry_time_info_only() -> None:
strategy = StrategyParams(
candidate_source_mode="intraday_first",
@ -229,6 +335,68 @@ def test_momentum_intraday_first_candidates_uses_entry_time_info_only() -> None:
assert result == {"2026-01-05": ["BBB"]}
def test_momentum_intraday_first_candidates_can_filter_event_types() -> None:
strategy = StrategyParams(
candidate_source_mode="intraday_first",
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_confirmation_return_pct=0.0,
min_morning_gain_pct=0.01,
min_entry_volume=50_000,
candidate_final_max_per_day=2,
candidate_require_event_flag=True,
candidate_allowed_event_types=["earnings_release"],
)
all_intraday = {
"2026-01-05": {
"AAA": [
{"timestamp": "2026-01-05T14:30:00+00:00", "open": 10.0, "high": 10.2, "low": 9.9, "close": 10.1, "volume": 30_000},
{"timestamp": "2026-01-05T14:35:00+00:00", "open": 10.1, "high": 10.3, "low": 10.0, "close": 10.2, "volume": 30_000},
{"timestamp": "2026-01-05T14:40:00+00:00", "open": 10.2, "high": 10.5, "low": 10.1, "close": 10.4, "volume": 30_000},
{"timestamp": "2026-01-05T14:45:00+00:00", "open": 10.4, "high": 10.7, "low": 10.3, "close": 10.6, "volume": 30_000},
{"timestamp": "2026-01-05T14:50:00+00:00", "open": 10.6, "high": 10.8, "low": 10.5, "close": 10.7, "volume": 30_000},
],
"BBB": [
{"timestamp": "2026-01-05T14:30:00+00:00", "open": 20.0, "high": 20.1, "low": 19.9, "close": 20.0, "volume": 40_000},
{"timestamp": "2026-01-05T14:35:00+00:00", "open": 20.0, "high": 20.2, "low": 19.9, "close": 20.1, "volume": 40_000},
{"timestamp": "2026-01-05T14:40:00+00:00", "open": 20.1, "high": 20.7, "low": 20.0, "close": 20.5, "volume": 40_000},
{"timestamp": "2026-01-05T14:45:00+00:00", "open": 20.5, "high": 21.0, "low": 20.4, "close": 20.9, "volume": 40_000},
{"timestamp": "2026-01-05T14:50:00+00:00", "open": 20.9, "high": 21.3, "low": 20.8, "close": 21.1, "volume": 40_000},
],
}
}
daily_enrichment = {
"AAA": {
"2026-01-05": {
"event_flag": True,
"event_score": 1.0,
"event_types": ["management_change"],
"gap_pct": 0.01,
"avg_daily_vol_14d": 1_000_000.0,
}
},
"BBB": {
"2026-01-05": {
"event_flag": True,
"event_score": 1.0,
"event_types": ["earnings_release"],
"gap_pct": 0.01,
"avg_daily_vol_14d": 1_000_000.0,
}
},
}
result = momentum_intraday_first_candidates(
all_intraday,
["2026-01-05"],
strategy,
daily_enrichment=daily_enrichment,
max_per_day=2,
)
assert result == {"2026-01-05": ["BBB"]}
def test_momentum_intraday_first_candidates_can_use_weighted_ranking() -> None:
strategy = StrategyParams(
candidate_source_mode="intraday_first",
@ -348,6 +516,151 @@ def test_momentum_intraday_first_candidates_weighted_ranking_can_use_prior_dolla
assert result == {"2026-01-05": ["BBB"]}
def test_momentum_intraday_first_candidates_weighted_ranking_can_use_sector_thrust() -> None:
strategy = StrategyParams(
candidate_source_mode="intraday_first",
candidate_intraday_rank_mode="weighted",
candidate_intraday_weight_gain=0.2,
candidate_intraday_weight_confirmation=0.2,
candidate_intraday_weight_entry_dollar_volume=0.1,
candidate_intraday_weight_sector_thrust=1.0,
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_confirmation_return_pct=0.0,
min_morning_gain_pct=0.01,
candidate_final_max_per_day=1,
use_sector_thrust_sleeve=True,
sector_thrust_min_members=2,
sector_thrust_min_gain_pct=0.01,
sector_thrust_min_confirmation_return_pct=0.003,
sector_thrust_min_entry_dollar_volume=50_000_000.0,
sector_thrust_min_avg_dollar_vol_30d=500_000_000.0,
sector_thrust_min_sector_avg_confirmation_return_pct=0.003,
sector_thrust_min_sector_total_entry_dollar_volume=120_000_000.0,
)
all_intraday = {
"2026-01-05": {
"ALLY_A": [
{"timestamp": "2026-01-05T14:30:00+00:00", "open": 100.0, "high": 100.2, "low": 99.9, "close": 100.0, "volume": 180_000},
{"timestamp": "2026-01-05T14:35:00+00:00", "open": 100.0, "high": 100.8, "low": 99.9, "close": 100.6, "volume": 180_000},
{"timestamp": "2026-01-05T14:40:00+00:00", "open": 100.6, "high": 101.2, "low": 100.5, "close": 101.0, "volume": 180_000},
{"timestamp": "2026-01-05T14:45:00+00:00", "open": 101.0, "high": 101.7, "low": 100.9, "close": 101.5, "volume": 180_000},
{"timestamp": "2026-01-05T14:50:00+00:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.6, "volume": 180_000},
],
"ALLY_B": [
{"timestamp": "2026-01-05T14:30:00+00:00", "open": 80.0, "high": 80.1, "low": 79.9, "close": 80.0, "volume": 170_000},
{"timestamp": "2026-01-05T14:35:00+00:00", "open": 80.0, "high": 80.6, "low": 79.9, "close": 80.4, "volume": 170_000},
{"timestamp": "2026-01-05T14:40:00+00:00", "open": 80.4, "high": 80.9, "low": 80.3, "close": 80.8, "volume": 170_000},
{"timestamp": "2026-01-05T14:45:00+00:00", "open": 80.8, "high": 81.4, "low": 80.7, "close": 81.2, "volume": 170_000},
{"timestamp": "2026-01-05T14:50:00+00:00", "open": 81.2, "high": 81.5, "low": 81.1, "close": 81.3, "volume": 170_000},
],
"SOLO": [
{"timestamp": "2026-01-05T14:30:00+00:00", "open": 20.0, "high": 20.3, "low": 19.9, "close": 20.1, "volume": 300_000},
{"timestamp": "2026-01-05T14:35:00+00:00", "open": 20.1, "high": 20.8, "low": 20.0, "close": 20.6, "volume": 300_000},
{"timestamp": "2026-01-05T14:40:00+00:00", "open": 20.6, "high": 21.1, "low": 20.5, "close": 20.9, "volume": 300_000},
{"timestamp": "2026-01-05T14:45:00+00:00", "open": 20.9, "high": 21.3, "low": 20.8, "close": 21.1, "volume": 300_000},
{"timestamp": "2026-01-05T14:50:00+00:00", "open": 21.1, "high": 21.3, "low": 21.0, "close": 21.2, "volume": 300_000},
],
}
}
daily_enrichment = {
"ALLY_A": {"2026-01-05": {"gap_pct": 0.01, "avg_daily_vol_14d": 5_000_000.0, "avg_dollar_vol_30d": 900_000_000.0}},
"ALLY_B": {"2026-01-05": {"gap_pct": 0.01, "avg_daily_vol_14d": 5_000_000.0, "avg_dollar_vol_30d": 850_000_000.0}},
"SOLO": {"2026-01-05": {"gap_pct": 0.01, "avg_daily_vol_14d": 10_000_000.0, "avg_dollar_vol_30d": 1_200_000_000.0}},
}
result = momentum_intraday_first_candidates(
all_intraday,
["2026-01-05"],
strategy,
daily_enrichment=daily_enrichment,
ticker_sectors={
"ALLY_A": "Technology",
"ALLY_B": "Technology",
"SOLO": "Energy",
},
max_per_day=1,
)
assert result == {"2026-01-05": ["ALLY_A"]}
def test_momentum_intraday_first_candidates_can_use_liquid_continuation_rank_mode() -> None:
strategy = StrategyParams(
candidate_source_mode="intraday_first",
candidate_intraday_rank_mode="liquid_continuation",
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_confirmation_return_pct=0.0,
min_morning_gain_pct=0.004,
candidate_final_max_per_day=1,
use_liquid_largecap_sleeve=True,
liquid_largecap_min_gain_pct=0.004,
liquid_largecap_max_gain_pct=0.03,
liquid_largecap_min_confirmation_return_pct=0.0005,
liquid_largecap_min_entry_dollar_volume=50_000_000.0,
liquid_largecap_min_avg_dollar_vol_30d=2_000_000_000.0,
liquid_largecap_max_entropy_20d=0.90,
use_moderate_gap_liquid_sleeve=True,
moderate_gap_liquid_min_gap_pct=0.002,
moderate_gap_liquid_max_gap_pct=0.04,
moderate_gap_liquid_min_gain_pct=0.005,
moderate_gap_liquid_max_gain_pct=0.04,
moderate_gap_liquid_min_confirmation_return_pct=0.001,
moderate_gap_liquid_min_entry_dollar_volume=25_000_000.0,
moderate_gap_liquid_min_avg_dollar_vol_30d=250_000_000.0,
moderate_gap_liquid_max_avg_dollar_vol_30d=4_000_000_000.0,
moderate_gap_liquid_min_volume_ratio_14d=0.02,
moderate_gap_liquid_max_entropy_20d=0.88,
)
all_intraday = {
"2026-01-05": {
"LIQ": [
{"timestamp": "2026-01-05T14:30:00+00:00", "open": 100.0, "high": 100.8, "low": 99.9, "close": 100.5, "volume": 220_000},
{"timestamp": "2026-01-05T14:35:00+00:00", "open": 100.5, "high": 101.2, "low": 100.4, "close": 101.0, "volume": 220_000},
{"timestamp": "2026-01-05T14:40:00+00:00", "open": 101.0, "high": 101.8, "low": 100.9, "close": 101.5, "volume": 220_000},
{"timestamp": "2026-01-05T14:45:00+00:00", "open": 101.5, "high": 102.2, "low": 101.4, "close": 102.0, "volume": 220_000},
{"timestamp": "2026-01-05T14:50:00+00:00", "open": 102.0, "high": 102.4, "low": 101.9, "close": 102.2, "volume": 220_000},
],
"HOT": [
{"timestamp": "2026-01-05T14:30:00+00:00", "open": 10.0, "high": 10.4, "low": 9.9, "close": 10.3, "volume": 120_000},
{"timestamp": "2026-01-05T14:35:00+00:00", "open": 10.3, "high": 10.7, "low": 10.2, "close": 10.6, "volume": 120_000},
{"timestamp": "2026-01-05T14:40:00+00:00", "open": 10.6, "high": 10.9, "low": 10.5, "close": 10.8, "volume": 120_000},
{"timestamp": "2026-01-05T14:45:00+00:00", "open": 10.8, "high": 11.0, "low": 10.7, "close": 10.9, "volume": 120_000},
{"timestamp": "2026-01-05T14:50:00+00:00", "open": 10.9, "high": 11.1, "low": 10.8, "close": 11.0, "volume": 120_000},
],
}
}
daily_enrichment = {
"LIQ": {
"2026-01-05": {
"gap_pct": 0.01,
"avg_daily_vol_14d": 5_000_000.0,
"avg_dollar_vol_30d": 3_000_000_000.0,
"entropy_20d": 0.70,
}
},
"HOT": {
"2026-01-05": {
"gap_pct": 0.02,
"avg_daily_vol_14d": 4_000_000.0,
"avg_dollar_vol_30d": 50_000_000.0,
"entropy_20d": 0.82,
}
},
}
result = momentum_intraday_first_candidates(
all_intraday,
["2026-01-05"],
strategy,
daily_enrichment=daily_enrichment,
max_per_day=2,
)
assert result == {"2026-01-05": ["LIQ"]}
def test_momentum_intraday_first_candidates_can_replace_tail_with_event_reserve() -> None:
strategy = StrategyParams(
candidate_source_mode="intraday_first",

@ -880,6 +880,163 @@ def test_select_momentum_sleeves_can_force_moderate_gap_liquid_pick() -> None:
assert picks == [("TER", "moderate_gap_liquid")]
def test_select_momentum_sleeves_can_force_sector_thrust_pick() -> None:
strategy = StrategyParams(
top_n=1,
use_five_sleeves=True,
use_sector_thrust_sleeve=True,
five_sleeve_core_weight=0.0,
five_sleeve_gap_weight=0.0,
five_sleeve_volume_weight=0.0,
five_sleeve_entropy_weight=0.0,
five_sleeve_trend_weight=0.0,
sector_thrust_weight=1.0,
five_sleeve_force_count=1,
sector_thrust_min_members=2,
sector_thrust_min_gain_pct=0.015,
sector_thrust_min_confirmation_return_pct=0.004,
sector_thrust_min_entry_dollar_volume=50_000_000.0,
sector_thrust_min_avg_dollar_vol_30d=500_000_000.0,
sector_thrust_min_sector_avg_confirmation_return_pct=0.004,
sector_thrust_min_sector_total_entry_dollar_volume=120_000_000.0,
)
morning_gains = {
"ALLY_A": {
"gain_pct": 0.03,
"entry_volume": 600_000,
"entry_dollar_volume": 80_000_000.0,
"avg_dollar_vol_30d": 900_000_000.0,
"confirmation_return_pct": 0.006,
},
"ALLY_B": {
"gain_pct": 0.028,
"entry_volume": 500_000,
"entry_dollar_volume": 70_000_000.0,
"avg_dollar_vol_30d": 850_000_000.0,
"confirmation_return_pct": 0.005,
},
"SOLO": {
"gain_pct": 0.05,
"entry_volume": 550_000,
"entry_dollar_volume": 90_000_000.0,
"avg_dollar_vol_30d": 1_000_000_000.0,
"confirmation_return_pct": 0.007,
},
}
picks = _select_momentum_sleeves(
morning_gains,
strategy,
ticker_sectors={
"ALLY_A": "Technology",
"ALLY_B": "Technology",
"SOLO": "Energy",
},
)
assert picks == [("ALLY_A", "sector_thrust")]
def test_select_momentum_sleeves_can_use_liquid_continuation_selection_mode() -> None:
strategy = StrategyParams(
top_n=2,
momentum_selection_mode="liquid_continuation",
)
morning_gains = {
"HOT": {
"gain_pct": 0.05,
"confirmation_return_pct": 0.002,
"entry_dollar_volume": 4_000_000.0,
"avg_dollar_vol_30d": 30_000_000.0,
"entropy_20d": 0.82,
"is_liquid_largecap": False,
"is_moderate_gap_liquid": False,
"is_sector_thrust": False,
},
"LIQ": {
"gain_pct": 0.015,
"confirmation_return_pct": 0.006,
"entry_dollar_volume": 90_000_000.0,
"avg_dollar_vol_30d": 3_000_000_000.0,
"entropy_20d": 0.72,
"is_liquid_largecap": True,
"is_moderate_gap_liquid": True,
"is_sector_thrust": False,
},
}
picks = _select_momentum_sleeves(morning_gains, strategy)
assert picks == [("LIQ", "liquid_continuation_core")]
def test_simulate_day_records_sector_thrust_trade_diagnostics() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_confirmation_return_pct=0.0,
min_morning_gain_pct=0.01,
top_n=1,
slippage_bps=0.0,
use_five_sleeves=True,
use_sector_thrust_sleeve=True,
five_sleeve_core_weight=0.0,
five_sleeve_gap_weight=0.0,
five_sleeve_volume_weight=0.0,
five_sleeve_entropy_weight=0.0,
five_sleeve_trend_weight=0.0,
sector_thrust_weight=1.0,
five_sleeve_force_count=1,
sector_thrust_min_members=2,
sector_thrust_min_gain_pct=0.015,
sector_thrust_min_confirmation_return_pct=0.004,
sector_thrust_min_entry_dollar_volume=50_000_000.0,
sector_thrust_min_avg_dollar_vol_30d=500_000_000.0,
)
bars_by_ticker = {
"ALLY_A": _bars(
"2026-01-13",
open_price=100.0,
closes=[100.2, 100.8, 101.2, 101.8, 102.0, 102.5],
volumes=[180_000, 180_000, 180_000, 180_000, 180_000, 180_000],
),
"ALLY_B": _bars(
"2026-01-13",
open_price=80.0,
closes=[80.2, 80.7, 81.0, 81.4, 81.6, 81.9],
volumes=[170_000, 170_000, 170_000, 170_000, 170_000, 170_000],
),
"SOLO": _bars(
"2026-01-13",
open_price=50.0,
closes=[50.2, 50.8, 51.2, 51.7, 51.8, 52.0],
volumes=[220_000, 220_000, 220_000, 220_000, 220_000, 220_000],
),
}
day = simulate_day(
bars_by_ticker,
"2026-01-13",
strategy,
daily_features_by_ticker={
"ALLY_A": {"avg_daily_vol_14d": 2_000_000.0, "avg_dollar_vol_30d": 900_000_000.0},
"ALLY_B": {"avg_daily_vol_14d": 2_000_000.0, "avg_dollar_vol_30d": 850_000_000.0},
"SOLO": {"avg_daily_vol_14d": 2_000_000.0, "avg_dollar_vol_30d": 1_200_000_000.0},
},
ticker_sectors={
"ALLY_A": "Technology",
"ALLY_B": "Technology",
"SOLO": "Energy",
},
)
assert len(day.trades) == 1
assert day.trades[0].trade_sleeve == "sector_thrust"
assert day.trades[0].is_sector_thrust is True
assert day.trades[0].sector_thrust_member_count == 2
assert day.trades[0].sector_thrust_total_entry_dollar_volume is not None
assert day.trades[0].sector_thrust_total_entry_dollar_volume > 120_000_000.0
def test_simulate_day_can_enable_event_sleeve_only_on_soft_days() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
@ -1120,6 +1277,267 @@ def test_simulate_day_can_apply_tail_risk_scaler_on_weak_support_single_name() -
assert len(day.trades) == 1
def test_tail_risk_event_exemption_requires_support_when_configured() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_morning_gain_pct=0.01,
top_n=1,
slippage_bps=0.0,
daily_budget_reset=True,
initial_capital=10_000.0,
use_event_sleeve=True,
event_min_score=1.0,
tail_risk_day_max_trades=1,
tail_risk_day_min_max_gain_pct=0.03,
tail_risk_day_max_support_score=0.35,
tail_risk_day_min_max_confirmation_return_pct=0.01,
tail_risk_day_require_no_event=True,
tail_risk_day_event_exemption_min_support_score=0.35,
tail_risk_day_scale=0.5,
)
day = simulate_day(
{
"WEAK_EVENT": _bars(
"2026-02-18",
open_price=10.0,
closes=[10.1, 10.35, 10.5, 10.7, 10.6, 10.2],
volumes=[120_000] * 6,
),
},
"2026-02-18",
strategy,
daily_features_by_ticker={
"WEAK_EVENT": {
"gap_pct": 0.04,
"avg_daily_vol_14d": 1_000_000.0,
"avg_dollar_vol_30d": 25_000_000.0,
"ret_5d": 0.07,
"entropy_20d": 0.82,
"event_flag": True,
"event_score": 1.0,
},
},
)
assert day.tail_risk_scaler == 0.5
assert round(day.capital_deployed, 2) == 5000.0
assert day.trades[0].support_score == 0.2
def test_tail_risk_event_exemption_keeps_supported_event_full_size() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_morning_gain_pct=0.01,
top_n=1,
slippage_bps=0.0,
daily_budget_reset=True,
initial_capital=10_000.0,
use_event_sleeve=True,
event_min_score=1.0,
tail_risk_day_max_trades=1,
tail_risk_day_min_max_gain_pct=0.03,
tail_risk_day_max_support_score=0.60,
tail_risk_day_min_max_confirmation_return_pct=0.01,
tail_risk_day_require_no_event=True,
tail_risk_day_event_exemption_min_support_score=0.35,
tail_risk_day_scale=0.5,
)
day = simulate_day(
{
"SUPPORTED_EVENT": _bars(
"2026-02-18",
open_price=10.0,
closes=[10.1, 10.35, 10.5, 10.7, 10.6, 10.2],
volumes=[500_000] * 6,
),
},
"2026-02-18",
strategy,
daily_features_by_ticker={
"SUPPORTED_EVENT": {
"gap_pct": 0.04,
"avg_daily_vol_14d": 3_000_000.0,
"avg_dollar_vol_30d": 100_000_000.0,
"ret_5d": 0.07,
"entropy_20d": 0.82,
"event_flag": True,
"event_score": 1.0,
},
},
)
assert day.tail_risk_scaler == 1.0
assert round(day.capital_deployed, 2) == 10000.0
assert day.trades[0].support_score is not None
assert day.trades[0].support_score >= 0.35
def test_low_momentum_single_name_scaler_reduces_weak_single_pick() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_morning_gain_pct=0.01,
top_n=1,
slippage_bps=0.0,
daily_budget_reset=True,
initial_capital=10_000.0,
low_momentum_single_name_max_gain_pct=0.025,
low_momentum_single_name_require_no_event=True,
low_momentum_single_name_exempt_largecap=True,
low_momentum_single_name_scale=0.55,
)
day = simulate_day(
{
"LOW": _bars(
"2025-12-15",
open_price=100.0,
closes=[100.8, 101.1, 101.4, 101.8, 101.7, 97.0],
volumes=[100_000] * 6,
),
},
"2025-12-15",
strategy,
daily_features_by_ticker={
"LOW": {
"gap_pct": 0.02,
"avg_daily_vol_14d": 1_000_000.0,
"avg_dollar_vol_30d": 150_000_000.0,
"ret_5d": 0.02,
"entropy_20d": 0.75,
"event_flag": False,
"event_score": 0.0,
},
},
)
assert day.tail_risk_scaler == 0.55
assert round(day.capital_deployed, 2) == 5500.0
assert len(day.trades) == 1
def test_soft_day_sparse_scaler_reduces_unsupported_soft_basket() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_morning_gain_pct=0.01,
top_n=1,
slippage_bps=0.0,
daily_budget_reset=True,
initial_capital=10_000.0,
market_regime_gap_threshold=-0.03,
market_regime_gap_ticker="SPY",
regime_size_scale_low=-0.02,
regime_size_scale_high=0.0,
regime_size_scale_min=0.5,
soft_day_scaler_threshold=0.8,
soft_day_sparse_max_trades=2,
soft_day_sparse_require_no_event=True,
soft_day_sparse_exempt_largecap=True,
soft_day_sparse_exempt_moderate_gap_liquid=True,
soft_day_sparse_scale=0.7,
)
day = simulate_day(
{
"FRAGILE": _bars(
"2026-01-28",
open_price=20.0,
closes=[20.1, 20.3, 20.5, 20.7, 20.9, 19.8],
volumes=[100_000] * 6,
),
},
"2026-01-28",
strategy,
daily_features_by_ticker={
"SPY": {"prev_close": 100.0, "today_open": 98.0},
"FRAGILE": {
"gap_pct": 0.015,
"avg_daily_vol_14d": 1_000_000.0,
"avg_dollar_vol_30d": 120_000_000.0,
"ret_5d": 0.01,
"entropy_20d": 0.75,
"event_flag": False,
"event_score": 0.0,
},
},
)
assert day.is_soft_day is True
assert day.soft_day_sparse_scaler == 0.7
assert day.tail_risk_scaler == 0.7
assert round(day.capital_deployed, 2) == 3500.0
def test_soft_day_sparse_scaler_keeps_supported_moderate_liquid_basket_full_size() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_morning_gain_pct=0.01,
top_n=1,
slippage_bps=0.0,
daily_budget_reset=True,
initial_capital=10_000.0,
market_regime_gap_threshold=-0.03,
market_regime_gap_ticker="SPY",
regime_size_scale_low=-0.02,
regime_size_scale_high=0.0,
regime_size_scale_min=0.5,
soft_day_scaler_threshold=0.8,
soft_day_sparse_max_trades=2,
soft_day_sparse_require_no_event=True,
soft_day_sparse_exempt_largecap=True,
soft_day_sparse_exempt_moderate_gap_liquid=True,
soft_day_sparse_scale=0.7,
use_moderate_gap_liquid_sleeve=True,
moderate_gap_liquid_min_gap_pct=0.005,
moderate_gap_liquid_max_gap_pct=0.025,
moderate_gap_liquid_min_gain_pct=0.015,
moderate_gap_liquid_max_gain_pct=0.04,
moderate_gap_liquid_min_confirmation_return_pct=0.005,
moderate_gap_liquid_min_entry_dollar_volume=20_000_000.0,
moderate_gap_liquid_min_avg_dollar_vol_30d=250_000_000.0,
moderate_gap_liquid_max_avg_dollar_vol_30d=2_000_000_000.0,
moderate_gap_liquid_min_volume_ratio_14d=0.04,
moderate_gap_liquid_max_entropy_20d=0.86,
)
day = simulate_day(
{
"TER": _bars(
"2026-01-29",
open_price=100.0,
closes=[100.4, 101.0, 101.7, 102.3, 102.7, 103.0],
volumes=[500_000] * 6,
),
},
"2026-01-29",
strategy,
daily_features_by_ticker={
"SPY": {"prev_close": 100.0, "today_open": 98.0},
"TER": {
"gap_pct": 0.012,
"avg_daily_vol_14d": 8_000_000.0,
"avg_dollar_vol_30d": 750_000_000.0,
"ret_5d": 0.03,
"entropy_20d": 0.79,
"event_flag": False,
"event_score": 0.0,
},
},
)
assert day.is_soft_day is True
assert day.soft_day_sparse_scaler == 1.0
assert day.tail_risk_scaler == 1.0
assert round(day.capital_deployed, 2) == 5000.0
assert day.trades[0].is_moderate_gap_liquid is True
def test_simulate_day_keeps_full_size_for_supported_single_name() -> None:
strategy = StrategyParams(
entry_minutes_after_open=15,
@ -1210,6 +1628,364 @@ def test_select_momentum_sleeves_can_add_liquid_largecap_fallback_when_sparse()
assert ("LQ", "liquid_largecap_fallback") in picks
def test_simulate_day_can_add_liquid_cluster_engine_without_disturbing_base_basket() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_morning_gain_pct=0.04,
min_confirmation_return_pct=0.003,
top_n=1,
slippage_bps=0.0,
daily_budget_reset=True,
initial_capital=10_000.0,
use_liquid_cluster_engine=True,
liquid_cluster_capital_fraction=0.25,
liquid_cluster_max_positions=1,
liquid_cluster_min_members=2,
liquid_cluster_min_gain_pct=0.015,
liquid_cluster_max_gain_pct=0.04,
liquid_cluster_min_confirmation_return_pct=0.003,
liquid_cluster_min_entry_dollar_volume=30_000_000.0,
liquid_cluster_min_avg_dollar_vol_30d=300_000_000.0,
liquid_cluster_min_volume_ratio_14d=0.10,
liquid_cluster_max_entropy_20d=0.80,
liquid_cluster_min_sector_avg_confirmation_return_pct=0.003,
liquid_cluster_min_sector_total_entry_dollar_volume=100_000_000.0,
)
day = simulate_day(
{
"CORE": _bars(
"2026-02-03",
open_price=100.0,
closes=[100.5, 102.0, 103.0, 106.0, 106.4, 107.0],
volumes=[250_000] * 6,
),
"CL_A": _bars(
"2026-02-03",
open_price=50.0,
closes=[50.2, 50.7, 50.9, 51.2, 51.3, 51.4],
volumes=[400_000] * 6,
),
"CL_B": _bars(
"2026-02-03",
open_price=60.0,
closes=[60.2, 60.7, 60.9, 61.3, 61.4, 61.5],
volumes=[350_000] * 6,
),
},
"2026-02-03",
strategy,
daily_features_by_ticker={
"CORE": {
"gap_pct": 0.03,
"avg_daily_vol_14d": 2_000_000.0,
"avg_dollar_vol_30d": 900_000_000.0,
"ret_5d": 0.12,
"entropy_20d": 0.40,
},
"CL_A": {
"gap_pct": 0.015,
"avg_daily_vol_14d": 3_000_000.0,
"avg_dollar_vol_30d": 800_000_000.0,
"ret_5d": 0.05,
"entropy_20d": 0.45,
},
"CL_B": {
"gap_pct": 0.012,
"avg_daily_vol_14d": 2_500_000.0,
"avg_dollar_vol_30d": 750_000_000.0,
"ret_5d": 0.04,
"entropy_20d": 0.48,
},
},
ticker_sectors={
"CORE": "Energy",
"CL_A": "Technology",
"CL_B": "Technology",
},
)
assert day.trades[0].ticker == "CORE"
assert day.trades[0].trade_sleeve == "core"
assert day.trades[1].ticker in {"CL_A", "CL_B"}
assert day.trades[1].trade_sleeve == "liquid_cluster_engine"
assert day.trades[1].is_liquid_cluster is True
assert day.trades[1].liquid_cluster_sector == "Technology"
assert day.capital_deployed == 10_000.0
def test_simulate_day_can_add_event_day_liquid_sleeve_without_changing_base_basket() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_morning_gain_pct=0.04,
min_confirmation_return_pct=0.003,
top_n=1,
use_five_sleeves=False,
slippage_bps=0.0,
daily_budget_reset=True,
initial_capital=10_000.0,
candidate_allowed_event_types=["earnings_release"],
use_event_day_liquid_sleeve=True,
event_day_liquid_capital_fraction=0.25,
event_day_liquid_max_positions=1,
event_day_liquid_min_event_names=1,
event_day_liquid_min_event_score=1.0,
event_day_liquid_min_event_support_score=0.2,
event_day_liquid_min_gain_pct=0.005,
event_day_liquid_max_gain_pct=0.03,
event_day_liquid_min_confirmation_return_pct=0.003,
event_day_liquid_min_entry_dollar_volume=30_000_000.0,
event_day_liquid_min_avg_dollar_vol_30d=500_000_000.0,
event_day_liquid_max_entropy_20d=0.80,
event_day_liquid_min_support_score=0.50,
liquid_largecap_min_gain_pct=0.005,
liquid_largecap_max_gain_pct=0.03,
liquid_largecap_min_confirmation_return_pct=0.003,
liquid_largecap_min_entry_dollar_volume=30_000_000.0,
liquid_largecap_min_avg_dollar_vol_30d=500_000_000.0,
liquid_largecap_max_entropy_20d=0.80,
)
day = simulate_day(
{
"CORE": _bars(
"2026-02-05",
open_price=100.0,
closes=[100.5, 102.0, 103.2, 105.0, 105.6, 106.0],
volumes=[250_000] * 6,
),
"EVT": _bars(
"2026-02-05",
open_price=50.0,
closes=[50.3, 51.0, 51.8, 52.4, 52.7, 53.0],
volumes=[250_000] * 6,
),
"LQ": _bars(
"2026-02-05",
open_price=200.0,
closes=[200.4, 201.0, 201.7, 202.6, 202.9, 203.5],
volumes=[250_000] * 6,
),
},
"2026-02-05",
strategy,
daily_features_by_ticker={
"CORE": {
"gap_pct": 0.02,
"avg_daily_vol_14d": 2_000_000.0,
"avg_dollar_vol_30d": 1_000_000_000.0,
"ret_5d": 0.12,
"entropy_20d": 0.40,
},
"EVT": {
"gap_pct": 0.03,
"avg_daily_vol_14d": 1_500_000.0,
"avg_dollar_vol_30d": 600_000_000.0,
"ret_5d": 0.08,
"entropy_20d": 0.45,
"event_flag": True,
"event_score": 2.0,
"event_types": ["earnings_release"],
},
"LQ": {
"gap_pct": 0.01,
"avg_daily_vol_14d": 3_000_000.0,
"avg_dollar_vol_30d": 2_000_000_000.0,
"ret_5d": 0.03,
"entropy_20d": 0.40,
},
},
)
assert [trade.ticker for trade in day.trades] == ["CORE", "LQ"]
assert [trade.trade_sleeve for trade in day.trades] == ["core", "event_day_liquid"]
assert day.capital_deployed == 10_000.0
def test_event_day_liquid_activation_can_use_broader_raw_event_types_than_core_event_filters() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_morning_gain_pct=0.04,
min_confirmation_return_pct=0.003,
top_n=1,
use_five_sleeves=False,
slippage_bps=0.0,
daily_budget_reset=True,
initial_capital=10_000.0,
candidate_allowed_event_types=["earnings_release"],
use_event_day_liquid_sleeve=True,
event_day_liquid_capital_fraction=0.25,
event_day_liquid_max_positions=1,
event_day_liquid_allowed_event_types=["unknown"],
event_day_liquid_min_event_names=1,
event_day_liquid_min_event_score=1.0,
event_day_liquid_min_gain_pct=0.005,
event_day_liquid_max_gain_pct=0.03,
event_day_liquid_min_confirmation_return_pct=0.003,
event_day_liquid_min_entry_dollar_volume=30_000_000.0,
event_day_liquid_min_avg_dollar_vol_30d=500_000_000.0,
event_day_liquid_max_entropy_20d=0.80,
event_day_liquid_min_support_score=0.50,
liquid_largecap_min_gain_pct=0.005,
liquid_largecap_max_gain_pct=0.03,
liquid_largecap_min_confirmation_return_pct=0.003,
liquid_largecap_min_entry_dollar_volume=30_000_000.0,
liquid_largecap_min_avg_dollar_vol_30d=500_000_000.0,
liquid_largecap_max_entropy_20d=0.80,
)
day = simulate_day(
{
"CORE": _bars(
"2026-02-06",
open_price=100.0,
closes=[100.5, 102.0, 103.2, 105.0, 105.6, 106.0],
volumes=[250_000] * 6,
),
"RAW_EVT": _bars(
"2026-02-06",
open_price=50.0,
closes=[50.2, 50.9, 51.6, 52.2, 52.5, 52.9],
volumes=[250_000] * 6,
),
"LQ": _bars(
"2026-02-06",
open_price=200.0,
closes=[200.4, 201.0, 201.7, 202.6, 202.9, 203.5],
volumes=[250_000] * 6,
),
},
"2026-02-06",
strategy,
daily_features_by_ticker={
"CORE": {
"gap_pct": 0.02,
"avg_daily_vol_14d": 2_000_000.0,
"avg_dollar_vol_30d": 1_000_000_000.0,
"ret_5d": 0.12,
"entropy_20d": 0.40,
},
"RAW_EVT": {
"gap_pct": 0.03,
"avg_daily_vol_14d": 1_500_000.0,
"avg_dollar_vol_30d": 600_000_000.0,
"ret_5d": 0.08,
"entropy_20d": 0.45,
"event_flag": True,
"event_score": 2.0,
"event_types": ["unknown"],
},
"LQ": {
"gap_pct": 0.01,
"avg_daily_vol_14d": 3_000_000.0,
"avg_dollar_vol_30d": 2_000_000_000.0,
"ret_5d": 0.03,
"entropy_20d": 0.40,
},
},
)
assert [trade.ticker for trade in day.trades] == ["CORE", "LQ"]
assert [trade.trade_sleeve for trade in day.trades] == ["core", "event_day_liquid"]
def test_simulate_day_can_add_sector_etf_proxy_sleeve_from_liquid_cluster() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_morning_gain_pct=0.04,
min_confirmation_return_pct=0.003,
top_n=1,
slippage_bps=0.0,
daily_budget_reset=True,
initial_capital=10_000.0,
use_sector_etf_sleeve=True,
sector_etf_capital_fraction=0.25,
sector_etf_max_positions=1,
sector_etf_min_sector_score=0.20,
liquid_cluster_min_members=2,
liquid_cluster_min_gain_pct=0.015,
liquid_cluster_max_gain_pct=0.04,
liquid_cluster_min_confirmation_return_pct=0.003,
liquid_cluster_min_entry_dollar_volume=30_000_000.0,
liquid_cluster_min_avg_dollar_vol_30d=300_000_000.0,
liquid_cluster_min_volume_ratio_14d=0.10,
liquid_cluster_max_entropy_20d=0.80,
liquid_cluster_min_sector_avg_confirmation_return_pct=0.003,
liquid_cluster_min_sector_total_entry_dollar_volume=100_000_000.0,
)
day = simulate_day(
{
"CORE": _bars(
"2026-02-04",
open_price=100.0,
closes=[100.5, 102.0, 103.0, 106.0, 106.4, 107.0],
volumes=[250_000] * 6,
),
"CL_A": _bars(
"2026-02-04",
open_price=50.0,
closes=[50.2, 50.7, 50.9, 51.2, 51.3, 51.4],
volumes=[400_000] * 6,
),
"CL_B": _bars(
"2026-02-04",
open_price=60.0,
closes=[60.2, 60.7, 60.9, 61.3, 61.4, 61.5],
volumes=[350_000] * 6,
),
},
"2026-02-04",
strategy,
daily_features_by_ticker={
"CORE": {
"gap_pct": 0.03,
"avg_daily_vol_14d": 2_000_000.0,
"avg_dollar_vol_30d": 900_000_000.0,
"ret_5d": 0.12,
"entropy_20d": 0.40,
},
"CL_A": {
"gap_pct": 0.015,
"avg_daily_vol_14d": 3_000_000.0,
"avg_dollar_vol_30d": 800_000_000.0,
"ret_5d": 0.05,
"entropy_20d": 0.45,
},
"CL_B": {
"gap_pct": 0.012,
"avg_daily_vol_14d": 2_500_000.0,
"avg_dollar_vol_30d": 750_000_000.0,
"ret_5d": 0.04,
"entropy_20d": 0.48,
},
},
ticker_sectors={
"CORE": "Energy",
"CL_A": "Technology",
"CL_B": "Technology",
},
sector_proxy_bars_by_ticker={
"XLK": _bars(
"2026-02-04",
open_price=200.0,
closes=[200.3, 201.0, 201.3, 202.0, 202.1, 202.6],
volumes=[150_000] * 6,
),
},
)
assert [trade.ticker for trade in day.trades] == ["CORE", "XLK"]
assert [trade.trade_sleeve for trade in day.trades] == ["core", "sector_etf"]
assert day.trades[1].liquid_cluster_sector == "Technology"
assert day.trades[1].sector_proxy_ticker == "XLK"
assert day.capital_deployed == 10_000.0
def test_compute_morning_gains_applies_entry_dollar_volume_filter() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,

Loading…
Cancel
Save