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.

119 lines
4.3 KiB
Python

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