"""Probe for checking Oracle price data availability for alternative parking ETFs. Checks whether BUFB, GPIX, JEPQ, SBIL, MERIX (and any custom symbols) have sufficient price history in the Oracle API for use as cash parking assets. Usage: python -m apps.tools.alternative_etf_parking_probe python -m apps.tools.alternative_etf_parking_probe --symbols JEPQ SBIL python -m apps.tools.alternative_etf_parking_probe --start 2020-01-01 --end 2026-04-01 """ from __future__ import annotations import argparse import asyncio import datetime as dt from typing import Any from libs.common.config import get_settings from libs.oracle_client.client import OracleClient from libs.oracle_client.exceptions import OracleNotFoundError from libs.oracle_client.price import PriceService _DEFAULT_SYMBOLS = ["BUFB", "GPIX", "JEPQ", "SBIL", "MERIX"] _BACKTEST_START = dt.date(2022, 1, 1) _BACKTEST_END = dt.date(2026, 4, 1) _BASELINE_SYMBOLS = ["QQQM", "SGOV", "SPY"] async def _check_symbol( svc: PriceService, ticker: str, start: dt.date, end: dt.date, ) -> dict[str, Any]: try: resp = await svc.get_daily_bars( ticker, start=start.isoformat(), end=end.isoformat(), ) bars = resp.bars if not bars: return {"ticker": ticker, "available": False, "reason": "empty response"} dates = sorted(dt.date.fromisoformat(b.date) for b in bars) first = dates[0] last = dates[-1] # "Viable" = data starts within 6 months of backtest start AND ends within 30 days of backtest end covers_start = first <= start + dt.timedelta(days=180) covers_end = last >= end - dt.timedelta(days=30) coverage_pct = len(bars) / max(1, (end - start).days / 365.0 * 252) usable = covers_start and covers_end and coverage_pct >= 0.5 first_close = float(bars[0].close) last_close = float(bars[-1].close) days_span = (last - first).days or 1 ann_return = (last_close / first_close) ** (365.0 / days_span) - 1.0 return { "ticker": ticker, "available": True, "bar_count": len(bars), "first_date": first.isoformat(), "last_date": last.isoformat(), "covers_backtest": first <= start, "usable": usable, "coverage_pct": round(coverage_pct * 100, 1), "first_close": first_close, "last_close": last_close, "ann_return_pct": round(ann_return * 100, 2), } except OracleNotFoundError: return {"ticker": ticker, "available": False, "reason": "not found in Oracle"} except Exception as exc: return {"ticker": ticker, "available": False, "reason": str(exc)} async def _run( symbols: list[str], start: dt.date, end: dt.date, ) -> None: s = get_settings() async with OracleClient(base_url=s.stock_oracle_url) as client: svc = PriceService(client) all_symbols = _BASELINE_SYMBOLS + symbols results = await asyncio.gather( *[_check_symbol(svc, sym, start, end) for sym in all_symbols] ) # Print results print() print("=" * 80) print(f" Oracle Data Availability Check ({start} → {end})") print("=" * 80) print(f" {'Ticker':<8} {'Bars':>5} {'First':<12} {'Last':<12} {'Cover%':>6} {'Ann%':>6} {'Usable':^7} Notes") print(" " + "-" * 76) viable: list[str] = [] for r in results: ticker = r["ticker"] available = r.get("available", False) is_baseline = ticker in _BASELINE_SYMBOLS tag = " [baseline]" if is_baseline else "" if available: usable = r.get("usable", False) cov = f"{r.get('coverage_pct', 0):.0f}%" ann = f"{r['ann_return_pct']:+.1f}%" usable_str = "YES" if usable else "PARTIAL" print( f" {ticker:<8} {r['bar_count']:>5} {r['first_date']:<12} {r['last_date']:<12} {cov:>6} {ann:>6} {usable_str:^7}{tag}" ) if not is_baseline and usable: viable.append(ticker) else: reason = r.get("reason", "unknown") print(f" {ticker:<8} {'N/A':>5} {'N/A':<12} {'N/A':<12} {'N/A':>6} {'N/A':>6} {'NO':^7} {reason}{tag}") print("=" * 80) print() if viable: print(f" ✓ Viable for parking backtest: {', '.join(viable)}") print() print(" Already added to snapshot_store (snapshot_store.py):") for sym in viable: lower = sym.lower() print(f" - _MACRO_TRADE_BAR_PREFIXES: ('{sym}', '{lower}')") print(f" - extra_symbol_responses fetch loop") print(f" Experiment configs to create:") print(f" - configs/experiments/parking_only_{lower}.json") else: print(" ✗ No alternative ETFs with sufficient Oracle data coverage") print() def main() -> None: parser = argparse.ArgumentParser(description="Check Oracle data for alternative parking ETFs") parser.add_argument( "--symbols", nargs="+", default=_DEFAULT_SYMBOLS, help=f"ETF symbols to check (default: {' '.join(_DEFAULT_SYMBOLS)})", ) parser.add_argument( "--start", default=_BACKTEST_START.isoformat(), help=f"Start date (default: {_BACKTEST_START})", ) parser.add_argument( "--end", default=_BACKTEST_END.isoformat(), help=f"End date (default: {_BACKTEST_END})", ) args = parser.parse_args() asyncio.run( _run( symbols=args.symbols, start=dt.date.fromisoformat(args.start), end=dt.date.fromisoformat(args.end), ) ) if __name__ == "__main__": main()