feat: dynamic market ticker watchlist (Markets page)

Add ability to add/remove custom tickers (e.g. EURCHF=X) on the
Markets & Prices page without editing config. Tickers are validated
via yfinance, persisted in market_watchlist SQLite table, merged into
the quotes feed as a 'custom' group, and shown in a dedicated tab
with per-card remove buttons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-22 21:54:58 +02:00
parent 78c60d5254
commit c3ea0b7f8c
5 changed files with 224 additions and 19 deletions

View File

@@ -39,6 +39,38 @@ def watchlist():
return WATCHLIST
@router.get("/custom-tickers")
def list_custom_tickers():
from services.database import get_market_custom_tickers
return get_market_custom_tickers()
@router.post("/custom-tickers/{ticker}")
def add_custom_ticker(ticker: str):
from services.data_fetcher import get_quote
from services.database import add_market_custom_ticker
import yfinance as yf
ticker = ticker.strip().upper()
q = get_quote(ticker)
if not q or not q.get("price"):
from fastapi import HTTPException
raise HTTPException(400, f"Ticker '{ticker}' not found on yfinance")
try:
info = yf.Ticker(ticker).fast_info
name = getattr(info, "long_name", None) or getattr(info, "short_name", None) or ticker
except Exception:
name = ticker
add_market_custom_ticker(ticker, name)
return {"ticker": ticker, "name": name, "price": q["price"]}
@router.delete("/custom-tickers/{ticker}")
def remove_custom_ticker(ticker: str):
from services.database import remove_market_custom_ticker
remove_market_custom_ticker(ticker.strip().upper())
return {"removed": ticker.upper()}
@router.get("/validate")
def validate_ticker(symbol: str = Query(..., description="Ticker to validate against yfinance")):
"""Check if a ticker is fetchable. Returns valid=True + live price/name, or valid=False + reason."""

View File

@@ -136,6 +136,24 @@ def get_all_quotes() -> Dict[str, List[Dict[str, Any]]]:
q["asset_class"] = asset_class
quotes.append(q)
result[asset_class] = quotes
# User-added custom tickers
try:
from services.database import get_market_custom_tickers
custom_entries = get_market_custom_tickers()
if custom_entries:
custom_quotes = []
for entry in custom_entries:
q = get_quote(entry["ticker"])
if q:
q["name"] = entry["name"] or entry["ticker"]
q["asset_class"] = "custom"
custom_quotes.append(q)
if custom_quotes:
result["custom"] = custom_quotes
except Exception:
pass
return result

View File

@@ -91,6 +91,12 @@ def init_db():
# Regime / counter-scenario architecture
"ALTER TABLE custom_patterns ADD COLUMN regime_tag TEXT",
"ALTER TABLE custom_patterns ADD COLUMN counter_of TEXT",
# Markets & Prices — user-defined watchlist
"""CREATE TABLE IF NOT EXISTS market_watchlist (
ticker TEXT PRIMARY KEY,
name TEXT,
added_at TEXT DEFAULT (datetime('now'))
)""",
]:
try:
c.execute(_sql)
@@ -2307,6 +2313,42 @@ def remove_watchlist_ticker(ticker: str) -> bool:
return changed
# ── Market Watchlist (user-added tickers for Markets page) ───────────────────
def get_market_custom_tickers() -> List[Dict]:
conn = get_conn()
rows = conn.execute(
"SELECT ticker, name, added_at FROM market_watchlist ORDER BY added_at ASC"
).fetchall()
conn.close()
return [dict(r) for r in rows]
def add_market_custom_ticker(ticker: str, name: str = "") -> bool:
conn = get_conn()
try:
conn.execute(
"INSERT OR IGNORE INTO market_watchlist (ticker, name) VALUES (?, ?)",
(ticker.upper(), name),
)
inserted = conn.total_changes > 0
conn.commit()
except Exception:
inserted = False
finally:
conn.close()
return inserted
def remove_market_custom_ticker(ticker: str) -> bool:
conn = get_conn()
conn.execute("DELETE FROM market_watchlist WHERE ticker=?", (ticker.upper(),))
changed = conn.total_changes > 0
conn.commit()
conn.close()
return changed
# ── System Logs ───────────────────────────────────────────────────────────────
def log_system_event(