From f96f39c962778e125080d5990a608eb67c83f699 Mon Sep 17 00:00:00 2001 From: alireza Date: Wed, 26 Aug 2026 14:24:00 +0330 Subject: [PATCH] feat: connect real intraday price ticks from DB to charts and remove RangeBar --- backend/currency_stream.py | 76 +++++++++++++++++++++++++++++-- frontend/src/routes/dashboard.tsx | 59 ++++++------------------ 2 files changed, 87 insertions(+), 48 deletions(-) diff --git a/backend/currency_stream.py b/backend/currency_stream.py index 717032e..f2bf47d 100644 --- a/backend/currency_stream.py +++ b/backend/currency_stream.py @@ -182,12 +182,23 @@ def _load_last_saved() -> None: try: with get_conn() as conn: rows = conn.execute( - f"SELECT name, price FROM {table} " + f"SELECT name, price, change_val, pct, dir, fetched_at FROM {table} " f"WHERE id IN (SELECT MAX(id) FROM {table} GROUP BY name)" ).fetchall() + loaded = [] for r in rows: if r["price"] is not None: _last_saved[kind][r["name"]] = r["price"] + loaded.append({ + "name": r["name"], + "price": r["price"], + "change_val": r["change_val"], + "pct": r["pct"], + "dir": r["dir"], + "fetched_at": r["fetched_at"] + }) + if loaded and not _latest[kind]: + _latest[kind] = loaded except Exception as e: print(f"[tgju] could not load last-saved {kind}: {e}") @@ -230,13 +241,70 @@ async def scrape_loop() -> None: await asyncio.sleep(INTERVAL_SECONDS) +def get_real_day_series(kind: str, max_points: int = 28) -> dict[str, list[float]]: + table = _TABLES.get(kind) + if not table: + return {} + today_prefix = datetime.now(timezone.utc).strftime("%Y-%m-%d") + out: dict[str, list[float]] = {} + try: + with get_conn() as conn: + query = f""" + SELECT name, price FROM {table} + WHERE fetched_at LIKE ? AND price IS NOT NULL AND price > 0 + ORDER BY name, id ASC + """ + rows = conn.execute(query, (f"{today_prefix}%",)).fetchall() + grouped: dict[str, list[float]] = {} + for r in rows: + name = r["name"] + if name not in grouped: + grouped[name] = [] + grouped[name].append(r["price"]) + + for name, raw_prices in grouped.items(): + if not raw_prices: + continue + deduped = [raw_prices[0]] + for p in raw_prices[1:]: + if p != deduped[-1]: + deduped.append(p) + if len(deduped) > max_points: + step = (len(deduped) - 1) / (max_points - 1) + sampled = [deduped[int(i * step)] for i in range(max_points - 1)] + sampled.append(deduped[-1]) + deduped = sampled + out[name] = deduped + return out + except Exception as e: + print(f"[currency_stream] error loading day series for {kind}: {e}") + return {} + + +def _attach_sparklines(kind: str) -> list[dict]: + day_series = get_real_day_series(kind) + res = [] + for r in _latest.get(kind, []): + name = r.get("name", "") + price = r.get("price", 0.0) or 0.0 + pct = r.get("pct", 0.0) or 0.0 + series = day_series.get(name) + if not series or len(series) < 2: + open_p = price / (1.0 + pct / 100.0) if (1.0 + pct / 100.0) > 0 else price + series = [round(open_p, 2), round(price, 2)] + elif series[-1] != price: + series = series + [price] + res.append({**r, "sparkline": series}) + return res + + def get_latest() -> list[dict]: - return _latest["currency"] + return _attach_sparklines("currency") def get_gold() -> list[dict]: - return _latest["gold"] + return _attach_sparklines("gold") def get_coin() -> list[dict]: - return _latest["coin"] + return _attach_sparklines("coin") diff --git a/frontend/src/routes/dashboard.tsx b/frontend/src/routes/dashboard.tsx index 584012c..469027e 100644 --- a/frontend/src/routes/dashboard.tsx +++ b/frontend/src/routes/dashboard.tsx @@ -237,6 +237,7 @@ type CurRow = { pct: number; dir: "up" | "down" | ""; fetched_at: string; + sparkline?: number[]; }; function DomesticView() { @@ -297,7 +298,7 @@ function DomesticView() { const allRows = useMemo(() => [...currency, ...gold, ...coin], [currency, gold, coin]); const seriesInput = useMemo( - () => allRows.map((r) => ({ name: r.name, price: r.price, pct: r.pct })), + () => allRows.map((r) => ({ name: r.name, price: r.price, pct: r.pct, sparkline: r.sparkline })), [allRows], ); const series = useRollingSeries(seriesInput); @@ -694,8 +695,8 @@ function DeltaPill({ /* ── Rolling client-side price history — for live snapshots that have no backend history. Seeds each series with today's open→now (from pct), then appends a point whenever the price ticks. ─────────────────────────────────────── */ -const SERIES_CAP = 24; -function useRollingSeries(rows: { name: string; price: number; pct?: number }[]) { +const SERIES_CAP = 28; +function useRollingSeries(rows: { name: string; price: number; pct?: number; sparkline?: number[] }[]) { const [series, setSeries] = useState>({}); const ref = useRef>({}); useEffect(() => { @@ -704,10 +705,18 @@ function useRollingSeries(rows: { name: string; price: number; pct?: number }[]) let changed = false; for (const r of rows) { if (!Number.isFinite(r.price)) continue; + // If backend provided real recorded market series from DB, use it directly! + if (r.sparkline && r.sparkline.length > 1) { + const existing = next[r.name]; + if (!existing || existing.length !== r.sparkline.length || existing[existing.length - 1] !== r.price) { + next[r.name] = r.sparkline; + changed = true; + } + continue; + } const cur = next[r.name]; if (!cur) { - // Seed with authentic market series showing real intraday peaks, troughs, and volatility - next[r.name] = generateMarketSeries(r.name, r.price, r.pct, 24); + next[r.name] = generateMarketSeries(r.name, r.price, r.pct, 28); changed = true; } else if (cur[cur.length - 1] !== r.price) { const arr = cur.concat(r.price); @@ -954,43 +963,7 @@ function AssetBadge({ meta, size = "md" }: { meta: AssetMeta; size?: "md" | "lg" domestic & commodity views. One big hero, a stack of side stats. ─── */ type HeroFmt = (n: number) => string; -// Day low→high track with a marker at the current value (from the rolling series). -function RangeBar({ - series, - value, - format, -}: { - series?: number[]; - value: number; - format: HeroFmt; -}) { - if (!series || series.length < 2) return null; - const lo = Math.min(...series); - const hi = Math.max(...series); - if (hi <= lo) return null; - const pos = Math.min(100, Math.max(0, ((value - lo) / (hi - lo)) * 100)); - return ( -
-
- {format(lo)} - - کف ↔ سقف امروز - - {format(hi)} -
-
-
-
-
-
- ); -} + function HeroCard({ item, format }: { item: KpiItem; format: HeroFmt }) { const { label, value, unit, pct, dir = "", series, meta } = item; @@ -1044,8 +1017,6 @@ function HeroCard({ item, format }: { item: KpiItem; format: HeroFmt }) { fillOpacity={0.3} />
- - ); }