Refactor and translate comments and docstrings in utility modules to English for better clarity and maintainability. Update Gunicorn and application startup messages for consistency in language. Enhance documentation with English translations for better accessibility.
This commit is contained in:
@@ -188,7 +188,7 @@ def login():
|
||||
user_id=user_id,
|
||||
username=user.get('username', username),
|
||||
role=user.get('role', 'admin'),
|
||||
token_version=new_token_version # 包含新的 token_version
|
||||
token_version=new_token_version # Contains new token_version
|
||||
)
|
||||
|
||||
if not token:
|
||||
|
||||
@@ -88,15 +88,15 @@ def _normalize_lang(lang: str | None) -> str:
|
||||
@backtest_bp.route('/backtest/precision-info', methods=['GET'])
|
||||
def get_precision_info():
|
||||
"""
|
||||
获取回测精度信息(用于前端提示)
|
||||
Get backtest accuracy information (for front-end prompts)
|
||||
|
||||
Params (Query String):
|
||||
market: 市场类型
|
||||
startDate: 开始日期 (YYYY-MM-DD)
|
||||
endDate: 结束日期 (YYYY-MM-DD)
|
||||
market: market type
|
||||
startDate: start date (YYYY-MM-DD)
|
||||
endDate: end date (YYYY-MM-DD)
|
||||
|
||||
Returns:
|
||||
精度信息,包含推荐的执行时间框架和预估K线数量
|
||||
Accuracy information, including recommended execution time frame and estimated number of K-lines
|
||||
"""
|
||||
try:
|
||||
# Use request.args for GET params
|
||||
@@ -164,7 +164,7 @@ def run_backtest():
|
||||
leverage = int(data.get('leverage', 1))
|
||||
trade_direction = data.get('tradeDirection', 'long') # long, short, both
|
||||
strategy_config = data.get('strategyConfig') or {}
|
||||
# 多时间框架回测开关(默认开启,仅加密货币市场有效)
|
||||
# Multi-timeframe backtesting switch (enabled by default, only valid for cryptocurrency markets)
|
||||
enable_mtf = data.get('enableMtf', True)
|
||||
if isinstance(enable_mtf, str):
|
||||
enable_mtf = enable_mtf.lower() in ['true', '1', 'yes']
|
||||
@@ -185,7 +185,7 @@ def run_backtest():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 参数验证
|
||||
# Parameter validation
|
||||
if not all([indicator_code, symbol, market, timeframe, start_date_str, end_date_str]):
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
@@ -193,27 +193,27 @@ def run_backtest():
|
||||
'data': None
|
||||
}), 400
|
||||
|
||||
# 转换日期
|
||||
# 开始日期:当天的 00:00:00
|
||||
# conversion date
|
||||
# Start date: 00:00:00 today
|
||||
start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
|
||||
# 结束日期:当天的 23:59:59,确保包含整天的数据
|
||||
# End date: 23:59:59 of the current day, ensuring that the entire day's data is included
|
||||
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').replace(hour=23, minute=59, second=59)
|
||||
|
||||
# 验证时间范围限制
|
||||
# Validation time range limit
|
||||
days_diff = (end_date - start_date).days
|
||||
|
||||
# 根据周期设置不同的时间限制
|
||||
# Set different time limits based on cycles
|
||||
if timeframe == '1m':
|
||||
max_days = 30 # 1分钟K线最多1个月
|
||||
max_days = 30 # 1 minute K-line up to 1 month
|
||||
max_range_text = '1 month'
|
||||
elif timeframe == '5m':
|
||||
max_days = 180 # 5分钟K线最多6个月
|
||||
max_days = 180 # 5 minute K-line up to 6 months
|
||||
max_range_text = '6 months'
|
||||
elif timeframe in ['15m', '30m']:
|
||||
max_days = 365 # 15分钟和30分钟K线最多1年
|
||||
max_days = 365 # 15-minute and 30-minute K-line up to 1 year
|
||||
max_range_text = '1 year'
|
||||
else: # 1H, 4H, 1D, 1W
|
||||
max_days = 1095 # 1小时及以上最多3年
|
||||
max_days = 1095 # 1 hour and above up to 3 years
|
||||
max_range_text = '3 years'
|
||||
|
||||
if days_diff > max_days:
|
||||
@@ -224,8 +224,8 @@ def run_backtest():
|
||||
}), 400
|
||||
|
||||
|
||||
# 执行回测(支持多时间框架高精度回测)
|
||||
# 加密货币市场且启用MTF时,使用多时间框架回测
|
||||
# Execute backtesting (supports multi-time frame high-precision backtesting)
|
||||
# Cryptocurrency markets and using multi-timeframe backtesting when MTF is enabled
|
||||
if enable_mtf and market.lower() in ['crypto', 'cryptocurrency']:
|
||||
result = backtest_service.run_multi_timeframe(
|
||||
indicator_code=indicator_code,
|
||||
@@ -257,7 +257,7 @@ def run_backtest():
|
||||
trade_direction=trade_direction,
|
||||
strategy_config=strategy_config
|
||||
)
|
||||
# 添加标准回测的精度信息
|
||||
# Add accuracy information for standard backtests
|
||||
result['precision_info'] = {
|
||||
'enabled': False,
|
||||
'timeframe': timeframe,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
Billing APIs - 会员购买/套餐配置(Mock支付)
|
||||
Billing APIs - Membership purchase/package configuration (Mock payment)
|
||||
|
||||
当前版本先实现“快速商业闭环”的最小可用:
|
||||
- 从系统设置(.env)读取 3 档会员(包月/包年/永久)金额与赠送积分配置
|
||||
- 用户在前端购买后立即开通/发放积分(后续可替换为真实支付网关)
|
||||
The current version first implements the minimum availability of "fast commercial closed loop":
|
||||
- Read the three levels of membership (monthly/annual/permanent) amounts and bonus points configuration from the system settings (.env)
|
||||
- Users activate/issue points immediately after purchasing on the front end (can be replaced with a real payment gateway later)
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify, request, g
|
||||
@@ -59,7 +59,7 @@ def purchase_membership():
|
||||
|
||||
|
||||
# =========================
|
||||
# USDT Pay (方案B)
|
||||
# USDT Pay (Plan B)
|
||||
# =========================
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Community APIs - 指标社区接口
|
||||
Community APIs - indicator community interface
|
||||
|
||||
提供指标市场、购买、评论等功能的 REST API。
|
||||
REST API that provides indicator markets, buying, commenting, and more.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify, request, g
|
||||
@@ -16,20 +16,20 @@ community_bp = Blueprint("community", __name__)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 指标市场
|
||||
# indicator market
|
||||
# ==========================================
|
||||
|
||||
@community_bp.route("/indicators", methods=["GET"])
|
||||
@login_required
|
||||
def get_market_indicators():
|
||||
"""
|
||||
获取市场指标列表
|
||||
Get a list of market indicators
|
||||
|
||||
Query params:
|
||||
page: 页码 (default 1)
|
||||
page_size: 每页数量 (default 12)
|
||||
keyword: 搜索关键词
|
||||
pricing_type: 'free' / 'paid' / 空(全部)
|
||||
page: page number (default 1)
|
||||
page_size: Number of pages per page (default 12)
|
||||
keyword: search keyword
|
||||
pricing_type: 'free' / 'paid' / empty (all)
|
||||
sort_by: 'newest' / 'hot' / 'price_asc' / 'price_desc' / 'rating'
|
||||
"""
|
||||
try:
|
||||
@@ -39,7 +39,7 @@ def get_market_indicators():
|
||||
pricing_type = request.args.get('pricing_type', '').strip() or None
|
||||
sort_by = request.args.get('sort_by', 'newest').strip()
|
||||
|
||||
# 限制每页数量
|
||||
# Limit the number of pages per page
|
||||
page_size = min(max(page_size, 1), 50)
|
||||
|
||||
service = get_community_service()
|
||||
@@ -62,7 +62,7 @@ def get_market_indicators():
|
||||
@community_bp.route("/indicators/<int:indicator_id>", methods=["GET"])
|
||||
@login_required
|
||||
def get_indicator_detail(indicator_id: int):
|
||||
"""获取指标详情"""
|
||||
"""Get indicator details"""
|
||||
try:
|
||||
service = get_community_service()
|
||||
result = service.get_indicator_detail(indicator_id, user_id=g.user_id)
|
||||
@@ -78,20 +78,20 @@ def get_indicator_detail(indicator_id: int):
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 购买功能
|
||||
# Purchase function
|
||||
# ==========================================
|
||||
|
||||
@community_bp.route("/indicators/<int:indicator_id>/purchase", methods=["POST"])
|
||||
@login_required
|
||||
def purchase_indicator(indicator_id: int):
|
||||
"""
|
||||
购买指标
|
||||
buy indicator
|
||||
|
||||
会自动:
|
||||
1. 检查积分是否充足
|
||||
2. 扣除买家积分,增加卖家积分
|
||||
3. 创建购买记录
|
||||
4. 复制指标到买家账户
|
||||
will automatically:
|
||||
1. Check whether the points are sufficient
|
||||
2. Deduct buyer points and increase seller points
|
||||
3. Create purchase records
|
||||
4. Copy the indicator to the buyer’s account
|
||||
"""
|
||||
try:
|
||||
service = get_community_service()
|
||||
@@ -113,7 +113,7 @@ def purchase_indicator(indicator_id: int):
|
||||
@community_bp.route("/my-purchases", methods=["GET"])
|
||||
@login_required
|
||||
def get_my_purchases():
|
||||
"""获取我购买的指标列表"""
|
||||
"""Get a list of indicators I purchased"""
|
||||
try:
|
||||
page = int(request.args.get('page', 1))
|
||||
page_size = int(request.args.get('page_size', 20))
|
||||
@@ -134,13 +134,13 @@ def get_my_purchases():
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 评论功能
|
||||
# Comment function
|
||||
# ==========================================
|
||||
|
||||
@community_bp.route("/indicators/<int:indicator_id>/comments", methods=["GET"])
|
||||
@login_required
|
||||
def get_comments(indicator_id: int):
|
||||
"""获取指标评论列表"""
|
||||
"""Get a list of indicator comments"""
|
||||
try:
|
||||
page = int(request.args.get('page', 1))
|
||||
page_size = int(request.args.get('page_size', 20))
|
||||
@@ -164,13 +164,13 @@ def get_comments(indicator_id: int):
|
||||
@login_required
|
||||
def add_comment(indicator_id: int):
|
||||
"""
|
||||
添加评论
|
||||
Add comment
|
||||
|
||||
Request body:
|
||||
rating: 1-5 星评分
|
||||
content: 评论内容(可选,最多500字)
|
||||
rating: 1-5 star rating
|
||||
content: Comment content (optional, up to 500 words)
|
||||
|
||||
注意:只有购买过的用户可以评论,且只能评论一次
|
||||
Note: Only users who have purchased can comment, and they can only comment once
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
@@ -199,11 +199,11 @@ def add_comment(indicator_id: int):
|
||||
@login_required
|
||||
def update_comment(indicator_id: int, comment_id: int):
|
||||
"""
|
||||
更新评论(只能修改自己的评论)
|
||||
Update comments (you can only modify your own comments)
|
||||
|
||||
Request body:
|
||||
rating: 1-5 星评分
|
||||
content: 评论内容(最多500字)
|
||||
rating: 1-5 star rating
|
||||
content: Comment content (up to 500 words)
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
@@ -232,7 +232,7 @@ def update_comment(indicator_id: int, comment_id: int):
|
||||
@community_bp.route("/indicators/<int:indicator_id>/my-comment", methods=["GET"])
|
||||
@login_required
|
||||
def get_my_comment(indicator_id: int):
|
||||
"""获取当前用户对指定指标的评论(用于编辑)"""
|
||||
"""Get the current user's comments on the specified indicator (for editing)"""
|
||||
try:
|
||||
service = get_community_service()
|
||||
result = service.get_user_comment(
|
||||
@@ -248,13 +248,13 @@ def get_my_comment(indicator_id: int):
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 实盘表现
|
||||
# Real offer performance
|
||||
# ==========================================
|
||||
|
||||
@community_bp.route("/indicators/<int:indicator_id>/performance", methods=["GET"])
|
||||
@login_required
|
||||
def get_indicator_performance(indicator_id: int):
|
||||
"""获取指标的实盘表现统计"""
|
||||
"""Get real performance statistics of indicators"""
|
||||
try:
|
||||
service = get_community_service()
|
||||
result = service.get_indicator_performance(indicator_id)
|
||||
@@ -267,11 +267,11 @@ def get_indicator_performance(indicator_id: int):
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 管理员审核功能
|
||||
# Administrator review function
|
||||
# ==========================================
|
||||
|
||||
def _is_admin():
|
||||
"""检查当前用户是否是管理员"""
|
||||
"""Check if the current user is an administrator"""
|
||||
role = getattr(g, 'user_role', None)
|
||||
return role == 'admin'
|
||||
|
||||
@@ -280,11 +280,11 @@ def _is_admin():
|
||||
@login_required
|
||||
def get_pending_indicators():
|
||||
"""
|
||||
获取待审核的指标列表(管理员专用)
|
||||
Get the list of indicators to be reviewed (for administrators only)
|
||||
|
||||
Query params:
|
||||
page: 页码 (default 1)
|
||||
page_size: 每页数量 (default 20)
|
||||
page: page number (default 1)
|
||||
page_size: Number of pages per page (default 20)
|
||||
review_status: 'pending' / 'approved' / 'rejected' / 'all'
|
||||
"""
|
||||
try:
|
||||
@@ -313,7 +313,7 @@ def get_pending_indicators():
|
||||
@community_bp.route("/admin/review-stats", methods=["GET"])
|
||||
@login_required
|
||||
def get_review_stats():
|
||||
"""获取审核统计数据(管理员专用)"""
|
||||
"""Get audit statistics (for administrators only)"""
|
||||
try:
|
||||
if not _is_admin():
|
||||
return jsonify({'code': 0, 'msg': 'admin_required', 'data': None}), 403
|
||||
@@ -332,11 +332,11 @@ def get_review_stats():
|
||||
@login_required
|
||||
def review_indicator(indicator_id: int):
|
||||
"""
|
||||
审核指标(管理员专用)
|
||||
Audit indicators (for administrators only)
|
||||
|
||||
Request body:
|
||||
action: 'approve' / 'reject'
|
||||
note: 审核备注(可选)
|
||||
note: review notes (optional)
|
||||
"""
|
||||
try:
|
||||
if not _is_admin():
|
||||
@@ -371,10 +371,10 @@ def review_indicator(indicator_id: int):
|
||||
@login_required
|
||||
def unpublish_indicator(indicator_id: int):
|
||||
"""
|
||||
下架指标(管理员专用)
|
||||
Delisting indicator (for administrators only)
|
||||
|
||||
Request body:
|
||||
note: 下架原因(可选)
|
||||
note: Reason for delisting (optional)
|
||||
"""
|
||||
try:
|
||||
if not _is_admin():
|
||||
@@ -403,7 +403,7 @@ def unpublish_indicator(indicator_id: int):
|
||||
@community_bp.route("/admin/indicators/<int:indicator_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
def admin_delete_indicator(indicator_id: int):
|
||||
"""删除指标(管理员专用)"""
|
||||
"""Delete indicator (only for administrators)"""
|
||||
try:
|
||||
if not _is_admin():
|
||||
return jsonify({'code': 0, 'msg': 'admin_required', 'data': None}), 403
|
||||
|
||||
@@ -105,7 +105,7 @@ def get_egress_ip():
|
||||
"data": {
|
||||
"ipv4": ipv4 or None,
|
||||
"ipv6": ipv6 or None,
|
||||
# 兼容旧前端:优先 IPv4,否则 IPv6
|
||||
# Compatible with old frontends: IPv4 first, otherwise IPv6
|
||||
"ip": ipv4 or ipv6 or None,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -38,22 +38,22 @@ logger = get_logger(__name__)
|
||||
global_market_bp = Blueprint("global_market", __name__)
|
||||
|
||||
# Cache for market data (simple in-memory cache)
|
||||
# 多用户场景下,合理的缓存可以大幅减少 API 请求
|
||||
# In multi-user scenarios, reasonable caching can significantly reduce API requests.
|
||||
_cache: Dict[str, Dict[str, Any]] = {}
|
||||
_cache_ttl = 60 # Default 60 seconds cache
|
||||
|
||||
# 缓存时间配置(秒)
|
||||
# Cache time configuration (seconds)
|
||||
CACHE_TTL = {
|
||||
"crypto_heatmap": 300, # 5分钟 - 加密货币变化快但热力图不需要实时
|
||||
"forex_pairs": 120, # 2分钟 - 外汇日内波动较小
|
||||
"stock_indices": 120, # 2分钟 - 指数变化较慢
|
||||
"market_overview": 120, # 2分钟 - 概览数据
|
||||
"market_heatmap": 120, # 2分钟 - 热力图
|
||||
"commodities": 120, # 2分钟 - 大宗商品
|
||||
"market_news": 180, # 3分钟 - 新闻
|
||||
"economic_calendar": 3600, # 1小时 - 日历事件
|
||||
"market_sentiment": 21600, # 6小时 - 宏观情绪变化缓慢
|
||||
"trading_opportunities": 3600, # 1小时 - 每小时更新一次
|
||||
"crypto_heatmap": 300, # 5 minutes - Cryptocurrencies change fast but heatmaps don’t need to be real-time
|
||||
"forex_pairs": 120, # 2 minutes - Forex intraday fluctuations are small
|
||||
"stock_indices": 120, # 2 minutes - index changes slowly
|
||||
"market_overview": 120, # 2 minutes - overview data
|
||||
"market_heatmap": 120, # 2 minutes - heat map
|
||||
"commodities": 120, # 2 minutes - Commodities
|
||||
"market_news": 180, # 3 minutes - News
|
||||
"economic_calendar": 3600, # 1 hour - calendar event
|
||||
"market_sentiment": 21600, # 6 hours - Macro sentiment changes slowly
|
||||
"trading_opportunities": 3600, # 1 hour - updated every hour
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ def _get_cached(key: str, ttl: int = None) -> Optional[Any]:
|
||||
"""Get cached data if not expired."""
|
||||
if key in _cache:
|
||||
entry = _cache[key]
|
||||
# 优先使用传入的 ttl,然后是 CACHE_TTL 配置,最后是默认值
|
||||
# Use the incoming ttl first, then the CACHE_TTL configuration, then the default value
|
||||
cache_ttl = ttl or CACHE_TTL.get(key, entry.get("ttl", _cache_ttl))
|
||||
if time.time() - entry.get("ts", 0) < cache_ttl:
|
||||
return entry.get("data")
|
||||
@@ -253,7 +253,7 @@ def _fetch_crypto_prices() -> List[Dict[str, Any]]:
|
||||
def _fetch_stock_indices() -> List[Dict[str, Any]]:
|
||||
"""Fetch major stock indices using yfinance."""
|
||||
indices = [
|
||||
# US Markets - 坐标错开避免重叠
|
||||
# US Markets - Coordinates are staggered to avoid overlap
|
||||
{"symbol": "^GSPC", "name_cn": "标普500", "name_en": "S&P 500", "region": "US", "flag": "🇺🇸", "lat": 40.7, "lng": -74.0},
|
||||
{"symbol": "^DJI", "name_cn": "道琼斯", "name_en": "Dow Jones", "region": "US", "flag": "🇺🇸", "lat": 38.5, "lng": -77.0},
|
||||
{"symbol": "^IXIC", "name_cn": "纳斯达克", "name_en": "NASDAQ", "region": "US", "flag": "🇺🇸", "lat": 37.5, "lng": -122.4},
|
||||
@@ -520,12 +520,12 @@ def _fetch_fear_greed_index() -> Dict[str, Any]:
|
||||
|
||||
def _fetch_vix() -> Dict[str, Any]:
|
||||
"""Fetch VIX (CBOE Volatility Index) with multiple fallbacks."""
|
||||
# 默认值 - 合理的市场中性水平
|
||||
# Default - a reasonable market neutral level
|
||||
DEFAULT_VIX = {"value": 18, "change": 0, "level": "low",
|
||||
"interpretation": "低波动 - 市场稳定",
|
||||
"interpretation_en": "Low - Market Stable"}
|
||||
|
||||
# 1) 尝试 yfinance
|
||||
# 1) Try yfinance
|
||||
try:
|
||||
import yfinance as yf
|
||||
logger.debug("Fetching VIX from yfinance")
|
||||
@@ -551,10 +551,10 @@ def _fetch_vix() -> Dict[str, Any]:
|
||||
except Exception as e:
|
||||
logger.warning(f"yfinance VIX failed, trying akshare: {e}")
|
||||
|
||||
# 2) 尝试 Akshare (对中国服务器友好)
|
||||
# 2) Try Akshare (friendly for Chinese servers)
|
||||
try:
|
||||
import akshare as ak
|
||||
vix_df = ak.index_vix() # VIX指数
|
||||
vix_df = ak.index_vix() # VIX index
|
||||
if vix_df is not None and len(vix_df) > 0:
|
||||
current = float(vix_df.iloc[-1]['close'])
|
||||
prev_close = float(vix_df.iloc[-2]['close']) if len(vix_df) >= 2 else current
|
||||
@@ -602,7 +602,7 @@ def _fetch_vix() -> Dict[str, Any]:
|
||||
|
||||
def _fetch_dollar_index() -> Dict[str, Any]:
|
||||
"""Fetch US Dollar Index (DXY) with multiple fallbacks."""
|
||||
# 默认值 - 合理的中性水平
|
||||
# Default - reasonably neutral level
|
||||
DEFAULT_DXY = {"value": 104, "change": 0, "level": "moderate_strong",
|
||||
"interpretation": "美元偏强 - 关注资金流向",
|
||||
"interpretation_en": "Moderately Strong - Watch capital flows"}
|
||||
@@ -610,7 +610,7 @@ def _fetch_dollar_index() -> Dict[str, Any]:
|
||||
current = 0
|
||||
change = 0
|
||||
|
||||
# 1) 尝试 yfinance
|
||||
# 1) Try yfinance
|
||||
try:
|
||||
import yfinance as yf
|
||||
logger.debug("Fetching DXY from yfinance")
|
||||
@@ -636,15 +636,15 @@ def _fetch_dollar_index() -> Dict[str, Any]:
|
||||
except Exception as e:
|
||||
logger.warning(f"yfinance DXY failed, trying akshare: {e}")
|
||||
|
||||
# 2) 尝试 Akshare 获取美元指数
|
||||
# 2) Try Akshare to get USD Index
|
||||
try:
|
||||
import akshare as ak
|
||||
# Akshare 外汇数据
|
||||
# Akshare Forex Data
|
||||
fx_df = ak.currency_boc_sina(symbol="美元")
|
||||
if fx_df is not None and len(fx_df) > 0:
|
||||
# 使用中行汇率估算 DXY (近似值)
|
||||
# Estimate DXY using Bank of China exchange rate (approximate value)
|
||||
usd_cny = float(fx_df.iloc[-1]['中行汇买价']) / 100
|
||||
current = usd_cny * 14.5 # 大致换算
|
||||
current = usd_cny * 14.5 # Approximate conversion
|
||||
change = 0
|
||||
logger.info(f"DXY estimated from akshare: {current:.2f}")
|
||||
else:
|
||||
@@ -698,14 +698,14 @@ def _fetch_yield_curve() -> Dict[str, Any]:
|
||||
# 10-year Treasury yield
|
||||
tnx = yf.Ticker("^TNX")
|
||||
|
||||
# 使用 try-except 包裹 history 调用
|
||||
# Use try-except to wrap history calls
|
||||
try:
|
||||
tnx_hist = tnx.history(period="5d")
|
||||
except Exception as hist_err:
|
||||
logger.warning(f"TNX history fetch failed: {hist_err}")
|
||||
tnx_hist = None
|
||||
|
||||
# 安全检查
|
||||
# security check
|
||||
if tnx_hist is None or tnx_hist.empty:
|
||||
logger.warning("TNX history is None or empty, returning default")
|
||||
return {
|
||||
@@ -1070,7 +1070,7 @@ def _fetch_financial_news(lang: str = "all") -> Dict[str, List[Dict[str, Any]]]:
|
||||
def _get_economic_calendar() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get economic calendar events with impact indicators.
|
||||
Impact: bullish (利多), bearish (利空), neutral (中性)
|
||||
Impact: bullish (positive), bearish (negative), neutral (neutral)
|
||||
"""
|
||||
today = datetime.now()
|
||||
events = []
|
||||
@@ -1084,7 +1084,7 @@ def _get_economic_calendar() -> List[Dict[str, Any]]:
|
||||
"importance": "high",
|
||||
"forecast": "180K",
|
||||
"previous": "175K",
|
||||
"impact_if_above": "bullish", # 高于预期利多美元
|
||||
"impact_if_above": "bullish", # Higher than expected bullish for dollar
|
||||
"impact_if_below": "bearish",
|
||||
"impact_desc": "高于预期利多美元/美股,低于预期利空",
|
||||
"impact_desc_en": "Above forecast: bullish USD/stocks; Below: bearish"
|
||||
@@ -1096,7 +1096,7 @@ def _get_economic_calendar() -> List[Dict[str, Any]]:
|
||||
"importance": "high",
|
||||
"forecast": "5.25%",
|
||||
"previous": "5.25%",
|
||||
"impact_if_above": "bearish", # 加息利空股市
|
||||
"impact_if_above": "bearish", # Raising interest rates is bad for the stock market
|
||||
"impact_if_below": "bullish",
|
||||
"impact_desc": "加息利空股市/加密货币,降息利多",
|
||||
"impact_desc_en": "Rate hike: bearish stocks/crypto; Cut: bullish"
|
||||
@@ -1108,7 +1108,7 @@ def _get_economic_calendar() -> List[Dict[str, Any]]:
|
||||
"importance": "high",
|
||||
"forecast": "0.3%",
|
||||
"previous": "0.4%",
|
||||
"impact_if_above": "bearish", # CPI高利空
|
||||
"impact_if_above": "bearish", # High CPI is negative
|
||||
"impact_if_below": "bullish",
|
||||
"impact_desc": "CPI高于预期增加加息预期,利空股市",
|
||||
"impact_desc_en": "Higher CPI increases rate hike expectations, bearish stocks"
|
||||
@@ -1132,7 +1132,7 @@ def _get_economic_calendar() -> List[Dict[str, Any]]:
|
||||
"importance": "high",
|
||||
"forecast": "0.10%",
|
||||
"previous": "0.10%",
|
||||
"impact_if_above": "bullish", # 日本加息利多日元
|
||||
"impact_if_above": "bullish", # Japan's interest rate hikes are bullish for the yen
|
||||
"impact_if_below": "bearish",
|
||||
"impact_desc": "加息预期利多日元,利空日股",
|
||||
"impact_desc_en": "Rate hike expectation: bullish JPY, bearish Nikkei"
|
||||
@@ -1315,11 +1315,11 @@ def _generate_heatmap_data() -> Dict[str, Any]:
|
||||
"crypto": [],
|
||||
"sectors": [],
|
||||
"forex": [],
|
||||
"commodities": [], # 新增大宗商品热力图
|
||||
"commodities": [], # Added commodity heat map
|
||||
"indices": []
|
||||
}
|
||||
|
||||
# Commodities heatmap (黄金、白银、原油等)
|
||||
# Commodities heatmap (gold, silver, crude oil, etc.)
|
||||
commodities_data = _get_cached("commodities")
|
||||
if not commodities_data:
|
||||
commodities_data = _fetch_commodities()
|
||||
@@ -1563,7 +1563,7 @@ def market_sentiment():
|
||||
Includes: Fear & Greed, VIX, DXY, Yield Curve, VXN, GVZ, VIX Term Structure.
|
||||
"""
|
||||
try:
|
||||
# 缓存6小时 (21600秒),宏观数据变化缓慢,减少 API 调用
|
||||
# Cache for 6 hours (21600 seconds), macro data changes slowly, reducing API calls
|
||||
MACRO_CACHE_TTL = 21600 # 6 hours
|
||||
cached = _get_cached("market_sentiment", MACRO_CACHE_TTL)
|
||||
if cached:
|
||||
@@ -1863,7 +1863,7 @@ def _analyze_opportunities_forex(opportunities: list):
|
||||
|
||||
|
||||
def _analyze_opportunities_polymarket(opportunities: list):
|
||||
"""扫描预测市场机会"""
|
||||
"""Scan for prediction market opportunities"""
|
||||
try:
|
||||
from app.data_sources.polymarket import PolymarketDataSource
|
||||
from app.services.polymarket_analyzer import PolymarketAnalyzer
|
||||
@@ -1871,24 +1871,24 @@ def _analyze_opportunities_polymarket(opportunities: list):
|
||||
polymarket_source = PolymarketDataSource()
|
||||
analyzer = PolymarketAnalyzer()
|
||||
|
||||
# 获取热门市场
|
||||
# Get popular markets
|
||||
markets = polymarket_source.get_trending_markets(limit=20)
|
||||
|
||||
for market in markets:
|
||||
try:
|
||||
# AI分析
|
||||
# AI analysis
|
||||
analysis = analyzer.analyze_market(market['market_id'])
|
||||
|
||||
if analysis.get('error'):
|
||||
continue
|
||||
|
||||
# 只添加高分机会
|
||||
# Only add high score chances
|
||||
if analysis.get('opportunity_score', 0) > 75:
|
||||
opportunities.append({
|
||||
"symbol": market['question'][:50], # 简化显示
|
||||
"symbol": market['question'][:50], # Simplified display
|
||||
"name": market['question'],
|
||||
"price": market['current_probability'],
|
||||
"change_24h": 0, # 预测市场没有24h涨跌幅概念
|
||||
"change_24h": 0, # There is no concept of 24h rise and fall in the prediction market
|
||||
"signal": "prediction_opportunity",
|
||||
"strength": "strong" if analysis.get('opportunity_score', 0) > 85 else "medium",
|
||||
"reason": f"AI预测概率{analysis.get('ai_predicted_probability', 0):.1f}%,市场概率{market['current_probability']:.1f}%,差异{analysis.get('divergence', 0):.1f}%",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
健康检查路由
|
||||
Health check routing
|
||||
"""
|
||||
from flask import Blueprint, jsonify
|
||||
from datetime import datetime
|
||||
@@ -9,7 +9,7 @@ health_bp = Blueprint('health', __name__)
|
||||
|
||||
@health_bp.route('/', methods=['GET'])
|
||||
def index():
|
||||
"""API 首页"""
|
||||
"""API Home Page"""
|
||||
return jsonify({
|
||||
'name': 'QuantDinger Python API',
|
||||
'version': '2.0.0',
|
||||
@@ -20,7 +20,7 @@ def index():
|
||||
|
||||
@health_bp.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""健康检查"""
|
||||
"""health check"""
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
@@ -29,5 +29,5 @@ def health_check():
|
||||
|
||||
@health_bp.route('/api/health', methods=['GET'])
|
||||
def api_health_check():
|
||||
"""兼容路径:用于容器健康检查/反代探针等场景。"""
|
||||
"""Compatible path: used for container health check/anti-generation probe and other scenarios."""
|
||||
return health_check()
|
||||
|
||||
@@ -210,7 +210,7 @@ def save_indicator():
|
||||
|
||||
now = _now_ts() # For BIGINT fields (createtime, updatetime)
|
||||
|
||||
# 检查用户是否是管理员(管理员发布的指标自动通过审核)
|
||||
# Check whether the user is an administrator (indicators published by the administrator automatically pass the review)
|
||||
user_role = getattr(g, 'user_role', 'user')
|
||||
is_admin = user_role == 'admin'
|
||||
|
||||
@@ -222,7 +222,7 @@ def save_indicator():
|
||||
except Exception:
|
||||
pass
|
||||
if indicator_id and indicator_id > 0:
|
||||
# 检查是否从未发布改为发布,需要设置审核状态
|
||||
# Check whether the change from unpublished to published requires setting the review status
|
||||
if publish_to_community:
|
||||
cur.execute(
|
||||
"SELECT publish_to_community, review_status FROM qd_indicator_codes WHERE id = ? AND user_id = ?",
|
||||
@@ -230,8 +230,8 @@ def save_indicator():
|
||||
)
|
||||
existing = cur.fetchone()
|
||||
was_published = existing and existing.get('publish_to_community')
|
||||
# 如果之前未发布,现在发布,设置审核状态
|
||||
# 管理员发布的直接通过,普通用户需要待审核
|
||||
# If it has not been published before, publish it now and set the review status
|
||||
# Posted by the administrator, it passes directly, and ordinary users need to wait for review.
|
||||
new_review_status = 'approved' if is_admin else 'pending'
|
||||
if not was_published:
|
||||
cur.execute(
|
||||
@@ -248,7 +248,7 @@ def save_indicator():
|
||||
new_review_status, user_id if is_admin else None, now, indicator_id, user_id),
|
||||
)
|
||||
else:
|
||||
# 已发布过的更新,保持原审核状态
|
||||
# Updates that have been released will remain in their original review status.
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE qd_indicator_codes
|
||||
@@ -261,7 +261,7 @@ def save_indicator():
|
||||
(name, code, description, publish_to_community, pricing_type, price, preview_image, vip_free, now, indicator_id, user_id),
|
||||
)
|
||||
else:
|
||||
# 取消发布,清除审核状态
|
||||
# Unpublish and clear review status
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE qd_indicator_codes
|
||||
@@ -275,7 +275,7 @@ def save_indicator():
|
||||
(name, code, description, publish_to_community, pricing_type, price, preview_image, now, indicator_id, user_id),
|
||||
)
|
||||
else:
|
||||
# 新建指标 - 管理员发布的直接通过,普通用户需要待审核
|
||||
# New indicators - those released by administrators are passed directly, ordinary users need to wait for review
|
||||
review_status = None
|
||||
if publish_to_community:
|
||||
review_status = 'approved' if is_admin else 'pending'
|
||||
@@ -329,12 +329,12 @@ def delete_indicator():
|
||||
@login_required
|
||||
def get_indicator_params():
|
||||
"""
|
||||
获取指标的参数声明
|
||||
Get parameter declaration of indicator
|
||||
|
||||
用于前端在策略创建时显示可配置的参数表单。
|
||||
Used by the front end to display a configurable parameter form when creating a policy.
|
||||
|
||||
Query params:
|
||||
indicator_id: 指标ID
|
||||
indicator_id: indicator ID
|
||||
|
||||
Returns:
|
||||
params: [
|
||||
@@ -342,7 +342,7 @@ def get_indicator_params():
|
||||
"name": "ma_fast",
|
||||
"type": "int",
|
||||
"default": 5,
|
||||
"description": "短期均线周期"
|
||||
"description": "Short-term moving average cycle"
|
||||
},
|
||||
...
|
||||
]
|
||||
@@ -541,7 +541,7 @@ def ai_generate():
|
||||
if not prompt:
|
||||
# Keep SSE contract (match PHP behavior) so frontend doesn't look "stuck".
|
||||
def _err_stream():
|
||||
yield "data: " + json.dumps({"error": "提示词不能为空"}, ensure_ascii=False) + "\n\n"
|
||||
yield "data: " + json.dumps({"error": "The prompt word cannot be empty"}, ensure_ascii=False) + "\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
return Response(
|
||||
@@ -740,22 +740,22 @@ IMPORTANT: Output Python code directly, without explanations, without descriptio
|
||||
@login_required
|
||||
def call_indicator():
|
||||
"""
|
||||
调用另一个指标(供前端 Pyodide 环境使用)
|
||||
Call another indicator (for use by the front-end Pyodide environment)
|
||||
|
||||
POST /api/indicator/callIndicator
|
||||
Body: {
|
||||
"indicatorRef": int | str, # 指标ID或名称
|
||||
"klineData": List[Dict], # K线数据
|
||||
"params": Dict, # 传递给被调用指标的参数(可选)
|
||||
"currentIndicatorId": int # 当前指标ID(用于循环依赖检测,可选)
|
||||
"indicatorRef": int | str, # indicator ID or name
|
||||
"klineData": List[Dict], # K-line data
|
||||
"params": Dict, # Parameters passed to the called indicator (optional)
|
||||
"currentIndicatorId": int # Current indicator ID (used for circular dependency detection, optional)
|
||||
}
|
||||
|
||||
Returns:
|
||||
{
|
||||
"code": 1,
|
||||
"data": {
|
||||
"df": List[Dict], # 执行后的DataFrame(转换为JSON)
|
||||
"columns": List[str] # DataFrame的列名
|
||||
"df": List[Dict], # DataFrame after execution (converted to JSON)
|
||||
"columns": List[str] # DataFrame column name
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -780,32 +780,32 @@ def call_indicator():
|
||||
"data": None
|
||||
}), 400
|
||||
|
||||
# 获取用户ID
|
||||
# Get user ID
|
||||
user_id = g.user_id
|
||||
|
||||
# 创建 IndicatorCaller
|
||||
# Create IndicatorCaller
|
||||
indicator_caller = IndicatorCaller(user_id, current_indicator_id)
|
||||
|
||||
# 将前端传入的K线数据转换为DataFrame
|
||||
# Convert the K-line data passed in from the front end into a DataFrame
|
||||
df = pd.DataFrame(kline_data)
|
||||
|
||||
# 确保必要的列存在
|
||||
# Make sure necessary columns exist
|
||||
required_columns = ['open', 'high', 'low', 'close', 'volume']
|
||||
for col in required_columns:
|
||||
if col not in df.columns:
|
||||
df[col] = 0.0
|
||||
|
||||
# 转换数据类型
|
||||
# Convert data type
|
||||
df['open'] = df['open'].astype('float64')
|
||||
df['high'] = df['high'].astype('float64')
|
||||
df['low'] = df['low'].astype('float64')
|
||||
df['close'] = df['close'].astype('float64')
|
||||
df['volume'] = df['volume'].astype('float64')
|
||||
|
||||
# 调用指标
|
||||
# call indicator
|
||||
result_df = indicator_caller.call_indicator(indicator_ref, df, params)
|
||||
|
||||
# 将DataFrame转换为JSON格式(前端可以使用的格式)
|
||||
# Convert the DataFrame to JSON format (a format that the front end can use)
|
||||
result_dict = result_df.to_dict(orient='records')
|
||||
|
||||
return jsonify({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
K线数据 API 路由
|
||||
K-line data API routing
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify
|
||||
from datetime import datetime
|
||||
@@ -17,17 +17,17 @@ kline_service = KlineService()
|
||||
@kline_bp.route('/kline', methods=['GET'])
|
||||
def get_kline():
|
||||
"""
|
||||
获取K线数据
|
||||
Get K-line data
|
||||
|
||||
参数:
|
||||
market: 市场类型 (Crypto, USStock, Forex, Futures)
|
||||
symbol: 交易对/股票代码
|
||||
timeframe: 时间周期 (1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W)
|
||||
limit: 数据条数 (默认300)
|
||||
before_time: 获取此时间之前的数据 (可选,Unix时间戳)
|
||||
parameter:
|
||||
market: market type (Crypto, USStock, Forex, Futures)
|
||||
symbol: trading pair/stock code
|
||||
timeframe: time period (1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W)
|
||||
limit: number of data items (default 300)
|
||||
before_time: Get data before this time (optional, Unix timestamp)
|
||||
"""
|
||||
try:
|
||||
# 强制 GET, 使用 request.args
|
||||
# To force GET, use request.args
|
||||
market = request.args.get('market', 'USStock')
|
||||
symbol = request.args.get('symbol', '')
|
||||
timeframe = request.args.get('timeframe', '1D')
|
||||
@@ -55,7 +55,7 @@ def get_kline():
|
||||
)
|
||||
|
||||
if not klines:
|
||||
# 针对特定情况给出更详细的提示
|
||||
# Give more detailed tips for specific situations
|
||||
msg = 'No data found'
|
||||
if market == 'Forex' and timeframe == '1m':
|
||||
msg = 'Forex 1-minute data requires Tiingo paid subscription'
|
||||
@@ -86,7 +86,7 @@ def get_kline():
|
||||
|
||||
@kline_bp.route('/price', methods=['GET'])
|
||||
def get_price():
|
||||
"""获取最新价格"""
|
||||
"""Get the latest price"""
|
||||
try:
|
||||
market = request.args.get('market', 'USStock')
|
||||
symbol = request.args.get('symbol', '')
|
||||
|
||||
@@ -291,10 +291,10 @@ def remove_watchlist():
|
||||
|
||||
|
||||
def get_single_price(market: str, symbol: str) -> dict:
|
||||
"""获取单个标的的价格数据"""
|
||||
"""Get price data of a single target"""
|
||||
try:
|
||||
# 使用 get_realtime_price 获取实时价格(内部已有30秒缓存)
|
||||
# 相比原先的 '1D' K线逻辑,这能更及时地反映 Crypto 等 24h 市场的变化
|
||||
# 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 {
|
||||
@@ -318,7 +318,7 @@ def get_single_price(market: str, symbol: str) -> dict:
|
||||
@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
|
||||
@@ -338,11 +338,11 @@ def get_watchlist_prices():
|
||||
'data': []
|
||||
}), 400
|
||||
|
||||
# logger.info(f"开始获取 {len(watchlist)} 个自选股价格数据")
|
||||
# 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', '')
|
||||
@@ -352,7 +352,7 @@ def get_watchlist_prices():
|
||||
future = executor.submit(get_single_price, market, symbol)
|
||||
futures[future] = (market, symbol)
|
||||
|
||||
# 收集结果(带超时保护)
|
||||
# Collect results (with timeout protection)
|
||||
completed_futures = set()
|
||||
try:
|
||||
for future in as_completed(futures, timeout=30):
|
||||
@@ -371,7 +371,7 @@ def get_watchlist_prices():
|
||||
'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}")
|
||||
@@ -406,11 +406,11 @@ def get_watchlist_prices():
|
||||
@market_bp.route('/price', methods=['GET'])
|
||||
def get_price():
|
||||
"""
|
||||
获取单个标的价格
|
||||
Get the price of a single target
|
||||
|
||||
参数:
|
||||
market: 市场类型
|
||||
symbol: 交易标的
|
||||
parameter:
|
||||
market: market type
|
||||
symbol: transaction target
|
||||
"""
|
||||
try:
|
||||
market = request.args.get('market', '')
|
||||
@@ -443,15 +443,15 @@ def get_price():
|
||||
@market_bp.route('/stock/name', methods=['POST'])
|
||||
def get_stock_name():
|
||||
"""
|
||||
获取股票名称
|
||||
Get stock name
|
||||
|
||||
请求体:
|
||||
Request body:
|
||||
{
|
||||
"market": "USStock",
|
||||
"symbol": "AAPL"
|
||||
}
|
||||
|
||||
响应:
|
||||
response:
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "success",
|
||||
@@ -479,7 +479,7 @@ def get_stock_name():
|
||||
'data': None
|
||||
}), 400
|
||||
|
||||
# 尝试从缓存获取(1天缓存)
|
||||
# Try to get from cache (1 day cache)
|
||||
cache_key = f"stock_name:{market}:{symbol}"
|
||||
cached_name = cache.get(cache_key)
|
||||
|
||||
@@ -491,30 +491,30 @@ def get_stock_name():
|
||||
'data': {'name': cached_name}
|
||||
})
|
||||
|
||||
# 根据不同市场获取股票名称
|
||||
stock_name = symbol # 默认使用代码
|
||||
# Get stock names based on different markets
|
||||
stock_name = symbol # Default use code
|
||||
|
||||
try:
|
||||
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':
|
||||
# 加密货币,使用交易对格式
|
||||
# Cryptocurrency, using trading pair format
|
||||
if '/' in symbol:
|
||||
stock_name = symbol
|
||||
else:
|
||||
stock_name = f"{symbol}/USDT"
|
||||
|
||||
elif market == 'Forex':
|
||||
# 外汇
|
||||
# Forex
|
||||
forex_names = {
|
||||
'XAUUSD': '黄金',
|
||||
'XAGUSD': '白银',
|
||||
@@ -528,7 +528,7 @@ def get_stock_name():
|
||||
stock_name = forex_names.get(symbol, symbol)
|
||||
|
||||
elif market == 'Futures':
|
||||
# 期货
|
||||
# futures
|
||||
futures_names = {
|
||||
'GC': '黄金期货',
|
||||
'SI': '白银期货',
|
||||
@@ -545,7 +545,7 @@ def get_stock_name():
|
||||
logger.warning(f"Failed to fetch stock name; falling back to symbol: {market}:{symbol} - {str(e)}")
|
||||
stock_name = symbol
|
||||
|
||||
# 缓存1天
|
||||
# Cache for 1 day
|
||||
cache.set(cache_key, stock_name, 86400)
|
||||
|
||||
return jsonify({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Polymarket预测市场API路由
|
||||
提供按需分析接口(只读,不涉及交易)
|
||||
Polymarket prediction market API routing
|
||||
Provide on-demand analysis interface (read-only, no transactions involved)
|
||||
"""
|
||||
from flask import Blueprint, jsonify, request, g
|
||||
|
||||
@@ -15,7 +15,7 @@ logger = get_logger(__name__)
|
||||
|
||||
polymarket_bp = Blueprint('polymarket', __name__)
|
||||
|
||||
# 初始化服务
|
||||
# Initialize service
|
||||
polymarket_source = PolymarketDataSource()
|
||||
|
||||
|
||||
@@ -23,20 +23,20 @@ polymarket_source = PolymarketDataSource()
|
||||
@login_required
|
||||
def analyze_polymarket():
|
||||
"""
|
||||
分析Polymarket预测市场(用户输入链接或标题)
|
||||
Analyze Polymarket prediction market (user enters link or title)
|
||||
|
||||
POST /api/polymarket/analyze
|
||||
Body: {
|
||||
"input": "https://polymarket.com/event/xxx" 或 "市场标题",
|
||||
"input": "https://polymarket.com/event/xxx" or "market title",
|
||||
"language": "zh-CN" (optional)
|
||||
}
|
||||
|
||||
流程:
|
||||
1. 从输入中解析market_id或slug
|
||||
2. 从API获取市场数据
|
||||
3. 检查计费并扣除积分
|
||||
4. 调用AI分析
|
||||
5. 返回分析结果
|
||||
process:
|
||||
1. Parse market_id or slug from input
|
||||
2. Get market data from API
|
||||
3. Check billing and deduct points
|
||||
4. Call AI analysis
|
||||
5. Return analysis results
|
||||
"""
|
||||
try:
|
||||
from app.services.billing_service import BillingService
|
||||
@@ -62,11 +62,11 @@ def analyze_polymarket():
|
||||
"data": None
|
||||
}), 400
|
||||
|
||||
# 1. 解析market_id或slug
|
||||
# 1. Parse market_id or slug
|
||||
market_id = None
|
||||
slug = None
|
||||
|
||||
# 尝试从URL中提取
|
||||
# Try to extract from URL
|
||||
url_patterns = [
|
||||
r'polymarket\.com/event/([^/?]+)',
|
||||
r'polymarket\.com/markets/(\d+)',
|
||||
@@ -77,20 +77,20 @@ def analyze_polymarket():
|
||||
match = re.search(pattern, input_text)
|
||||
if match:
|
||||
extracted = match.group(1)
|
||||
# 如果是数字,是market_id;否则是slug
|
||||
# If it is a number, it is market_id; otherwise it is slug
|
||||
if extracted.isdigit():
|
||||
market_id = extracted
|
||||
else:
|
||||
slug = extracted
|
||||
break
|
||||
|
||||
# 如果没有从URL提取到,尝试搜索市场
|
||||
# If not extracted from the URL, try searching the market
|
||||
if not market_id and not slug:
|
||||
# 尝试通过标题搜索
|
||||
# Try searching by title
|
||||
logger.info(f"Searching for market by title: {input_text[:100]}")
|
||||
search_results = polymarket_source.search_markets(input_text, limit=5)
|
||||
if search_results:
|
||||
# 使用第一个搜索结果
|
||||
# Use first search result
|
||||
market_id = search_results[0].get('market_id')
|
||||
logger.info(f"Found market via search: {market_id}")
|
||||
|
||||
@@ -101,11 +101,11 @@ def analyze_polymarket():
|
||||
"data": None
|
||||
}), 400
|
||||
|
||||
# 2. 获取市场数据
|
||||
# 2. Obtain market data
|
||||
if market_id:
|
||||
market = polymarket_source.get_market_details(market_id)
|
||||
elif slug:
|
||||
# 通过slug查找市场(需要先搜索)
|
||||
# Find the market by slug (need to search first)
|
||||
search_results = polymarket_source.search_markets(slug, limit=10)
|
||||
market = None
|
||||
for result in search_results:
|
||||
@@ -115,7 +115,7 @@ def analyze_polymarket():
|
||||
break
|
||||
|
||||
if not market and search_results:
|
||||
# 使用第一个搜索结果
|
||||
# Use first search result
|
||||
market = search_results[0]
|
||||
market_id = market.get('market_id')
|
||||
|
||||
@@ -136,7 +136,7 @@ def analyze_polymarket():
|
||||
"data": None
|
||||
}), 400
|
||||
|
||||
# 3. 检查计费
|
||||
# 3. Check billing
|
||||
billing = BillingService()
|
||||
cost = 0
|
||||
|
||||
@@ -156,7 +156,7 @@ def analyze_polymarket():
|
||||
}
|
||||
}), 400
|
||||
|
||||
# 扣除积分(使用check_and_consume方法,它会自动从配置中获取成本)
|
||||
# Deduct points (use check_and_consume method, it will automatically get the cost from the configuration)
|
||||
success, error_msg = billing.check_and_consume(
|
||||
user_id=user_id,
|
||||
feature='polymarket_deep_analysis',
|
||||
@@ -164,7 +164,7 @@ def analyze_polymarket():
|
||||
)
|
||||
|
||||
if not success:
|
||||
# 检查是否是积分不足的错误
|
||||
# Check whether it is an error due to insufficient points
|
||||
if error_msg.startswith('insufficient_credits'):
|
||||
parts = error_msg.split(':')
|
||||
if len(parts) >= 3:
|
||||
@@ -185,9 +185,9 @@ def analyze_polymarket():
|
||||
"data": None
|
||||
}), 500
|
||||
|
||||
# 4. 执行AI分析(传递语言和模型参数)
|
||||
# 4. Perform AI analysis (pass language and model parameters)
|
||||
analyzer = PolymarketAnalyzer()
|
||||
model = request.get_json().get('model') # 可选:从请求中获取模型参数
|
||||
model = request.get_json().get('model') # Optional: Get model parameters from request
|
||||
analysis_result = analyzer.analyze_market(
|
||||
market_id,
|
||||
user_id=user_id,
|
||||
@@ -203,7 +203,7 @@ def analyze_polymarket():
|
||||
"data": None
|
||||
}), 500
|
||||
|
||||
# 5. 获取剩余积分
|
||||
# 5. Get remaining points
|
||||
remaining_credits = 0
|
||||
if billing.is_billing_enabled():
|
||||
remaining_credits = float(billing.get_user_credits(user_id))
|
||||
@@ -245,7 +245,7 @@ def get_polymarket_history():
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
|
||||
# 获取总数
|
||||
# Get total
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) AS total
|
||||
FROM qd_analysis_tasks
|
||||
@@ -254,7 +254,7 @@ def get_polymarket_history():
|
||||
total_row = cur.fetchone()
|
||||
total = total_row['total'] if total_row else 0
|
||||
|
||||
# 获取历史记录
|
||||
# Get history
|
||||
cur.execute("""
|
||||
SELECT
|
||||
t.id,
|
||||
@@ -273,7 +273,7 @@ def get_polymarket_history():
|
||||
rows = cur.fetchall() or []
|
||||
cur.close()
|
||||
|
||||
# 解析结果
|
||||
# Parse results
|
||||
items = []
|
||||
for row in rows:
|
||||
result_json = row.get('result_json', '{}')
|
||||
|
||||
@@ -42,8 +42,8 @@ def _now_ts() -> int:
|
||||
|
||||
def _serialize_monitor_ts(value):
|
||||
"""
|
||||
JSON 序列化监控时间字段。PostgreSQL TIMESTAMP 无 tz 时按 UTC 解释(与 Docker 默认一致),
|
||||
输出带 Z 的 ISO,避免前端把无时区字符串当本地时间而偏差 8 小时。
|
||||
JSON serialized monitoring time field. PostgreSQL TIMESTAMP is interpreted in UTC when there is no tz (consistent with Docker's default),
|
||||
Output the ISO with Z to prevent the front end from treating the non-time zone string as local time and deviating by 8 hours.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
@@ -85,17 +85,17 @@ def _get_single_price(market: str, symbol: str, force_refresh: bool = False) ->
|
||||
"""
|
||||
Get price data for a single symbol.
|
||||
|
||||
优先使用实时报价 API(ticker),降级使用分钟/日线 K 线数据。
|
||||
这样可以在交易时段获取更实时的价格,而不是只显示日线收盘价。
|
||||
Priority is given to using the real-time quotation API (ticker), and downgrading to minute/daily K-line data.
|
||||
This allows for more real-time prices during the trading session, rather than just showing daily closing prices.
|
||||
|
||||
内置速率限制:同一市场的请求间隔至少 REQUEST_INTERVAL 秒,
|
||||
避免触发 API 限制(如 yfinance、Tiingo、Finnhub 等)。
|
||||
Built-in rate limit: requests for the same market must be at least REQUEST_INTERVAL seconds apart,
|
||||
Avoid triggering API limits (like yfinance, Tiingo, Finnhub, etc.).
|
||||
|
||||
Args:
|
||||
force_refresh: 是否强制刷新(跳过缓存)
|
||||
force_refresh: whether to force refresh (skip cache)
|
||||
"""
|
||||
try:
|
||||
# 速率限制:同一市场的请求间隔
|
||||
# Rate Limit: Interval between requests for the same market
|
||||
with _request_lock:
|
||||
now = time.time()
|
||||
last_time = _last_request_time.get(market, 0)
|
||||
@@ -104,7 +104,7 @@ def _get_single_price(market: str, symbol: str, force_refresh: bool = False) ->
|
||||
time.sleep(wait_time)
|
||||
_last_request_time[market] = time.time()
|
||||
|
||||
# 使用新的 get_realtime_price 方法获取实时价格
|
||||
# Get real-time prices using the new get_realtime_price method
|
||||
price_data = kline_service.get_realtime_price(market, symbol, force_refresh=force_refresh)
|
||||
|
||||
return {
|
||||
@@ -113,7 +113,7 @@ def _get_single_price(market: str, symbol: str, force_refresh: bool = False) ->
|
||||
'price': price_data.get('price', 0),
|
||||
'change': price_data.get('change', 0),
|
||||
'changePercent': price_data.get('changePercent', 0),
|
||||
'source': price_data.get('source', 'unknown') # 记录数据来源,便于调试
|
||||
'source': price_data.get('source', 'unknown') # Record data sources for easy debugging
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch price {market}:{symbol} - {str(e)}")
|
||||
@@ -598,7 +598,7 @@ def add_monitor():
|
||||
db.commit()
|
||||
cur.close()
|
||||
|
||||
# 创建后立即在后台跑一轮:立刻发通知,并以完成时刻为基准写入 next_run_at(间隔后再次执行)
|
||||
# Run one round in the background immediately after creation: send a notification immediately, and write next_run_at based on the completion time (execute again after the interval)
|
||||
if is_active and monitor_id:
|
||||
try:
|
||||
from app.services.portfolio_monitor import run_single_monitor as _run_single_monitor
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Settings API - 读取和保存 .env 配置
|
||||
Settings API - Reading and saving .env configurations
|
||||
|
||||
Admin-only endpoints for system configuration management.
|
||||
"""
|
||||
@@ -16,7 +16,7 @@ logger = get_logger(__name__)
|
||||
|
||||
settings_bp = Blueprint('settings', __name__)
|
||||
|
||||
# .env 文件路径
|
||||
# .env file path
|
||||
ENV_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
|
||||
|
||||
|
||||
@@ -68,16 +68,16 @@ def _refresh_runtime_services() -> None:
|
||||
except Exception as e:
|
||||
logger.warning(f"Singleton reset skipped: {module_name}.{field_name}: {e}")
|
||||
|
||||
# 配置项定义(分组)- 按功能模块划分,每个配置项包含描述
|
||||
# Configuration item definition (grouping) - divided by functional modules, each configuration item contains a description
|
||||
# ---------------------------------------------------------------
|
||||
# 精简原则:
|
||||
# - 部署级配置(host/port/debug)不在 UI 暴露,用户通过 .env 或 docker-compose 设置
|
||||
# - 内部调优参数(超时/重试/tick间隔/向量维度等)使用默认值即可,不暴露给普通用户
|
||||
# - 只保留用户真正需要配置的功能开关和 API Key
|
||||
# Streamlining principle:
|
||||
# - Deployment-level configuration (host/port/debug) is not exposed in the UI, users can set it through .env or docker-compose
|
||||
# - Internal tuning parameters (timeout/retry/tick interval/vector dimension, etc.) can use default values and are not exposed to ordinary users.
|
||||
# - Only keep the function switches and API Keys that users really need to configure
|
||||
# ---------------------------------------------------------------
|
||||
CONFIG_SCHEMA = {
|
||||
|
||||
# ==================== 1. 安全认证 ====================
|
||||
# ==================== 1. Security certification ====================
|
||||
'auth': {
|
||||
'title': 'Security & Authentication',
|
||||
'icon': 'lock',
|
||||
@@ -114,7 +114,7 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 2. AI/LLM 配置 ====================
|
||||
# ==================== 2. AI/LLM configuration ====================
|
||||
'ai': {
|
||||
'title': 'AI / LLM & Search',
|
||||
'icon': 'robot',
|
||||
@@ -342,7 +342,7 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 3. 实盘交易 ====================
|
||||
# ==================== 3. Real offer ====================
|
||||
'trading': {
|
||||
'title': 'Live Trading',
|
||||
'icon': 'stock',
|
||||
@@ -366,7 +366,7 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 4. 数据源配置 ====================
|
||||
# ==================== 4. Data source configuration ====================
|
||||
'data_source': {
|
||||
'title': 'Data Sources',
|
||||
'icon': 'database',
|
||||
@@ -402,7 +402,7 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 5. 邮件配置 ====================
|
||||
# ==================== 5. Email configuration ====================
|
||||
'email': {
|
||||
'title': 'Email (SMTP)',
|
||||
'icon': 'mail',
|
||||
@@ -460,7 +460,7 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 6. 短信配置 ====================
|
||||
# ==================== 6. SMS configuration ====================
|
||||
'sms': {
|
||||
'title': 'SMS (Twilio)',
|
||||
'icon': 'phone',
|
||||
@@ -571,7 +571,7 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 8. 网络代理 ====================
|
||||
# ==================== 8. Network proxy ====================
|
||||
'network': {
|
||||
'title': 'Network & Proxy',
|
||||
'icon': 'global',
|
||||
@@ -587,7 +587,7 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 10. 注册与 OAuth ====================
|
||||
# ==================== 10. Registration and OAuth ====================
|
||||
'security': {
|
||||
'title': 'Registration & OAuth',
|
||||
'icon': 'safety',
|
||||
@@ -658,7 +658,7 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 11. 计费配置 ====================
|
||||
# ==================== 11. Billing configuration ====================
|
||||
'billing': {
|
||||
'title': 'Billing & Credits',
|
||||
'icon': 'dollar',
|
||||
@@ -716,7 +716,7 @@ CONFIG_SCHEMA = {
|
||||
'description': 'Credits granted every 30 days for lifetime members'
|
||||
},
|
||||
|
||||
# ===== USDT Pay (方案B:每单独立地址) =====
|
||||
# ===== USDT Pay (Plan B: independent address for each order) =====
|
||||
{
|
||||
'key': 'USDT_PAY_ENABLED',
|
||||
'label': 'Enable USDT Pay',
|
||||
@@ -809,7 +809,7 @@ CONFIG_SCHEMA = {
|
||||
|
||||
|
||||
def read_env_file():
|
||||
"""读取 .env 文件"""
|
||||
"""Read .env file"""
|
||||
env_values = {}
|
||||
|
||||
if not os.path.exists(ENV_FILE_PATH):
|
||||
@@ -820,15 +820,15 @@ def read_env_file():
|
||||
with open(ENV_FILE_PATH, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
# 跳过空行和注释
|
||||
# Skip empty lines and comments
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
# 解析 KEY=VALUE
|
||||
# Parse KEY=VALUE
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
# 移除引号
|
||||
# Remove quotes
|
||||
if (value.startswith('"') and value.endswith('"')) or \
|
||||
(value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
@@ -840,11 +840,11 @@ def read_env_file():
|
||||
|
||||
|
||||
def write_env_file(env_values):
|
||||
"""写入 .env 文件,保留注释和格式"""
|
||||
"""Write to .env file, preserving comments and formatting"""
|
||||
lines = []
|
||||
existing_keys = set()
|
||||
|
||||
# 读取原文件保留格式
|
||||
# Read the original file and keep the format
|
||||
if os.path.exists(ENV_FILE_PATH):
|
||||
try:
|
||||
with open(ENV_FILE_PATH, 'r', encoding='utf-8') as f:
|
||||
@@ -852,18 +852,18 @@ def write_env_file(env_values):
|
||||
original_line = line
|
||||
stripped = line.strip()
|
||||
|
||||
# 保留空行和注释
|
||||
# Keep blank lines and comments
|
||||
if not stripped or stripped.startswith('#'):
|
||||
lines.append(original_line)
|
||||
continue
|
||||
|
||||
# 更新已存在的键
|
||||
# Update an existing key
|
||||
if '=' in stripped:
|
||||
key = stripped.split('=', 1)[0].strip()
|
||||
if key in env_values:
|
||||
existing_keys.add(key)
|
||||
value = env_values[key]
|
||||
# 如果值包含特殊字符,用引号包裹
|
||||
# If the value contains special characters, wrap it in quotes
|
||||
if ' ' in str(value) or '"' in str(value) or "'" in str(value):
|
||||
lines.append(f'{key}="{value}"\n')
|
||||
else:
|
||||
@@ -875,7 +875,7 @@ def write_env_file(env_values):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read .env file for update: {e}")
|
||||
|
||||
# 添加新的键
|
||||
# Add new key
|
||||
new_keys = set(env_values.keys()) - existing_keys
|
||||
if new_keys:
|
||||
if lines and not lines[-1].endswith('\n'):
|
||||
@@ -888,7 +888,7 @@ def write_env_file(env_values):
|
||||
else:
|
||||
lines.append(f'{key}={value}\n')
|
||||
|
||||
# 写入文件
|
||||
# write file
|
||||
try:
|
||||
with open(ENV_FILE_PATH, 'w', encoding='utf-8') as f:
|
||||
f.writelines(lines)
|
||||
@@ -902,7 +902,7 @@ def write_env_file(env_values):
|
||||
@login_required
|
||||
@admin_required
|
||||
def get_settings_schema():
|
||||
"""获取配置项定义 (admin only)"""
|
||||
"""Get configuration item definition (admin only)"""
|
||||
return jsonify({
|
||||
'code': 1,
|
||||
'msg': 'success',
|
||||
@@ -914,10 +914,10 @@ def get_settings_schema():
|
||||
@login_required
|
||||
@admin_required
|
||||
def get_settings_values():
|
||||
"""获取当前配置值 - 包括敏感信息(真实值)(admin only)"""
|
||||
"""Get current configuration values - including sensitive information (real values) (admin only)"""
|
||||
env_values = read_env_file()
|
||||
|
||||
# 构建返回数据,返回真实值
|
||||
# Construct return data and return true value
|
||||
result = {}
|
||||
for group_key, group in CONFIG_SCHEMA.items():
|
||||
result[group_key] = {}
|
||||
@@ -925,7 +925,7 @@ def get_settings_values():
|
||||
key = item['key']
|
||||
value = env_values.get(key, item.get('default', ''))
|
||||
result[group_key][key] = value
|
||||
# 标记密码类型是否已配置
|
||||
# Marks whether the password type is configured
|
||||
if item['type'] == 'password':
|
||||
result[group_key][f'{key}_configured'] = bool(value)
|
||||
|
||||
@@ -940,16 +940,16 @@ def get_settings_values():
|
||||
@login_required
|
||||
@admin_required
|
||||
def save_settings():
|
||||
"""保存配置 (admin only)"""
|
||||
"""Save configuration (admin only)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'code': 0, 'msg': 'Invalid request payload'})
|
||||
|
||||
# 读取当前配置
|
||||
# Read current configuration
|
||||
current_env = read_env_file()
|
||||
|
||||
# 更新配置
|
||||
# Update configuration
|
||||
updates = {}
|
||||
for group_key, group_values in data.items():
|
||||
if group_key not in CONFIG_SCHEMA:
|
||||
@@ -960,23 +960,23 @@ def save_settings():
|
||||
if key in group_values:
|
||||
new_value = group_values[key]
|
||||
|
||||
# 空值处理
|
||||
# Null value handling
|
||||
if new_value is None or new_value == '':
|
||||
if not item.get('required', True):
|
||||
updates[key] = ''
|
||||
else:
|
||||
updates[key] = str(new_value)
|
||||
|
||||
# 合并更新
|
||||
# Merge updates
|
||||
current_env.update(updates)
|
||||
|
||||
# 写入文件
|
||||
# write file
|
||||
if write_env_file(current_env):
|
||||
# 清除配置缓存
|
||||
# Clear configuration cache
|
||||
clear_config_cache()
|
||||
# 热重载运行时环境变量(无需重启进程)
|
||||
# Hot reload runtime environment variables (no need to restart the process)
|
||||
_reload_runtime_env()
|
||||
# 重置依赖配置的服务单例(下次请求自动按新配置重建)
|
||||
# Reset the service singleton that depends on the configuration (the next request will be automatically rebuilt according to the new configuration)
|
||||
_refresh_runtime_services()
|
||||
|
||||
return jsonify({
|
||||
@@ -1001,7 +1001,7 @@ def save_settings():
|
||||
@login_required
|
||||
@admin_required
|
||||
def get_openrouter_balance():
|
||||
"""查询 OpenRouter 账户余额 (admin only)"""
|
||||
"""Check OpenRouter account balance (admin only)"""
|
||||
try:
|
||||
import requests
|
||||
from app.config.api_keys import APIKeys
|
||||
@@ -1014,7 +1014,7 @@ def get_openrouter_balance():
|
||||
'data': None
|
||||
})
|
||||
|
||||
# 调用 OpenRouter API 查询余额
|
||||
# Call OpenRouter API to query balance
|
||||
# https://openrouter.ai/docs#limits
|
||||
resp = requests.get(
|
||||
'https://openrouter.ai/api/v1/auth/key',
|
||||
@@ -1027,11 +1027,11 @@ def get_openrouter_balance():
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
# OpenRouter 返回格式: {"data": {"label": "...", "usage": 0.0, "limit": null, ...}}
|
||||
# OpenRouter return format: {"data": {"label": "...", "usage": 0.0, "limit": null, ...}}
|
||||
key_data = data.get('data', {})
|
||||
usage = key_data.get('usage', 0) # 已使用金额
|
||||
limit = key_data.get('limit') # 限额(可能为null表示无限制)
|
||||
limit_remaining = key_data.get('limit_remaining') # 剩余额度
|
||||
usage = key_data.get('usage', 0) # Amount used
|
||||
limit = key_data.get('limit') # limit (may be null for no limit)
|
||||
limit_remaining = key_data.get('limit_remaining') # remaining balance
|
||||
is_free_tier = key_data.get('is_free_tier', False)
|
||||
rate_limit = key_data.get('rate_limit', {})
|
||||
|
||||
@@ -1039,9 +1039,9 @@ def get_openrouter_balance():
|
||||
'code': 1,
|
||||
'msg': 'success',
|
||||
'data': {
|
||||
'usage': round(usage, 4), # 已使用(美元)
|
||||
'limit': limit, # 总限额
|
||||
'limit_remaining': round(limit_remaining, 4) if limit_remaining is not None else None, # 剩余额度
|
||||
'usage': round(usage, 4), # Used (USD)
|
||||
'limit': limit, # total limit
|
||||
'limit_remaining': round(limit_remaining, 4) if limit_remaining is not None else None, # remaining balance
|
||||
'is_free_tier': is_free_tier,
|
||||
'rate_limit': rate_limit,
|
||||
'label': key_data.get('label', '')
|
||||
@@ -1079,13 +1079,13 @@ def get_openrouter_balance():
|
||||
@login_required
|
||||
@admin_required
|
||||
def test_connection():
|
||||
"""测试API连接 (admin only)"""
|
||||
"""Test API connection (admin only)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
service = data.get('service')
|
||||
|
||||
if service == 'openrouter':
|
||||
# 测试 OpenRouter 连接
|
||||
# Test OpenRouter connection
|
||||
from app.services.llm import LLMService
|
||||
llm = LLMService()
|
||||
result = llm.test_connection()
|
||||
@@ -1095,7 +1095,7 @@ def test_connection():
|
||||
return jsonify({'code': 0, 'msg': 'OpenRouter connection failed'})
|
||||
|
||||
elif service == 'finnhub':
|
||||
# 测试 Finnhub 连接
|
||||
# Test Finnhub connection
|
||||
import requests
|
||||
api_key = data.get('api_key') or os.getenv('FINNHUB_API_KEY')
|
||||
if not api_key:
|
||||
|
||||
@@ -600,7 +600,7 @@ def get_positions():
|
||||
pct = _calc_pnl_percent(entry, size, pnl)
|
||||
|
||||
rr = dict(r)
|
||||
# 确保 entry_price 有值(如果数据库中是 NULL,使用计算出的 entry 值)
|
||||
# Make sure entry_price has a value (if it is NULL in the database, use the calculated entry value)
|
||||
if not rr.get("entry_price") or float(rr.get("entry_price") or 0.0) <= 0:
|
||||
rr["entry_price"] = float(entry or 0.0)
|
||||
else:
|
||||
@@ -818,10 +818,10 @@ def test_connection():
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
|
||||
# 记录请求数据(用于调试,但不记录敏感信息)
|
||||
# Log request data (for debugging, but do not log sensitive information)
|
||||
logger.debug(f"Connection test request keys: {list(data.keys())}")
|
||||
|
||||
# 获取交易所配置
|
||||
# Get exchange configuration
|
||||
exchange_config = data.get('exchange_config', data)
|
||||
|
||||
# Local deployment: no encryption/decryption; accept dict or JSON string.
|
||||
@@ -832,7 +832,7 @@ def test_connection():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 验证 exchange_config 是否为字典
|
||||
# Verify exchange_config is a dictionary
|
||||
if not isinstance(exchange_config, dict):
|
||||
logger.error(f"Invalid exchange_config type: {type(exchange_config)}, data: {str(exchange_config)[:200]}")
|
||||
# Frontend expects HTTP 200 with {code:0} for business failures.
|
||||
@@ -844,21 +844,21 @@ def test_connection():
|
||||
user_id = g.user_id if hasattr(g, 'user_id') else 1
|
||||
resolved = resolve_exchange_config(exchange_config, user_id=user_id)
|
||||
|
||||
# 验证必要字段 (check resolved config after credential merge)
|
||||
# Verify required fields (check resolved config after credential merge)
|
||||
if not resolved.get('exchange_id'):
|
||||
return jsonify({'code': 0, 'msg': 'Please select an exchange', 'data': None})
|
||||
|
||||
api_key = resolved.get('api_key', '')
|
||||
secret_key = resolved.get('secret_key', '')
|
||||
|
||||
# 详细日志排查
|
||||
# Detailed log troubleshooting
|
||||
logger.info(f"Testing connection: exchange_id={resolved.get('exchange_id')}")
|
||||
if api_key:
|
||||
logger.info(f"API Key: {api_key[:5]}... (len={len(api_key)})")
|
||||
if secret_key:
|
||||
logger.info(f"Secret Key: {secret_key[:5]}... (len={len(secret_key)})")
|
||||
|
||||
# 检查是否有特殊字符
|
||||
# Check if there are special characters
|
||||
if api_key and api_key.strip() != api_key:
|
||||
logger.warning("API key contains leading/trailing whitespace")
|
||||
if secret_key and secret_key.strip() != secret_key:
|
||||
@@ -1052,7 +1052,7 @@ def get_strategy_notifications():
|
||||
created_at = item.get('created_at')
|
||||
if created_at:
|
||||
if hasattr(created_at, 'timestamp'):
|
||||
# 无时区 datetime:连接已 SET TIME ZONE UTC,按 UTC 解释再转 Unix,避免服务端本地 TZ 误判
|
||||
# No time zone datetime: The connection has SET TIME ZONE UTC, interpret it according to UTC and then transfer to Unix to avoid misjudgment of the local TZ on the server side.
|
||||
if getattr(created_at, 'tzinfo', None) is None:
|
||||
created_at = created_at.replace(tzinfo=_dt_tz.utc)
|
||||
item['created_at'] = int(created_at.timestamp())
|
||||
|
||||
Reference in New Issue
Block a user