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:
dienakdz
2026-04-06 16:47:36 +07:00
parent 3ca291a346
commit 11e2e5aaa6
64 changed files with 2323 additions and 2336 deletions
+1 -1
View File
@@ -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:
+19 -19
View File
@@ -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,
+5 -5
View File
@@ -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)
# =========================
+41 -41
View File
@@ -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 buyers 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
+1 -1
View File
@@ -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,
},
}
+40 -40
View File
@@ -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 dont 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}%",
+4 -4
View File
@@ -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()
+26 -26
View File
@@ -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({
+11 -11
View File
@@ -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', '')
+24 -24
View File
@@ -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({
+29 -29
View File
@@ -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_idslug
# 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', '{}')
+11 -11
View File
@@ -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.
优先使用实时报价 APIticker降级使用分钟/日线 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 限制 yfinanceTiingoFinnhub
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
+53 -53
View File
@@ -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:
+8 -8
View File
@@ -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())