feat: overlay stock price on trend charts with right Y axis
- New GET /api/market/prices/{ticker} endpoint serving OHLCV data from
market_snapshots, deduped by bar_timestamp
- New useMarketPrices hook in frontend
- Trend chart now shows price (purple line) on a right Y axis ($)
alongside trend metrics (%) on the left Y axis
- Custom tooltip formats price as dollars, metrics as percentages
- Price line uses connectNulls for days with missing bar data
This commit is contained in:
@@ -465,6 +465,56 @@ async def list_trend_history(
|
||||
return results
|
||||
|
||||
|
||||
@app.get("/api/market/prices/{ticker}")
|
||||
async def get_market_prices(
|
||||
ticker: str,
|
||||
limit: int = Query(default=30, le=200),
|
||||
):
|
||||
"""Return historical close prices for a ticker from market_snapshots.
|
||||
|
||||
Each row has a bar_date (from the Polygon bar timestamp) and OHLCV data.
|
||||
Ordered oldest-first for chart rendering.
|
||||
"""
|
||||
ticker = ticker.upper()
|
||||
rows = await pool.fetch(
|
||||
"""SELECT
|
||||
captured_at,
|
||||
(data->>'c')::float AS close,
|
||||
(data->>'o')::float AS open,
|
||||
(data->>'h')::float AS high,
|
||||
(data->>'l')::float AS low,
|
||||
(data->>'v')::float AS volume,
|
||||
(data->>'t')::bigint AS bar_timestamp
|
||||
FROM market_snapshots
|
||||
WHERE ticker = $1 AND snapshot_type = 'bar'
|
||||
ORDER BY captured_at ASC
|
||||
LIMIT $2""",
|
||||
ticker, limit,
|
||||
)
|
||||
results = []
|
||||
seen_dates: set[str] = set()
|
||||
for r in rows:
|
||||
# Deduplicate by bar_timestamp (same day bar captured multiple times)
|
||||
bar_ts = r["bar_timestamp"]
|
||||
if bar_ts is None:
|
||||
continue
|
||||
date_key = str(bar_ts)
|
||||
if date_key in seen_dates:
|
||||
continue
|
||||
seen_dates.add(date_key)
|
||||
results.append({
|
||||
"ticker": ticker,
|
||||
"close": r["close"],
|
||||
"open": r["open"],
|
||||
"high": r["high"],
|
||||
"low": r["low"],
|
||||
"volume": r["volume"],
|
||||
"bar_timestamp": bar_ts,
|
||||
"captured_at": r["captured_at"].isoformat() if r["captured_at"] else None,
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
@app.get("/api/trends/{trend_id}")
|
||||
async def get_trend(trend_id: str):
|
||||
"""Get a single trend summary by ID."""
|
||||
|
||||
Reference in New Issue
Block a user