feat: connect real intraday price ticks from DB to charts and remove RangeBar

This commit is contained in:
alireza 2026-08-26 14:24:00 +03:30
parent 39bd8c5bee
commit f96f39c962
2 changed files with 87 additions and 48 deletions

View File

@ -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")

View File

@ -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 opennow (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<Record<string, number[]>>({});
const ref = useRef<Record<string, number[]>>({});
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 (
<div className="relative" dir="ltr">
<div className="mb-1 flex items-center justify-between text-[10px] font-mono text-muted-foreground">
<span>{format(lo)}</span>
<span className="text-[9px] opacity-70" dir="rtl">
کف سقف امروز
</span>
<span>{format(hi)}</span>
</div>
<div className="relative h-1.5 rounded-full bg-muted">
<div
className="absolute top-0 h-full rounded-full bg-foreground/15"
style={{ width: `${pos}%` }}
/>
<div
className="absolute top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-background bg-foreground shadow"
style={{ left: `${pos}%` }}
/>
</div>
</div>
);
}
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}
/>
</div>
<RangeBar series={series} value={value} format={format} />
</motion.div>
);
}