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: if print_progress:
print(f"\n Intraday loaded: {len(all_intraday)} days") 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): if _momentum_uses_historical_intraday_first(config.strategy):
candidates = momentum_intraday_first_candidates( candidates = momentum_intraday_first_candidates(
all_intraday, all_intraday,
trading_days, trading_days,
config.strategy, config.strategy,
daily_enrichment=daily_enrichment, daily_enrichment=daily_enrichment,
ticker_sectors=ticker_sectors,
max_per_day=config.strategy.candidate_final_max_per_day, max_per_day=config.strategy.candidate_final_max_per_day,
) )
else: else:
@ -609,7 +612,7 @@ async def build_momentum_research_context(
context = MomentumResearchContext( context = MomentumResearchContext(
config=config, config=config,
tickers=tickers, tickers=tickers,
ticker_sectors=await _load_ticker_sectors_with_oracle(tickers, client), ticker_sectors=ticker_sectors,
trading_days=trading_days, trading_days=trading_days,
daily_bars=daily_bars, daily_bars=daily_bars,
all_intraday=all_intraday, all_intraday=all_intraday,
@ -680,6 +683,7 @@ def simulate_momentum_params(
trading_days, trading_days,
strategy, strategy,
daily_enrichment=context.daily_enrichment, daily_enrichment=context.daily_enrichment,
ticker_sectors=context.ticker_sectors,
max_per_day=strategy.candidate_final_max_per_day, max_per_day=strategy.candidate_final_max_per_day,
) )
else: 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) _prior_lookback = int(getattr(orb_params, "prior_event_lookback_days", 0) or 0)
if _prior_lookback > 0: if _prior_lookback > 0:
all_tickers = list({t for day_tickers in candidates.values() for t in day_tickers}) 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( 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") print(f" Prior-event coverage: {len(event_features)} tickers with events")
else: 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) _prior_lookback_sw = int(getattr(orb_params, "prior_event_lookback_days", 0) or 0)
if _prior_lookback_sw > 0: if _prior_lookback_sw > 0:
all_tickers_sw = list({t for day_tickers in candidates.values() for t in day_tickers}) 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( 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") print(f" Prior-event coverage: {len(event_features)} tickers with events")
else: else:

@ -95,6 +95,7 @@ def run_sweep(
momentum_enrichment: dict | None = None, momentum_enrichment: dict | None = None,
vix_by_day: dict[str, float] | None = None, vix_by_day: dict[str, float] | None = None,
ticker_sectors: dict[str, str] | None = None, ticker_sectors: dict[str, str] | None = None,
sector_proxy_intraday_by_day: dict[str, dict[str, list[dict]]] | None = None,
) -> list[SweepResult]: ) -> list[SweepResult]:
"""Run simulation for each parameter combination. """Run simulation for each parameter combination.
@ -132,6 +133,7 @@ def run_sweep(
daily_enrichment=momentum_enrichment, daily_enrichment=momentum_enrichment,
vix_by_day=vix_by_day, vix_by_day=vix_by_day,
ticker_sectors=ticker_sectors, 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}") 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" universe_profile = "midwide-liquid-long-v1"
elif "smallcap" in snapshot_id: elif "smallcap" in snapshot_id:
universe_profile = "smallcap-liquid-long-v1" universe_profile = "smallcap-liquid-long-v1"
elif "broad" in snapshot_id:
universe_profile = "broad-liquid-long-v1"
if console: if console:
console.print(f"\n[bold yellow]Snapshot '{snapshot_id}' is stale — refreshing...[/]") console.print(f"\n[bold yellow]Snapshot '{snapshot_id}' is stale — refreshing...[/]")

@ -5,7 +5,9 @@ import argparse
import asyncio import asyncio
import datetime as dt import datetime as dt
import uuid import uuid
from pathlib import Path
import yaml
from sqlalchemy import select from sqlalchemy import select
from libs.common.config import get_settings from libs.common.config import get_settings
@ -19,13 +21,27 @@ from libs.oracle_client import FilingsService, make_oracle_client
logger = get_logger(__name__) 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( async def poll_filings(
run_id: str, run_id: str,
start_date: str | None = None, start_date: str | None = None,
end_date: str | None = None, end_date: str | None = None,
symbols: list[str] | None = None,
) -> dict[str, int]: ) -> dict[str, int]:
settings = get_settings() settings = get_settings()
symbols = settings.get_symbols() if symbols is None:
symbols = settings.get_symbols()
app_config = settings.get_app_config() app_config = settings.get_app_config()
form_types = ",".join(app_config.get("pipeline", {}).get("form_types", ["8-K", "6-K"])) form_types = ",".join(app_config.get("pipeline", {}).get("form_types", ["8-K", "6-K"]))
@ -58,66 +74,85 @@ async def poll_filings(
ticker_to_symbol = {s.ticker: s.symbol_id for s in symbol_result.scalars().all()} ticker_to_symbol = {s.ticker: s.symbol_id for s in symbol_result.scalars().all()}
for ticker in symbols: for ticker in symbols:
try: last_exc: Exception | None = None
response = await svc.search_filings( response = None
ticker, for attempt in range(3):
form_type=form_types, try:
start_date=effective_start, response = await svc.search_filings(
end_date=end_date, ticker,
) form_type=form_types,
stats["seen"] += len(response.filings) start_date=effective_start,
end_date=end_date,
for filing in response.filings:
# Check for duplicate
existing = await session.execute(
select(Document).where(
Document.accession_no == filing.accession_no,
Document.form_type == filing.form_type,
)
)
if existing.scalar_one_or_none() is not None:
stats["skipped"] += 1
continue
doc_id = make_document_id(
"sec",
f"TICKER::{ticker}",
filing.filing_date,
filing.accession_no,
) )
last_exc = None
doc = Document( break
document_id=doc_id, except Exception as exc:
source_name="sec", last_exc = exc
issuer_id=ticker_to_issuer.get(ticker), wait = 2 ** attempt # 1s, 2s, 4s
symbol_id=ticker_to_symbol.get(ticker), logger.warning(
accession_no=filing.accession_no, "poll_retry",
form_type=filing.form_type,
filing_date=dt.date.fromisoformat(filing.filing_date),
accepted_at_utc=(
dt.datetime.fromisoformat(
filing.accepted_at.replace("Z", "+00:00")
)
if filing.accepted_at
else None
),
primary_document_name=filing.primary_document,
item_numbers=filing.items if filing.items else None,
parsed_status="pending",
)
session.add(doc)
stats["written"] += 1
logger.info(
"new_filing_discovered",
ticker=ticker, ticker=ticker,
accession_no=filing.accession_no, attempt=attempt + 1,
form_type=filing.form_type, wait=wait,
filing_date=filing.filing_date, error=str(exc) or repr(exc),
exc_type=type(exc).__name__,
) )
await asyncio.sleep(wait)
except Exception as exc: if last_exc is not None:
logger.error("poll_error", ticker=ticker, error=str(exc) or repr(exc), exc_type=type(exc).__name__) logger.error("poll_error", ticker=ticker, error=str(last_exc) or repr(last_exc), exc_type=type(last_exc).__name__)
stats["errors"] += 1 stats["errors"] += 1
continue
stats["seen"] += len(response.filings)
for filing in response.filings:
# Check for duplicate
existing = await session.execute(
select(Document).where(
Document.accession_no == filing.accession_no,
Document.form_type == filing.form_type,
)
)
if existing.scalar_one_or_none() is not None:
stats["skipped"] += 1
continue
doc_id = make_document_id(
"sec",
f"TICKER::{ticker}",
filing.filing_date,
filing.accession_no,
)
doc = Document(
document_id=doc_id,
source_name="sec",
issuer_id=ticker_to_issuer.get(ticker),
symbol_id=ticker_to_symbol.get(ticker),
accession_no=filing.accession_no,
form_type=filing.form_type,
filing_date=dt.date.fromisoformat(filing.filing_date),
accepted_at_utc=(
dt.datetime.fromisoformat(
filing.accepted_at.replace("Z", "+00:00")
)
if filing.accepted_at
else None
),
primary_document_name=filing.primary_document,
item_numbers=filing.items if filing.items else None,
parsed_status="pending",
)
session.add(doc)
stats["written"] += 1
logger.info(
"new_filing_discovered",
ticker=ticker,
accession_no=filing.accession_no,
form_type=filing.form_type,
filing_date=filing.filing_date,
)
# Update job record # Update job record
job.status = "succeeded" if stats["errors"] == 0 else "partial" job.status = "succeeded" if stats["errors"] == 0 else "partial"
@ -146,13 +181,26 @@ def main() -> None:
metavar="YYYY-MM-DD", metavar="YYYY-MM-DD",
help="End date for filing search (default: today)", 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() args = parser.parse_args()
settings = get_settings() settings = get_settings()
configure_logging(settings.log_level) configure_logging(settings.log_level)
bind_job_run_id(args.run_id) 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__": 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: _meta:
name: Leader Intraday Momentum High WR Intraday First 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 id: 25
strategy_mode: momentum strategy_mode: momentum
strategy: strategy:
@ -28,14 +28,24 @@ strategy:
event_weight: 0.12 event_weight: 0.12
event_min_score: 1.0 event_min_score: 1.0
event_sleeve_soft_day_only: true 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_max_trades: 3
tail_risk_day_min_max_gain_pct: 0.025 tail_risk_day_min_max_gain_pct: 0.025
tail_risk_day_max_support_score: 1.0 tail_risk_day_max_support_score: 1.0
tail_risk_day_min_max_entropy_20d: null tail_risk_day_min_max_entropy_20d: null
tail_risk_day_min_max_confirmation_return_pct: 0.01 tail_risk_day_min_max_confirmation_return_pct: 0.01
tail_risk_day_require_no_event: true 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_exempt_largecap: true
tail_risk_day_scale: 0.55 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 max_entropy_20d: 0.86
entropy_size_scale_low: 0.78 entropy_size_scale_low: 0.78
entropy_size_scale_high: 0.86 entropy_size_scale_high: 0.86
@ -106,6 +116,27 @@ strategy:
candidate_intraday_weight_avg_dollar_vol_30d: 0.08 candidate_intraday_weight_avg_dollar_vol_30d: 0.08
candidate_intraday_moderate_liquid_reserve_slots: 1 candidate_intraday_moderate_liquid_reserve_slots: 1
candidate_intraday_moderate_liquid_reserve_trigger_below: 2 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 use_liquid_largecap_sleeve: true
liquid_largecap_weight: 0.05 liquid_largecap_weight: 0.05
use_moderate_gap_liquid_sleeve: true use_moderate_gap_liquid_sleeve: true

@ -1,7 +1,7 @@
_meta: _meta:
name: Leader Intraday Momentum Defended name: Leader Intraday Momentum High WR Intraday First Cluster Overlay Base
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. 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: 31 id: 26
strategy_mode: momentum strategy_mode: momentum
strategy: strategy:
compound_returns: false compound_returns: false
@ -10,9 +10,9 @@ strategy:
min_confirmation_return_pct: 0.005 min_confirmation_return_pct: 0.005
exit_minutes_before_close: 10 exit_minutes_before_close: 10
stop_loss_pct: null stop_loss_pct: null
trailing_stop_pct: -0.08 trailing_stop_pct: -0.07
overextended_trailing_gain_pct: 0.04 overextended_trailing_gain_pct: 0.04
overextended_trailing_stop_pct: -0.075 overextended_trailing_stop_pct: -0.065
min_morning_gain_pct: 0.015 min_morning_gain_pct: 0.015
max_morning_gain_pct: 0.06 max_morning_gain_pct: 0.06
max_gap_pct: 0.055 max_gap_pct: 0.055
@ -24,32 +24,36 @@ strategy:
max_positions_per_sector: 2 max_positions_per_sector: 2
use_five_sleeves: true use_five_sleeves: true
five_sleeve_force_count: 4 five_sleeve_force_count: 4
tail_risk_day_max_trades: 1 use_event_sleeve: true
tail_risk_day_min_max_gain_pct: 0.03 event_weight: 0.12
tail_risk_day_max_support_score: 0.25 event_min_score: 1.0
tail_risk_day_min_max_entropy_20d: 0.79 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_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_exempt_largecap: true
tail_risk_day_scale: 0.55 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 max_entropy_20d: 0.86
entropy_size_scale_low: 0.78 entropy_size_scale_low: 0.78
entropy_size_scale_high: 0.86 entropy_size_scale_high: 0.86
entropy_size_scale_min: 0.6 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 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_days: 0
recent_live_scan_min_price: 2.0 recent_live_scan_min_price: 2.0
recent_live_scan_avg_volume_min: 200000 recent_live_scan_avg_volume_min: 200000
@ -78,6 +82,7 @@ strategy:
recent_live_scan_liquid_largecap_max_entropy_20d: 0.9 recent_live_scan_liquid_largecap_max_entropy_20d: 0.9
initial_capital: 10000.0 initial_capital: 10000.0
slippage_bps: 5.0 slippage_bps: 5.0
market_regime_spy_threshold: null
candidate_source_mode: intraday_first candidate_source_mode: intraday_first
candidate_seed_threshold: 0.0075 candidate_seed_threshold: 0.0075
candidate_seed_max_per_day: 150 candidate_seed_max_per_day: 150
@ -94,6 +99,12 @@ strategy:
candidate_seed_leader_min_ret_5d: 0.15 candidate_seed_leader_min_ret_5d: 0.15
candidate_seed_leader_min_atr_pct: 0.06 candidate_seed_leader_min_atr_pct: 0.06
candidate_seed_leader_max_entropy_20d: 0.75 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_final_max_per_day: 14
candidate_intraday_rank_mode: weighted candidate_intraday_rank_mode: weighted
candidate_intraday_weight_gain: 0.10 candidate_intraday_weight_gain: 0.10
@ -103,8 +114,22 @@ strategy:
candidate_intraday_weight_gap: 0.05 candidate_intraday_weight_gap: 0.05
candidate_intraday_weight_low_entropy: 0.05 candidate_intraday_weight_low_entropy: 0.05
candidate_intraday_weight_avg_dollar_vol_30d: 0.08 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 use_liquid_largecap_sleeve: true
liquid_largecap_weight: 0.05 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_slots: 1
fallback_liquid_largecap_trigger_below: 2 fallback_liquid_largecap_trigger_below: 2
liquid_largecap_min_gain_pct: 0.004 liquid_largecap_min_gain_pct: 0.004
@ -113,6 +138,26 @@ strategy:
liquid_largecap_min_entry_dollar_volume: 50000000.0 liquid_largecap_min_entry_dollar_volume: 50000000.0
liquid_largecap_min_avg_dollar_vol_30d: 2000000000.0 liquid_largecap_min_avg_dollar_vol_30d: 2000000000.0
liquid_largecap_max_entropy_20d: 0.87 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: universe:
source: broad source: broad
min_price: 10.0 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: _meta:
id: 34 id: 105
name: "ORB Gainers V23 Safe v6" name: "ORB Gainers V24.1 Candidate Entry Cap"
status: experimental status: research
parent: orb_gainers_v23_safe_v4 live_readiness: experimental
parent: orb_gainers_v24_quality_overlay
description: > description: >
V23 Safe v6 — v4 + risk_per_trade_pct 2%→3%. Narrow candidate for V24.1. Keeps V24 unchanged except reducing
max_simultaneous_entries from 3 to 2 to lower correlated 09:35-10:15
Safe v5 (atr×0.50) 실패: +39.11% 수익이지만 DD -13.81% (v4 -11.01% 대비 +2.8pp 악화), worst_day -$509. burst risk without altering signal ranking or stop logic.
Hypothesis: atr 조정이 아닌 per-trade risk 증가가 더 효율적.
v6 가설: 2%→3% risk 증가 시 partial_exit + rolling_loss + 2-simultaneous 안전장치가
DD를 V23 기준(-12.91%) 이하로 유지하면서 수익을 +50~55%로 끌어올릴 수 있는가?
strategy_mode: orb strategy_mode: orb
@ -19,14 +16,18 @@ orb_strategy:
live_readiness: experimental live_readiness: experimental
orb_minutes: 5 orb_minutes: 5
sim_bar_minutes: 5 sim_bar_minutes: 5
entry_direction: long_only entry_direction: long_only
order_timeout_minutes: 45 order_timeout_minutes: 45
allow_doji_breakout: true allow_doji_breakout: true
allow_red_to_green_breakout: true allow_red_to_green_breakout: true
min_price: 10.0 min_price: 10.0
min_avg_dollar_volume: 25000000 min_avg_dollar_volume: 25000000
min_atr_14: 0.50 min_atr_14: 0.50
min_atr_pct: 0.04 min_atr_pct: 0.04
min_rvol: 1.5 min_rvol: 1.5
min_abs_gap_pct: 0.02 min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000 min_premarket_dollar_vol: 1500000
@ -35,42 +36,51 @@ orb_strategy:
min_candidates_to_trade: 1 min_candidates_to_trade: 1
ticker_cooldown_days: 0 ticker_cooldown_days: 0
max_gap_pct: 0.04 max_gap_pct: 0.04
min_candidate_breadth: 0.60 min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015 market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ market_regime_ticker: QQQ
rolling_loss_days: 7 rolling_loss_days: 7
rolling_loss_threshold: -0.02 rolling_loss_threshold: -0.07
max_simultaneous_entries: 2 max_simultaneous_entries: 2
min_breakout_rel_vol: 1.2 min_breakout_rel_vol: 1.2
weight_rvol: 0.35 weight_rvol: 0.35
weight_gap: 0.20 weight_gap: 0.20
weight_dollar_vol: 0.05 weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25 weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0 weight_body_ratio: 0.0
weight_momentum: 0.15 weight_momentum: 0.15
weight_obv_slope: 0.05
atr_stop_multiplier: 0.75 atr_stop_multiplier: 0.75
breakeven_at_r: 1.0 breakeven_at_r: 1.0
trailing_at_r: 1.0 trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8 trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0 trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3 trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 1.0
partial_exit_at_r: 99.0
partial_exit_pct: 0.50 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 max_position_pct: 0.70
daily_max_loss_pct: 0.02 daily_max_loss_pct: 0.05
max_stops_per_day: 3 max_stops_per_day: 5
exit_minutes_before_close: 5 exit_minutes_before_close: 5
slippage_bps: 5.0 slippage_bps: 5.0
initial_capital: 10000 initial_capital: 10000
compound_returns: false compound_returns: false
daily_budget_reset: true daily_budget_reset: true
settlement_days: 1 settlement_days: 1
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50 drawdown_governor_threshold: 0.025
streak_sizing_win_bonus: 0.0 drawdown_governor_min_scale: 0.30
streak_sizing_max: 1.0
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe: universe:
source: midlarge 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: _meta:
id: 36 id: 101
name: "ORB Gainers V23 Safe v8" name: "ORB Gainers V24.1 Candidate W0.02"
status: experimental status: research
parent: orb_gainers_v23_safe_v7 live_readiness: experimental
parent: orb_gainers_v24_quality_overlay
description: > description: >
V23 Safe v8 — v7 + risk_per_trade_pct 4%→5% (V23 level). 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
Risk sweep trend (2026-04-21): prefers a lighter accumulation bias.
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 파라미터만 제거)
strategy_mode: orb strategy_mode: orb
@ -20,14 +16,18 @@ orb_strategy:
live_readiness: experimental live_readiness: experimental
orb_minutes: 5 orb_minutes: 5
sim_bar_minutes: 5 sim_bar_minutes: 5
entry_direction: long_only entry_direction: long_only
order_timeout_minutes: 45 order_timeout_minutes: 45
allow_doji_breakout: true allow_doji_breakout: true
allow_red_to_green_breakout: true allow_red_to_green_breakout: true
min_price: 10.0 min_price: 10.0
min_avg_dollar_volume: 25000000 min_avg_dollar_volume: 25000000
min_atr_14: 0.50 min_atr_14: 0.50
min_atr_pct: 0.04 min_atr_pct: 0.04
min_rvol: 1.5 min_rvol: 1.5
min_abs_gap_pct: 0.02 min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000 min_premarket_dollar_vol: 1500000
@ -36,42 +36,51 @@ orb_strategy:
min_candidates_to_trade: 1 min_candidates_to_trade: 1
ticker_cooldown_days: 0 ticker_cooldown_days: 0
max_gap_pct: 0.04 max_gap_pct: 0.04
min_candidate_breadth: 0.60 min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015 market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ market_regime_ticker: QQQ
rolling_loss_days: 7 rolling_loss_days: 7
rolling_loss_threshold: -0.02 rolling_loss_threshold: -0.07
max_simultaneous_entries: 2 max_simultaneous_entries: 3
min_breakout_rel_vol: 1.2 min_breakout_rel_vol: 1.2
weight_rvol: 0.35 weight_rvol: 0.35
weight_gap: 0.20 weight_gap: 0.20
weight_dollar_vol: 0.05 weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25 weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0 weight_body_ratio: 0.0
weight_momentum: 0.15 weight_momentum: 0.15
weight_obv_slope: 0.02
atr_stop_multiplier: 0.75 atr_stop_multiplier: 0.75
breakeven_at_r: 1.0 breakeven_at_r: 1.0
trailing_at_r: 1.0 trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8 trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0 trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3 trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 1.0
partial_exit_at_r: 99.0
partial_exit_pct: 0.50 partial_exit_pct: 0.50
# === KEY CHANGE: 4%→5% per-trade risk (V23 level) ===
risk_per_trade_pct: 0.05 risk_per_trade_pct: 0.05
max_position_pct: 0.70 max_position_pct: 0.70
daily_max_loss_pct: 0.02 daily_max_loss_pct: 0.05
max_stops_per_day: 3 max_stops_per_day: 5
exit_minutes_before_close: 5 exit_minutes_before_close: 5
slippage_bps: 5.0 slippage_bps: 5.0
initial_capital: 10000 initial_capital: 10000
compound_returns: false compound_returns: false
daily_budget_reset: true daily_budget_reset: true
settlement_days: 1 settlement_days: 1
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50 drawdown_governor_threshold: 0.025
streak_sizing_win_bonus: 0.0 drawdown_governor_min_scale: 0.30
streak_sizing_max: 1.0
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe: universe:
source: midlarge source: midlarge

@ -1,21 +1,13 @@
_meta: _meta:
id: 32 id: 102
name: "ORB Gainers V23 Safe v4" name: "ORB Gainers V24.1 Candidate W0.03"
status: validated status: research
parent: orb_gainers_v23_safe_v2 live_readiness: experimental
parent: orb_gainers_v24_quality_overlay
description: > description: >
V23 Safe v4 — v2 + rolling_loss_days 5→7. 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
v2 max DD 원인: Nov 21 손실 후 rolling window 5일 만료로 Dec 2에 재진입 허용. prefers a lighter accumulation bias.
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건
strategy_mode: orb strategy_mode: orb
@ -24,14 +16,18 @@ orb_strategy:
live_readiness: experimental live_readiness: experimental
orb_minutes: 5 orb_minutes: 5
sim_bar_minutes: 5 sim_bar_minutes: 5
entry_direction: long_only entry_direction: long_only
order_timeout_minutes: 45 order_timeout_minutes: 45
allow_doji_breakout: true allow_doji_breakout: true
allow_red_to_green_breakout: true allow_red_to_green_breakout: true
min_price: 10.0 min_price: 10.0
min_avg_dollar_volume: 25000000 min_avg_dollar_volume: 25000000
min_atr_14: 0.50 min_atr_14: 0.50
min_atr_pct: 0.04 min_atr_pct: 0.04
min_rvol: 1.5 min_rvol: 1.5
min_abs_gap_pct: 0.02 min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000 min_premarket_dollar_vol: 1500000
@ -40,41 +36,51 @@ orb_strategy:
min_candidates_to_trade: 1 min_candidates_to_trade: 1
ticker_cooldown_days: 0 ticker_cooldown_days: 0
max_gap_pct: 0.04 max_gap_pct: 0.04
min_candidate_breadth: 0.60 min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015 market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ market_regime_ticker: QQQ
rolling_loss_days: 7 rolling_loss_days: 7
rolling_loss_threshold: -0.02 rolling_loss_threshold: -0.07
max_simultaneous_entries: 2 max_simultaneous_entries: 3
min_breakout_rel_vol: 1.2 min_breakout_rel_vol: 1.2
weight_rvol: 0.35 weight_rvol: 0.35
weight_gap: 0.20 weight_gap: 0.20
weight_dollar_vol: 0.05 weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25 weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0 weight_body_ratio: 0.0
weight_momentum: 0.15 weight_momentum: 0.15
weight_obv_slope: 0.03
atr_stop_multiplier: 0.75 atr_stop_multiplier: 0.75
breakeven_at_r: 1.0 breakeven_at_r: 1.0
trailing_at_r: 1.0 trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8 trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0 trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3 trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 1.0
partial_exit_at_r: 99.0
partial_exit_pct: 0.50 partial_exit_pct: 0.50
risk_per_trade_pct: 0.02
risk_per_trade_pct: 0.05
max_position_pct: 0.70 max_position_pct: 0.70
daily_max_loss_pct: 0.02 daily_max_loss_pct: 0.05
max_stops_per_day: 3 max_stops_per_day: 5
exit_minutes_before_close: 5 exit_minutes_before_close: 5
slippage_bps: 5.0 slippage_bps: 5.0
initial_capital: 10000 initial_capital: 10000
compound_returns: false compound_returns: false
daily_budget_reset: true daily_budget_reset: true
settlement_days: 1 settlement_days: 1
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50 drawdown_governor_threshold: 0.025
streak_sizing_win_bonus: 0.0 drawdown_governor_min_scale: 0.30
streak_sizing_max: 1.0
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe: universe:
source: midlarge source: midlarge

@ -1,18 +1,13 @@
_meta: _meta:
id: 35 id: 103
name: "ORB Gainers V23 Safe v7" name: "ORB Gainers V24.1 Candidate W0.04"
status: experimental status: research
parent: orb_gainers_v23_safe_v6 live_readiness: experimental
parent: orb_gainers_v24_quality_overlay
description: > description: >
V23 Safe v7 — v6 + risk_per_trade_pct 3%→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
Safe v6 breakthrough (2026-04-21): risk 2%→3% dramatically improved ALL metrics: prefers a lighter accumulation bias.
+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%는 중간 지점 탐색.
strategy_mode: orb strategy_mode: orb
@ -21,14 +16,18 @@ orb_strategy:
live_readiness: experimental live_readiness: experimental
orb_minutes: 5 orb_minutes: 5
sim_bar_minutes: 5 sim_bar_minutes: 5
entry_direction: long_only entry_direction: long_only
order_timeout_minutes: 45 order_timeout_minutes: 45
allow_doji_breakout: true allow_doji_breakout: true
allow_red_to_green_breakout: true allow_red_to_green_breakout: true
min_price: 10.0 min_price: 10.0
min_avg_dollar_volume: 25000000 min_avg_dollar_volume: 25000000
min_atr_14: 0.50 min_atr_14: 0.50
min_atr_pct: 0.04 min_atr_pct: 0.04
min_rvol: 1.5 min_rvol: 1.5
min_abs_gap_pct: 0.02 min_abs_gap_pct: 0.02
min_premarket_dollar_vol: 1500000 min_premarket_dollar_vol: 1500000
@ -37,42 +36,51 @@ orb_strategy:
min_candidates_to_trade: 1 min_candidates_to_trade: 1
ticker_cooldown_days: 0 ticker_cooldown_days: 0
max_gap_pct: 0.04 max_gap_pct: 0.04
min_candidate_breadth: 0.60 min_candidate_breadth: 0.60
market_regime_spy_threshold: 0.0015 market_regime_spy_threshold: 0.0015
market_regime_ticker: QQQ market_regime_ticker: QQQ
rolling_loss_days: 7 rolling_loss_days: 7
rolling_loss_threshold: -0.02 rolling_loss_threshold: -0.07
max_simultaneous_entries: 2 max_simultaneous_entries: 3
min_breakout_rel_vol: 1.2 min_breakout_rel_vol: 1.2
weight_rvol: 0.35 weight_rvol: 0.35
weight_gap: 0.20 weight_gap: 0.20
weight_dollar_vol: 0.05 weight_dollar_vol: 0.05
weight_premarket_dollar_vol: 0.25 weight_premarket_dollar_vol: 0.25
weight_body_ratio: 0.0 weight_body_ratio: 0.0
weight_momentum: 0.15 weight_momentum: 0.15
weight_obv_slope: 0.04
atr_stop_multiplier: 0.75 atr_stop_multiplier: 0.75
breakeven_at_r: 1.0 breakeven_at_r: 1.0
trailing_at_r: 1.0 trailing_at_r: 1.0
trailing_stop_atr_multiplier: 0.8 trailing_stop_atr_multiplier: 0.8
trailing_tighten_at_r: 2.0 trailing_tighten_at_r: 2.0
trailing_stop_atr_multiplier_tight: 0.3 trailing_stop_atr_multiplier_tight: 0.3
partial_exit_at_r: 1.0
partial_exit_at_r: 99.0
partial_exit_pct: 0.50 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 max_position_pct: 0.70
daily_max_loss_pct: 0.02 daily_max_loss_pct: 0.05
max_stops_per_day: 3 max_stops_per_day: 5
exit_minutes_before_close: 5 exit_minutes_before_close: 5
slippage_bps: 5.0 slippage_bps: 5.0
initial_capital: 10000 initial_capital: 10000
compound_returns: false compound_returns: false
daily_budget_reset: true daily_budget_reset: true
settlement_days: 1 settlement_days: 1
drawdown_governor_threshold: 0.015
drawdown_governor_min_scale: 0.50 drawdown_governor_threshold: 0.025
streak_sizing_win_bonus: 0.0 drawdown_governor_min_scale: 0.30
streak_sizing_max: 1.0
streak_sizing_win_bonus: 0.70
streak_sizing_max: 2.5
universe: universe:
source: midlarge 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" "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": { "midlarge-liquid-long-v1-oot-2020-2021_canonical": {
"purpose": "oot", "purpose": "oot",
"refresh_policy": "manual_only", "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_MIDPLUS_LIQUID_LONG_V1 = "midplus-liquid-long-v1"
_UNIVERSE_PROFILE_MIDWIDE_LIQUID_LONG_V1 = "midwide-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_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_PROFILES: dict[str, dict[str, Any]] = {
_UNIVERSE_PROFILE_MIDLARGE_LIQUID_LONG_V1: { _UNIVERSE_PROFILE_MIDLARGE_LIQUID_LONG_V1: {
@ -70,6 +71,13 @@ _UNIVERSE_PROFILES: dict[str, dict[str, Any]] = {
"exchange": "NYSE,NASDAQ,AMEX", "exchange": "NYSE,NASDAQ,AMEX",
"exclude_types": "ETF,FUND,ADR,SPAC", "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 import os
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from uuid import uuid4
import pyarrow as pa import pyarrow as pa
import pyarrow.parquet as pq import pyarrow.parquet as pq
@ -197,7 +198,7 @@ class IntradayCache:
_INTRADAY_NEGATIVE_REASON_KEY: reason.encode(), _INTRADAY_NEGATIVE_REASON_KEY: reason.encode(),
}) })
table = pa.Table.from_pylist([], schema=schema) table = pa.Table.from_pylist([], schema=schema)
tmp = p.with_suffix(".tmp") tmp = p.with_suffix(f".{uuid4().hex}.tmp")
try: try:
pq.write_table(table, str(tmp), compression="snappy") pq.write_table(table, str(tmp), compression="snappy")
os.replace(str(tmp), str(p)) 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. """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, 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. 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 Marks each trading day within this window after qualifying events as
guidance_update event as event_flag=True, event_score=1.0.""" 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 weight_attention_wiki: float = 0.0
"""Wikipedia attention weight for actual stocks-in-play ranking.""" """Wikipedia attention weight for actual stocks-in-play ranking."""

