"""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 ( '
' '

Waiting for a run

' "

Press Get fresh forecast. The app pulls the latest BBCA prices, " "refits the model and shows where the next closes are likely to land.

" "
" ) def error(message: str) -> str: return f'

No forecast

{esc(message)}

' 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 = ( '
' f"Market open. Running price {esc(rp(prices.live_price))}, not a close yet, {esc(used)}.
" ) demo = "" if prices.source == "demo": demo = '
DEMO MODE · synthetic prices, not real BBCA data
' 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"""
{demo}
RUN {run_no:03d} · {esc(pd.Timestamp.now(tz=TZ).strftime('%H:%M:%S'))} WIB

{esc(base_label)}

{esc(rp(base_value))}

{esc(base_date)}

Forecast · {esc(day(fc.dates[0]))}

{esc(rp(nxt))}

{esc(pct(move, signed=True))} vs {esc(base_label.lower())}

95% range

{esc(rp(fc.lower[0]))} – {esc(rp(fc.upper[0]))}

±{esc(pct(width / 2))} around the forecast

{live}
""" 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( "" f"{esc(day(d))}" f"{esc(rp(m))}" f" prev else 'down' if m < prev else 'flat')}'>{esc(pct(move, signed=True))}" f"{esc(rp(lo))} – {esc(rp(hi))}" "" ) prev = m return ( '

Forecast by session

' '
' "" "" + "".join(rows) + "
SessionClosevs start95% range
" '

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.

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

How it did over the last {len(b.actual)} sessions

{esc(verdict)}

MAPEMAE
SARIMA, next-day{esc(pct(b.model_mape))}{esc(rp(b.model_mae))}
Naive: same as last close{esc(pct(b.naive_mape))}{esc(rp(b.naive_mae))}

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.

""" def status(pred: Prediction) -> str: p = pred.prices src = "synthetic demo data" if p.source == "demo" else "Yahoo Finance (BBCA.JK, adjusted)" return ( f'

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.

" )