fix: custom tickers now placed in their natural asset-class tab
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,16 @@ def list_custom_tickers():
|
|||||||
return get_market_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}")
|
@router.post("/custom-tickers/{ticker}")
|
||||||
def add_custom_ticker(ticker: str):
|
def add_custom_ticker(ticker: str):
|
||||||
from services.data_fetcher import get_quote
|
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"):
|
if not q or not q.get("price"):
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
raise HTTPException(400, f"Ticker '{ticker}' not found on yfinance")
|
raise HTTPException(400, f"Ticker '{ticker}' not found on yfinance")
|
||||||
|
name = ticker
|
||||||
|
asset_class = "custom"
|
||||||
try:
|
try:
|
||||||
info = yf.Ticker(ticker).fast_info
|
info = yf.Ticker(ticker).fast_info
|
||||||
name = getattr(info, "long_name", None) or getattr(info, "short_name", None) or ticker
|
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:
|
except Exception:
|
||||||
name = ticker
|
pass
|
||||||
add_market_custom_ticker(ticker, name)
|
add_market_custom_ticker(ticker, name, asset_class)
|
||||||
return {"ticker": ticker, "name": name, "price": q["price"]}
|
return {"ticker": ticker, "name": name, "asset_class": asset_class, "price": q["price"]}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/custom-tickers/{ticker}")
|
@router.delete("/custom-tickers/{ticker}")
|
||||||
|
|||||||
@@ -137,20 +137,23 @@ def get_all_quotes() -> Dict[str, List[Dict[str, Any]]]:
|
|||||||
quotes.append(q)
|
quotes.append(q)
|
||||||
result[asset_class] = quotes
|
result[asset_class] = quotes
|
||||||
|
|
||||||
# User-added custom tickers
|
# User-added custom tickers — placed in their detected asset class group
|
||||||
try:
|
try:
|
||||||
from services.database import get_market_custom_tickers
|
from services.database import get_market_custom_tickers
|
||||||
custom_entries = get_market_custom_tickers()
|
custom_entries = get_market_custom_tickers()
|
||||||
if custom_entries:
|
for entry in (custom_entries or []):
|
||||||
custom_quotes = []
|
q = get_quote(entry["ticker"])
|
||||||
for entry in custom_entries:
|
if not q:
|
||||||
q = get_quote(entry["ticker"])
|
continue
|
||||||
if q:
|
q["name"] = entry["name"] or entry["ticker"]
|
||||||
q["name"] = entry["name"] or entry["ticker"]
|
grp = entry.get("asset_class") or "custom"
|
||||||
q["asset_class"] = "custom"
|
q["asset_class"] = grp
|
||||||
custom_quotes.append(q)
|
if grp not in result:
|
||||||
if custom_quotes:
|
result[grp] = []
|
||||||
result["custom"] = custom_quotes
|
# 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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ def init_db():
|
|||||||
name TEXT,
|
name TEXT,
|
||||||
added_at TEXT DEFAULT (datetime('now'))
|
added_at TEXT DEFAULT (datetime('now'))
|
||||||
)""",
|
)""",
|
||||||
|
"ALTER TABLE market_watchlist ADD COLUMN asset_class TEXT DEFAULT 'custom'",
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
c.execute(_sql)
|
c.execute(_sql)
|
||||||
@@ -2318,18 +2319,18 @@ def remove_watchlist_ticker(ticker: str) -> bool:
|
|||||||
def get_market_custom_tickers() -> List[Dict]:
|
def get_market_custom_tickers() -> List[Dict]:
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
rows = conn.execute(
|
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()
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return [dict(r) for r in rows]
|
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()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR IGNORE INTO market_watchlist (ticker, name) VALUES (?, ?)",
|
"INSERT OR IGNORE INTO market_watchlist (ticker, name, asset_class) VALUES (?, ?, ?)",
|
||||||
(ticker.upper(), name),
|
(ticker.upper(), name, asset_class),
|
||||||
)
|
)
|
||||||
inserted = conn.total_changes > 0
|
inserted = conn.total_changes > 0
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
Reference in New Issue
Block a user