FastAPI application that ingests CFTC Commitments of Traders data into SQLite and exposes it via a REST API with analytics endpoints (screener, percentile rank, concentration). Includes CLI for historical and weekly data ingestion, Docker setup, and a frontend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
196 lines
7.3 KiB
Python
196 lines
7.3 KiB
Python
from fastapi import APIRouter, HTTPException, Query
|
|
from typing import Optional
|
|
|
|
from app.db import get_db
|
|
from app.api.models import PercentileResponse, ScreenerRow
|
|
|
|
router = APIRouter(prefix="/api/analytics", tags=["analytics"])
|
|
|
|
|
|
@router.get("/screener", response_model=list[ScreenerRow])
|
|
def screener(
|
|
exchange: Optional[str] = Query(None),
|
|
lookback_weeks: int = Query(156, ge=4, le=1560),
|
|
top_n: int = Query(50, ge=1, le=500),
|
|
direction: Optional[str] = Query(None, pattern="^(long|short)$"),
|
|
):
|
|
"""
|
|
Return markets ranked by their current non-commercial net position
|
|
relative to the historical distribution (percentile rank).
|
|
"""
|
|
with get_db() as conn:
|
|
# Get all commodities, optionally filtered by exchange
|
|
exchange_filter = "AND c.exchange_abbr = ?" if exchange else ""
|
|
exchange_params = [exchange] if exchange else []
|
|
|
|
# For each commodity: get latest date, latest noncomm_net,
|
|
# and compute percentile rank over last N weeks
|
|
rows = conn.execute(
|
|
f"""
|
|
WITH latest AS (
|
|
SELECT c.cftc_code, c.name AS commodity, c.exchange_abbr AS exchange,
|
|
MAX(r.report_date) AS latest_date
|
|
FROM commodities c
|
|
JOIN reports r ON r.commodity_id = c.id
|
|
{exchange_filter}
|
|
GROUP BY c.cftc_code
|
|
),
|
|
latest_pos AS (
|
|
SELECT l.cftc_code, l.commodity, l.exchange, l.latest_date,
|
|
p.open_interest,
|
|
(p.noncomm_long - p.noncomm_short) AS noncomm_net,
|
|
p.chg_noncomm_long, p.chg_noncomm_short
|
|
FROM latest l
|
|
JOIN commodities c ON c.cftc_code = l.cftc_code
|
|
JOIN reports r ON r.commodity_id = c.id AND r.report_date = l.latest_date
|
|
JOIN positions p ON p.report_id = r.id AND p.row_type = 'All'
|
|
),
|
|
lookback AS (
|
|
SELECT c.cftc_code,
|
|
(p.noncomm_long - p.noncomm_short) AS net,
|
|
ROW_NUMBER() OVER (PARTITION BY c.cftc_code ORDER BY r.report_date DESC) AS rn
|
|
FROM commodities c
|
|
JOIN reports r ON r.commodity_id = c.id
|
|
JOIN positions p ON p.report_id = r.id AND p.row_type = 'All'
|
|
),
|
|
pct AS (
|
|
SELECT lp.cftc_code,
|
|
lp.commodity,
|
|
lp.exchange,
|
|
lp.latest_date,
|
|
lp.open_interest,
|
|
lp.noncomm_net,
|
|
lp.chg_noncomm_long,
|
|
lp.chg_noncomm_short,
|
|
CAST(
|
|
(SELECT COUNT(*) FROM lookback lb2
|
|
WHERE lb2.cftc_code = lp.cftc_code
|
|
AND lb2.rn <= ? AND lb2.net < lp.noncomm_net)
|
|
AS REAL
|
|
) / NULLIF(
|
|
(SELECT COUNT(*) FROM lookback lb3
|
|
WHERE lb3.cftc_code = lp.cftc_code AND lb3.rn <= ?),
|
|
0
|
|
) * 100.0 AS pct_rank
|
|
FROM latest_pos lp
|
|
)
|
|
SELECT cftc_code, commodity, exchange, latest_date,
|
|
noncomm_net, open_interest, pct_rank,
|
|
chg_noncomm_long, chg_noncomm_short
|
|
FROM pct
|
|
ORDER BY pct_rank DESC
|
|
LIMIT ?
|
|
""",
|
|
exchange_params + [lookback_weeks, lookback_weeks, top_n],
|
|
).fetchall()
|
|
|
|
result = [ScreenerRow(**dict(r)) for r in rows]
|
|
if direction == 'long':
|
|
result = [r for r in result if r.pct_rank is not None and r.pct_rank >= 50]
|
|
elif direction == 'short':
|
|
result = [r for r in result if r.pct_rank is not None and r.pct_rank < 50]
|
|
return result
|
|
|
|
|
|
@router.get("/{cftc_code}/net-position-percentile", response_model=PercentileResponse)
|
|
def net_position_percentile(
|
|
cftc_code: str,
|
|
lookback_weeks: int = Query(156, ge=4, le=1560),
|
|
):
|
|
"""
|
|
Where does the current non-commercial net position sit in the
|
|
historical distribution over the last N weeks?
|
|
"""
|
|
with get_db() as conn:
|
|
row = conn.execute(
|
|
"SELECT id, name FROM commodities WHERE cftc_code = ?", (cftc_code,)
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail=f"Commodity {cftc_code} not found")
|
|
commodity_name = row['name']
|
|
|
|
# Get last N weekly net positions
|
|
history = conn.execute(
|
|
"""
|
|
SELECT (p.noncomm_long - p.noncomm_short) AS net
|
|
FROM positions p
|
|
JOIN reports r ON r.id = p.report_id
|
|
JOIN commodities c ON c.id = r.commodity_id
|
|
WHERE c.cftc_code = ? AND p.row_type = 'All'
|
|
ORDER BY r.report_date DESC
|
|
LIMIT ?
|
|
""",
|
|
(cftc_code, lookback_weeks),
|
|
).fetchall()
|
|
|
|
if not history:
|
|
raise HTTPException(status_code=404, detail="No position data found")
|
|
|
|
nets = [r[0] for r in history if r[0] is not None]
|
|
if not nets:
|
|
return PercentileResponse(
|
|
cftc_code=cftc_code, commodity=commodity_name,
|
|
current_net=None, percentile=None, z_score=None,
|
|
lookback_weeks=lookback_weeks, period_min=None, period_max=None,
|
|
)
|
|
|
|
current = nets[0]
|
|
n = len(nets)
|
|
below = sum(1 for v in nets[1:] if v < current)
|
|
percentile = round(below / max(n - 1, 1) * 100, 1)
|
|
|
|
mean = sum(nets) / n
|
|
variance = sum((v - mean) ** 2 for v in nets) / n
|
|
std = variance ** 0.5
|
|
z_score = round((current - mean) / std, 2) if std > 0 else 0.0
|
|
|
|
return PercentileResponse(
|
|
cftc_code=cftc_code,
|
|
commodity=commodity_name,
|
|
current_net=current,
|
|
percentile=percentile,
|
|
z_score=z_score,
|
|
lookback_weeks=n,
|
|
period_min=min(nets),
|
|
period_max=max(nets),
|
|
)
|
|
|
|
|
|
@router.get("/{cftc_code}/concentration")
|
|
def get_concentration(
|
|
cftc_code: str,
|
|
from_date: Optional[str] = Query(None),
|
|
to_date: Optional[str] = Query(None),
|
|
row_type: str = Query("All", pattern="^(All|Old|Other)$"),
|
|
):
|
|
with get_db() as conn:
|
|
row = conn.execute(
|
|
"SELECT id FROM commodities WHERE cftc_code = ?", (cftc_code,)
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail=f"Commodity {cftc_code} not found")
|
|
|
|
sql = """
|
|
SELECT r.report_date,
|
|
cn.conc_gross_long_4, cn.conc_gross_short_4,
|
|
cn.conc_gross_long_8, cn.conc_gross_short_8,
|
|
cn.conc_net_long_4, cn.conc_net_short_4,
|
|
cn.conc_net_long_8, cn.conc_net_short_8
|
|
FROM concentration cn
|
|
JOIN reports r ON r.id = cn.report_id
|
|
JOIN commodities c ON c.id = r.commodity_id
|
|
WHERE c.cftc_code = ? AND cn.row_type = ?
|
|
"""
|
|
params: list = [cftc_code, row_type]
|
|
if from_date:
|
|
sql += " AND r.report_date >= ?"
|
|
params.append(from_date)
|
|
if to_date:
|
|
sql += " AND r.report_date <= ?"
|
|
params.append(to_date)
|
|
sql += " ORDER BY r.report_date ASC"
|
|
|
|
rows = conn.execute(sql, params).fetchall()
|
|
|
|
return {"cftc_code": cftc_code, "row_type": row_type, "data": [dict(r) for r in rows]}
|