feat: Multi-user system with PostgreSQL - WIP temporary save
This commit is contained in:
@@ -1,120 +1,106 @@
|
||||
"""
|
||||
Seed symbol list used by Market APIs in local-only mode.
|
||||
Market symbols seed data and lookup functions.
|
||||
|
||||
This file is derived from the legacy MySQL `qd_market_symbols` table (non-secret data).
|
||||
It provides a deterministic offline list for:
|
||||
- hot symbols per market
|
||||
- lightweight symbol search
|
||||
Data is stored in PostgreSQL table `qd_market_symbols` (initialized via migrations/init.sql).
|
||||
This module provides helper functions to query hot symbols, search, and get symbol names.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
SEED_MARKET_SYMBOLS: List[Dict] = [
|
||||
# AShare
|
||||
{'market': 'AShare', 'symbol': '000001', 'name': '平安银行', 'exchange': 'SZSE', 'currency': 'CNY', 'is_active': 1, 'is_hot': 1, 'sort_order': 100},
|
||||
{'market': 'AShare', 'symbol': '000002', 'name': '万科A', 'exchange': 'SZSE', 'currency': 'CNY', 'is_active': 1, 'is_hot': 1, 'sort_order': 99},
|
||||
{'market': 'AShare', 'symbol': '600000', 'name': '浦发银行', 'exchange': 'SSE', 'currency': 'CNY', 'is_active': 1, 'is_hot': 1, 'sort_order': 98},
|
||||
{'market': 'AShare', 'symbol': '600036', 'name': '招商银行', 'exchange': 'SSE', 'currency': 'CNY', 'is_active': 1, 'is_hot': 1, 'sort_order': 97},
|
||||
{'market': 'AShare', 'symbol': '600519', 'name': '贵州茅台', 'exchange': 'SSE', 'currency': 'CNY', 'is_active': 1, 'is_hot': 1, 'sort_order': 96},
|
||||
{'market': 'AShare', 'symbol': '000858', 'name': '五粮液', 'exchange': 'SZSE', 'currency': 'CNY', 'is_active': 1, 'is_hot': 1, 'sort_order': 95},
|
||||
{'market': 'AShare', 'symbol': '002415', 'name': '海康威视', 'exchange': 'SZSE', 'currency': 'CNY', 'is_active': 1, 'is_hot': 1, 'sort_order': 94},
|
||||
{'market': 'AShare', 'symbol': '300059', 'name': '东方财富', 'exchange': 'SZSE', 'currency': 'CNY', 'is_active': 1, 'is_hot': 1, 'sort_order': 93},
|
||||
{'market': 'AShare', 'symbol': '000725', 'name': '京东方A', 'exchange': 'SZSE', 'currency': 'CNY', 'is_active': 1, 'is_hot': 1, 'sort_order': 92},
|
||||
{'market': 'AShare', 'symbol': '002594', 'name': '比亚迪', 'exchange': 'SZSE', 'currency': 'CNY', 'is_active': 1, 'is_hot': 1, 'sort_order': 91},
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# USStock
|
||||
{'market': 'USStock', 'symbol': 'AAPL', 'name': 'Apple Inc.', 'exchange': 'NASDAQ', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 100},
|
||||
{'market': 'USStock', 'symbol': 'MSFT', 'name': 'Microsoft Corporation', 'exchange': 'NASDAQ', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 99},
|
||||
{'market': 'USStock', 'symbol': 'GOOGL', 'name': 'Alphabet Inc.', 'exchange': 'NASDAQ', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 98},
|
||||
{'market': 'USStock', 'symbol': 'AMZN', 'name': 'Amazon.com Inc.', 'exchange': 'NASDAQ', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 97},
|
||||
{'market': 'USStock', 'symbol': 'TSLA', 'name': 'Tesla, Inc.', 'exchange': 'NASDAQ', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 96},
|
||||
{'market': 'USStock', 'symbol': 'META', 'name': 'Meta Platforms Inc.', 'exchange': 'NASDAQ', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 95},
|
||||
{'market': 'USStock', 'symbol': 'NVDA', 'name': 'NVIDIA Corporation', 'exchange': 'NASDAQ', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 94},
|
||||
{'market': 'USStock', 'symbol': 'JPM', 'name': 'JPMorgan Chase & Co.', 'exchange': 'NYSE', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 93},
|
||||
{'market': 'USStock', 'symbol': 'V', 'name': 'Visa Inc.', 'exchange': 'NYSE', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 92},
|
||||
{'market': 'USStock', 'symbol': 'JNJ', 'name': 'Johnson & Johnson', 'exchange': 'NYSE', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 91},
|
||||
|
||||
# HShare
|
||||
{'market': 'HShare', 'symbol': '00700', 'name': 'Tencent Holdings', 'exchange': 'HKEX', 'currency': 'HKD', 'is_active': 1, 'is_hot': 1, 'sort_order': 100},
|
||||
{'market': 'HShare', 'symbol': '09988', 'name': 'Alibaba Group', 'exchange': 'HKEX', 'currency': 'HKD', 'is_active': 1, 'is_hot': 1, 'sort_order': 99},
|
||||
{'market': 'HShare', 'symbol': '03690', 'name': 'Meituan', 'exchange': 'HKEX', 'currency': 'HKD', 'is_active': 1, 'is_hot': 1, 'sort_order': 98},
|
||||
{'market': 'HShare', 'symbol': '01810', 'name': 'Xiaomi Corporation', 'exchange': 'HKEX', 'currency': 'HKD', 'is_active': 1, 'is_hot': 1, 'sort_order': 97},
|
||||
{'market': 'HShare', 'symbol': '02318', 'name': 'Ping An Insurance', 'exchange': 'HKEX', 'currency': 'HKD', 'is_active': 1, 'is_hot': 1, 'sort_order': 96},
|
||||
{'market': 'HShare', 'symbol': '01398', 'name': 'ICBC', 'exchange': 'HKEX', 'currency': 'HKD', 'is_active': 1, 'is_hot': 1, 'sort_order': 95},
|
||||
{'market': 'HShare', 'symbol': '00939', 'name': 'CCB', 'exchange': 'HKEX', 'currency': 'HKD', 'is_active': 1, 'is_hot': 1, 'sort_order': 94},
|
||||
{'market': 'HShare', 'symbol': '01299', 'name': 'AIA Group', 'exchange': 'HKEX', 'currency': 'HKD', 'is_active': 1, 'is_hot': 1, 'sort_order': 93},
|
||||
{'market': 'HShare', 'symbol': '02020', 'name': 'Anta Sports', 'exchange': 'HKEX', 'currency': 'HKD', 'is_active': 1, 'is_hot': 1, 'sort_order': 92},
|
||||
{'market': 'HShare', 'symbol': '01024', 'name': 'Kuaishou Technology', 'exchange': 'HKEX', 'currency': 'HKD', 'is_active': 1, 'is_hot': 1, 'sort_order': 91},
|
||||
|
||||
# Crypto
|
||||
{'market': 'Crypto', 'symbol': 'BTC/USDT', 'name': 'Bitcoin', 'exchange': 'Binance', 'currency': 'USDT', 'is_active': 1, 'is_hot': 1, 'sort_order': 100},
|
||||
{'market': 'Crypto', 'symbol': 'ETH/USDT', 'name': 'Ethereum', 'exchange': 'Binance', 'currency': 'USDT', 'is_active': 1, 'is_hot': 1, 'sort_order': 99},
|
||||
{'market': 'Crypto', 'symbol': 'BNB/USDT', 'name': 'BNB', 'exchange': 'Binance', 'currency': 'USDT', 'is_active': 1, 'is_hot': 1, 'sort_order': 98},
|
||||
{'market': 'Crypto', 'symbol': 'SOL/USDT', 'name': 'Solana', 'exchange': 'Binance', 'currency': 'USDT', 'is_active': 1, 'is_hot': 1, 'sort_order': 97},
|
||||
{'market': 'Crypto', 'symbol': 'XRP/USDT', 'name': 'Ripple', 'exchange': 'Binance', 'currency': 'USDT', 'is_active': 1, 'is_hot': 1, 'sort_order': 96},
|
||||
{'market': 'Crypto', 'symbol': 'ADA/USDT', 'name': 'Cardano', 'exchange': 'Binance', 'currency': 'USDT', 'is_active': 1, 'is_hot': 1, 'sort_order': 95},
|
||||
{'market': 'Crypto', 'symbol': 'DOGE/USDT', 'name': 'Dogecoin', 'exchange': 'Binance', 'currency': 'USDT', 'is_active': 1, 'is_hot': 1, 'sort_order': 94},
|
||||
{'market': 'Crypto', 'symbol': 'DOT/USDT', 'name': 'Polkadot', 'exchange': 'Binance', 'currency': 'USDT', 'is_active': 1, 'is_hot': 1, 'sort_order': 93},
|
||||
{'market': 'Crypto', 'symbol': 'MATIC/USDT', 'name': 'Polygon', 'exchange': 'Binance', 'currency': 'USDT', 'is_active': 1, 'is_hot': 1, 'sort_order': 92},
|
||||
{'market': 'Crypto', 'symbol': 'AVAX/USDT', 'name': 'Avalanche', 'exchange': 'Binance', 'currency': 'USDT', 'is_active': 1, 'is_hot': 1, 'sort_order': 91},
|
||||
|
||||
# Forex (names are kept as-is from legacy dump)
|
||||
{'market': 'Forex', 'symbol': 'XAUUSD', 'name': 'Euro/US Dollar', 'exchange': 'Forex', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 100},
|
||||
{'market': 'Forex', 'symbol': 'XAGUSD', 'name': 'British Pound/US Dollar', 'exchange': 'Forex', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 99},
|
||||
{'market': 'Forex', 'symbol': 'EURUSD', 'name': 'US Dollar/Japanese Yen', 'exchange': 'Forex', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 98},
|
||||
{'market': 'Forex', 'symbol': 'GBPUSD', 'name': 'Australian Dollar/US Dollar', 'exchange': 'Forex', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 97},
|
||||
{'market': 'Forex', 'symbol': 'USDJPY', 'name': 'US Dollar/Canadian Dollar', 'exchange': 'Forex', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 96},
|
||||
{'market': 'Forex', 'symbol': 'AUDUSD', 'name': 'US Dollar/Swiss Franc', 'exchange': 'Forex', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 95},
|
||||
{'market': 'Forex', 'symbol': 'USDCAD', 'name': 'New Zealand Dollar/US Dollar', 'exchange': 'Forex', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 94},
|
||||
{'market': 'Forex', 'symbol': 'NZDUSD', 'name': 'US Dollar/Offshore Chinese Yuan', 'exchange': 'Forex', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 93},
|
||||
{'market': 'Forex', 'symbol': 'USDCHF', 'name': 'Euro/British Pound', 'exchange': 'Forex', 'currency': 'EUR', 'is_active': 1, 'is_hot': 1, 'sort_order': 92},
|
||||
{'market': 'Forex', 'symbol': 'EURJPY', 'name': 'Euro/Japanese Yen', 'exchange': 'Forex', 'currency': 'EUR', 'is_active': 1, 'is_hot': 1, 'sort_order': 91},
|
||||
|
||||
# Futures
|
||||
{'market': 'Futures', 'symbol': 'CL', 'name': 'WTI Crude Oil', 'exchange': 'NYMEX', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 100},
|
||||
{'market': 'Futures', 'symbol': 'GC', 'name': 'Gold', 'exchange': 'COMEX', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 99},
|
||||
{'market': 'Futures', 'symbol': 'SI', 'name': 'Silver', 'exchange': 'COMEX', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 98},
|
||||
{'market': 'Futures', 'symbol': 'NG', 'name': 'Natural Gas', 'exchange': 'NYMEX', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 97},
|
||||
{'market': 'Futures', 'symbol': 'HG', 'name': 'Copper', 'exchange': 'COMEX', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 96},
|
||||
{'market': 'Futures', 'symbol': 'ZC', 'name': 'Corn', 'exchange': 'CBOT', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 95},
|
||||
{'market': 'Futures', 'symbol': 'ZS', 'name': 'Soybeans', 'exchange': 'CBOT', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 94},
|
||||
{'market': 'Futures', 'symbol': 'ZW', 'name': 'Wheat', 'exchange': 'CBOT', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 93},
|
||||
{'market': 'Futures', 'symbol': 'ES', 'name': 'S&P 500 E-mini', 'exchange': 'CME', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 92},
|
||||
{'market': 'Futures', 'symbol': 'NQ', 'name': 'NASDAQ 100 E-mini', 'exchange': 'CME', 'currency': 'USD', 'is_active': 1, 'is_hot': 1, 'sort_order': 91},
|
||||
]
|
||||
def _get_db_connection():
|
||||
"""Get database connection, returns None if not available."""
|
||||
try:
|
||||
from app.utils.db import get_db_connection
|
||||
return get_db_connection()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_hot_symbols(market: str, limit: int = 10) -> List[Dict]:
|
||||
"""
|
||||
Get hot symbols for a market.
|
||||
|
||||
Args:
|
||||
market: Market name (e.g., 'Crypto', 'USStock', 'AShare')
|
||||
limit: Maximum number of results
|
||||
|
||||
Returns:
|
||||
List of {market, symbol, name} dicts
|
||||
"""
|
||||
market = (market or '').strip()
|
||||
items = [x for x in SEED_MARKET_SYMBOLS if x.get('market') == market and int(x.get('is_active', 1)) == 1 and int(x.get('is_hot', 0)) == 1]
|
||||
items.sort(key=lambda x: int(x.get('sort_order', 0)), reverse=True)
|
||||
return [{'market': x['market'], 'symbol': x['symbol'], 'name': x.get('name') or ''} for x in items[: max(limit, 0)]]
|
||||
if not market:
|
||||
return []
|
||||
|
||||
try:
|
||||
with _get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT market, symbol, name FROM qd_market_symbols
|
||||
WHERE market = ? AND is_active = 1 AND is_hot = 1
|
||||
ORDER BY sort_order DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(market, max(limit, 0))
|
||||
)
|
||||
rows = cur.fetchall() or []
|
||||
cur.close()
|
||||
return [{'market': r['market'], 'symbol': r['symbol'], 'name': r.get('name') or ''} for r in rows]
|
||||
except Exception as e:
|
||||
logger.debug(f"get_hot_symbols from DB failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def search_symbols(market: str, keyword: str, limit: int = 20) -> List[Dict]:
|
||||
"""
|
||||
Search symbols by keyword.
|
||||
|
||||
Args:
|
||||
market: Market name
|
||||
keyword: Search keyword (matches symbol or name)
|
||||
limit: Maximum number of results
|
||||
|
||||
Returns:
|
||||
List of {market, symbol, name} dicts
|
||||
"""
|
||||
market = (market or '').strip()
|
||||
kw = (keyword or '').strip().upper()
|
||||
kw = (keyword or '').strip()
|
||||
if not market or not kw:
|
||||
return []
|
||||
|
||||
items = [x for x in SEED_MARKET_SYMBOLS if x.get('market') == market and int(x.get('is_active', 1)) == 1]
|
||||
out: List[Dict] = []
|
||||
|
||||
for x in sorted(items, key=lambda i: int(i.get('sort_order', 0)), reverse=True):
|
||||
sym = str(x.get('symbol') or '').upper()
|
||||
name = str(x.get('name') or '').upper()
|
||||
if kw in sym or kw in name:
|
||||
out.append({'market': x['market'], 'symbol': x['symbol'], 'name': x.get('name') or ''})
|
||||
if len(out) >= max(limit, 0):
|
||||
break
|
||||
|
||||
return out
|
||||
|
||||
# Use ILIKE for case-insensitive search in PostgreSQL
|
||||
pattern = f'%{kw}%'
|
||||
|
||||
try:
|
||||
with _get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT market, symbol, name FROM qd_market_symbols
|
||||
WHERE market = ? AND is_active = 1
|
||||
AND (UPPER(symbol) LIKE UPPER(?) OR UPPER(name) LIKE UPPER(?))
|
||||
ORDER BY sort_order DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(market, pattern, pattern, max(limit, 0))
|
||||
)
|
||||
rows = cur.fetchall() or []
|
||||
cur.close()
|
||||
return [{'market': r['market'], 'symbol': r['symbol'], 'name': r.get('name') or ''} for r in rows]
|
||||
except Exception as e:
|
||||
logger.debug(f"search_symbols from DB failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def _normalize_for_match(market: str, symbol: str) -> str:
|
||||
"""Normalize symbol for matching (padding digits for A-Share/H-Share)."""
|
||||
m = (market or '').strip()
|
||||
s = (symbol or '').strip().upper()
|
||||
if not m or not s:
|
||||
@@ -123,7 +109,7 @@ def _normalize_for_match(market: str, symbol: str) -> str:
|
||||
# A-Share codes are usually 6 digits
|
||||
if m == 'AShare' and s.isdigit() and len(s) < 6:
|
||||
s = s.zfill(6)
|
||||
# H-Share codes are often 5 digits in the dump
|
||||
# H-Share codes are often 5 digits
|
||||
if m == 'HShare' and s.isdigit() and len(s) < 5:
|
||||
s = s.zfill(5)
|
||||
return s
|
||||
@@ -131,8 +117,14 @@ def _normalize_for_match(market: str, symbol: str) -> str:
|
||||
|
||||
def get_symbol_name(market: str, symbol: str) -> Optional[str]:
|
||||
"""
|
||||
Best-effort exact lookup for display name.
|
||||
Returns None if not found.
|
||||
Get display name for a symbol.
|
||||
|
||||
Args:
|
||||
market: Market name
|
||||
symbol: Symbol (e.g., 'AAPL', 'BTC/USDT', '600519')
|
||||
|
||||
Returns:
|
||||
Symbol name or None if not found
|
||||
"""
|
||||
m = (market or '').strip()
|
||||
if not m:
|
||||
@@ -142,20 +134,65 @@ def get_symbol_name(market: str, symbol: str) -> Optional[str]:
|
||||
if not s:
|
||||
return None
|
||||
|
||||
# Crypto: allow user to pass BTC (try BTC/USDT) or full pair.
|
||||
# Crypto: allow user to pass BTC (try BTC/USDT) or full pair
|
||||
candidate_symbols = [s]
|
||||
if m == 'Crypto' and '/' not in s:
|
||||
candidate_symbols.append(f"{s}/USDT")
|
||||
|
||||
for item in SEED_MARKET_SYMBOLS:
|
||||
if item.get('market') != m:
|
||||
continue
|
||||
item_sym = _normalize_for_match(m, str(item.get('symbol') or ''))
|
||||
for cand in candidate_symbols:
|
||||
if item_sym == cand:
|
||||
name = item.get('name')
|
||||
return str(name) if name else None
|
||||
|
||||
try:
|
||||
with _get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
for cand in candidate_symbols:
|
||||
cur.execute(
|
||||
"SELECT name FROM qd_market_symbols WHERE market = ? AND UPPER(symbol) = ?",
|
||||
(m, cand.upper())
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row and row.get('name'):
|
||||
cur.close()
|
||||
return str(row['name'])
|
||||
cur.close()
|
||||
except Exception as e:
|
||||
logger.debug(f"get_symbol_name from DB failed: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_all_symbols(market: str = None) -> List[Dict]:
|
||||
"""
|
||||
Get all active symbols, optionally filtered by market.
|
||||
|
||||
Args:
|
||||
market: Optional market filter
|
||||
|
||||
Returns:
|
||||
List of symbol records
|
||||
"""
|
||||
try:
|
||||
with _get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
if market:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT market, symbol, name, exchange, currency, is_hot, sort_order
|
||||
FROM qd_market_symbols
|
||||
WHERE market = ? AND is_active = 1
|
||||
ORDER BY sort_order DESC
|
||||
""",
|
||||
(market.strip(),)
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT market, symbol, name, exchange, currency, is_hot, sort_order
|
||||
FROM qd_market_symbols
|
||||
WHERE is_active = 1
|
||||
ORDER BY market, sort_order DESC
|
||||
"""
|
||||
)
|
||||
rows = cur.fetchall() or []
|
||||
cur.close()
|
||||
return [dict(r) for r in rows]
|
||||
except Exception as e:
|
||||
logger.debug(f"get_all_symbols from DB failed: {e}")
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user