Update Stock WebService to version 0
This commit is contained in:
parent
838a3a4eb7
commit
091b2f3502
1 changed files with 62 additions and 74 deletions
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue