Seed dashboard price chart with real OHLCV history from Yahoo Finance
Browse files- Script: --seed-chart flag writes OHLCV candles into dashboard_history.db
- Dashboard: auto-seeds from ohlcv/*.json on first run if DB is empty
- Dashboard: /history/symbols endpoint for pre-populating chart dropdown
- Frontend: loads available symbols on page load for pre-session chart display
- Fix HISTORY_DB default path after shared_data -> shared/data merge
- 1W shows ~5 daily candles, 1M shows ~20 daily candles with real prices
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- dashboard/dashboard.py +46 -1
- dashboard/templates/index.html +20 -0
- scripts/update_securities_prices.py +47 -0
dashboard/dashboard.py
CHANGED
|
@@ -220,7 +220,7 @@ def _generate_and_broadcast():
|
|
| 220 |
print(f"[Dashboard/LLM] LLM failed: {source}")
|
| 221 |
|
| 222 |
# ββ OHLCV History ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 223 |
-
HISTORY_DB = os.getenv("HISTORY_DB", "/app/data/dashboard_history.db")
|
| 224 |
BUCKET_SIZE = 60 # 1-minute candles
|
| 225 |
|
| 226 |
PERIOD_SECONDS = {
|
|
@@ -248,9 +248,42 @@ def init_history_db():
|
|
| 248 |
)
|
| 249 |
""")
|
| 250 |
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
conn.close()
|
| 252 |
|
| 253 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
def record_trade(symbol, price, qty, ts):
|
| 255 |
if not symbol or price <= 0:
|
| 256 |
return
|
|
@@ -770,6 +803,18 @@ def session_status():
|
|
| 770 |
|
| 771 |
# ββ History endpoint βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 772 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 773 |
@app.route("/history/<symbol>")
|
| 774 |
def history(symbol):
|
| 775 |
period = request.args.get("period", "1d")
|
|
|
|
| 220 |
print(f"[Dashboard/LLM] LLM failed: {source}")
|
| 221 |
|
| 222 |
# ββ OHLCV History ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 223 |
+
HISTORY_DB = os.getenv("HISTORY_DB", "/app/shared/data/dashboard_history.db")
|
| 224 |
BUCKET_SIZE = 60 # 1-minute candles
|
| 225 |
|
| 226 |
PERIOD_SECONDS = {
|
|
|
|
| 248 |
)
|
| 249 |
""")
|
| 250 |
conn.commit()
|
| 251 |
+
|
| 252 |
+
# Auto-seed from OHLCV JSON files if DB is empty
|
| 253 |
+
row_count = conn.execute("SELECT COUNT(*) FROM ohlcv").fetchone()[0]
|
| 254 |
+
if row_count == 0:
|
| 255 |
+
_seed_history_from_ohlcv(conn)
|
| 256 |
+
|
| 257 |
conn.close()
|
| 258 |
|
| 259 |
|
| 260 |
+
def _seed_history_from_ohlcv(conn):
|
| 261 |
+
"""Seed dashboard_history.db from shared/data/ohlcv/*.json on first run."""
|
| 262 |
+
import glob as globmod
|
| 263 |
+
ohlcv_dir = os.path.join(os.path.dirname(HISTORY_DB), "ohlcv")
|
| 264 |
+
if not os.path.isdir(ohlcv_dir):
|
| 265 |
+
return
|
| 266 |
+
count = 0
|
| 267 |
+
for path in sorted(globmod.glob(os.path.join(ohlcv_dir, "*.json"))):
|
| 268 |
+
sym = os.path.splitext(os.path.basename(path))[0]
|
| 269 |
+
try:
|
| 270 |
+
with open(path) as f:
|
| 271 |
+
bars = json.load(f)
|
| 272 |
+
for bar in bars:
|
| 273 |
+
from datetime import datetime as _dt
|
| 274 |
+
dt = _dt.strptime(bar["date"], "%Y-%m-%d").replace(hour=10)
|
| 275 |
+
bucket = int(dt.timestamp() // 60) * 60
|
| 276 |
+
conn.execute(
|
| 277 |
+
"INSERT OR IGNORE INTO ohlcv VALUES (?,?,?,?,?,?,?)",
|
| 278 |
+
(sym, bucket, bar["open"], bar["high"], bar["low"], bar["close"], bar["volume"]),
|
| 279 |
+
)
|
| 280 |
+
count += 1
|
| 281 |
+
except Exception as e:
|
| 282 |
+
print(f"[History] Error seeding {sym}: {e}")
|
| 283 |
+
conn.commit()
|
| 284 |
+
print(f"[History] Auto-seeded {count} candles from OHLCV files")
|
| 285 |
+
|
| 286 |
+
|
| 287 |
def record_trade(symbol, price, qty, ts):
|
| 288 |
if not symbol or price <= 0:
|
| 289 |
return
|
|
|
|
| 803 |
|
| 804 |
# ββ History endpoint βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 805 |
|
| 806 |
+
@app.route("/history/symbols")
|
| 807 |
+
def history_symbols():
|
| 808 |
+
"""Return list of symbols that have historical OHLCV data."""
|
| 809 |
+
try:
|
| 810 |
+
conn = sqlite3.connect(HISTORY_DB)
|
| 811 |
+
rows = conn.execute("SELECT DISTINCT symbol FROM ohlcv ORDER BY symbol").fetchall()
|
| 812 |
+
conn.close()
|
| 813 |
+
return jsonify({"symbols": [r[0] for r in rows]})
|
| 814 |
+
except Exception:
|
| 815 |
+
return jsonify({"symbols": []})
|
| 816 |
+
|
| 817 |
+
|
| 818 |
@app.route("/history/<symbol>")
|
| 819 |
def history(symbol):
|
| 820 |
period = request.args.get("period", "1d")
|
dashboard/templates/index.html
CHANGED
|
@@ -1617,6 +1617,26 @@
|
|
| 1617 |
connectSSE();
|
| 1618 |
loadAIConfig();
|
| 1619 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1620 |
// Refresh order book panel every 3 seconds
|
| 1621 |
setInterval(() => {
|
| 1622 |
const sym = document.getElementById("bbo-symbol-select").value;
|
|
|
|
| 1617 |
connectSSE();
|
| 1618 |
loadAIConfig();
|
| 1619 |
|
| 1620 |
+
// Pre-populate history symbol dropdown from DB (shows chart before session starts)
|
| 1621 |
+
try {
|
| 1622 |
+
const resp = await fetch("/history/symbols");
|
| 1623 |
+
const data = await resp.json();
|
| 1624 |
+
const histSel = document.getElementById("history-symbol");
|
| 1625 |
+
const existing = new Set(Array.from(histSel.options).map(o => o.value));
|
| 1626 |
+
(data.symbols || []).forEach(sym => {
|
| 1627 |
+
if (!existing.has(sym)) {
|
| 1628 |
+
const opt = document.createElement("option");
|
| 1629 |
+
opt.value = sym; opt.textContent = sym;
|
| 1630 |
+
histSel.appendChild(opt);
|
| 1631 |
+
}
|
| 1632 |
+
});
|
| 1633 |
+
// Default to 1W view if there's historical data and no live session
|
| 1634 |
+
if (data.symbols && data.symbols.length > 0 && !document.getElementById("history-period").value) {
|
| 1635 |
+
document.getElementById("history-period").value = "1w";
|
| 1636 |
+
}
|
| 1637 |
+
loadHistory();
|
| 1638 |
+
} catch(e) { /* ignore */ }
|
| 1639 |
+
|
| 1640 |
// Refresh order book panel every 3 seconds
|
| 1641 |
setInterval(() => {
|
| 1642 |
const sym = document.getElementById("bbo-symbol-select").value;
|
scripts/update_securities_prices.py
CHANGED
|
@@ -8,12 +8,14 @@ Usage:
|
|
| 8 |
python scripts/update_securities_prices.py --history 10 # also print 10-day history
|
| 9 |
python scripts/update_securities_prices.py --reset-start # reset start_price to oldest close
|
| 10 |
python scripts/update_securities_prices.py --ohlcv 60 # export 60 days of OHLCV for RL agent
|
|
|
|
| 11 |
|
| 12 |
Requires: pip install yfinance
|
| 13 |
"""
|
| 14 |
import argparse
|
| 15 |
import json
|
| 16 |
import os
|
|
|
|
| 17 |
import sys
|
| 18 |
from datetime import datetime
|
| 19 |
|
|
@@ -45,6 +47,7 @@ TICKER_MAP = {
|
|
| 45 |
|
| 46 |
SECURITIES_FILE = os.path.join(os.path.dirname(__file__), "..", "shared", "data", "securities.txt")
|
| 47 |
OHLCV_DIR = os.path.join(os.path.dirname(__file__), "..", "shared", "data", "ohlcv")
|
|
|
|
| 48 |
|
| 49 |
# Tickers that trade in USD and need EUR conversion
|
| 50 |
USD_TICKERS = {"AMZN", "TSLA", "NVDA", "GOOGL", "AAPL"}
|
|
@@ -206,6 +209,45 @@ def export_ohlcv(results):
|
|
| 206 |
print(f"Exported OHLCV for {exported} symbols to {OHLCV_DIR}")
|
| 207 |
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
def main():
|
| 210 |
parser = argparse.ArgumentParser(description="Update securities.txt with real ATHEX prices")
|
| 211 |
parser.add_argument("--history", type=int, default=0,
|
|
@@ -218,6 +260,8 @@ def main():
|
|
| 218 |
help="Output results as JSON")
|
| 219 |
parser.add_argument("--ohlcv", type=int, default=0,
|
| 220 |
help="Export N days of OHLCV data for RL agent (default: 0, disabled)")
|
|
|
|
|
|
|
| 221 |
args = parser.parse_args()
|
| 222 |
|
| 223 |
days = max(args.history, args.ohlcv, 10)
|
|
@@ -236,6 +280,9 @@ def main():
|
|
| 236 |
if args.ohlcv > 0:
|
| 237 |
print(f"\n=== Exporting OHLCV for RL Agent ({days} bars) ===")
|
| 238 |
export_ohlcv(results)
|
|
|
|
|
|
|
|
|
|
| 239 |
else:
|
| 240 |
print("(dry run β no files updated)")
|
| 241 |
|
|
|
|
| 8 |
python scripts/update_securities_prices.py --history 10 # also print 10-day history
|
| 9 |
python scripts/update_securities_prices.py --reset-start # reset start_price to oldest close
|
| 10 |
python scripts/update_securities_prices.py --ohlcv 60 # export 60 days of OHLCV for RL agent
|
| 11 |
+
python scripts/update_securities_prices.py --seed-chart # seed dashboard price chart DB
|
| 12 |
|
| 13 |
Requires: pip install yfinance
|
| 14 |
"""
|
| 15 |
import argparse
|
| 16 |
import json
|
| 17 |
import os
|
| 18 |
+
import sqlite3
|
| 19 |
import sys
|
| 20 |
from datetime import datetime
|
| 21 |
|
|
|
|
| 47 |
|
| 48 |
SECURITIES_FILE = os.path.join(os.path.dirname(__file__), "..", "shared", "data", "securities.txt")
|
| 49 |
OHLCV_DIR = os.path.join(os.path.dirname(__file__), "..", "shared", "data", "ohlcv")
|
| 50 |
+
HISTORY_DB = os.path.join(os.path.dirname(__file__), "..", "shared", "data", "dashboard_history.db")
|
| 51 |
|
| 52 |
# Tickers that trade in USD and need EUR conversion
|
| 53 |
USD_TICKERS = {"AMZN", "TSLA", "NVDA", "GOOGL", "AAPL"}
|
|
|
|
| 209 |
print(f"Exported OHLCV for {exported} symbols to {OHLCV_DIR}")
|
| 210 |
|
| 211 |
|
| 212 |
+
def seed_dashboard_db(results):
|
| 213 |
+
"""Insert OHLCV data into dashboard_history.db for the price chart.
|
| 214 |
+
|
| 215 |
+
Each daily bar is inserted as a single candle at 10:00 market open
|
| 216 |
+
of that trading day (bucket = Unix timestamp rounded to 60s).
|
| 217 |
+
"""
|
| 218 |
+
conn = sqlite3.connect(HISTORY_DB)
|
| 219 |
+
conn.execute("""
|
| 220 |
+
CREATE TABLE IF NOT EXISTS ohlcv (
|
| 221 |
+
symbol TEXT,
|
| 222 |
+
bucket INTEGER,
|
| 223 |
+
open REAL,
|
| 224 |
+
high REAL,
|
| 225 |
+
low REAL,
|
| 226 |
+
close REAL,
|
| 227 |
+
volume INTEGER,
|
| 228 |
+
PRIMARY KEY (symbol, bucket)
|
| 229 |
+
)
|
| 230 |
+
""")
|
| 231 |
+
|
| 232 |
+
inserted = 0
|
| 233 |
+
for sym, data in results.items():
|
| 234 |
+
if data is None or "ohlcv" not in data:
|
| 235 |
+
continue
|
| 236 |
+
for bar in data["ohlcv"]:
|
| 237 |
+
# Convert date string to Unix timestamp at 10:00 local time
|
| 238 |
+
dt = datetime.strptime(bar["date"], "%Y-%m-%d").replace(hour=10, minute=0)
|
| 239 |
+
bucket = int(dt.timestamp() // 60) * 60
|
| 240 |
+
conn.execute(
|
| 241 |
+
"INSERT OR REPLACE INTO ohlcv VALUES (?,?,?,?,?,?,?)",
|
| 242 |
+
(sym, bucket, bar["open"], bar["high"], bar["low"], bar["close"], bar["volume"]),
|
| 243 |
+
)
|
| 244 |
+
inserted += 1
|
| 245 |
+
|
| 246 |
+
conn.commit()
|
| 247 |
+
conn.close()
|
| 248 |
+
print(f"Seeded {inserted} candles into {HISTORY_DB}")
|
| 249 |
+
|
| 250 |
+
|
| 251 |
def main():
|
| 252 |
parser = argparse.ArgumentParser(description="Update securities.txt with real ATHEX prices")
|
| 253 |
parser.add_argument("--history", type=int, default=0,
|
|
|
|
| 260 |
help="Output results as JSON")
|
| 261 |
parser.add_argument("--ohlcv", type=int, default=0,
|
| 262 |
help="Export N days of OHLCV data for RL agent (default: 0, disabled)")
|
| 263 |
+
parser.add_argument("--seed-chart", action="store_true",
|
| 264 |
+
help="Seed dashboard_history.db with fetched OHLCV for price chart display")
|
| 265 |
args = parser.parse_args()
|
| 266 |
|
| 267 |
days = max(args.history, args.ohlcv, 10)
|
|
|
|
| 280 |
if args.ohlcv > 0:
|
| 281 |
print(f"\n=== Exporting OHLCV for RL Agent ({days} bars) ===")
|
| 282 |
export_ohlcv(results)
|
| 283 |
+
if args.seed_chart:
|
| 284 |
+
print(f"\n=== Seeding Dashboard Price Chart ===")
|
| 285 |
+
seed_dashboard_db(results)
|
| 286 |
else:
|
| 287 |
print("(dry run β no files updated)")
|
| 288 |
|