You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fithia2/apps/tools/alternative_etf_parking_pro...

163 lines
5.7 KiB
Python

"""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()