@ -778,6 +778,9 @@ def write_results(
"sector_scaler": r.sector_scaler, "sector_scaler": r.sector_scaler,
"tail_risk_scaler": r.tail_risk_scaler, "tail_risk_scaler": r.tail_risk_scaler,
"is_soft_day": r.is_soft_day, "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 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 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 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_wiki_spike = getattr(strategy, "candidate_min_attention_wiki_spike_10d", None) if strategy else None
min_article_count = ( min_article_count = (
getattr(strategy, "candidate_min_attention_article_count_3d", None) 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: if gap_pct is None or gap_pct < threshold:
continue continue
event_flag = bool(info.get("event_flag")) event_flag, event_score = _momentum_effective_event_state(info, allowed_event_types)
event_score = float(info.get("event_score") or 0.0)
wiki_spike = float(info.get("attention_wiki_spike_10d") or 0.0) wiki_spike = float(info.get("attention_wiki_spike_10d") or 0.0)
article_count = int(info.get("attention_article_count_3d") or 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) us_article_count = int(info.get("attention_us_article_count_3d") or 0)
@ -707,6 +707,46 @@ def momentum_pre_screen_candidates(
return result 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( def _momentum_candidate_signal_passes(
info: dict, info: dict,
strategy, strategy,
@ -716,13 +756,13 @@ def _momentum_candidate_signal_passes(
return True return True
require_event_flag = bool(getattr(strategy, "candidate_require_event_flag", False)) require_event_flag = bool(getattr(strategy, "candidate_require_event_flag", False))
min_event_score = getattr(strategy, "candidate_min_event_score", None) 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_wiki_spike = getattr(strategy, "candidate_min_attention_wiki_spike_10d", None)
min_article_count = getattr(strategy, "candidate_min_attention_article_count_3d", 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_us_article_count = getattr(strategy, "candidate_min_attention_us_article_count_3d", None)
min_resolver_conf = getattr(strategy, "candidate_min_attention_resolver_confidence", None) min_resolver_conf = getattr(strategy, "candidate_min_attention_resolver_confidence", None)
event_flag = bool(info.get("event_flag")) event_flag, event_score = _momentum_effective_event_state(info, allowed_event_types)
event_score = float(info.get("event_score") or 0.0)
wiki_spike = float(info.get("attention_wiki_spike_10d") or 0.0) wiki_spike = float(info.get("attention_wiki_spike_10d") or 0.0)
article_count = int(info.get("attention_article_count_3d") or 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) us_article_count = int(info.get("attention_us_article_count_3d") or 0)
@ -749,6 +789,7 @@ def _momentum_intraday_weighted_score(
strategy, strategy,
) -> float: ) -> float:
"""Weighted same-day candidate score for intraday-first ranking.""" """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: def _clip_unit(value: float | None, cap: float) -> float:
if value is None or cap <= 0: if value is None or cap <= 0:
@ -785,6 +826,7 @@ def _momentum_intraday_weighted_score(
if entropy_20d is not None if entropy_20d is not None
else 0.0 else 0.0
) )
support_score = _same_day_support_score(info)
event_score = float(daily_info.get("event_score") or 0.0) event_score = float(daily_info.get("event_score") or 0.0)
wiki_spike = float(daily_info.get("attention_wiki_spike_10d") 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( score += float(getattr(strategy, "candidate_intraday_weight_avg_dollar_vol_30d", 0.0) or 0.0) * _prior_dollar_vol_score(
avg_dollar_vol_30d 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( score += float(getattr(strategy, "candidate_intraday_weight_gap", 0.0) or 0.0) * _clip_unit(
gap_pct, 0.10 gap_pct, 0.10
) )
@ -818,6 +867,10 @@ def _momentum_intraday_weighted_score(
ret_5d, 0.20 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_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( score += float(getattr(strategy, "candidate_intraday_weight_event_score", 0.0) or 0.0) * _clip_unit(
event_score, 3.0 event_score, 3.0
) )
@ -830,12 +883,34 @@ def _momentum_intraday_weighted_score(
return 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( def _momentum_intraday_event_reserve_eligible(
daily_info: dict, daily_info: dict,
strategy, strategy,
) -> bool: ) -> bool:
if not bool(daily_info.get("event_flag")): if not bool(daily_info.get("event_flag")):
return False 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) min_score = getattr(strategy, "candidate_intraday_event_reserve_min_score", None)
if min_score is None: if min_score is None:
return True return True
@ -1070,6 +1145,7 @@ def momentum_intraday_first_candidates(
strategy, strategy,
*, *,
daily_enrichment: dict[str, dict[str, dict]] | None = None, daily_enrichment: dict[str, dict[str, dict]] | None = None,
ticker_sectors: dict[str, str] | None = None,
max_per_day: int | None = None, max_per_day: int | None = None,
) -> dict[str, list[str]]: ) -> dict[str, list[str]]:
"""Build the final momentum shortlist from entry-time intraday information. """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 already bounded the intraday fetch set. The final ranking uses only
information known by the entry / confirmation bar of the same day. 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: if max_per_day is None:
max_per_day = max(1, int(getattr(strategy, "candidate_final_max_per_day", 30) or 30)) 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, day,
daily_features_by_ticker=daily_info_by_ticker, daily_features_by_ticker=daily_info_by_ticker,
) )
gains = _annotate_sector_thrust_features(
gains,
shortlist_strategy,
ticker_sectors,
)
if not gains: if not gains:
continue continue
filtered_gains = { filtered_gains = {
@ -1163,9 +1248,47 @@ def momentum_intraday_first_candidates(
daily_info_by_ticker, daily_info_by_ticker,
strategy, strategy,
max_per_day=max_per_day, max_per_day=max_per_day,
)
continue
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 continue
picks = _select_momentum_sleeves(rankable_gains, shortlist_strategy, ticker_sectors=None) picks = _select_momentum_sleeves(
rankable_gains,
shortlist_strategy,
ticker_sectors=ticker_sectors,
)
if picks: if picks:
ranked_tickers = _apply_momentum_intraday_event_reserve( ranked_tickers = _apply_momentum_intraday_event_reserve(
[ticker for ticker, _sleeve in picks], [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, _normalize_candidate_map,
_momentum_intraday_seed_candidates, _momentum_intraday_seed_candidates,
_momentum_strategy_uses_candidate_stage_catalyst, _momentum_strategy_uses_candidate_stage_catalyst,
_momentum_strategy_uses_daily_enrichment,
_momentum_strategy_requires_regime_ticker_daily, _momentum_strategy_requires_regime_ticker_daily,
_momentum_strategy_uses_attention, _momentum_strategy_uses_attention,
_momentum_strategy_uses_catalyst, _momentum_strategy_uses_catalyst,
_momentum_strategy_uses_sector_labels,
_momentum_strategy_uses_sector_proxies,
_retain_recent_intraday_shortlist, _retain_recent_intraday_shortlist,
_recent_intraday_first_candidates, _recent_intraday_first_candidates,
_strategy_for_recent_live_scan, _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 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: def test_momentum_strategy_uses_event_reserve_and_event_sleeve_for_fetch_activation() -> None:
reserve_strategy = StrategyParams(candidate_intraday_event_reserve_slots=1) reserve_strategy = StrategyParams(candidate_intraday_event_reserve_slots=1)
sleeve_strategy = StrategyParams(use_event_sleeve=True, event_weight=0.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_catalyst(reserve_strategy) is True
assert _momentum_strategy_uses_candidate_stage_catalyst(reserve_strategy) is False 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(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: 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 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: def test_momentum_intraday_seed_candidates_only_apply_signal_filters_in_final_pass() -> None:
daily_bars = { daily_bars = {
"AAA": [ "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: 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 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"]} 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: def test_momentum_intraday_first_candidates_uses_entry_time_info_only() -> None:
strategy = StrategyParams( strategy = StrategyParams(
candidate_source_mode="intraday_first", 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"]} 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: def test_momentum_intraday_first_candidates_can_use_weighted_ranking() -> None:
strategy = StrategyParams( strategy = StrategyParams(
candidate_source_mode="intraday_first", 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"]} 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: def test_momentum_intraday_first_candidates_can_replace_tail_with_event_reserve() -> None:
strategy = StrategyParams( strategy = StrategyParams(
candidate_source_mode="intraday_first", 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")] 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: def test_simulate_day_can_enable_event_sleeve_only_on_soft_days() -> None:
strategy = StrategyParams( strategy = StrategyParams(
entry_minutes_after_open=10, 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 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: def test_simulate_day_keeps_full_size_for_supported_single_name() -> None:
strategy = StrategyParams( strategy = StrategyParams(
entry_minutes_after_open=15, 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 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: def test_compute_morning_gains_applies_entry_dollar_volume_filter() -> None:
strategy = StrategyParams( strategy = StrategyParams(
entry_minutes_after_open=10, entry_minutes_after_open=10,

Loading…
Cancel
Save