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.
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
"""
|
|
ETF models: mapping tables and persisted ETF holdings snapshots
|
|
"""
|
|
|
|
from sqlalchemy import Column, String, DateTime, Index, UniqueConstraint, Float, ForeignKey, JSON
|
|
from sqlalchemy.sql import func
|
|
from sqlalchemy.dialects.postgresql import UUID, TIMESTAMP
|
|
from datetime import datetime, timezone
|
|
import uuid
|
|
from app.core.database import Base
|
|
|
|
|
|
class CusipMap(Base):
|
|
__tablename__ = "cusip_map"
|
|
|
|
# CUSIP is 9-character identifier; some sources may include shorter variants
|
|
cusip = Column(String(20), primary_key=True, index=True)
|
|
symbol = Column(String(32), index=True)
|
|
description = Column(String, nullable=True)
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
__table_args__ = (
|
|
Index("idx_cusip_symbol", "symbol"),
|
|
)
|
|
|
|
|
|
class ETFCIKMap(Base):
|
|
__tablename__ = "etf_cik_map"
|
|
|
|
# Store uppercased ticker symbols and numeric CIK (string)
|
|
ticker = Column(String(16), primary_key=True, index=True)
|
|
cik = Column(String(20), index=True)
|
|
name = Column(String, nullable=True)
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
__table_args__ = (
|
|
Index("idx_etf_cik", "cik"),
|
|
UniqueConstraint("ticker", name="uq_etf_ticker"),
|
|
)
|
|
|
|
|
|
class ETFSeriesMap(Base):
|
|
__tablename__ = "etf_series_map"
|
|
|
|
ticker = Column(String(16), primary_key=True, index=True)
|
|
series_id = Column(String(32), index=True)
|
|
class_id = Column(String(32), nullable=True)
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("ticker", name="uq_etf_series_ticker"),
|
|
Index("idx_etf_series_series_id", "series_id"),
|
|
)
|
|
|
|
|
|
class ETFHoldingsSnapshot(Base):
|
|
__tablename__ = "etf_holdings_snapshot"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
ticker = Column(String(16), nullable=False, index=True)
|
|
snapshot_date = Column(TIMESTAMP(timezone=True), nullable=False)
|
|
source = Column(String(20), nullable=True) # 'SCRAPER' or 'SEC'
|
|
cik = Column(String(20), nullable=True)
|
|
filing_accession = Column(String(64), nullable=True)
|
|
xml_url = Column(String, nullable=True)
|
|
metadata_json = Column(JSON, nullable=True)
|
|
created_at = Column(TIMESTAMP(timezone=True), default=lambda: datetime.now(timezone.utc))
|
|
updated_at = Column(TIMESTAMP(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("ticker", "snapshot_date", name="uq_etf_snapshot"),
|
|
Index("idx_etf_snapshot_ticker_date", "ticker", "snapshot_date"),
|
|
)
|
|
|
|
|
|
class ETFHolding(Base):
|
|
__tablename__ = "etf_holdings"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
snapshot_id = Column(UUID(as_uuid=True), ForeignKey("etf_holdings_snapshot.id"), index=True, nullable=False)
|
|
name = Column(String, nullable=True)
|
|
cusip = Column(String(20), index=True, nullable=True)
|
|
ticker = Column(String(32), index=True, nullable=True)
|
|
shares = Column(Float, nullable=True)
|
|
value = Column(Float, nullable=True)
|
|
percentage = Column(Float, nullable=True)
|
|
created_at = Column(TIMESTAMP(timezone=True), default=lambda: datetime.now(timezone.utc))
|
|
|
|
__table_args__ = (
|
|
Index("idx_etf_holding_snapshot", "snapshot_id"),
|
|
Index("idx_etf_holding_cusip", "cusip"),
|
|
Index("idx_etf_holding_ticker", "ticker"),
|
|
)
|
|
|