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>
75 lines
2.9 KiB
Python
75 lines
2.9 KiB
Python
from fastapi import APIRouter, HTTPException, Query
|
|
from typing import Optional
|
|
|
|
from app.db import get_db
|
|
from app.api.models import CommodityMeta, ExchangeInfo
|
|
|
|
router = APIRouter(prefix="/api", tags=["commodities"])
|
|
|
|
|
|
@router.get("/exchanges", response_model=list[ExchangeInfo])
|
|
def get_exchanges():
|
|
with get_db() as conn:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT c.exchange_abbr, c.exchange,
|
|
COUNT(DISTINCT c.id) AS commodity_count
|
|
FROM commodities c
|
|
WHERE EXISTS (SELECT 1 FROM reports r WHERE r.commodity_id = c.id)
|
|
OR EXISTS (SELECT 1 FROM disagg_reports dr WHERE dr.commodity_id = c.id)
|
|
GROUP BY c.exchange_abbr
|
|
ORDER BY commodity_count DESC
|
|
"""
|
|
).fetchall()
|
|
return [ExchangeInfo(**dict(r)) for r in rows]
|
|
|
|
|
|
@router.get("/commodities", response_model=list[CommodityMeta])
|
|
def get_commodities(exchange: Optional[str] = Query(None)):
|
|
sql = """
|
|
SELECT c.cftc_code, c.name, c.exchange, c.exchange_abbr, c.contract_unit,
|
|
COALESCE(MIN(r.report_date), MIN(dr.report_date)) AS first_date,
|
|
COALESCE(MAX(r.report_date), MAX(dr.report_date)) AS last_date,
|
|
COUNT(DISTINCT r.report_date) AS week_count,
|
|
CASE WHEN COUNT(DISTINCT dr.report_date) > 0 THEN 1 ELSE 0 END AS has_disagg
|
|
FROM commodities c
|
|
LEFT JOIN reports r ON r.commodity_id = c.id
|
|
LEFT JOIN disagg_reports dr ON dr.commodity_id = c.id
|
|
"""
|
|
params = []
|
|
if exchange:
|
|
sql += " WHERE c.exchange_abbr = ?"
|
|
params.append(exchange)
|
|
sql += """
|
|
GROUP BY c.id
|
|
HAVING COUNT(DISTINCT r.report_date) > 0 OR COUNT(DISTINCT dr.report_date) > 0
|
|
ORDER BY c.exchange_abbr, c.name
|
|
"""
|
|
|
|
with get_db() as conn:
|
|
rows = conn.execute(sql, params).fetchall()
|
|
return [CommodityMeta(**dict(r)) for r in rows]
|
|
|
|
|
|
@router.get("/commodities/{cftc_code}", response_model=CommodityMeta)
|
|
def get_commodity(cftc_code: str):
|
|
with get_db() as conn:
|
|
row = conn.execute(
|
|
"""
|
|
SELECT c.cftc_code, c.name, c.exchange, c.exchange_abbr, c.contract_unit,
|
|
COALESCE(MIN(r.report_date), MIN(dr.report_date)) AS first_date,
|
|
COALESCE(MAX(r.report_date), MAX(dr.report_date)) AS last_date,
|
|
COUNT(DISTINCT r.report_date) AS week_count,
|
|
CASE WHEN COUNT(DISTINCT dr.report_date) > 0 THEN 1 ELSE 0 END AS has_disagg
|
|
FROM commodities c
|
|
LEFT JOIN reports r ON r.commodity_id = c.id
|
|
LEFT JOIN disagg_reports dr ON dr.commodity_id = c.id
|
|
WHERE c.cftc_code = ?
|
|
GROUP BY c.id
|
|
""",
|
|
(cftc_code,),
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail=f"Commodity {cftc_code} not found")
|
|
return CommodityMeta(**dict(row))
|