V2.2.1: Membership & Billing, USDT TRC20 payment, VIP free indicators, AI Trading Radar, simplified strategy creation, system settings simplification, bug fixes and UI improvements

This commit is contained in:
TIANHE
2026-02-27 01:57:04 +08:00
parent ae82cc0d4e
commit ffdd2ffbae
71 changed files with 4067 additions and 6073 deletions
@@ -6,8 +6,6 @@ Goal:
from public data sources, then persist it into watchlist records.
Notes:
- For A shares we prefer akshare when available (requested).
- For H shares we use Tencent quote API (no key required).
- For US stocks we use Finnhub (if configured) or yfinance.
- For Crypto/Forex/Futures we provide best-effort fallbacks.
"""
@@ -26,122 +24,13 @@ from app.data.market_symbols_seed import get_symbol_name as seed_get_symbol_name
logger = get_logger(__name__)
try:
import akshare as ak # type: ignore
HAS_AKSHARE = True
except Exception:
ak = None
HAS_AKSHARE = False
def _normalize_symbol_for_market(market: str, symbol: str) -> str:
m = (market or '').strip()
s = (symbol or '').strip().upper()
if not m or not s:
return s
if m == 'AShare' and s.isdigit():
return s.zfill(6)
if m == 'HShare' and s.isdigit():
return s.zfill(5)
return s
def _tencent_quote_code(market: str, symbol: str) -> Optional[str]:
"""
Convert symbol to Tencent quote code, e.g.
- AShare: sh600000 / sz000001 / bj430047
- HShare: hk00700
"""
m = (market or '').strip()
s = _normalize_symbol_for_market(m, symbol)
if not s:
return None
if m == 'AShare':
if s.startswith('6'):
return f"sh{s}"
if s.startswith('0') or s.startswith('3'):
return f"sz{s}"
if s.startswith('4') or s.startswith('8'):
return f"bj{s}"
return None
if m == 'HShare':
if s.isdigit():
return f"hk{s}"
# allow already prefixed
if s.startswith('HK') and s[2:].isdigit():
return f"hk{s[2:]}"
if s.startswith('HK') and len(s) > 2:
return f"hk{s[2:]}"
return f"hk{s}"
return None
def _resolve_name_from_tencent(market: str, symbol: str) -> Optional[str]:
"""
Tencent quote endpoint: http://qt.gtimg.cn/q=sz000858
Returns:
v_sz000858="51~五 粮 液~000858~..."; -> name is the 2nd field split by '~'
"""
code = _tencent_quote_code(market, symbol)
if not code:
return None
try:
url = f"http://qt.gtimg.cn/q={code}"
resp = requests.get(url, timeout=5)
# Tencent often responds in GBK for Chinese names
resp.encoding = 'gbk'
text = resp.text or ''
# Extract quoted payload
m = re.search(r'="([^"]*)"', text)
payload = m.group(1) if m else ''
if not payload:
return None
parts = payload.split('~')
if len(parts) < 2:
return None
name = (parts[1] or '').strip().replace(' ', '')
return name if name else None
except Exception as e:
logger.debug(f"Tencent name resolve failed: {market} {symbol}: {e}")
return None
def _resolve_name_from_akshare_ashare(symbol: str) -> Optional[str]:
"""
Resolve A-share name via akshare (no API key required).
"""
if not HAS_AKSHARE or ak is None:
return None
try:
# Prefer per-symbol endpoint (avoids fetching the whole market list).
if hasattr(ak, "stock_individual_info_em"):
df = ak.stock_individual_info_em(symbol=symbol)
if df is not None and not df.empty and 'item' in df.columns and 'value' in df.columns:
info = {str(r['item']).strip(): r['value'] for _, r in df.iterrows()}
name = str(info.get('股票简称') or info.get('证券简称') or '').strip()
return name if name else None
# Fallback: spot list (may be slow / large)
if hasattr(ak, "stock_zh_a_spot_em"):
df2 = ak.stock_zh_a_spot_em()
if df2 is not None and not df2.empty:
row = df2[df2['代码'] == symbol].iloc[0]
name = str(row.get('名称') or '').strip()
return name if name else None
except Exception as e:
logger.debug(f"akshare name resolve failed (AShare {symbol}): {e}")
return None
return None
def _resolve_name_from_yfinance(symbol: str) -> Optional[str]:
"""
Best-effort company name via yfinance.
@@ -211,13 +100,6 @@ def resolve_symbol_name(market: str, symbol: str) -> Optional[str]:
return seed
# 2) Market-specific
if m == 'AShare':
# Requested: use akshare for A shares, do not depend on Tencent by default.
return _resolve_name_from_akshare_ashare(s)
if m == 'HShare':
return _resolve_name_from_tencent(m, s)
if m == 'USStock':
# Prefer Finnhub if configured (more stable for company name),
# otherwise fall back to yfinance.
@@ -239,5 +121,3 @@ def resolve_symbol_name(market: str, symbol: str) -> Optional[str]:
return s
return None