From a442b2caca1a7913ba941a733dbf4462687a9c93 Mon Sep 17 00:00:00 2001 From: Rufus King Date: Mon, 27 Jul 2026 20:23:56 -0400 Subject: [PATCH] Added DateTime to WebService --- Stocks/WebService/app.py | 100 ++++++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 12 deletions(-) diff --git a/Stocks/WebService/app.py b/Stocks/WebService/app.py index bd3d3db..f930126 100644 --- a/Stocks/WebService/app.py +++ b/Stocks/WebService/app.py @@ -29,7 +29,7 @@ def get_db(): """) return conn -_current_cache = {} # ticker -> (price, resolved_date, fetched_at) +_current_cache = {} # ticker -> (price, resolved_date, date, time, fetched_at) _current_cache_lock = threading.Lock() @@ -45,24 +45,45 @@ def fetch_yahoo_price(ticker, target_date): 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) + r = requests.get( + url, + params={"period1": period1, "period2": period2, "interval": "1d"}, + headers=headers, + timeout=10 + ) 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"] + meta = result.get("meta", {}) except (KeyError, IndexError, TypeError): return None, "no historical data" + # Yahoo's timestamp for the latest regular-market update + market_timestamp = meta.get("regularMarketTime") + + if market_timestamp: + market_datetime = datetime.utcfromtimestamp(market_timestamp) + market_date = market_datetime.strftime("%Y-%m-%d") + market_time = market_datetime.strftime("%H:%M:%S") + else: + market_date = None + market_time = None + 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) @@ -70,7 +91,12 @@ def fetch_yahoo_price(ticker, target_date): if best is None: return None, "no trading day found in window" - return {"resolved_date": best[0].strftime("%Y-%m-%d"), "price": round(best[1], 4)}, None + return { + "resolved_date": best[0].strftime("%Y-%m-%d"), + "price": round(best[1], 4), + "date": market_date, + "time": market_time + }, None @app.route("/current") @@ -79,23 +105,47 @@ def current_price(): return jsonify({"error": "unauthorized"}), 401 ticker = request.args.get("ticker", "").upper().strip() + if not ticker: return jsonify({"error": "missing ticker"}), 400 now = time.time() + with _current_cache_lock: cached = _current_cache.get(ticker) - if cached and (now - cached[2]) < CURRENT_TTL_SECONDS: - return jsonify({"ticker": ticker, "price": cached[0], "resolved_date": cached[1], "cached": True}) + + if cached and (now - cached[4]) < CURRENT_TTL_SECONDS: + return jsonify({ + "ticker": ticker, + "price": cached[0], + "resolved_date": cached[1], + "cached": True, + "date": cached[2], + "time": cached[3] + }) result, err = fetch_yahoo_price(ticker, datetime.utcnow()) + if err: return jsonify({"ticker": ticker, "error": err}), 502 with _current_cache_lock: - _current_cache[ticker] = (result["price"], result["resolved_date"], now) + _current_cache[ticker] = ( + result["price"], + result["resolved_date"], + result["date"], + result["time"], + now + ) - return jsonify({"ticker": ticker, "price": result["price"], "resolved_date": result["resolved_date"], "cached": False}) + return jsonify({ + "ticker": ticker, + "price": result["price"], + "resolved_date": result["resolved_date"], + "cached": False, + "date": result["date"], + "time": result["time"] + }) @app.route("/historical") @@ -105,6 +155,7 @@ def historical_price(): ticker = request.args.get("ticker", "").upper().strip() date_str = request.args.get("date", "").strip() + if not ticker or not date_str: return jsonify({"error": "missing ticker or date"}), 400 @@ -113,35 +164,60 @@ 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 instead + # If someone asks "historical" for today/future, + # treat it like /current instead 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=?", (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_date": row[0], + "price": row[1], + "cached": True + }) 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, result["resolved_date"], result["price"]), + "INSERT OR REPLACE INTO historical_cache " + "(ticker, requested_date, resolved_date, price) " + "VALUES (?, ?, ?, ?)", + ( + ticker, + date_str, + result["resolved_date"], + result["price"] + ), ) + conn.commit() conn.close() - return jsonify({"ticker": ticker, "requested_date": date_str, "resolved_date": result["resolved_date"], "price": result["price"], "cached": False}) + return jsonify({ + "ticker": ticker, + "requested_date": date_str, + "resolved_date": result["resolved_date"], + "price": result["price"], + "cached": False + }) @app.route("/health")