From 33097e1812310cc96ad1330bcbcf24aff4ddae6f Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Mon, 22 Jun 2026 22:11:31 +0200 Subject: [PATCH] fix: custom tickers now placed in their natural asset-class tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detect yfinance quote_type (CURRENCY→forex, FUTURE→energy, INDEX→indices, ETF→etfs, EQUITY→equities) when adding a custom ticker and persist it in market_watchlist.asset_class - get_all_quotes() merges custom tickers into their proper group (e.g. EURUSD=X appears under Forex) instead of always under a separate "Custom" group - "Custom" tab only shows tickers whose type couldn't be detected - Add market_watchlist.asset_class migration; ensure backtest_lab_runs and market_watchlist are always created at init_db() time Co-Authored-By: Claude Sonnet 4.6 --- backend/routers/market_data.py | 20 +++++++++++++++++--- backend/services/data_fetcher.py | 25 ++++++++++++++----------- backend/services/database.py | 9 +++++---- 3 files changed, 36 insertions(+), 18 deletions(-) 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()