Spaces:
Running
Running
| """ | |
| app.py β "Did This Stock Actually Change?" | |
| Type a ticker. Free Yahoo data comes in, and you get a plain answer: which moves in | |
| the past year were real events, which were ordinary wobble, whether the ride itself | |
| got rougher β plus the latest free headlines. No API key, no account, nothing stored. | |
| """ | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| from matplotlib.figure import Figure | |
| import numpy as np | |
| import gradio as gr | |
| import stock | |
| INK = "#1b1b1b" | |
| RED = "#d64545" | |
| ORANGE = "#e8a33d" | |
| CALM = "#2e9e5b" | |
| PERIODS = {"6 months": "6mo", "1 year": "1y", "2 years": "2y"} | |
| SENS = {1: 1.5, 2: 1.0, 3: 0.7} # 1 = only the undeniable β¦ 3 = flag early | |
| def make_plot(ticker, dates, close, a, currency): | |
| n = len(close) | |
| fig = Figure(figsize=(7.6, 4.0), dpi=96) | |
| ax = fig.add_subplot(111) | |
| t = np.arange(n) | |
| ax.plot(t, close, color=INK, lw=1.2) | |
| for e in a["shocks"]: | |
| ax.axvspan(e["i0"] + 1, e["i1"] + 1, color=RED, alpha=0.18) | |
| mid = (e["i0"] + e["i1"]) // 2 + 1 | |
| ax.text(mid, float(np.max(close)), stock.pct(e["cum_ret"]), | |
| color=RED, fontsize=8.5, ha="center", va="top", fontweight="bold") | |
| for v in a["vols"]: | |
| ax.axvline(v["at"] + 1, color=ORANGE, lw=1.6, ls="--") | |
| ax.text(v["at"] + 1, float(np.min(close)), " ride changed", | |
| color=ORANGE, fontsize=8.5, va="bottom", fontweight="bold") | |
| # x ticks: ~6 date labels | |
| idx = np.linspace(0, n - 1, 6).astype(int) | |
| ax.set_xticks(idx) | |
| ax.set_xticklabels([dates[i] for i in idx], fontsize=8) | |
| cur = f" ({currency})" if currency else "" | |
| ax.set_ylabel(f"price{cur}") | |
| ne = len(a["shocks"]) | |
| ax.set_title(f"{ticker.upper()} β red = real events ({ne}), everything else = ordinary wobble", | |
| fontsize=11) | |
| ax.grid(alpha=0.22) | |
| fig.tight_layout() | |
| return fig | |
| def news_md(ticker, items, a, dates): | |
| if not items: | |
| return ("*No free headlines available right now (Yahoo occasionally rate-limits " | |
| "shared servers β try again in a minute).*") | |
| lines = [] | |
| recent_shock = any(e["i1"] + 1 >= len(dates) - 15 for e in a["shocks"]) | |
| if recent_shock: | |
| lines.append("One of the flagged events is **recent** β these headlines may be " | |
| "the *why*:") | |
| lines.append(f"#### π° Latest free headlines for {ticker.upper()}") | |
| for it in items: | |
| meta = " Β· ".join(x for x in (it["publisher"], it["when"]) if x) | |
| title = it["title"].replace("|", "-") | |
| if it["url"]: | |
| lines.append(f"- [{title}]({it['url']}) <small>{meta}</small>") | |
| else: | |
| lines.append(f"- {title} <small>{meta}</small>") | |
| lines.append("<small>Headlines are Yahoo Finance's free feed β recent items only; " | |
| "they cannot be matched to events from months ago.</small>") | |
| return "\n".join(lines) | |
| def run_check(ticker, period_label, sensitivity): | |
| ticker = (ticker or "").strip() | |
| if not ticker: | |
| return "Type a ticker first (Yahoo format β e.g. `AAPL`, `NOK`, `BTC-USD`, `^GSPC`).", None, "" | |
| dates, close, currency, err = stock.fetch_prices(ticker, PERIODS[period_label]) | |
| if err: | |
| return err, None, "" | |
| a = stock.analyze(dates, close, SENS[int(sensitivity)]) | |
| md = stock.verdict_md(ticker.upper(), dates, close, a, currency) | |
| fig = make_plot(ticker, dates, close, a, currency) | |
| nm = news_md(ticker, stock.fetch_news(ticker), a, dates) | |
| return md, fig, nm | |
| HEADER = """ | |
| # π Did This Stock Actually Change? | |
| Financial charts make **every** wiggle look like a story. Most wiggles are dice. | |
| Type any ticker and get the honest split: | |
| > which moves were **real events** (worth asking *why*), which were **ordinary | |
| > wobble** (worth ignoring), whether **the ride itself got rougher** β and whether the | |
| > period's overall drift is even **distinguishable from luck**. | |
| Free Yahoo data, free headlines, **no API key, no account, nothing stored**. | |
| Works for stocks (`AAPL`, `NOK`), crypto (`BTC-USD`), indices (`^GSPC`, `^OMXH25`). | |
| """ | |
| FOOTER = """ | |
| --- | |
| <small>Engine: the same *Clutch* surprise gate that powers the | |
| [compute demo](https://huggingface.co/spaces/Aluode/Clutch2) and | |
| [Did Something Actually Change?](https://huggingface.co/spaces/Aluode/DidItChange), | |
| here fed daily returns (shocks) and rolling volatility (regime changes), with measured | |
| false-alarm and detection rates in the README. It describes the past only, predicts | |
| nothing, and is not investment advice. Built by Antti Luode (PerceptionLab). | |
| *Do not hype. Do not lie. Just show.*</small> | |
| """ | |
| with gr.Blocks(title="Did This Stock Actually Change?") as demo: | |
| gr.Markdown(HEADER) | |
| with gr.Row(): | |
| s_ticker = gr.Textbox(label="Ticker (Yahoo format)", value="NOK", scale=2) | |
| s_period = gr.Dropdown(list(PERIODS), value="1 year", label="Window", scale=1) | |
| s_sens = gr.Slider(1, 3, 2, step=1, scale=2, | |
| label="suspicion (1 = only the undeniable Β· 3 = flag early hints)") | |
| s_run = gr.Button("π Check it", variant="primary", scale=1) | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| s_plot = gr.Plot(label="the year, judged") | |
| s_md = gr.Markdown() | |
| with gr.Column(scale=2): | |
| s_news = gr.Markdown() | |
| gr.Examples([["NOK"], ["AAPL"], ["NVDA"], ["BTC-USD"], ["^GSPC"]], inputs=[s_ticker]) | |
| s_run.click(run_check, [s_ticker, s_period, s_sens], [s_md, s_plot, s_news]) | |
| gr.Markdown(FOOTER) | |
| if __name__ == "__main__": | |
| demo.launch() | |