|
|
|
@ -72,6 +72,7 @@ from libs.intraday.screener import (
|
|
|
|
resolve_universe,
|
|
|
|
resolve_universe,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
from libs.intraday.simulator import (
|
|
|
|
from libs.intraday.simulator import (
|
|
|
|
|
|
|
|
SECTOR_PROXY_TICKERS,
|
|
|
|
_bar_at_offset,
|
|
|
|
_bar_at_offset,
|
|
|
|
_dollar_volume_up_to_bar,
|
|
|
|
_dollar_volume_up_to_bar,
|
|
|
|
_market_open_ts,
|
|
|
|
_market_open_ts,
|
|
|
|
@ -380,6 +381,88 @@ def _orb_strategy_uses_vix(params: ORBStrategyParams) -> bool:
|
|
|
|
) or params.vix_size_scale_min != 1.0
|
|
|
|
) or params.vix_size_scale_min != 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _prefetch_prior_event_features_db(
|
|
|
|
|
|
|
|
tickers: list[str],
|
|
|
|
|
|
|
|
trading_days: list[str],
|
|
|
|
|
|
|
|
lookback_calendar_days: int = 7,
|
|
|
|
|
|
|
|
event_types: tuple[str, ...] = ("earnings_release", "guidance_update"),
|
|
|
|
|
|
|
|
) -> dict[str, dict[str, dict]]:
|
|
|
|
|
|
|
|
"""Bulk-fetch prior earnings/guidance events from DB and build event feature map.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For each ticker, finds events of the specified types in the DB events table
|
|
|
|
|
|
|
|
within [start - lookback_calendar_days, end], then marks each trading day
|
|
|
|
|
|
|
|
within `lookback_calendar_days` after an event as event_flag=True, event_score=1.0.
|
|
|
|
|
|
|
|
Trading days with no recent event retain event_flag=False, event_score=0.0.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
import asyncpg
|
|
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not tickers or not trading_days:
|
|
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start_date = trading_days[0]
|
|
|
|
|
|
|
|
end_date = trading_days[-1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Expand lookback window to capture events before the first trading day
|
|
|
|
|
|
|
|
start_dt = datetime.strptime(start_date, "%Y-%m-%d").date()
|
|
|
|
|
|
|
|
end_dt = datetime.strptime(end_date, "%Y-%m-%d").date()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# symbol_id format: SYM::{TICKER}::US
|
|
|
|
|
|
|
|
symbol_ids = [f"SYM::{t}::US" for t in tickers]
|
|
|
|
|
|
|
|
ticker_from_sid = {f"SYM::{t}::US": t for t in tickers}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dsn = get_settings().postgres_dsn.replace("+asyncpg", "")
|
|
|
|
|
|
|
|
conn = await asyncpg.connect(dsn=dsn)
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
SELECT symbol_id, event_date::text AS event_date
|
|
|
|
|
|
|
|
FROM events
|
|
|
|
|
|
|
|
WHERE symbol_id = ANY($1)
|
|
|
|
|
|
|
|
AND event_type = ANY($2)
|
|
|
|
|
|
|
|
AND event_date BETWEEN $3 AND $4
|
|
|
|
|
|
|
|
ORDER BY symbol_id, event_date
|
|
|
|
|
|
|
|
""",
|
|
|
|
|
|
|
|
symbol_ids,
|
|
|
|
|
|
|
|
list(event_types),
|
|
|
|
|
|
|
|
(start_dt - timedelta(days=lookback_calendar_days)),
|
|
|
|
|
|
|
|
end_dt,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
|
|
|
await conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Group event dates per ticker
|
|
|
|
|
|
|
|
from collections import defaultdict
|
|
|
|
|
|
|
|
events_by_ticker: dict[str, list[str]] = defaultdict(list)
|
|
|
|
|
|
|
|
for row in rows:
|
|
|
|
|
|
|
|
ticker = ticker_from_sid.get(row["symbol_id"])
|
|
|
|
|
|
|
|
if ticker:
|
|
|
|
|
|
|
|
events_by_ticker[ticker].append(row["event_date"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Build feature map: for each trading day, mark True if within lookback window after an event
|
|
|
|
|
|
|
|
result: dict[str, dict[str, dict]] = {}
|
|
|
|
|
|
|
|
for ticker in tickers:
|
|
|
|
|
|
|
|
event_dates = events_by_ticker.get(ticker, [])
|
|
|
|
|
|
|
|
if not event_dates:
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
ticker_map: dict[str, dict] = {}
|
|
|
|
|
|
|
|
for td_str in trading_days:
|
|
|
|
|
|
|
|
td = datetime.strptime(td_str, "%Y-%m-%d").date()
|
|
|
|
|
|
|
|
# Check if any event falls in [td - lookback_calendar_days, td - 1]
|
|
|
|
|
|
|
|
has_prior_event = False
|
|
|
|
|
|
|
|
for ev_str in event_dates:
|
|
|
|
|
|
|
|
ev = datetime.strptime(ev_str, "%Y-%m-%d").date()
|
|
|
|
|
|
|
|
days_ago = (td - ev).days
|
|
|
|
|
|
|
|
if 1 <= days_ago <= lookback_calendar_days:
|
|
|
|
|
|
|
|
has_prior_event = True
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
if has_prior_event:
|
|
|
|
|
|
|
|
ticker_map[td_str] = {"event_flag": True, "event_score": 1.0}
|
|
|
|
|
|
|
|
if ticker_map:
|
|
|
|
|
|
|
|
result[ticker] = ticker_map
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _merge_orb_event_features(
|
|
|
|
def _merge_orb_event_features(
|
|
|
|
enrichment: dict[str, dict[str, dict]],
|
|
|
|
enrichment: dict[str, dict[str, dict]],
|
|
|
|
event_features: dict[str, dict[str, dict]],
|
|
|
|
event_features: dict[str, dict[str, dict]],
|
|
|
|
@ -466,6 +549,13 @@ def _momentum_strategy_uses_daily_enrichment(params: StrategyParams) -> bool:
|
|
|
|
(params.entropy_size_scale_low, None),
|
|
|
|
(params.entropy_size_scale_low, None),
|
|
|
|
(params.entropy_size_scale_high, None),
|
|
|
|
(params.entropy_size_scale_high, None),
|
|
|
|
(params.use_moderate_gap_liquid_sleeve, False),
|
|
|
|
(params.use_moderate_gap_liquid_sleeve, False),
|
|
|
|
|
|
|
|
(params.use_liquid_cluster_engine, False),
|
|
|
|
|
|
|
|
(params.use_sector_etf_sleeve, False),
|
|
|
|
|
|
|
|
(params.liquid_cluster_require_special_liquidity_gate, False),
|
|
|
|
|
|
|
|
(params.liquid_cluster_min_avg_dollar_vol_30d, None),
|
|
|
|
|
|
|
|
(params.liquid_cluster_max_avg_dollar_vol_30d, None),
|
|
|
|
|
|
|
|
(params.liquid_cluster_min_volume_ratio_14d, None),
|
|
|
|
|
|
|
|
(params.liquid_cluster_max_entropy_20d, None),
|
|
|
|
(params.candidate_seed_moderate_liquid_overlay_slots, 0),
|
|
|
|
(params.candidate_seed_moderate_liquid_overlay_slots, 0),
|
|
|
|
(params.candidate_intraday_moderate_liquid_reserve_slots, 0),
|
|
|
|
(params.candidate_intraday_moderate_liquid_reserve_slots, 0),
|
|
|
|
(params.market_regime_gap_threshold, None),
|
|
|
|
(params.market_regime_gap_threshold, None),
|
|
|
|
@ -480,6 +570,23 @@ def _momentum_strategy_uses_daily_enrichment(params: StrategyParams) -> bool:
|
|
|
|
) or params.use_five_sleeves
|
|
|
|
) or params.use_five_sleeves
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _momentum_strategy_uses_sector_labels(params: StrategyParams) -> bool:
|
|
|
|
|
|
|
|
return bool(
|
|
|
|
|
|
|
|
params.max_positions_per_sector
|
|
|
|
|
|
|
|
or params.use_sector_thrust_sleeve
|
|
|
|
|
|
|
|
or params.use_liquid_cluster_engine
|
|
|
|
|
|
|
|
or params.use_sector_etf_sleeve
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _momentum_strategy_uses_sector_proxies(params: StrategyParams) -> bool:
|
|
|
|
|
|
|
|
return bool(
|
|
|
|
|
|
|
|
params.use_sector_etf_sleeve
|
|
|
|
|
|
|
|
and params.sector_etf_capital_fraction > 0
|
|
|
|
|
|
|
|
and params.sector_etf_max_positions > 0
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _momentum_strategy_requires_regime_ticker_daily(params: StrategyParams) -> bool:
|
|
|
|
def _momentum_strategy_requires_regime_ticker_daily(params: StrategyParams) -> bool:
|
|
|
|
return any(
|
|
|
|
return any(
|
|
|
|
value is not None and value != default
|
|
|
|
value is not None and value != default
|
|
|
|
@ -496,6 +603,7 @@ def _momentum_strategy_uses_catalyst(params: StrategyParams) -> bool:
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
params.candidate_require_event_flag
|
|
|
|
params.candidate_require_event_flag
|
|
|
|
or params.candidate_min_event_score is not None
|
|
|
|
or params.candidate_min_event_score is not None
|
|
|
|
|
|
|
|
or bool(params.candidate_allowed_event_types)
|
|
|
|
or params.candidate_seed_event_overlay_slots > 0
|
|
|
|
or params.candidate_seed_event_overlay_slots > 0
|
|
|
|
or params.candidate_seed_event_min_score is not None
|
|
|
|
or params.candidate_seed_event_min_score is not None
|
|
|
|
or params.candidate_weight_event_score > 0
|
|
|
|
or params.candidate_weight_event_score > 0
|
|
|
|
@ -505,6 +613,10 @@ def _momentum_strategy_uses_catalyst(params: StrategyParams) -> bool:
|
|
|
|
or params.use_event_sleeve
|
|
|
|
or params.use_event_sleeve
|
|
|
|
or params.event_weight > 0
|
|
|
|
or params.event_weight > 0
|
|
|
|
or params.event_min_score is not None
|
|
|
|
or params.event_min_score is not None
|
|
|
|
|
|
|
|
or params.use_event_day_liquid_sleeve
|
|
|
|
|
|
|
|
or params.event_day_liquid_min_event_score is not None
|
|
|
|
|
|
|
|
or params.event_day_liquid_min_event_support_score is not None
|
|
|
|
|
|
|
|
or params.event_day_liquid_min_total_event_entry_dollar_volume is not None
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -512,11 +624,35 @@ def _momentum_strategy_uses_candidate_stage_catalyst(params: StrategyParams) ->
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
params.candidate_require_event_flag
|
|
|
|
params.candidate_require_event_flag
|
|
|
|
or params.candidate_min_event_score is not None
|
|
|
|
or params.candidate_min_event_score is not None
|
|
|
|
|
|
|
|
or bool(params.candidate_allowed_event_types)
|
|
|
|
or params.candidate_seed_event_overlay_slots > 0
|
|
|
|
or params.candidate_seed_event_overlay_slots > 0
|
|
|
|
or params.candidate_seed_event_min_score is not None
|
|
|
|
or params.candidate_seed_event_min_score is not None
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _momentum_candidate_allowed_event_types(strategy: StrategyParams) -> set[str]:
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
str(value).strip().lower()
|
|
|
|
|
|
|
|
for value in strategy.candidate_allowed_event_types
|
|
|
|
|
|
|
|
if str(value).strip()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _momentum_candidate_event_types_pass(info: dict, strategy: StrategyParams) -> bool:
|
|
|
|
|
|
|
|
allowed_event_types = _momentum_candidate_allowed_event_types(strategy)
|
|
|
|
|
|
|
|
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_strategy_uses_attention(params: StrategyParams) -> bool:
|
|
|
|
def _momentum_strategy_uses_attention(params: StrategyParams) -> bool:
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
params.candidate_weight_attention_wiki > 0
|
|
|
|
params.candidate_weight_attention_wiki > 0
|
|
|
|
@ -849,6 +985,8 @@ def _augment_momentum_seed_candidates_with_liquid_overlay(
|
|
|
|
info = enrichment.get(ticker, {}).get(day, {})
|
|
|
|
info = enrichment.get(ticker, {}).get(day, {})
|
|
|
|
if not bool(info.get("event_flag")):
|
|
|
|
if not bool(info.get("event_flag")):
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
if not _momentum_candidate_event_types_pass(info, strategy):
|
|
|
|
|
|
|
|
continue
|
|
|
|
event_score = float(info.get("event_score") or 0.0)
|
|
|
|
event_score = float(info.get("event_score") or 0.0)
|
|
|
|
if event_min_score is not None and event_score < float(event_min_score):
|
|
|
|
if event_min_score is not None and event_score < float(event_min_score):
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
@ -1451,7 +1589,7 @@ async def run(config: IntradayConfig, refresh_cache: bool = False) -> tuple:
|
|
|
|
if is_orb
|
|
|
|
if is_orb
|
|
|
|
else (
|
|
|
|
else (
|
|
|
|
await _load_ticker_sectors_with_oracle(tickers, client)
|
|
|
|
await _load_ticker_sectors_with_oracle(tickers, client)
|
|
|
|
if config.strategy.max_positions_per_sector
|
|
|
|
if _momentum_strategy_uses_sector_labels(config.strategy)
|
|
|
|
else {}
|
|
|
|
else {}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
@ -1528,27 +1666,36 @@ async def run(config: IntradayConfig, refresh_cache: bool = False) -> tuple:
|
|
|
|
candidates, daily_bars, trading_days, enrichment, orb_params
|
|
|
|
candidates, daily_bars, trading_days, enrichment, orb_params
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if _orb_strategy_uses_catalyst(orb_params):
|
|
|
|
if _orb_strategy_uses_catalyst(orb_params):
|
|
|
|
event_tickers = _orb_candidate_event_tickers(candidates, enrichment, orb_params)
|
|
|
|
_prior_lookback = int(getattr(orb_params, "prior_event_lookback_days", 0) or 0)
|
|
|
|
print(f" Fetching filing catalyst events for {len(event_tickers)} tickers...")
|
|
|
|
if _prior_lookback > 0:
|
|
|
|
_evt_last_pct = [-1]
|
|
|
|
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...")
|
|
|
|
|
|
|
|
event_features = await _prefetch_prior_event_features_db(
|
|
|
|
|
|
|
|
all_tickers, trading_days, lookback_calendar_days=_prior_lookback
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
print(f" Prior-event coverage: {len(event_features)} tickers with events")
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
event_tickers = _orb_candidate_event_tickers(candidates, enrichment, orb_params)
|
|
|
|
|
|
|
|
print(f" Fetching filing catalyst events for {len(event_tickers)} tickers...")
|
|
|
|
|
|
|
|
_evt_last_pct = [-1]
|
|
|
|
|
|
|
|
|
|
|
|
def event_progress(completed: int, total: int) -> None:
|
|
|
|
def event_progress(completed: int, total: int) -> None:
|
|
|
|
pct = int(completed / total * 10) * 10 if total > 0 else 0
|
|
|
|
pct = int(completed / total * 10) * 10 if total > 0 else 0
|
|
|
|
if pct > _evt_last_pct[0] or completed == total:
|
|
|
|
if pct > _evt_last_pct[0] or completed == total:
|
|
|
|
_evt_last_pct[0] = pct
|
|
|
|
_evt_last_pct[0] = pct
|
|
|
|
sys.stdout.write(f"\r {_make_progress_bar(completed, total)}")
|
|
|
|
sys.stdout.write(f"\r {_make_progress_bar(completed, total)}")
|
|
|
|
sys.stdout.flush()
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
|
|
|
event_features = await fetch_filing_event_features_bulk(
|
|
|
|
event_features = await fetch_filing_event_features_bulk(
|
|
|
|
event_tickers,
|
|
|
|
event_tickers,
|
|
|
|
trading_days[0],
|
|
|
|
trading_days[0],
|
|
|
|
trading_days[-1],
|
|
|
|
trading_days[-1],
|
|
|
|
client,
|
|
|
|
client,
|
|
|
|
cache=event_cache,
|
|
|
|
cache=event_cache,
|
|
|
|
concurrency=16,
|
|
|
|
concurrency=16,
|
|
|
|
progress_callback=event_progress,
|
|
|
|
progress_callback=event_progress,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
print()
|
|
|
|
print()
|
|
|
|
_merge_orb_event_features(enrichment, event_features)
|
|
|
|
_merge_orb_event_features(enrichment, event_features)
|
|
|
|
|
|
|
|
|
|
|
|
if _orb_strategy_uses_attention(orb_params):
|
|
|
|
if _orb_strategy_uses_attention(orb_params):
|
|
|
|
@ -1832,6 +1979,7 @@ async def run(config: IntradayConfig, refresh_cache: bool = False) -> tuple:
|
|
|
|
trading_days,
|
|
|
|
trading_days,
|
|
|
|
config.strategy,
|
|
|
|
config.strategy,
|
|
|
|
daily_enrichment=momentum_enrichment,
|
|
|
|
daily_enrichment=momentum_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,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
total_pairs = sum(len(v) for v in candidates.values())
|
|
|
|
total_pairs = sum(len(v) for v in candidates.values())
|
|
|
|
@ -1851,6 +1999,36 @@ async def run(config: IntradayConfig, refresh_cache: bool = False) -> tuple:
|
|
|
|
f"{total_pairs} ticker-day pairs across {len(candidates)} days"
|
|
|
|
f"{total_pairs} ticker-day pairs across {len(candidates)} days"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sector_proxy_intraday: dict[str, dict[str, list[dict]]] | None = None
|
|
|
|
|
|
|
|
if (not is_orb) and _momentum_strategy_uses_sector_proxies(config.strategy):
|
|
|
|
|
|
|
|
print(" Fetching sector ETF proxy bars...")
|
|
|
|
|
|
|
|
proxy_candidates = {day: list(SECTOR_PROXY_TICKERS) for day in trading_days}
|
|
|
|
|
|
|
|
_proxy_last_pct = [-1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def proxy_progress(completed: int, total: int, hits: int, calls: int) -> None:
|
|
|
|
|
|
|
|
if completed == 0 and calls == 0 and total > 0:
|
|
|
|
|
|
|
|
sys.stdout.write("\n")
|
|
|
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
_proxy_last_pct[0] = -1
|
|
|
|
|
|
|
|
pct = int(completed / total * 10) * 10 if total > 0 else 0
|
|
|
|
|
|
|
|
if pct > _proxy_last_pct[0] or completed == total:
|
|
|
|
|
|
|
|
_proxy_last_pct[0] = pct
|
|
|
|
|
|
|
|
sys.stdout.write(
|
|
|
|
|
|
|
|
f"\r {_make_progress_bar(completed, total)} "
|
|
|
|
|
|
|
|
f"cache:{hits} api:{calls}"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sector_proxy_intraday = await fetch_intraday_bulk(
|
|
|
|
|
|
|
|
proxy_candidates,
|
|
|
|
|
|
|
|
client,
|
|
|
|
|
|
|
|
cache,
|
|
|
|
|
|
|
|
skip_oracle_when_unhealthy=True,
|
|
|
|
|
|
|
|
concurrency=4,
|
|
|
|
|
|
|
|
progress_callback=proxy_progress,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
print(f"\n Done. {len(sector_proxy_intraday)} days with sector ETF proxy data")
|
|
|
|
|
|
|
|
|
|
|
|
if not is_orb:
|
|
|
|
if not is_orb:
|
|
|
|
# Step 5: Simulate (momentum mode still runs after full preload)
|
|
|
|
# Step 5: Simulate (momentum mode still runs after full preload)
|
|
|
|
print("\nSimulating trades...")
|
|
|
|
print("\nSimulating trades...")
|
|
|
|
@ -1870,6 +2048,7 @@ async def run(config: IntradayConfig, refresh_cache: bool = False) -> tuple:
|
|
|
|
daily_enrichment=momentum_enrichment,
|
|
|
|
daily_enrichment=momentum_enrichment,
|
|
|
|
vix_by_day=momentum_vix_by_day,
|
|
|
|
vix_by_day=momentum_vix_by_day,
|
|
|
|
ticker_sectors=ticker_sectors,
|
|
|
|
ticker_sectors=ticker_sectors,
|
|
|
|
|
|
|
|
sector_proxy_intraday_by_day=sector_proxy_intraday,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
print()
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
@ -1924,7 +2103,7 @@ async def run_with_sweep(config: IntradayConfig, sweep_path: str) -> None:
|
|
|
|
if is_orb
|
|
|
|
if is_orb
|
|
|
|
else (
|
|
|
|
else (
|
|
|
|
await _load_ticker_sectors_with_oracle(tickers, client)
|
|
|
|
await _load_ticker_sectors_with_oracle(tickers, client)
|
|
|
|
if config.strategy.max_positions_per_sector
|
|
|
|
if _momentum_strategy_uses_sector_labels(config.strategy)
|
|
|
|
else {}
|
|
|
|
else {}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
@ -1986,26 +2165,35 @@ async def run_with_sweep(config: IntradayConfig, sweep_path: str) -> None:
|
|
|
|
candidates, daily_bars, trading_days, enrichment, orb_params
|
|
|
|
candidates, daily_bars, trading_days, enrichment, orb_params
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if _orb_strategy_uses_catalyst(orb_params_sweep_check):
|
|
|
|
if _orb_strategy_uses_catalyst(orb_params_sweep_check):
|
|
|
|
event_tickers = _orb_candidate_event_tickers(candidates, enrichment, orb_params)
|
|
|
|
_prior_lookback_sw = int(getattr(orb_params, "prior_event_lookback_days", 0) or 0)
|
|
|
|
_evt_prog_last = [-1]
|
|
|
|
if _prior_lookback_sw > 0:
|
|
|
|
|
|
|
|
all_tickers_sw = list({t for day_tickers in candidates.values() for t in day_tickers})
|
|
|
|
|
|
|
|
print(f" Prefetching prior-event features from DB (D-{_prior_lookback_sw}) for {len(all_tickers_sw)} tickers...")
|
|
|
|
|
|
|
|
event_features = await _prefetch_prior_event_features_db(
|
|
|
|
|
|
|
|
all_tickers_sw, trading_days, lookback_calendar_days=_prior_lookback_sw
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
print(f" Prior-event coverage: {len(event_features)} tickers with events")
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
event_tickers = _orb_candidate_event_tickers(candidates, enrichment, orb_params)
|
|
|
|
|
|
|
|
_evt_prog_last = [-1]
|
|
|
|
|
|
|
|
|
|
|
|
def event_prog(completed: int, total: int) -> None:
|
|
|
|
def event_prog(completed: int, total: int) -> None:
|
|
|
|
pct = int(completed / total * 10) * 10 if total > 0 else 0
|
|
|
|
pct = int(completed / total * 10) * 10 if total > 0 else 0
|
|
|
|
if pct > _evt_prog_last[0] or completed == total:
|
|
|
|
if pct > _evt_prog_last[0] or completed == total:
|
|
|
|
_evt_prog_last[0] = pct
|
|
|
|
_evt_prog_last[0] = pct
|
|
|
|
sys.stdout.write(f"\r {_make_progress_bar(completed, total)}")
|
|
|
|
sys.stdout.write(f"\r {_make_progress_bar(completed, total)}")
|
|
|
|
sys.stdout.flush()
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
|
|
|
event_features = await fetch_filing_event_features_bulk(
|
|
|
|
event_features = await fetch_filing_event_features_bulk(
|
|
|
|
event_tickers,
|
|
|
|
event_tickers,
|
|
|
|
trading_days[0],
|
|
|
|
trading_days[0],
|
|
|
|
trading_days[-1],
|
|
|
|
trading_days[-1],
|
|
|
|
client,
|
|
|
|
client,
|
|
|
|
cache=event_cache,
|
|
|
|
cache=event_cache,
|
|
|
|
concurrency=16,
|
|
|
|
concurrency=16,
|
|
|
|
progress_callback=event_prog,
|
|
|
|
progress_callback=event_prog,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
print()
|
|
|
|
print()
|
|
|
|
_merge_orb_event_features(enrichment, event_features)
|
|
|
|
_merge_orb_event_features(enrichment, event_features)
|
|
|
|
if _orb_strategy_uses_vix(orb_params_sweep_check):
|
|
|
|
if _orb_strategy_uses_vix(orb_params_sweep_check):
|
|
|
|
print(" Fetching VIX regime series for ORB sweep...")
|
|
|
|
print(" Fetching VIX regime series for ORB sweep...")
|
|
|
|
@ -2149,6 +2337,7 @@ async def run_with_sweep(config: IntradayConfig, sweep_path: str) -> None:
|
|
|
|
trading_days,
|
|
|
|
trading_days,
|
|
|
|
config.strategy,
|
|
|
|
config.strategy,
|
|
|
|
daily_enrichment=momentum_enrichment,
|
|
|
|
daily_enrichment=momentum_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,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
total_pairs = sum(len(v) for v in candidates.values())
|
|
|
|
total_pairs = sum(len(v) for v in candidates.values())
|
|
|
|
@ -2168,6 +2357,34 @@ async def run_with_sweep(config: IntradayConfig, sweep_path: str) -> None:
|
|
|
|
f"{total_pairs} ticker-day pairs across {len(candidates)} days"
|
|
|
|
f"{total_pairs} ticker-day pairs across {len(candidates)} days"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sector_proxy_intraday: dict[str, dict[str, list[dict]]] | None = None
|
|
|
|
|
|
|
|
if (not is_orb) and _momentum_strategy_uses_sector_proxies(config.strategy):
|
|
|
|
|
|
|
|
print(" Fetching sector ETF proxy bars for sweep...")
|
|
|
|
|
|
|
|
proxy_candidates = {day: list(SECTOR_PROXY_TICKERS) for day in trading_days}
|
|
|
|
|
|
|
|
_proxy_prog_last = [-1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def proxy_prog(completed: int, total: int, hits: int, calls: int) -> None:
|
|
|
|
|
|
|
|
if completed == 0 and calls == 0 and total > 0:
|
|
|
|
|
|
|
|
sys.stdout.write("\n")
|
|
|
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
_proxy_prog_last[0] = -1
|
|
|
|
|
|
|
|
pct = int(completed / total * 10) * 10 if total > 0 else 0
|
|
|
|
|
|
|
|
if pct > _proxy_prog_last[0] or completed == total:
|
|
|
|
|
|
|
|
_proxy_prog_last[0] = pct
|
|
|
|
|
|
|
|
sys.stdout.write(
|
|
|
|
|
|
|
|
f"\r {_make_progress_bar(completed, total)} cache:{hits} api:{calls}"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sector_proxy_intraday = await fetch_intraday_bulk(
|
|
|
|
|
|
|
|
proxy_candidates,
|
|
|
|
|
|
|
|
client,
|
|
|
|
|
|
|
|
cache,
|
|
|
|
|
|
|
|
concurrency=4,
|
|
|
|
|
|
|
|
progress_callback=proxy_prog,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
print(f"\n Done. {len(sector_proxy_intraday)} days with sector ETF proxy data")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\nRunning {sweep.total_combinations} sweep combinations...")
|
|
|
|
print(f"\nRunning {sweep.total_combinations} sweep combinations...")
|
|
|
|
completed_sw = [0]
|
|
|
|
completed_sw = [0]
|
|
|
|
_sweep_last_pct = [-1]
|
|
|
|
_sweep_last_pct = [-1]
|
|
|
|
@ -2187,6 +2404,7 @@ async def run_with_sweep(config: IntradayConfig, sweep_path: str) -> None:
|
|
|
|
momentum_enrichment=momentum_enrichment,
|
|
|
|
momentum_enrichment=momentum_enrichment,
|
|
|
|
vix_by_day=momentum_vix_by_day,
|
|
|
|
vix_by_day=momentum_vix_by_day,
|
|
|
|
ticker_sectors=ticker_sectors if not is_orb else None,
|
|
|
|
ticker_sectors=ticker_sectors if not is_orb else None,
|
|
|
|
|
|
|
|
sector_proxy_intraday_by_day=sector_proxy_intraday,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
print()
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
@ -2212,6 +2430,7 @@ async def run_with_sweep(config: IntradayConfig, sweep_path: str) -> None:
|
|
|
|
daily_enrichment=momentum_enrichment,
|
|
|
|
daily_enrichment=momentum_enrichment,
|
|
|
|
vix_by_day=momentum_vix_by_day,
|
|
|
|
vix_by_day=momentum_vix_by_day,
|
|
|
|
ticker_sectors=ticker_sectors,
|
|
|
|
ticker_sectors=ticker_sectors,
|
|
|
|
|
|
|
|
sector_proxy_intraday_by_day=sector_proxy_intraday,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
print("\n=== Best Configuration Detail ===")
|
|
|
|
print("\n=== Best Configuration Detail ===")
|
|
|
|
print(format_summary(best.metrics, best_config))
|
|
|
|
print(format_summary(best.metrics, best_config))
|
|
|
|
|