Spaces:
Sleeping
Sleeping
Download ui/render.py from nandutt/bbca-forecast: direct link, hf CLI and curl.
- Browser
- Download file 6.22 kB
-
https://huggingface.co/spaces/nandutt/bbca-forecast/resolve/main/ui/render.py
- Command line
-
hf download hf://spaces/nandutt/bbca-forecast/ui/render.py
-
curl -L -o render.py https://huggingface.co/spaces/nandutt/bbca-forecast/resolve/main/ui/render.py
6.22 kB
| """HTML pieces for the page. Every dynamic string goes through esc().""" | |
| from __future__ import annotations | |
| import html | |
| import math | |
| import pandas as pd | |
| from bbca.data import TZ | |
| from bbca.backtest import Backtest | |
| from bbca.model import Forecast | |
| from bbca.service import Prediction | |
| def esc(value) -> str: | |
| return html.escape(str(value), quote=True) | |
| def rp(x: float) -> str: | |
| return f"Rp {x:,.0f}" | |
| def pct(x: float, signed: bool = False) -> str: | |
| if x is None or (isinstance(x, float) and math.isnan(x)): | |
| return "n/a" | |
| return f"{x * 100:+.2f}%" if signed else f"{x * 100:.2f}%" | |
| def day(ts: pd.Timestamp) -> str: | |
| return pd.Timestamp(ts).strftime("%a %d %b %Y") | |
| def placeholder() -> str: | |
| return ( | |
| '<div class="bb-board bb-empty">' | |
| '<p class="bb-kicker">Waiting for a run</p>' | |
| "<p>Press <b>Get fresh forecast</b>. The app pulls the latest BBCA prices, " | |
| "refits the model and shows where the next closes are likely to land.</p>" | |
| "</div>" | |
| ) | |
| def error(message: str) -> str: | |
| return f'<div class="bb-board bb-error"><p class="bb-kicker">No forecast</p><p>{esc(message)}</p></div>' | |
| def board(pred: Prediction, run_no: int) -> str: | |
| fc: Forecast = pred.forecast | |
| prices = pred.prices | |
| last_close = float(prices.closes.iloc[-1]) | |
| last_date = prices.closes.index[-1] | |
| nxt = float(fc.mean[0]) | |
| move = nxt / fc.base_price - 1 | |
| width = (fc.upper[0] - fc.lower[0]) / nxt | |
| tone = "up" if move > 0.0005 else "down" if move < -0.0005 else "flat" | |
| live = "" | |
| if prices.live_price is not None: | |
| used = "used as the starting point" if fc.base_is_live else "not used (switched off)" | |
| live = ( | |
| '<div class="bb-live"><span class="bb-dot"></span>' | |
| f"Market open. Running price {esc(rp(prices.live_price))}, not a close yet, {esc(used)}.</div>" | |
| ) | |
| demo = "" | |
| if prices.source == "demo": | |
| demo = '<div class="bb-demo">DEMO MODE · synthetic prices, not real BBCA data</div>' | |
| base_label = "Running price" if fc.base_is_live else "Last close" | |
| base_date = "today, intraday" if fc.base_is_live else day(last_date) | |
| base_value = fc.base_price if fc.base_is_live else last_close | |
| return f""" | |
| <div class="bb-board"> | |
| {demo} | |
| <div class="bb-run">RUN {run_no:03d} · {esc(pd.Timestamp.now(tz=TZ).strftime('%H:%M:%S'))} WIB</div> | |
| <div class="bb-tiles"> | |
| <div class="bb-tile"> | |
| <p class="bb-kicker">{esc(base_label)}</p> | |
| <p class="bb-num">{esc(rp(base_value))}</p> | |
| <p class="bb-sub">{esc(base_date)}</p> | |
| </div> | |
| <div class="bb-tile bb-main bb-{tone}"> | |
| <p class="bb-kicker">Forecast · {esc(day(fc.dates[0]))}</p> | |
| <p class="bb-num">{esc(rp(nxt))}</p> | |
| <p class="bb-sub"><span class="bb-chip">{esc(pct(move, signed=True))}</span> vs {esc(base_label.lower())}</p> | |
| </div> | |
| <div class="bb-tile"> | |
| <p class="bb-kicker">95% range</p> | |
| <p class="bb-num bb-num-sm">{esc(rp(fc.lower[0]))} – {esc(rp(fc.upper[0]))}</p> | |
| <p class="bb-sub">±{esc(pct(width / 2))} around the forecast</p> | |
| </div> | |
| </div> | |
| {live} | |
| </div>""" | |
| def table(fc: Forecast) -> str: | |
| rows = [] | |
| prev = fc.base_price | |
| for d, m, lo, hi in zip(fc.dates, fc.mean, fc.lower, fc.upper): | |
| move = m / fc.base_price - 1 | |
| rows.append( | |
| "<tr>" | |
| f"<td>{esc(day(d))}</td>" | |
| f"<td class='bb-r'>{esc(rp(m))}</td>" | |
| f"<td class='bb-r bb-{('up' if m > prev else 'down' if m < prev else 'flat')}'>{esc(pct(move, signed=True))}</td>" | |
| f"<td class='bb-r bb-muted'>{esc(rp(lo))} – {esc(rp(hi))}</td>" | |
| "</tr>" | |
| ) | |
| prev = m | |
| return ( | |
| '<div class="bb-card"><p class="bb-kicker">Forecast by session</p>' | |
| '<div class="bb-scroll"><table class="bb-table"><thead><tr>' | |
| "<th>Session</th><th class='bb-r'>Close</th><th class='bb-r'>vs start</th><th class='bb-r'>95% range</th>" | |
| "</tr></thead><tbody>" + "".join(rows) + "</tbody></table></div>" | |
| '<p class="bb-note">Weekdays only. IDX holidays are not skipped, so a date can land on a day the market is closed. ' | |
| "The range widens fast: past a few days it says more about uncertainty than about price.</p></div>" | |
| ) | |
| def backtest(b: Backtest) -> str: | |
| gap = b.model_mape / b.naive_mape - 1 if b.naive_mape else float("nan") | |
| if gap < -0.02: | |
| verdict = f"SARIMA beat the naive guess, with {pct(-gap)} less error." | |
| tone = "up" | |
| elif gap <= 0.02: | |
| verdict = "SARIMA and the naive guess were about even." | |
| tone = "flat" | |
| else: | |
| verdict = f"SARIMA did worse than just repeating the last close ({pct(gap)} more error)." | |
| tone = "down" | |
| hit = "n/a" if math.isnan(b.direction_hit) else f"{b.direction_hit * 100:.0f}% of {b.direction_days} days" | |
| return f""" | |
| <div class="bb-card"> | |
| <p class="bb-kicker">How it did over the last {len(b.actual)} sessions</p> | |
| <p class="bb-verdict bb-{tone}">{esc(verdict)}</p> | |
| <div class="bb-scroll"><table class="bb-table"> | |
| <thead><tr><th></th><th class='bb-r'>MAPE</th><th class='bb-r'>MAE</th></tr></thead> | |
| <tbody> | |
| <tr><td>SARIMA, next-day</td><td class='bb-r'>{esc(pct(b.model_mape))}</td><td class='bb-r'>{esc(rp(b.model_mae))}</td></tr> | |
| <tr><td>Naive: same as last close</td><td class='bb-r'>{esc(pct(b.naive_mape))}</td><td class='bb-r'>{esc(rp(b.naive_mae))}</td></tr> | |
| </tbody> | |
| </table></div> | |
| <p class="bb-note">Up/down called right: {esc(hit)}. The model is fitted before this window and only sees each | |
| real close after forecasting it. A low MAPE on daily prices is easy, since the naive guess already gets | |
| {esc(pct(b.naive_mape))}. Beating that is the real test.</p> | |
| </div>""" | |
| def status(pred: Prediction) -> str: | |
| p = pred.prices | |
| src = "synthetic demo data" if p.source == "demo" else "Yahoo Finance (BBCA.JK, adjusted)" | |
| return ( | |
| f'<p class="bb-status">Data: {esc(src)}, fetched {esc(p.fetched_at.strftime("%d %b %Y %H:%M"))} WIB. ' | |
| f"Model: SARIMA(0,1,0)(2,1,0)[5] on the last {pred.forecast.fit_points} sessions. " | |
| "A statistics exercise, not investment advice.</p>" | |
| ) | |