nas08-scripts/Stocks/WebService/app.py

230 lines
6.1 KiB
Python
Raw Permalink Normal View History

2026-07-27 20:01:02 -04:00
import os
import time
import sqlite3
import threading
from datetime import datetime, timedelta
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
# ---- Config ----
SHARED_SECRET = os.environ.get("PROXY_SECRET", "")
DB_PATH = os.environ.get("DB_PATH", "/data/cache.db")
2026-07-27 20:11:24 -04:00
CURRENT_TTL_SECONDS = 15 * 60 # re-check "today" every 15 min in case NAV just posted
2026-07-27 20:01:02 -04:00
_db_lock = threading.Lock()
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS historical_cache (
ticker TEXT NOT NULL,
requested_date TEXT NOT NULL,
resolved_date TEXT NOT NULL,
price REAL NOT NULL,
PRIMARY KEY (ticker, requested_date)
)
""")
return conn
2026-07-27 20:23:56 -04:00
_current_cache = {} # ticker -> (price, resolved_date, date, time, fetched_at)
2026-07-27 20:01:02 -04:00
_current_cache_lock = threading.Lock()
def check_auth():
key = request.args.get("key", "")
return bool(SHARED_SECRET) and key == SHARED_SECRET
2026-07-27 20:02:47 -04:00
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}"
2026-07-27 20:11:24 -04:00
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
2026-07-27 20:23:56 -04:00
r = requests.get(
url,
params={"period1": period1, "period2": period2, "interval": "1d"},
headers=headers,
timeout=10
)
2026-07-27 20:02:47 -04:00
if r.status_code != 200:
return None, f"yahoo http {r.status_code}"
data = r.json()
2026-07-27 20:23:56 -04:00
2026-07-27 20:02:47 -04:00
try:
result = data["chart"]["result"][0]
timestamps = result["timestamp"]
closes = result["indicators"]["quote"][0]["close"]
2026-07-27 20:23:56 -04:00
meta = result.get("meta", {})
2026-07-27 20:02:47 -04:00
except (KeyError, IndexError, TypeError):
return None, "no historical data"
2026-07-27 20:23:56 -04:00
# 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
2026-07-27 20:02:47 -04:00
best = None
2026-07-27 20:23:56 -04:00
2026-07-27 20:02:47 -04:00
for ts, close in zip(timestamps, closes):
if close is None:
continue
2026-07-27 20:23:56 -04:00
2026-07-27 20:02:47 -04:00
day = datetime.utcfromtimestamp(ts)
2026-07-27 20:23:56 -04:00
2026-07-27 20:02:47 -04:00
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"
2026-07-27 20:23:56 -04:00
return {
"resolved_date": best[0].strftime("%Y-%m-%d"),
"price": round(best[1], 4),
"date": market_date,
"time": market_time
}, None
2026-07-27 20:02:47 -04:00
2026-07-27 20:01:02 -04:00
@app.route("/current")
def current_price():
if not check_auth():
return jsonify({"error": "unauthorized"}), 401
ticker = request.args.get("ticker", "").upper().strip()
2026-07-27 20:23:56 -04:00
2026-07-27 20:01:02 -04:00
if not ticker:
return jsonify({"error": "missing ticker"}), 400
now = time.time()
2026-07-27 20:23:56 -04:00
2026-07-27 20:01:02 -04:00
with _current_cache_lock:
cached = _current_cache.get(ticker)
2026-07-27 20:23:56 -04:00
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]
})
2026-07-27 20:01:02 -04:00
2026-07-27 20:02:47 -04:00
result, err = fetch_yahoo_price(ticker, datetime.utcnow())
2026-07-27 20:23:56 -04:00
2026-07-27 20:02:47 -04:00
if err:
return jsonify({"ticker": ticker, "error": err}), 502
2026-07-27 20:01:02 -04:00
with _current_cache_lock:
2026-07-27 20:23:56 -04:00
_current_cache[ticker] = (
result["price"],
result["resolved_date"],
result["date"],
result["time"],
now
)
2026-07-27 20:01:02 -04:00
2026-07-27 20:23:56 -04:00
return jsonify({
"ticker": ticker,
"price": result["price"],
"resolved_date": result["resolved_date"],
"cached": False,
"date": result["date"],
"time": result["time"]
})
2026-07-27 20:11:24 -04:00
2026-07-27 20:01:02 -04:00
@app.route("/historical")
def historical_price():
if not check_auth():
return jsonify({"error": "unauthorized"}), 401
ticker = request.args.get("ticker", "").upper().strip()
date_str = request.args.get("date", "").strip()
2026-07-27 20:23:56 -04:00
2026-07-27 20:01:02 -04:00
if not ticker or not date_str:
return jsonify({"error": "missing ticker or date"}), 400
try:
target_date = datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
return jsonify({"error": "date must be YYYY-MM-DD"}), 400
2026-07-27 20:23:56 -04:00
# If someone asks "historical" for today/future,
# treat it like /current instead
2026-07-27 20:02:47 -04:00
if target_date.date() >= datetime.utcnow().date():
return current_price()
2026-07-27 20:01:02 -04:00
with _db_lock:
conn = get_db()
2026-07-27 20:23:56 -04:00
2026-07-27 20:01:02 -04:00
row = conn.execute(
2026-07-27 20:11:24 -04:00
"SELECT resolved_date, price FROM historical_cache WHERE ticker=? AND requested_date=?",
2026-07-27 20:01:02 -04:00
(ticker, date_str),
).fetchone()
2026-07-27 20:23:56 -04:00
2026-07-27 20:01:02 -04:00
conn.close()
if row:
2026-07-27 20:23:56 -04:00
return jsonify({
"ticker": ticker,
"requested_date": date_str,
"resolved_date": row[0],
"price": row[1],
"cached": True
})
2026-07-27 20:01:02 -04:00
2026-07-27 20:02:47 -04:00
result, err = fetch_yahoo_price(ticker, target_date)
2026-07-27 20:23:56 -04:00
2026-07-27 20:02:47 -04:00
if err:
return jsonify({"ticker": ticker, "error": err}), 502
2026-07-27 20:01:02 -04:00
with _db_lock:
conn = get_db()
2026-07-27 20:23:56 -04:00
2026-07-27 20:01:02 -04:00
conn.execute(
2026-07-27 20:23:56 -04:00
"INSERT OR REPLACE INTO historical_cache "
"(ticker, requested_date, resolved_date, price) "
"VALUES (?, ?, ?, ?)",
(
ticker,
date_str,
result["resolved_date"],
result["price"]
),
2026-07-27 20:01:02 -04:00
)
2026-07-27 20:23:56 -04:00
2026-07-27 20:01:02 -04:00
conn.commit()
conn.close()
2026-07-27 20:23:56 -04:00
return jsonify({
"ticker": ticker,
"requested_date": date_str,
"resolved_date": result["resolved_date"],
"price": result["price"],
"cached": False
})
2026-07-27 20:01:02 -04:00
@app.route("/health")
def health():
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5005)