Spaces:
Sleeping
Sleeping
File size: 6,219 Bytes
ed25d75 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | """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>"
)
|