COTexplorer/app/api/models.py
Greg 90c2ae3f35 Expose disaggregated COT data in the UI (wheat focus)
The disagg dataset (2019–2026, 468 markets) was in the DB but invisible.
This wires it into every layer of the app:

Backend:
- models.py: add has_disagg to CommodityMeta; add DisaggPositionPoint,
  DisaggHistoryResponse, DisaggScreenerRow models
- commodities.py: join disagg_reports to populate has_disagg flag and
  correct first/last dates; HAVING filter removes markets with no data
- disagg.py (new): /api/disagg/{code}/history, /api/disagg/screener,
  /api/disagg/{code}/net-position-percentile, /api/disagg/compare
- main.py: register disagg router

Frontend:
- Metric selector shows Disaggregated optgroup (Managed Money, Prod/Merchant,
  Swap Dealer, Other Rept) when market has has_disagg=true, hides Legacy group
- Detail view auto-switches to disagg endpoint and defaults to m_money_net
  for disagg markets; shows green "Disaggregated" badge
- Screener always uses disagg endpoint (Managed Money percentile rank)
- Compare uses /api/disagg/compare for disagg metrics
- style.css: add .badge-disagg green variant

Result: wheat markets (SRW, HRW, HRSpring, Black Sea) now show 7 years of
disaggregated positioning data with Managed Money as the default metric.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:22:16 +01:00

191 lines
4.7 KiB
Python

from typing import Optional
from pydantic import BaseModel
class CommodityMeta(BaseModel):
cftc_code: str
name: str
exchange: str
exchange_abbr: str
contract_unit: Optional[str]
first_date: Optional[str]
last_date: Optional[str]
week_count: int
has_disagg: bool = False
class ExchangeInfo(BaseModel):
exchange_abbr: str
exchange: str
commodity_count: int
class PositionPoint(BaseModel):
report_date: str
open_interest: Optional[int]
noncomm_long: Optional[int]
noncomm_short: Optional[int]
noncomm_spreading: Optional[int]
noncomm_net: Optional[int]
comm_long: Optional[int]
comm_short: Optional[int]
comm_net: Optional[int]
nonrept_long: Optional[int]
nonrept_short: Optional[int]
nonrept_net: Optional[int]
chg_open_interest: Optional[int]
chg_noncomm_long: Optional[int]
chg_noncomm_short: Optional[int]
chg_comm_long: Optional[int]
chg_comm_short: Optional[int]
pct_noncomm_long: Optional[float]
pct_noncomm_short: Optional[float]
pct_comm_long: Optional[float]
pct_comm_short: Optional[float]
traders_total: Optional[int]
traders_noncomm_long: Optional[int]
traders_noncomm_short: Optional[int]
traders_comm_long: Optional[int]
traders_comm_short: Optional[int]
class HistoryResponse(BaseModel):
commodity: CommodityMeta
row_type: str
data: list[PositionPoint]
class LatestRowData(BaseModel):
row_type: str
positions: PositionPoint
concentration: Optional[dict]
class LatestResponse(BaseModel):
commodity: CommodityMeta
report_date: str
rows: list[LatestRowData]
class ExtremePoint(BaseModel):
value: Optional[float]
date: Optional[str]
class ExtremesResponse(BaseModel):
cftc_code: str
commodity: str
noncomm_net: dict
open_interest: dict
comm_net: dict
class ScreenerRow(BaseModel):
cftc_code: str
commodity: str
exchange: str
latest_date: str
noncomm_net: Optional[int]
open_interest: Optional[int]
pct_rank: Optional[float]
chg_noncomm_long: Optional[int]
chg_noncomm_short: Optional[int]
class ComparePoint(BaseModel):
report_date: str
value: Optional[float]
class CompareResponse(BaseModel):
metric: str
commodities: list[CommodityMeta]
series: dict[str, list[ComparePoint]]
class PercentileResponse(BaseModel):
cftc_code: str
commodity: str
current_net: Optional[int]
percentile: Optional[float]
z_score: Optional[float]
lookback_weeks: int
period_min: Optional[int]
period_max: Optional[int]
class DisaggPositionPoint(BaseModel):
report_date: str
open_interest: Optional[int]
prod_merc_long: Optional[int]
prod_merc_short: Optional[int]
prod_merc_net: Optional[int]
swap_long: Optional[int]
swap_short: Optional[int]
swap_spread: Optional[int]
swap_net: Optional[int]
m_money_long: Optional[int]
m_money_short: Optional[int]
m_money_spread: Optional[int]
m_money_net: Optional[int]
other_rept_long: Optional[int]
other_rept_short: Optional[int]
other_rept_net: Optional[int]
nonrept_long: Optional[int]
nonrept_short: Optional[int]
nonrept_net: Optional[int]
chg_open_interest: Optional[int]
chg_m_money_long: Optional[int]
chg_m_money_short: Optional[int]
chg_prod_merc_long: Optional[int]
chg_prod_merc_short: Optional[int]
chg_swap_long: Optional[int]
chg_swap_short: Optional[int]
pct_open_interest: Optional[float]
pct_m_money_long: Optional[float]
pct_m_money_short: Optional[float]
pct_prod_merc_long: Optional[float]
pct_prod_merc_short: Optional[float]
pct_swap_long: Optional[float]
pct_swap_short: Optional[float]
traders_total: Optional[int]
traders_m_money_long: Optional[int]
traders_m_money_short: Optional[int]
traders_prod_merc_long: Optional[int]
traders_prod_merc_short: Optional[int]
class DisaggHistoryResponse(BaseModel):
commodity: CommodityMeta
row_type: str
data: list[DisaggPositionPoint]
class DisaggScreenerRow(BaseModel):
cftc_code: str
commodity: str
exchange: str
latest_date: str
m_money_net: Optional[int]
open_interest: Optional[int]
pct_rank: Optional[float]
chg_m_money_long: Optional[int]
chg_m_money_short: Optional[int]
class ReportDateInfo(BaseModel):
date: str
commodity_count: int
class ReportSnapshotRow(BaseModel):
cftc_code: str
commodity: str
exchange: str
open_interest: Optional[int]
noncomm_net: Optional[int]
comm_net: Optional[int]
pct_noncomm_long: Optional[float]
pct_noncomm_short: Optional[float]
traders_total: Optional[int]