2025-12-29 03:06:49 +08:00
|
|
|
|
"""
|
|
|
|
|
|
Symbol/company name resolver for local-only mode.
|
|
|
|
|
|
|
|
|
|
|
|
Goal:
|
|
|
|
|
|
- When a symbol is not present in our seed list, try to resolve a human-readable name
|
|
|
|
|
|
from public data sources, then persist it into watchlist records.
|
|
|
|
|
|
|
|
|
|
|
|
Notes:
|
|
|
|
|
|
- For US stocks we use Finnhub (if configured) or yfinance.
|
|
|
|
|
|
- For Crypto/Forex/Futures we provide best-effort fallbacks.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
2026-04-09 14:30:51 +07:00
|
|
|
|
from typing import Optional
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
|
|
from app.data.market_symbols_seed import get_symbol_name as seed_get_symbol_name
|
2026-04-09 14:30:51 +07:00
|
|
|
|
from app.utils.logger import get_logger
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_symbol_for_market(market: str, symbol: str) -> str:
|
2026-04-09 14:30:51 +07:00
|
|
|
|
s = (symbol or "").strip().upper()
|
2025-12-29 03:06:49 +08:00
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_name_from_yfinance(symbol: str) -> Optional[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Best-effort company name via yfinance.
|
|
|
|
|
|
"""
|
2026-04-09 14:30:51 +07:00
|
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
|
def _try_one(sym: str) -> Optional[str]:
|
|
|
|
|
|
import yfinance as yf
|
2026-04-09 14:30:51 +07:00
|
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
|
t = yf.Ticker(sym)
|
|
|
|
|
|
info = getattr(t, "info", None)
|
|
|
|
|
|
if not isinstance(info, dict) or not info:
|
|
|
|
|
|
return None
|
2026-04-09 14:30:51 +07:00
|
|
|
|
name = (info.get("longName") or info.get("shortName") or "").strip()
|
2025-12-29 03:06:49 +08:00
|
|
|
|
return name if name else None
|
2026-04-09 14:30:51 +07:00
|
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
|
try:
|
|
|
|
|
|
# yfinance uses '-' for some tickers (e.g. BRK-B) while users may input 'BRK.B'
|
|
|
|
|
|
out = _try_one(symbol)
|
|
|
|
|
|
if out:
|
|
|
|
|
|
return out
|
2026-04-09 14:30:51 +07:00
|
|
|
|
if "." in symbol:
|
|
|
|
|
|
out = _try_one(symbol.replace(".", "-"))
|
2025-12-29 03:06:49 +08:00
|
|
|
|
if out:
|
|
|
|
|
|
return out
|
|
|
|
|
|
return None
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.debug(f"yfinance name resolve failed: {symbol}: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_name_from_finnhub(symbol: str) -> Optional[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Finnhub company profile (requires FINNHUB_API_KEY).
|
|
|
|
|
|
https://finnhub.io/docs/api/company-profile2
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2026-04-09 14:30:51 +07:00
|
|
|
|
api_key = (os.getenv("FINNHUB_API_KEY") or "").strip()
|
2025-12-29 03:06:49 +08:00
|
|
|
|
if not api_key:
|
|
|
|
|
|
return None
|
|
|
|
|
|
url = "https://finnhub.io/api/v1/stock/profile2"
|
|
|
|
|
|
resp = requests.get(url, params={"symbol": symbol, "token": api_key}, timeout=8)
|
|
|
|
|
|
if resp.status_code != 200:
|
|
|
|
|
|
return None
|
|
|
|
|
|
data = resp.json() if resp.text else {}
|
|
|
|
|
|
if not isinstance(data, dict) or not data:
|
|
|
|
|
|
return None
|
2026-04-09 14:30:51 +07:00
|
|
|
|
name = (data.get("name") or data.get("ticker") or "").strip()
|
2025-12-29 03:06:49 +08:00
|
|
|
|
return name if name else None
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.debug(f"Finnhub name resolve failed: {symbol}: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_symbol_name(market: str, symbol: str) -> Optional[str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Resolve a display name for a symbol.
|
|
|
|
|
|
Priority:
|
|
|
|
|
|
1) Seed mapping (fast, offline)
|
|
|
|
|
|
2) Market-specific public sources
|
|
|
|
|
|
3) Reasonable fallback (None)
|
|
|
|
|
|
"""
|
2026-04-09 14:30:51 +07:00
|
|
|
|
m = (market or "").strip()
|
2025-12-29 03:06:49 +08:00
|
|
|
|
s = _normalize_symbol_for_market(m, symbol)
|
|
|
|
|
|
if not m or not s:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 1) Seed
|
|
|
|
|
|
seed = seed_get_symbol_name(m, s)
|
|
|
|
|
|
if seed:
|
|
|
|
|
|
return seed
|
|
|
|
|
|
|
|
|
|
|
|
# 2) Market-specific
|
2026-04-09 14:30:51 +07:00
|
|
|
|
if m == "USStock":
|
2025-12-29 03:06:49 +08:00
|
|
|
|
# Prefer Finnhub if configured (more stable for company name),
|
|
|
|
|
|
# otherwise fall back to yfinance.
|
|
|
|
|
|
return _resolve_name_from_finnhub(s) or _resolve_name_from_yfinance(s)
|
|
|
|
|
|
|
|
|
|
|
|
# Crypto: at least return base ticker-like display (not a "company", but better than empty)
|
2026-04-09 14:30:51 +07:00
|
|
|
|
if m == "Crypto":
|
|
|
|
|
|
if "/" in s:
|
|
|
|
|
|
base = s.split("/")[0].strip()
|
2025-12-29 03:06:49 +08:00
|
|
|
|
return base if base else None
|
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
# Forex: keep as-is (e.g. EURUSD) – you can later replace with a nicer mapping if needed.
|
2026-04-09 14:30:51 +07:00
|
|
|
|
if m == "Forex":
|
2025-12-29 03:06:49 +08:00
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
# Futures: keep as-is
|
2026-04-09 14:30:51 +07:00
|
|
|
|
if m == "Futures":
|
2025-12-29 03:06:49 +08:00
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
return None
|