"""Short Volume Sync: fetch FINRA short volume → short_sale_daily.""" from __future__ import annotations import argparse import asyncio import datetime as dt import uuid from sqlalchemy.dialects.postgresql import insert from libs.common.config import get_settings from libs.common.ids import new_job_run_id from libs.common.logging import bind_job_run_id, configure_logging, get_logger from libs.db.models import JobRun, ShortSaleDaily, SyncCheckpoint from libs.db.session import get_session from libs.oracle_client import FinraService, make_oracle_client logger = get_logger(__name__) async def run_short_volume_sync(run_id: str) -> dict[str, int]: settings = get_settings() symbols = settings.get_symbols() stats = {"seen": 0, "written": 0, "errors": 0} async with make_oracle_client() as client: svc = FinraService(client) async with get_session() as session: job = JobRun( job_run_id=uuid.UUID(run_id), job_name="short_volume_sync", source_name="finra", run_date=dt.date.today(), status="running", ) session.add(job) await session.flush() for symbol in symbols: try: response = await svc.get_short_volume(symbol, days=30) stats["seen"] += len(response.data) rows = [] for entry in response.data: rows.append({ "ticker_raw": symbol, "trade_date": dt.date.fromisoformat(entry.date), "short_volume": entry.short_volume, "short_exempt_volume": entry.short_exempt_volume, "total_volume": entry.total_volume, "source_name": "finra", "created_at_utc": dt.datetime.now(tz=dt.UTC), }) if rows: stmt = ( insert(ShortSaleDaily) .values(rows) .on_conflict_do_nothing( constraint="uq_short_sale_ticker_date_source" ) ) await session.execute(stmt) stats["written"] += len(rows) logger.info("short_volume_synced", symbol=symbol, count=len(rows)) except Exception as exc: logger.error("short_volume_error", symbol=symbol, error=str(exc)) stats["errors"] += 1 # Update checkpoint cp_stmt = ( insert(SyncCheckpoint) .values( domain="finra:short_volume", last_sync_at_utc=dt.datetime.now(tz=dt.UTC), last_sync_params={"symbols": symbols}, status="success", created_at_utc=dt.datetime.now(tz=dt.UTC), updated_at_utc=dt.datetime.now(tz=dt.UTC), ) .on_conflict_do_update( constraint="uq_sync_checkpoints_domain", set_={ "last_sync_at_utc": dt.datetime.now(tz=dt.UTC), "status": "success" if stats["errors"] == 0 else "partial", "updated_at_utc": dt.datetime.now(tz=dt.UTC), }, ) ) await session.execute(cp_stmt) job.status = "succeeded" if stats["errors"] == 0 else "partial" job.finished_at_utc = dt.datetime.now(tz=dt.UTC) job.records_seen = stats["seen"] job.records_written = stats["written"] job.error_count = stats["errors"] logger.info("short_volume_sync_done", **stats) return stats def main() -> None: parser = argparse.ArgumentParser(description="Short Volume Sync") parser.add_argument("--run-id", default=new_job_run_id()) args = parser.parse_args() settings = get_settings() configure_logging(settings.log_level) bind_job_run_id(args.run_id) asyncio.run(run_short_volume_sync(args.run_id)) if __name__ == "__main__": main()