File size: 5,532 Bytes
3151dff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
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()