| |
| """ |
| Fetch real ATHEX stock prices from Yahoo Finance and update securities.txt. |
| Optionally exports OHLCV bars for the RL agent observation vector. |
| |
| Usage: |
| python scripts/update_securities_prices.py # update current prices |
| python scripts/update_securities_prices.py --history 10 # also print 10-day history |
| python scripts/update_securities_prices.py --reset-start # reset start_price to oldest close |
| python scripts/update_securities_prices.py --ohlcv 60 # export 60 days of OHLCV for RL agent |
| python scripts/update_securities_prices.py --seed-chart # seed dashboard price chart DB |
| |
| Requires: pip install yfinance |
| """ |
| import argparse |
| import json |
| import os |
| import sqlite3 |
| import sys |
| from datetime import datetime |
|
|
| try: |
| import yfinance as yf |
| except ImportError: |
| print("ERROR: yfinance not installed. Run: pip install yfinance") |
| sys.exit(1) |
|
|
| |
| TICKER_MAP = { |
| "ALPHA": "ALPHA.AT", |
| "PEIR": "TPEIR.AT", |
| "EXAE": "EXAE.AT", |
| "QUEST": "QUEST.AT", |
| "NBG": "ETE.AT", |
| "EUROB": "EUROB.AT", |
| "AEG": "AEGN.AT", |
| "INTKA": "INTRK.AT", |
| "AAAK": "AAAK.AT", |
| "ATTIK": "ATTICA.AT", |
| "AMZN": "AMZN", |
| "TSLA": "TSLA", |
| "NVDA": "NVDA", |
| "GOOGL": "GOOGL", |
| "AAPL": "AAPL", |
| "ENX": "ENX.PA", |
| } |
|
|
| SECURITIES_FILE = os.path.join(os.path.dirname(__file__), "..", "shared", "data", "securities.txt") |
| OHLCV_DIR = os.path.join(os.path.dirname(__file__), "..", "shared", "data", "ohlcv") |
| HISTORY_DB = os.path.join(os.path.dirname(__file__), "..", "shared", "data", "dashboard_history.db") |
|
|
| |
| USD_TICKERS = {"AMZN", "TSLA", "NVDA", "GOOGL", "AAPL"} |
|
|
|
|
| def _get_usd_eur_rate() -> float: |
| """Fetch current USD/EUR exchange rate from Yahoo Finance.""" |
| try: |
| t = yf.Ticker("USDEUR=X") |
| hist = t.history(period="5d") |
| if not hist.empty: |
| rate = float(hist.iloc[-1]["Close"]) |
| print(f" USD/EUR rate: {rate:.4f}") |
| return rate |
| except Exception as e: |
| print(f" WARNING: Could not fetch USD/EUR rate ({e}), using fallback", file=sys.stderr) |
| return 0.92 |
|
|
|
|
| def _get_usd_eur_series(days: int) -> dict[str, float]: |
| """Fetch daily USD/EUR rates. Returns {date_str: rate}.""" |
| try: |
| t = yf.Ticker("USDEUR=X") |
| hist = t.history(period=f"{days + 10}d") |
| if not hist.empty: |
| return {d.strftime("%Y-%m-%d"): round(float(r["Close"]), 6) for d, r in hist.iterrows()} |
| except Exception: |
| pass |
| return {} |
|
|
|
|
| def fetch_prices(days=15): |
| """Fetch price history for all securities. USD prices are converted to EUR.""" |
| results = {} |
| period = f"{days + 5}d" |
|
|
| |
| usd_eur_daily = {} |
| needs_conversion = any(sym in USD_TICKERS for sym in TICKER_MAP) |
| if needs_conversion: |
| print(" Fetching USD/EUR exchange rates...") |
| usd_eur_daily = _get_usd_eur_series(days + 5) |
| fallback_rate = _get_usd_eur_rate() |
|
|
| for sym, ticker in TICKER_MAP.items(): |
| try: |
| t = yf.Ticker(ticker) |
| hist = t.history(period=period) |
| if hist.empty: |
| print(f" WARNING: {sym} ({ticker}): no data returned", file=sys.stderr) |
| results[sym] = None |
| continue |
|
|
| hist = hist.tail(days) |
|
|
| |
| convert = sym in USD_TICKERS |
|
|
| history = [] |
| ohlcv = [] |
| for date, row in hist.iterrows(): |
| date_str = date.strftime("%Y-%m-%d") |
| rate = usd_eur_daily.get(date_str, fallback_rate) if convert else 1.0 |
|
|
| history.append({ |
| "date": date_str, |
| "close": round(row["Close"] * rate, 4), |
| }) |
| ohlcv.append({ |
| "date": date_str, |
| "open": round(row["Open"] * rate, 4), |
| "high": round(row["High"] * rate, 4), |
| "low": round(row["Low"] * rate, 4), |
| "close": round(row["Close"] * rate, 4), |
| "volume": int(row["Volume"]), |
| }) |
|
|
| rate_last = 1.0 |
| if convert: |
| last_date = hist.index[-1].strftime("%Y-%m-%d") |
| rate_last = usd_eur_daily.get(last_date, fallback_rate) |
|
|
| results[sym] = { |
| "current": round(hist.iloc[-1]["Close"] * rate_last, 4), |
| "start": round(hist.iloc[0]["Close"] * (usd_eur_daily.get(hist.index[0].strftime("%Y-%m-%d"), fallback_rate) if convert else 1.0), 4), |
| "history": history, |
| "ohlcv": ohlcv, |
| "converted": convert, |
| } |
| if convert: |
| print(f" {sym}: converted USD -> EUR (rate ~{rate_last:.4f})") |
| except Exception as e: |
| print(f" ERROR: {sym} ({ticker}): {e}", file=sys.stderr) |
| results[sym] = None |
|
|
| return results |
|
|
|
|
| def update_securities_file(results, reset_start=False): |
| """Update securities.txt with real prices.""" |
| |
| existing = {} |
| if os.path.exists(SECURITIES_FILE): |
| with open(SECURITIES_FILE, "r") as f: |
| for line in f: |
| line = line.strip() |
| if not line or line.startswith("#"): |
| continue |
| parts = line.split() |
| if len(parts) >= 3: |
| existing[parts[0]] = {"start": float(parts[1]), "current": float(parts[2])} |
|
|
| |
| for sym, data in results.items(): |
| if data is None: |
| continue |
| if reset_start or sym not in existing: |
| existing[sym] = {"start": data["start"], "current": data["current"]} |
| else: |
| existing[sym]["current"] = data["current"] |
| if reset_start: |
| existing[sym]["start"] = data["start"] |
|
|
| |
| with open(SECURITIES_FILE, "w") as f: |
| f.write("#SYMBOL\t<start_price>\t<current_price>\n") |
| for sym in TICKER_MAP: |
| if sym in existing: |
| f.write(f"{sym}\t{existing[sym]['start']:.2f}\t{existing[sym]['current']:.2f}\n") |
|
|
| print(f"Updated {SECURITIES_FILE}") |
|
|
|
|
| def print_history(results): |
| """Print price history table.""" |
| print("\n=== ATHEX Price History ===\n") |
| for sym, data in results.items(): |
| if data is None: |
| print(f"{sym}: no data available") |
| continue |
| print(f"{sym} (Yahoo: {TICKER_MAP[sym]}) Current: {data['current']:.2f} EUR") |
| for day in data["history"]: |
| print(f" {day['date']} {day['close']:.4f}") |
| print() |
|
|
|
|
| def export_ohlcv(results): |
| """Export OHLCV bars as JSON files for the RL agent to seed real price history.""" |
| os.makedirs(OHLCV_DIR, exist_ok=True) |
| exported = 0 |
| for sym, data in results.items(): |
| if data is None or "ohlcv" not in data: |
| continue |
| ohlcv_file = os.path.join(OHLCV_DIR, f"{sym}.json") |
| with open(ohlcv_file, "w") as f: |
| json.dump(data["ohlcv"], f, indent=2) |
| exported += 1 |
| print(f" {sym}: {len(data['ohlcv'])} bars -> {ohlcv_file}") |
| print(f"Exported OHLCV for {exported} symbols to {OHLCV_DIR}") |
|
|
|
|
| def seed_dashboard_db(results): |
| """Insert OHLCV data into dashboard_history.db for the price chart. |
| |
| Each daily bar is inserted as a single candle at 10:00 market open |
| of that trading day (bucket = Unix timestamp rounded to 60s). |
| """ |
| conn = sqlite3.connect(HISTORY_DB) |
| conn.execute(""" |
| CREATE TABLE IF NOT EXISTS ohlcv ( |
| symbol TEXT, |
| bucket INTEGER, |
| open REAL, |
| high REAL, |
| low REAL, |
| close REAL, |
| volume INTEGER, |
| PRIMARY KEY (symbol, bucket) |
| ) |
| """) |
|
|
| inserted = 0 |
| for sym, data in results.items(): |
| if data is None or "ohlcv" not in data: |
| continue |
| for bar in data["ohlcv"]: |
| |
| dt = datetime.strptime(bar["date"], "%Y-%m-%d").replace(hour=10, minute=0) |
| bucket = int(dt.timestamp() // 60) * 60 |
| conn.execute( |
| "INSERT OR REPLACE INTO ohlcv VALUES (?,?,?,?,?,?,?)", |
| (sym, bucket, bar["open"], bar["high"], bar["low"], bar["close"], bar["volume"]), |
| ) |
| inserted += 1 |
|
|
| conn.commit() |
| conn.close() |
| print(f"Seeded {inserted} candles into {HISTORY_DB}") |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Update securities.txt with real ATHEX prices") |
| parser.add_argument("--history", type=int, default=0, |
| help="Number of historical trading days to display (default: 0)") |
| parser.add_argument("--reset-start", action="store_true", |
| help="Reset start_price to oldest fetched closing price") |
| parser.add_argument("--dry-run", action="store_true", |
| help="Fetch and display prices without writing to file") |
| parser.add_argument("--json", action="store_true", |
| help="Output results as JSON") |
| parser.add_argument("--ohlcv", type=int, default=0, |
| help="Export N days of OHLCV data for RL agent (default: 0, disabled)") |
| parser.add_argument("--seed-chart", action="store_true", |
| help="Seed dashboard_history.db with fetched OHLCV for price chart display") |
| args = parser.parse_args() |
|
|
| days = max(args.history, args.ohlcv, 10) |
| print(f"Fetching {days}-day history from Yahoo Finance...") |
| results = fetch_prices(days=days) |
|
|
| if args.json: |
| print(json.dumps(results, indent=2)) |
| return |
|
|
| if args.history > 0: |
| print_history(results) |
|
|
| if not args.dry_run: |
| update_securities_file(results, reset_start=args.reset_start) |
| if args.ohlcv > 0: |
| print(f"\n=== Exporting OHLCV for RL Agent ({days} bars) ===") |
| export_ohlcv(results) |
| if args.seed_chart: |
| print(f"\n=== Seeding Dashboard Price Chart ===") |
| seed_dashboard_db(results) |
| else: |
| print("(dry run — no files updated)") |
|
|
| |
| print("\n--- Current Prices ---") |
| for sym, data in results.items(): |
| status = f"{data['current']:.2f} EUR" if data else "N/A" |
| print(f" {sym:6s} {status}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|