diff --git a/backend/routers/market_data.py b/backend/routers/market_data.py index b8d8197..e9dba99 100644 --- a/backend/routers/market_data.py +++ b/backend/routers/market_data.py @@ -45,6 +45,16 @@ def list_custom_tickers(): return get_market_custom_tickers() +_QUOTE_TYPE_TO_ASSET_CLASS = { + "CURRENCY": "forex", + "FUTURE": "energy", + "INDEX": "indices", + "ETF": "etfs", + "EQUITY": "equities", + "MUTUALFUND": "etfs", +} + + @router.post("/custom-tickers/{ticker}") def add_custom_ticker(ticker: str): from services.data_fetcher import get_quote @@ -55,13 +65,17 @@ def add_custom_ticker(ticker: str): if not q or not q.get("price"): from fastapi import HTTPException raise HTTPException(400, f"Ticker '{ticker}' not found on yfinance") + name = ticker + asset_class = "custom" try: info = yf.Ticker(ticker).fast_info name = getattr(info, "long_name", None) or getattr(info, "short_name", None) or ticker + quote_type = getattr(info, "quote_type", "") or "" + asset_class = _QUOTE_TYPE_TO_ASSET_CLASS.get(quote_type.upper(), "custom") except Exception: - name = ticker - add_market_custom_ticker(ticker, name) - return {"ticker": ticker, "name": name, "price": q["price"]} + pass + add_market_custom_ticker(ticker, name, asset_class) + return {"ticker": ticker, "name": name, "asset_class": asset_class, "price": q["price"]} @router.delete("/custom-tickers/{ticker}") diff --git a/backend/services/data_fetcher.py b/backend/services/data_fetcher.py index ebebc12..9bbb339 100644 --- a/backend/services/data_fetcher.py +++ b/backend/services/data_fetcher.py @@ -137,20 +137,23 @@ def get_all_quotes() -> Dict[str, List[Dict[str, Any]]]: quotes.append(q) result[asset_class] = quotes - # User-added custom tickers + # User-added custom tickers — placed in their detected asset class group 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 + for entry in (custom_entries or []): + q = get_quote(entry["ticker"]) + if not q: + continue + q["name"] = entry["name"] or entry["ticker"] + grp = entry.get("asset_class") or "custom" + q["asset_class"] = grp + if grp not in result: + result[grp] = [] + # avoid duplicates (ticker already in built-in group) + existing_symbols = {x["symbol"] for x in result[grp]} + if q["symbol"] not in existing_symbols: + result[grp].append(q) except Exception: pass diff --git a/backend/services/database.py b/backend/services/database.py index 8eaf8b8..2df1cd4 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -97,6 +97,7 @@ def init_db(): name TEXT, added_at TEXT DEFAULT (datetime('now')) )""", + "ALTER TABLE market_watchlist ADD COLUMN asset_class TEXT DEFAULT 'custom'", ]: try: c.execute(_sql) @@ -2318,18 +2319,18 @@ def remove_watchlist_ticker(ticker: str) -> bool: 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" + "SELECT ticker, name, asset_class, 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: +def add_market_custom_ticker(ticker: str, name: str = "", asset_class: str = "custom") -> bool: conn = get_conn() try: conn.execute( - "INSERT OR IGNORE INTO market_watchlist (ticker, name) VALUES (?, ?)", - (ticker.upper(), name), + "INSERT OR IGNORE INTO market_watchlist (ticker, name, asset_class) VALUES (?, ?, ?)", + (ticker.upper(), name, asset_class), ) inserted = conn.total_changes > 0 conn.commit()