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:
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user