From 091b2f3502c2f866ab372e83276052aea9de5495 Mon Sep 17 00:00:00 2001 From: Rufus King Date: Mon, 27 Jul 2026 20:02:47 -0400 Subject: [PATCH] Update Stock WebService to version 0 --- Stocks/WebService/app.py | 136 ++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 74 deletions(-) diff --git a/Stocks/WebService/app.py b/Stocks/WebService/app.py index 44c6db7..37cca90 100644 --- a/Stocks/WebService/app.py +++ b/Stocks/WebService/app.py @@ -10,10 +10,9 @@ from flask import Flask, request, jsonify app = Flask(__name__) # ---- Config ---- -FINNHUB_API_KEY = os.environ.get("FINNHUB_API_KEY", "") SHARED_SECRET = os.environ.get("PROXY_SECRET", "") DB_PATH = os.environ.get("DB_PATH", "/data/cache.db") -FINNHUB_MAX_PER_MIN = 40 # stay safely under Finnhub's 60/min cap +CURRENT_TTL_SECONDS = 15 * 60 # re-check "today" every 15 min in case NAV just> _db_lock = threading.Lock() @@ -30,29 +29,56 @@ def get_db(): """) return conn -_current_cache = {} +_current_cache = {} # ticker -> (price, resolved_date, fetched_at) _current_cache_lock = threading.Lock() -CURRENT_TTL_SECONDS = 60 - -_finnhub_calls = [] -_finnhub_lock = threading.Lock() - -def finnhub_throttle(): - with _finnhub_lock: - now = time.time() - while _finnhub_calls and _finnhub_calls[0] < now - 60: - _finnhub_calls.pop(0) - if len(_finnhub_calls) >= FINNHUB_MAX_PER_MIN: - sleep_for = 60 - (now - _finnhub_calls[0]) + 0.1 - time.sleep(max(sleep_for, 0)) - _finnhub_calls.append(time.time()) +@app.after_request +def add_cors_headers(response): + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS" + response.headers["Access-Control-Allow-Headers"] = "Content-Type" + return response def check_auth(): key = request.args.get("key", "") return bool(SHARED_SECRET) and key == SHARED_SECRET +def fetch_yahoo_price(ticker, target_date): + """Ask Yahoo for the last trading-day close on or before target_date.""" + period2 = int((target_date + timedelta(days=1)).timestamp()) + period1 = int((target_date - timedelta(days=10)).timestamp()) + + url = f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}" + headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWe> + r = requests.get(url, params={"period1": period1, "period2": period2, "inte> + + if r.status_code != 200: + return None, f"yahoo http {r.status_code}" + + data = r.json() + try: + result = data["chart"]["result"][0] + timestamps = result["timestamp"] + closes = result["indicators"]["quote"][0]["close"] + except (KeyError, IndexError, TypeError): + return None, "no historical data" + + best = None + for ts, close in zip(timestamps, closes): + if close is None: + continue + day = datetime.utcfromtimestamp(ts) + if day.date() <= target_date.date(): + if best is None or day > best[0]: + best = (day, close) + + if best is None: + return None, "no trading day found in window" + + return {"resolved_date": best[0].strftime("%Y-%m-%d"), "price": round(best[> + + @app.route("/current") def current_price(): if not check_auth(): @@ -65,30 +91,17 @@ def current_price(): now = time.time() with _current_cache_lock: cached = _current_cache.get(ticker) - if cached and (now - cached[1]) < CURRENT_TTL_SECONDS: - return jsonify({"ticker": ticker, "price": cached[0], "quote_time": cached[2], "cached": True}) + if cached and (now - cached[2]) < CURRENT_TTL_SECONDS: + return jsonify({"ticker": ticker, "price": cached[0], "resolved_dat> - finnhub_throttle() - r = requests.get( - "https://finnhub.io/api/v1/quote", - params={"symbol": ticker, "token": FINNHUB_API_KEY}, - timeout=10, - ) - data = r.json() - - if not data or data.get("c") in (None, 0): - return jsonify({"ticker": ticker, "error": "no data"}), 502 - - price = data["c"] - quote_time = None - if data.get("t"): - quote_time = datetime.utcfromtimestamp(data["t"]).isoformat() + "Z" + result, err = fetch_yahoo_price(ticker, datetime.utcnow()) + if err: + return jsonify({"ticker": ticker, "error": err}), 502 with _current_cache_lock: - _current_cache[ticker] = (price, now, quote_time) - - return jsonify({"ticker": ticker, "price": price, "quote_time": quote_time, "cached": False}) + _current_cache[ticker] = (result["price"], result["resolved_date"], now) + return jsonify({"ticker": ticker, "price": result["price"], "resolved_date"> @app.route("/historical") def historical_price(): @@ -105,60 +118,35 @@ def historical_price(): except ValueError: return jsonify({"error": "date must be YYYY-MM-DD"}), 400 + # If someone asks "historical" for today/future, treat it like /current ins> + if target_date.date() >= datetime.utcnow().date(): + return current_price() + with _db_lock: conn = get_db() row = conn.execute( - "SELECT resolved_date, price FROM historical_cache WHERE ticker=? AND requested_date=?", + "SELECT resolved_date, price FROM historical_cache WHERE ticker=? A> (ticker, date_str), ).fetchone() conn.close() if row: - return jsonify({"ticker": ticker, "requested_date": date_str, "resolved_date": row[0], "price": row[1], "cached": True}) + return jsonify({"ticker": ticker, "requested_date": date_str, "resolved> - period2 = int((target_date + timedelta(days=1)).timestamp()) - period1 = int((target_date - timedelta(days=10)).timestamp()) - - url = f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}" - headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} - r = requests.get(url, params={"period1": period1, "period2": period2, "interval": "1d"}, headers=headers, timeout=10) - - if r.status_code != 200: - return jsonify({"ticker": ticker, "error": f"yahoo http {r.status_code}"}), 502 - - data = r.json() - try: - result = data["chart"]["result"][0] - timestamps = result["timestamp"] - closes = result["indicators"]["quote"][0]["close"] - except (KeyError, IndexError, TypeError): - return jsonify({"ticker": ticker, "error": "no historical data"}), 502 - - best = None - for ts, close in zip(timestamps, closes): - if close is None: - continue - day = datetime.utcfromtimestamp(ts) - if day.date() <= target_date.date(): - if best is None or day > best[0]: - best = (day, close) - - if best is None: - return jsonify({"ticker": ticker, "error": "no trading day found in window"}), 502 - - resolved_date = best[0].strftime("%Y-%m-%d") - price = round(best[1], 4) + result, err = fetch_yahoo_price(ticker, target_date) + if err: + return jsonify({"ticker": ticker, "error": err}), 502 with _db_lock: conn = get_db() conn.execute( - "INSERT OR REPLACE INTO historical_cache (ticker, requested_date, resolved_date, price) VALUES (?, ?, ?, ?)", - (ticker, date_str, resolved_date, price), + "INSERT OR REPLACE INTO historical_cache (ticker, requested_date, r> + (ticker, date_str, result["resolved_date"], result["price"]), ) conn.commit() conn.close() - return jsonify({"ticker": ticker, "requested_date": date_str, "resolved_date": resolved_date, "price": price, "cached": False}) + return jsonify({"ticker": ticker, "requested_date": date_str, "resolved_dat> @app.route("/health")