AndyKandy26 commited on
Commit
5cf14de
Β·
verified Β·
1 Parent(s): 830abee

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +125 -0
  2. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FinWise β€” FastAPI backend for HuggingFace Spaces
3
+ -------------------------------------------------
4
+ β€’ Serves all static HTML/CSS/JS files from the same directory
5
+ β€’ /api/quotes?symbols=NVDA,AAPL,MSFT β†’ live quotes via yfinance
6
+ β€’ /api/chart?symbol=NVDA&days=8 β†’ daily closes for sparkline
7
+ β€’ Full CORS enabled so the browser JS can call these endpoints freely
8
+ β€’ Runs on port 7860 (HuggingFace default)
9
+ """
10
+
11
+ import os
12
+ import json
13
+ from pathlib import Path
14
+ from typing import List
15
+
16
+ import yfinance as yf
17
+ from fastapi import FastAPI, Query, HTTPException
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from fastapi.staticfiles import StaticFiles
20
+ from fastapi.responses import FileResponse, JSONResponse
21
+ import uvicorn
22
+
23
+ # ── App ───────────────────────────────────────────────────────────────────────
24
+ app = FastAPI(title="FinWise API", docs_url="/api/docs")
25
+
26
+ # Allow all origins (the page is served from the same Space, but be permissive)
27
+ app.add_middleware(
28
+ CORSMiddleware,
29
+ allow_origins=["*"],
30
+ allow_methods=["GET"],
31
+ allow_headers=["*"],
32
+ )
33
+
34
+ # ── Helpers ──────────────────────────────────────────────────────────────────
35
+ def safe_float(val, decimals: int = 2):
36
+ """Return a rounded float or None if value is NaN / missing."""
37
+ try:
38
+ v = float(val)
39
+ if v != v: # NaN check
40
+ return None
41
+ return round(v, decimals)
42
+ except Exception:
43
+ return None
44
+
45
+
46
+ # ── Routes ───────────────────────────────────────────────────────────────────
47
+
48
+ @app.get("/api/quotes")
49
+ def get_quotes(symbols: str = Query(..., description="Comma-separated tickers, e.g. NVDA,AAPL")):
50
+ """
51
+ Returns a dict keyed by ticker symbol:
52
+ { "NVDA": { "price": 875.24, "chg1d": 2.4, "vol": 45000000 }, ... }
53
+ """
54
+ tickers = [t.strip().upper() for t in symbols.split(",") if t.strip()]
55
+ if not tickers:
56
+ raise HTTPException(status_code=400, detail="No valid symbols provided")
57
+ if len(tickers) > 30:
58
+ raise HTTPException(status_code=400, detail="Max 30 symbols per request")
59
+
60
+ try:
61
+ # yfinance batch download β€” fast_info is the lightest call
62
+ data = yf.Tickers(" ".join(tickers))
63
+ result = {}
64
+ for sym in tickers:
65
+ try:
66
+ t = data.tickers[sym]
67
+ fi = t.fast_info # lightweight, no extra HTTP round-trip
68
+ result[sym] = {
69
+ "price": safe_float(fi.last_price),
70
+ "chg1d": safe_float(fi.last_price / fi.previous_close * 100 - 100)
71
+ if fi.previous_close else None,
72
+ "vol": int(fi.three_month_average_volume or 0) or None,
73
+ "prev": safe_float(fi.previous_close),
74
+ "high": safe_float(fi.day_high),
75
+ "low": safe_float(fi.day_low),
76
+ }
77
+ except Exception:
78
+ result[sym] = {"price": None, "chg1d": None, "vol": None}
79
+
80
+ return JSONResponse(content=result)
81
+
82
+ except Exception as e:
83
+ raise HTTPException(status_code=502, detail=f"yfinance error: {str(e)}")
84
+
85
+
86
+ @app.get("/api/chart")
87
+ def get_chart(symbol: str = Query(...), days: int = Query(default=8, ge=2, le=30)):
88
+ """
89
+ Returns daily closing prices for the last `days` trading days.
90
+ Used for sparklines.
91
+ { "symbol": "NVDA", "closes": [820.1, 835.4, ...] }
92
+ """
93
+ sym = symbol.strip().upper()
94
+ try:
95
+ hist = yf.Ticker(sym).history(period=f"{days}d", interval="1d")
96
+ closes = [safe_float(v) for v in hist["Close"].tolist() if v == v]
97
+ closes = [c for c in closes if c is not None]
98
+ return JSONResponse(content={"symbol": sym, "closes": closes[-7:]})
99
+ except Exception as e:
100
+ raise HTTPException(status_code=502, detail=f"yfinance chart error: {str(e)}")
101
+
102
+
103
+ @app.get("/api/health")
104
+ def health():
105
+ return {"status": "ok", "service": "FinWise API"}
106
+
107
+
108
+ # ── Serve static files (HTML/CSS/JS) ─────────────────────────────────────────
109
+ # All .html files in the same directory are served directly.
110
+ # The root "/" returns index.html.
111
+ BASE_DIR = Path(__file__).parent
112
+
113
+ @app.get("/")
114
+ def root():
115
+ return FileResponse(BASE_DIR / "index.html")
116
+
117
+ # Mount everything else as static β€” CSS, JS, other HTML pages
118
+ # We do this AFTER the API routes so /api/* is not caught by StaticFiles
119
+ app.mount("/", StaticFiles(directory=str(BASE_DIR), html=True), name="static")
120
+
121
+
122
+ # ── Entry point ─────��─────────────────────────────────────────────────────────
123
+ if __name__ == "__main__":
124
+ port = int(os.environ.get("PORT", 7860))
125
+ uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn[standard]==0.29.0
3
+ yfinance==0.2.40
4
+ pandas==2.2.2
5
+ requests==2.31.0