Refactor code for improved readability and consistency
- Cleaned up whitespace and formatting in various files including http.py, language.py, logger.py, safe_exec.py, and SQL migration scripts. - Consolidated import statements and removed unnecessary blank lines. - Updated logging configuration for better clarity. - Enhanced the safe execution code with improved error handling and logging. - Removed commented-out code and unnecessary variables in backfill_zero_trades.py and other scripts. - Added a pyproject.toml for Ruff and Vulture configuration. - Introduced requirements-dev.txt for development dependencies. - Removed commented-out stock entries in init.sql for cleaner migration scripts.
This commit is contained in:
@@ -2,45 +2,55 @@
|
||||
Market API routes (local-only).
|
||||
Provides watchlist, market metadata, symbol search, and pricing helpers for the frontend.
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
import traceback
|
||||
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from app.services.kline import KlineService
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.cache import CacheManager
|
||||
from app.utils.db import get_db_connection
|
||||
from app.utils.config_loader import load_addon_config
|
||||
from app.utils.auth import login_required
|
||||
from flask import Blueprint, g, jsonify, request
|
||||
|
||||
from app.data.market_symbols_seed import (
|
||||
get_hot_symbols as seed_get_hot_symbols,
|
||||
search_symbols as seed_search_symbols,
|
||||
get_symbol_name as seed_get_symbol_name
|
||||
)
|
||||
from app.data.market_symbols_seed import (
|
||||
get_symbol_name as seed_get_symbol_name,
|
||||
)
|
||||
from app.data.market_symbols_seed import (
|
||||
search_symbols as seed_search_symbols,
|
||||
)
|
||||
from app.services.kline import KlineService
|
||||
from app.services.symbol_name import resolve_symbol_name
|
||||
from app.utils.auth import login_required
|
||||
from app.utils.cache import CacheManager
|
||||
from app.utils.config_loader import load_addon_config
|
||||
from app.utils.db import get_db_connection
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
market_bp = Blueprint('market', __name__)
|
||||
market_bp = Blueprint("market", __name__)
|
||||
kline_service = KlineService()
|
||||
cache = CacheManager()
|
||||
|
||||
# Thread pool for parallel price fetching
|
||||
executor = ThreadPoolExecutor(max_workers=10)
|
||||
|
||||
|
||||
def _now_ts() -> int:
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def _normalize_symbol(symbol: str) -> str:
|
||||
return (symbol or '').strip().upper()
|
||||
return (symbol or "").strip().upper()
|
||||
|
||||
|
||||
def _ensure_watchlist_table():
|
||||
# Table is created by db schema init; this is only a sanity hook.
|
||||
return True
|
||||
|
||||
@market_bp.route('/config', methods=['GET'])
|
||||
|
||||
@market_bp.route("/config", methods=["GET"])
|
||||
def get_public_config():
|
||||
"""
|
||||
Public config for frontend (local mode).
|
||||
@@ -48,60 +58,57 @@ def get_public_config():
|
||||
"""
|
||||
try:
|
||||
cfg = load_addon_config()
|
||||
models = (cfg.get('ai', {}) or {}).get('models')
|
||||
models = (cfg.get("ai", {}) or {}).get("models")
|
||||
if not isinstance(models, dict) or not models:
|
||||
# Fallback defaults (offline friendly)
|
||||
models = {
|
||||
# Keep some legacy defaults
|
||||
'openai/gpt-4o': 'GPT-4o',
|
||||
|
||||
"openai/gpt-4o": "GPT-4o",
|
||||
# Unified frontend model list (OpenRouter-style ids)
|
||||
'x-ai/grok-code-fast-1': 'xAI: Grok Code Fast 1',
|
||||
'x-ai/grok-4-fast': 'xAI: Grok 4 Fast',
|
||||
'x-ai/grok-4.1-fast': 'xAI: Grok 4.1 Fast',
|
||||
'google/gemini-2.5-flash': 'Google: Gemini 2.5 Flash',
|
||||
'google/gemini-2.0-flash-001': 'Google: Gemini 2.0 Flash',
|
||||
'google/gemini-3-pro-preview': 'Google: Gemini 3 Pro Preview',
|
||||
'google/gemini-2.5-flash-lite': 'Google: Gemini 2.5 Flash Lite',
|
||||
'google/gemini-2.5-pro': 'Google: Gemini 2.5 Pro',
|
||||
'openai/gpt-4o-mini': 'OpenAI: GPT-4o-mini',
|
||||
'openai/gpt-5-mini': 'OpenAI: GPT-5 Mini',
|
||||
'openai/gpt-4.1-mini': 'OpenAI: GPT-4.1 Mini',
|
||||
'deepseek/deepseek-v3.2': 'DeepSeek: DeepSeek V3.2',
|
||||
'minimax/minimax-m2': 'MiniMax: MiniMax M2',
|
||||
'anthropic/claude-sonnet-4': 'Anthropic: Claude Sonnet 4',
|
||||
'anthropic/claude-sonnet-4.5': 'Anthropic: Claude Sonnet 4.5',
|
||||
'anthropic/claude-opus-4.5': 'Anthropic: Claude Opus 4.5',
|
||||
'anthropic/claude-haiku-4.5': 'Anthropic: Claude Haiku 4.5',
|
||||
'z-ai/glm-4.6': 'Z.AI: GLM 4.6',
|
||||
"x-ai/grok-code-fast-1": "xAI: Grok Code Fast 1",
|
||||
"x-ai/grok-4-fast": "xAI: Grok 4 Fast",
|
||||
"x-ai/grok-4.1-fast": "xAI: Grok 4.1 Fast",
|
||||
"google/gemini-2.5-flash": "Google: Gemini 2.5 Flash",
|
||||
"google/gemini-2.0-flash-001": "Google: Gemini 2.0 Flash",
|
||||
"google/gemini-3-pro-preview": "Google: Gemini 3 Pro Preview",
|
||||
"google/gemini-2.5-flash-lite": "Google: Gemini 2.5 Flash Lite",
|
||||
"google/gemini-2.5-pro": "Google: Gemini 2.5 Pro",
|
||||
"openai/gpt-4o-mini": "OpenAI: GPT-4o-mini",
|
||||
"openai/gpt-5-mini": "OpenAI: GPT-5 Mini",
|
||||
"openai/gpt-4.1-mini": "OpenAI: GPT-4.1 Mini",
|
||||
"deepseek/deepseek-v3.2": "DeepSeek: DeepSeek V3.2",
|
||||
"minimax/minimax-m2": "MiniMax: MiniMax M2",
|
||||
"anthropic/claude-sonnet-4": "Anthropic: Claude Sonnet 4",
|
||||
"anthropic/claude-sonnet-4.5": "Anthropic: Claude Sonnet 4.5",
|
||||
"anthropic/claude-opus-4.5": "Anthropic: Claude Opus 4.5",
|
||||
"anthropic/claude-haiku-4.5": "Anthropic: Claude Haiku 4.5",
|
||||
"z-ai/glm-4.6": "Z.AI: GLM 4.6",
|
||||
}
|
||||
return jsonify({'code': 1, 'msg': 'success', 'data': {'models': models, 'qdt_cost': {}}})
|
||||
return jsonify({"code": 1, "msg": "success", "data": {"models": models, "qdt_cost": {}}})
|
||||
except Exception as e:
|
||||
logger.error(f"get_public_config failed: {str(e)}")
|
||||
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
|
||||
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
|
||||
|
||||
@market_bp.route('/types', methods=['GET'])
|
||||
|
||||
@market_bp.route("/types", methods=["GET"])
|
||||
def get_market_types():
|
||||
"""Return supported market types for the add-watchlist modal."""
|
||||
# Keep a stable UX order; add CN/HK stocks near US stocks.
|
||||
desired_order = ['USStock', 'CNStock', 'HKStock', 'Crypto', 'Forex', 'Futures']
|
||||
# Keep a stable UX order for the supported market set.
|
||||
desired_order = ["USStock", "Crypto", "Forex", "Futures"]
|
||||
order_rank = {v: i for i, v in enumerate(desired_order)}
|
||||
|
||||
def _normalize_item(x):
|
||||
# Expected: {value: 'USStock', i18nKey: '...'}
|
||||
if isinstance(x, dict):
|
||||
v = (x.get('value') or '').strip()
|
||||
v = (x.get("value") or "").strip()
|
||||
if not v:
|
||||
return None
|
||||
return {
|
||||
'value': v,
|
||||
'i18nKey': x.get('i18nKey') or f'dashboard.analysis.market.{v}'
|
||||
}
|
||||
return {"value": v, "i18nKey": x.get("i18nKey") or f"dashboard.analysis.market.{v}"}
|
||||
if isinstance(x, str):
|
||||
v = x.strip()
|
||||
if not v:
|
||||
return None
|
||||
return {'value': v, 'i18nKey': f'dashboard.analysis.market.{v}'}
|
||||
return {"value": v, "i18nKey": f"dashboard.analysis.market.{v}"}
|
||||
return None
|
||||
|
||||
def _sort_items(items):
|
||||
@@ -111,79 +118,79 @@ def get_market_types():
|
||||
norm = _normalize_item(it)
|
||||
if norm:
|
||||
out.append(norm)
|
||||
out.sort(key=lambda it: (order_rank.get(it['value'], 10_000)))
|
||||
out.sort(key=lambda it: order_rank.get(it["value"], 10_000))
|
||||
return out
|
||||
|
||||
cfg = load_addon_config()
|
||||
data = (cfg.get('market', {}) or {}).get('types')
|
||||
data = (cfg.get("market", {}) or {}).get("types")
|
||||
|
||||
# Normalize & force desired order (even if config overrides the list order).
|
||||
if isinstance(data, list) and data:
|
||||
data = _sort_items(data)
|
||||
else:
|
||||
data = _sort_items(desired_order)
|
||||
return jsonify({'code': 1, 'msg': 'success', 'data': data})
|
||||
return jsonify({"code": 1, "msg": "success", "data": data})
|
||||
|
||||
|
||||
@market_bp.route('/menuFooterConfig', methods=['GET'])
|
||||
@market_bp.route("/menuFooterConfig", methods=["GET"])
|
||||
def get_menu_footer_config():
|
||||
"""
|
||||
Compatibility stub for old PHP `getMenuFooterConfig`.
|
||||
Frontend can also hardcode this locally; this endpoint remains for completeness.
|
||||
"""
|
||||
data = {
|
||||
'contact': {
|
||||
'support_url': 'https://github.com/',
|
||||
'feature_request_url': 'https://github.com/',
|
||||
'email': 'support@example.com',
|
||||
'live_chat_url': 'https://github.com/'
|
||||
"contact": {
|
||||
"support_url": "https://github.com/",
|
||||
"feature_request_url": "https://github.com/",
|
||||
"email": "support@example.com",
|
||||
"live_chat_url": "https://github.com/",
|
||||
},
|
||||
'social_accounts': [
|
||||
{'name': 'GitHub', 'icon': 'github', 'url': 'https://github.com/'},
|
||||
{'name': 'X', 'icon': 'x', 'url': 'https://x.com/'}
|
||||
"social_accounts": [
|
||||
{"name": "GitHub", "icon": "github", "url": "https://github.com/"},
|
||||
{"name": "X", "icon": "x", "url": "https://x.com/"},
|
||||
],
|
||||
'legal': {
|
||||
'user_agreement': '',
|
||||
'privacy_policy': ''
|
||||
},
|
||||
'copyright': '© 2025-2026 QuantDinger'
|
||||
"legal": {"user_agreement": "", "privacy_policy": ""},
|
||||
"copyright": "© 2025-2026 QuantDinger",
|
||||
}
|
||||
return jsonify({'code': 1, 'msg': 'success', 'data': data})
|
||||
return jsonify({"code": 1, "msg": "success", "data": data})
|
||||
|
||||
@market_bp.route('/symbols/search', methods=['GET'])
|
||||
|
||||
@market_bp.route("/symbols/search", methods=["GET"])
|
||||
def search_symbols():
|
||||
"""
|
||||
Lightweight symbol search.
|
||||
In local-only mode we keep this simple; frontend allows manual input when no results.
|
||||
"""
|
||||
try:
|
||||
market = (request.args.get('market') or '').strip()
|
||||
keyword = (request.args.get('keyword') or '').strip().upper()
|
||||
limit = int(request.args.get('limit') or 20)
|
||||
market = (request.args.get("market") or "").strip()
|
||||
keyword = (request.args.get("keyword") or "").strip().upper()
|
||||
limit = int(request.args.get("limit") or 20)
|
||||
|
||||
if not market or not keyword:
|
||||
return jsonify({'code': 1, 'msg': 'success', 'data': []})
|
||||
return jsonify({"code": 1, "msg": "success", "data": []})
|
||||
|
||||
out = seed_search_symbols(market=market, keyword=keyword, limit=limit)
|
||||
return jsonify({'code': 1, 'msg': 'success', 'data': out})
|
||||
return jsonify({"code": 1, "msg": "success", "data": out})
|
||||
except Exception as e:
|
||||
logger.error(f"search_symbols failed: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return jsonify({'code': 0, 'msg': str(e), 'data': []}), 500
|
||||
return jsonify({"code": 0, "msg": str(e), "data": []}), 500
|
||||
|
||||
@market_bp.route('/symbols/hot', methods=['GET'])
|
||||
|
||||
@market_bp.route("/symbols/hot", methods=["GET"])
|
||||
def get_hot_symbols():
|
||||
"""Return a small curated hot list per market (local-only)."""
|
||||
try:
|
||||
market = (request.args.get('market') or '').strip()
|
||||
limit = int(request.args.get('limit') or 10)
|
||||
market = (request.args.get("market") or "").strip()
|
||||
limit = int(request.args.get("limit") or 10)
|
||||
hot = seed_get_hot_symbols(market=market, limit=limit)
|
||||
return jsonify({'code': 1, 'msg': 'success', 'data': hot})
|
||||
return jsonify({"code": 1, "msg": "success", "data": hot})
|
||||
except Exception as e:
|
||||
logger.error(f"get_hot_symbols failed: {str(e)}")
|
||||
return jsonify({'code': 0, 'msg': str(e), 'data': []}), 500
|
||||
return jsonify({"code": 0, "msg": str(e), "data": []}), 500
|
||||
|
||||
@market_bp.route('/watchlist/get', methods=['GET'])
|
||||
|
||||
@market_bp.route("/watchlist/get", methods=["GET"])
|
||||
@login_required
|
||||
def get_watchlist():
|
||||
"""Get watchlist for the current user."""
|
||||
@@ -193,8 +200,7 @@ def get_watchlist():
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute(
|
||||
"SELECT id, market, symbol, name FROM qd_watchlist WHERE user_id = ? ORDER BY id DESC",
|
||||
(user_id,)
|
||||
"SELECT id, market, symbol, name FROM qd_watchlist WHERE user_id = ? ORDER BY id DESC", (user_id,)
|
||||
)
|
||||
rows = cur.fetchall() or []
|
||||
|
||||
@@ -202,42 +208,43 @@ def get_watchlist():
|
||||
# This keeps UI consistent without requiring users to re-add items.
|
||||
for row in rows:
|
||||
try:
|
||||
market = row.get('market')
|
||||
symbol = row.get('symbol')
|
||||
current_name = (row.get('name') or '').strip()
|
||||
market = row.get("market")
|
||||
symbol = row.get("symbol")
|
||||
current_name = (row.get("name") or "").strip()
|
||||
if not market or not symbol:
|
||||
continue
|
||||
if current_name and current_name != symbol:
|
||||
continue
|
||||
resolved = resolve_symbol_name(market, symbol) or seed_get_symbol_name(market, symbol)
|
||||
if resolved and resolved != current_name:
|
||||
row['name'] = resolved
|
||||
row["name"] = resolved
|
||||
cur.execute(
|
||||
"UPDATE qd_watchlist SET name = ?, updated_at = NOW() WHERE user_id = ? AND market = ? AND symbol = ?",
|
||||
(resolved, user_id, market, symbol)
|
||||
(resolved, user_id, market, symbol),
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
db.commit()
|
||||
cur.close()
|
||||
return jsonify({'code': 1, 'msg': 'success', 'data': rows})
|
||||
return jsonify({"code": 1, "msg": "success", "data": rows})
|
||||
except Exception as e:
|
||||
logger.error(f"get_watchlist failed: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return jsonify({'code': 0, 'msg': str(e), 'data': []}), 500
|
||||
return jsonify({"code": 0, "msg": str(e), "data": []}), 500
|
||||
|
||||
@market_bp.route('/watchlist/add', methods=['POST'])
|
||||
|
||||
@market_bp.route("/watchlist/add", methods=["POST"])
|
||||
@login_required
|
||||
def add_watchlist():
|
||||
"""Add a symbol to watchlist for the current user."""
|
||||
try:
|
||||
user_id = g.user_id
|
||||
data = request.get_json() or {}
|
||||
market = (data.get('market') or '').strip()
|
||||
symbol = _normalize_symbol(data.get('symbol'))
|
||||
name_in = (data.get('name') or '').strip()
|
||||
market = (data.get("market") or "").strip()
|
||||
symbol = _normalize_symbol(data.get("symbol"))
|
||||
name_in = (data.get("name") or "").strip()
|
||||
if not market or not symbol:
|
||||
return jsonify({'code': 0, 'msg': 'Missing market or symbol', 'data': None}), 400
|
||||
return jsonify({"code": 0, "msg": "Missing market or symbol", "data": None}), 400
|
||||
|
||||
# Prefer frontend-provided name (search results), otherwise resolve via seed/public sources.
|
||||
resolved = resolve_symbol_name(market, symbol) or seed_get_symbol_name(market, symbol)
|
||||
@@ -248,47 +255,45 @@ def add_watchlist():
|
||||
# Insert or update (PostgreSQL UPSERT)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO qd_watchlist (user_id, market, symbol, name, created_at, updated_at)
|
||||
INSERT INTO qd_watchlist (user_id, market, symbol, name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, NOW(), NOW())
|
||||
ON CONFLICT(user_id, market, symbol) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
updated_at = NOW()
|
||||
""",
|
||||
(user_id, market, symbol, name)
|
||||
(user_id, market, symbol, name),
|
||||
)
|
||||
db.commit()
|
||||
cur.close()
|
||||
|
||||
return jsonify({'code': 1, 'msg': 'success', 'data': None})
|
||||
return jsonify({"code": 1, "msg": "success", "data": None})
|
||||
except Exception as e:
|
||||
logger.error(f"add_watchlist failed: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
|
||||
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
|
||||
|
||||
@market_bp.route('/watchlist/remove', methods=['POST'])
|
||||
|
||||
@market_bp.route("/watchlist/remove", methods=["POST"])
|
||||
@login_required
|
||||
def remove_watchlist():
|
||||
"""Remove a symbol from watchlist for the current user."""
|
||||
try:
|
||||
user_id = g.user_id
|
||||
data = request.get_json() or {}
|
||||
symbol = _normalize_symbol(data.get('symbol'))
|
||||
symbol = _normalize_symbol(data.get("symbol"))
|
||||
if not symbol:
|
||||
return jsonify({'code': 0, 'msg': 'Missing symbol', 'data': None}), 400
|
||||
return jsonify({"code": 0, "msg": "Missing symbol", "data": None}), 400
|
||||
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute(
|
||||
"DELETE FROM qd_watchlist WHERE user_id = ? AND symbol = ?",
|
||||
(user_id, symbol)
|
||||
)
|
||||
cur.execute("DELETE FROM qd_watchlist WHERE user_id = ? AND symbol = ?", (user_id, symbol))
|
||||
db.commit()
|
||||
cur.close()
|
||||
return jsonify({'code': 1, 'msg': 'success', 'data': None})
|
||||
return jsonify({"code": 1, "msg": "success", "data": None})
|
||||
except Exception as e:
|
||||
logger.error(f"remove_watchlist failed: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
|
||||
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
|
||||
|
||||
|
||||
def get_single_price(market: str, symbol: str) -> dict:
|
||||
@@ -297,62 +302,54 @@ def get_single_price(market: str, symbol: str) -> dict:
|
||||
# Use get_realtime_price to get the real-time price (30 seconds cached internally)
|
||||
# Compared with the original '1D' K-line logic, this can reflect changes in 24h markets such as Crypto in a more timely manner
|
||||
price_data = kline_service.get_realtime_price(market, symbol)
|
||||
|
||||
|
||||
return {
|
||||
'market': market,
|
||||
'symbol': symbol,
|
||||
'price': price_data.get('price', 0),
|
||||
'change': price_data.get('change', 0),
|
||||
'changePercent': price_data.get('changePercent', 0)
|
||||
"market": market,
|
||||
"symbol": symbol,
|
||||
"price": price_data.get("price", 0),
|
||||
"change": price_data.get("change", 0),
|
||||
"changePercent": price_data.get("changePercent", 0),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch price {market}:{symbol} - {str(e)}")
|
||||
return {
|
||||
'market': market,
|
||||
'symbol': symbol,
|
||||
'price': 0,
|
||||
'change': 0,
|
||||
'changePercent': 0
|
||||
}
|
||||
return {"market": market, "symbol": symbol, "price": 0, "change": 0, "changePercent": 0}
|
||||
|
||||
|
||||
@market_bp.route('/watchlist/prices', methods=['GET'])
|
||||
@market_bp.route("/watchlist/prices", methods=["GET"])
|
||||
def get_watchlist_prices():
|
||||
"""
|
||||
Get the prices of self-selected stocks in batches
|
||||
|
||||
|
||||
Params (Query String):
|
||||
watchlist: JSON string of list of {market, symbol} objects
|
||||
e.g. ?watchlist=[{"market":"USStock","symbol":"AAPL"}]
|
||||
"""
|
||||
try:
|
||||
watchlist_str = request.args.get('watchlist', '[]')
|
||||
watchlist_str = request.args.get("watchlist", "[]")
|
||||
try:
|
||||
watchlist = json.loads(watchlist_str)
|
||||
except Exception:
|
||||
watchlist = []
|
||||
|
||||
|
||||
if not watchlist or not isinstance(watchlist, list):
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'msg': 'Invalid watchlist format (expected JSON list in query param)',
|
||||
'data': []
|
||||
}), 400
|
||||
|
||||
return jsonify(
|
||||
{"code": 0, "msg": "Invalid watchlist format (expected JSON list in query param)", "data": []}
|
||||
), 400
|
||||
|
||||
# logger.info(f"Start getting {len(watchlist)} self-selected stock price data")
|
||||
|
||||
|
||||
results = []
|
||||
|
||||
|
||||
# Fetch prices in parallel using thread pool
|
||||
futures = {}
|
||||
for item in watchlist:
|
||||
market = item.get('market', '')
|
||||
symbol = item.get('symbol', '')
|
||||
|
||||
market = item.get("market", "")
|
||||
symbol = item.get("symbol", "")
|
||||
|
||||
if market and symbol:
|
||||
future = executor.submit(get_single_price, market, symbol)
|
||||
futures[future] = (market, symbol)
|
||||
|
||||
|
||||
# Collect results (with timeout protection)
|
||||
completed_futures = set()
|
||||
try:
|
||||
@@ -364,94 +361,70 @@ def get_watchlist_prices():
|
||||
except Exception as e:
|
||||
market, symbol = futures[future]
|
||||
logger.warning(f"Price fetch failed: {market}:{symbol} - {str(e)}")
|
||||
results.append({
|
||||
'market': market,
|
||||
'symbol': symbol,
|
||||
'price': 0,
|
||||
'change': 0,
|
||||
'changePercent': 0
|
||||
})
|
||||
results.append({"market": market, "symbol": symbol, "price": 0, "change": 0, "changePercent": 0})
|
||||
except TimeoutError:
|
||||
# Add default results for unfinished tasks on timeout
|
||||
for future, (market, symbol) in futures.items():
|
||||
if future not in completed_futures:
|
||||
logger.warning(f"Price fetch timed out: {market}:{symbol}")
|
||||
results.append({
|
||||
'market': market,
|
||||
'symbol': symbol,
|
||||
'price': 0,
|
||||
'change': 0,
|
||||
'changePercent': 0,
|
||||
'error': 'timeout'
|
||||
})
|
||||
|
||||
success_count = sum(1 for r in results if r.get('price', 0) > 0)
|
||||
results.append(
|
||||
{
|
||||
"market": market,
|
||||
"symbol": symbol,
|
||||
"price": 0,
|
||||
"change": 0,
|
||||
"changePercent": 0,
|
||||
"error": "timeout",
|
||||
}
|
||||
)
|
||||
|
||||
success_count = sum(1 for r in results if r.get("price", 0) > 0)
|
||||
logger.info(f"Watchlist prices: {success_count}/{len(results)} successful")
|
||||
|
||||
return jsonify({
|
||||
'code': 1,
|
||||
'msg': 'success',
|
||||
'data': results
|
||||
})
|
||||
|
||||
|
||||
return jsonify({"code": 1, "msg": "success", "data": results})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Batch watchlist price fetch failed: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'msg': f'Failed: {str(e)}',
|
||||
'data': []
|
||||
}), 500
|
||||
return jsonify({"code": 0, "msg": f"Failed: {str(e)}", "data": []}), 500
|
||||
|
||||
|
||||
@market_bp.route('/price', methods=['GET'])
|
||||
@market_bp.route("/price", methods=["GET"])
|
||||
def get_price():
|
||||
"""
|
||||
Get the price of a single target
|
||||
|
||||
|
||||
parameter:
|
||||
market: market type
|
||||
symbol: transaction target
|
||||
"""
|
||||
try:
|
||||
market = request.args.get('market', '')
|
||||
symbol = request.args.get('symbol', '')
|
||||
|
||||
market = request.args.get("market", "")
|
||||
symbol = request.args.get("symbol", "")
|
||||
|
||||
if not market or not symbol:
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'msg': 'Missing market or symbol parameter(s)',
|
||||
'data': None
|
||||
}), 400
|
||||
|
||||
return jsonify({"code": 0, "msg": "Missing market or symbol parameter(s)", "data": None}), 400
|
||||
|
||||
result = get_single_price(market, symbol)
|
||||
|
||||
return jsonify({
|
||||
'code': 1,
|
||||
'msg': 'success',
|
||||
'data': result
|
||||
})
|
||||
|
||||
|
||||
return jsonify({"code": 1, "msg": "success", "data": result})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch price: {str(e)}")
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'msg': f'Failed: {str(e)}',
|
||||
'data': None
|
||||
}), 500
|
||||
return jsonify({"code": 0, "msg": f"Failed: {str(e)}", "data": None}), 500
|
||||
|
||||
|
||||
@market_bp.route('/stock/name', methods=['POST'])
|
||||
@market_bp.route("/stock/name", methods=["POST"])
|
||||
def get_stock_name():
|
||||
"""
|
||||
Get stock name
|
||||
|
||||
|
||||
Request body:
|
||||
{
|
||||
"market": "USStock",
|
||||
"symbol": "AAPL"
|
||||
}
|
||||
|
||||
|
||||
response:
|
||||
{
|
||||
"code": 1,
|
||||
@@ -464,102 +437,82 @@ def get_stock_name():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'msg': 'Request body is required',
|
||||
'data': None
|
||||
}), 400
|
||||
|
||||
market = data.get('market', '')
|
||||
symbol = data.get('symbol', '')
|
||||
|
||||
return jsonify({"code": 0, "msg": "Request body is required", "data": None}), 400
|
||||
|
||||
market = data.get("market", "")
|
||||
symbol = data.get("symbol", "")
|
||||
|
||||
if not market or not symbol:
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'msg': 'Missing market or symbol parameter(s)',
|
||||
'data': None
|
||||
}), 400
|
||||
|
||||
return jsonify({"code": 0, "msg": "Missing market or symbol parameter(s)", "data": None}), 400
|
||||
|
||||
# Try to get from cache (1 day cache)
|
||||
cache_key = f"stock_name:{market}:{symbol}"
|
||||
cached_name = cache.get(cache_key)
|
||||
|
||||
|
||||
if cached_name:
|
||||
logger.debug(f"Stock name cache hit: {market}:{symbol}")
|
||||
return jsonify({
|
||||
'code': 1,
|
||||
'msg': 'success',
|
||||
'data': {'name': cached_name}
|
||||
})
|
||||
|
||||
return jsonify({"code": 1, "msg": "success", "data": {"name": cached_name}})
|
||||
|
||||
# Get stock names based on different markets
|
||||
stock_name = symbol # Default use code
|
||||
|
||||
|
||||
try:
|
||||
if market == 'USStock':
|
||||
if market == "USStock":
|
||||
# For stocks, try to get basic information
|
||||
import yfinance as yf
|
||||
|
||||
|
||||
yf_symbol = symbol
|
||||
ticker = yf.Ticker(yf_symbol)
|
||||
info = ticker.info
|
||||
|
||||
|
||||
# Try to get the name
|
||||
stock_name = info.get('longName') or info.get('shortName') or symbol
|
||||
|
||||
elif market == 'Crypto':
|
||||
stock_name = info.get("longName") or info.get("shortName") or symbol
|
||||
|
||||
elif market == "Crypto":
|
||||
# Cryptocurrency, using trading pair format
|
||||
if '/' in symbol:
|
||||
if "/" in symbol:
|
||||
stock_name = symbol
|
||||
else:
|
||||
stock_name = f"{symbol}/USDT"
|
||||
|
||||
elif market == 'Forex':
|
||||
|
||||
elif market == "Forex":
|
||||
# Forex
|
||||
forex_names = {
|
||||
'XAUUSD': '黄金',
|
||||
'XAGUSD': '白银',
|
||||
'EURUSD': '欧元/美元',
|
||||
'GBPUSD': '英镑/美元',
|
||||
'USDJPY': '美元/日元',
|
||||
'AUDUSD': '澳元/美元',
|
||||
'USDCAD': '美元/加元',
|
||||
'USDCHF': '美元/瑞郎',
|
||||
"XAUUSD": "黄金",
|
||||
"XAGUSD": "白银",
|
||||
"EURUSD": "欧元/美元",
|
||||
"GBPUSD": "英镑/美元",
|
||||
"USDJPY": "美元/日元",
|
||||
"AUDUSD": "澳元/美元",
|
||||
"USDCAD": "美元/加元",
|
||||
"USDCHF": "美元/瑞郎",
|
||||
}
|
||||
stock_name = forex_names.get(symbol, symbol)
|
||||
|
||||
elif market == 'Futures':
|
||||
|
||||
elif market == "Futures":
|
||||
# futures
|
||||
futures_names = {
|
||||
'GC': '黄金期货',
|
||||
'SI': '白银期货',
|
||||
'CL': '原油期货',
|
||||
'NG': '天然气期货',
|
||||
'ZC': '玉米期货',
|
||||
'ZW': '小麦期货',
|
||||
'BTCUSDT': 'BTC永续合约',
|
||||
'ETHUSDT': 'ETH永续合约',
|
||||
"GC": "黄金期货",
|
||||
"SI": "白银期货",
|
||||
"CL": "原油期货",
|
||||
"NG": "天然气期货",
|
||||
"ZC": "玉米期货",
|
||||
"ZW": "小麦期货",
|
||||
"BTCUSDT": "BTC永续合约",
|
||||
"ETHUSDT": "ETH永续合约",
|
||||
}
|
||||
stock_name = futures_names.get(symbol, symbol)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch stock name; falling back to symbol: {market}:{symbol} - {str(e)}")
|
||||
stock_name = symbol
|
||||
|
||||
|
||||
# Cache for 1 day
|
||||
cache.set(cache_key, stock_name, 86400)
|
||||
|
||||
return jsonify({
|
||||
'code': 1,
|
||||
'msg': 'success',
|
||||
'data': {'name': stock_name}
|
||||
})
|
||||
|
||||
|
||||
return jsonify({"code": 1, "msg": "success", "data": {"name": stock_name}})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch stock name: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'msg': f'Failed: {str(e)}',
|
||||
'data': None
|
||||
}), 500
|
||||
return jsonify({"code": 0, "msg": f"Failed: {str(e)}", "data": None}), 500
|
||||
|
||||
Reference in New Issue
Block a user