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.
163 lines
5.7 KiB
Python
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()
|