From 11e2e5aaa697303e8f946ed1255c5c0ba6bd342a Mon Sep 17 00:00:00 2001 From: dienakdz Date: Mon, 6 Apr 2026 16:47:36 +0700 Subject: [PATCH] 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. --- backend_api_python/app/__init__.py | 5 +- backend_api_python/app/config/__init__.py | 12 +- backend_api_python/app/config/api_keys.py | 13 +- backend_api_python/app/config/data_sources.py | 21 +- backend_api_python/app/config/database.py | 38 +- backend_api_python/app/config/settings.py | 22 +- .../app/data_sources/__init__.py | 20 +- backend_api_python/app/data_sources/base.py | 67 ++- .../app/data_sources/cache_manager.py | 90 ++-- .../app/data_sources/circuit_breaker.py | 82 ++-- backend_api_python/app/data_sources/crypto.py | 112 ++--- .../app/data_sources/factory.py | 44 +- backend_api_python/app/data_sources/forex.py | 170 +++---- .../app/data_sources/futures.py | 74 +-- .../app/data_sources/polymarket.py | 422 +++++++++--------- .../app/data_sources/rate_limiter.py | 110 ++--- .../app/data_sources/us_stock.py | 90 ++-- backend_api_python/app/routes/auth.py | 2 +- backend_api_python/app/routes/backtest.py | 38 +- backend_api_python/app/routes/billing.py | 10 +- backend_api_python/app/routes/community.py | 82 ++-- backend_api_python/app/routes/credentials.py | 2 +- .../app/routes/global_market.py | 80 ++-- backend_api_python/app/routes/health.py | 8 +- backend_api_python/app/routes/indicator.py | 52 +-- backend_api_python/app/routes/kline.py | 22 +- backend_api_python/app/routes/market.py | 48 +- backend_api_python/app/routes/polymarket.py | 58 +-- backend_api_python/app/routes/portfolio.py | 22 +- backend_api_python/app/routes/settings.py | 106 ++--- backend_api_python/app/routes/strategy.py | 16 +- backend_api_python/app/services/__init__.py | 2 +- .../app/services/analysis_memory.py | 18 +- backend_api_python/app/services/backtest.py | 80 ++-- .../app/services/billing_service.py | 134 +++--- .../app/services/community_service.py | 142 +++--- .../app/services/fast_analysis.py | 398 ++++++++--------- .../app/services/indicator_params.py | 90 ++-- backend_api_python/app/services/kline.py | 72 +-- .../app/services/live_trading/execution.py | 22 +- .../app/services/live_trading/symbols.py | 12 +- .../app/services/market_data_collector.py | 378 ++++++++-------- .../app/services/pending_order_worker.py | 8 +- .../app/services/polymarket_analyzer.py | 158 +++---- .../app/services/polymarket_batch_analyzer.py | 52 +-- .../app/services/polymarket_worker.py | 62 +-- .../app/services/portfolio_monitor.py | 82 ++-- backend_api_python/app/services/search.py | 212 ++++----- .../app/services/signal_notifier.py | 21 +- .../app/services/trading_executor.py | 400 ++++++++--------- .../app/services/usdt_payment_service.py | 8 +- .../app/services/user_service.py | 18 +- backend_api_python/app/utils/__init__.py | 2 +- backend_api_python/app/utils/auth.py | 16 +- backend_api_python/app/utils/cache.py | 10 +- backend_api_python/app/utils/config_loader.py | 22 +- backend_api_python/app/utils/db_postgres.py | 4 +- backend_api_python/app/utils/http.py | 14 +- backend_api_python/app/utils/logger.py | 20 +- backend_api_python/app/utils/safe_exec.py | 122 ++--- backend_api_python/gunicorn_config.py | 14 +- backend_api_python/run.py | 2 +- docs/CHANGELOG.md | 54 +-- docs/FRONTEND_FAST_ANALYSIS.md | 72 +-- 64 files changed, 2323 insertions(+), 2336 deletions(-) diff --git a/backend_api_python/app/__init__.py b/backend_api_python/app/__init__.py index 8016bc2..70d90d7 100644 --- a/backend_api_python/app/__init__.py +++ b/backend_api_python/app/__init__.py @@ -34,7 +34,7 @@ def get_pending_order_worker(): def start_polymarket_worker(): - """启动Polymarket后台任务""" + """Start the Polymarket background task.""" try: from app.services.polymarket_worker import get_polymarket_worker get_polymarket_worker().start() @@ -150,7 +150,7 @@ def restore_running_strategies(): logger.info(f"[OK] {strategy_type_name} {strategy_id} restored") else: logger.warning(f"[FAIL] {strategy_type_name} {strategy_id} restore failed (state may be stale)") - # 如果恢复失败,更新数据库状态为stopped,避免策略处于"僵尸"状态 + # If recovery fails, update the database status to stopped to prevent the policy from being in a "zombie" state. try: strategy_service.update_strategy_status(strategy_id, 'stopped') logger.info(f"[FIX] Updated strategy {strategy_id} status to 'stopped' after restore failure") @@ -222,4 +222,3 @@ def create_app(config_name='default'): restore_running_strategies() return app - diff --git a/backend_api_python/app/config/__init__.py b/backend_api_python/app/config/__init__.py index 31a910c..bb4029e 100644 --- a/backend_api_python/app/config/__init__.py +++ b/backend_api_python/app/config/__init__.py @@ -1,6 +1,6 @@ """ -配置模块 -统一导出所有配置 +Configuration module +Export all configurations uniformly """ from app.config.settings import Config from app.config.api_keys import APIKeys @@ -15,17 +15,17 @@ from app.config.data_sources import ( ) __all__ = [ - # 主配置 + # main configuration 'Config', - # API 密钥 + # API key 'APIKeys', - # 数据库/缓存 + # Database/cache 'RedisConfig', 'CacheConfig', - # 数据源 + # data source 'DataSourceConfig', 'FinnhubConfig', 'TiingoConfig', diff --git a/backend_api_python/app/config/api_keys.py b/backend_api_python/app/config/api_keys.py index 865e181..515f296 100644 --- a/backend_api_python/app/config/api_keys.py +++ b/backend_api_python/app/config/api_keys.py @@ -5,8 +5,7 @@ All third-party keys should be provided via environment variables (recommended: import os class MetaAPIKeys(type): - """API Keys 元类,用于支持类属性的动态获取""" - + """API Keys metaclass, used to support dynamic acquisition of class attributes""" @property def FINNHUB_API_KEY(cls): from app.utils.config_loader import load_addon_config @@ -95,18 +94,18 @@ class MetaAPIKeys(type): class APIKeys(metaclass=MetaAPIKeys): - """API 密钥配置类""" - + """API key configuration class""" + @classmethod def get(cls, key_name: str, default: str = '') -> str: - """获取 API 密钥""" - # 尝试从类属性获取 + """Get API key""" + # Try to get from class attribute if hasattr(cls, key_name): return getattr(cls, key_name) return default @classmethod def is_configured(cls, key_name: str) -> bool: - """检查 API 密钥是否已配置""" + """Check if the API key is configured""" value = cls.get(key_name) return bool(value and value.strip()) diff --git a/backend_api_python/app/config/data_sources.py b/backend_api_python/app/config/data_sources.py index ff114f9..9629c79 100644 --- a/backend_api_python/app/config/data_sources.py +++ b/backend_api_python/app/config/data_sources.py @@ -1,5 +1,5 @@ """ -数据源配置 +Data source configuration """ import os @@ -24,7 +24,7 @@ class MetaDataSourceConfig(type): class DataSourceConfig(metaclass=MetaDataSourceConfig): - """数据源通用配置""" + """General data source configuration.""" pass @@ -51,9 +51,7 @@ class MetaFinnhubConfig(type): class FinnhubConfig(metaclass=MetaFinnhubConfig): - """Finnhub 数据源配置""" - pass - + """Finnhub data source configuration""" class MetaTiingoConfig(type): @property @@ -68,9 +66,7 @@ class MetaTiingoConfig(type): class TiingoConfig(metaclass=MetaTiingoConfig): - """Tiingo 数据源配置""" - pass - + """Tiingo data source configuration""" class MetaYFinanceConfig(type): @property @@ -94,9 +90,7 @@ class MetaYFinanceConfig(type): class YFinanceConfig(metaclass=MetaYFinanceConfig): - """Yahoo Finance 数据源配置""" - pass - + """Yahoo Finance data source configuration""" class MetaCCXTConfig(type): @property @@ -146,7 +140,7 @@ class MetaCCXTConfig(type): class CCXTConfig(metaclass=MetaCCXTConfig): - """CCXT 加密货币数据源配置""" + """CCXT cryptocurrency data source configuration.""" pass @@ -166,5 +160,4 @@ class MetaAkshareConfig(type): class AkshareConfig(metaclass=MetaAkshareConfig): - """Akshare 数据源配置""" - pass + """Akshare data source configuration""" diff --git a/backend_api_python/app/config/database.py b/backend_api_python/app/config/database.py index f6a8f60..88524fc 100644 --- a/backend_api_python/app/config/database.py +++ b/backend_api_python/app/config/database.py @@ -1,11 +1,10 @@ """ -数据库和缓存配置 +Database and cache configuration """ import os class MetaRedisConfig(type): - """Redis 配置""" - + """Redis configuration""" @property def HOST(cls): return os.getenv('REDIS_HOST', 'localhost') @@ -36,22 +35,21 @@ class MetaRedisConfig(type): class RedisConfig(metaclass=MetaRedisConfig): - """Redis 缓存配置""" - + """Redis cache configuration.""" + @classmethod def get_url(cls) -> str: - """获取 Redis 连接 URL""" + """Get the Redis connection URL.""" if cls.PASSWORD: return f"redis://:{cls.PASSWORD}@{cls.HOST}:{cls.PORT}/{cls.DB}" return f"redis://{cls.HOST}:{cls.PORT}/{cls.DB}" class MetaCacheConfig(type): - """缓存业务配置""" - - @property + """Cache business configuration.""" + def ENABLED(cls): - # 强制默认关闭,除非环境变量显式开启 + # Forced to be off by default unless explicitly enabled by an environment variable return os.getenv('CACHE_ENABLED', 'False').lower() == 'true' @property @@ -61,15 +59,15 @@ class MetaCacheConfig(type): @property def KLINE_CACHE_TTL(cls): return { - '1m': 5, # 1分钟K线缓存5秒 - '3m': 30, # 3分钟K线缓存30秒 - '5m': 60, # 5分钟K线缓存1分钟 - '15m': 300, # 15分钟K线缓存5分钟 - '30m': 300, # 30分钟K线缓存5分钟 - '1H': 300, # 1小时K线缓存5分钟 - '4H': 300, # 4小时K线缓存5分钟 - '1D': 300, # 日K线缓存5分钟 - # 兼容小写 + '1m': 5, # K-line cache for 1 minute and 5 seconds + '3m': 30, # 3 minutes K-line cache 30 seconds + '5m': 60, # 5 minutes K-line cache 1 minute + '15m': 300, # 15 minutes K-line cache 5 minutes + '30m': 300, # 30 minutes K-line cache 5 minutes + '1H': 300, # 1 hour K-line cache for 5 minutes + '4H': 300, # 4 hours K-line cache for 5 minutes + '1D': 300, # Daily K-line cache for 5 minutes + # Compatible with lowercase '1h': 300, '4h': 300, '1d': 300, @@ -85,5 +83,5 @@ class MetaCacheConfig(type): class CacheConfig(metaclass=MetaCacheConfig): - """缓存配置""" + """Cache configuration.""" pass diff --git a/backend_api_python/app/config/settings.py b/backend_api_python/app/config/settings.py index aeb0242..6e79a6a 100644 --- a/backend_api_python/app/config/settings.py +++ b/backend_api_python/app/config/settings.py @@ -1,11 +1,11 @@ """ -应用主配置 +Apply main configuration """ import os class MetaConfig(type): - # ==================== 服务配置 ==================== - # 服务启动参数通常由环境变量或命令行参数决定,不建议从数据库读取 + # ==================== Service Configuration ==================== + # Service startup parameters are usually determined by environment variables or command line parameters. Reading from the database is not recommended. @property def HOST(cls): @@ -27,7 +27,7 @@ class MetaConfig(type): def VERSION(cls): return '2.0.0' - # ==================== 认证配置 ==================== + # ==================== Authentication configuration ==================== @property def SECRET_KEY(cls): return os.getenv('SECRET_KEY', 'quantdinger-secret-key-change-me') @@ -40,8 +40,8 @@ class MetaConfig(type): def ADMIN_PASSWORD(cls): return os.getenv('ADMIN_PASSWORD', '123456') - # ==================== 日志配置 ==================== - # 日志配置通常在应用启动最早阶段需要,建议保持环境变量 + # ==================== Log configuration ==================== + # Log configuration is usually required at the earliest stage of application startup. It is recommended to maintain environment variables. @property def LOG_LEVEL(cls): @@ -63,7 +63,7 @@ class MetaConfig(type): def LOG_BACKUP_COUNT(cls): return int(os.getenv('LOG_BACKUP_COUNT', 5)) - # ==================== 安全配置 ==================== + # ==================== Security Configuration ==================== @property def RATE_LIMIT(cls): @@ -71,7 +71,7 @@ class MetaConfig(type): val = load_addon_config().get('app', {}).get('rate_limit') return int(val) if val is not None else int(os.getenv('RATE_LIMIT', 100)) - # ==================== 功能开关 ==================== + # ==================== Function switch ==================== @property def ENABLE_CACHE(cls): @@ -90,9 +90,9 @@ class MetaConfig(type): return os.getenv('ENABLE_REQUEST_LOG', 'True').lower() == 'true' class Config(metaclass=MetaConfig): - """应用配置类""" - + """Application configuration class.""" + @classmethod def get_log_path(cls) -> str: - """获取日志文件完整路径""" + """Get the full path of the log file.""" return os.path.join(cls.LOG_DIR, cls.LOG_FILE) diff --git a/backend_api_python/app/data_sources/__init__.py b/backend_api_python/app/data_sources/__init__.py index 9e309ef..d1b3c45 100644 --- a/backend_api_python/app/data_sources/__init__.py +++ b/backend_api_python/app/data_sources/__init__.py @@ -1,11 +1,11 @@ """ -数据源模块 -支持多种市场的K线数据获取 +data source module +Support K-line data acquisition for multiple markets -改进版本(参考 daily_stock_analysis 项目): -- 熔断器保护 (circuit_breaker) -- 数据缓存 (cache_manager) -- 防封禁策略 (rate_limiter) +Improved version (refer to daily_stock_analysis project): +- Fuse protection (circuit_breaker) +- Data cache (cache_manager) +- Anti-ban strategy (rate_limiter) """ from app.data_sources.factory import DataSourceFactory from app.data_sources.circuit_breaker import ( @@ -26,17 +26,17 @@ from app.data_sources.rate_limiter import ( ) __all__ = [ - # 工厂 + # factory 'DataSourceFactory', - # 熔断器 + # fuse 'CircuitBreaker', 'get_realtime_circuit_breaker', - # 缓存 + # cache 'DataCache', 'get_realtime_cache', 'get_kline_cache', 'get_stock_info_cache', - # 限流器 + # current limiter 'RateLimiter', 'get_random_user_agent', 'random_sleep', diff --git a/backend_api_python/app/data_sources/base.py b/backend_api_python/app/data_sources/base.py index e14c953..053cc74 100644 --- a/backend_api_python/app/data_sources/base.py +++ b/backend_api_python/app/data_sources/base.py @@ -1,6 +1,6 @@ """ -数据源基类 -定义统一的数据源接口 +Data source base class +Define a unified data source interface """ from abc import ABC, abstractmethod from typing import Dict, List, Any, Optional @@ -11,7 +11,7 @@ from app.utils.logger import get_logger logger = get_logger(__name__) -# K线周期映射(秒数) +# K-line cycle mapping (seconds) TIMEFRAME_SECONDS = { '1m': 60, '5m': 300, @@ -25,8 +25,8 @@ TIMEFRAME_SECONDS = { class BaseDataSource(ABC): - """数据源基类""" - + """Data source base class.""" + name: str = "base" @abstractmethod @@ -38,16 +38,16 @@ class BaseDataSource(ABC): before_time: Optional[int] = None ) -> List[Dict[str, Any]]: """ - 获取K线数据 + Get K-line data Args: - symbol: 交易对/股票代码 - timeframe: 时间周期 (1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W) - limit: 数据条数 - before_time: 获取此时间之前的数据(Unix时间戳,秒) + symbol: trading pair/stock code + timeframe: time period (1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W) + limit: number of data items + before_time: Get data before this time (Unix timestamp, seconds) Returns: - K线数据列表,格式: + K-line data list, format: [{"time": int, "open": float, "high": float, "low": float, "close": float, "volume": float}, ...] """ pass @@ -70,7 +70,7 @@ class BaseDataSource(ABC): close: float, volume: float ) -> Dict[str, Any]: - """格式化单条K线数据""" + """Format a single K-line record.""" return { 'time': timestamp, 'open': round(float(open_price), 4), @@ -87,15 +87,15 @@ class BaseDataSource(ABC): buffer_ratio: float = 1.2 ) -> int: """ - 计算获取指定数量K线所需的时间范围(秒) + Calculate the time range (seconds) required to obtain the specified number of K-lines Args: - timeframe: 时间周期 - limit: K线数量 - buffer_ratio: 缓冲系数 + timeframe: time period + limit: number of K-lines + buffer_ratio: buffer coefficient Returns: - 时间范围(秒) + Time range (seconds) """ seconds_per_candle = TIMEFRAME_SECONDS.get(timeframe, 86400) return int(seconds_per_candle * limit * buffer_ratio) @@ -107,24 +107,24 @@ class BaseDataSource(ABC): before_time: Optional[int] = None ) -> List[Dict[str, Any]]: """ - 过滤和限制K线数据 + Filter and limit K-line data Args: - klines: K线数据列表 - limit: 最大数量 - before_time: 过滤此时间之后的数据 + klines: K-line data list + limit: maximum quantity + before_time: Filter data after this time Returns: - 处理后的K线数据 + Processed K-line data """ - # 按时间排序 + # Sort by time klines.sort(key=lambda x: x['time']) - # 过滤时间 + # filter time if before_time: klines = [k for k in klines if k['time'] < before_time] - # 限制数量(取最新的) + # Limit quantity (take the latest) if len(klines) > limit: klines = klines[-limit:] @@ -136,12 +136,12 @@ class BaseDataSource(ABC): klines: List[Dict[str, Any]], timeframe: str ): - """记录获取结果日志。 + """Record the result log. - 延迟判断: - - K 线 time 为 Unix 秒(UTC),与 datetime.now(UTC) 比较,避免本地时区误差。 - - 日线/周线:最后一根通常是「上一交易日收盘」,周末/节假日可达 3~4 天, - 原先用 2×86400s(48h)会在周一早盘误报;改为日线最多容忍约 5 个自然日,周线更宽。 + Delayed judgment: + - K-line time is Unix seconds (UTC), compared with datetime.now(UTC) to avoid local time zone errors. + - Daily/weekly line: The last line is usually the "close of the previous trading day", and it can last 3 to 4 days on weekends/holidays. + Originally, using 2×86400s (48h) would cause false alarms in Monday morning trading; instead, the daily line tolerates up to about 5 natural days, and the weekly line is wider. """ if klines: latest_ts = int(klines[-1]["time"]) @@ -151,13 +151,13 @@ class BaseDataSource(ABC): tf_sec = TIMEFRAME_SECONDS.get(timeframe, 3600) if tf_sec < 86400: - # 分钟/小时级:超过约 2 根 K 未更新则告警 + # Minute/hour level: If it exceeds about 2 K, an alarm will be issued if it is not updated. max_diff = tf_sec * 2 elif tf_sec == 86400: - # 日线:覆盖周末 + 短假期(约 5 个自然日) + # Daily line: covering weekends + short holidays (about 5 calendar days) max_diff = 5 * 86400 else: - # 周线:允许跨多周数据滞后 + # Weekly: Allows data lags across multiple weeks max_diff = max(tf_sec * 2, 21 * 86400) if time_diff > max_diff: @@ -167,4 +167,3 @@ class BaseDataSource(ABC): ) else: logger.warning(f"{self.name}: no data for {symbol}") - diff --git a/backend_api_python/app/data_sources/cache_manager.py b/backend_api_python/app/data_sources/cache_manager.py index 2d5dfbf..2be9a65 100644 --- a/backend_api_python/app/data_sources/cache_manager.py +++ b/backend_api_python/app/data_sources/cache_manager.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- """ =================================== -数据缓存管理模块 +Data cache management module =================================== -参考 daily_stock_analysis 项目实现 -用于缓存实时行情和K线数据,减少重复请求 +Refer to daily_stock_analysis project implementation +Used to cache realtime market and K-line data to reduce repeated requests -特性: -1. TTL (Time To Live) 过期机制 -2. LRU (Least Recently Used) 淘汰策略 -3. 按数据类型分区管理 +characteristic: +1. TTL (Time To Live) expiration mechanism +2. LRU (Least Recently Used) elimination strategy +3. Partition management by data type """ import time @@ -26,37 +26,37 @@ logger = logging.getLogger(__name__) @dataclass class CacheEntry: - """缓存条目""" + """cache entry""" data: Any timestamp: float ttl: float hit_count: int = 0 def is_expired(self) -> bool: - """检查是否过期""" + """Check if expired""" return time.time() - self.timestamp > self.ttl def age(self) -> float: - """返回缓存年龄(秒)""" + """Return cache age (seconds)""" return time.time() - self.timestamp class DataCache: """ - 数据缓存管理器 + Data Cache Manager - 特性: - - TTL 过期机制 - - 最大容量限制 - - LRU 淘汰策略 - - 线程安全 + characteristic: + - TTL expiration mechanism + - Maximum capacity limit + - LRU elimination strategy + - Thread safety """ def __init__( self, name: str = "default", - default_ttl: float = 600.0, # 默认10分钟 - max_size: int = 1000 # 最大缓存条目数 + default_ttl: float = 600.0, # Default 10 minutes + max_size: int = 1000 # Maximum number of cache entries ): self.name = name self.default_ttl = default_ttl @@ -64,16 +64,16 @@ class DataCache: self._cache: OrderedDict[str, CacheEntry] = OrderedDict() self._lock = threading.RLock() - # 统计信息 + # Statistics self._hits = 0 self._misses = 0 def get(self, key: str) -> Optional[Any]: """ - 获取缓存数据 + Get cached data Returns: - 缓存的数据,不存在或过期返回 None + Cached data, returns None if it does not exist or has expired. """ with self._lock: if key not in self._cache: @@ -82,14 +82,14 @@ class DataCache: entry = self._cache[key] - # 检查是否过期 + # Check if expired if entry.is_expired(): del self._cache[key] self._misses += 1 logger.debug(f"[缓存] {self.name}:{key} 已过期,删除") return None - # 更新访问顺序(LRU) + # Update access order (LRU) self._cache.move_to_end(key) entry.hit_count += 1 self._hits += 1 @@ -104,15 +104,15 @@ class DataCache: ttl: Optional[float] = None ) -> None: """ - 设置缓存数据 + Set cache data Args: - key: 缓存键 - data: 缓存数据 - ttl: 过期时间(秒),None 使用默认值 + key: cache key + data: cache data + ttl: expiration time (seconds), None uses the default value """ with self._lock: - # 检查容量,执行 LRU 淘汰 + # Check capacity, perform LRU elimination while len(self._cache) >= self.max_size: oldest_key, _ = self._cache.popitem(last=False) logger.debug(f"[缓存] {self.name} 容量已满,淘汰: {oldest_key}") @@ -127,7 +127,7 @@ class DataCache: logger.debug(f"[缓存更新] {self.name}:{key} TTL={actual_ttl}s") def delete(self, key: str) -> bool: - """删除缓存条目""" + """Delete cache entry""" with self._lock: if key in self._cache: del self._cache[key] @@ -136,7 +136,7 @@ class DataCache: return False def clear(self) -> int: - """清空缓存""" + """Clear cache""" with self._lock: count = len(self._cache) self._cache.clear() @@ -144,7 +144,7 @@ class DataCache: return count def cleanup_expired(self) -> int: - """清理过期条目""" + """Clean up expired entries""" with self._lock: expired_keys = [ key for key, entry in self._cache.items() @@ -158,7 +158,7 @@ class DataCache: return len(expired_keys) def stats(self) -> Dict[str, Any]: - """获取缓存统计信息""" + """Get cache statistics""" with self._lock: total_requests = self._hits + self._misses hit_rate = self._hits / total_requests if total_requests > 0 else 0 @@ -175,43 +175,43 @@ class DataCache: # ============================================ -# 全局缓存实例 +# Global cache instance # ============================================ -# 实时行情缓存(20分钟TTL) +# Real-time quotation caching (20 minutes TTL) _realtime_cache = DataCache( name="realtime", - default_ttl=1200.0, # 20分钟 + default_ttl=1200.0, # 20 minutes max_size=6000 ) -# K线数据缓存(5分钟TTL,按需缓存) +# K-line data caching (5 minutes TTL, caching on demand) _kline_cache = DataCache( name="kline", - default_ttl=300.0, # 5分钟 - max_size=500 # 最多500个交易对 + default_ttl=300.0, # 5 minutes + max_size=500 # Up to 500 trading pairs ) -# 股票基本信息缓存(1天TTL) +# Stock basic information cache (1 day TTL) _stock_info_cache = DataCache( name="stock_info", - default_ttl=86400.0, # 24小时 + default_ttl=86400.0, # 24 hours max_size=6000 ) def get_realtime_cache() -> DataCache: - """获取实时行情缓存""" + """Get real-time quotation cache""" return _realtime_cache def get_kline_cache() -> DataCache: - """获取K线数据缓存""" + """Get K-line data cache""" return _kline_cache def get_stock_info_cache() -> DataCache: - """获取股票信息缓存""" + """Get stock information cache""" return _stock_info_cache @@ -222,9 +222,9 @@ def generate_kline_cache_key( before_time: Optional[int] = None ) -> str: """ - 生成K线缓存键 + Generate K-line cache key - 格式: symbol:timeframe:limit[:before_time] + Format: symbol:timeframe:limit[:before_time] """ key = f"{symbol}:{timeframe}:{limit}" if before_time: diff --git a/backend_api_python/app/data_sources/circuit_breaker.py b/backend_api_python/app/data_sources/circuit_breaker.py index 20f52bd..c5a0206 100644 --- a/backend_api_python/app/data_sources/circuit_breaker.py +++ b/backend_api_python/app/data_sources/circuit_breaker.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- """ =================================== -熔断器模块 (Circuit Breaker) +Circuit Breaker =================================== -参考 daily_stock_analysis 项目实现 -用于管理数据源的熔断/冷却状态,避免连续失败时反复请求 +Refer to daily_stock_analysis project implementation +Used to manage the fuse/cooling status of data sources to avoid repeated requests when continuous failures occur. -状态机: -CLOSED(正常) --失败N次--> OPEN(熔断)--冷却时间到--> HALF_OPEN(半开) -HALF_OPEN --成功--> CLOSED -HALF_OPEN --失败--> OPEN +State machine: +CLOSED (normal) --failed N times --> OPEN (melted) --cooling time expired --> HALF_OPEN (half open) +HALF_OPEN --Success--> CLOSED +HALF_OPEN --Failure--> OPEN """ import time @@ -22,38 +22,38 @@ logger = logging.getLogger(__name__) class CircuitState(Enum): - """熔断器状态""" - CLOSED = "closed" # 正常状态 - OPEN = "open" # 熔断状态(不可用) - HALF_OPEN = "half_open" # 半开状态(试探性请求) + """fuse status""" + CLOSED = "closed" # normal state + OPEN = "open" # Fuse status (not available) + HALF_OPEN = "half_open" # Half-open state (exploratory request) class CircuitBreaker: """ - 熔断器 - 管理数据源的熔断/冷却状态 + Circuit Breakers - Manage the blown/cooling status of data sources - 策略: - - 连续失败 N 次后进入熔断状态 - - 熔断期间跳过该数据源 - - 冷却时间后自动恢复半开状态 - - 半开状态下单次成功则完全恢复,失败则继续熔断 + Strategy: + - Enter the fuse state after N consecutive failures + - Skip this data source during the circuit breaker period + - Automatically returns to half-open state after cooling time + - In the half-open state, if a single success is successful, it will be fully restored, if it fails, the fuse will continue to be broken. """ def __init__( self, - failure_threshold: int = 3, # 连续失败次数阈值 - cooldown_seconds: float = 300.0, # 冷却时间(秒),默认5分钟 - half_open_max_calls: int = 1 # 半开状态最大尝试次数 + failure_threshold: int = 3, # Continuous failure threshold + cooldown_seconds: float = 300.0, # Cooling time (seconds), default 5 minutes + half_open_max_calls: int = 1 # Maximum number of attempts in half-open state ): self.failure_threshold = failure_threshold self.cooldown_seconds = cooldown_seconds self.half_open_max_calls = half_open_max_calls - # 各数据源状态 {source_name: {state, failures, last_failure_time, half_open_calls}} + # Status of each data source {source_name: {state, failures, last_failure_time, half_open_calls}} self._states: Dict[str, Dict[str, Any]] = {} def _get_state(self, source: str) -> Dict[str, Any]: - """获取或初始化数据源状态""" + """Get or initialize data source status""" if source not in self._states: self._states[source] = { 'state': CircuitState.CLOSED, @@ -66,10 +66,10 @@ class CircuitBreaker: def is_available(self, source: str) -> bool: """ - 检查数据源是否可用 + Check if the data source is available - 返回 True 表示可以尝试请求 - 返回 False 表示应跳过该数据源 + Return True to indicate that the request can be attempted + Return False to indicate that the data source should be skipped """ state = self._get_state(source) current_time = time.time() @@ -78,10 +78,10 @@ class CircuitBreaker: return True if state['state'] == CircuitState.OPEN: - # 检查冷却时间 + # Check cool down time time_since_failure = current_time - state['last_failure_time'] if time_since_failure >= self.cooldown_seconds: - # 冷却完成,进入半开状态 + # Cooling is completed and enters the half-open state state['state'] = CircuitState.HALF_OPEN state['half_open_calls'] = 0 logger.info(f"[熔断器] {source} 冷却完成,进入半开状态") @@ -92,7 +92,7 @@ class CircuitBreaker: return False if state['state'] == CircuitState.HALF_OPEN: - # 半开状态下限制请求次数 + # Limit the number of requests in the half-open state if state['half_open_calls'] < self.half_open_max_calls: return True return False @@ -100,21 +100,21 @@ class CircuitBreaker: return True def record_success(self, source: str) -> None: - """记录成功请求""" + """Log successful request""" state = self._get_state(source) if state['state'] == CircuitState.HALF_OPEN: - # 半开状态下成功,完全恢复 + # Successful in half-open state, full recovery logger.info(f"[熔断器] {source} 半开状态请求成功,恢复正常") - # 重置状态 + # reset state state['state'] = CircuitState.CLOSED state['failures'] = 0 state['half_open_calls'] = 0 state['last_error'] = None def record_failure(self, source: str, error: Optional[str] = None) -> None: - """记录失败请求""" + """Logging failed requests""" state = self._get_state(source) current_time = time.time() @@ -123,12 +123,12 @@ class CircuitBreaker: state['last_error'] = error if state['state'] == CircuitState.HALF_OPEN: - # 半开状态下失败,继续熔断 + # Fails in half-open state and continues to fuse state['state'] = CircuitState.OPEN state['half_open_calls'] = 0 logger.warning(f"[熔断器] {source} 半开状态请求失败,继续熔断 {self.cooldown_seconds}s") elif state['failures'] >= self.failure_threshold: - # 达到阈值,进入熔断 + # reaches the threshold and enters the circuit breaker state['state'] = CircuitState.OPEN logger.warning(f"[熔断器] {source} 连续失败 {state['failures']} 次,进入熔断状态 " f"(冷却 {self.cooldown_seconds}s)") @@ -136,7 +136,7 @@ class CircuitBreaker: logger.warning(f"[熔断器] 最后错误: {error}") def get_status(self) -> Dict[str, Dict[str, Any]]: - """获取所有数据源状态""" + """Get all data source status""" return { source: { 'state': info['state'].value, @@ -147,7 +147,7 @@ class CircuitBreaker: } def reset(self, source: Optional[str] = None) -> None: - """重置熔断器状态""" + """Reset fuse status""" if source: if source in self._states: del self._states[source] @@ -158,17 +158,17 @@ class CircuitBreaker: # ============================================ -# 全局熔断器实例 +# Global circuit breaker example # ============================================ -# 实时行情熔断器(更严格的策略) +# Real-time market circuit breaker (more stringent strategy) _realtime_circuit_breaker = CircuitBreaker( - failure_threshold=2, # 连续失败2次熔断 - cooldown_seconds=180.0, # 冷却3分钟 + failure_threshold=2, # Failed 2 times in a row + cooldown_seconds=180.0, # Cool for 3 minutes half_open_max_calls=1 ) def get_realtime_circuit_breaker() -> CircuitBreaker: - """获取实时行情熔断器""" + """Get realtime market breaker""" return _realtime_circuit_breaker diff --git a/backend_api_python/app/data_sources/crypto.py b/backend_api_python/app/data_sources/crypto.py index f7bbf9f..dcd16fa 100644 --- a/backend_api_python/app/data_sources/crypto.py +++ b/backend_api_python/app/data_sources/crypto.py @@ -1,6 +1,6 @@ """ -加密货币数据源 -使用 CCXT (Coinbase) 获取数据 +Cryptocurrency data source +Get data using CCXT (Coinbase) """ from typing import Dict, List, Any, Optional, Tuple from datetime import datetime, timedelta @@ -14,14 +14,14 @@ logger = get_logger(__name__) class CryptoDataSource(BaseDataSource): - """加密货币数据源""" + """Cryptocurrency data source""" name = "Crypto/CCXT" - # 时间周期映射 + # time period mapping TIMEFRAME_MAP = CCXTConfig.TIMEFRAME_MAP - # 常见的报价货币列表(按优先级排序) + # List of common quote currencies (sorted by priority) COMMON_QUOTES = ['USDT', 'USD', 'BTC', 'ETH', 'BUSD', 'USDC', 'BNB', 'EUR', 'GBP'] def __init__(self): @@ -30,7 +30,7 @@ class CryptoDataSource(BaseDataSource): 'enableRateLimit': CCXTConfig.ENABLE_RATE_LIMIT } - # 如果配置了代理 + # If a proxy is configured if CCXTConfig.PROXY: config['proxies'] = { 'http': CCXTConfig.PROXY, @@ -39,7 +39,7 @@ class CryptoDataSource(BaseDataSource): exchange_id = CCXTConfig.DEFAULT_EXCHANGE - # 动态加载交易所类 + # Dynamically loading exchange classes if not hasattr(ccxt, exchange_id): logger.warning(f"CCXT exchange '{exchange_id}' not found, falling back to 'coinbase'") exchange_id = 'coinbase' @@ -47,17 +47,17 @@ class CryptoDataSource(BaseDataSource): exchange_class = getattr(ccxt, exchange_id) self.exchange = exchange_class(config) - # 延迟加载 markets(首次使用时加载) + # Lazy loading of markets (loaded on first use) self._markets_loaded = False self._markets_cache = None def _ensure_markets_loaded(self) -> bool: - """确保 markets 已加载(用于符号验证)""" + """Make sure markets are loaded (for symbol verification)""" if self._markets_loaded and self._markets_cache is not None: return True try: - # 某些交易所需要显式加载 markets + # Some exchanges require explicit loading of markets if hasattr(self.exchange, 'load_markets'): self.exchange.load_markets(reload=False) self._markets_cache = getattr(self.exchange, 'markets', {}) @@ -69,13 +69,13 @@ class CryptoDataSource(BaseDataSource): def _normalize_symbol(self, symbol: str) -> Tuple[str, str]: """ - 规范化符号格式,返回 (normalized_symbol, base_currency) + Normalized symbol format, returns (normalized_symbol, base_currency) - 处理各种输入格式: + Handles various input formats: - BTC/USDT -> BTC/USDT - BTCUSDT -> BTC/USDT - BTC/USDT:USDT -> BTC/USDT - - BTC -> BTC/USDT (默认) + - BTC -> BTC/USDT (default) - PI, TRX -> PI/USDT, TRX/USDT """ if not symbol: @@ -83,13 +83,13 @@ class CryptoDataSource(BaseDataSource): sym = symbol.strip() - # 移除 swap/futures 后缀 + # Remove swap/futures suffix if ':' in sym: sym = sym.split(':', 1)[0] sym = sym.upper() - # 如果已经有分隔符,直接解析 + # If there is already a separator, parse it directly if '/' in sym: parts = sym.split('/', 1) base = parts[0].strip() @@ -97,26 +97,26 @@ class CryptoDataSource(BaseDataSource): if base and quote: return f"{base}/{quote}", base - # 尝试从常见报价货币中识别 + # Try to identify from common quote currencies for quote in self.COMMON_QUOTES: if sym.endswith(quote) and len(sym) > len(quote): base = sym[:-len(quote)] if base: return f"{base}/{quote}", base - # 如果无法识别,默认使用 USDT + # If not recognized, USDT will be used by default. return f"{sym}/USDT", sym def _find_valid_symbol(self, base: str, preferred_quote: str = 'USDT') -> Optional[str]: """ - 在交易所的 markets 中查找有效的符号 + Find valid symbols in the exchange's markets Args: - base: 基础货币(如 'PI', 'TRX') - preferred_quote: 首选的报价货币 + base: base currency (e.g. 'PI', 'TRX') + preferred_quote: preferred quote currency Returns: - 找到的有效符号,如果找不到则返回 None + A valid symbol found, or None if not found """ if not self._ensure_markets_loaded(): return None @@ -125,14 +125,14 @@ class CryptoDataSource(BaseDataSource): if not markets: return None - # 按优先级尝试不同的报价货币 + # Try different quote currencies by priority quotes_to_try = [preferred_quote] + [q for q in self.COMMON_QUOTES if q != preferred_quote] for quote in quotes_to_try: candidate = f"{base}/{quote}" if candidate in markets: market = markets[candidate] - # 检查市场是否活跃 + # Check if the market is active if market.get('active', True): return candidate @@ -140,14 +140,14 @@ class CryptoDataSource(BaseDataSource): def _normalize_symbol_for_exchange(self, symbol: str) -> str: """ - 根据交易所特性规范化符号 + Standardize symbols based on exchange characteristics - 不同交易所的符号格式要求: - - Binance: BTC/USDT (标准格式) - - OKX: BTC/USDT (标准格式,但某些币种可能不支持) - - Coinbase: BTC/USD (通常使用 USD 而不是 USDT) - - Kraken: XBT/USD (BTC 映射为 XBT) - - Bitfinex: tBTCUST (特殊格式) + Symbol format requirements for different exchanges: + - Binance: BTC/USDT (standard format) + - OKX: BTC/USDT (standard format, but some currencies may not be supported) + - Coinbase: BTC/USD (usually use USD instead of USDT) + - Kraken: XBT/USD (BTC is mapped to XBT) + - Bitfinex: tBTCUST (special format) """ normalized, base = self._normalize_symbol(symbol) @@ -156,9 +156,9 @@ class CryptoDataSource(BaseDataSource): exchange_id = getattr(self.exchange, 'id', '').lower() - # 特殊处理:某些交易所的符号映射 + # Special handling: symbol mapping for certain exchanges if exchange_id == 'coinbase': - # Coinbase 通常使用 USD 而不是 USDT + # Coinbase usually uses USD instead of USDT if normalized.endswith('/USDT'): usd_version = normalized.replace('/USDT', '/USD') if self._ensure_markets_loaded(): @@ -166,7 +166,7 @@ class CryptoDataSource(BaseDataSource): if usd_version in markets: return usd_version - # 尝试在交易所中查找有效符号 + # Try to find a valid symbol on the exchange if self._ensure_markets_loaded(): valid_symbol = self._find_valid_symbol(base, normalized.split('/')[1] if '/' in normalized else 'USDT') if valid_symbol: @@ -181,19 +181,19 @@ class CryptoDataSource(BaseDataSource): Accepts common formats: - BTC/USDT, BTCUSDT, BTC/USDT:USDT - PI, TRX (will be normalized and searched across exchanges) - - 自动适配不同交易所的符号格式要求 + - Automatically adapt to the symbol format requirements of different exchanges """ if not symbol or not symbol.strip(): return {'last': 0, 'symbol': symbol} - # 规范化符号 + # normalized notation normalized = self._normalize_symbol_for_exchange(symbol) if not normalized: logger.warning(f"Failed to normalize symbol: {symbol}") return {'last': 0, 'symbol': symbol} - # 尝试获取 ticker + # Try to get ticker try: ticker = self.exchange.fetch_ticker(normalized) if ticker and isinstance(ticker, dict): @@ -209,7 +209,7 @@ class CryptoDataSource(BaseDataSource): ]) if is_symbol_error: - # 尝试查找替代符号 + # Try to find alternative symbols base = normalized.split('/')[0] if '/' in normalized else normalized if self._ensure_markets_loaded(): valid_symbol = self._find_valid_symbol(base) @@ -222,7 +222,7 @@ class CryptoDataSource(BaseDataSource): except Exception as e2: logger.debug(f"Alternative symbol {valid_symbol} also failed: {e2}") - # 如果所有尝试都失败,记录警告并返回默认值 + # If all attempts fail, log a warning and return a default value logger.warning( f"Symbol '{symbol}' (normalized: {normalized}) not found on {self.exchange.id}. " f"Error: {str(e)[:100]}" @@ -237,20 +237,20 @@ class CryptoDataSource(BaseDataSource): limit: int, before_time: Optional[int] = None ) -> List[Dict[str, Any]]: - """获取加密货币K线数据""" + """Get cryptocurrency K-line data""" klines = [] try: ccxt_timeframe = self.TIMEFRAME_MAP.get(timeframe, '1d') - # 使用统一的符号规范化方法 + # Use a unified symbol normalization method symbol_pair = self._normalize_symbol_for_exchange(symbol) if not symbol_pair: logger.warning(f"Failed to normalize symbol for K-line: {symbol}") return [] - # logger.info(f"获取加密货币K线: {symbol_pair}, 周期: {ccxt_timeframe}, 条数: {limit}") + # logger.info(f"Get cryptocurrency K-line: {symbol_pair}, period: {ccxt_timeframe}, number of bars: {limit}") ohlcv = self._fetch_ohlcv(symbol_pair, ccxt_timeframe, limit, before_time, timeframe) @@ -258,12 +258,12 @@ class CryptoDataSource(BaseDataSource): logger.warning(f"CCXT returned no K-lines: {symbol_pair}") return [] - # 转换数据格式 + # Convert data format for candle in ohlcv: if len(candle) < 6: continue klines.append(self.format_kline( - timestamp=int(candle[0] / 1000), # 毫秒转秒 + timestamp=int(candle[0] / 1000), # Milliseconds to seconds open_price=candle[1], high=candle[2], low=candle[3], @@ -271,10 +271,10 @@ class CryptoDataSource(BaseDataSource): volume=candle[5] )) - # 过滤和限制 + # Filter and restrict klines = self.filter_and_limit(klines, limit, before_time) - # 记录结果 + # Record results self.log_result(symbol, klines, timeframe) except Exception as e: @@ -292,19 +292,19 @@ class CryptoDataSource(BaseDataSource): before_time: Optional[int], timeframe: str ) -> List: - """获取OHLCV数据(支持分页获取完整数据)""" + """Obtain OHLCV data (supports paging to obtain complete data)""" try: if before_time: - # 计算时间范围 + # Calculation time range total_seconds = self.calculate_time_range(timeframe, limit) end_time = datetime.fromtimestamp(before_time) start_time = end_time - timedelta(seconds=total_seconds) since = int(start_time.timestamp() * 1000) end_ms = before_time * 1000 - # logger.info(f"历史数据请求: since={since//1000}, end={before_time}, 时间跨度={total_seconds/86400:.1f}天") + # logger.info(f"Historical data request: since={since//1000}, end={before_time}, time span={total_seconds/86400:.1f} days") - # 分页获取数据,直到覆盖完整时间范围 + # Fetch data in pages until the complete time range is covered all_ohlcv = [] batch_limit = 300 # Coinbase limit is often 300, safer than 1000 current_since = since @@ -322,25 +322,25 @@ class CryptoDataSource(BaseDataSource): all_ohlcv.extend(batch) - # 获取最后一条数据的时间,作为下次请求的起始时间 + # The time when the last piece of data is obtained is used as the starting time of the next request last_timestamp = batch[-1][0] - # 如果最后一条数据时间超过了结束时间,或者返回数据少于请求量,说明已经获取完毕 + # If the time of the last data exceeds the end time, or the returned data is less than the requested amount, it means that the acquisition has been completed. # if last_timestamp >= end_ms or len(batch) < batch_limit: if last_timestamp >= end_ms: break - # 下次从最后一条的下一个时间点开始 + # Next time, start from the next time point of the last item timeframe_ms = TIMEFRAME_SECONDS.get(timeframe, 86400) * 1000 current_since = last_timestamp + timeframe_ms - # logger.info(f"分页获取中: 已获取 {len(all_ohlcv)} 条, 继续从 {datetime.fromtimestamp(current_since/1000)}") + # logger.info(f"Getting in paging: {len(all_ohlcv)} items have been obtained, continue from {datetime.fromtimestamp(current_since/1000)}") ohlcv = all_ohlcv else: ohlcv = self.exchange.fetch_ohlcv(symbol_pair, ccxt_timeframe, limit=limit) - # logger.info(f"CCXT 返回 {len(ohlcv) if ohlcv else 0} 条数据") + # logger.info(f"CCXT returns {len(ohlcv) if ohlcv else 0} pieces of data") return ohlcv except Exception as e: @@ -355,7 +355,7 @@ class CryptoDataSource(BaseDataSource): before_time: Optional[int], timeframe: str ) -> List: - """备用获取方法""" + """Alternate acquisition method""" try: total_seconds = self.calculate_time_range(timeframe, limit) @@ -367,7 +367,7 @@ class CryptoDataSource(BaseDataSource): since = int((datetime.now() - timedelta(seconds=total_seconds)).timestamp() * 1000) ohlcv = self.exchange.fetch_ohlcv(symbol_pair, ccxt_timeframe, since=since, limit=limit) - # logger.info(f"CCXT 备用方法返回 {len(ohlcv) if ohlcv else 0} 条数据") + # logger.info(f"CCXT alternative method returns {len(ohlcv) if ohlcv else 0} pieces of data") return ohlcv except Exception as e: logger.error(f"CCXT fallback method also failed: {str(e)}") diff --git a/backend_api_python/app/data_sources/factory.py b/backend_api_python/app/data_sources/factory.py index 826bf4d..ff71397 100644 --- a/backend_api_python/app/data_sources/factory.py +++ b/backend_api_python/app/data_sources/factory.py @@ -1,6 +1,6 @@ """ -数据源工厂 -根据市场类型返回对应的数据源 +data source factory +Return the corresponding data source according to the market type """ from typing import Dict, List, Any, Optional @@ -11,20 +11,20 @@ logger = get_logger(__name__) class DataSourceFactory: - """数据源工厂""" + """data source factory""" _sources: Dict[str, BaseDataSource] = {} @classmethod def get_source(cls, market: str) -> BaseDataSource: """ - 获取指定市场的数据源 + Get the data source for the specified market Args: - market: 市场类型 (Crypto, USStock, Forex, Futures) + market: market type (Crypto, USStock, Forex, Futures) Returns: - 数据源实例 + Data source instance """ if market not in cls._sources: cls._sources[market] = cls._create_source(market) @@ -48,7 +48,7 @@ class DataSourceFactory: @classmethod def _create_source(cls, market: str) -> BaseDataSource: - """创建数据源实例""" + """Create data source instance""" if market == 'Crypto': from app.data_sources.crypto import CryptoDataSource return CryptoDataSource() @@ -74,23 +74,23 @@ class DataSourceFactory: before_time: Optional[int] = None ) -> List[Dict[str, Any]]: """ - 获取K线数据的便捷方法 + A convenient way to obtain K-line data Args: - market: 市场类型 - symbol: 交易对/股票代码 - timeframe: 时间周期 - limit: 数据条数 - before_time: 获取此时间之前的数据 + market: market type + symbol: trading pair/stock code + timeframe: time period + limit: number of data items + before_time: Get data before this time Returns: - K线数据列表 + K-line data list """ try: source = cls.get_source(market) klines = source.get_kline(symbol, timeframe, limit, before_time) - # 确保数据按时间排序 + # Make sure the data is sorted by time klines.sort(key=lambda x: x['time']) return klines @@ -101,17 +101,17 @@ class DataSourceFactory: @classmethod def get_ticker(cls, market: str, symbol: str) -> Dict[str, Any]: """ - 获取实时报价的便捷方法 + The convenient way to get realtime quotes Args: - market: 市场类型 - symbol: 交易对/股票代码 + market: market type + symbol: trading pair/stock code Returns: - 实时报价数据: { - 'last': 最新价, - 'change': 涨跌额, - 'changePercent': 涨跌幅, + Real-time quotation data: { + 'last': latest price, + 'change': change amount, + 'changePercent': increase or decrease, ... } """ diff --git a/backend_api_python/app/data_sources/forex.py b/backend_api_python/app/data_sources/forex.py index 2e17843..01b6827 100644 --- a/backend_api_python/app/data_sources/forex.py +++ b/backend_api_python/app/data_sources/forex.py @@ -1,6 +1,6 @@ """ -外汇数据源 -使用 Tiingo 获取外汇数据 +Forex data source +Get Forex Data with Tiingo """ from typing import Dict, List, Any, Optional from datetime import datetime, timedelta @@ -14,39 +14,39 @@ from app.config import TiingoConfig, APIKeys logger = get_logger(__name__) -# 全局缓存 - 减少 Tiingo API 调用 +# Global Cache - Reduce Tiingo API calls _forex_cache: Dict[str, Dict[str, Any]] = {} _forex_cache_lock = threading.Lock() -_FOREX_CACHE_TTL = 60 # 外汇价格缓存 60 秒 (Tiingo 免费 API 限制严格) +_FOREX_CACHE_TTL = 60 # Forex price caching for 60 seconds (Tiingo free API has strict limits) class ForexDataSource(BaseDataSource): - """外汇数据源 (Tiingo)""" + """Forex data source (Tiingo)""" name = "Forex/Tiingo" - # Tiingo resampleFreq 映射 - # Tiingo 免费账户支持: 5min, 15min, 30min, 1hour, 4hour, 1day - # 注意: 1min 需要付费订阅, 1week/1month 不被 Tiingo FX API 支持 + # Tiingo resampleFreq mapping + # Tiingo free account support: 5min, 15min, 30min, 1hour, 4hour, 1day + # Note: 1min requires paid subscription, 1week/1month is not supported by Tiingo FX API TIMEFRAME_MAP = { - '1m': '1min', # 需要付费订阅 + '1m': '1min', # Paid subscription required '5m': '5min', '15m': '15min', '30m': '30min', '1H': '1hour', '4H': '4hour', '1D': '1day', - '1W': None, # Tiingo 不支持,需要聚合 - '1M': None # Tiingo 不支持,需要聚合 + '1W': None, # Tiingo does not support it and needs to be aggregated. + '1M': None # Tiingo does not support it and needs to be aggregated. } - # 外汇对映射 (Tiingo 使用标准 ticker,如 eurusd, audusd) - # 大写也可以,Tiingo 通常不区分大小写,但建议统一 + # Forex pair mapping (Tiingo uses standard tickers such as eurusd, audusd) + # Uppercase letters are also acceptable. Tiingo is usually not case-sensitive, but uniformity is recommended. SYMBOL_MAP = { - # 贵金属 (Tiingo 不一定支持所有 OANDA 格式的贵金属,通常是 XAUUSD) + # Precious metals (Tiingo does not necessarily support all precious metals in OANDA format, usually XAUUSD) 'XAUUSD': 'xauusd', 'XAGUSD': 'xagusd', - # 主要货币对 + # major currency pairs 'EURUSD': 'eurusd', 'GBPUSD': 'gbpusd', 'USDJPY': 'usdjpy', @@ -63,18 +63,18 @@ class ForexDataSource(BaseDataSource): def get_ticker(self, symbol: str) -> Dict[str, Any]: """ - 获取外汇实时报价 + Get realtime quotes for foreign exchange - 使用 Tiingo FX Top-of-Book API 获取实时报价 - 带有 60 秒缓存以避免频繁触发 Tiingo 速率限制 + Get realtime quotes using the Tiingo FX Top-of-Book API + Comes with 60 second cache to avoid triggering Tiingo rate limit frequently Returns: dict: { - 'last': 当前价格 (mid price), - 'bid': 买价, - 'ask': 卖价, - 'change': 涨跌额, - 'changePercent': 涨跌幅 + 'last': current price (mid price), + 'bid': buying price, + 'ask': selling price, + 'change': change amount, + 'changePercent': increase or decrease } """ api_key = APIKeys.TIINGO_API_KEY @@ -82,7 +82,7 @@ class ForexDataSource(BaseDataSource): logger.warning("Tiingo API key not configured") return {'last': 0, 'symbol': symbol} - # 检查缓存 + # Check cache cache_key = f"ticker_{symbol}" with _forex_cache_lock: cached = _forex_cache.get(cache_key) @@ -93,7 +93,7 @@ class ForexDataSource(BaseDataSource): return cached try: - # 解析 symbol + # parse symbol tiingo_symbol = self.SYMBOL_MAP.get(symbol) if not tiingo_symbol: tiingo_symbol = symbol.lower() @@ -106,7 +106,7 @@ class ForexDataSource(BaseDataSource): 'token': api_key } - # 重试逻辑:处理 429 速率限制 + # Retry logic: Handling 429 rate limiting for attempt in range(3): response = requests.get(url, params=params, timeout=TiingoConfig.TIMEOUT) if response.status_code == 429: @@ -119,7 +119,7 @@ class ForexDataSource(BaseDataSource): if response.status_code == 429: logger.warning("Tiingo rate limit exceeded for ticker request") logger.info("Note: Tiingo 1-minute forex data requires a paid subscription") - # 返回缓存数据(如果有的话,即使已过期) + # Return cached data (if available, even if expired) with _forex_cache_lock: if cache_key in _forex_cache: logger.info(f"Returning stale cache for {symbol} due to rate limit") @@ -136,19 +136,19 @@ class ForexDataSource(BaseDataSource): ask = float(item.get('askPrice', 0) or 0) mid = float(item.get('midPrice', 0) or 0) - # 如果没有 midPrice,计算中间价 + # If there is no midPrice, calculate the mid price if not mid and bid and ask: mid = (bid + ask) / 2 last_price = mid or bid or ask - # 获取前一天收盘价来计算涨跌(需要额外请求日线数据) + # Get the closing price of the previous day to calculate the rise and fall (additional request for daily data is required) prev_close = 0 change = 0 change_pct = 0 try: - # 获取昨日收盘价 + # Get yesterday's closing price yesterday = (datetime.now() - timedelta(days=2)).strftime('%Y-%m-%d') today = datetime.now().strftime('%Y-%m-%d') price_url = f"{self.base_url}/fx/{tiingo_symbol}/prices" @@ -167,7 +167,7 @@ class ForexDataSource(BaseDataSource): change = last_price - prev_close change_pct = (change / prev_close) * 100 except Exception: - pass # 涨跌计算失败不影响主要功能 + pass # Failure to calculate the rise or fall does not affect the main functions result = { 'last': round(last_price, 5), @@ -179,7 +179,7 @@ class ForexDataSource(BaseDataSource): '_cache_time': time.time() } - # 缓存结果 + # cache results with _forex_cache_lock: _forex_cache[cache_key] = result @@ -191,7 +191,7 @@ class ForexDataSource(BaseDataSource): return {'last': 0, 'symbol': symbol} def _get_timeframe_seconds(self, timeframe: str) -> int: - """获取时间周期对应的秒数""" + """Get the number of seconds corresponding to the time period""" return TIMEFRAME_SECONDS.get(timeframe, 86400) def get_kline( @@ -202,80 +202,80 @@ class ForexDataSource(BaseDataSource): before_time: Optional[int] = None ) -> List[Dict[str, Any]]: """ - 获取外汇K线数据 + Get foreign exchange K-line data Args: - symbol: 外汇对代码(如 XAUUSD, EURUSD) - timeframe: 时间周期 - limit: 数据条数 - before_time: 结束时间戳 + symbol: Forex pair symbol (such as XAUUSD, EURUSD) + timeframe: time period + limit: number of data items + before_time: end timestamp """ - # 动态获取 API Key + # Dynamically obtain API Key api_key = APIKeys.TIINGO_API_KEY if not api_key: logger.error("Tiingo API key is not configured") return [] try: - # 1. 解析 Symbol + # 1. Parse Symbol tiingo_symbol = self.SYMBOL_MAP.get(symbol) if not tiingo_symbol: - # 尝试智能转换: EURUSD -> eurusd + # Try smart conversion: EURUSD -> eurusd tiingo_symbol = symbol.lower() - # 2. 解析 Resolution (resampleFreq) + # 2. Analysis Resolution (resampleFreq) resample_freq = self.TIMEFRAME_MAP.get(timeframe) - # 特殊处理:1W/1M 需要用日线聚合 + # Special treatment: 1W/1M requires daily aggregation aggregate_to_weekly = (timeframe == '1W') aggregate_to_monthly = (timeframe == '1M') - original_limit = limit # 保存原始请求数量 + original_limit = limit # Save original request quantity if aggregate_to_weekly or aggregate_to_monthly: - # 用日线数据聚合 + # Aggregate using daily data resample_freq = '1day' - # 限制周线/月线的最大请求数量(Tiingo 免费 API 有数据量限制) - # 周线最多请求 100 周 = 700 天 ≈ 2年 - # 月线最多请求 36 月 = 1080 天 ≈ 3年 + # Limit the maximum number of weekly/monthly requests (Tiingo free API has data volume limit) + # The maximum weekly request is 100 weeks = 700 days ≈ 2 years + # The maximum monthly request is 36 months = 1080 days ≈ 3 years max_limit = 100 if aggregate_to_weekly else 36 original_limit = min(original_limit, max_limit) - # 需要更多日线数据来聚合(周线需要7天,月线需要30天) + # More daily data is needed to aggregate (weekly lines require 7 days, monthly lines require 30 days) limit = original_limit * (7 if aggregate_to_weekly else 30) if not resample_freq: logger.warning(f"Tiingo does not support timeframe: {timeframe}") return [] - # 1分钟数据需要付费订阅提示 + # 1 minute data requires paid subscription reminder if timeframe == '1m': logger.info(f"Note: Tiingo 1-minute forex data requires a paid subscription") - # 3. 计算时间范围 + # 3. Calculation time range if before_time: end_dt = datetime.fromtimestamp(before_time) else: end_dt = datetime.now() - # 根据周期和数量计算开始时间 - # 注意:聚合模式下使用日线秒数计算 + # Calculate start time based on period and quantity + # Note: Use daily seconds calculation in aggregation mode if aggregate_to_weekly or aggregate_to_monthly: - tf_seconds = 86400 # 日线秒数 + tf_seconds = 86400 # daily seconds else: tf_seconds = self._get_timeframe_seconds(timeframe) - # 多取一些缓冲时间(1.5倍,外汇周末不交易) + # Get more buffer time (1.5 times, foreign exchange does not trade on weekends) start_dt = end_dt - timedelta(seconds=limit * tf_seconds * 1.5) - # Tiingo 免费 API 最多支持约 5 年数据,限制最大时间范围 - max_days = 365 * 3 # 最多 3 年 + # Tiingo free API supports up to about 5 years of data, limiting the maximum time range + max_days = 365 * 3 # up to 3 years if (end_dt - start_dt).days > max_days: start_dt = end_dt - timedelta(days=max_days) logger.info(f"Tiingo: Limited date range to {max_days} days") - # 格式化日期为 YYYY-MM-DD (Tiingo 支持该格式) + # Format the date as YYYY-MM-DD (Tiingo supports this format) start_date_str = start_dt.strftime('%Y-%m-%d') end_date_str = end_dt.strftime('%Y-%m-%d') - # 4. API 请求(带重试逻辑) + # 4. API request (with retry logic) # URL: https://api.tiingo.com/tiingo/fx/{ticker}/prices url = f"{self.base_url}/fx/{tiingo_symbol}/prices" @@ -289,9 +289,9 @@ class ForexDataSource(BaseDataSource): # logger.info(f"Tiingo Request: {url} params={params}") - # 重试逻辑:处理 429 速率限制 + # Retry logic: Handling 429 rate limiting max_retries = 3 - retry_delay = 2 # 秒 + retry_delay = 2 # Second response = None for attempt in range(max_retries): @@ -299,13 +299,13 @@ class ForexDataSource(BaseDataSource): response = requests.get(url, params=params, timeout=TiingoConfig.TIMEOUT) if response.status_code == 429: - # 速率限制,等待后重试 + # Rate limit, wait and try again wait_time = retry_delay * (attempt + 1) logger.warning(f"Tiingo rate limit (429), waiting {wait_time}s before retry ({attempt + 1}/{max_retries})") time.sleep(wait_time) continue - break # 成功或其他错误,退出重试循环 + break # Success or other errors, exit the retry loop except requests.exceptions.Timeout: if attempt < max_retries - 1: @@ -329,7 +329,7 @@ class ForexDataSource(BaseDataSource): response.raise_for_status() data = response.json() - # 5. 处理响应 + # 5. Process the response # Tiingo returns a list of dicts: # [ # { @@ -350,15 +350,15 @@ class ForexDataSource(BaseDataSource): klines = [] for item in data: - # 解析时间: "2023-01-01T00:00:00.000Z" + # Parsing time: "2023-01-01T00:00:00.000Z" dt_str = item.get('date') - # Tiingo 返回的是 UTC 时间 ISO 格式,需要正确处理时区 - # 将 UTC 时间转换为本地时间戳 + # Tiingo returns UTC time in ISO format and needs to handle the time zone correctly. + # Convert UTC time to local timestamp if dt_str.endswith('Z'): - dt_str = dt_str[:-1] + '+00:00' # 替换 Z 为 +00:00 表示 UTC + dt_str = dt_str[:-1] + '+00:00' # Replace Z with +00:00 for UTC dt = datetime.fromisoformat(dt_str) - ts = int(dt.timestamp()) # 现在会正确处理 UTC 时区 + ts = int(dt.timestamp()) # UTC time zone is now handled correctly klines.append({ 'time': ts, @@ -366,13 +366,13 @@ class ForexDataSource(BaseDataSource): 'high': float(item.get('high')), 'low': float(item.get('low')), 'close': float(item.get('close')), - 'volume': 0.0 # Tiingo FX 通常没有 volume + 'volume': 0.0 # Tiingo FX usually does not have volume }) - # 按时间排序 + # Sort by time klines.sort(key=lambda x: x['time']) - # 如果需要聚合到周线或月线 + # If you need to aggregate to weekly or monthly lines if aggregate_to_weekly: klines = self._aggregate_to_weekly(klines) logger.debug(f"Aggregated {len(klines)} weekly candles from daily data") @@ -380,11 +380,11 @@ class ForexDataSource(BaseDataSource): klines = self._aggregate_to_monthly(klines) logger.debug(f"Aggregated {len(klines)} monthly candles from daily data") - # 过滤到原始请求数量 + # Filter to original request count if len(klines) > original_limit: klines = klines[-original_limit:] - # logger.info(f"获取到 {len(klines)} 条 Tiingo 外汇数据") + # logger.info(f"obtained {len(klines)} pieces of Tiingo foreign exchange data") return klines except requests.exceptions.RequestException as e: @@ -395,7 +395,7 @@ class ForexDataSource(BaseDataSource): return [] def _aggregate_to_weekly(self, daily_klines: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """将日线数据聚合为周线""" + """Aggregate daily data into weekly data""" if not daily_klines: return [] @@ -405,15 +405,15 @@ class ForexDataSource(BaseDataSource): for kline in daily_klines: dt = datetime.fromtimestamp(kline['time']) - # 获取该日期所在周的周一 + # Get the Monday of the week in which the date is located week_start = dt - timedelta(days=dt.weekday()) week_key = week_start.strftime('%Y-%W') if week_key != current_week: - # 保存上一周的数据 + # Save data from last week if week_data: weekly_klines.append(week_data) - # 开始新的一周 + # start a new week current_week = week_key week_data = { 'time': int(week_start.timestamp()), @@ -424,20 +424,20 @@ class ForexDataSource(BaseDataSource): 'volume': kline['volume'] } else: - # 更新本周数据 + # Update this week's data week_data['high'] = max(week_data['high'], kline['high']) week_data['low'] = min(week_data['low'], kline['low']) week_data['close'] = kline['close'] week_data['volume'] += kline['volume'] - # 添加最后一周 + # Add last week if week_data: weekly_klines.append(week_data) return weekly_klines def _aggregate_to_monthly(self, daily_klines: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """将日线数据聚合为月线""" + """Aggregate daily data into monthly data""" if not daily_klines: return [] @@ -450,10 +450,10 @@ class ForexDataSource(BaseDataSource): month_key = dt.strftime('%Y-%m') if month_key != current_month: - # 保存上个月的数据 + # Save last month’s data if month_data: monthly_klines.append(month_data) - # 开始新的一月 + # start a new month current_month = month_key month_start = dt.replace(day=1, hour=0, minute=0, second=0) month_data = { @@ -465,13 +465,13 @@ class ForexDataSource(BaseDataSource): 'volume': kline['volume'] } else: - # 更新本月数据 + # Update this month's data month_data['high'] = max(month_data['high'], kline['high']) month_data['low'] = min(month_data['low'], kline['low']) month_data['close'] = kline['close'] month_data['volume'] += kline['volume'] - # 添加最后一月 + # Add last month if month_data: monthly_klines.append(month_data) diff --git a/backend_api_python/app/data_sources/futures.py b/backend_api_python/app/data_sources/futures.py index 29ff142..dc0aa16 100644 --- a/backend_api_python/app/data_sources/futures.py +++ b/backend_api_python/app/data_sources/futures.py @@ -1,8 +1,8 @@ """ -期货数据源 -支持: -1. 加密货币期货(Binance Futures via CCXT) -2. 传统期货(Yahoo Finance) +Futures data source +support: +1. Cryptocurrency Futures (Binance Futures via CCXT) +2. Traditional futures (Yahoo Finance) """ from typing import Dict, List, Any, Optional from datetime import datetime, timedelta @@ -17,11 +17,11 @@ logger = get_logger(__name__) class FuturesDataSource(BaseDataSource): - """期货数据源""" + """Futures data source""" name = "Futures" - # Yahoo Finance时间周期映射 + # Yahoo Finance time period mapping YF_TIMEFRAME_MAP = { '1m': '1m', '5m': '5m', @@ -33,21 +33,21 @@ class FuturesDataSource(BaseDataSource): '1W': '1wk' } - # CCXT时间周期映射 + # CCXT time period mapping CCXT_TIMEFRAME_MAP = CCXTConfig.TIMEFRAME_MAP - # 传统期货合约代码(Yahoo Finance) + # Traditional futures contract code (Yahoo Finance) YF_SYMBOLS = { - 'GC': 'GC=F', # 黄金期货 - 'SI': 'SI=F', # 白银期货 - 'CL': 'CL=F', # 原油期货 - 'NG': 'NG=F', # 天然气期货 - 'ZC': 'ZC=F', # 玉米期货 - 'ZW': 'ZW=F', # 小麦期货 + 'GC': 'GC=F', # gold futures + 'SI': 'SI=F', # Silver futures + 'CL': 'CL=F', # Crude oil futures + 'NG': 'NG=F', # Natural gas futures + 'ZC': 'ZC=F', # Corn futures + 'ZW': 'ZW=F', # Wheat futures } def __init__(self): - # 初始化CCXT(用于加密货币期货) + # Initialize CCXT (for cryptocurrency futures) config = { 'timeout': CCXTConfig.TIMEOUT, 'enableRateLimit': CCXTConfig.ENABLE_RATE_LIMIT, @@ -103,7 +103,7 @@ class FuturesDataSource(BaseDataSource): return self.exchange.fetch_ticker(sym) def _get_timeframe_seconds(self, timeframe: str) -> int: - """获取时间周期对应的秒数""" + """Get the number of seconds corresponding to the time period""" return TIMEFRAME_SECONDS.get(timeframe, 86400) def get_kline( @@ -114,15 +114,15 @@ class FuturesDataSource(BaseDataSource): before_time: Optional[int] = None ) -> List[Dict[str, Any]]: """ - 获取期货K线数据 + Get futures K-line data Args: - symbol: 期货合约代码 - timeframe: 时间周期 - limit: 数据条数 - before_time: 结束时间戳 + symbol: futures contract code + timeframe: time period + limit: number of data items + before_time: end timestamp """ - # 判断是传统期货还是加密货币期货 + # Determine whether it is traditional futures or cryptocurrency futures if symbol in self.YF_SYMBOLS or symbol.endswith('=F'): return self._get_traditional_futures(symbol, timeframe, limit, before_time) else: @@ -135,19 +135,19 @@ class FuturesDataSource(BaseDataSource): limit: int, before_time: Optional[int] = None ) -> List[Dict[str, Any]]: - """使用yfinance获取传统期货数据""" + """Use yfinance to obtain traditional futures data""" try: - # 转换symbol格式 + # Convert symbol format yf_symbol = self.YF_SYMBOLS.get(symbol, symbol) if not yf_symbol.endswith('=F'): yf_symbol = symbol + '=F' - # 转换时间周期 + # conversion time period yf_interval = self.YF_TIMEFRAME_MAP.get(timeframe, '1d') - # logger.info(f"获取传统期货K线: {yf_symbol}, 周期: {yf_interval}, 条数: {limit}") + # logger.info(f"Get traditional futures K-line: {yf_symbol}, period: {yf_interval}, number of bars: {limit}") - # 计算时间范围 + # Calculation time range if before_time: end_time = datetime.fromtimestamp(before_time) else: @@ -156,10 +156,10 @@ class FuturesDataSource(BaseDataSource): tf_seconds = self._get_timeframe_seconds(timeframe) start_time = end_time - timedelta(seconds=tf_seconds * limit * 1.5) - # yfinance 的 end 参数是不包含的(exclusive),需要加一天 + # The end parameter of yfinance is not included (exclusive), and one day needs to be added. end_time_inclusive = end_time + timedelta(days=1) - # 获取数据 + # Get data ticker = yf.Ticker(yf_symbol) df = ticker.history( start=start_time, @@ -171,7 +171,7 @@ class FuturesDataSource(BaseDataSource): logger.warning(f"No data: {yf_symbol}") return [] - # 转换格式 + # Convert format klines = [] for index, row in df.iterrows(): klines.append({ @@ -187,7 +187,7 @@ class FuturesDataSource(BaseDataSource): if len(klines) > limit: klines = klines[-limit:] - # logger.info(f"获取到 {len(klines)} 条传统期货数据") + # logger.info(f"obtained {len(klines)} pieces of traditional futures data") return klines except Exception as e: @@ -201,15 +201,15 @@ class FuturesDataSource(BaseDataSource): limit: int, before_time: Optional[int] = None ) -> List[Dict[str, Any]]: - """使用CCXT获取加密货币期货数据""" + """Obtain cryptocurrency futures data using CCXT""" try: - # 确保symbol格式正确 + # Make sure the symbol format is correct ccxt_symbol = symbol if '/' in symbol else f"{symbol}/USDT" ccxt_timeframe = self.CCXT_TIMEFRAME_MAP.get(timeframe, '1d') - # logger.info(f"获取加密货币期货K线: {ccxt_symbol}, 周期: {ccxt_timeframe}, 条数: {limit}") + # logger.info(f"Get cryptocurrency futures K-line: {ccxt_symbol}, period: {ccxt_timeframe}, number of bars: {limit}") - # 获取数据 + # Get data if before_time: since_time = before_time - limit * self._get_timeframe_seconds(timeframe) ohlcv = self.exchange.fetch_ohlcv( @@ -225,7 +225,7 @@ class FuturesDataSource(BaseDataSource): limit=limit ) - # 转换格式 + # Convert format klines = [] for candle in ohlcv: klines.append({ @@ -237,7 +237,7 @@ class FuturesDataSource(BaseDataSource): 'volume': float(candle[5]) }) - # logger.info(f"获取到 {len(klines)} 条加密货币期货数据") + # logger.info(f"obtained {len(klines)} pieces of cryptocurrency futures data") return klines except Exception as e: diff --git a/backend_api_python/app/data_sources/polymarket.py b/backend_api_python/app/data_sources/polymarket.py index 9aeace3..ff75fb0 100644 --- a/backend_api_python/app/data_sources/polymarket.py +++ b/backend_api_python/app/data_sources/polymarket.py @@ -1,6 +1,6 @@ """ -Polymarket预测市场数据源 -从Polymarket获取预测市场数据 +Polymarket prediction market data source +Get prediction market data from Polymarket """ import time import requests @@ -15,17 +15,17 @@ logger = get_logger(__name__) class PolymarketDataSource: - """Polymarket预测市场数据源""" + """Polymarket prediction market data source""" def __init__(self): - # Polymarket官方API端点(根据官方文档) - # Gamma API: 市场、事件、标签、搜索等(完全公开,无需认证) + # Polymarket official API endpoint (according to official documentation) + # Gamma API: markets, events, tags, searches, etc. (fully public, no authentication required) self.gamma_api = "https://gamma-api.polymarket.com" - # Data API: 用户持仓、交易、活动等(完全公开,无需认证) + # Data API: User positions, transactions, activities, etc. (fully public, no authentication required) self.data_api = "https://data-api.polymarket.com" - # CLOB API: 订单簿、价格、交易操作(公开端点无需认证) + # CLOB API: Order book, prices, trading operations (public endpoints require no authentication) self.clob_api = "https://clob.polymarket.com" - self.cache_ttl = 300 # 5分钟缓存 + self.cache_ttl = 300 # 5 minutes cache self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', @@ -34,36 +34,36 @@ class PolymarketDataSource: def get_trending_markets(self, category: str = None, limit: int = 50) -> List[Dict]: """ - 获取热门预测市场 + Get popular prediction markets Args: - category: 类别筛选 (crypto, politics, economics, sports, all) - limit: 返回数量限制 + category: category filter (crypto, politics, economics, sports, all) + limit: return quantity limit Returns: - 预测市场列表 + Prediction market list """ try: - # 先从数据库缓存读取 + # Read from database cache first cached = self._get_cached_markets(category, limit) if cached: return cached - # 从真实API获取 - 获取多个分类的数据以确保多样性 + # Fetch from real API - Fetch data from multiple categories to ensure diversity all_markets = [] if category and category != "all": - # 如果指定了类别,只获取该类别的数据 + # If a category is specified, only data for that category will be obtained markets = self._fetch_markets_from_api(category, limit * 2) all_markets.extend(markets) else: - # 如果没有指定类别或指定了"all",获取多个分类的数据 + # If no category is specified or "all" is specified, get data from multiple categories categories_to_fetch = ["crypto", "politics", "economics", "sports"] for cat in categories_to_fetch: markets = self._fetch_markets_from_api(cat, limit // len(categories_to_fetch) + 10) all_markets.extend(markets) - # 去重(按market_id) + # Deduplication (by market_id) seen = set() unique_markets = [] for market in all_markets: @@ -72,15 +72,15 @@ class PolymarketDataSource: seen.add(market_id) unique_markets.append(market) - # 按交易量排序 + # Sort by transaction volume unique_markets.sort(key=lambda x: x.get('volume_24h', 0), reverse=True) - # 保存到数据库缓存 + # Save to database cache if unique_markets: self._save_markets_to_db(unique_markets) return unique_markets[:limit] - # 如果API失败,返回空列表(不再使用示例数据) + # If the API fails, an empty list is returned (sample data is no longer used) logger.warning("Polymarket API unavailable, returning empty list") return [] @@ -89,15 +89,15 @@ class PolymarketDataSource: return [] def get_market_details(self, market_id: str) -> Optional[Dict]: - """获取单个市场详情""" + """Get individual market details""" try: - # 确保market_id是字符串 + # Make sure market_id is a string market_id = str(market_id).strip() if not market_id: logger.warning("Empty market_id provided") return None - # 先从数据库读取 + # Read from database first try: with get_db_connection() as db: cur = db.cursor() @@ -111,9 +111,9 @@ class PolymarketDataSource: cur.close() if row: - # RealDictCursor返回字典,使用键访问 + #RealDictCursor returns the dictionary, accessed using keys db_market_id = str(row.get('market_id') or market_id) - # 解析outcome_tokens(可能是JSON字符串) + # Parse outcome_tokens (may be a JSON string) outcome_tokens = {} outcome_tokens_raw = row.get('outcome_tokens') if outcome_tokens_raw: @@ -140,9 +140,9 @@ class PolymarketDataSource: } except Exception as db_error: logger.warning(f"Database query failed for market {market_id}: {db_error}") - # 继续尝试从API获取 + # Continue trying to get it from the API - # 如果数据库没有,从API获取 + # If the database does not exist, get it from the API logger.info(f"Market {market_id} not in database, fetching from API") market = self._fetch_market_from_api(market_id) if market: @@ -161,34 +161,34 @@ class PolymarketDataSource: def get_market_history(self, market_id: str, days: int = 30) -> List[Dict]: """获取市场历史价格数据""" - # 这里需要实现历史数据获取逻辑 - # 暂时返回空列表 + # Here you need to implement historical data acquisition logic + # Temporarily returns an empty list return [] def search_markets(self, keyword: str, limit: int = 20, use_cache: bool = True) -> List[Dict]: """ - 搜索相关预测市场 - 优先从API获取实时数据,数据库仅作为可选缓存 + Search related prediction markets + Priority is given to obtaining real-time data from the API, and the database is only used as an optional cache. Args: - keyword: 搜索关键词 - limit: 返回结果数量限制 - use_cache: 是否使用数据库缓存(AI分析时应设为False以获取最新数据) + keyword: search keyword + limit: limit on the number of returned results + use_cache: whether to use database cache (should be set to False during AI analysis to obtain the latest data) """ try: logger.info(f"Searching Polymarket markets for keyword: '{keyword}' (limit={limit}, use_cache={use_cache})") - # 如果允许使用缓存,先尝试从数据库搜索 + # If caching is allowed, try searching from the database first if use_cache: with get_db_connection() as db: cur = db.cursor() - # 改进搜索:同时搜索question和slug字段,也支持market_id精确匹配 + # Improved search: search question and slug fields at the same time, also support market_id exact matching keyword_lower = keyword.lower() is_numeric = keyword_lower.isdigit() has_hyphens = '-' in keyword_lower if is_numeric: - # 如果是纯数字,可能是market_id,精确匹配 + # If it is a pure number, it may be market_id, an exact match cur.execute(""" SELECT market_id, question, category, current_probability, volume_24h, liquidity, end_date_iso, status, slug @@ -198,7 +198,7 @@ class PolymarketDataSource: LIMIT %s """, (keyword, limit)) elif has_hyphens: - # 如果包含连字符,可能是slug,优先匹配slug + # If it contains a hyphen, it may be a slug, and the slug will be matched first. cur.execute(""" SELECT market_id, question, category, current_probability, volume_24h, liquidity, end_date_iso, status, slug @@ -210,7 +210,7 @@ class PolymarketDataSource: LIMIT %s """, (f"%{keyword}%", f"%{keyword}%", f"%{keyword}%", limit)) else: - # 普通文本搜索 + # Normal text search cur.execute(""" SELECT market_id, question, category, current_probability, volume_24h, liquidity, end_date_iso, status, slug @@ -238,58 +238,58 @@ class PolymarketDataSource: "slug": row.get('slug') if row.get('slug') and not str(row.get('slug', '')).isdigit() else None } for row in rows] - # 直接从Gamma API获取并过滤(AI分析时使用) + # Obtain and filter directly from Gamma API (used during AI analysis) logger.info(f"Fetching from API for keyword '{keyword}' (use_cache={use_cache})...") - # 优化:如果关键词看起来像slug,先尝试直接查询(避免全量获取) + # Optimization: If the keyword looks like a slug, try direct query first (avoid fetching the full amount) import re keyword_lower = keyword.lower().strip() is_slug_like = '-' in keyword_lower and not keyword_lower.isdigit() if is_slug_like: - # 尝试直接通过slug查询(最高效,根据Polymarket API文档) + # Try to query directly through slug (most efficient, according to Polymarket API documentation) direct_market = self._fetch_market_by_slug(keyword_lower) if direct_market: logger.info(f"Found market directly by slug (no need to fetch all markets): {keyword_lower}") return [direct_market] - # 如果直接查询失败,获取更多数据以便有足够的选择空间 - # 进行多次请求以获取更多市场(每次最多100个事件,但每个事件可能包含多个市场) + # If direct query fails, get more data so that there is enough room for selection + # Make multiple requests to get more markets (up to 100 events each time, but each event may contain multiple markets) all_markets = [] - max_requests = 3 # 最多请求3次,获取300个事件(约4500个市场) + max_requests = 3 # Request up to 3 times to get 300 events (about 4500 markets) for page in range(max_requests): page_markets = self._fetch_from_gamma_api(category=None, limit=100) if not page_markets: break all_markets.extend(page_markets) - # 如果已经获取了足够多的市场,可以提前停止 - if len(all_markets) >= 3000: # 最多获取3000个市场 + # If enough markets have been acquired, you can stop early + if len(all_markets) >= 3000: # Get up to 3000 markets break logger.info(f"Fetched page {page + 1}/{max_requests}, total markets: {len(all_markets)}") - # 短暂延迟,避免API限流 + # Short delay to avoid API current limit if page < max_requests - 1: time.sleep(0.5) logger.info(f"Fetched {len(all_markets)} markets from API, filtering for keyword '{keyword}'...") - # 按关键词过滤(支持多个关键词匹配) - # 如果关键词看起来像slug(包含连字符),也尝试匹配slug + # Filter by keyword (supports multiple keyword matching) + # If the keyword looks like a slug (contains a hyphen), also try to match the slug keyword_is_slug = '-' in keyword_lower - # 提取关键词(去除常见停用词和标点) - # 提取关键词:去除标点,保留字母数字和连字符 + # Extract keywords (remove common stop words and punctuation) + # Extract keywords: remove punctuation, retain alphanumeric characters and hyphens keyword_words = re.findall(r'\b\w+\b', keyword_lower) - # 过滤掉太短的词(少于3个字符)和常见停用词 + # Filter out words that are too short (less than 3 characters) and common stop words stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'will', 'would', 'should', 'could', 'may', 'might', 'can', 'must'} keyword_words = [w for w in keyword_words if len(w) >= 3 and w not in stop_words] - # 如果没有提取到关键词,使用原始关键词 + # If no keywords are extracted, use the original keywords if not keyword_words: keyword_words = [keyword_lower] logger.info(f"Extracted keywords: {keyword_words} from '{keyword}'") filtered = [] - scored_markets = [] # 用于存储带评分的结果 - top_candidates = [] # 用于存储接近匹配的候选(用于调试) + scored_markets = [] # Used to store scored results + top_candidates = [] # Used to store close matching candidates (for debugging) for market in all_markets: question = market.get("question", "").lower() @@ -299,7 +299,7 @@ class PolymarketDataSource: score = 0 match_reason = "" - # 1. 完全匹配(最高优先级,分数100) + # 1. Exact match (highest priority, score 100) if keyword_lower in question: score = 100 match_reason = "exact_match_question" @@ -307,7 +307,7 @@ class PolymarketDataSource: score = 100 match_reason = "exact_match_slug" - # 2. 如果关键词看起来像slug,检查slug字段 + # 2. If the keyword looks like a slug, check the slug field if score < 100 and keyword_is_slug: if keyword_lower == slug: score = 100 @@ -316,49 +316,49 @@ class PolymarketDataSource: score = 90 match_reason = "partial_slug_match" - # 3. 如果关键词是纯数字,检查market_id + # 3. If the keyword is a pure number, check market_id if score < 90 and keyword_lower.isdigit(): if keyword_lower == market_id: score = 100 match_reason = "market_id_match" - # 4. 关键词匹配:检查所有关键词是否都在问题中 + # 4. Keyword matching: Check whether all keywords are in the question if score < 90 and keyword_words: - # 计算匹配的关键词数量 + # Calculate the number of matching keywords matched_words = sum(1 for word in keyword_words if word in question or word in slug) if matched_words > 0: - # 匹配率 + # Match rate match_ratio = matched_words / len(keyword_words) - # 降低阈值:从60%降到40%,提高匹配率 + # Lower the threshold: from 60% to 40% to improve the matching rate if match_ratio >= 0.4: - score = int(60 + match_ratio * 30) # 60-90分 + score = int(60 + match_ratio * 30) # 60-90 minutes match_reason = f"keyword_match_{matched_words}/{len(keyword_words)}" else: - # 记录接近匹配的候选(用于调试) + # Log close matching candidates (for debugging) if matched_words >= 1 and len(top_candidates) < 5: top_candidates.append((match_ratio, market.get('question', '')[:80], matched_words, len(keyword_words))) - # 5. 部分匹配:检查关键词的主要部分是否在问题中 + # 5. Partial matching: Check whether the main part of the keyword is in the question if score < 60 and keyword_words: - # 如果关键词包含多个词,尝试匹配主要部分 + # If the keyword contains multiple words, try to match the main part if len(keyword_words) > 1: - # 取前3个最重要的词(通常是名词) + # Take the first 3 most important words (usually nouns) important_words = keyword_words[:3] matched_important = sum(1 for word in important_words if word in question or word in slug) - # 降低要求:至少匹配1个重要词即可 + # Lower the requirement: match at least 1 important word if matched_important >= 1: score = 50 match_reason = f"important_words_match_{matched_important}/{len(important_words)}" - if score >= 50: # 降低最低分数要求从60到50 + if score >= 50: # Lower the minimum score requirement from 60 to 50 scored_markets.append((score, market, match_reason)) logger.debug(f"Matched (score={score}, reason={match_reason}): {market.get('question', '')[:60]}") - # 按分数排序,取前limit个 + # Sort by score, take the first limit scored_markets.sort(key=lambda x: x[0], reverse=True) filtered = [market for score, market, reason in scored_markets[:limit]] - # 输出调试信息 + # Output debugging information if len(scored_markets) == 0 and top_candidates: logger.warning(f"No exact matches found. Top candidates (partial matches):") for ratio, question, matched, total in top_candidates: @@ -379,7 +379,7 @@ class PolymarketDataSource: with get_db_connection() as db: cur = db.cursor() - # 检查缓存是否新鲜(5分钟内) + # Check cache is fresh (within 5 minutes) cutoff_time = datetime.now() - timedelta(seconds=self.cache_ttl) query = """ @@ -406,7 +406,7 @@ class PolymarketDataSource: for row in rows: market_id = str(row.get('market_id') or '') slug = row.get('slug') - # 确保使用正确的URL构建方法 + # Make sure to use the correct URL building method polymarket_url = self._build_polymarket_url(slug, market_id) result.append({ "market_id": market_id, @@ -430,19 +430,19 @@ class PolymarketDataSource: def _fetch_markets_from_api(self, category: str = None, limit: int = 50) -> List[Dict]: """ - 从Polymarket Gamma API获取市场数据 - 使用官方推荐的 /events 端点 + Retrieve market data from the Polymarket Gamma API + Use the officially recommended /events endpoint """ try: - # 使用Gamma API的/events端点(官方推荐方式) + # Use the /events endpoint of Gamma API (official recommended method) markets = self._fetch_from_gamma_api(category, limit) if markets: - # 按volume_24h降序排序(因为API不支持order参数,需要本地排序) + # Sort by volume_24h in descending order (because the API does not support the order parameter, local sorting is required) markets.sort(key=lambda x: x.get('volume_24h', 0), reverse=True) - return markets[:limit] # 返回排序后的前limit个 + return markets[:limit] # Return the first limit after sorting - # 如果API返回空列表,记录警告(可能是API暂时不可用、网络问题或限流) - logger.warning(f"Gamma API failed to fetch markets for category '{category}' (可能原因: API暂时不可用、网络问题、限流或返回空数据)") + # If the API returns an empty list, record a warning (it may be that the API is temporarily unavailable, network problems or throttling) + logger.warning(f"Gamma API failed to fetch markets for category '{category}' (possible reasons: API is temporarily unavailable, network problems, current limiting or returning empty data)") return [] except Exception as e: @@ -451,30 +451,30 @@ class PolymarketDataSource: def _fetch_from_gamma_api(self, category: str = None, limit: int = 50) -> List[Dict]: """ - 使用Gamma API的/events端点获取市场数据(官方推荐方式) - 参考: https://docs.polymarket.com/market-data/fetching-markets + Retrieve market data from the Polymarket Gamma API + Use the officially recommended /events endpoint """ try: - # 使用/events端点获取活跃市场(推荐方式) - # 根据官方文档:https://docs.polymarket.com/market-data/fetching-markets - # order参数支持的值:volume_24hr, volume, liquidity, competitive, start_date, end_date - # 但某些端点可能不支持,先尝试不带order参数 + # Use the /events endpoint to obtain active markets (recommended method) + # According to official documentation: https://docs.polymarket.com/market-data/fetching-markets + # Supported values ​​for the order parameter: volume_24hr, volume, liquidity, competitive, start_date, end_date + # But some endpoints may not support it, try without the order parameter first. url = f"{self.gamma_api}/events" params = { "active": "true", "closed": "false", - "limit": min(limit * 2, 100) # 获取更多数据以便排序和筛选 + "limit": min(limit * 2, 100) # Get more data for sorting and filtering } - # 尝试添加排序参数(如果API支持) - # 根据文档,可能的排序字段:volume_24hr, volume, liquidity等 - # 如果API不支持,会在422错误后移除 + # Try adding sorting parameters (if supported by API) + # According to the documentation, possible sorting fields: volume_24hr, volume, liquidity, etc. + # If the API is not supported, it will be removed after a 422 error. - # 如果指定了类别,需要通过tag_id筛选 - # 注意:需要先获取tag_id,这里先用关键词推断 + # If a category is specified, it needs to be filtered by tag_id + # Note: You need to get the tag_id first, and use keywords to infer it here. if category: - # 可以尝试通过搜索或标签来筛选 - # 暂时先获取所有,然后在解析时过滤 + # You can try filtering by search or tags + # Get them all temporarily, then filter when parsing pass logger.info(f"Fetching from Gamma API: {url} with params: {params}") @@ -487,21 +487,21 @@ class PolymarketDataSource: data = response.json() logger.debug(f"Gamma API returned data type: {type(data)}, keys: {list(data.keys()) if isinstance(data, dict) else 'list'}") - # Gamma API返回的可能是列表或包含data字段的对象 + #The Gamma API may return a list or an object containing a data field if isinstance(data, list): logger.info(f"Gamma API returned list with {len(data)} items") markets = self._parse_gamma_events(data, category) logger.info(f"Parsed {len(markets)} markets from Gamma API") return markets elif isinstance(data, dict): - # 可能是 {"data": [...]} 格式 + # Possibly {"data": [...]} format if "data" in data: events_list = data["data"] logger.info(f"Gamma API returned dict with 'data' field containing {len(events_list) if isinstance(events_list, list) else 'non-list'} items") markets = self._parse_gamma_events(events_list, category) logger.info(f"Parsed {len(markets)} markets from Gamma API") return markets - # 或者直接是事件对象 + # Or directly the event object elif "id" in data or "slug" in data: logger.info("Gamma API returned single event object") markets = self._parse_gamma_events([data], category) @@ -518,14 +518,14 @@ class PolymarketDataSource: logger.error(f"Response text (first 500 chars): {response.text[:500]}") return [] - # 非200状态码 + # Non-200 status code status_code = response.status_code if status_code == 429: - logger.warning(f"Gamma API rate limited (429). 建议: 稍后重试或减少请求频率") + logger.warning(f"Gamma API rate limited (429). Suggestion: Try again later or reduce the request frequency") elif status_code == 503: - logger.warning(f"Gamma API service unavailable (503). Polymarket API可能正在维护") + logger.warning(f"Gamma API service unavailable (503). Polymarket API may be under maintenance") elif status_code >= 500: - logger.warning(f"Gamma API server error ({status_code}). Polymarket服务器可能暂时不可用") + logger.warning(f"Gamma API server error ({status_code}). The Polymarket server may be temporarily unavailable") else: logger.warning(f"Gamma API returned status {status_code}") logger.debug(f"Response headers: {dict(response.headers)}") @@ -533,23 +533,23 @@ class PolymarketDataSource: return [] except requests.exceptions.Timeout: - logger.warning("Gamma API request timeout after 15 seconds (可能原因: 网络延迟或API响应慢)") + logger.warning("Gamma API request timeout after 15 seconds (possible reason: network delay or slow API response)") return [] except requests.exceptions.ConnectionError as ce: - logger.warning(f"Gamma API connection error: {ce} (可能原因: 网络连接问题或Polymarket API不可达)") + logger.warning(f"Gamma API connection error: {ce} (Possible reason: network connection problem or Polymarket API is unreachable)") return [] except Exception as e: - logger.warning(f"Gamma API failed: {e} (可能原因: API格式变更、网络问题或服务异常)") + logger.warning(f"Gamma API failed: {e} (Possible reasons: API format change, network problem or service abnormality)") return [] def _parse_gamma_events(self, events_data: List[Dict], category_filter: str = None) -> List[Dict]: """ - 解析Gamma API返回的事件数据 - Gamma API的/events端点返回事件对象,每个事件包含关联的市场数据 + Parse event data returned by the Gamma API + The /events endpoint of the Gamma API returns event objects, each containing associated market data - 根据官方文档,事件对象结构: - - event对象包含markets数组 - - 每个market包含clobTokenIds、outcomePrices等字段 + According to the official documentation, the event object structure is: + - The event object contains a markets array + - Each market contains fields such as clobTokenIds and outcomePrices """ parsed = [] if not events_data: @@ -558,7 +558,7 @@ class PolymarketDataSource: logger.info(f"Parsing {len(events_data)} events from Gamma API") - # 记录第一个事件的键,用于调试 + # Record the key of the first event for debugging if events_data: first_event_keys = list(events_data[0].keys())[:10] logger.info(f"First event keys: {first_event_keys}") @@ -566,29 +566,29 @@ class PolymarketDataSource: for idx, event in enumerate(events_data): try: - # Gamma API的事件对象结构 - # 事件可能有多个市场(markets字段),或者直接包含市场信息 + #Gamma API event object structure + # The event may have multiple markets (markets field), or directly contain market information markets = event.get("markets", []) - # 如果事件没有markets字段,可能事件本身就是市场数据 + # If the event does not have a markets field, the event itself may be market data. if not markets: - # 检查是否直接是市场对象(有question或title字段) + # Check whether it is a market object directly (with question or title field) if "question" in event or "title" in event or "slug" in event: markets = [event] else: - if idx < 3: # 只记录前3个的详细信息 + if idx < 3: # Only record the details of the first 3 logger.debug(f"Event {idx} has no markets and doesn't look like a market. Keys: {list(event.keys())[:10]}") continue - if idx < 3: # 只记录前3个的详细信息 + if idx < 3: # Only record the details of the first 3 logger.debug(f"Processing event {idx} with {len(markets)} markets") for market_idx, market in enumerate(markets): - # 提取市场基本信息 + # Extract basic market information market_id = market.get("id") or market.get("slug") or event.get("id") or event.get("slug", "") question = market.get("question") or event.get("question") or market.get("title") or event.get("title", "") - if idx < 3 and market_idx < 2: # 记录前几个市场的详细信息 + if idx < 3 and market_idx < 2: # Record detailed information of the first few markets logger.info(f"Event {idx}, Market {market_idx}: id={market_id}, question={question[:50] if question else 'None'}, event_slug={event.get('slug')}, market_slug={market.get('slug')}, keys={list(market.keys())[:10]}") if not question: @@ -596,18 +596,18 @@ class PolymarketDataSource: logger.warning(f"Event {idx}, Market {market_idx}: No question found, skipping. Market keys: {list(market.keys())[:10]}") continue - # 推断类别 + # Infer category inferred_category = self._infer_category(question) - # 如果指定了类别筛选,进行过滤 + # If category filtering is specified, filter if category_filter and inferred_category != category_filter: continue - # 获取概率和outcome数据 + # Get probability and outcome data current_probability = 50.0 outcome_tokens = {} - # 方法1: 从CLOB API获取实时价格(最准确) + # Method 1: Get real-time prices from CLOB API (most accurate) try: condition_id = market.get("conditionId") or event.get("conditionId") if condition_id: @@ -623,7 +623,7 @@ class PolymarketDataSource: except Exception as e: logger.debug(f"Failed to get prices from CLOB API: {e}") - # 方法2: 处理outcomePrices字段(可能是JSON字符串) + # Method 2: Process the outcomePrices field (may be a JSON string) if current_probability == 50.0: outcome_prices_str = market.get("outcomePrices") or event.get("outcomePrices") if outcome_prices_str: @@ -633,7 +633,7 @@ class PolymarketDataSource: else: outcome_prices = outcome_prices_str - # outcomePrices通常是["0.65", "0.35"]格式,对应YES和NO + # outcomePrices is usually in the format ["0.65", "0.35"], corresponding to YES and NO if isinstance(outcome_prices, list) and len(outcome_prices) >= 2: yes_price = float(outcome_prices[0]) if outcome_prices[0] else 0 no_price = float(outcome_prices[1]) if outcome_prices[1] else 0 @@ -643,16 +643,16 @@ class PolymarketDataSource: except Exception as e: logger.debug(f"Failed to parse outcomePrices: {e}") - # 从market或event中获取outcomes - # outcomes可能是对象数组、字符串数组,或者需要从其他字段解析 + # Get outcomes from market or event + #outcomes may be an object array, a string array, or need to be parsed from other fields outcomes = market.get("outcomes") or market.get("tokens") or event.get("outcomes") or [] - # 处理outcomes数组(可能是对象或字符串) + # Process the outcomes array (may be an object or a string) for outcome in outcomes: try: - # 如果outcome是字符串,跳过或尝试解析 + # If outcome is a string, skip or try to parse if isinstance(outcome, str): - # 可能是简单的字符串标识,如"YES"或"NO" + # May be a simple string identifier such as "YES" or "NO" outcome_upper = outcome.upper() if "YES" in outcome_upper: if "YES" not in outcome_tokens: @@ -662,12 +662,12 @@ class PolymarketDataSource: outcome_tokens["NO"] = {"price": 0.5, "volume": 0} continue - # outcome是对象 + # outcome is an object if not isinstance(outcome, dict): continue title = str(outcome.get("title") or outcome.get("name", "")).upper() - # 获取价格(可能是price、probability或currentPrice) + # Get the price (may be price, probability or currentPrice) price = float(outcome.get("price") or outcome.get("probability") or outcome.get("currentPrice") or 0) if "YES" in title or title == "YES" or outcome.get("outcome") == "Yes": @@ -685,14 +685,14 @@ class PolymarketDataSource: logger.debug(f"Failed to parse outcome: {e}") continue - # 如果没有找到outcomes,尝试从其他字段获取概率 + # If outcomes are not found, try to get probabilities from other fields if current_probability == 50.0: - # 尝试从market的probability字段获取 + # Try to get it from the probability field of the market prob = market.get("probability") or market.get("yesProbability") or event.get("probability") if prob: current_probability = float(prob) * 100 if float(prob) <= 1 else float(prob) - # 获取交易量和流动性 + # Get trading volume and liquidity volume_24h = float( market.get("volume_24hr") or market.get("volume24hr") or @@ -709,7 +709,7 @@ class PolymarketDataSource: 0 ) - # 解析结束日期 + # Parse end date end_date_iso = None end_date = market.get("endDate") or market.get("end_date") or event.get("endDate") or event.get("end_date") if end_date: @@ -717,34 +717,34 @@ class PolymarketDataSource: if isinstance(end_date, (int, float)): end_date_iso = datetime.fromtimestamp(end_date).isoformat() + "Z" elif isinstance(end_date, str): - # 尝试解析ISO格式字符串 + # Try to parse the ISO format string end_date_iso = end_date except: pass - # 获取slug用于构建URL - # 根据Polymarket API文档:slug应该直接从API返回的数据中获取 - # URL格式: https://polymarket.com/event/{slug} - # slug是字符串标识符,不是数字ID + # Get the slug used to build the URL + # According to Polymarket API documentation: slug should be obtained directly from the data returned by the API + # URL format: https://polymarket.com/event/{slug} + # slug is a string identifier, not a numeric ID slug = None - # 优先从event获取slug(因为event包含markets) + # Get the slug from the event first (because the event contains markets) if event.get("slug"): slug_str = str(event.get("slug", "")).strip() - # 如果slug不是纯数字,且包含字母或连字符,则是有效slug + # If the slug is not a pure number and contains letters or hyphens, it is a valid slug if slug_str and not slug_str.isdigit() and ('-' in slug_str or any(c.isalpha() for c in slug_str)): slug = slug_str - # 如果event没有有效slug,尝试从market获取 + # If the event does not have a valid slug, try to get it from the market if not slug and market.get("slug"): slug_str = str(market.get("slug", "")).strip() if slug_str and not slug_str.isdigit() and ('-' in slug_str or any(c.isalpha() for c in slug_str)): slug = slug_str - # 如果仍然没有有效slug,尝试通过API查询获取 + # If there is still no valid slug, try to obtain it through API query if not slug and market_id: try: - # 使用markets端点通过ID查询,获取完整的slug信息 + # Use the markets endpoint to query by ID to obtain complete slug information detail_market = self._fetch_market_detail_by_id(market_id) if detail_market and detail_market.get("slug"): slug_str = str(detail_market.get("slug", "")).strip() @@ -753,7 +753,7 @@ class PolymarketDataSource: except Exception as e: logger.debug(f"Failed to fetch slug for market {market_id}: {e}") - # 构建URL(使用统一的辅助方法) + # Build URL (using unified helper methods) polymarket_url = self._build_polymarket_url(slug, market_id) if not slug: logger.warning(f"Market {market_id} has no valid slug, using markets endpoint as fallback") @@ -769,12 +769,12 @@ class PolymarketDataSource: "status": "active" if market.get("active", event.get("active", True)) else "closed", "outcome_tokens": outcome_tokens, "polymarket_url": polymarket_url, - "slug": slug if slug else None # 保存slug(如果不是数字) + "slug": slug if slug else None # Save slug (if not a number) } parsed.append(market_data) - if idx < 3 and market_idx < 2: # 记录成功解析的市场 + if idx < 3 and market_idx < 2: # Record successfully parsed markets logger.info(f"Successfully parsed market: {question[:50]}, prob={current_probability:.1f}%, volume={volume_24h}") except Exception as e: @@ -785,15 +785,15 @@ class PolymarketDataSource: return parsed def _parse_rest_markets(self, markets_data: List[Dict]) -> List[Dict]: - """解析REST API返回的市场数据""" + """Parse REST API data""" parsed = [] for market in markets_data: try: - # 提取基本信息 + # Extract basic information market_id = market.get("id") or market.get("slug") or market.get("market_id", "") question = market.get("question") or market.get("title", "") - # 计算概率 + # Calculate probability current_probability = 50.0 outcome_tokens = {} @@ -816,10 +816,10 @@ class PolymarketDataSource: volume_24h = float(market.get("volume_24h", market.get("volume", 0)) or 0) liquidity = float(market.get("liquidity", 0) or 0) - # 推断类别 + # inferred category category = self._infer_category(question) - # 解析结束日期 + # parse end date end_date_iso = market.get("end_date") or market.get("endDate") if isinstance(end_date_iso, (int, float)): try: @@ -827,15 +827,15 @@ class PolymarketDataSource: except: end_date_iso = None - # 获取slug用于构建URL + # Get the slug used to build the URL slug = None slug_str = str(market.get('slug', '')).strip() if market.get('slug') else '' - # 检查slug是否有效(不是数字,且包含字母或连字符) + # Check if the slug is valid (not a number and contains letters or hyphens) if slug_str and not slug_str.isdigit() and ('-' in slug_str or any(c.isalpha() for c in slug_str)): slug = slug_str else: - # 如果slug无效,尝试通过API查询获取 + # If the slug is invalid, try to obtain it through API query try: detail_market = self._fetch_market_detail_by_id(market_id) if detail_market and detail_market.get("slug"): @@ -845,7 +845,7 @@ class PolymarketDataSource: except Exception as e: logger.debug(f"Failed to fetch slug for market {market_id}: {e}") - # 构建URL(使用统一的辅助方法) + # Build URL (using unity helper methods) polymarket_url = self._build_polymarket_url(slug, market_id) if not slug: logger.warning(f"Market {market_id} has no valid slug, using markets endpoint as fallback") @@ -870,55 +870,55 @@ class PolymarketDataSource: return parsed def _infer_category(self, question: str) -> str: - """从问题中推断类别""" + """Infer categories from questions""" question_lower = question.lower() - # 加密货币关键词 + # Cryptocurrency Keywords crypto_keywords = ['btc', 'bitcoin', 'eth', 'ethereum', 'sol', 'solana', 'crypto', 'token', 'coin', 'defi', 'nft'] if any(kw in question_lower for kw in crypto_keywords): return "crypto" - # 政治关键词 + # political keywords politics_keywords = ['election', 'president', 'trump', 'biden', 'senate', 'congress', 'vote', 'political', 'democrat', 'republican'] if any(kw in question_lower for kw in politics_keywords): return "politics" - # 经济关键词 + # economic keywords economics_keywords = ['gdp', 'inflation', 'unemployment', 'fed', 'federal reserve', 'interest rate', 'economic', 'economy', 'recession', 'gdp growth', 'cpi', 'ppi'] if any(kw in question_lower for kw in economics_keywords): return "economics" - # 体育关键词 + # Sports keywords sports_keywords = ['nfl', 'nba', 'mlb', 'soccer', 'football', 'basketball', 'baseball', 'championship', 'world cup', 'olympics', 'super bowl', 'stanley cup', 'world series'] if any(kw in question_lower for kw in sports_keywords): return "sports" - # 科技关键词 + # Technology keywords tech_keywords = ['ai', 'artificial intelligence', 'chatgpt', 'openai', 'tech', 'technology', 'apple', 'google', 'microsoft', 'meta', 'tesla', 'ipo', 'startup'] if any(kw in question_lower for kw in tech_keywords): return "tech" - # 金融关键词 + # financial keywords finance_keywords = ['stock', 's&p', 'dow', 'nasdaq', 'market cap', 'earnings', 'revenue', 'profit', 'bank', 'banking', 'financial', 'trading'] if any(kw in question_lower for kw in finance_keywords): return "finance" - # 地缘政治关键词 + # geopolitical keywords geopolitics_keywords = ['war', 'conflict', 'russia', 'ukraine', 'china', 'taiwan', 'north korea', 'iran', 'israel', 'palestine', 'middle east', 'nato', 'sanctions'] if any(kw in question_lower for kw in geopolitics_keywords): return "geopolitics" - # 文化关键词 + # Cultural keywords culture_keywords = ['movie', 'film', 'oscar', 'grammy', 'award', 'celebrity', 'music', 'album', 'tv show', 'series', 'netflix', 'disney'] if any(kw in question_lower for kw in culture_keywords): return "culture" - # 气候关键词 + # climate keywords climate_keywords = ['climate', 'global warming', 'temperature', 'carbon', 'emission', 'renewable', 'solar', 'wind energy', 'paris agreement', 'cop'] if any(kw in question_lower for kw in climate_keywords): return "climate" - # 娱乐关键词 + # Entertainment keywords entertainment_keywords = ['game', 'gaming', 'esports', 'tournament', 'streaming', 'youtube', 'twitch', 'podcast', 'comic', 'anime', 'manga'] if any(kw in question_lower for kw in entertainment_keywords): return "entertainment" @@ -927,19 +927,19 @@ class PolymarketDataSource: def _build_polymarket_url(self, slug: Optional[str], market_id: str) -> str: """ - 根据slug构建Polymarket URL - 参考: https://docs.polymarket.com/market-data/fetching-markets + Build Polymarket URL based on slug + Reference: https://docs.polymarket.com/market-data/fetching-markets Args: - slug: 从API或数据库获取的slug(可能是None或数字字符串) - market_id: 市场ID(作为备选) + slug: slug obtained from API or database (may be None or numeric string) + market_id: Market ID (as an alternative) Returns: - Polymarket URL字符串 + Polymarket URL string """ if slug: slug_str = str(slug).strip() - # 检查slug是否有效(不是数字,且包含字母或连字符) + # Check if the slug is valid (not a number and contains letters or hyphens) if slug_str and not slug_str.isdigit() and ('-' in slug_str or any(c.isalpha() for c in slug_str)): import re slug_clean = re.sub(r'[^a-zA-Z0-9\-]', '-', slug_str) @@ -947,12 +947,12 @@ class PolymarketDataSource: if slug_clean: return f"https://polymarket.com/event/{slug_clean}" - # 如果没有有效slug,尝试通过API获取slug + # If there is no valid slug, try to obtain the slug through the API if market_id: try: detail_market = self._fetch_market_detail_by_id(market_id) if detail_market: - # 尝试从detail中获取slug + # Try to get the slug from detail event_slug = detail_market.get('slug') if event_slug: slug_str = str(event_slug).strip() @@ -963,7 +963,7 @@ class PolymarketDataSource: if slug_clean: return f"https://polymarket.com/event/{slug_clean}" - # 如果event没有slug,尝试从markets中获取 + # If the event does not have a slug, try to get it from markets markets = detail_market.get('markets', []) if markets: for m in markets: @@ -979,17 +979,17 @@ class PolymarketDataSource: except Exception as e: logger.debug(f"Failed to fetch slug for market {market_id}: {e}") - # 如果所有方法都失败,返回搜索页面(更可靠) - # 注意:Polymarket的URL格式可能已经改变,使用搜索作为fallback + # If all else fails, return to the search page (more reliable) + # Note: Polymarket's URL format may have changed, use search as fallback return f"https://polymarket.com/search?q={market_id}" def _fetch_market_detail_by_id(self, market_id: str) -> Optional[Dict]: """ - 通过market ID从API获取市场详情(用于获取slug) - 参考: https://docs.polymarket.com/market-data/fetching-markets + Get market details from API by market ID (used to get slug) + Reference: https://docs.polymarket.com/market-data/fetching-markets """ try: - # 方法1: 尝试通过events端点查询(推荐,因为events包含markets) + # Method 1: Try querying through the events endpoint (recommended because events include markets) url = f"{self.gamma_api}/events" params = {"active": "true", "closed": "false", "limit": 100} response = self.session.get(url, params=params, timeout=10) @@ -1005,9 +1005,9 @@ class PolymarketDataSource: for market in markets: m_id = market.get("id") or market.get("slug") or "" e_id = event.get("id") or event.get("slug") or "" - # 匹配market_id或event_id + # Match market_id or event_id if str(m_id) == str(market_id) or str(e_id) == str(market_id): - # 返回event(因为event包含slug) + # Return event (because event contains slug) return event elif isinstance(events, dict): if "data" in events: @@ -1023,7 +1023,7 @@ class PolymarketDataSource: if str(m_id) == str(market_id) or str(e_id) == str(market_id): return event - # 方法2: 尝试通过markets端点查询 + # Method 2: Try querying through the markets endpoint url = f"{self.gamma_api}/markets" params = {"id": market_id, "limit": 1} response = self.session.get(url, params=params, timeout=10) @@ -1042,12 +1042,12 @@ class PolymarketDataSource: def _fetch_market_by_slug(self, slug: str) -> Optional[Dict]: """ - 直接通过slug查询市场(最高效的方式) - 根据Polymarket API文档:https://docs.polymarket.com/market-data/fetching-markets - 可以使用 /markets?slug=xxx 直接查询 + Query the market directly through slug (the most efficient way) + According to Polymarket API documentation: https://docs.polymarket.com/market-data/fetching-markets + You can use /markets?slug=xxx to query directly """ try: - # 方法1: 尝试通过markets端点直接查询slug + # Method 1: Try querying the slug directly through the markets endpoint url = f"{self.gamma_api}/markets" params = {"slug": slug, "limit": 10} logger.info(f"Fetching market by slug from Gamma API: {url} with params: {params}") @@ -1056,26 +1056,26 @@ class PolymarketDataSource: if response.status_code == 200: data = response.json() if isinstance(data, list) and len(data) > 0: - # 解析返回的市场数据 + # Parse the returned market data markets = self._parse_gamma_events(data) - # 精确匹配slug + # Exact match slug for market in markets: market_slug = market.get("slug", "").lower() if market_slug == slug.lower() or slug.lower() in market_slug: logger.info(f"Found market by slug: {slug}") return market - # 如果没有精确匹配,返回第一个 + # If there is no exact match, return the first if markets: logger.info(f"Found market by slug (fuzzy match): {slug}") return markets[0] elif isinstance(data, dict): - # 单个市场对象 + # single market object markets = self._parse_gamma_events([data]) if markets: logger.info(f"Found market by slug: {slug}") return markets[0] - # 方法2: 尝试通过events端点查询(events可能包含slug信息) + # Method 2: Try to query through the events endpoint (events may contain slug information) url = f"{self.gamma_api}/events" params = {"active": "true", "closed": "false", "limit": 100} response = self.session.get(url, params=params, timeout=10) @@ -1084,7 +1084,7 @@ class PolymarketDataSource: data = response.json() events = data if isinstance(data, list) else (data.get("data", []) if isinstance(data, dict) else []) - # 在返回的事件中查找匹配的slug + # Find the matching slug in the returned event for event in events: event_slug = (event.get("slug") or "").lower() if event_slug == slug.lower() or slug.lower() in event_slug: @@ -1102,20 +1102,20 @@ class PolymarketDataSource: def _fetch_market_from_api(self, market_id: str) -> Optional[Dict]: """ - 从Gamma API获取单个市场数据 - 支持通过slug或id查询 + Get individual market data from Gamma API + Support query by slug or id """ try: - # 判断是slug还是market_id + # Determine whether it is slug or market_id is_slug = not market_id.isdigit() and ('-' in market_id or any(c.isalpha() for c in market_id)) - # 如果是slug,优先使用直接查询方法 + # If it is a slug, the direct query method is preferred. if is_slug: market = self._fetch_market_by_slug(market_id) if market: return market - # 方法1: 通过markets端点查询(支持id和slug) + # Method 1: Query through markets endpoint (supports id and slug) url = f"{self.gamma_api}/markets" params = {"id": market_id, "limit": 10} if not is_slug else {"slug": market_id, "limit": 10} response = self.session.get(url, params=params, timeout=10) @@ -1131,7 +1131,7 @@ class PolymarketDataSource: if markets: return markets[0] - # 方法2: 通过events端点搜索(作为备选) + # Method 2: Search via events endpoint (as an alternative) url = f"{self.gamma_api}/events" params = { "active": "true", @@ -1144,7 +1144,7 @@ class PolymarketDataSource: data = response.json() events = data if isinstance(data, list) else (data.get("data", []) if isinstance(data, dict) else []) - # 在返回的事件中查找匹配的市场 + # Find matching markets in returned events for event in events: markets = event.get("markets", []) if not markets: @@ -1164,22 +1164,22 @@ class PolymarketDataSource: return None def _save_markets_to_db(self, markets: List[Dict]): - """保存市场数据到数据库""" + """Save market data to database""" try: with get_db_connection() as db: cur = db.cursor() for market in markets: - # 获取slug,但如果是数字则不要使用(数字不是有效的slug) + # Get the slug, but don't use it if it's a number (numbers are not valid slugs) slug = market.get('slug') or None - # 如果slug是数字,说明不是有效的slug,设置为None + # If the slug is a number, it means it is not a valid slug and is set to None. if slug and str(slug).isdigit(): slug = None - # 清理slug,只保留字母数字和连字符 + # Clean slug, keep only alphanumerics and hyphens import re if slug: slug = re.sub(r'[^a-zA-Z0-9\-]', '-', str(slug)) slug = slug.strip('-') - # 如果清理后为空或仍然是数字,设置为None + # If it is empty or still a number after cleaning, set to None if not slug or slug.isdigit(): slug = None @@ -1218,9 +1218,9 @@ class PolymarketDataSource: def _get_sample_markets(self, category: str = None, limit: int = 50) -> List[Dict]: """ - 获取示例市场数据(已弃用) - 现在应该使用真实的API数据 + Get sample market data (deprecated) + You should now use real API data. """ - # 不再返回示例数据,返回空列表 + # No longer returns sample data, returns an empty list logger.warning("Sample data method called, but real API should be used instead") return [] diff --git a/backend_api_python/app/data_sources/rate_limiter.py b/backend_api_python/app/data_sources/rate_limiter.py index 9987eb5..0183b22 100644 --- a/backend_api_python/app/data_sources/rate_limiter.py +++ b/backend_api_python/app/data_sources/rate_limiter.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- """ =================================== -防封禁工具模块 (Rate Limiter) +Anti-ban tool module (Rate Limiter) =================================== -参考 daily_stock_analysis 项目实现 -提供反爬虫策略: -1. 随机休眠(Jitter) -2. 随机 User-Agent 轮换 -3. 指数退避重试 -4. 请求频率限制 +Refer to daily_stock_analysis project implementation +Provide anti-crawler strategies: +1. Random sleep (Jitter) +2. Random User-Agent rotation +3. Exponential backoff retry +4. Request frequency limit """ import time @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) # ============================================ -# User-Agent 池 +# User-Agent Pond # ============================================ USER_AGENTS = [ @@ -48,19 +48,19 @@ USER_AGENTS = [ def get_random_user_agent() -> str: - """获取随机 User-Agent""" + """Get a random User-Agent""" return random.choice(USER_AGENTS) def get_request_headers(referer: Optional[str] = None) -> dict: """ - 获取带有随机 User-Agent 的请求头 + Get request header with random User-Agent Args: - referer: 可选的 Referer 头 + referer: optional Referer header Returns: - 请求头字典 + Request header dictionary """ headers = { 'User-Agent': get_random_user_agent(), @@ -77,7 +77,7 @@ def get_request_headers(referer: Optional[str] = None) -> dict: # ============================================ -# 随机休眠 +# Random sleep # ============================================ def random_sleep( @@ -86,31 +86,31 @@ def random_sleep( log: bool = False ) -> None: """ - 随机休眠(Jitter) + Random sleep (Jitter) - 防封禁策略:模拟人类行为的随机延迟 - 在请求之间加入不规则的等待时间 + Anti-ban strategy: simulate random delays in human behavior + Incorporate irregular wait times between requests Args: - min_seconds: 最小休眠时间(秒) - max_seconds: 最大休眠时间(秒) - log: 是否记录日志 + min_seconds: Minimum sleep time (seconds) + max_seconds: Maximum sleep time (seconds) + log: whether to log """ sleep_time = random.uniform(min_seconds, max_seconds) if log: - logger.debug(f"随机休眠 {sleep_time:.2f} 秒...") + logger.debug(f"Random hibernation {sleep_time:.2f} seconds...") time.sleep(sleep_time) # ============================================ -# 请求频率限制器 +# Request frequency limiter # ============================================ class RateLimiter: """ - 请求频率限制器 + Request frequency limiter - 确保请求之间有最小间隔时间 + Ensure there is a minimum amount of time between requests """ def __init__( @@ -120,12 +120,12 @@ class RateLimiter: jitter_max: float = 1.5 ): """ - 初始化频率限制器 + Initialize frequency limiter Args: - min_interval: 最小请求间隔(秒) - jitter_min: 随机抖动最小值(秒) - jitter_max: 随机抖动最大值(秒) + min_interval: Minimum request interval (seconds) + jitter_min: minimum random jitter (seconds) + jitter_max: maximum random jitter (seconds) """ self.min_interval = min_interval self.jitter_min = jitter_min @@ -134,37 +134,37 @@ class RateLimiter: def wait(self) -> float: """ - 等待直到可以发起下一次请求 + Wait until the next request can be made Returns: - 实际等待的时间(秒) + Actual waiting time (seconds) """ wait_time = 0.0 if self._last_request_time is not None: elapsed = time.time() - self._last_request_time if elapsed < self.min_interval: - # 补充休眠到最小间隔 + # Supplement sleep to minimum interval wait_time = self.min_interval - elapsed time.sleep(wait_time) - # 添加随机抖动 + # Add random jitter jitter = random.uniform(self.jitter_min, self.jitter_max) time.sleep(jitter) wait_time += jitter - # 记录本次请求时间 + # Record the time of this request self._last_request_time = time.time() return wait_time def reset(self) -> None: - """重置限制器""" + """reset limiter""" self._last_request_time = None # ============================================ -# 指数退避重试装饰器 +# Exponential backoff retry decorator # ============================================ def retry_with_backoff( @@ -176,17 +176,17 @@ def retry_with_backoff( on_retry: Optional[Callable[[int, Exception], None]] = None ): """ - 指数退避重试装饰器 + Exponential backoff retry decorator Args: - max_attempts: 最大重试次数 - base_delay: 基础延迟时间(秒) - max_delay: 最大延迟时间(秒) - exponential_base: 指数基数 - exceptions: 需要重试的异常类型 - on_retry: 重试时的回调函数 + max_attempts: Maximum number of retries + base_delay: base delay time (seconds) + max_delay: maximum delay time (seconds) + exponential_base: exponential base + exceptions: Exception types that need to be retried + on_retry: callback function when retrying - 使用示例: + Usage example: @retry_with_backoff(max_attempts=3, exceptions=(ConnectionError, TimeoutError)) def fetch_data(): ... @@ -203,20 +203,20 @@ def retry_with_backoff( last_exception = e if attempt == max_attempts: - logger.error(f"[重试] {func.__name__} 已达最大重试次数 ({max_attempts}),放弃") + logger.error(f"[Retry] {func.__name__} has reached the maximum number of retries ({max_attempts}), giving up") raise - # 计算退避延迟: base_delay * (exponential_base ^ (attempt - 1)) + # Calculate the backoff delay: base_delay * (exponential_base ^ (attempt - 1)) delay = min( base_delay * (exponential_base ** (attempt - 1)), max_delay ) - # 添加随机抖动 (±20%) + # Add random jitter (±20%) delay *= random.uniform(0.8, 1.2) logger.warning( - f"[重试] {func.__name__} 第 {attempt}/{max_attempts} 次失败: {e}, " - f"等待 {delay:.1f}s 后重试..." + f"[Retry] {func.__name__} failed for the {attempt}/{max_attempts} time: {e}, " + f"waiting {delay:.1f}s before retrying..." ) if on_retry: @@ -224,7 +224,7 @@ def retry_with_backoff( time.sleep(delay) - # 不应该到达这里 + # Shouldn't have gotten here raise last_exception return wrapper @@ -232,24 +232,24 @@ def retry_with_backoff( # ============================================ -# 全局限流器实例 +# Global current limiter example # ============================================ -# 东方财富接口限流器(较严格) +# Oriental Fortune interface current limiter (more stringent) _eastmoney_limiter = RateLimiter( min_interval=2.0, jitter_min=1.0, jitter_max=3.0 ) -# 腾讯财经接口限流器(较宽松) +# Tencent Finance interface current limiter (relatively loose) _tencent_limiter = RateLimiter( min_interval=1.0, jitter_min=0.5, jitter_max=1.5 ) -# Akshare 接口限流器 +# Akshare interface current limiter _akshare_limiter = RateLimiter( min_interval=2.0, jitter_min=1.5, @@ -258,15 +258,15 @@ _akshare_limiter = RateLimiter( def get_eastmoney_limiter() -> RateLimiter: - """获取东方财富限流器""" + """Get Oriental Wealth Current Limiter""" return _eastmoney_limiter def get_tencent_limiter() -> RateLimiter: - """获取腾讯财经限流器""" + """Get Tencent Finance current limiter""" return _tencent_limiter def get_akshare_limiter() -> RateLimiter: - """获取 Akshare 限流器""" + """Get Akshare Throttler""" return _akshare_limiter diff --git a/backend_api_python/app/data_sources/us_stock.py b/backend_api_python/app/data_sources/us_stock.py index 2ac1e77..fa84f7f 100644 --- a/backend_api_python/app/data_sources/us_stock.py +++ b/backend_api_python/app/data_sources/us_stock.py @@ -1,6 +1,6 @@ """ -美股数据源 -使用 yfinance 和 finnhub 获取数据 +US stock data source +Get data using yfinance and finnhub """ from typing import Dict, List, Any, Optional from datetime import datetime, timedelta @@ -15,11 +15,11 @@ logger = get_logger(__name__) class USStockDataSource(BaseDataSource): - """美股数据源""" + """US stock data source""" name = "USStock/yfinance" - # yfinance 时间周期映射 + # yfinance time period mapping INTERVAL_MAP = { '1m': '1m', '5m': '5m', @@ -31,7 +31,7 @@ class USStockDataSource(BaseDataSource): '1W': '1wk' } - # 不同周期获取数据的天数范围 + # The range of days to obtain data in different periods DAYS_MAP = { '1m': lambda limit: min(7, max(1, (limit // 390) + 2)), '5m': lambda limit: min(60, max(1, (limit // 78) + 2)), @@ -44,7 +44,7 @@ class USStockDataSource(BaseDataSource): } def __init__(self): - # 初始化 finnhub 作为备选 + # Initialize finnhub as an alternative self.finnhub_client = None try: import finnhub @@ -56,45 +56,45 @@ class USStockDataSource(BaseDataSource): def get_ticker(self, symbol: str) -> Dict[str, Any]: """ - 获取美股实时报价 + Get realtime quotes for U.S. stocks - 优先使用 Finnhub(更实时),降级使用 yfinance fast_info + Use Finnhub first (more real-time), downgrade to yfinance fast_info Returns: dict: { - 'last': 当前价格, - 'change': 涨跌额, - 'changePercent': 涨跌幅, - 'high': 最高价, - 'low': 最低价, - 'open': 开盘价, - 'previousClose': 昨收价 + 'last': current price, + 'change': change amount, + 'changePercent': increase or decrease, + 'high': highest price, + 'low': lowest price, + 'open': opening price, + 'previousClose': yesterday's closing price } """ symbol = (symbol or '').strip().upper() - # 优先使用 Finnhub(实时数据) + # Prefer using Finnhub (live data) if self.finnhub_client: try: quote = self.finnhub_client.quote(symbol) if quote and quote.get('c'): return { - 'last': quote.get('c', 0), # 当前价格 - 'change': quote.get('d', 0), # 涨跌额 - 'changePercent': quote.get('dp', 0), # 涨跌幅 - 'high': quote.get('h', 0), # 日内最高 - 'low': quote.get('l', 0), # 日内最低 - 'open': quote.get('o', 0), # 开盘价 - 'previousClose': quote.get('pc', 0) # 昨收价 + 'last': quote.get('c', 0), # current price + 'change': quote.get('d', 0), # Changes + 'changePercent': quote.get('dp', 0), # Increase or decrease + 'high': quote.get('h', 0), # Best in Japan + 'low': quote.get('l', 0), # Lowest within the day + 'open': quote.get('o', 0), # opening price + 'previousClose': quote.get('pc', 0) # Yesterday's closing price } except Exception as e: logger.warning(f"Finnhub quote failed for {symbol}: {e}") - # 降级使用 yfinance + # Downgrade to use yfinance try: ticker = yf.Ticker(symbol) - # 尝试 fast_info(更快) + # Try fast_info (faster) try: fast_info = ticker.fast_info last_price = fast_info.get('lastPrice') or fast_info.get('last_price') @@ -115,7 +115,7 @@ class USStockDataSource(BaseDataSource): except Exception as e: logger.debug(f"yfinance fast_info failed for {symbol}: {e}") - # 降级使用 info(较慢但数据更全) + # Downgrade to use info (slower but more complete data) try: info = ticker.info last_price = info.get('regularMarketPrice') or info.get('currentPrice') @@ -136,7 +136,7 @@ class USStockDataSource(BaseDataSource): except Exception as e: logger.debug(f"yfinance info failed for {symbol}: {e}") - # 最后降级:使用最近的 1 分钟 K 线 + # Last downgrade: use the most recent 1-minute K-line try: hist = ticker.history(period='1d', interval='1m') if hist is not None and not hist.empty: @@ -152,7 +152,7 @@ class USStockDataSource(BaseDataSource): 'high': float(hist['High'].max()), 'low': float(hist['Low'].min()), 'open': open_price, - 'previousClose': open_price # 近似 + 'previousClose': open_price # approximate } except Exception as e: logger.debug(f"yfinance history fallback failed for {symbol}: {e}") @@ -169,7 +169,7 @@ class USStockDataSource(BaseDataSource): limit: int, before_time: Optional[int] = None ) -> List[Dict[str, Any]]: - """获取美股K线数据""" + """Get U.S. stock K-line data""" klines = [] try: @@ -177,7 +177,7 @@ class USStockDataSource(BaseDataSource): days_func = self.DAYS_MAP.get(timeframe, lambda x: x + 1) days = days_func(limit) - # 计算日期范围 + # Calculate date range if before_time: end_date = datetime.fromtimestamp(before_time) start_date = end_date - timedelta(days=days) @@ -185,13 +185,13 @@ class USStockDataSource(BaseDataSource): end_date = datetime.now() start_date = end_date - timedelta(days=days) - # logger.info(f"使用 yfinance 获取 {symbol}, 周期: {interval}, 日期: {start_date.date()} ~ {end_date.date()}") + # logger.info(f"Use yfinance to get {symbol}, period: {interval}, date: {start_date.date()} ~ {end_date.date()}") - # 尝试 yfinance + # Try yfinance df = self._fetch_yfinance(symbol, interval, start_date, end_date) if df is None or df.empty: - # 尝试 finnhub + # try finnhub if self.finnhub_client and timeframe == '1D': klines = self._fetch_finnhub(symbol, start_date, end_date, limit) if klines: @@ -199,10 +199,10 @@ class USStockDataSource(BaseDataSource): else: klines = self._convert_dataframe(df, limit) - # 过滤和限制 + # Filter and restrict klines = self.filter_and_limit(klines, limit, before_time) - # 记录结果 + # Record results self.log_result(symbol, klines, timeframe) except Exception as e: @@ -213,12 +213,12 @@ class USStockDataSource(BaseDataSource): return klines def _fetch_yfinance(self, symbol: str, interval: str, start_date: datetime, end_date: datetime): - """使用 yfinance 获取数据""" + """Use yfinance to get data""" try: ticker = yf.Ticker(symbol) - # yfinance 的 end 参数是不包含的(exclusive),所以需要加一天才能包含 end_date 当天的数据 - # 例如:end="2026-01-12" 实际只返回到 2026-01-11 的数据 + # The end parameter of yfinance is not included (exclusive), so you need to add one day to include the end_date data of the current day. + # For example: end="2026-01-12" actually only returns the data of 2026-01-11 end_date_inclusive = end_date + timedelta(days=1) df = ticker.history( @@ -226,7 +226,7 @@ class USStockDataSource(BaseDataSource): end=end_date_inclusive.strftime('%Y-%m-%d'), interval=interval ) - # logger.info(f"yfinance 返回 {len(df) if df is not None and not df.empty else 0} 条数据") + # logger.info(f"yfinance returns {len(df) if df is not None and not df.empty else 0} pieces of data") return df except Exception as e: logger.warning(f"yfinance fetch failed: {e}") @@ -239,13 +239,13 @@ class USStockDataSource(BaseDataSource): end_date: datetime, limit: int ) -> List[Dict[str, Any]]: - """使用 finnhub 获取日线数据""" + """Use finnhub to get daily data""" klines = [] try: start_ts = int(start_date.timestamp()) end_ts = int(end_date.timestamp()) - # logger.info(f"使用 Finnhub 获取 {symbol} 日线数据") + # logger.info(f"Use Finnhub to obtain {symbol} daily data") candles = self.finnhub_client.stock_candles(symbol, 'D', start_ts, end_ts) if candles and candles.get('s') == 'ok': @@ -258,18 +258,18 @@ class USStockDataSource(BaseDataSource): close=candles['c'][i], volume=candles['v'][i] )) - # logger.info(f"Finnhub 返回 {len(klines)} 条数据") + # logger.info(f"Finnhub returns {len(klines)} pieces of data") except Exception as e: logger.error(f"Finnhub fetch failed: {e}") return klines def _convert_dataframe(self, df, limit: int) -> List[Dict[str, Any]]: - """转换 DataFrame 为K线列表""" + """Convert DataFrame to K-line list""" klines = [] df = df.tail(limit).reset_index() - # 确定时间列名(日线是 Date,分钟级是 Datetime) + # Determine the time column name (the daily line is Date, the minute level is Datetime) time_col = None if 'Datetime' in df.columns: time_col = 'Datetime' @@ -284,7 +284,7 @@ class USStockDataSource(BaseDataSource): for _, row in df.iterrows(): try: - # 处理时间戳 + # Processing timestamps time_value = row[time_col] if hasattr(time_value, 'timestamp'): ts = int(time_value.timestamp()) diff --git a/backend_api_python/app/routes/auth.py b/backend_api_python/app/routes/auth.py index 5e8ec3b..5c9e384 100644 --- a/backend_api_python/app/routes/auth.py +++ b/backend_api_python/app/routes/auth.py @@ -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: diff --git a/backend_api_python/app/routes/backtest.py b/backend_api_python/app/routes/backtest.py index 01e3be0..f94552d 100644 --- a/backend_api_python/app/routes/backtest.py +++ b/backend_api_python/app/routes/backtest.py @@ -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, diff --git a/backend_api_python/app/routes/billing.py b/backend_api_python/app/routes/billing.py index bad35f5..00875f6 100644 --- a/backend_api_python/app/routes/billing.py +++ b/backend_api_python/app/routes/billing.py @@ -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) # ========================= diff --git a/backend_api_python/app/routes/community.py b/backend_api_python/app/routes/community.py index 13db897..8ba59ad 100644 --- a/backend_api_python/app/routes/community.py +++ b/backend_api_python/app/routes/community.py @@ -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/", 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//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//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//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//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/", 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 diff --git a/backend_api_python/app/routes/credentials.py b/backend_api_python/app/routes/credentials.py index 929831f..b036400 100644 --- a/backend_api_python/app/routes/credentials.py +++ b/backend_api_python/app/routes/credentials.py @@ -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, }, } diff --git a/backend_api_python/app/routes/global_market.py b/backend_api_python/app/routes/global_market.py index 0b93802..a01ed74 100644 --- a/backend_api_python/app/routes/global_market.py +++ b/backend_api_python/app/routes/global_market.py @@ -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}%", diff --git a/backend_api_python/app/routes/health.py b/backend_api_python/app/routes/health.py index 3d9fd12..5bc2e86 100644 --- a/backend_api_python/app/routes/health.py +++ b/backend_api_python/app/routes/health.py @@ -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() diff --git a/backend_api_python/app/routes/indicator.py b/backend_api_python/app/routes/indicator.py index 1bda0fd..2e58de1 100644 --- a/backend_api_python/app/routes/indicator.py +++ b/backend_api_python/app/routes/indicator.py @@ -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({ diff --git a/backend_api_python/app/routes/kline.py b/backend_api_python/app/routes/kline.py index c04b846..b40df2b 100644 --- a/backend_api_python/app/routes/kline.py +++ b/backend_api_python/app/routes/kline.py @@ -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', '') diff --git a/backend_api_python/app/routes/market.py b/backend_api_python/app/routes/market.py index bbf28e0..552caaa 100644 --- a/backend_api_python/app/routes/market.py +++ b/backend_api_python/app/routes/market.py @@ -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({ diff --git a/backend_api_python/app/routes/polymarket.py b/backend_api_python/app/routes/polymarket.py index e141cb7..a767a0c 100644 --- a/backend_api_python/app/routes/polymarket.py +++ b/backend_api_python/app/routes/polymarket.py @@ -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', '{}') diff --git a/backend_api_python/app/routes/portfolio.py b/backend_api_python/app/routes/portfolio.py index 6f7440a..93014d3 100644 --- a/backend_api_python/app/routes/portfolio.py +++ b/backend_api_python/app/routes/portfolio.py @@ -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 diff --git a/backend_api_python/app/routes/settings.py b/backend_api_python/app/routes/settings.py index 0b8460d..723a01e 100644 --- a/backend_api_python/app/routes/settings.py +++ b/backend_api_python/app/routes/settings.py @@ -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: diff --git a/backend_api_python/app/routes/strategy.py b/backend_api_python/app/routes/strategy.py index c30210c..28fe706 100644 --- a/backend_api_python/app/routes/strategy.py +++ b/backend_api_python/app/routes/strategy.py @@ -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()) diff --git a/backend_api_python/app/services/__init__.py b/backend_api_python/app/services/__init__.py index 3a6939c..316d1b0 100644 --- a/backend_api_python/app/services/__init__.py +++ b/backend_api_python/app/services/__init__.py @@ -1,5 +1,5 @@ """ -业务服务层 +business service layer """ from app.services.kline import KlineService from app.services.backtest import BacktestService diff --git a/backend_api_python/app/services/analysis_memory.py b/backend_api_python/app/services/analysis_memory.py index 6353775..4b0cb1a 100644 --- a/backend_api_python/app/services/analysis_memory.py +++ b/backend_api_python/app/services/analysis_memory.py @@ -20,11 +20,11 @@ logger = get_logger(__name__) def _safe_json_parse(val, default=None): - """安全解析 JSON - 处理已是 Python 对象或字符串的情况""" + """Safely parse JSON - handle cases where it's already a Python object or string""" if val is None: return default if isinstance(val, (dict, list)): - return val # 已经是 Python 对象 (PostgreSQL JSONB 自动转换) + return val # Already a Python object (PostgreSQL JSONB automatically converted) if isinstance(val, str): try: return json.loads(val) @@ -48,7 +48,7 @@ class AnalysisMemory: with get_db_connection() as db: cur = db.cursor() - # 创建表(如果不存在) + # Create table if it does not exist cur.execute(""" CREATE TABLE IF NOT EXISTS qd_analysis_memory ( id SERIAL PRIMARY KEY, @@ -80,7 +80,7 @@ class AnalysisMemory: ); """) - # 检查并添加缺失的列(用于已存在的表) + # Check and add missing columns (for existing tables) cur.execute(""" DO $$ BEGIN @@ -151,7 +151,7 @@ class AnalysisMemory: END $$; """) - # 创建索引 + #Create index cur.execute(""" CREATE INDEX IF NOT EXISTS idx_analysis_memory_symbol ON qd_analysis_memory(market, symbol); @@ -187,7 +187,7 @@ class AnalysisMemory: with get_db_connection() as db: cur = db.cursor() - # 准备数据 + # Prepare data market = analysis_result.get("market") symbol = analysis_result.get("symbol") decision = analysis_result.get("decision") @@ -221,7 +221,7 @@ class AnalysisMemory: "completed", "", )) - # 使用 lastrowid 属性获取 ID(execute 内部已经处理了 RETURNING) + # Use the lastrowid attribute to get the ID (execute has already processed RETURNING internally) memory_id = cur.lastrowid db.commit() cur.close() @@ -425,8 +425,8 @@ class AnalysisMemory: summary, reasons, scores, indicators, raw, "processing", "", )) - # PostgresCursor.execute() 会在 INSERT 时提前 fetchone() 消耗 RETURNING 结果, - # 所以这里不要再 cur.fetchone(),直接取 lastrowid。 + # PostgresCursor.execute() will consume the RETURNING result in advance from fetchone() during INSERT. + # So don’t use cur.fetchone() here, just get lastrowid. memory_id = cur.lastrowid db.commit() cur.close() diff --git a/backend_api_python/app/services/backtest.py b/backend_api_python/app/services/backtest.py index 00862ec..3d32e9c 100644 --- a/backend_api_python/app/services/backtest.py +++ b/backend_api_python/app/services/backtest.py @@ -877,7 +877,7 @@ class BacktestService: if i > 0 and i % progress_log_interval == 0: progress_pct = (i / total_exec_candles) * 100 logger.info(f"Execution progress: {i}/{total_exec_candles} ({progress_pct:.1f}%), trades={executed_trades_count}, position={position}") - # 爆仓后直接停止回测,输出结果 + # After the liquidation, the backtest will be stopped directly and the results will be output. if is_liquidated: break @@ -1175,7 +1175,7 @@ class BacktestService: executed_trades_count += 1 if executed_trades_count <= 10: logger.info(f"Trade #{executed_trades_count}: close_short (before open_long) @ {timestamp}, price={close_price:.4f}, profit={close_profit:.2f}") - # 检查是否爆仓 + # Check whether the position is liquidated if capital < min_capital_to_trade: is_liquidated = True capital = 0 @@ -1232,7 +1232,7 @@ class BacktestService: highest_since_entry = None lowest_since_entry = None pending_signal = None - # 检查是否爆仓 + # Check whether the position is liquidated if capital < min_capital_to_trade: is_liquidated = True capital = 0 @@ -1263,7 +1263,7 @@ class BacktestService: executed_trades_count += 1 if executed_trades_count <= 10: logger.info(f"Trade #{executed_trades_count}: close_long (before open_short) @ {timestamp}, price={close_price:.4f}, profit={close_profit:.2f}") - # 检查是否爆仓 + # Check whether the position is liquidated if capital < min_capital_to_trade: is_liquidated = True capital = 0 @@ -1321,7 +1321,7 @@ class BacktestService: highest_since_entry = None lowest_since_entry = None pending_signal = None - # 检查是否爆仓 + # Check whether the position is liquidated if capital < min_capital_to_trade: is_liquidated = True capital = 0 @@ -1763,16 +1763,16 @@ class BacktestService: local_vars['commission'] = backtest_params.get('commission', 0.0002) local_vars['trade_direction'] = backtest_params.get('trade_direction', 'both') - # === 指标参数支持 === - # 从 backtest_params 获取用户设置的指标参数 + # === Indicator parameter support === + # Get the indicator parameters set by the user from backtest_params user_indicator_params = (backtest_params or {}).get('indicator_params', {}) - # 解析指标代码中声明的参数 + # Parse the parameters declared in the indicator code declared_params = IndicatorParamsParser.parse_params(code) - # 合并参数(用户值优先,否则使用默认值) + # Merge parameters (user values ​​take precedence, otherwise default values ​​are used) merged_params = IndicatorParamsParser.merge_params(declared_params, user_indicator_params) local_vars['params'] = merged_params - # === 指标调用器支持 === + # === Indicator caller support === user_id = (backtest_params or {}).get('user_id', 1) indicator_id = (backtest_params or {}).get('indicator_id') indicator_caller = IndicatorCaller(user_id, indicator_id) @@ -2453,7 +2453,7 @@ import pandas as pd add_short_price_arr = signals.get('add_short_price', pd.Series([0.0] * len(df))).values for i, (timestamp, row) in enumerate(df.iterrows()): - # 爆仓后直接停止回测,输出结果 + # After the liquidation, the backtest will be stopped directly and the results will be output. if is_liquidated: break @@ -2471,7 +2471,7 @@ import pandas as pd 'balance': 0 }) equity_curve.append({'time': timestamp.strftime('%Y-%m-%d %H:%M'), 'value': 0}) - break # 直接停止 + break # Stop directly # Use OHLC to evaluate triggers. high = row['high'] @@ -3126,7 +3126,7 @@ import pandas as pd lowest_since_entry = None trend_add_times = dca_add_times = trend_reduce_times = adverse_reduce_times = 0 last_trend_add_anchor = last_dca_add_anchor = last_trend_reduce_anchor = last_adverse_reduce_anchor = None - # 检查是否爆仓 + # Check whether the position is liquidated if capital < min_capital_to_trade: is_liquidated = True capital = 0 @@ -3251,7 +3251,7 @@ import pandas as pd lowest_since_entry = None trend_add_times = dca_add_times = trend_reduce_times = adverse_reduce_times = 0 last_trend_add_anchor = last_dca_add_anchor = last_trend_reduce_anchor = last_adverse_reduce_anchor = None - # 检查是否爆仓 + # Check whether the position is liquidated if capital < min_capital_to_trade: is_liquidated = True capital = 0 @@ -3354,7 +3354,7 @@ import pandas as pd # If liquidation hit, check SL signal first if position != 0 and not is_liquidated: if position_type == 'long' and low <= liquidation_price: - # Long触及爆仓线:检查是否有止损信号 + # Long touches the liquidation line: check whether there is a stop loss signal has_stop_loss = close_long_arr[i] and close_long_price_arr[i] > 0 stop_loss_price = close_long_price_arr[i] if has_stop_loss else 0 @@ -3397,7 +3397,7 @@ import pandas as pd continue elif position_type == 'short' and high >= liquidation_price: - # Short触及爆仓线:检查是否有止损信号 + # Short hits the liquidation line: check if there is a stop loss signal has_stop_loss = close_short_arr[i] and close_short_price_arr[i] > 0 stop_loss_price = close_short_price_arr[i] if has_stop_loss else 0 @@ -3530,7 +3530,7 @@ import pandas as pd strategy_config: Optional[Dict[str, Any]] = None ) -> tuple: """ - 使用旧格式信号进行交易模拟(保持兼容性) + Trading simulation using old format signals (maintaining compatibility) """ equity_curve = [] trades = [] @@ -3628,7 +3628,7 @@ import pandas as pd signals_exec = signals for i, (timestamp, row) in enumerate(df.iterrows()): - # 爆仓后直接停止回测,输出结果 + # After the liquidation, the backtest will be stopped directly and the results will be output. if is_liquidated: break @@ -3775,7 +3775,7 @@ import pandas as pd if signal == 0 and position != 0 and position_type in ['long', 'short'] and capital >= min_capital_to_trade: # Long if position_type == 'long' and position > 0: - # Trend add(顺势加仓:上涨触发) + # Trend add (increase positions with the trend: rising trigger) if trend_add_enabled and trend_add_step_pct_eff > 0 and trend_add_size_pct > 0 and (trend_add_max_times == 0 or trend_add_times < trend_add_max_times): anchor = last_trend_add_anchor if last_trend_add_anchor is not None else entry_price trigger = anchor * (1 + trend_add_step_pct_eff) @@ -3808,7 +3808,7 @@ import pandas as pd 'balance': round(max(0, capital), 2) }) - # DCA add(逆势加仓:下跌触发) + # DCA add (Add position against the trend: Triggered by decline) if dca_add_enabled and dca_add_step_pct_eff > 0 and dca_add_size_pct > 0 and (dca_add_max_times == 0 or dca_add_times < dca_add_max_times): anchor = last_dca_add_anchor if last_dca_add_anchor is not None else entry_price trigger = anchor * (1 - dca_add_step_pct_eff) @@ -3841,7 +3841,7 @@ import pandas as pd 'balance': round(max(0, capital), 2) }) - # Trend reduce(顺势减仓:上涨触发) + # Trend reduce (reduce positions with the trend: triggered by rising prices) if trend_reduce_enabled and trend_reduce_step_pct_eff > 0 and trend_reduce_size_pct > 0 and (trend_reduce_max_times == 0 or trend_reduce_times < trend_reduce_max_times): anchor = last_trend_reduce_anchor if last_trend_reduce_anchor is not None else entry_price trigger = anchor * (1 + trend_reduce_step_pct_eff) @@ -3876,7 +3876,7 @@ import pandas as pd 'balance': round(max(0, capital), 2) }) - # Adverse reduce(逆势减仓:下跌触发) + # Adverse reduce (against the trend: falling trigger) if position_type == 'long' and position > 0 and adverse_reduce_enabled and adverse_reduce_step_pct_eff > 0 and adverse_reduce_size_pct > 0 and (adverse_reduce_max_times == 0 or adverse_reduce_times < adverse_reduce_max_times): anchor = last_adverse_reduce_anchor if last_adverse_reduce_anchor is not None else entry_price trigger = anchor * (1 - adverse_reduce_step_pct_eff) @@ -3915,7 +3915,7 @@ import pandas as pd if position_type == 'short' and position < 0: shares_total = abs(position) - # Trend add(顺势加空:下跌触发) + # Trend add (Add short with the trend: triggered by decline) if trend_add_enabled and trend_add_step_pct_eff > 0 and trend_add_size_pct > 0 and (trend_add_max_times == 0 or trend_add_times < trend_add_max_times): anchor = last_trend_add_anchor if last_trend_add_anchor is not None else entry_price trigger = anchor * (1 - trend_add_step_pct_eff) @@ -3949,7 +3949,7 @@ import pandas as pd 'balance': round(max(0, capital), 2) }) - # DCA add(逆势加空:上涨触发) + # DCA add (Add short against the trend: rise trigger) if dca_add_enabled and dca_add_step_pct_eff > 0 and dca_add_size_pct > 0 and (dca_add_max_times == 0 or dca_add_times < dca_add_max_times): anchor = last_dca_add_anchor if last_dca_add_anchor is not None else entry_price trigger = anchor * (1 + dca_add_step_pct_eff) @@ -3983,7 +3983,7 @@ import pandas as pd 'balance': round(max(0, capital), 2) }) - # Trend reduce(顺势减空:下跌触发,回补一部分) + # Trend reduce (short reduction: triggered by decline, covering part of it) if trend_reduce_enabled and trend_reduce_step_pct_eff > 0 and trend_reduce_size_pct > 0 and (trend_reduce_max_times == 0 or trend_reduce_times < trend_reduce_max_times): anchor = last_trend_reduce_anchor if last_trend_reduce_anchor is not None else entry_price trigger = anchor * (1 - trend_reduce_step_pct_eff) @@ -4019,7 +4019,7 @@ import pandas as pd 'balance': round(max(0, capital), 2) }) - # Adverse reduce(逆势减空:上涨触发) + # Adverse reduce (against the trend: rising trigger) if position_type == 'short' and position < 0 and adverse_reduce_enabled and adverse_reduce_step_pct_eff > 0 and adverse_reduce_size_pct > 0 and (adverse_reduce_max_times == 0 or adverse_reduce_times < adverse_reduce_max_times): anchor = last_adverse_reduce_anchor if last_adverse_reduce_anchor is not None else entry_price trigger = anchor * (1 + adverse_reduce_step_pct_eff) @@ -4464,7 +4464,7 @@ import pandas as pd # Note: check after all signals, SL/TP takes priority if position != 0 and not is_liquidated: if position_type == 'long': - # Long爆仓:价格跌破爆仓线 + # Long liquidation: the price falls below the liquidation line if price <= liquidation_price: logger.warning(f"Long liquidation! entry={entry_price:.2f}, current={price:.2f}, liq_price={liquidation_price:.2f}") is_liquidated = True @@ -4486,7 +4486,7 @@ import pandas as pd }) continue elif position_type == 'short': - # Short爆仓:价格涨破爆仓线 + # Short liquidation: the price rises above the liquidation line if price >= liquidation_price: logger.warning(f"Short liquidation! entry={entry_price:.2f}, current={price:.2f}, liq_price={liquidation_price:.2f}") is_liquidated = True @@ -4603,7 +4603,7 @@ import pandas as pd end_date: datetime, total_commission: float = 0 ) -> Dict: - """计算回测指标""" + """Calculate backtest indicators""" if not equity_curve: return {} @@ -4670,7 +4670,7 @@ import pandas as pd } def _calculate_max_drawdown(self, values: List[float]) -> float: - """计算最大回撤""" + """Calculate maximum drawdown""" if not values: return 0 @@ -4688,12 +4688,12 @@ import pandas as pd def _calculate_sharpe(self, values: List[float], timeframe: str = '1D', risk_free_rate: float = 0.02) -> float: """ - 计算夏普比率 + Calculate Sharpe Ratio Args: - values: 权益曲线数值列表 - timeframe: 时间周期 - risk_free_rate: 无风险收益率(年化) + values: list of equity curve values + timeframe: time period + risk_free_rate: risk-free rate of return (annualized) """ if len(values) < 2: return 0 @@ -4706,11 +4706,11 @@ import pandas as pd # Determine annualization factor by timeframe annualization_factor = { '1m': 252 * 24 * 60, # 1m candle: ~362,880 - '5m': 252 * 24 * 12, # 5分钟K:约72,576 - '15m': 252 * 24 * 4, # 15分钟K:约24,192 - '30m': 252 * 24 * 2, # 30分钟K:约12,096 + '5m': 252 * 24 * 12, # 5 minutes K: about 72,576 + '15m': 252 * 24 * 4, # 15 minutes K: about 24,192 + '30m': 252 * 24 * 2, # 30 minutes K: about 12,096 '1H': 252 * 24, # 1H candle: 6,048 - '4H': 252 * 6, # 4小时K:1,512 + '4H': 252 * 6, # 4 hours K: 1,512 '1D': 252, # 1D candle: 252 '1W': 52 # 1W candle: 52 }.get(timeframe, 252) @@ -4746,7 +4746,7 @@ import pandas as pd equity_curve: List, trades: List ) -> Dict[str, Any]: - """格式化回测结果""" + """Format backtest results""" # Simplify equity curve max_points = 500 if len(equity_curve) > max_points: @@ -4755,7 +4755,7 @@ import pandas as pd # Clean NaN/Inf values for JSON serialization def clean_value(value): - """清理数值,将NaN/Inf转换为0""" + """Clean values, convert NaN/Inf to 0""" if isinstance(value, float): if np.isnan(value) or np.isinf(value): return 0 diff --git a/backend_api_python/app/services/billing_service.py b/backend_api_python/app/services/billing_service.py index 56e2b16..e62ed37 100644 --- a/backend_api_python/app/services/billing_service.py +++ b/backend_api_python/app/services/billing_service.py @@ -1,13 +1,13 @@ """ -Billing Service - 统一计费服务 +Billing Service - unified billing service -负责用户积分余额、功能扣费、会员状态与套餐发放。 -当前计费模型为: -1. 是否扣费由 `BILLING_ENABLED` 与各功能 cost 配置决定 -2. VIP/会员状态用于会员套餐与权益展示 -3. 社区指标的 `vip_free` 逻辑在社区购买流程中单独处理,不在这里做全局旁路 +Responsible for user points balance, function deductions, membership status and package issuance. +The current billing model is: +1. Whether to deduct fees is determined by `BILLING_ENABLED` and the cost configuration of each function. +2. VIP/member status is used to display membership packages and benefits. +3. The `vip_free` logic of community indicators is processed separately in the community purchase process, and no global bypass is done here. -计费配置存储在 `.env` 文件中,可通过系统设置界面配置。 +Accounting configuration is stored in the `.env` file and can be configured through the system settings interface. """ import os import time @@ -21,16 +21,16 @@ from app.utils.logger import get_logger logger = get_logger(__name__) -# 功能计费配置键名 +# Function billing configuration key name BILLING_CONFIG_PREFIX = 'BILLING_' -# 默认计费配置 +# Default billing configuration DEFAULT_BILLING_CONFIG = { - # 全局开关 - 'enabled': False, # 是否启用计费 + # global switch + 'enabled': False, # Whether to enable billing - # 各功能积分消耗(0表示免费) - # ai_analysis 统一单价:即时分析 / AI过滤 / 定时任务 均按此单价 × 标的数扣费 + # Point consumption for each function (0 means free) + # ai_analysis unified unit price: real-time analysis / AI filtering / scheduled tasks are all deducted based on this unit price × number of targets 'cost_ai_analysis': 10, 'cost_ai_code_gen': 30, 'cost_polymarket_deep_analysis': 15, @@ -45,15 +45,15 @@ FEATURE_NAMES = { class BillingService: - """计费服务类""" + """Billing service category""" def __init__(self): self._config_cache = None self._config_cache_time = 0 - self._cache_ttl = 60 # 配置缓存60秒 + self._cache_ttl = 60 # Configure cache for 60 seconds def get_billing_config(self) -> Dict[str, Any]: - """获取计费配置""" + """Get billing configuration""" now = time.time() if self._config_cache and (now - self._config_cache_time) < self._cache_ttl: return self._config_cache @@ -80,23 +80,23 @@ class BillingService: return config def clear_config_cache(self): - """清除配置缓存""" + """Clear configuration cache""" self._config_cache = None self._config_cache_time = 0 def is_billing_enabled(self) -> bool: - """检查是否启用计费""" + """Check if billing is enabled""" config = self.get_billing_config() return config.get('enabled', False) def get_feature_cost(self, feature: str) -> int: - """获取指定功能的积分消耗,0 表示免费""" + """Get the points consumption of the specified function, 0 means free""" config = self.get_billing_config() cost_key = f'cost_{feature}' return config.get(cost_key, 0) def get_user_credits(self, user_id: int) -> Decimal: - """获取用户积分余额""" + """Get user points balance""" try: with get_db_connection() as db: cur = db.cursor() @@ -116,10 +116,10 @@ class BillingService: def get_user_vip_status(self, user_id: int) -> Tuple[bool, Optional[datetime]]: """ - 获取用户VIP状态 + Get user VIP status Returns: - (is_vip, expires_at): VIP是否有效, VIP过期时间 + (is_vip, expires_at): whether the VIP is valid, VIP expiration time """ try: with get_db_connection() as db: @@ -138,11 +138,11 @@ class BillingService: if row and row.get('vip_expires_at'): expires_at = row['vip_expires_at'] - # 确保是 datetime 对象 + # Make sure it is a datetime object if isinstance(expires_at, str): expires_at = datetime.fromisoformat(expires_at.replace('Z', '+00:00')) - # 检查是否过期 + # Check if expired now = datetime.now(timezone.utc) if expires_at.tzinfo is None: expires_at = expires_at.replace(tzinfo=timezone.utc) @@ -449,49 +449,49 @@ class BillingService: def check_and_consume(self, user_id: int, feature: str, reference_id: str = '') -> Tuple[bool, str]: """ - 检查并消耗积分 + Check and spend points Args: - user_id: 用户ID - feature: 功能名称(ai_analysis / polymarket_deep_analysis) - reference_id: 关联ID(可选) + user_id: user ID + feature: function name (ai_analysis / polymarket_deep_analysis) + reference_id: association ID (optional) Returns: - (success, message): 是否成功, 提示消息 + (success, message): Whether it is successful, prompt message """ - # 检查是否启用计费 + # Check if billing is enabled if not self.is_billing_enabled(): return True, 'billing_disabled' config = self.get_billing_config() cost = self.get_feature_cost(feature) - # 免费功能 + # free features if cost <= 0: return True, 'free_feature' - # 说明:这里不再根据 VIP 做全局免扣积分旁路。 - # VIP / membership 仅保留为套餐、到期时间和社区 vip_free 指标权益。 + # Note: There is no longer a global point deduction bypass based on VIP. + # VIP/membership is reserved only for package, expiry time and community vip_free metric benefits. - # 检查积分余额 + # Check points balance credits = self.get_user_credits(user_id) if credits < cost: return False, f'insufficient_credits:{credits}:{cost}' - # 扣除积分 + # Points deducted try: new_balance = credits - Decimal(str(cost)) with get_db_connection() as db: cur = db.cursor() - # 更新用户积分 + # Update user points cur.execute( "UPDATE qd_users SET credits = ?, updated_at = NOW() WHERE id = ?", (float(new_balance), user_id) ) - # 记录日志 - 使用 UTC 时间确保跨时区显示正确 + # Logging - Use UTC time to ensure correct display across time zones feature_name = FEATURE_NAMES.get(feature, feature) created_at_utc = datetime.now(timezone.utc) cur.execute( @@ -516,15 +516,15 @@ class BillingService: def add_credits(self, user_id: int, amount: int, action: str = 'recharge', remark: str = '', operator_id: int = None, reference_id: str = '') -> Tuple[bool, str]: """ - 增加用户积分 + Increase user points Args: - user_id: 用户ID - amount: 增加金额(正数) - action: 操作类型(recharge/admin_adjust/refund/referral_bonus/register_bonus) - remark: 备注 - operator_id: 操作人ID(管理员操作时) - reference_id: 关联ID(如被邀请用户ID、订单号等) + user_id: user ID + amount: increase the amount (positive number) + action: action type (recharge/admin_adjust/refund/referral_bonus/register_bonus) + remark: remark + operator_id: operator ID (when the administrator operates) + reference_id: association ID (such as invited user ID, order number, etc.) Returns: (success, message) @@ -539,13 +539,13 @@ class BillingService: with get_db_connection() as db: cur = db.cursor() - # 更新用户积分 + # Update user points cur.execute( "UPDATE qd_users SET credits = ?, updated_at = NOW() WHERE id = ?", (float(new_balance), user_id) ) - # 记录日志(包含 reference_id) + # Logging (contains reference_id) cur.execute( """ INSERT INTO qd_credits_log @@ -568,13 +568,13 @@ class BillingService: def set_credits(self, user_id: int, amount: int, remark: str = '', operator_id: int = None) -> Tuple[bool, str]: """ - 设置用户积分(管理员直接设置) + Set user points (set directly by the administrator) Args: - user_id: 用户ID - amount: 设置的金额 - remark: 备注 - operator_id: 操作人ID + user_id: user ID + amount: the set amount + remark: remark + operator_id: operator ID Returns: (success, message) @@ -589,13 +589,13 @@ class BillingService: with get_db_connection() as db: cur = db.cursor() - # 更新用户积分 + # Update user points cur.execute( "UPDATE qd_users SET credits = ?, updated_at = NOW() WHERE id = ?", (amount, user_id) ) - # 记录日志 + # logging cur.execute( """ INSERT INTO qd_credits_log @@ -618,13 +618,13 @@ class BillingService: def set_vip(self, user_id: int, expires_at: Optional[datetime], remark: str = '', operator_id: int = None) -> Tuple[bool, str]: """ - 设置用户VIP状态 + Set user VIP status Args: - user_id: 用户ID - expires_at: VIP过期时间(None表示取消VIP) - remark: 备注 - operator_id: 操作人ID + user_id: user ID + expires_at: VIP expiration time (None means canceling VIP) + remark: remark + operator_id: operator ID Returns: (success, message) @@ -633,13 +633,13 @@ class BillingService: with get_db_connection() as db: cur = db.cursor() - # 更新VIP过期时间 + # Update VIP expiration time cur.execute( "UPDATE qd_users SET vip_expires_at = ?, updated_at = NOW() WHERE id = ?", (expires_at, user_id) ) - # 记录日志 + # logging action = 'vip_grant' if expires_at else 'vip_revoke' log_remark = remark or (f'VIP granted until {expires_at}' if expires_at else 'VIP revoked') cur.execute( @@ -662,21 +662,21 @@ class BillingService: return False, str(e) def get_credits_log(self, user_id: int, page: int = 1, page_size: int = 20) -> Dict[str, Any]: - """获取用户积分变动日志""" + """Get user points change log""" offset = (page - 1) * page_size try: with get_db_connection() as db: cur = db.cursor() - # 获取总数 + # Get total cur.execute( "SELECT COUNT(*) as count FROM qd_credits_log WHERE user_id = ?", (user_id,) ) total = cur.fetchone()['count'] - # 获取日志 + # Get log cur.execute( """ SELECT id, action, amount, balance_after, feature, reference_id, remark, created_at @@ -700,7 +700,7 @@ class BillingService: if getattr(dt, 'tzinfo', None) is not None: d['created_at'] = dt.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S') + 'Z' else: - # 无时区:新记录用 UTC 写入,旧记录可能为服务器本地时间,统一按 UTC 返回以便前端正确转换 + # No time zone: New records are written in UTC, and old records may be in the local time of the server. They are returned in UTC to facilitate correct conversion by the front end. d['created_at'] = dt.strftime('%Y-%m-%dT%H:%M:%S') + 'Z' logs.append(d) @@ -716,7 +716,7 @@ class BillingService: return {'items': [], 'total': 0, 'page': 1, 'page_size': page_size, 'total_pages': 0} def get_user_billing_info(self, user_id: int) -> Dict[str, Any]: - """获取用户计费与会员信息快照(供前端显示)""" + """Get a snapshot of user billing and membership information (for front-end display)""" credits = self.get_user_credits(user_id) is_vip, vip_expires_at = self.get_user_vip_status(user_id) config = self.get_billing_config() @@ -734,12 +734,12 @@ class BillingService: } -# 全局单例 +# Global singleton _billing_service = None def get_billing_service() -> BillingService: - """获取计费服务单例""" + """Get billing service singleton instance""" global _billing_service if _billing_service is None: _billing_service = BillingService() diff --git a/backend_api_python/app/services/community_service.py b/backend_api_python/app/services/community_service.py index 2cf0fb3..c3470e1 100644 --- a/backend_api_python/app/services/community_service.py +++ b/backend_api_python/app/services/community_service.py @@ -1,7 +1,7 @@ """ -Community Service - 指标社区服务 +Community Service - indicator community service -处理指标市场、购买、评论等功能。 +Handles functions such as indicator markets, purchases, and comments. """ import json import time @@ -16,7 +16,7 @@ logger = get_logger(__name__) class CommunityService: - """指标社区服务类""" + """Indicator Community Service Category""" def __init__(self): self.billing = get_billing_service() @@ -31,7 +31,7 @@ class CommunityService: pass # ========================================== - # 指标市场 + # indicator market # ========================================== def get_market_indicators( @@ -41,16 +41,16 @@ class CommunityService: keyword: str = None, pricing_type: str = None, # 'free' / 'paid' / None(all) sort_by: str = 'newest', # 'newest' / 'hot' / 'price_asc' / 'price_desc' / 'rating' - user_id: int = None # 当前用户ID,用于判断是否已购买 + user_id: int = None # Current user ID, used to determine whether purchase has been made ) -> Dict[str, Any]: - """获取市场上已发布的指标列表""" + """Get a list of published indicators on the market""" offset = (page - 1) * page_size try: with get_db_connection() as db: cur = db.cursor() - # 构建查询条件 - 只显示已发布且审核通过的指标 + # Build query conditions - only display published and approved indicators where_clauses = ["i.publish_to_community = 1", "(i.review_status = 'approved' OR i.review_status IS NULL)"] params = [] @@ -66,7 +66,7 @@ class CommunityService: where_sql = " AND ".join(where_clauses) - # 排序 + # sort order_map = { 'newest': 'i.created_at DESC', 'hot': 'i.purchase_count DESC, i.view_count DESC', @@ -76,7 +76,7 @@ class CommunityService: } order_sql = order_map.get(sort_by, 'i.created_at DESC') - # 获取总数 + # Get total count_sql = f""" SELECT COUNT(*) as count FROM qd_indicator_codes i @@ -85,7 +85,7 @@ class CommunityService: cur.execute(count_sql, tuple(params)) total = cur.fetchone()['count'] - # 获取列表(联表查询作者信息) + # Get the list (join the table to query author information) query_sql = f""" SELECT i.id, i.name, i.description, i.pricing_type, i.price, COALESCE(i.vip_free, FALSE) as vip_free, @@ -102,7 +102,7 @@ class CommunityService: cur.execute(query_sql, tuple(params + [page_size, offset])) rows = cur.fetchall() or [] - # 如果有当前用户,查询已购买的指标 + # If there is a current user, query the purchased indicators purchased_ids = set() if user_id: indicator_ids = [r['id'] for r in rows] @@ -116,7 +116,7 @@ class CommunityService: cur.close() - # 格式化返回数据 + #Format return data items = [] for row in rows: items.append({ @@ -160,7 +160,7 @@ class CommunityService: with get_db_connection() as db: cur = db.cursor() - # 获取指标信息 + # Get indicator information cur.execute(""" SELECT i.id, i.name, i.description, i.pricing_type, i.price, COALESCE(i.vip_free, FALSE) as vip_free, @@ -179,12 +179,12 @@ class CommunityService: cur.close() return None - # 检查是否已发布到社区(或者是自己的指标) + # Check if it has been published to the community (or its own indicator) if not row['publish_to_community'] and row['user_id'] != user_id: cur.close() return None - # 检查是否已购买 + # Check if purchased is_purchased = False if user_id: cur.execute( @@ -193,7 +193,7 @@ class CommunityService: ) is_purchased = cur.fetchone() is not None - # 增加浏览次数 + # Increase the number of views cur.execute( "UPDATE qd_indicator_codes SET view_count = COALESCE(view_count, 0) + 1 WHERE id = ?", (indicator_id,) @@ -230,7 +230,7 @@ class CommunityService: return None # ========================================== - # 购买功能 + # Purchase function # ========================================== def purchase_indicator(self, buyer_id: int, indicator_id: int) -> Tuple[bool, str, Dict[str, Any]]: @@ -244,7 +244,7 @@ class CommunityService: with get_db_connection() as db: cur = db.cursor() - # 1. 获取指标信息 + # 1. Get indicator information cur.execute(""" SELECT id, user_id, name, code, description, pricing_type, price, COALESCE(vip_free, FALSE) as vip_free, preview_image, is_encrypted @@ -266,12 +266,12 @@ class CommunityService: # VIP-free indicator: VIP users can get it without credits charge effective_price = 0.0 if (vip_free and is_vip) else price - # 2. 检查是否购买自己的指标 + # 2. Check whether to buy your own indicator if seller_id == buyer_id: cur.close() return False, 'cannot_buy_own', {} - # 3. 检查是否已购买 + # 3. Check if purchased cur.execute( "SELECT id FROM qd_indicator_purchases WHERE indicator_id = ? AND buyer_id = ?", (indicator_id, buyer_id) @@ -280,7 +280,7 @@ class CommunityService: cur.close() return False, 'already_purchased', {} - # 4. 如果是付费指标,检查并扣除积分 + # 4. If it is a paid indicator, check and deduct points if pricing_type != 'free' and effective_price > 0: buyer_credits = self.billing.get_user_credits(buyer_id) if buyer_credits < effective_price: @@ -290,22 +290,22 @@ class CommunityService: 'current': float(buyer_credits) } - # 扣除买家积分 + # Deduct buyer points new_buyer_balance = buyer_credits - Decimal(str(effective_price)) cur.execute( "UPDATE qd_users SET credits = ?, updated_at = NOW() WHERE id = ?", (float(new_buyer_balance), buyer_id) ) - # 记录买家积分日志 + # Record buyer points log cur.execute(""" INSERT INTO qd_credits_log (user_id, action, amount, balance_after, feature, reference_id, remark, created_at) VALUES (?, 'indicator_purchase', ?, ?, 'indicator_purchase', ?, ?, NOW()) """, (buyer_id, -effective_price, float(new_buyer_balance), str(indicator_id), - f"购买指标: {indicator['name']}")) + f"Buy indicator: {indicator['name']}")) - # 给卖家增加积分(可配置抽成比例,这里先100%给卖家) + # Increase points for sellers (the commission ratio can be configured, here 100% is given to sellers first) seller_credits = self.billing.get_user_credits(seller_id) new_seller_balance = seller_credits + Decimal(str(effective_price)) cur.execute( @@ -313,22 +313,22 @@ class CommunityService: (float(new_seller_balance), seller_id) ) - # 记录卖家积分日志 + # Record seller points log cur.execute(""" INSERT INTO qd_credits_log (user_id, action, amount, balance_after, feature, reference_id, remark, created_at) VALUES (?, 'indicator_sale', ?, ?, 'indicator_sale', ?, ?, NOW()) """, (seller_id, effective_price, float(new_seller_balance), str(indicator_id), - f"出售指标: {indicator['name']}")) + f"Sell indicator: {indicator['name']}")) - # 5. 创建购买记录 + # 5. Create purchase record cur.execute(""" INSERT INTO qd_indicator_purchases (indicator_id, buyer_id, seller_id, price, created_at) VALUES (?, ?, ?, ?, NOW()) """, (indicator_id, buyer_id, seller_id, effective_price)) - # 6. 复制指标到买家账户 + # 6. Copy the indicator to the buyer’s account now_ts = int(time.time()) # Get vip_free as boolean from indicator vip_free_value = bool(indicator.get('vip_free') or False) @@ -349,7 +349,7 @@ class CommunityService: now_ts, now_ts )) - # 7. 更新指标购买次数 + # 7. Update indicator purchase times cur.execute(""" UPDATE qd_indicator_codes SET purchase_count = COALESCE(purchase_count, 0) + 1 @@ -374,14 +374,14 @@ class CommunityService: with get_db_connection() as db: cur = db.cursor() - # 获取总数 + # Get total cur.execute( "SELECT COUNT(*) as count FROM qd_indicator_purchases WHERE buyer_id = ?", (user_id,) ) total = cur.fetchone()['count'] - # 获取列表 + # Get list cur.execute(""" SELECT p.id as purchase_id, p.price as purchase_price, p.created_at as purchase_time, @@ -429,7 +429,7 @@ class CommunityService: return {'items': [], 'total': 0, 'page': 1, 'page_size': page_size, 'total_pages': 0} # ========================================== - # 评论功能 + # Comment function # ========================================== def get_comments(self, indicator_id: int, page: int = 1, page_size: int = 20) -> Dict[str, Any]: @@ -440,14 +440,14 @@ class CommunityService: with get_db_connection() as db: cur = db.cursor() - # 获取总数(只统计一级评论) + # Get the total number (only first-level comments are counted) cur.execute(""" SELECT COUNT(*) as count FROM qd_indicator_comments WHERE indicator_id = ? AND parent_id IS NULL AND is_deleted = 0 """, (indicator_id,)) total = cur.fetchone()['count'] - # 获取评论列表 + # Get the list of comments cur.execute(""" SELECT c.id, c.rating, c.content, c.created_at, @@ -498,14 +498,14 @@ class CommunityService: 添加评论(只有购买过的用户可以评论,且只能评论一次) """ try: - # 验证评分范围 + # Verify score range rating = max(1, min(5, int(rating))) - content = (content or '').strip()[:500] # 限制500字 + content = (content or '').strip()[:500] # Limit 500 words with get_db_connection() as db: cur = db.cursor() - # 检查指标是否存在 + # Check if the indicator exists cur.execute( "SELECT id, user_id FROM qd_indicator_codes WHERE id = ? AND publish_to_community = 1", (indicator_id,) @@ -515,12 +515,12 @@ class CommunityService: cur.close() return False, 'indicator_not_found', {} - # 不能评论自己的指标 + # Cannot comment on own indicators if indicator['user_id'] == user_id: cur.close() return False, 'cannot_comment_own', {} - # 检查是否已购买(免费指标也需要"获取"才能评论) + # Check if it has been purchased (free indicators also need to be "acquired" to comment) cur.execute( "SELECT id FROM qd_indicator_purchases WHERE indicator_id = ? AND buyer_id = ?", (indicator_id, user_id) @@ -529,7 +529,7 @@ class CommunityService: cur.close() return False, 'not_purchased', {} - # 检查是否已评论 + # Check if comments have been made cur.execute( "SELECT id FROM qd_indicator_comments WHERE indicator_id = ? AND user_id = ? AND parent_id IS NULL", (indicator_id, user_id) @@ -538,7 +538,7 @@ class CommunityService: cur.close() return False, 'already_commented', {} - # 添加评论 + #Add comment cur.execute(""" INSERT INTO qd_indicator_comments (indicator_id, user_id, rating, content, created_at, updated_at) @@ -546,7 +546,7 @@ class CommunityService: """, (indicator_id, user_id, rating, content)) comment_id = cur.lastrowid - # 更新指标的评分统计 + # Update the scoring statistics of the indicator cur.execute(""" UPDATE qd_indicator_codes SET @@ -586,7 +586,7 @@ class CommunityService: with get_db_connection() as db: cur = db.cursor() - # 检查评论是否存在且属于当前用户 + # Check if the comment exists and belongs to the current user cur.execute(""" SELECT id, rating as old_rating FROM qd_indicator_comments WHERE id = ? AND user_id = ? AND indicator_id = ? AND is_deleted = 0 @@ -599,14 +599,14 @@ class CommunityService: old_rating = comment['old_rating'] - # 更新评论 + # Update comment cur.execute(""" UPDATE qd_indicator_comments SET rating = ?, content = ?, updated_at = NOW() WHERE id = ? """, (rating, content, comment_id)) - # 如果评分变了,更新指标的平均评分 + # If the rating changes, update the average rating of the metric if old_rating != rating: cur.execute(""" UPDATE qd_indicator_codes @@ -656,7 +656,7 @@ class CommunityService: return None # ========================================== - # 管理员审核功能 + # Administrator review function # ========================================== def get_pending_indicators( @@ -672,7 +672,7 @@ class CommunityService: with get_db_connection() as db: cur = db.cursor() - # 构建查询条件 + # Build query conditions where_clauses = ["i.publish_to_community = 1"] params = [] @@ -682,7 +682,7 @@ class CommunityService: where_sql = " AND ".join(where_clauses) - # 获取总数 + # Get total count_sql = f""" SELECT COUNT(*) as count FROM qd_indicator_codes i @@ -691,7 +691,7 @@ class CommunityService: cur.execute(count_sql, tuple(params)) total = cur.fetchone()['count'] - # 获取列表 + # Get the list query_sql = f""" SELECT i.id, i.name, i.description, i.pricing_type, i.price, @@ -720,7 +720,7 @@ class CommunityService: 'pricing_type': row['pricing_type'] or 'free', 'price': float(row['price'] or 0), 'preview_image': row['preview_image'] or '', - 'code': row['code'] or '', # 管理员可以看代码 + 'code': row['code'] or '', # Administrators can view the code 'review_status': row['review_status'] or 'pending', 'review_note': row['review_note'] or '', 'reviewed_at': row['reviewed_at'].isoformat() if row['reviewed_at'] else None, @@ -761,7 +761,7 @@ class CommunityService: with get_db_connection() as db: cur = db.cursor() - # 检查指标是否存在且已发布到社区 + # Check if the indicator exists and has been published to the community cur.execute(""" SELECT id, name, user_id FROM qd_indicator_codes WHERE id = ? AND publish_to_community = 1 @@ -772,7 +772,7 @@ class CommunityService: cur.close() return False, 'indicator_not_found' - # 更新审核状态 + # Update review status cur.execute(""" UPDATE qd_indicator_codes SET review_status = ?, review_note = ?, reviewed_at = NOW(), reviewed_by = ? @@ -797,7 +797,7 @@ class CommunityService: with get_db_connection() as db: cur = db.cursor() - # 检查指标是否存在 + # Check if the indicator exists cur.execute(""" SELECT id, name FROM qd_indicator_codes WHERE id = ? """, (indicator_id,)) @@ -807,13 +807,13 @@ class CommunityService: cur.close() return False, 'indicator_not_found' - # 下架(取消发布) + # Remove from shelves (cancel publication) cur.execute(""" UPDATE qd_indicator_codes SET publish_to_community = 0, review_status = 'rejected', review_note = ?, reviewed_at = NOW(), reviewed_by = ? WHERE id = ? - """, (f"下架: {note}" if note else "管理员下架", admin_id, indicator_id)) + """, (f"Removal: {note}" if note else "Removal by administrator", admin_id, indicator_id)) db.commit() cur.close() @@ -831,7 +831,7 @@ class CommunityService: with get_db_connection() as db: cur = db.cursor() - # 检查指标是否存在 + # Check if the indicator exists cur.execute("SELECT id, name FROM qd_indicator_codes WHERE id = ?", (indicator_id,)) indicator = cur.fetchone() @@ -839,13 +839,13 @@ class CommunityService: cur.close() return False, 'indicator_not_found' - # 删除关联的评论 + # Delete associated comments cur.execute("DELETE FROM qd_indicator_comments WHERE indicator_id = ?", (indicator_id,)) - # 删除关联的购买记录 + # Delete associated purchase history cur.execute("DELETE FROM qd_indicator_purchases WHERE indicator_id = ?", (indicator_id,)) - # 删除指标 + # Delete indicator cur.execute("DELETE FROM qd_indicator_codes WHERE id = ?", (indicator_id,)) db.commit() @@ -859,7 +859,7 @@ class CommunityService: return False, f'error: {str(e)}' def get_review_stats(self) -> Dict[str, int]: - """获取审核统计""" + """Get audit statistics""" try: with get_db_connection() as db: cur = db.cursor() @@ -884,7 +884,7 @@ class CommunityService: return {'pending': 0, 'approved': 0, 'rejected': 0} # ========================================== - # 实盘表现(聚合回测 + 实盘交易数据) + # Real performance (aggregated backtest + real transaction data) # ========================================== def get_indicator_performance(self, indicator_id: int) -> Dict[str, Any]: @@ -908,7 +908,7 @@ class CommunityService: with get_db_connection() as db: cur = db.cursor() - # ---------- Part 1: 回测数据(从 result_json 解析) ---------- + # ---------- Part 1: Backtest data (parsed from result_json) ---------- bt_returns = [] bt_win_rates = [] bt_drawdowns = [] @@ -941,21 +941,21 @@ class CommunityService: bt_run_count = len(bt_returns) - # ---------- Part 2: 实盘交易数据 ---------- + # ---------- Part 2: Real transaction data ---------- live_strategy_count = 0 live_trade_count = 0 live_win_rate = 0.0 live_total_profit = 0.0 try: - # 找出使用该指标的策略(indicator_config JSON 中 indicator_id 匹配) + # Find the strategy that uses this indicator (matches indicator_id in indicator_config JSON) cur.execute(""" SELECT id FROM qd_strategies_trading WHERE indicator_config::text LIKE %s """, (f'%"indicator_id": {indicator_id}%',)) strategy_rows = cur.fetchall() - # 也尝试匹配无空格的格式 + # Also try to match formats without spaces if not strategy_rows: cur.execute(""" SELECT id FROM qd_strategies_trading @@ -993,7 +993,7 @@ class CommunityService: total_strategy_count = bt_run_count + live_strategy_count total_trade_count = sum(bt_trade_counts) + live_trade_count - # 综合胜率:优先实盘 > 回测平均 + # Comprehensive winning rate: Prioritize real offer > Backtest average if live_trade_count > 0: combined_win_rate = live_win_rate elif bt_win_rates: @@ -1001,13 +1001,13 @@ class CommunityService: else: combined_win_rate = 0.0 - # 平均收益率(回测 totalReturn %) + # Average return (backtest totalReturn %) avg_return = round(sum(bt_returns) / len(bt_returns), 2) if bt_returns else 0.0 - # 总利润:优先用实盘绝对利润,无实盘则显示回测平均收益率 + #Total profit: priority is given to the absolute profit of the real offer. If there is no real offer, the average return rate of the backtest will be displayed. combined_profit = live_total_profit if live_trade_count > 0 else avg_return - # 最大回撤取回测中最差的(maxDrawdown 是负数,取最小即最差) + # The maximum drawdown is the worst in the backtest (maxDrawdown is a negative number, the smallest is the worst) avg_drawdown = round(min(bt_drawdowns), 2) if bt_drawdowns else 0.0 if total_strategy_count == 0 and total_trade_count == 0: @@ -1027,7 +1027,7 @@ class CommunityService: return default_result -# 全局单例 +# Global singleton _community_service = None diff --git a/backend_api_python/app/services/fast_analysis.py b/backend_api_python/app/services/fast_analysis.py index 605e572..39aade0 100644 --- a/backend_api_python/app/services/fast_analysis.py +++ b/backend_api_python/app/services/fast_analysis.py @@ -1,12 +1,12 @@ """ Fast Analysis Service 3.0 -系统性重构版本 - 使用统一的数据采集器 +Systematic refactoring version - using unified data collector -核心改进: -1. 数据源统一 - 使用 MarketDataCollector,与K线模块、自选列表完全一致 -2. 宏观数据 - 新增美元指数、VIX、利率等宏观经济指标 -3. 多维新闻 - 使用结构化API,无需深度阅读 -4. 单次LLM调用 - 强约束prompt,输出结构化分析 +Core improvements: +1. Unified data sources - use MarketDataCollector, which is completely consistent with the K-line module and watch list +2. Macroeconomic data - added macroeconomic indicators such as the US dollar index, VIX, and interest rates +3. Multi-dimensional news - using structured API, no need for in-depth reading +4. Single LLM call - strong constraint prompt, output structured analysis """ import json import os @@ -185,12 +185,12 @@ def _is_major_geopolitical_news_text(combined_text: str) -> bool: class FastAnalysisService: """ - 快速分析服务 3.0 + Rapid Analysis Service 3.0 - 架构: - 1. 数据采集层 - MarketDataCollector (统一数据源) - 2. 分析层 - 单次LLM调用 (强约束prompt) - 3. 记忆层 - 分析历史存储和检索 + Architecture: + 1. Data collection layer - MarketDataCollector (unified data source) + 2. Analysis layer - single LLM call (strong constraint prompt) + 3. Memory layer - analysis history storage and retrieval """ def __init__(self): @@ -212,14 +212,14 @@ class FastAnalysisService: timeout: int = 45, ) -> Dict[str, Any]: """ - 使用统一的数据采集器收集市场数据 + Collect market data using a unified data collector - 数据层次: - 1. 核心数据: 价格、K线、技术指标 - 2. 基本面: 公司信息、财务数据 - 3. 宏观数据: DXY、VIX、TNX、黄金等 - 4. 情绪数据: 新闻、市场情绪 - 5. 预测市场: 相关预测市场事件(新增) + Data level: + 1. Core data: price, K-line, technical indicators + 2. Fundamentals: Company information, financial data + 3. Macro data: DXY, VIX, TNX, gold, etc. + 4. Sentiment data: news, market sentiment + 5. Prediction market: related prediction market events (new) """ return self.data_collector.collect_all( market=market, @@ -227,8 +227,8 @@ class FastAnalysisService: timeframe=timeframe, include_macro=include_macro, include_news=include_news, - include_polymarket=include_polymarket, # 包含预测市场数据 - timeout=timeout, # 增加超时时间,确保数据收集完成 + include_polymarket=include_polymarket, # Contains prediction market data + timeout=timeout, # Increase timeout to ensure data collection is complete ) def _calculate_indicators(self, kline_data: List[Dict]) -> Dict[str, Any]: @@ -678,13 +678,13 @@ IMPORTANT: return system_prompt, user_prompt def _format_financial_statements(self, statements: Dict[str, Any]) -> str: - """格式化财务报表数据用于提示词""" + """Formatting financial statement data for prompt words""" if not statements: return "财务报表数据暂不可用" lines = [] - # 资产负债表 + # balance sheet if 'balance_sheet' in statements: bs = statements['balance_sheet'] lines.append("资产负债表 (Balance Sheet):") @@ -702,7 +702,7 @@ IMPORTANT: current_ratio = bs['current_assets'] / bs['current_liabilities'] if bs['current_liabilities'] > 0 else 0 lines.append(f" - 流动比率: {current_ratio:.2f}") - # 利润表 + # income statement if 'income_statement' in statements: is_stmt = statements['income_statement'] lines.append("利润表 (Income Statement):") @@ -717,7 +717,7 @@ IMPORTANT: if is_stmt.get('eps'): lines.append(f" - 每股收益: ${is_stmt['eps']:.2f}") - # 现金流量表 + # cash flow statement if 'cash_flow' in statements: cf = statements['cash_flow'] lines.append("现金流量表 (Cash Flow):") @@ -729,13 +729,13 @@ IMPORTANT: return "\n".join(lines) if lines else "财务报表数据暂不可用" def _format_earnings_data(self, earnings: Dict[str, Any]) -> str: - """格式化盈利数据用于提示词""" + """Format profit data for prompt words""" if not earnings: return "盈利数据暂不可用" lines = [] - # 历史盈利 + # historical profit if 'history' in earnings and earnings['history']: lines.append("历史盈利 (Earnings History):") for i, hist in enumerate(earnings['history'][:4], 1): @@ -753,7 +753,7 @@ IMPORTANT: line += f", 超预期={surprise_str}" lines.append(line) - # 未来盈利 + # future profit if 'upcoming' in earnings: upcoming = earnings['upcoming'] if upcoming.get('next_earnings_date'): @@ -763,7 +763,7 @@ IMPORTANT: if upcoming.get('revenue_estimate'): lines.append(f" - 收入预期: ${upcoming['revenue_estimate']:,.0f}") - # 季度盈利 + # quarterly profit if 'quarterly' in earnings: q = earnings['quarterly'] if q.get('latest_quarter'): @@ -776,25 +776,25 @@ IMPORTANT: return "\n".join(lines) if lines else "盈利数据暂不可用" def _format_macro_summary(self, macro: Dict[str, Any], market: str) -> str: - """格式化宏观数据摘要""" + """Format macro data summaries""" if not macro: return "宏观数据暂不可用" lines = [] - # 美元指数 + # dollar index if 'DXY' in macro: dxy = macro['DXY'] direction = "↑" if dxy.get('change', 0) > 0 else "↓" lines.append(f"- {dxy.get('name', 'USD Index')}: {dxy.get('price', 'N/A')} ({direction}{abs(dxy.get('changePercent', 0)):.2f}%)") - # 美元强弱对不同资产的影响 + # The impact of the strength of the U.S. dollar on different assets if market == 'Crypto': impact = "利空加密货币" if dxy.get('change', 0) > 0 else "利好加密货币" lines.append(f" ⚠️ 美元{direction} {impact}") elif market == 'Forex': lines.append(f" ⚠️ 美元{direction} 直接影响外汇走势") - # VIX恐慌指数 + # VIX panic index if 'VIX' in macro: vix = macro['VIX'] vix_value = vix.get('price', 0) @@ -808,7 +808,7 @@ IMPORTANT: level = "低波动 (<15)" lines.append(f"- {vix.get('name', 'VIX')}: {vix_value:.2f} - {level}") - # 美债收益率 + # U.S. Treasury yields if 'TNX' in macro: tnx = macro['TNX'] direction = "↑" if tnx.get('change', 0) > 0 else "↓" @@ -816,19 +816,19 @@ IMPORTANT: if tnx.get('price', 0) > 4.5: lines.append(" ⚠️ 高利率环境,对估值不利") - # 黄金 + # gold if 'GOLD' in macro: gold = macro['GOLD'] direction = "↑" if gold.get('change', 0) > 0 else "↓" lines.append(f"- {gold.get('name', 'Gold')}: ${gold.get('price', 'N/A'):.2f} ({direction}{abs(gold.get('changePercent', 0)):.2f}%)") - # 标普500 + # S&P 500 if 'SPY' in macro: spy = macro['SPY'] direction = "↑" if spy.get('change', 0) > 0 else "↓" lines.append(f"- {spy.get('name', 'S&P 500')}: ${spy.get('price', 'N/A'):.2f} ({direction}{abs(spy.get('changePercent', 0)):.2f}%)") - # 比特币 (作为风险指标) + # Bitcoin (as a risk indicator) if 'BTC' in macro and market != 'Crypto': btc = macro['BTC'] direction = "↑" if btc.get('change', 0) > 0 else "↓" @@ -876,8 +876,8 @@ IMPORTANT: logger.info(f"Fast analysis starting: {market}:{symbol}") # Consensus timeframes: - # - 默认:用用户传入的 timeframe 作为主周期,再加一个上层周期(1D/4H)提升稳定性 - # - 也允许通过 env 覆盖(逗号分隔),例如 AI_ANALYSIS_CONSENSUS_TIMEFRAMES=1D,4H + # - Default: use the timeframe passed in by the user as the main cycle, and add an upper cycle (1D/4H) to improve stability + # - Overriding via env (comma separated) is also allowed, e.g. AI_ANALYSIS_CONSENSUS_TIMEFRAMES=1D,4H env_tfs = os.getenv("AI_ANALYSIS_CONSENSUS_TIMEFRAMES", "").strip() if env_tfs: consensus_timeframes = [t.strip() for t in env_tfs.split(",") if t.strip()] @@ -1067,16 +1067,16 @@ IMPORTANT: # Validate we have essential data - with fallback to indicators current_price = None - # 优先从 price 数据获取 + # Get it from price data first if data.get("price") and data["price"].get("price"): current_price = data["price"]["price"] - # Fallback: 从 indicators 获取 (如果 K 线成功计算了) + # Fallback: Get from indicators (if the K-line is calculated successfully) if not current_price and data.get("indicators"): current_price = data["indicators"].get("current_price") if current_price: logger.info(f"Using price from indicators: ${current_price}") - # 构建简化的 price 数据 + # Build simplified price data data["price"] = { "price": current_price, "change": 0, @@ -1084,7 +1084,7 @@ IMPORTANT: "source": "indicators_fallback" } - # Fallback: 从 kline 最后一根获取 + # Fallback: Get from the last kline if not current_price and data.get("kline"): klines = data["kline"] if klines and len(klines) > 0: @@ -1346,13 +1346,13 @@ IMPORTANT: "take_profit": analysis.get("take_profit"), "position_size_pct": analysis.get("position_size_pct", 10), "timeframe": analysis.get("timeframe", "medium"), - # camelCase + 语义别名:供私有前端/旧版组件绑定(勿用 indicators.trading_levels 充当计划) + # camelCase + semantic alias: for private front-end/legacy component binding (do not use indicators.trading_levels as a plan) "entryPrice": analysis.get("entry_price"), "stopLoss": analysis.get("stop_loss"), "takeProfit": analysis.get("take_profit"), "positionSizePct": analysis.get("position_size_pct", 10), "decision": str(analysis.get("decision", "HOLD") or "HOLD").upper(), - # 与 stop_loss / take_profit 数值相同;命名强调「亏损离场 / 盈利目标」避免与多单参考线混淆 + # The same value as stop_loss / take_profit; the naming emphasizes "loss exit / profit target" to avoid confusion with the long order reference line "loss_exit_price": analysis.get("stop_loss"), "profit_target_price": analysis.get("take_profit"), }, @@ -1398,12 +1398,12 @@ IMPORTANT: def _build_decision_guidance(self, rsi_value: float, macd_signal: str, ma_trend: str, change_24h: float) -> str: """ - 根据技术指标构建决策指导,帮助AI做出更合理的决策。 - 强调SELL信号是有效的做空机会。 + Build decision guidance based on technical indicators to help AI make more reasonable decisions. + Emphasize that the SELL signal is an effective short selling opportunity. """ guidance_parts = [] - # RSI 指导 - 更积极地识别做空机会 + # RSI Guidance - Identify shorting opportunities more aggressively if rsi_value > 70: guidance_parts.append("🔴 RSI > 70 (超买): 强烈建议SELL做空,避免BUY") elif rsi_value > 60: @@ -1415,7 +1415,7 @@ IMPORTANT: else: guidance_parts.append("⚪ RSI 40-60 (中性): 技术面中性,需要结合其他指标判断") - # MACD 指导 - 明确做空信号 + # MACD Guidance - Clear Short Signal if macd_signal == "bullish": guidance_parts.append("🟢 MACD 看涨: 支持BUY做多") elif macd_signal == "bearish": @@ -1423,7 +1423,7 @@ IMPORTANT: else: guidance_parts.append("⚪ MACD 中性: 无明显方向") - # MA 趋势指导 - 识别趋势反转机会 + # MA Trend Guidance - Identify Trend Reversal Opportunities if "uptrend" in ma_trend.lower() or "strong_uptrend" in ma_trend.lower(): if rsi_value > 60: guidance_parts.append("⚠️ 均线向上但RSI超买: 可能接近顶部,考虑SELL做空") @@ -1434,13 +1434,13 @@ IMPORTANT: else: guidance_parts.append("⚪ 均线横盘: 趋势不明确") - # 24小时涨跌幅指导 - 识别过度波动 + # 24-hour price range guidance - identifying excessive volatility if change_24h > 5: guidance_parts.append("🔴 24h涨幅 > 5%: 可能已过度上涨,建议SELL做空或获利了结") elif change_24h < -5: guidance_parts.append("🟢 24h跌幅 > 5%: 可能已过度下跌,可以考虑BUY做多") - # 综合建议 + # Comprehensive suggestions sell_signals = sum([ rsi_value > 60, macd_signal == "bearish", @@ -1465,14 +1465,14 @@ IMPORTANT: def _has_major_news(self, news_data: List[Dict]) -> bool: """ - 检查是否有重大新闻事件。 - 重大新闻包括:监管变化、重大合作、丑闻、重大政策、地缘政治事件等。 - 地缘类使用词边界与分级,避免 toward/extension/us 等子串误判。 + Check for breaking news events. + Breaking news includes: regulatory changes, major collaborations, scandals, major policies, geopolitical events, etc. + The geographical category uses word boundaries and classification to avoid misjudgment of substrings such as toward/extension/us. """ if not news_data: return False - # 子串关键词(较长词或中文,避免过短英文误匹配) + # Substring keywords (longer words or Chinese to avoid mismatching of too short English) major_keywords = [ "regulation", "regulatory", "approval", "policy", "government", "central bank", "监管", "禁令", "批准", "政策", "政府", "央行", @@ -1481,7 +1481,7 @@ IMPORTANT: "sanctions", "embargo", "制裁", "中东", "海湾", "北约", "united states", "middle east", ] - # 短英文词用词边界匹配(不用裸子串) + # Use word boundary matching for short English words (without using naked substrings) major_short_patterns = [ re.compile(r"\b(?:ban|banned|banning)\b", re.I), re.compile(r"\b(?:crisis|crises)\b", re.I), @@ -1510,31 +1510,31 @@ IMPORTANT: def _has_macro_event(self, macro_data: Dict, market: str) -> bool: """ - 检查是否有重大宏观事件。 - 重大宏观事件包括:VIX异常高、DXY大幅波动、利率政策变化等。 + Check for major macro events. + Major macro events include: abnormally high VIX, large fluctuations in DXY, changes in interest rate policies, etc. """ if not macro_data: return False - # 检查VIX(恐慌指数) + # Check the VIX (fear index) if "VIX" in macro_data: vix = macro_data["VIX"] vix_value = vix.get("price", 0) - if vix_value > 30: # VIX > 30 表示极度恐慌 + if vix_value > 30: # VIX > 30 indicates extreme panic return True - # 检查DXY大幅波动(>1%) + # Check for large DXY swings (>1%) if "DXY" in macro_data: dxy = macro_data["DXY"] change_pct = abs(dxy.get("changePercent", 0)) - if change_pct > 1.0: # 美元指数波动超过1% + if change_pct > 1.0: # The U.S. dollar index fluctuates more than 1% return True - # 检查利率变化(对股票和加密货币影响大) + # Check for interest rate changes (big impact on stocks and cryptocurrencies) if "TNX" in macro_data and market in ["USStock", "Crypto"]: tnx = macro_data["TNX"] change_pct = abs(tnx.get("changePercent", 0)) - if change_pct > 2.0: # 利率变化超过2% + if change_pct > 2.0: # Interest rates change by more than 2% return True return False @@ -1688,7 +1688,7 @@ IMPORTANT: else: analysis["decision"] = decision - # 基于技术指标验证决策合理性(允许宏观/新闻因素覆盖) + # Validate decision-making rationality based on technical indicators (allow macro/news factor coverage) if indicators: analysis = self._validate_decision_against_indicators( analysis, indicators, confidence, @@ -1704,14 +1704,14 @@ IMPORTANT: def _validate_decision_against_indicators(self, analysis: Dict, indicators: Dict, confidence: int, has_major_news: bool = False, has_macro_event: bool = False) -> Dict: """ - 根据技术指标验证决策的合理性,但允许宏观/新闻因素覆盖技术指标。 + Justify decisions against technical indicators, but allow macro/news factors to override technical indicators. Args: - analysis: AI分析结果 - indicators: 技术指标数据 - confidence: 置信度 - has_major_news: 是否有重大新闻事件 - has_macro_event: 是否有重大宏观事件 + analysis: AI analysis results + indicators: technical indicator data + confidence: confidence + has_major_news: Is there a major news event? + has_macro_event: Is there a major macro event? """ decision = analysis.get("decision", "HOLD") rsi_data = indicators.get("rsi", {}) @@ -1722,70 +1722,70 @@ IMPORTANT: macd_signal = macd_data.get("signal", "neutral") ma_trend = ma_data.get("trend", "sideways") - # 如果置信度太低,强制改为HOLD + # If the confidence level is too low, force it to HOLD if confidence < 60: if decision != "HOLD": logger.warning(f"Decision {decision} with low confidence {confidence}, forcing to HOLD") analysis["decision"] = "HOLD" - analysis["confidence"] = max(confidence, 45) # 降低置信度 + analysis["confidence"] = max(confidence, 45) # Reduce confidence return analysis - # 如果有重大新闻或宏观事件,允许覆盖技术指标(但记录警告) + # Allows technical indicators to be overridden (but logs warnings) if there is major news or macro events allow_override = has_major_news or has_macro_event - # 检查BUY决策是否与技术指标矛盾 + # Check whether the BUY decision conflicts with technical indicators if decision == "BUY": conflicts = [] - # RSI > 70 时不应该BUY(除非有重大利好) + # You should not buy when RSI > 70 (unless there is a major upside) if rsi_value > 70: conflicts.append(f"RSI {rsi_value:.1f} > 70 (超买)") - # MACD看跌时不应该BUY(除非有重大利好) + # You should not BUY when MACD is bearish (unless there is a major upside) if macd_signal == "bearish": conflicts.append("MACD bearish") - # 均线趋势向下时不应该BUY(除非有重大利好) - # 只有当趋势非常强烈时才认为是冲突(避免过于敏感) + # You should not buy when the moving average trend is downward (unless there is a major benefit) + # Only consider a conflict if the trend is very strong (avoid being too sensitive) if "strong_downtrend" in ma_trend.lower() or ("downtrend" in ma_trend.lower() and rsi_value > 50): conflicts.append(f"MA trend: {ma_trend}") if conflicts: if allow_override: - # 允许覆盖,但降低置信度并添加说明 + # Allow override, but lower confidence and add description logger.info(f"BUY decision conflicts with indicators but major news/macro event allows override: {', '.join(conflicts)}") analysis["confidence"] = max(confidence - 15, 50) original_summary = analysis.get("summary", "") analysis["summary"] = f"{original_summary} [注意:技术指标显示{', '.join(conflicts)},但重大事件可能改变趋势]" else: - # 没有重大事件,强制改为HOLD + # If there is no major event, it is forced to be changed to HOLD. logger.warning(f"BUY decision conflicts with indicators and no major event: {', '.join(conflicts)}. Forcing to HOLD") analysis["decision"] = "HOLD" analysis["confidence"] = max(confidence - 20, 40) original_summary = analysis.get("summary", "") analysis["summary"] = f"{original_summary} [注意:技术指标显示{', '.join(conflicts)},建议观望]" - # 检查SELL决策是否与技术指标矛盾(放宽限制,因为SELL是有效的做空机会) + # Check whether SELL decisions contradict technical indicators (relax restrictions because SELL is a valid short opportunity) elif decision == "SELL": conflicts = [] - # 只有在强烈看涨信号时才阻止SELL(放宽条件) - # RSI < 30 且 MACD看涨 且 均线向上时,才认为矛盾 + # Only block SELL (relax conditions) if there is a strong bullish signal + # It is considered a contradiction when RSI < 30 and MACD is bullish and the moving average is upward. if rsi_value < 30 and macd_signal == "bullish" and "uptrend" in ma_trend.lower(): conflicts.append(f"Strong bullish signals (RSI {rsi_value:.1f} < 30, MACD bullish, uptrend)") - # 或者 RSI < 30 且 均线强烈向上 + # Or RSI < 30 and the moving average is strongly upward elif rsi_value < 30 and "strong_uptrend" in ma_trend.lower(): conflicts.append(f"Very strong uptrend with oversold RSI {rsi_value:.1f}") if conflicts: if allow_override: - # 允许覆盖,但降低置信度并添加说明 + # Allow override, but lower confidence and add description logger.info(f"SELL decision conflicts with strong bullish indicators but major news/macro event allows override: {', '.join(conflicts)}") analysis["confidence"] = max(confidence - 15, 50) original_summary = analysis.get("summary", "") analysis["summary"] = f"{original_summary} [注意:技术指标显示{', '.join(conflicts)},但重大事件可能改变趋势]" else: - # 只有在非常强烈的看涨信号时才改为HOLD + # Only change to HOLD if there is a very strong bullish signal logger.warning(f"SELL decision conflicts with very strong bullish indicators: {', '.join(conflicts)}. Forcing to HOLD") analysis["decision"] = "HOLD" analysis["confidence"] = max(confidence - 20, 40) @@ -1796,16 +1796,16 @@ IMPORTANT: def _calculate_objective_score(self, data: Dict[str, Any], current_price: float) -> Dict[str, float]: """ - 基于客观数据计算量化评分系统 + Calculates a quantitative scoring system based on objective data - 返回一个-100到+100的分数: - - +100: 强烈利多(强烈BUY) - - +70到+100: 强烈利多(强烈BUY) - - +40到+70: 利多(BUY) - - -40到+40: 中性(HOLD) - - -70到-40: 利空(SELL) - - -100到-70: 强烈利空(强烈SELL) - - -100: 强烈利空(强烈SELL) + Return a score between -100 and +100: + - +100: Strong bullish (strong BUY) + - +70 to +100: Strong bullish (strong BUY) + - +40 to +70: BUY + - -40 to +40: Neutral (HOLD) + - -70 to -40: SELL + - -100 to -70: Strongly bearish (strongly SELL) + - -100: Strongly bearish (strongly SELL) """ indicators = data.get("indicators") or {} fundamental = data.get("fundamental") or {} @@ -1813,27 +1813,27 @@ IMPORTANT: macro = data.get("macro") or {} price_data = data.get("price") or {} - # 1. 技术指标评分 (-100 to +100) + # 1. Technical indicator score (-100 to +100) technical_score = self._calculate_technical_score(indicators, price_data) - # 2. 基本面评分 (-100 to +100) + # 2. Fundamental score (-100 to +100) fundamental_score = self._calculate_fundamental_score(fundamental, data.get("market", "")) - # 3. 新闻情绪评分 (-100 to +100) + # 3. News sentiment score (-100 to +100) sentiment_score = self._calculate_sentiment_score(news) - # 4. 宏观环境评分 (-100 to +100) + # 4. Macro environment score (-100 to +100) macro_score = self._calculate_macro_score(macro, data.get("market", "")) - # 5. 综合评分(加权平均) - # 优化权重:默认技术35%,基本面20%,情绪25%(包含地缘政治),宏观20%(提高宏观权重) - # 但要做“可用信息重加权”:当某些模块缺失(如新闻/宏观没取到),不要用0分去稀释整体强度, - # 而是重新归一化权重,让技术信号在缺失时仍可发挥主导作用。 + # 5. Comprehensive rating (weighted average) + # Optimization weight: Default technical 35%, fundamentals 20%, sentiment 25% (including geopolitics), macro 20% (increase macro weight) + # But we need to "reweight the available information": when some modules are missing (such as news/macro is not obtained), do not use 0 points to dilute the overall strength. + # Instead, the weights are renormalized so that technical signals can still play a leading role in their absence. market_type = str(data.get("market") or "") fundamental_present = (market_type == "USStock") and bool(fundamental) sentiment_present = bool(news) macro_present = bool(macro) - # indicators 一旦成功计算通常就存在,但这里也做一次保护 + # indicators usually exist once they are successfully calculated, but they are also protected here. technical_present = bool(indicators) weights = { @@ -1897,28 +1897,28 @@ IMPORTANT: return cfg def _calculate_technical_score(self, indicators: Dict, price_data: Dict) -> float: - """计算技术指标评分 (-100 to +100)""" + """Calculate technical indicator score (-100 to +100)""" score = 0.0 weight_sum = 0.0 - # RSI 评分 (-50 to +50) + # RSI score (-50 to +50) rsi_data = indicators.get("rsi", {}) rsi_value = rsi_data.get("value", 50) if rsi_value > 0: if rsi_value > 70: - rsi_score = -50 # 超买,强烈利空 + rsi_score = -50 # Overbought, strongly bearish elif rsi_value > 60: - rsi_score = -30 # 偏超买,利空 + rsi_score = -30 # Overbought, negative elif rsi_value < 30: - rsi_score = +50 # 超卖,强烈利多 + rsi_score = +50 # Oversold, strongly bullish elif rsi_value < 40: - rsi_score = +30 # 偏超卖,利多 + rsi_score = +30 # Oversold, bullish else: - rsi_score = (50 - rsi_value) * 0.6 # 40-60之间,线性映射 + rsi_score = (50 - rsi_value) * 0.6 # Between 40-60, linear mapping score += rsi_score * 0.30 weight_sum += 0.30 - # MACD 评分 (-40 to +40) + # MACD score (-40 to +40) macd_data = indicators.get("macd", {}) macd_signal = macd_data.get("signal", "neutral") if macd_signal == "bullish": @@ -1930,7 +1930,7 @@ IMPORTANT: score += macd_score * 0.25 weight_sum += 0.25 - # 均线趋势评分 (-40 to +40) + # Moving average trend score (-40 to +40) ma_data = indicators.get("moving_averages", {}) ma_trend = ma_data.get("trend", "sideways") if "strong_uptrend" in ma_trend.lower(): @@ -1946,36 +1946,36 @@ IMPORTANT: score += ma_score * 0.25 weight_sum += 0.25 - # 24小时涨跌幅评分 (-20 to +20) + # 24-hour rise and fall score (-20 to +20) change_24h = price_data.get("changePercent", 0) if change_24h > 10: - change_score = -20 # 过度上涨,利空 + change_score = -20 # Excessive rise is bad elif change_24h > 5: change_score = -10 elif change_24h < -10: - change_score = +20 # 过度下跌,利多 + change_score = +20 # Excessive decline, bullish elif change_24h < -5: change_score = +10 else: - change_score = change_24h * 2 # 线性映射 + change_score = change_24h * 2 # linear mapping score += change_score * 0.20 weight_sum += 0.20 - # ========== 额外技术特征(轻量增强,不改变主体结构) ========== - # 这些特征来自 MarketDataCollector._calculate_indicators 的输出: - # - price_position: 过去20根K线区间位置 0~100 - # - volume_ratio: 最新成交量 / 20期均量 + # ========== Additional technical features (lightweight enhancement, no change to the main structure) ========== + # These characteristics come from the output of MarketDataCollector._calculate_indicators: + # - price_position: range position of the past 20 K-lines 0~100 + # - volume_ratio: latest trading volume / average volume of 20 periods # - bollinger: BB_upper/BB_lower/BB_width # - volatility: atr, pct extra_score = 0.0 extra_weight = 0.0 - # 1) 区间位置:接近区间顶部更偏利空,接近区间底部更偏利多 + # 1) Range position: Close to the top of the range is more bearish, and close to the bottom of the range is more bullish. try: pp = float(indicators.get("price_position", 50.0)) - # 0~100 -> -15~+15 (线性映射,中心50为0) + # 0~100 -> -15~+15 (linear mapping, center 50 is 0) pp_score = (50.0 - pp) * 0.3 - # 在极端区域增强信号 + # Boost signal in extreme areas if pp >= 85: pp_score -= 5 elif pp <= 15: @@ -1985,7 +1985,7 @@ IMPORTANT: except Exception: pass - # 2) 布林带触及:突破上轨偏利空,跌破下轨偏利多 + # 2) The Bollinger Bands are touched: a breakthrough of the upper band is negative, and a fall below the lower band is positive. try: cur_px = float(indicators.get("current_price") or price_data.get("price") or 0.0) bb = indicators.get("bollinger") or {} @@ -2006,7 +2006,7 @@ IMPORTANT: except Exception: pass - # 3) 成交量放大:在趋势方向上加分,逆趋势减分(弱信号) + # 3) Trading volume amplification: plus points in the direction of the trend and minus points against the trend (weak signal) try: vr = float(indicators.get("volume_ratio") or 1.0) trend = str(indicators.get("trend") or indicators.get("moving_averages", {}).get("trend") or "").lower() @@ -2018,22 +2018,22 @@ IMPORTANT: extra_score += -8 extra_weight += 0.15 else: - # 放量但无趋势:更偏不确定,略微降低(当作偏利空风险) + # Large volume but no trend: more uncertain, slightly lower (considered to be a bearish risk) extra_score += -3 extra_weight += 0.10 elif vr <= 0.6: - # 缩量:趋势信号可信度下降(轻微回归到0) + # Shrinkage: The credibility of the trend signal decreases (slight return to 0) extra_score += 0 extra_weight += 0.05 except Exception: pass - # 4) 高波动:减少强方向自信(用“缩放”形式实现,避免硬反转) + # 4) High volatility: reduce strong directional confidence (implemented in the form of "scaling" to avoid hard reversals) try: vol = indicators.get("volatility") or {} vol_pct = float(vol.get("pct") or 0.0) if vol_pct >= 6.0: - # 极高波动:把额外分数打折,并轻微把总体拉回0 + # Extremely High Volatility: Discounts extra points and slightly brings the total back to 0 extra_score *= 0.6 score *= 0.92 elif vol_pct >= 3.5: @@ -2049,29 +2049,29 @@ IMPORTANT: score += extra_norm * 0.15 weight_sum += 0.15 - # 归一化到-100到+100 + # Normalized to -100 to +100 if weight_sum > 0: score = score / weight_sum * 100 return max(-100, min(100, score)) def _calculate_fundamental_score(self, fundamental: Dict, market: str) -> float: - """计算基本面评分 (-100 to +100)""" + """Calculate fundamental score (-100 to +100)""" if market != "USStock" or not fundamental: - return 0.0 # 非美股或无基本面数据,返回中性 + return 0.0 # Non-U.S. stocks or no fundamental data, return neutral score = 0.0 factors = 0 - # PE Ratio 评分 + # PE Ratio score pe_ratio = fundamental.get("pe_ratio") if pe_ratio and pe_ratio > 0: if pe_ratio < 15: - pe_score = +20 # 低PE,利多 + pe_score = +20 # Low PE, bullish elif pe_ratio < 25: pe_score = +10 elif pe_ratio > 50: - pe_score = -20 # 高PE,利空 + pe_score = -20 # High PE, negative elif pe_ratio > 35: pe_score = -10 else: @@ -2079,15 +2079,15 @@ IMPORTANT: score += pe_score factors += 1 - # ROE 评分 + # ROE score roe = fundamental.get("roe") if roe: if roe > 20: - roe_score = +20 # 高ROE,利多 + roe_score = +20 # High ROE, Rita elif roe > 15: roe_score = +10 elif roe < 5: - roe_score = -20 # 低ROE,利空 + roe_score = -20 # Low ROE, bad news elif roe < 10: roe_score = -10 else: @@ -2095,15 +2095,15 @@ IMPORTANT: score += roe_score factors += 1 - # 营收增长评分 + # revenue growth score revenue_growth = fundamental.get("revenue_growth") if revenue_growth: if revenue_growth > 20: - growth_score = +20 # 高增长,利多 + growth_score = +20 # High growth, good news elif revenue_growth > 10: growth_score = +10 elif revenue_growth < -10: - growth_score = -20 # 负增长,利空 + growth_score = -20 # negative growth, bad elif revenue_growth < 0: growth_score = -10 else: @@ -2111,15 +2111,15 @@ IMPORTANT: score += growth_score factors += 1 - # 利润率评分 + # Profitability score profit_margin = fundamental.get("profit_margin") if profit_margin: if profit_margin > 20: - margin_score = +15 # 高利润率,利多 + margin_score = +15 # High profit margin, profit elif profit_margin > 10: margin_score = +7 elif profit_margin < 0: - margin_score = -15 # 亏损,利空 + margin_score = -15 # loss, bad elif profit_margin < 5: margin_score = -7 else: @@ -2127,31 +2127,31 @@ IMPORTANT: score += margin_score factors += 1 - # 债务权益比评分 + # Debt to Equity Ratio Score debt_to_equity = fundamental.get("debt_to_equity") if debt_to_equity: if debt_to_equity < 0.5: - debt_score = +10 # 低负债,利多 + debt_score = +10 # Low debt, good profits elif debt_to_equity > 2.0: - debt_score = -10 # 高负债,利空 + debt_score = -10 # High debt, bad else: debt_score = 0 score += debt_score factors += 1 - # 归一化(如果有多个因素) + # Normalization (if there are multiple factors) if factors > 0: - score = score / factors * 100 / 4 # 最大可能分数是4个因素各20分=80,归一化到100 + score = score / factors * 100 / 4 # The maximum possible score is 20 points for each of the 4 factors = 80, normalized to 100 return max(-100, min(100, score)) def _calculate_sentiment_score(self, news: List[Dict]) -> float: """ - 计算新闻情绪评分 (-100 to +100) - 地缘/冲突类:词边界 + 分级惩罚,单条封顶,避免 extension/toward 等误判叠加。 + Calculate news sentiment score (-100 to +100) + Geographical/conflict category: word boundary + hierarchical punishment, single capping, to avoid superposition of misjudgments such as extension/toward. """ if not news: - return 0.0 # 无新闻,中性 + return 0.0 # No news, neutral positive_count = 0 negative_count = 0 @@ -2210,55 +2210,55 @@ IMPORTANT: def _calculate_macro_score(self, macro: Dict, market: str) -> float: """ - 计算宏观环境评分 (-100 to +100) - 包含VIX、DXY、利率等宏观经济指标 + Calculate macro environment score (-100 to +100) + Contains macroeconomic indicators such as VIX, DXY, interest rates, etc. """ if not macro: - return 0.0 # 无宏观数据,中性 + return 0.0 # No macro data, neutral score = 0.0 factors = 0 - # VIX 评分(恐慌指数)- 权重提高 + # VIX score (fear index) - increased weight vix = macro.get("VIX", {}) vix_value = vix.get("price", 0) if vix_value > 0: if vix_value > 35: - vix_score = -50 # 极高恐慌(如战争期间),严重利空 + vix_score = -50 # Extremely high panic (such as during a war), severely negative elif vix_value > 30: - vix_score = -40 # 高恐慌,严重利空 + vix_score = -40 # High panic, serious negative news elif vix_value > 25: - vix_score = -30 # 较高恐慌,利空 + vix_score = -30 # Higher panic, bad news elif vix_value > 20: - vix_score = -15 # 中等恐慌,轻微利空 + vix_score = -15 # Moderate panic, slightly negative elif vix_value < 12: - vix_score = +20 # 低恐慌,利多 + vix_score = +20 # Low panic, bullish elif vix_value < 15: - vix_score = +10 # 较低恐慌,轻微利多 + vix_score = +10 # Lower panic, slightly bullish else: vix_score = 0 score += vix_score factors += 1 - # DXY 评分(美元指数)- 权重提高 + # DXY Score (USD Index) - Increased weighting dxy = macro.get("DXY", {}) dxy_value = dxy.get("price", 0) dxy_change = dxy.get("changePercent", 0) if dxy_value > 0: - # 对于加密货币和商品,强美元通常是利空 + # For Cryptocurrencies and Commodities, A Strong USD Is Typically Bearish if market in ["Crypto", "Forex", "Futures"]: if dxy_change > 2: - dxy_score = -30 # 美元大幅走强,严重利空 + dxy_score = -30 # The sharp strengthening of the US dollar is seriously negative elif dxy_change > 1: - dxy_score = -20 # 美元走强,利空 + dxy_score = -20 # A stronger U.S. dollar is a negative elif dxy_change < -2: - dxy_score = +30 # 美元大幅走弱,利多 + dxy_score = +30 # The U.S. dollar weakens sharply, which is bullish elif dxy_change < -1: - dxy_score = +20 # 美元走弱,利多 + dxy_score = +20 # A weaker dollar is bullish else: dxy_score = 0 else: - # 对股票也有影响,但较小 + # It also has an impact on stocks, but it’s smaller if dxy_change > 2: dxy_score = -10 elif dxy_change < -2: @@ -2268,21 +2268,21 @@ IMPORTANT: score += dxy_score factors += 1 - # 利率评分(TNX)- 权重提高 + # Interest Rate Score (TNX) - Increased weighting tnx = macro.get("TNX", {}) tnx_change = tnx.get("changePercent", 0) tnx_value = tnx.get("price", 0) if tnx_change != 0 or tnx_value > 0: - # 利率上升对成长股和加密货币通常是利空 + # Rising interest rates are generally negative for growth stocks and cryptocurrencies if market in ["Crypto", "USStock"]: if tnx_change > 3: - tnx_score = -30 # 利率大幅上升,严重利空 + tnx_score = -30 # Interest rates rise sharply, which is seriously negative elif tnx_change > 2: - tnx_score = -20 # 利率上升,利空 + tnx_score = -20 # Rising interest rates are bad elif tnx_change < -3: - tnx_score = +30 # 利率大幅下降,利多 + tnx_score = +30 # A sharp drop in interest rates is bullish elif tnx_change < -2: - tnx_score = +20 # 利率下降,利多 + tnx_score = +20 # Falling interest rates are bullish else: tnx_score = 0 else: @@ -2290,7 +2290,7 @@ IMPORTANT: score += tnx_score factors += 1 - # 恐惧贪婪指数(更适合 Crypto):极端贪婪偏利空,极端恐惧偏利多(弱信号) + # Fear and greed index (more suitable for Crypto): extreme greed is negative, extreme fear is bullish (weak signal) try: fg = macro.get("FEAR_GREED", {}) or {} fg_value = float(fg.get("price") or 0.0) @@ -2310,12 +2310,12 @@ IMPORTANT: except Exception: pass - # 归一化(考虑权重) + # Normalization (considering weights) if factors > 0: - # 最大可能分数:VIX(-50~+20), DXY(-30~+30), TNX(-30~+30) = 约-110到+80 - # 归一化到-100到+100 - # 加上 Fear&Greed 的幅度(约 15),给点 buffer - max_possible = 125 # 最大绝对值 + # Maximum possible score: VIX(-50~+20), DXY(-30~+30), TNX(-30~+30) = about -110 to +80 + # Normalized to -100 to +100 + # Add the amplitude of Fear&Greed (about 15) and give some buffer + max_possible = 125 # maximum absolute value score = score / max_possible * 100 return max(-100, min(100, score)) @@ -2330,23 +2330,23 @@ IMPORTANT: def _score_to_decision(self, score: float, *, market: str = "Crypto") -> str: """ - 根据客观评分转换为决策 + Transformed into decisions based on objective scoring - 优化后的阈值(大幅缩小HOLD区间,使决策更明确): - - score >= +20: BUY(利多) - - score <= -20: SELL(利空) - - -20 < score < +20: HOLD(中性) + Optimized threshold (significantly narrows the HOLD interval to make decisions clearer): + - score >= +20: BUY (profit) + - score <= -20: SELL (bad) + - -20 < score < +20: HOLD (neutral) - 分级决策(用于更细粒度的判断): - - score >= +70: 强烈BUY - - +40 <= score < +70: 明显BUY + Hierarchical decision-making (for finer-grained judgment): + - score >= +70: strong BUY + - +40 <= score < +70: obvious BUY - +20 <= score < +40: BUY - - +10 < score < +20: 弱利多(倾向于BUY,但可HOLD) - - -10 <= score <= +10: 中性HOLD(真正的中性区间) - - -20 < score < -10: 弱利空(倾向于SELL,但可HOLD) + - +10 < score < +20: Weak profit and long (tend to BUY, but can HOLD) + - -10 <= score <= +10: Neutral HOLD (true neutral interval) + - -20 < score < -10: Weakly bearish (inclined to SELL, but can be HOLD) - -40 < score <= -20: SELL - - -70 < score <= -40: 明显SELL - - score <= -70: 强烈SELL + - -70 < score <= -40: obviously SELL + - score <= -70: Strong SELL """ cfg = self._get_ai_calibration(market=market) buy_thr = float(cfg.get("buy_threshold") or 20.0) @@ -2361,14 +2361,14 @@ IMPORTANT: def _calculate_overall_score(self, analysis: Dict) -> int: """Calculate weighted overall score (legacy method, now uses objective score if available).""" - # 优先使用客观评分 + # Prioritize objective scoring if "objective_score" in analysis: objective = analysis["objective_score"] overall = objective.get("overall_score", 50) - # 转换为0-100格式(原系统使用) + # Convert to 0-100 format (used by the original system) return max(0, min(100, int(50 + overall * 0.5))) - # 降级到LLM评分 + # Downgraded to LLM rating tech = analysis.get("technical_score", 50) fund = analysis.get("fundamental_score", 50) sent = analysis.get("sentiment_score", 50) diff --git a/backend_api_python/app/services/indicator_params.py b/backend_api_python/app/services/indicator_params.py index 2f74641..4d5a23a 100644 --- a/backend_api_python/app/services/indicator_params.py +++ b/backend_api_python/app/services/indicator_params.py @@ -1,17 +1,17 @@ """ Indicator Parameters Parser and Helper Functions -支持两个核心功能: -1. 指标参数外部传递 - 解析指标代码中的 @param 声明 -2. 指标调用其他指标 - 提供 call_indicator() 函数 +Supports two core functions: +1. External transfer of indicator parameters - parse the @param statement in the indicator code +2. Indicators call other indicators - provide call_indicator() function -参数声明格式: -# @param param_name type default_value 描述 -# @param ma_fast int 5 短期均线周期 -# @param ma_slow int 20 长期均线周期 -# @param threshold float 0.5 阈值 +Parameter declaration format: +# @param param_name type default_value description +# @param ma_fast int 5 short-term moving average period +# @param ma_slow int 20 long-term moving average period +# @param threshold float 0.5 threshold -支持的类型:int, float, bool, str +Supported types: int, float, bool, str """ import re @@ -24,9 +24,9 @@ logger = get_logger(__name__) class IndicatorParamsParser: - """解析指标代码中的参数声明""" + """Parsing parameter declarations in indicator code""" - # 参数声明正则:# @param name type default description + # Parameter declaration rules: # @param name type default description PARAM_PATTERN = re.compile( r'#\s*@param\s+(\w+)\s+(int|float|bool|str|string)\s+(\S+)\s*(.*)', re.IGNORECASE @@ -35,7 +35,7 @@ class IndicatorParamsParser: @classmethod def parse_params(cls, indicator_code: str) -> List[Dict[str, Any]]: """ - 解析指标代码中的参数声明 + Parsing parameter declarations in indicator code Returns: List of param definitions: @@ -44,7 +44,7 @@ class IndicatorParamsParser: "name": "ma_fast", "type": "int", "default": 5, - "description": "短期均线周期" + "description": "Short-term moving average cycle" }, ... ] @@ -62,10 +62,10 @@ class IndicatorParamsParser: default_str = match.group(3) description = match.group(4).strip() if match.group(4) else '' - # 转换默认值类型 + # Convert default value type default = cls._convert_value(default_str, param_type) - # 规范化类型名 + # Canonical type name if param_type == 'string': param_type = 'str' @@ -80,7 +80,7 @@ class IndicatorParamsParser: @classmethod def _convert_value(cls, value_str: str, param_type: str) -> Any: - """转换字符串值为对应类型""" + """Convert string value to corresponding type""" try: param_type = param_type.lower() if param_type == 'int': @@ -97,14 +97,14 @@ class IndicatorParamsParser: @classmethod def merge_params(cls, declared_params: List[Dict], user_params: Dict[str, Any]) -> Dict[str, Any]: """ - 合并声明的参数和用户提供的参数 + Merge declared parameters with user-supplied parameters Args: - declared_params: 从代码中解析的参数声明 - user_params: 用户提供的参数值 + declared_params: parameter declarations parsed from code + user_params: user-provided parameter values Returns: - 合并后的参数字典(使用用户值或默认值) + Merged parameter dictionary (using user values ​​or default values) """ result = {} for param in declared_params: @@ -113,10 +113,10 @@ class IndicatorParamsParser: default = param['default'] if name in user_params: - # 用户提供了值,转换为正确类型 + # User supplied value, converted to correct type result[name] = cls._convert_value(str(user_params[name]), param_type) else: - # 使用默认值 + # Use default value result[name] = default return result @@ -124,58 +124,58 @@ class IndicatorParamsParser: class IndicatorCaller: """ - 指标调用器 - 允许一个指标调用另一个指标 + Indicator caller - allows one indicator to call another indicator - 使用方式(在指标代码中): - # 按ID调用 + Usage (in indicator code): + # Call by ID rsi_df = call_indicator(5, df) - # 按名称调用(自己的指标) + # Call by name (own indicator) macd_df = call_indicator('My MACD', df) """ - # 最大调用深度,防止循环依赖 + # Maximum call depth to prevent circular dependencies MAX_CALL_DEPTH = 5 def __init__(self, user_id: int, current_indicator_id: int = None): self.user_id = user_id self.current_indicator_id = current_indicator_id - self._call_stack = [] # 调用栈,用于检测循环依赖 + self._call_stack = [] # Call stack for detecting circular dependencies def call_indicator( self, - indicator_ref: Any, # int (ID) 或 str (名称) + indicator_ref: Any, # int (ID) or str (name) df: 'pd.DataFrame', params: Dict[str, Any] = None, _depth: int = 0 ) -> Optional['pd.DataFrame']: """ - 调用另一个指标并返回结果 + Call another indicator and return the result Args: - indicator_ref: 指标ID或名称 - df: 输入的K线数据 - params: 传递给被调用指标的参数 - _depth: 内部使用,跟踪调用深度 + indicator_ref: indicator ID or name + df: input K-line data + params: parameters passed to the called indicator + _depth: used internally to track call depth Returns: - 执行后的DataFrame,包含被调用指标计算的列 + DataFrame after execution, containing columns calculated by the called indicator """ import pandas as pd import numpy as np - # 检查调用深度 + # Check call depth if _depth >= self.MAX_CALL_DEPTH: logger.error(f"Indicator call depth exceeded {self.MAX_CALL_DEPTH}") return df.copy() - # 获取指标代码 + # Get indicator code indicator_code, indicator_id = self._get_indicator_code(indicator_ref) if not indicator_code: logger.warning(f"Indicator not found: {indicator_ref}") return df.copy() - # 检查循环依赖 + # Check for circular dependencies if indicator_id in self._call_stack: logger.error(f"Circular dependency detected: {self._call_stack} -> {indicator_id}") return df.copy() @@ -183,11 +183,11 @@ class IndicatorCaller: self._call_stack.append(indicator_id) try: - # 解析并合并参数 + # Parse and merge parameters declared_params = IndicatorParamsParser.parse_params(indicator_code) merged_params = IndicatorParamsParser.merge_params(declared_params, params or {}) - # 准备执行环境 + # Prepare execution environment df_copy = df.copy() local_vars = { 'df': df_copy, @@ -200,11 +200,11 @@ class IndicatorCaller: 'np': np, 'pd': pd, 'params': merged_params, - # 递归调用支持 + # Recursive call support 'call_indicator': lambda ref, d, p=None: self.call_indicator(ref, d, p, _depth + 1) } - # 安全执行 + # Safe execution import builtins def safe_import(name, *args, **kwargs): allowed_modules = ['numpy', 'pandas', 'math', 'json', 'time'] @@ -236,19 +236,19 @@ class IndicatorCaller: self._call_stack.pop() def _get_indicator_code(self, indicator_ref: Any) -> Tuple[Optional[str], Optional[int]]: - """获取指标代码""" + """Get indicator code""" try: with get_db_connection() as db: cursor = db.cursor() if isinstance(indicator_ref, int): - # 按ID查询 + # Query by ID cursor.execute(""" SELECT id, code FROM qd_indicator_codes WHERE id = %s AND (user_id = %s OR publish_to_community = 1) """, (indicator_ref, self.user_id)) else: - # 按名称查询(优先自己的指标) + # Query by name (priority to own indicators) cursor.execute(""" SELECT id, code FROM qd_indicator_codes WHERE name = %s AND user_id = %s diff --git a/backend_api_python/app/services/kline.py b/backend_api_python/app/services/kline.py index d78e876..618c9e3 100644 --- a/backend_api_python/app/services/kline.py +++ b/backend_api_python/app/services/kline.py @@ -1,5 +1,5 @@ """ -K线数据服务 +K-line data service """ from typing import Dict, List, Any, Optional @@ -12,7 +12,7 @@ logger = get_logger(__name__) class KlineService: - """K线数据服务""" + """K-line data service""" def __init__(self): self.cache = CacheManager() @@ -27,27 +27,27 @@ class KlineService: before_time: Optional[int] = None ) -> List[Dict[str, Any]]: """ - 获取K线数据 + Get K-line data Args: - market: 市场类型 (Crypto, USStock, Forex, Futures) - symbol: 交易对/股票代码 - timeframe: 时间周期 - limit: 数据条数 - before_time: 获取此时间之前的数据 + market: market type (Crypto, USStock, Forex, Futures) + symbol: trading pair/stock code + timeframe: time period + limit: number of data items + before_time: Get data before this time Returns: - K线数据列表 + K-line data list """ - # 构建缓存键(历史数据不缓存) + # Build a cache key (historical data is not cached) if not before_time: cache_key = f"kline:{market}:{symbol}:{timeframe}:{limit}" cached = self.cache.get(cache_key) if cached: - # logger.info(f"命中缓存: {cache_key}") + # logger.info(f"Hit cache: {cache_key}") return cached - # 获取数据 + # Get data klines = DataSourceFactory.get_kline( market=market, symbol=symbol, @@ -56,16 +56,16 @@ class KlineService: before_time=before_time ) - # 设置缓存(仅最新数据) + # Set cache (latest data only) if klines and not before_time: ttl = self.cache_ttl.get(timeframe, 300) self.cache.set(cache_key, klines, ttl) - # logger.info(f"缓存设置: {cache_key}, TTL: {ttl}s") + # logger.info(f"Cache settings: {cache_key}, TTL: {ttl}s") return klines def get_latest_price(self, market: str, symbol: str) -> Optional[Dict[str, Any]]: - """获取最新价格(使用1分钟K线,已弃用,建议使用 get_realtime_price)""" + """Get the latest price (use 1-minute K-line, deprecated, it is recommended to use get_realtime_price)""" klines = self.get_kline(market, symbol, '1m', 1) if klines: return klines[-1] @@ -73,29 +73,29 @@ class KlineService: def get_realtime_price(self, market: str, symbol: str, force_refresh: bool = False) -> Dict[str, Any]: """ - 获取实时价格(优先使用 ticker API,降级使用分钟 K 线) + Get real-time prices (priority to use ticker API, downgrade to minute K-line) Args: - market: 市场类型 (Crypto, USStock, Forex, Futures) - symbol: 交易对/股票代码 - force_refresh: 是否强制刷新(跳过缓存) + market: market type (Crypto, USStock, Forex, Futures) + symbol: trading pair/stock code + force_refresh: whether to force refresh (skip cache) Returns: - 实时价格数据: { - 'price': 最新价格, - 'change': 涨跌额, - 'changePercent': 涨跌幅, - 'high': 最高价, - 'low': 最低价, - 'open': 开盘价, - 'previousClose': 昨收价, - 'source': 数据来源 ('ticker' 或 'kline') + Real-time price data: { + 'price': latest price, + 'change': change amount, + 'changePercent': increase or decrease, + 'high': highest price, + 'low': lowest price, + 'open': opening price, + 'previousClose': yesterday's closing price, + 'source': data source ('ticker' or 'kline') } """ - # 构建缓存键(短时间缓存,避免频繁请求) + # Build a cache key (short-term cache to avoid frequent requests) cache_key = f"realtime_price:{market}:{symbol}" - # 如果不是强制刷新,尝试使用缓存 + # If it is not a forced refresh, try using caching if not force_refresh: cached = self.cache.get(cache_key) if cached: @@ -112,7 +112,7 @@ class KlineService: 'source': 'unknown' } - # 优先尝试使用 ticker API 获取实时价格 + # First try to use the ticker API to get real-time prices try: ticker = DataSourceFactory.get_ticker(market, symbol) if ticker and ticker.get('last', 0) > 0: @@ -126,13 +126,13 @@ class KlineService: 'previousClose': ticker.get('previousClose', 0), 'source': 'ticker' } - # 缓存 30 秒 + # Cache for 30 seconds self.cache.set(cache_key, result, 30) return result except Exception as e: logger.debug(f"Ticker API failed for {market}:{symbol}, falling back to kline: {e}") - # 降级:使用 1 分钟 K 线 + # Downgrade: Use 1 minute candlestick try: klines = self.get_kline(market, symbol, '1m', 2) if klines and len(klines) > 0: @@ -153,13 +153,13 @@ class KlineService: 'previousClose': prev_close, 'source': 'kline_1m' } - # 缓存 30 秒 + # Cache for 30 seconds self.cache.set(cache_key, result, 30) return result except Exception as e: logger.debug(f"1m kline failed for {market}:{symbol}, trying daily: {e}") - # 最后降级:使用日线数据(适用于非交易时间) + # Last downgrade: using daily data (applies to non-trading hours) try: klines = self.get_kline(market, symbol, '1D', 2) if klines and len(klines) > 0: @@ -180,7 +180,7 @@ class KlineService: 'previousClose': prev_close, 'source': 'kline_1d' } - # 日线数据缓存 5 分钟 + # Daily data cache for 5 minutes self.cache.set(cache_key, result, 300) return result except Exception as e: diff --git a/backend_api_python/app/services/live_trading/execution.py b/backend_api_python/app/services/live_trading/execution.py index ecd8fff..f7bc9d5 100644 --- a/backend_api_python/app/services/live_trading/execution.py +++ b/backend_api_python/app/services/live_trading/execution.py @@ -41,37 +41,37 @@ MT5Client = None def _normalize_symbol_for_order(symbol: str, market_type: str = "swap") -> str: """ - 规范化符号格式,确保符号符合交易所要求。 + Standardize symbol formats to ensure symbols comply with exchange requirements. - 处理各种输入格式: + Handles various input formats: - BTC/USDT -> BTC/USDT - BTCUSDT -> BTC/USDT - BTC/USDT:USDT -> BTC/USDT - - PI, TRX -> PI/USDT, TRX/USDT (默认添加 /USDT) + - PI, TRX -> PI/USDT, TRX/USDT (/USDT is added by default) Args: - symbol: 原始符号 - market_type: 市场类型 (spot/swap) + symbol: original symbol + market_type: market type (spot/swap) Returns: - 规范化后的符号 + normalized symbols """ if not symbol: return symbol sym = symbol.strip() - # 移除 swap/futures 后缀 + # Remove swap/futures suffix if ':' in sym: sym = sym.split(':', 1)[0] sym = sym.upper() - # 如果已经有分隔符,直接返回(假设格式正确) + # If there is already a separator, return it directly (assuming the format is correct) if '/' in sym: return sym - # 尝试从常见报价货币中识别 + # Try to identify from common quote currencies common_quotes = ['USDT', 'USD', 'BTC', 'ETH', 'BUSD', 'USDC'] for quote in common_quotes: if sym.endswith(quote) and len(sym) > len(quote): @@ -79,7 +79,7 @@ def _normalize_symbol_for_order(symbol: str, market_type: str = "swap") -> str: if base: return f"{base}/{quote}" - # 如果无法识别,默认使用 USDT + # If not recognized, USDT will be used by default. return f"{sym}/USDT" @@ -148,7 +148,7 @@ def place_order_from_signal( if mt == "spot" and ("short" in (signal_type or "").lower()): raise LiveTradingError("spot market does not support short signals") - # 规范化符号格式(统一处理裸符号如 PI, TRX 等) + # Standardized symbol format (unified processing of bare symbols such as PI, TRX, etc.) symbol = _normalize_symbol_for_order(symbol, market_type=mt) if isinstance(client, BinanceFuturesClient): diff --git a/backend_api_python/app/services/live_trading/symbols.py b/backend_api_python/app/services/live_trading/symbols.py index d20e582..400e6d3 100644 --- a/backend_api_python/app/services/live_trading/symbols.py +++ b/backend_api_python/app/services/live_trading/symbols.py @@ -15,18 +15,18 @@ from typing import Dict, Tuple def _split_base_quote(symbol: str) -> Tuple[str, str]: """ - 分割符号为基础货币和报价货币。 + The split symbols are base currency and quote currency. - 处理各种格式: + Handle various formats: - BTC/USDT -> (BTC, USDT) - - BTCUSDT -> (BTCUSDT, "") - 需要进一步处理 - - PI, TRX -> (PI, "") - 需要进一步处理 + - BTCUSDT -> (BTCUSDT, "") - requires further processing + - PI, TRX -> (PI, "") - requires further processing """ s = (symbol or "").strip() if ":" in s: s = s.split(":", 1)[0] if "/" not in s: - # 尝试识别报价货币(常见格式:BASEQUOTE) + # Try to identify the quote currency (common format: BASEQUOTE) s_upper = s.upper() common_quotes = ['USDT', 'USD', 'BTC', 'ETH', 'BUSD', 'USDC', 'BNB'] for quote in common_quotes: @@ -34,7 +34,7 @@ def _split_base_quote(symbol: str) -> Tuple[str, str]: base = s_upper[:-len(quote)] if base: return base, quote - # 无法识别,返回原符号和空报价 + # Unrecognized, the original symbol and empty quote are returned. return s_upper, "" base, quote = s.split("/", 1) return base.strip().upper(), quote.strip().upper() diff --git a/backend_api_python/app/services/market_data_collector.py b/backend_api_python/app/services/market_data_collector.py index 4044240..68d5fba 100644 --- a/backend_api_python/app/services/market_data_collector.py +++ b/backend_api_python/app/services/market_data_collector.py @@ -1,17 +1,17 @@ """ -市场数据采集服务 - AI分析专用 +Market data collection service - dedicated to AI analysis -设计理念: -1. 数据为王 - 先把数据获取做好、做稳定 -2. 统一数据源 - 完全复用 DataSourceFactory 和 kline_service -3. 复用全球金融板块 - 宏观数据、情绪数据复用 global_market.py 的缓存 -4. 快速稳定 - 不依赖慢速外部服务(如Jina Reader) +Design concept: +1. Data is king - first obtain data well and make it stable +2. Unified data source - completely reuse DataSourceFactory and kline_service +3. Reuse the global financial sector - reuse the cache of global_market.py for macro data and sentiment data +4. Fast and stable - does not rely on slow external services (such as Jina Reader) -数据源映射: -- 价格/K线: DataSourceFactory (已验证,与K线模块、自选列表一致) -- 宏观数据: 复用 global_market.py (VIX, DXY, TNX, Fear&Greed等,带缓存) -- 新闻: Finnhub API (结构化数据,无需深度阅读) -- 基本面: Finnhub (美股) / 固定描述 (加密) +Data source mapping: +- Price/K-line: DataSourceFactory (verified, consistent with K-line module and self-selected list) +- Macro data: reuse global_market.py (VIX, DXY, TNX, Fear&Greed, etc., with cache) +- News: Finnhub API (structured data, no in-depth reading required) +- Fundamentals: Finnhub (US stocks) / Fixed description (crypto) """ import time @@ -32,15 +32,15 @@ logger = get_logger(__name__) class MarketDataCollector: """ - 市场数据采集器 + Market data collector - 职责:为AI分析提供完整、准确、及时的市场数据 + Responsibilities: Provide complete, accurate and timely market data for AI analysis - 数据层次: - 1. 核心数据 (必须成功): 价格、K线 - 2. 分析数据 (增强): 技术指标、基本面 - 3. 宏观数据 (可选): 复用 global_market.py (VIX, DXY, TNX, Fear&Greed等) - 4. 情绪数据 (可选): 新闻、市场情绪 + Data level: + 1. Core data (must succeed): price, K-line + 2. Analyze data (enhanced): technical indicators, fundamentals + 3. Macro data (optional): reuse global_market.py (VIX, DXY, TNX, Fear&Greed, etc.) + 4. Sentiment data (optional): news, market sentiment """ def __init__(self): @@ -50,7 +50,7 @@ class MarketDataCollector: self._init_clients() def _init_clients(self): - """初始化外部API客户端""" + """Initialize external API client""" # Finnhub finnhub_key = APIKeys.FINNHUB_API_KEY if finnhub_key: @@ -74,23 +74,23 @@ class MarketDataCollector: timeframe: str = "1D", include_macro: bool = True, include_news: bool = True, - include_polymarket: bool = True, # 新增:是否包含预测市场数据 + include_polymarket: bool = True, # New: Whether to include prediction market data timeout: int = 30 ) -> Dict[str, Any]: """ - 采集所有市场数据 + Collect all market data Args: - market: 市场类型 (USStock, Crypto, Forex, Futures) - symbol: 标的代码 - timeframe: K线周期 - include_macro: 是否包含宏观数据 - include_news: 是否包含新闻 - include_polymarket: 是否包含预测市场数据 - timeout: 总超时时间(秒) + market: market type (USStock, Crypto, Forex, Futures) + symbol: target code + timeframe: K-line cycle + include_macro: whether to include macro data + include_news: whether to include news + include_polymarket: whether to include prediction market data + timeout: total timeout (seconds) Returns: - 完整的市场数据字典 + Complete Market Data Dictionary """ start_time = time.time() @@ -99,21 +99,21 @@ class MarketDataCollector: "symbol": symbol, "timeframe": timeframe, "collected_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - # 核心数据 + # core data "price": None, "kline": None, "indicators": {}, - # 基本面 + # Fundamentals "fundamental": {}, "company": {}, - # 宏观 + # Macro "macro": {}, - # 情绪 + # mood "news": [], "sentiment": {}, - # 预测市场 + # prediction market "polymarket": [], - # 元数据 + # metadata "_meta": { "success_items": [], "failed_items": [], @@ -121,19 +121,19 @@ class MarketDataCollector: } } - # === 阶段1: 核心数据 (并行获取) === + # === Phase 1: Core data (parallel acquisition) === with ThreadPoolExecutor(max_workers=4) as executor: core_futures = { executor.submit(self._get_price, market, symbol): "price", executor.submit(self._get_kline, market, symbol, timeframe, 60): "kline", } - # 如果需要基本面,也并行获取 + # If fundamentals are needed, also obtain them in parallel if market == 'USStock': core_futures[executor.submit(self._get_fundamental, market, symbol)] = "fundamental" core_futures[executor.submit(self._get_company, market, symbol)] = "company" elif market == 'Crypto': - # 加密货币的"基本面"是固定描述 + # Cryptocurrency 'fundamentals' are fixed description core_futures[executor.submit(self._get_crypto_info, symbol)] = "fundamental" try: @@ -152,12 +152,12 @@ class MarketDataCollector: except TimeoutError: logger.warning(f"Core data fetch timed out for {market}:{symbol}") - # 计算技术指标 (本地计算,不需要外部API) + # Calculate technical indicators (local calculation, no external API required) if data.get("kline"): data["indicators"] = self._calculate_indicators(data["kline"]) data["_meta"]["success_items"].append("indicators") - # === 阶段2: 宏观数据 (如果需要) === + # === Stage 2: Macro data (if required) === if include_macro: try: data["macro"] = self._get_macro_data(market, timeout=10) @@ -167,10 +167,10 @@ class MarketDataCollector: logger.warning(f"Macro data fetch failed: {e}") data["_meta"]["failed_items"].append("macro") - # === 阶段3: 新闻/情绪 (如果需要) === + # === Stage 3: News/Sentiment (if needed) === if include_news: try: - # 获取公司名称以改善搜索 + # Get company name to improve search company_name = None if data.get("company"): company_name = data["company"].get("name") @@ -185,7 +185,7 @@ class MarketDataCollector: logger.warning(f"News fetch failed: {e}") data["_meta"]["failed_items"].append("news") - # === 阶段4: 预测市场数据 (如果需要) === + # === Stage 4: Prediction market data (if required) === if include_polymarket: try: polymarket_events = self._get_polymarket_events(symbol, market) @@ -196,7 +196,7 @@ class MarketDataCollector: logger.debug(f"Polymarket data fetch failed: {e}") data["_meta"]["failed_items"].append("polymarket") - # 记录总耗时 + # Record the total time spent data["_meta"]["duration_ms"] = int((time.time() - start_time) * 1000) logger.info(f"Market data collection completed for {market}:{symbol} in {data['_meta']['duration_ms']}ms") logger.info(f" Success: {data['_meta']['success_items']}") @@ -204,16 +204,16 @@ class MarketDataCollector: return data - # ==================== 核心数据获取 ==================== + # ==================== Core data acquisition ==================== def _get_price(self, market: str, symbol: str) -> Optional[Dict[str, Any]]: """ - 获取实时价格 - 使用 kline_service (与自选列表一致) + Get live prices - use kline_service (same as watchlist) """ try: price_data = self.kline_service.get_realtime_price(market, symbol, force_refresh=True) if price_data and price_data.get('price', 0) > 0: - # 安全转换为 float,处理 None 值 + # Safe conversion to float, handling None values def safe_float(val, default=0.0): if val is None: return default @@ -236,7 +236,7 @@ class MarketDataCollector: except Exception as e: logger.warning(f"Price fetch failed for {market}:{symbol}: {e}") - # 如果 kline_service 失败,尝试从 K 线最后一根获取价格 + # If kline_service fails, try to get the price from the last K-line try: klines = DataSourceFactory.get_kline(market, symbol, "1D", 2) if klines and len(klines) > 0: @@ -267,7 +267,7 @@ class MarketDataCollector: self, market: str, symbol: str, timeframe: str, limit: int = 60 ) -> Optional[List[Dict[str, Any]]]: """ - 获取K线数据 - 使用 DataSourceFactory (与K线模块一致) + Get K-line data - use DataSourceFactory (consistent with K-line module) """ try: klines = DataSourceFactory.get_kline(market, symbol, timeframe, limit) @@ -279,14 +279,14 @@ class MarketDataCollector: def _calculate_indicators(self, klines: List[Dict[str, Any]]) -> Dict[str, Any]: """ - 计算技术指标 (本地计算,无外部依赖) + Calculate technical indicators (local calculation, no external dependencies) - 返回格式符合前端 FastAnalysisReport.vue 的期望。 - 口径说明(与常见行情终端对齐): - - RSI(14):Wilder 平滑(首段均幅为前 14 期简单平均,其后递推)。 - - MACD:收盘 EMA12/EMA26(首值=前 N 日 SMA),信号线=MACD 的 EMA9(SMA 种子)。 - - MA:SMA。枢轴:上一根 K 的 H/L/C。摆动高低:近 20 根 H/L 窗口极值。 - - 布林:20 收盘 SMA ± 2×总体标准差。ATR(14):Wilder(首 ATR=前 14 期 TR 简单平均,其后递推)。 + The return format meets the expectations of the front-end FastAnalysisReport.vue. + Caliber description (aligned with common market terminals): + - RSI(14): Wilder smoothing (the average amplitude of the first period is the simple average of the previous 14 periods, and then recursively). + - MACD: Closing EMA12/EMA26 (first value = SMA of the previous N days), signal line = EMA9 of MACD (SMA seed). + - MA: SMA. Pivot: H/L/C of the previous king. Swing High and Low: Near 20 H/L window extremes. + - Bollinger: 20 closing SMA ± 2× population standard deviation. ATR(14): Wilder (first ATR = simple average of TR in the first 14 periods, followed by recursion). """ if not klines or len(klines) < 5: return {} @@ -317,7 +317,7 @@ class MarketDataCollector: 'signal': rsi_signal, } - # ========== MACD(SMA 种子 EMA,与常见终端一致)========== + # ========== MACD (SMA seed EMA, consistent with common terminals) ========== if len(closes) >= 34: macd_raw = self._calc_macd(closes) macd_val = macd_raw.get('MACD', 0) @@ -342,7 +342,7 @@ class MarketDataCollector: 'trend': macd_trend, } - # ========== 移动平均线 ========== + # ========== Moving Average ========== ma5 = sum(closes[-5:]) / 5 if len(closes) >= 5 else current_price ma10 = sum(closes[-10:]) / 10 if len(closes) >= 10 else current_price ma20 = sum(closes[-20:]) / 20 if len(closes) >= 20 else current_price @@ -365,39 +365,39 @@ class MarketDataCollector: 'trend': ma_trend, } - # 先算布林带,供下方合成支撑/阻力使用(键名 BB_upper / BB_lower) + # Calculate the Bollinger Bands first and use them to synthesize support/resistance below (key name BB_upper / BB_lower) bb_for_levels: Dict[str, Any] = {} if len(closes) >= 20: bb_for_levels = self._calc_bollinger(closes, 20, 2) or {} - # ========== 支撑/阻力位 (多种方法综合) ========== - # 方法1: 枢轴点 (Pivot Points) - 使用前一日数据 + # ========== Support/Resistance Level (Several Methods Comprehensive) ========== + # Method 1: Pivot Points - Use previous day's data if len(klines) >= 2: prev_high = float(klines[-2].get('high', highs[-2]) if len(highs) >= 2 else current_price * 1.02) prev_low = float(klines[-2].get('low', lows[-2]) if len(lows) >= 2 else current_price * 0.98) prev_close = float(klines[-2].get('close', closes[-2]) if len(closes) >= 2 else current_price) pivot = (prev_high + prev_low + prev_close) / 3 - r1 = 2 * pivot - prev_low # 阻力位1 - s1 = 2 * pivot - prev_high # 支撑位1 - r2 = pivot + (prev_high - prev_low) # 阻力位2 - s2 = pivot - (prev_high - prev_low) # 支撑位2 + r1 = 2 * pivot - prev_low # Resistance level 1 + s1 = 2 * pivot - prev_high # Support level 1 + r2 = pivot + (prev_high - prev_low) # Resistance Level 2 + s2 = pivot - (prev_high - prev_low) # Support level 2 else: pivot = current_price r1 = r2 = current_price * 1.02 s1 = s2 = current_price * 0.98 - # 方法2: 近期高低点 + # Method 2: Recent highs and lows recent_highs = highs[-20:] if len(highs) >= 20 else highs recent_lows = lows[-20:] if len(lows) >= 20 else lows swing_high = max(recent_highs) if recent_highs else current_price * 1.05 swing_low = min(recent_lows) if recent_lows else current_price * 0.95 - # 方法3: 布林上下轨(与 _calc_bollinger 返回字段一致) + # Method 3: Bollinger upper and lower rails (consistent with the _calc_bollinger return field) bb_upper = bb_for_levels.get('BB_upper', swing_high) bb_lower = bb_for_levels.get('BB_lower', swing_low) - # 综合取值: 取多种方法的平均/加权 + # Comprehensive value: average/weighted by multiple methods resistance = round((r1 + swing_high + bb_upper) / 3, 6) support = round((s1 + swing_low + bb_lower) / 3, 6) @@ -411,10 +411,10 @@ class MarketDataCollector: 'r2': round(r2, 6), 'swing_high': round(swing_high, 6), 'swing_low': round(swing_low, 6), - 'method': 'pivot_swing_bb_avg' # 标注计算方法 + 'method': 'pivot_swing_bb_avg' # Label calculation method } - # ========== ATR 和波动率(Wilder ATR,全序列递推至最新一根)========== + # ========== ATR and volatility (Wilder ATR, the whole sequence is recursively recursed to the latest one) ========== atr = 0.0 if len(klines) >= 14: atr = float(self._calc_atr_wilder(klines, period=14)) @@ -433,21 +433,21 @@ class MarketDataCollector: indicators['volatility'] = { 'level': volatility_level, 'pct': round(volatility_pct, 2), - 'atr': round(atr, 6), # 添加 ATR 绝对值 + 'atr': round(atr, 6), # Add ATR absolute value } - # ========== 止盈止损建议 (基于 ATR 和支撑/阻力) ========== - # 止损: 基于 2x ATR 或支撑位,取更保守的 + # ========== Take Profit and Stop Loss Recommendations (Based on ATR and Support/Resistance) ========== + # Stop Loss: Based on 2x ATR or support, whichever is more conservative atr_stop_loss = current_price - (2 * atr) if atr > 0 else current_price * 0.95 support_stop = indicators['levels']['support'] - suggested_stop_loss = max(atr_stop_loss, support_stop * 0.99) # 略低于支撑位 + suggested_stop_loss = max(atr_stop_loss, support_stop * 0.99) # Just below support - # 止盈: 基于 3x ATR 或阻力位,考虑风险回报比 + # Take Profit: Based on 3x ATR or resistance level, considering risk reward ratio atr_take_profit = current_price + (3 * atr) if atr > 0 else current_price * 1.05 resistance_tp = indicators['levels']['resistance'] - suggested_take_profit = min(atr_take_profit, resistance_tp * 1.01) # 略高于阻力位 + suggested_take_profit = min(atr_take_profit, resistance_tp * 1.01) # Just above resistance - # 风险回报比 + # risk reward ratio risk = current_price - suggested_stop_loss reward = suggested_take_profit - current_price risk_reward_ratio = round(reward / risk, 2) if risk > 0 else 0 @@ -456,21 +456,21 @@ class MarketDataCollector: 'suggested_stop_loss': round(suggested_stop_loss, 6), 'suggested_take_profit': round(suggested_take_profit, 6), 'risk_reward_ratio': risk_reward_ratio, - 'atr_multiplier_sl': 2.0, # 止损使用 2x ATR - 'atr_multiplier_tp': 3.0, # 止盈使用 3x ATR + 'atr_multiplier_sl': 2.0, # Stop loss using 2x ATR + 'atr_multiplier_tp': 3.0, # Take profit using 3x ATR 'method': 'atr_support_resistance' } - # ========== 布林带 (附加,与 bb_for_levels 同一次计算) ========== + # ========== Bollinger Bands (additional, same calculation as bb_for_levels) ========== if bb_for_levels: indicators['bollinger'] = bb_for_levels - # ========== 成交量 (附加) ========== + # ========== Volume (Additional) ========== if len(volumes) >= 20: avg_vol = sum(volumes[-20:]) / 20 indicators['volume_ratio'] = round(volumes[-1] / avg_vol, 2) if avg_vol > 0 else 1.0 - # ========== 价格位置 (附加) ========== + # ========== Price Position (Additional) ========== if len(closes) >= 20: high_20 = max(highs[-20:]) low_20 = min(lows[-20:]) @@ -479,7 +479,7 @@ class MarketDataCollector: else: indicators['price_position'] = 50.0 - # ========== 整体趋势 (附加) ========== + # ========== Overall Trends (Additional) ========== indicators['trend'] = ma_trend indicators['current_price'] = round(current_price, 6) @@ -490,7 +490,7 @@ class MarketDataCollector: return {} def _calc_rsi(self, closes: List[float], period: int = 14) -> float: - """Wilder RSI:首段均幅为前 period 期涨跌简单平均,之后按 Wilder 平滑递推。""" + """Wilder RSI: The average amplitude of the first period is a simple average of the rise and fall of the previous period, and then it is smoothed by Wilder.""" if len(closes) < period + 1: return 50.0 @@ -516,8 +516,8 @@ class MarketDataCollector: def _ema_series_sma_seed(self, data: List[float], period: int) -> List[Optional[float]]: """ - 标准 EMA:首值 = 前 period 根简单平均(SMA),之后 EMA_t = (P_t - EMA_{t-1}) * k + EMA_{t-1},k=2/(period+1)。 - 前 period-1 根无定义,返回 None。 + Standard EMA: first value = first period root simple average (SMA), then EMA_t = (P_t - EMA_{t-1}) * k + EMA_{t-1}, k=2/(period+1). + The first period-1 root is undefined and None is returned. """ n = len(data) out: List[Optional[float]] = [None] * n @@ -535,7 +535,7 @@ class MarketDataCollector: def _calc_macd(self, closes: List[float]) -> Dict[str, float]: """ MACD(12,26,9):DIF = EMA12(close) − EMA26(close),DEA = EMA9(DIF),柱 = DIF − DEA。 - 各 EMA 均采用 SMA 种子;DIF 自第 26 根 K 起有定义,信号线对 DIF 子序列再算 EMA9。 + Each EMA uses SMA seeds; DIF is defined from the 26th K onwards, and EMA9 is calculated for the DIF subsequence of the signal line pair. """ n = len(closes) ema12 = self._ema_series_sma_seed(closes, 12) @@ -566,7 +566,7 @@ class MarketDataCollector: } def _true_ranges(self, klines: List[Dict[str, Any]]) -> List[float]: - """每根 K 的 True Range(首根仅 H−L)。""" + """True Range for each root of K (the first root is only H−L).""" trs: List[float] = [] for i, k in enumerate(klines): h = float(k.get('high', 0)) @@ -582,7 +582,7 @@ class MarketDataCollector: return trs def _calc_atr_wilder(self, klines: List[Dict[str, Any]], period: int = 14) -> float: - """Wilder ATR:首 ATR = 前 period 期 TR 简单平均,之后 ATR_t = (ATR_{t-1}*(period-1)+TR_t)/period。""" + """Wilder ATR: First ATR = simple average of TR in the previous period, then ATR_t = (ATR_{t-1}*(period-1)+TR_t)/period.""" trs = self._true_ranges(klines) if len(trs) < period: return 0.0 @@ -592,7 +592,7 @@ class MarketDataCollector: return atr def _calc_bollinger(self, closes: List[float], period: int = 20, std_dev: int = 2) -> Dict[str, float]: - """布林带:中轨为 period 收盘 SMA,σ 为总体标准差(方差/period),上下轨=中轨±std_dev×σ。""" + """Bollinger Bands: The middle rail is the period closing SMA, σ is the overall standard deviation (variance/period), the upper and lower rails = middle rail ±std_dev×σ.""" if len(closes) < period: return {} @@ -609,10 +609,10 @@ class MarketDataCollector: 'BB_width': round((std_dev * std * 2) / middle * 100, 2) if middle > 0 else 0 } - # ==================== 基本面数据 ==================== + # ==================== Fundamental data ==================== def _get_fundamental(self, market: str, symbol: str) -> Optional[Dict[str, Any]]: - """获取基本面数据""" + """Get fundamental data""" try: if market == 'USStock': return self._get_us_fundamental(symbol) @@ -622,12 +622,12 @@ class MarketDataCollector: def _get_us_fundamental(self, symbol: str) -> Optional[Dict[str, Any]]: """ - 美股基本面 - Finnhub + yfinance - 包括:基础财务指标 + 财报数据(资产负债表、利润表、现金流量表) + US stock fundamentals - Finnhub + yfinance + Includes: basic financial indicators + financial report data (balance sheet, income statement, cash flow statement) """ result = {} - # === 1. 基础财务指标 (Finnhub) === + # === 1. Basic financial indicators (Finnhub) === if self._finnhub_client: try: metrics = self._finnhub_client.company_basic_financials(symbol, 'all') @@ -653,12 +653,12 @@ class MarketDataCollector: except Exception as e: logger.debug(f"Finnhub fundamental failed for {symbol}: {e}") - # === 2. yfinance 补充基础指标 === + # === 2. yfinance supplements basic indicators === try: ticker = yf.Ticker(symbol) info = ticker.info or {} - # 补充缺失的基础指标 + # Supplement missing basic indicators if not result.get('pe_ratio'): result['pe_ratio'] = info.get('trailingPE') or info.get('forwardPE') if not result.get('pb_ratio'): @@ -678,7 +678,7 @@ class MarketDataCollector: if not result.get('eps'): result['eps'] = info.get('trailingEps') - # 补充更多财务指标 + # Add more financial indicators result.update({ 'revenue': info.get('totalRevenue'), 'gross_profit': info.get('grossProfits'), @@ -695,12 +695,12 @@ class MarketDataCollector: except Exception as e: logger.debug(f"yfinance fundamental failed for {symbol}: {e}") - # === 3. 获取财报数据(资产负债表、利润表、现金流量表)=== + # === 3. Obtain financial report data (balance sheet, income statement, cash flow statement) === financial_statements = self._get_financial_statements(symbol) if financial_statements: result['financial_statements'] = financial_statements - # === 4. 获取盈利报告(Earnings)=== + # === 4. Get Earnings === earnings_data = self._get_earnings_data(symbol) if earnings_data: result['earnings'] = earnings_data @@ -709,19 +709,19 @@ class MarketDataCollector: def _get_financial_statements(self, symbol: str) -> Optional[Dict[str, Any]]: """ - 获取财务报表数据(资产负债表、利润表、现金流量表) + Obtain financial statement data (balance sheet, income statement, cash flow statement) - 使用 yfinance 获取,包含最近几个季度的数据 + Use yfinance to obtain, including data for recent quarters """ try: ticker = yf.Ticker(symbol) statements = {} - # 资产负债表 (Balance Sheet) + # Balance Sheet try: balance_sheet = ticker.balance_sheet if balance_sheet is not None and not balance_sheet.empty: - # 获取最近4个季度 + # Get the last 4 quarters latest_quarters = balance_sheet.columns[:4] if len(balance_sheet.columns) >= 4 else balance_sheet.columns statements['balance_sheet'] = { 'latest_date': str(latest_quarters[0]) if len(latest_quarters) > 0 else None, @@ -736,7 +736,7 @@ class MarketDataCollector: except Exception as e: logger.debug(f"Balance sheet fetch failed for {symbol}: {e}") - # 利润表 (Income Statement) + # Income Statement try: income_stmt = ticker.financials if income_stmt is not None and not income_stmt.empty: @@ -752,7 +752,7 @@ class MarketDataCollector: except Exception as e: logger.debug(f"Income statement fetch failed for {symbol}: {e}") - # 现金流量表 (Cash Flow Statement) + # Cash Flow Statement try: cashflow = ticker.cashflow if cashflow is not None and not cashflow.empty: @@ -775,10 +775,10 @@ class MarketDataCollector: def _get_earnings_data(self, symbol: str) -> Optional[Dict[str, Any]]: """ - 获取盈利报告数据(Earnings) + Get earnings report data (Earnings) - 使用 quarterly_income_stmt 替代已弃用的 Ticker.earnings / quarterly_earnings, - 历史季度摘要从利润表推导;盈利日历仍用 ticker.calendar(若可用)。 + Use quarterly_income_stmt instead of deprecated Ticker.earnings / quarterly_earnings, + Historical quarterly summaries are derived from the income statement; the earnings calendar still uses ticker.calendar (if available). """ def _pick_float(stmt: pd.DataFrame, row_names: tuple, col) -> Optional[float]: for name in row_names: @@ -796,7 +796,7 @@ class MarketDataCollector: ticker = yf.Ticker(symbol) earnings_data: Dict[str, Any] = {} - # 季度利润表(yfinance 推荐路径,避免 fundamentals.Ticker.earnings 弃用告警) + # Quarterly income statement (yfinance recommended path to avoid fundamentals.Ticker.earnings deprecation warning) try: q_inc = ticker.quarterly_income_stmt if q_inc is not None and not q_inc.empty and len(q_inc.columns) > 0: @@ -824,7 +824,7 @@ class MarketDataCollector: "earnings": ni, } - # 最近若干季度 EPS(来自利润表行,非一致预期) + # EPS for recent quarters (from income statement lines, not consensus estimates) earnings_data["history"] = [] for col in cols: eps = _pick_float(q_inc, ("Diluted EPS", "Basic EPS"), col) @@ -837,7 +837,7 @@ class MarketDataCollector: except Exception as e: logger.debug(f"Quarterly income statement (earnings) fetch failed for {symbol}: {e}") - # 盈利日历(未来盈利日期与一致预期) + # Earnings Calendar (Future Earnings Dates and Consensus Expectations) try: earnings_calendar = ticker.calendar if earnings_calendar is not None and not earnings_calendar.empty: @@ -861,8 +861,8 @@ class MarketDataCollector: return None def _get_crypto_info(self, symbol: str) -> Optional[Dict[str, Any]]: - """加密货币信息 (固定描述为主)""" - # 常见加密货币的描述 + """Cryptocurrency information (mainly fixed description)""" + # Descriptions of common cryptocurrencies crypto_info = { 'BTC': { 'name': 'Bitcoin', @@ -896,7 +896,7 @@ class MarketDataCollector: }, } - # 提取基础代币名 + # Extract base token name base = symbol.split('/')[0] if '/' in symbol else symbol base = base.upper() @@ -910,7 +910,7 @@ class MarketDataCollector: } def _get_company(self, market: str, symbol: str) -> Optional[Dict[str, Any]]: - """获取公司信息""" + """Get company information""" try: if market == 'USStock' and self._finnhub_client: profile = self._finnhub_client.company_profile2(symbol=symbol) @@ -930,19 +930,19 @@ class MarketDataCollector: return None - # ==================== 宏观数据 (复用全球金融板块) ==================== + # ==================== Macro data (reusing the global financial sector) ==================== def _get_macro_data(self, market: str, timeout: int = 10) -> Dict[str, Any]: """ - 获取宏观经济数据 - 复用 global_market.py 的函数和缓存 + Get macroeconomic data - reuse global_market.py functions and cache - 优势: - 1. 数据与全球金融页面一致 - 2. 复用30秒/5分钟缓存,降低API调用 - 3. 已有完整的数据解读和级别判断 + Advantages: + 1. The data is consistent with the global financial page + 2. Reuse 30 seconds/5 minutes cache to reduce API calls + 3. Have complete data interpretation and level judgment """ try: - # 复用 global_market.py 的市场情绪数据 (有5分钟缓存) + # Reuse market sentiment data from global_market.py (with 5-minute cache) from app.routes.global_market import ( _fetch_vix, _fetch_dollar_index, _fetch_yield_curve, _fetch_fear_greed_index, @@ -951,12 +951,12 @@ class MarketDataCollector: result = {} - # 1) 尝试从缓存获取 (global_market 的缓存, 6小时有效) + # 1) Try to get it from cache (global_market cache, valid for 6 hours) MACRO_CACHE_TTL = 21600 # 6 hours cached_sentiment = _get_cached("market_sentiment", MACRO_CACHE_TTL) if cached_sentiment: logger.info("Using cached sentiment data from global_market (6h cache)") - # 转换格式 + # Convert format if cached_sentiment.get('vix'): vix = cached_sentiment['vix'] result['VIX'] = { @@ -1004,7 +1004,7 @@ class MarketDataCollector: if result: return result - # 2) 如果没有缓存,快速并行获取关键指标 + # 2) If there is no cache, quickly obtain key indicators in parallel logger.info("Fetching macro data from global_market functions") with ThreadPoolExecutor(max_workers=4) as executor: @@ -1021,7 +1021,7 @@ class MarketDataCollector: try: data = future.result(timeout=5) if data: - # 转换为统一格式 + # Convert to unified format if key == 'VIX': result[key] = { 'name': 'VIX恐慌指数', @@ -1063,9 +1063,9 @@ class MarketDataCollector: except TimeoutError: logger.warning("Macro data fetch timed out") - # 注:黄金等大宗商品数据不再作为宏观指标获取 - # 原因:1) 如果分析的是黄金,价格已在 _get_price 中获取 - # 2) 减少 API 调用,提高稳定性 + # Note: Gold and other commodity data are no longer available as macro indicators + # Reasons: 1) If the analysis is gold, the price has been obtained in _get_price + # 2) Reduce API calls and improve stability pass return result @@ -1077,23 +1077,23 @@ class MarketDataCollector: logger.error(f"_get_macro_data failed: {e}") return {} - # ==================== 新闻/情绪数据 ==================== + # ==================== News/Sentiment Data ==================== def _get_news( self, market: str, symbol: str, company_name: str = None, timeout: int = 8 ) -> Dict[str, Any]: """ - 获取新闻和情绪数据 + Get news and sentiment data - 策略(按优先级): - 1. 结构化API (Finnhub) - 美股首选 - 2. 搜索引擎 (Tavily/Google/Bing/SerpAPI) - 补充搜索 - 3. 情绪分析 - Finnhub 社交媒体情绪 + Strategies (by priority): + 1. Structured API (Finnhub) - the first choice for US stocks + 2. Search Engine (Tavily/Google/Bing/SerpAPI) - Supplementary Search + 3. Sentiment Analysis - Finnhub Social Media Sentiment """ news_list = [] sentiment = {} - # === 1) Finnhub 新闻 (美股首选) === + # === 1) Finnhub News (Preferred for US stocks) === if self._finnhub_client: try: end_date = datetime.now().strftime('%Y-%m-%d') @@ -1104,10 +1104,10 @@ class MarketDataCollector: if market == 'USStock': raw_news = self._finnhub_client.company_news(symbol, _from=start_date, to=end_date) elif market == 'Crypto': - # 加密货币通用新闻 + # General Cryptocurrency News raw_news = self._finnhub_client.general_news('crypto', min_id=0) else: - # 其他市场通用新闻 + # Other general market news raw_news = self._finnhub_client.general_news('general', min_id=0) if raw_news: @@ -1126,7 +1126,7 @@ class MarketDataCollector: except Exception as e: logger.debug(f"Finnhub news fetch failed: {e}") - # === 2) Finnhub 情绪分数 (美股社交媒体情绪) === + # === 2) Finnhub Sentiment Score (U.S. stock social media sentiment) === if self._finnhub_client and market == 'USStock': try: social = self._finnhub_client.stock_social_sentiment(symbol) @@ -1136,19 +1136,19 @@ class MarketDataCollector: except Exception as e: logger.debug(f"Finnhub sentiment fetch failed: {e}") - # === 3) 搜索引擎补充 (如果新闻太少) === + # === 3) Search engine supplement (if there is too little news) === if len(news_list) < 5: search_news = self._get_news_from_search(market, symbol, company_name) news_list.extend(search_news) - # === 4) 获取全球重大事件新闻(地缘政治、战争等) === - # 这些事件会影响所有市场,特别是加密货币 + # === 4) Get news on major global events (geopolitics, wars, etc.) === + # These events affect all markets, especially cryptocurrencies global_events = self._get_global_major_events() if global_events: news_list.extend(global_events) logger.info(f"Added {len(global_events)} global major events to news list") - # 去重(按标题) + # Remove duplicates (by title) seen_titles = set() unique_news = [] for item in news_list: @@ -1157,11 +1157,11 @@ class MarketDataCollector: seen_titles.add(title) unique_news.append(item) - # 按时间排序 + # Sort by time unique_news.sort(key=lambda x: x.get('datetime', ''), reverse=True) return { - "news": unique_news[:15], # 最多15条 + "news": unique_news[:15], # Maximum 15 articles "sentiment": sentiment, } @@ -1169,9 +1169,9 @@ class MarketDataCollector: self, market: str, symbol: str, company_name: str = None ) -> List[Dict[str, Any]]: """ - 从搜索引擎获取新闻 + Get news from search engines - 使用增强的搜索服务 (Tavily/Google/Bing/SerpAPI) + Use enhanced search services (Tavily/Google/Bing/SerpAPI) """ news_list = [] @@ -1182,10 +1182,10 @@ class MarketDataCollector: if not search_service.is_available: return news_list - # 构建搜索名称 + # Build search name search_name = company_name or symbol - # 搜索股票新闻 + # Search stock news response = search_service.search_stock_news( stock_code=symbol, stock_name=search_name, @@ -1211,11 +1211,11 @@ class MarketDataCollector: def _get_global_major_events(self) -> List[Dict]: """ - 获取全球重大事件新闻(地缘政治、战争、重大政策等) - 这些事件会影响所有市场,特别是加密货币 + Get news on major global events (geopolitics, wars, major policies, etc.) + These events affect all markets, especially cryptocurrencies Returns: - 全球重大事件新闻列表 + News list of major global events """ news_list = [] @@ -1226,10 +1226,10 @@ class MarketDataCollector: if not search_service.is_available: return news_list - # 搜索全球重大事件(最近24小时) - # 优化:减少搜索次数,只搜索最重要的查询 + # Search for major global events (last 24 hours) + # Optimize: Reduce the number of searches and search only the most important queries global_event_queries = [ - "war conflict breaking news today" # 只搜索最重要的查询,减少API调用 + "war conflict breaking news today" # Search only the most important queries and reduce API calls ] for query in global_event_queries: @@ -1237,17 +1237,17 @@ class MarketDataCollector: response = search_service.search_with_fallback( query=query, max_results=2, - days=1 # 只搜索最近1天的新闻 + days=1 # Search only the news of the last 1 day ) if response.success and response.results: for result in response.results: - # 检查是否是重大事件(包含关键词) + # Check if it is a major event (including keywords) title_lower = result.title.lower() snippet_lower = (result.snippet or "").lower() text = f"{title_lower} {snippet_lower}" - # 重大事件关键词 + # Major event keywords major_event_keywords = [ "war", "conflict", "military", "attack", "strike", "sanctions", "geopolitical", "crisis", "tension", "iran", "israel", "russia", @@ -1263,14 +1263,14 @@ class MarketDataCollector: "source": f"全球事件:{result.source}", "url": result.url, "sentiment": "negative" if any(kw in text for kw in ["war", "conflict", "attack", "战争", "冲突", "袭击"]) else "neutral", - "is_global_event": True # 标记为全球事件 + "is_global_event": True # Flag as global event }) logger.info(f"Found global major event: {result.title[:60]}") except Exception as e: logger.debug(f"Failed to search global events with query '{query}': {e}") continue - # 去重 + # Remove duplicates seen_titles = set() unique_events = [] for item in news_list: @@ -1279,7 +1279,7 @@ class MarketDataCollector: seen_titles.add(title) unique_events.append(item) - return unique_events[:5] # 最多返回5条全球重大事件 + return unique_events[:5] # Returns up to 5 major global events except Exception as e: logger.debug(f"Failed to get global major events: {e}") @@ -1287,33 +1287,33 @@ class MarketDataCollector: def _get_polymarket_events(self, symbol: str, market: str) -> List[Dict]: """ - 获取与资产相关的预测市场事件 - 直接调用Polymarket API获取实时数据,不依赖本地数据库 + Get prediction market events related to an asset + Directly call Polymarket API to obtain real-time data without relying on local database Args: - symbol: 资产符号 - market: 市场类型 + symbol: asset symbol + market: market type Returns: - 相关预测市场事件列表 + List of related prediction market events """ try: from app.data_sources.polymarket import PolymarketDataSource polymarket_source = PolymarketDataSource() - # 提取关键词 + # Extract keywords keywords = self._extract_polymarket_keywords(symbol, market) logger.info(f"Extracted Polymarket keywords for {symbol}: {keywords}") - # 优化:使用缓存加速,减少API调用时间 - # 对于AI分析,使用短期缓存(5分钟)即可,既保证时效性又提升性能 - # 进一步优化:限制关键词数量,只搜索最重要的关键词(最多2个) + # Optimization: Use cache acceleration to reduce API call time + # For AI analysis, use short-term cache (5 minutes) to ensure timeliness and improve performance. + # Further optimization: limit the number of keywords and search only the most important keywords (up to 2) related_markets = [] - max_keywords = 2 # 最多只搜索2个关键词,减少API调用 + max_keywords = 2 # Only search for 2 keywords at most, reducing API calls for keyword in keywords[:max_keywords]: try: - # 使用use_cache=True启用缓存,减少API调用时间 + # Use use_cache=True to enable caching and reduce API call time markets = polymarket_source.search_markets(keyword, limit=5, use_cache=True) logger.info(f"Found {len(markets)} markets for keyword '{keyword}' (cached)") related_markets.extend(markets) @@ -1321,23 +1321,23 @@ class MarketDataCollector: logger.warning(f"Failed to search Polymarket for keyword '{keyword}': {e}") continue - # 去重 + # Remove duplicates seen = set() result = [] for market_data in related_markets: market_id = market_data.get('market_id') if market_id and market_id not in seen: seen.add(market_id) - # 构建正确的 Polymarket URL - # 优先使用已有的 polymarket_url,如果没有则根据 slug 或 market_id 构建 + # Build the correct Polymarket URL + # Prioritize using the existing polymarket_url, if not, build it based on slug or market_id polymarket_url = market_data.get('polymarket_url') if not polymarket_url: slug = market_data.get('slug') if slug and not str(slug).isdigit() and ('-' in str(slug) or any(c.isalpha() for c in str(slug))): - # 使用有效的 slug + # Use a valid slug polymarket_url = f"https://polymarket.com/event/{slug}" else: - # 使用 markets 端点(更可靠) + # Use markets endpoint (more reliable) polymarket_url = f"https://polymarket.com/markets/{market_id}" result.append({ @@ -1358,19 +1358,19 @@ class MarketDataCollector: def _extract_polymarket_keywords(self, symbol: str, market: str) -> List[str]: """ - 提取用于搜索预测市场的关键词 - 优化:只保留最重要的关键词,减少API调用次数 + Extract keywords used to search prediction markets + Optimization: Only retain the most important keywords and reduce the number of API calls """ keywords = [] - # 基础符号(最重要) + # Basic symbols (most important) if '/' in symbol: base = symbol.split('/')[0] keywords.append(base) else: keywords.append(symbol) - # 加密货币全名映射(只保留一个最重要的全名,避免重复) + # Cryptocurrency full name mapping (retain only the most important full name to avoid duplication) crypto_names = { 'BTC': 'Bitcoin', 'ETH': 'Ethereum', @@ -1386,13 +1386,13 @@ class MarketDataCollector: base_symbol = symbol.split('/')[0] if '/' in symbol else symbol if base_symbol in crypto_names: - # 只添加一个全名,避免大小写重复 + # Add only one full name to avoid duplication of upper and lower case keywords.append(crypto_names[base_symbol]) - # 优化:移除通用关键词(如 '$100k', 'ETF', 'approval'),这些会匹配到很多不相关的市场 - # 只保留与资产直接相关的关键词,最多2-3个 + # Optimization: Remove generic keywords (such as '$100k', 'ETF', 'approval'), which will match many irrelevant markets + # Only keep keywords directly related to assets, up to 2-3 - # 去重并限制数量(最多3个关键词) + # Remove duplicates and limit the number (maximum 3 keywords) unique_keywords = [] seen = set() for kw in keywords: @@ -1400,18 +1400,18 @@ class MarketDataCollector: if kw_lower not in seen: seen.add(kw_lower) unique_keywords.append(kw) - if len(unique_keywords) >= 3: # 最多3个关键词 + if len(unique_keywords) >= 3: # Up to 3 keywords break logger.info(f"Extracted {len(unique_keywords)} Polymarket keywords (optimized from {len(keywords)}): {unique_keywords}") return unique_keywords -# 全局实例 +# global instance _collector: Optional[MarketDataCollector] = None def get_market_data_collector() -> MarketDataCollector: - """获取市场数据采集器单例""" + """Get market data collector singleton""" global _collector if _collector is None: _collector = MarketDataCollector() diff --git a/backend_api_python/app/services/pending_order_worker.py b/backend_api_python/app/services/pending_order_worker.py index 79f3cf3..dd43475 100644 --- a/backend_api_python/app/services/pending_order_worker.py +++ b/backend_api_python/app/services/pending_order_worker.py @@ -206,8 +206,8 @@ class PendingOrderWorker: try: sc = load_strategy_configs(int(sid)) exec_mode = (sc.get("execution_mode") or "").strip().lower() - # 修改:即使signal模式,如果指定了target_strategy_id(策略启动时调用),也要同步 - # 这样可以清理用户在交易所手动平仓但数据库记录还在的"幽灵持仓" + # Modification: Even in signal mode, if target_strategy_id (called when the strategy is started) is specified, it must be synchronized + # This can clear up "ghost positions" where users manually close positions on the exchange but still have database records. if exec_mode != "live" and not target_strategy_id: logger.debug(f"[PositionSync] Strategy {sid} skipped: execution_mode='{exec_mode}' (needs 'live' or explicit target)") continue @@ -215,7 +215,7 @@ class PendingOrderWorker: exchange_config = resolve_exchange_config(sc.get("exchange_config") or {}, user_id=sync_user_id) safe_cfg = safe_exchange_config_for_log(exchange_config) - # 检查 exchange_id 是否有效,如果为空或无效则跳过同步(signal模式可能没有配置交易所) + # Check if exchange_id is valid, skip synchronization if it is empty or invalid (signal mode may not have an exchange configured) exchange_id = str(exchange_config.get("exchange_id") or "").strip().lower() if not exchange_id: logger.debug(f"[PositionSync] Strategy {sid} skipped: exchange_id is empty (signal mode or no exchange config)") @@ -248,7 +248,7 @@ class PendingOrderWorker: except ImportError: pass - # 尝试创建客户端,如果失败则跳过(可能是配置错误) + # Try to create client, skip if it fails (probably misconfiguration) try: client = create_client(exchange_config, market_type=market_type) except Exception as e: diff --git a/backend_api_python/app/services/polymarket_analyzer.py b/backend_api_python/app/services/polymarket_analyzer.py index 0c6a5a2..04c237a 100644 --- a/backend_api_python/app/services/polymarket_analyzer.py +++ b/backend_api_python/app/services/polymarket_analyzer.py @@ -1,6 +1,6 @@ """ -Polymarket预测市场分析器 -分析预测市场,生成AI预测和交易机会推荐 +Polymarket Prediction Market Analyzer +Analyze prediction markets and generate AI predictions and trading opportunity recommendations """ import json import re @@ -17,7 +17,7 @@ logger = get_logger(__name__) class PolymarketAnalyzer: - """预测市场AI分析器""" + """Prediction Market AI Analyzer""" def __init__(self): self.llm_service = LLMService() @@ -26,19 +26,19 @@ class PolymarketAnalyzer: def analyze_market(self, market_id: str, user_id: int = None, use_cache: bool = True, language: str = 'zh-CN', model: str = None) -> Dict: """ - 分析单个预测市场 + Analyze a single prediction market Args: - market_id: 市场ID - user_id: 用户ID(可选,用于用户特定分析) - use_cache: 是否使用缓存的分析结果(默认True) - language: 语言设置('zh-CN' 或 'en-US'),用于生成对应语言的AI分析结果 + market_id: Market ID + user_id: User ID (optional, for user-specific analysis) + use_cache: whether to use cached analysis results (default True) + language: language setting ('zh-CN' or 'en-US'), used to generate AI analysis results in the corresponding language Returns: - 分析结果字典 + Analysis results dictionary """ try: - # 1. 获取市场数据 + # 1. Get market data market = self.polymarket_source.get_market_details(market_id) if not market: return { @@ -46,21 +46,21 @@ class PolymarketAnalyzer: "market_id": market_id } - # 2. 如果使用缓存,检查是否有缓存的分析结果(30分钟有效) + # 2. If cache is used, check whether there are cached analysis results (valid for 30 minutes) if use_cache: cached_analysis = self._get_cached_analysis(market_id, user_id) if cached_analysis: - cache_minutes = 30 # 缓存30分钟 + cache_minutes = 30 # Cache for 30 minutes if self._is_analysis_fresh(cached_analysis, max_age_minutes=cache_minutes): logger.debug(f"Using cached analysis for market {market_id}") return cached_analysis - # 3. 收集相关数据 + # 3. Collect relevant data related_news = self._get_related_news(market['question']) related_assets = self._identify_related_assets(market['question']) asset_data = self._get_asset_data(related_assets) - # 4. AI分析 + # 4. AI analysis ai_result = self._ai_predict_probability( question=market['question'], current_market_prob=market['current_probability'], @@ -69,20 +69,20 @@ class PolymarketAnalyzer: language=language ) - # 5. 计算机会评分 + # 5. Calculate opportunity scores opportunity_score = self._calculate_opportunity_score( ai_prob=ai_result['predicted_probability'], market_prob=market['current_probability'], confidence=ai_result['confidence'] ) - # 6. 生成推荐 + # 6. Generate recommendations recommendation = self._generate_recommendation( divergence=ai_result['predicted_probability'] - market['current_probability'], confidence=ai_result['confidence'] ) - # 7. 构建分析结果 + # 7. Construct analysis results analysis_result = { "market_id": market_id, "ai_predicted_probability": ai_result['predicted_probability'], @@ -98,7 +98,7 @@ class PolymarketAnalyzer: "opportunity_score": opportunity_score } - # 8. 保存到数据库 + # 8. Save to database self._save_analysis_to_db(analysis_result, user_id) return analysis_result @@ -112,54 +112,54 @@ class PolymarketAnalyzer: def generate_asset_trading_opportunities(self, market_id: str) -> List[Dict]: """ - 基于预测市场生成相关资产的交易机会 + Generate trading opportunities for related assets based on prediction markets Args: - market_id: 预测市场ID + market_id: prediction market ID Returns: - 资产交易机会列表 + List of asset trading opportunities """ try: - # 1. 分析预测市场 + # 1. Analyze prediction markets market_analysis = self.analyze_market(market_id) if market_analysis.get('error'): return [] - # 2. 识别相关资产 + # 2. Identify relevant assets related_assets = market_analysis.get('related_assets', []) if not related_assets: return [] - # 3. 对每个资产进行技术分析 + # 3. Perform technical analysis on each asset opportunities = [] for asset in related_assets: try: - # 推断市场类型 + # Infer market type market_type = self._infer_market(asset) - # 获取资产数据 + # Get asset data asset_data = self.data_collector.collect_all( market=market_type, symbol=asset, timeframe="1D", - include_polymarket=False # 避免循环 + include_polymarket=False # avoid loops ) - # 技术分析 + # technical analysis technical_analysis = self._analyze_technical(asset_data) - # 结合预测市场信号 + # Incorporate Predictive Market Signals if market_analysis['recommendation'] == "YES": - # 预测事件发生概率高 → 相关资产可能上涨 + # Predicted event probability is high → related assets may rise signal = "BUY" if technical_analysis.get('trend') == "bullish" else "HOLD" elif market_analysis['recommendation'] == "NO": - # 预测事件发生概率低 → 相关资产可能下跌 + # The probability of the predicted event is low → the related assets may fall signal = "SELL" if technical_analysis.get('trend') == "bearish" else "HOLD" else: signal = "HOLD" - # 计算综合置信度 + # Calculate overall confidence confidence = ( market_analysis['confidence_score'] * 0.6 + technical_analysis.get('confidence', 50) * 0.4 @@ -184,7 +184,7 @@ class PolymarketAnalyzer: logger.debug(f"Failed to analyze asset {asset} for market {market_id}: {e}") continue - # 保存机会到数据库 + # Save opportunity to database if opportunities: self._save_opportunities_to_db(market_id, opportunities) @@ -196,12 +196,12 @@ class PolymarketAnalyzer: def _ai_predict_probability(self, question: str, current_market_prob: float, related_news: List, asset_data: Dict, language: str = 'zh-CN') -> Dict: - """使用AI预测事件概率""" + """Using AI to predict event probabilities""" try: - # 根据语言设置构建prompt + # Build prompt based on language settings is_english = language.lower() in ['en', 'en-us', 'en_us'] - # 构建prompt + # build prompt news_text = "\n".join([f"- {n.get('title', '')[:100]}" for n in related_news[:5]]) asset_text = "" @@ -287,7 +287,7 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) system_prompt = "你是一个专业的市场分析师,擅长分析预测市场事件。请基于提供的数据,客观评估事件发生的概率。请使用中文回答。" - # 调用LLM + # Call LLM messages = [ { "role": "system", @@ -305,13 +305,13 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) temperature=0.3 ) - # 解析结果 + # Parse results if isinstance(result, str): result = json.loads(result) - # 验证和规范化 + # Validation and normalization predicted_prob = float(result.get('predicted_probability', current_market_prob)) - predicted_prob = max(0, min(100, predicted_prob)) # 限制在0-100 + predicted_prob = max(0, min(100, predicted_prob)) # Limit to 0-100 confidence = float(result.get('confidence', 70)) confidence = max(0, min(100, confidence)) @@ -326,7 +326,7 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) except Exception as e: logger.error(f"AI prediction failed: {e}", exc_info=True) - # 返回默认值 + # Return to default value return { 'predicted_probability': current_market_prob, 'confidence': 50.0, @@ -338,28 +338,28 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) def _calculate_opportunity_score(self, ai_prob: float, market_prob: float, confidence: float) -> float: """ - 计算机会评分(0-100) + Calculate opportunity rating (0-100) - 逻辑: - - AI与市场差异越大,机会越好 - - 置信度越高,机会越好 + logic: + - The greater the difference between AI and the market, the better the opportunity + - The higher the confidence, the better the chance """ divergence = abs(ai_prob - market_prob) - # 差异越大,机会越好(最大40分) + # The bigger the difference, the better the chance (maximum 40 points) divergence_score = min(divergence * 2, 40) - # 置信度越高,机会越好(最大60分) + # The higher the confidence level, the better the chance (maximum 60 points) confidence_score = confidence * 0.6 return round(divergence_score + confidence_score, 2) def _generate_recommendation(self, divergence: float, confidence: float) -> str: """ - 生成推荐:YES/NO/HOLD + Generate recommendations: YES/NO/HOLD - 逻辑: - - AI概率 > 市场概率 + 5% 且置信度 > 60 → YES - - AI概率 < 市场概率 - 5% 且置信度 > 60 → NO - - 其他 → HOLD + logic: + - AI Probability > Market Probability + 5% and Confidence > 60 → YES + - AI probability < market probability - 5% and confidence level > 60 → NO + - Others → HOLD """ if divergence > 5 and confidence > 60: return "YES" @@ -369,7 +369,7 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) return "HOLD" def _assess_risk(self, market: Dict, ai_result: Dict) -> str: - """评估风险等级""" + """Assess risk level""" confidence = ai_result.get('confidence', 50) divergence = abs(ai_result.get('predicted_probability', 50) - market.get('current_probability', 50)) @@ -381,19 +381,19 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) return "low" def _get_related_news(self, question: str) -> List[Dict]: - """获取相关问题相关的新闻""" - # 提取关键词 + """Get news on relevant issues""" + # Extract keywords keywords = self._extract_keywords(question) - # 这里可以调用新闻API,暂时返回空列表 - # 实际实现时可以调用现有的新闻服务 + # Here you can call the news API and temporarily return an empty list + # In actual implementation, existing news services can be called return [] def _identify_related_assets(self, question: str) -> List[str]: - """识别问题中提到的相关资产""" + """Identify related assets mentioned in the question""" assets = [] - # 加密货币关键词映射 + # Cryptocurrency Keyword Mapping crypto_keywords = { 'BTC': ['BTC', 'Bitcoin', 'bitcoin', 'btc'], 'ETH': ['ETH', 'Ethereum', 'ethereum', 'eth'], @@ -412,11 +412,11 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) if any(kw in question_upper for kw in keywords): assets.append(f"{symbol}/USDT") - # 去重 + # Remove duplicates return list(set(assets)) def _get_asset_data(self, assets: List[str]) -> Optional[Dict]: - """获取资产数据(取第一个资产)""" + """Get asset data (get the first asset)""" if not assets: return None @@ -433,7 +433,7 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) return None def _analyze_technical(self, asset_data: Dict) -> Dict: - """简单的技术分析""" + """simple technical analysis""" if not asset_data: return { 'trend': 'neutral', @@ -445,7 +445,7 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) indicators = asset_data.get('indicators', {}) price_data = asset_data.get('price', {}) - # 简单的趋势判断 + # Simple trend judgment rsi = indicators.get('rsi', {}).get('value', 50) macd_signal = indicators.get('macd', {}).get('signal', 'neutral') @@ -465,22 +465,22 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) } def _infer_market(self, symbol: str) -> str: - """推断市场类型""" + """Infer market type""" if '/' in symbol: return "Crypto" elif len(symbol) <= 5 and symbol.isupper(): return "USStock" else: - return "Crypto" # 默认 + return "Crypto" # default def _extract_keywords(self, text: str) -> List[str]: - """提取关键词""" - # 简单的关键词提取 + """Extract keywords""" + # Simple keyword extraction words = re.findall(r'\b[A-Z][a-z]+\b|\b[A-Z]{2,}\b', text) return [w.lower() for w in words if len(w) > 2] def _get_cached_analysis(self, market_id: str, user_id: int = None) -> Optional[Dict]: - """获取缓存的分析结果""" + """Get cached analysis results""" try: with get_db_connection() as db: cur = db.cursor() @@ -506,7 +506,7 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) cur.close() if row: - # RealDictCursor返回字典,使用键访问 + #RealDictCursor returns the dictionary, accessed using keys key_factors_raw = row.get('key_factors') key_factors = [] if key_factors_raw: @@ -551,19 +551,19 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) def _save_analysis_to_db(self, analysis: Dict, user_id: int = None, language: str = 'en-US', model: str = None): """ - 保存分析结果到数据库 + Save analysis results to database Args: - analysis: 分析结果字典 - user_id: 用户ID - language: 语言设置 - model: 使用的模型 + analysis: dictionary of analysis results + user_id: user ID + language: language settings + model: model used """ try: with get_db_connection() as db: cur = db.cursor() - # 1. 保存到 qd_polymarket_ai_analysis 表(Polymarket专用表) + # 1. Save to qd_polymarket_ai_analysis table (Polymarket special table) cur.execute(""" INSERT INTO qd_polymarket_ai_analysis (market_id, user_id, ai_predicted_probability, market_probability, @@ -584,7 +584,7 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) analysis.get('related_assets', []) )) - # 2. 同时保存到 qd_analysis_tasks 表(用于管理员统计和统一的历史记录查看) + # 2. Save to the qd_analysis_tasks table at the same time (for administrator statistics and unified historical record viewing) market_info = analysis.get('market', {}) market_title = market_info.get('question', '') or market_info.get('title', '') or f"Polymarket Market {analysis['market_id']}" result_json = json.dumps({ @@ -592,7 +592,7 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) 'market_title': market_title, 'analysis': analysis, 'market': market_info, - 'type': 'polymarket' # 标记为Polymarket分析 + 'type': 'polymarket' # Mark as Polymarket analysis }, ensure_ascii=False) cur.execute(""" @@ -602,8 +602,8 @@ IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) RETURNING id """, ( int(user_id) if user_id else 1, - 'Polymarket', # market字段 - str(analysis['market_id']), # symbol字段存储market_id + 'Polymarket', # market field + str(analysis['market_id']), # symbol field stores market_id str(model) if model else '', str(language), 'completed', diff --git a/backend_api_python/app/services/polymarket_batch_analyzer.py b/backend_api_python/app/services/polymarket_batch_analyzer.py index 56feb14..1379f06 100644 --- a/backend_api_python/app/services/polymarket_batch_analyzer.py +++ b/backend_api_python/app/services/polymarket_batch_analyzer.py @@ -1,6 +1,6 @@ """ -Polymarket批量分析器 -一次性分析多个市场,由AI筛选出有交易机会的市场 +Polymarket Batch Analyzer +Analyze multiple markets at once and use AI to screen out markets with trading opportunities. """ import json from typing import List, Dict, Optional @@ -13,7 +13,7 @@ logger = get_logger(__name__) class PolymarketBatchAnalyzer: - """批量分析预测市场,由AI筛选交易机会""" + """Batch analysis predicts the market, and AI screens trading opportunities""" def __init__(self): self.llm_service = LLMService() @@ -21,20 +21,20 @@ class PolymarketBatchAnalyzer: def batch_analyze_markets(self, markets: List[Dict], max_opportunities: int = 20) -> List[Dict]: """ - 批量分析市场,由AI筛选出有交易机会的市场 + Analyze the market in batches and use AI to screen out markets with trading opportunities. Args: - markets: 市场列表 - max_opportunities: 最多返回多少个交易机会 + markets: list of markets + max_opportunities: The maximum number of trading opportunities returned Returns: - 筛选后的市场列表(包含AI分析结果) + Filtered market list (including AI analysis results) """ if not markets: return [] try: - # 1. 构建批量分析的prompt + # 1. Build prompts for batch analysis markets_summary = self._build_markets_summary(markets) prompt = f"""你是一个专业的预测市场分析师。请分析以下预测市场列表,筛选出最有交易机会的市场。 @@ -69,7 +69,7 @@ class PolymarketBatchAnalyzer: - 优先选择:高交易量 + 明显概率偏差 + 高置信度 - 简要说明原因,不要冗长""" - # 2. 调用LLM进行批量分析 + # 2. Call LLM for batch analysis messages = [ { "role": "system", @@ -88,7 +88,7 @@ class PolymarketBatchAnalyzer: temperature=0.3 ) - # 3. 解析结果 + # 3. Parse the results if isinstance(result, str): try: result = json.loads(result) @@ -101,7 +101,7 @@ class PolymarketBatchAnalyzer: logger.warning("LLM returned no opportunities, using fallback") return self._fallback_analysis(markets, max_opportunities) - # 4. 将AI分析结果合并到市场数据中 + # 4. Incorporate AI analysis results into market data opportunities_map = {opp.get('market_id'): opp for opp in opportunities} analyzed_markets = [] @@ -112,24 +112,24 @@ class PolymarketBatchAnalyzer: opp = opportunities_map.get(market_id) if opp: - # 获取AI预测的概率 + # Get the probability predicted by AI predicted_prob = float(opp.get('predicted_probability', market.get('current_probability', 50.0))) market_prob = market.get('current_probability', 50.0) divergence = predicted_prob - market_prob - # 合并AI分析结果 + # Merge AI analysis results market['ai_analysis'] = { - 'predicted_probability': predicted_prob, # 使用AI预测的概率 + 'predicted_probability': predicted_prob, # Probability predicted using AI 'recommendation': opp.get('recommendation', 'HOLD'), 'confidence_score': float(opp.get('confidence', 0)), 'opportunity_score': float(opp.get('opportunity_score', 0)), - 'divergence': divergence, # AI预测概率 - 市场概率 + 'divergence': divergence, # AI Predicted Probability - Market Probability 'reasoning': opp.get('reason', ''), 'key_factors': opp.get('key_factors', []) } analyzed_markets.append(market) - # 5. 按机会评分排序 + # 5. Sort by opportunity score analyzed_markets.sort( key=lambda x: x.get('ai_analysis', {}).get('opportunity_score', 0), reverse=True @@ -158,12 +158,12 @@ class PolymarketBatchAnalyzer: return self._fallback_analysis(markets, max_opportunities) def _build_markets_summary(self, markets: List[Dict]) -> str: - """构建市场摘要,用于批量分析""" + """Build market summaries for batch analysis""" summary_lines = [] - for i, market in enumerate(markets[:50], 1): # 限制最多50个,避免prompt过长 + for i, market in enumerate(markets[:50], 1): # Limit to 50 to avoid prompts that are too long market_id = market.get('market_id', '') - question = market.get('question', '')[:100] # 限制长度 + question = market.get('question', '')[:100] # Limit length prob = market.get('current_probability', 50.0) volume = market.get('volume_24h', 0) category = market.get('category', 'other') @@ -179,14 +179,14 @@ class PolymarketBatchAnalyzer: return "\n\n".join(summary_lines) def _fallback_analysis(self, markets: List[Dict], max_opportunities: int) -> List[Dict]: - """回退分析:基于简单规则筛选""" + """Fallback analysis: filtering based on simple rules""" opportunities = [] for market in markets: prob = market.get('current_probability', 50.0) volume = market.get('volume_24h', 0) - # 简单规则:交易量大 + 概率偏离50% + # Simple rule: large trading volume + 50% probability of deviation if volume > 10000 and abs(prob - 50.0) > 10: opportunity_score = min(60 + abs(prob - 50.0) * 0.5, 90) @@ -201,7 +201,7 @@ class PolymarketBatchAnalyzer: } opportunities.append(market) - # 按机会评分排序 + # Sort by opportunity score opportunities.sort( key=lambda x: x.get('ai_analysis', {}).get('opportunity_score', 0), reverse=True @@ -210,7 +210,7 @@ class PolymarketBatchAnalyzer: return opportunities[:max_opportunities] def save_batch_analysis(self, markets: List[Dict]): - """保存批量分析结果到数据库""" + """Save batch analysis results to database""" try: with get_db_connection() as db: cur = db.cursor() @@ -223,13 +223,13 @@ class PolymarketBatchAnalyzer: continue try: - # 先删除该市场的旧分析记录(user_id为NULL的通用分析) + # First delete the old analysis records of this market (general analysis with user_id as NULL) cur.execute(""" DELETE FROM qd_polymarket_ai_analysis WHERE market_id = %s AND user_id IS NULL """, (market_id,)) - # 插入新的分析记录 + #Insert new analysis record cur.execute(""" INSERT INTO qd_polymarket_ai_analysis (market_id, user_id, ai_predicted_probability, market_probability, @@ -238,7 +238,7 @@ class PolymarketBatchAnalyzer: VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW()) """, ( market_id, - None, # 通用分析 + None, # General analysis float(ai_analysis.get('predicted_probability', market.get('current_probability', 50.0))), market.get('current_probability', 50.0), float(ai_analysis.get('divergence', 0)), diff --git a/backend_api_python/app/services/polymarket_worker.py b/backend_api_python/app/services/polymarket_worker.py index 1fa2ed2..cd5b115 100644 --- a/backend_api_python/app/services/polymarket_worker.py +++ b/backend_api_python/app/services/polymarket_worker.py @@ -1,6 +1,6 @@ """ -Polymarket后台任务 -每30分钟更新一次市场数据,并批量分析市场机会 +Polymarket background tasks +Update market data every 30 minutes and analyze market opportunities in batches """ import threading import time @@ -15,15 +15,15 @@ logger = get_logger(__name__) class PolymarketWorker: - """Polymarket数据更新和分析后台任务""" + """Polymarket data update and analysis background tasks""" - def __init__(self, update_interval_minutes: int = 30, analysis_cache_minutes: int = 1440): # 24小时缓存 + def __init__(self, update_interval_minutes: int = 30, analysis_cache_minutes: int = 1440): # 24 hours cache """ - 初始化后台任务 + Initialize background tasks Args: - update_interval_minutes: 市场数据更新间隔(分钟) - analysis_cache_minutes: AI分析结果缓存时间(分钟) + update_interval_minutes: market data update interval (minutes) + analysis_cache_minutes: AI analysis result cache time (minutes) """ self.update_interval_minutes = update_interval_minutes self.analysis_cache_minutes = analysis_cache_minutes @@ -35,7 +35,7 @@ class PolymarketWorker: self._last_update_ts = 0.0 def start(self) -> bool: - """启动后台任务""" + """Start background task""" with self._lock: if self._thread and self._thread.is_alive(): return True @@ -46,7 +46,7 @@ class PolymarketWorker: return True def stop(self, timeout_sec: float = 5.0) -> None: - """停止后台任务""" + """Stop background tasks""" with self._lock: if not self._thread or not self._thread.is_alive(): return @@ -58,36 +58,36 @@ class PolymarketWorker: logger.info("PolymarketWorker stopped") def _run_loop(self) -> None: - """主循环""" + """main loop""" logger.info("PolymarketWorker loop started") - # 启动时立即执行一次 + # Execute once immediately on startup self._update_markets_and_analyze() while not self._stop_event.is_set(): try: - # 等待指定时间间隔 + # Wait for specified time interval wait_seconds = self.update_interval_minutes * 60 if self._stop_event.wait(wait_seconds): - break # 如果收到停止信号,退出循环 + break # If a stop signal is received, exit the loop - # 执行更新和分析 + # Perform updates and analysis self._update_markets_and_analyze() except Exception as e: logger.error(f"PolymarketWorker loop error: {e}", exc_info=True) - # 出错后等待1分钟再重试 + # After an error, wait 1 minute and try again self._stop_event.wait(60) logger.info("PolymarketWorker loop stopped") def _update_markets_and_analyze(self) -> None: - """更新市场数据并分析""" + """Update market data and analyze""" try: logger.info("Starting Polymarket data update and analysis...") start_time = time.time() - # 1. 更新市场数据(从所有主要分类获取) + # 1. Update market data (obtained from all major categories) categories = ["crypto", "politics", "economics", "sports", "tech", "finance", "geopolitics", "culture", "climate", "entertainment"] all_markets = [] @@ -99,7 +99,7 @@ class PolymarketWorker: except Exception as e: logger.warning(f"Failed to fetch markets for category {category}: {e}") - # 去重(按market_id) + # Deduplication (by market_id) unique_markets = {} for market in all_markets: market_id = market.get('market_id') @@ -108,28 +108,28 @@ class PolymarketWorker: logger.info(f"Total unique markets: {len(unique_markets)}") - # 2. 批量分析市场(一次性分析所有市场,由AI筛选机会) + # 2. Analyze the market in batches (analyze all markets at once, and use AI to screen opportunities) markets_list = list(unique_markets.values()) logger.info(f"Starting batch analysis for {len(markets_list)} markets...") - # 优化策略:先用规则筛选,只对高价值机会调用LLM - # 这样可以大幅减少LLM调用次数,节省token + # Optimization strategy: first use rules to filter and only call LLM on high-value opportunities + # This can greatly reduce the number of LLM calls and save tokens. - # 1. 先用规则筛选出最有价值的机会(不调用LLM) + # 1. First use rules to filter out the most valuable opportunities (without calling LLM) rule_based_opportunities = [] for market in markets_list: prob = market.get('current_probability', 50.0) volume = market.get('volume_24h', 0) divergence = abs(prob - 50.0) - # 规则筛选:高交易量 + 明显概率偏差 + # Rule screening: high trading volume + obvious probability deviation if volume > 5000 and divergence > 8: rule_based_opportunities.append(market) - # 2. 只对规则筛选出的机会调用LLM(最多30个,节省token) + # 2. Only call LLM for opportunities filtered out by rules (up to 30, saving tokens) if rule_based_opportunities: logger.info(f"Rule-based filtering: {len(rule_based_opportunities)} opportunities, analyzing top 30 with LLM") - # 按交易量和概率偏差排序,取前30个 + # Sort by transaction volume and probability deviation, take the top 30 rule_based_opportunities.sort( key=lambda x: (x.get('volume_24h', 0) * abs(x.get('current_probability', 50) - 50)), reverse=True @@ -138,13 +138,13 @@ class PolymarketWorker: analyzed_markets = self.batch_analyzer.batch_analyze_markets( top_opportunities, - max_opportunities=30 # 只分析30个最有价值的机会 + max_opportunities=30 # Analyze only the 30 most valuable opportunities ) else: logger.info("No rule-based opportunities found, skipping LLM analysis") analyzed_markets = [] - # 3. 保存分析结果到数据库 + # 3. Save the analysis results to the database if analyzed_markets: self.batch_analyzer.save_batch_analysis(analyzed_markets) analyzed_count = len(analyzed_markets) @@ -160,18 +160,18 @@ class PolymarketWorker: def force_update(self) -> None: - """强制立即更新(用于手动触发)""" + """Force immediate update (for manual triggering)""" logger.info("Force update triggered") self._update_markets_and_analyze() -# 全局单例 +# Global singleton _polymarket_worker: Optional[PolymarketWorker] = None _worker_lock = threading.Lock() def get_polymarket_worker() -> PolymarketWorker: - """获取PolymarketWorker单例""" + """Get PolymarketWorker singleton""" global _polymarket_worker with _worker_lock: if _polymarket_worker is None: @@ -184,5 +184,5 @@ def get_polymarket_worker() -> PolymarketWorker: return _polymarket_worker -# 需要导入os +# Need to import os import os diff --git a/backend_api_python/app/services/portfolio_monitor.py b/backend_api_python/app/services/portfolio_monitor.py index e09433b..59ca0b9 100644 --- a/backend_api_python/app/services/portfolio_monitor.py +++ b/backend_api_python/app/services/portfolio_monitor.py @@ -26,7 +26,7 @@ DEFAULT_USER_ID = 1 _monitor_thread: Optional[threading.Thread] = None _stop_event = threading.Event() -# 多语言消息模板 +# Multilingual message templates ALERT_MESSAGES = { 'zh-CN': { 'price_above': '🔔 价格突破预警: {symbol} 当前价格 ${current_price:.4f} 已突破 ${threshold:.4f}', @@ -67,9 +67,9 @@ def _now_ts() -> int: def _resolve_notification_delivery(user_id: int, notification_config: Optional[Dict[str, Any]]) -> Dict[str, Any]: """ - 合并个人中心保存的 notification_settings 到 targets,并规范化 channels。 - 前端创建监控常只传 channels(email/telegram/webhook),不传 targets;若不合并则外发渠道全部跳过且无任何送达。 - 若当前 channels 均无法送达(无邮箱/Chat ID 等),则追加 browser 保证站内通知。 + Merge notification_settings saved in the personal center to targets and normalize channels. + When creating monitoring on the front end, only channels (email/telegram/webhook) are usually passed, and targets are not passed; if not merged, all outgoing channels will be skipped and no delivery will be made. + If the current channels cannot be delivered (no email/Chat ID, etc.), add a browser to ensure on-site notification. """ cfg: Dict[str, Any] = dict(notification_config) if isinstance(notification_config, dict) else {} raw_ch = cfg.get('channels') @@ -784,19 +784,19 @@ def _build_telegram_report( # ── Header / Overview ── if is_zh: - lines: List[str] = ["📊 AI资产分析报告", ""] - overview = ["📈 概览"] + lines: List[str] = ["📊 AI Asset Analysis Report", ""] + overview = ["📈 Overview"] if held: - overview.append(f"• 持仓: {len(held)} 个") - overview.append(f"• 总成本: ${total_cost:,.2f}") - overview.append(f"• 总盈亏: {pnl_sign}${total_pnl:,.2f} ({pnl_sign}{total_pnl_pct:.1f}%)") + overview.append(f"•Positions: {len(held)}") + overview.append(f"•Total cost: ${total_cost:,.2f}") + overview.append(f"•Total profit and loss: {pnl_sign}${total_pnl:,.2f} ({pnl_sign}{total_pnl_pct:.1f}%)") if watched: - overview.append(f"• 观察: {len(watched)} 个") + overview.append(f"• Observations: {len(watched)}") lines.extend(overview) lines.extend([ "", - "🤖 AI建议汇总", - f"🟢 买入: {buy_count} | 🔴 卖出: {sell_count} | 🟡 持有: {hold_count}", + "🤖 Summary of AI suggestions", + f"🟢 Buy: {buy_count} | 🔴 Sell: {sell_count} | 🟡 Hold: {hold_count}", ]) else: lines = ["📊 AI Asset Analysis Report", ""] @@ -820,7 +820,7 @@ def _build_telegram_report( emoji = {'BUY': '🟢', 'SELL': '🔴', 'HOLD': '🟡'}.get(decision, '⚪') d_text = decision if is_zh: - d_text = {'BUY': '买入', 'SELL': '卖出', 'HOLD': '持有'}.get(decision, '持有') + d_text = {'BUY': 'Buy', 'SELL': 'Sell', 'HOLD': 'Hold'}.get(decision, 'Hold') lines.append(f"\n{emoji} {pa.get('name', pa.get('symbol'))} ({pa.get('market')}/{pa.get('symbol')})") if show_pnl: pnl = pa.get('pnl', 0) @@ -828,13 +828,13 @@ def _build_telegram_report( ps = '+' if pnl >= 0 else '' lines.append( f" 💰 ${pa.get('current_price', 0):,.2f} | " - f"{'盈亏' if is_zh else 'P&L'}: {ps}${pnl:,.2f} ({ps}{pnl_pct:.1f}%)" + f"{'profit and loss' if is_zh else 'P&L'}: {ps}${pnl:,.2f} ({ps}{pnl_pct:.1f}%)" ) else: lines.append(f" 💰 {'现价' if is_zh else 'Price'}: ${pa.get('current_price', 0):,.2f}") lines.append( f" 🎯 {'建议' if is_zh else 'Rec'}: {d_text} " - f"({'置信度' if is_zh else 'Conf'}: {pa.get('confidence', 50)}%)" + f"({'confidence' if is_zh else 'Conf'}: {pa.get('confidence', 50)}%)" ) reasoning = pa.get('reasoning', '') if reasoning: @@ -842,20 +842,20 @@ def _build_telegram_report( # ── Holdings section ── if held: - lines.extend(["", f"📋 {'持仓分析' if is_zh else 'Holdings'}"]) + lines.extend(["", f"📋 {'position analysis' if is_zh else 'Holdings'}"]) for pa in held: _render_pa(pa, show_pnl=True) # ── Watchlist section ── if watched: - lines.extend(["", f"👁 {'观察列表' if is_zh else 'Watchlist'}"]) + lines.extend(["", f"👁 {'Watchlist' if is_zh else 'Watchlist'}"]) for pa in watched: _render_pa(pa, show_pnl=False) # ── Errors ── for pa in errored: label = pa.get('name') or pa.get('symbol') or '?' - lines.append(f"\n⚠️ {label}: {'分析失败' if is_zh else 'Analysis failed'}") + lines.append(f"\n⚠️ {label}: {'Analysis failed' if is_zh else 'Analysis failed'}") if custom_prompt: lines.extend(["", f"👤 {'关注点' if is_zh else 'Focus'}: {custom_prompt}"]) @@ -864,7 +864,7 @@ def _build_telegram_report( "", "─────────────────────", f"⏰ {time.strftime('%Y-%m-%d %H:%M')}", - f"{'由 QuantDinger 多智能体系统生成' if is_zh else 'Generated by QuantDinger Multi-Agent System'}", + f"{'Generated by QuantDinger Multi-Agent System' if is_zh else 'Generated by QuantDinger Multi-Agent System'}", ]) return '\n'.join(lines) @@ -893,11 +893,11 @@ def _build_batch_telegram_report( for pa in m_analyses: if pa.get('error'): label = pa.get('name') or pa.get('symbol') or '?' - section_lines.append(f" ⚠️ {label}: {'分析失败' if is_zh else 'Failed'}") + section_lines.append(f" ⚠️ {label}: {'Analysis failed' if is_zh else 'Failed'}") continue decision = pa.get('final_decision', 'HOLD') emoji = {'BUY': '🟢', 'SELL': '🔴', 'HOLD': '🟡'}.get(decision, '⚪') - d_text = ({'BUY': '买入', 'SELL': '卖出', 'HOLD': '持有'}.get(decision, '持有')) if is_zh else decision + d_text = ({'BUY': 'Buy', 'SELL': 'Sell', 'HOLD': 'Hold'}.get(decision, 'Hold')) if is_zh else decision cur_price = pa.get('current_price', 0) section_lines.append( f"{emoji} {pa.get('name', pa.get('symbol'))} ({pa.get('market')}/{pa.get('symbol')})" @@ -913,7 +913,7 @@ def _build_batch_telegram_report( section_lines.append(f" 💰 {'现价' if is_zh else 'Price'}: ${cur_price:,.2f}") section_lines.append( f" 🎯 {'建议' if is_zh else 'Rec'}: {d_text} " - f"({'置信度' if is_zh else 'Conf'}: {pa.get('confidence', 50)}%)" + f"({'confidence' if is_zh else 'Conf'}: {pa.get('confidence', 50)}%)" ) reasoning = pa.get('reasoning', '') if reasoning: @@ -932,20 +932,20 @@ def _build_batch_telegram_report( if is_zh: header = [ - "📊 定时资产监测报告", + "📊 Regular asset monitoring report", "", - "📈 综合概览", - f"• 监控任务: {len(monitor_results)} 个", - f"• 标的数量: {len(all_analyses)} 个", + "📈 General Overview", + f"•Monitoring tasks: {len(monitor_results)}", + f"• Number of targets: {len(all_analyses)}", ] if held: - header.append(f"• 持仓: {len(held)} 个 | 总成本: ${total_cost:,.2f} | 盈亏: {pnl_sign}${total_pnl:,.2f} ({pnl_sign}{total_pnl_pct:.1f}%)") + header.append(f"• Positions: {len(held)} | Total cost: ${total_cost:,.2f} | Profit and loss: {pnl_sign}${total_pnl:,.2f} ({pnl_sign}{total_pnl_pct:.1f}%)") if watched: - header.append(f"• 观察: {len(watched)} 个") + header.append(f"• Observations: {len(watched)}") header.extend([ "", - "🤖 AI建议汇总", - f"🟢 买入: {buy_c} | 🔴 卖出: {sell_c} | 🟡 持有: {hold_c}", + "🤖 Summary of AI suggestions", + f"🟢 Buy: {buy_c} | 🔴 Sell: {sell_c} | 🟡 Hold: {hold_c}", ]) else: header = [ @@ -969,7 +969,7 @@ def _build_batch_telegram_report( "", "─────────────────────", f"⏰ {time.strftime('%Y-%m-%d %H:%M')}", - f"{'由 QuantDinger 多智能体系统生成' if is_zh else 'Generated by QuantDinger Multi-Agent System'}", + f"{'Generated by QuantDinger Multi-Agent System' if is_zh else 'Generated by QuantDinger Multi-Agent System'}", ] return '\n'.join(header + monitor_sections + footer) @@ -1043,7 +1043,7 @@ def _send_batch_notification( is_zh = language.startswith('zh') names = ', '.join(r.get('_meta', {}).get('monitor_name', '?') for r in successful) - title = f"📊 定时资产监测: {names}" if is_zh else f"📊 Scheduled Report: {names}" + title = f"📊 Scheduled Asset Monitoring: {names}" if is_zh else f"📊 Scheduled Report: {names}" if len(title) > 255: title = title[:252] + '...' @@ -1117,12 +1117,12 @@ def _send_monitor_notification( channels = notification_config.get('channels') or ['browser'] targets = notification_config.get('targets', {}) - title = f"📊 资产监测: {monitor_name}" if language.startswith('zh') else f"📊 Portfolio Monitor: {monitor_name}" + title = f"📊 Asset Monitor: {monitor_name}" if language.startswith('zh') else f"📊 Portfolio Monitor: {monitor_name}" if len(title) > 255: title = title[:252] + '...' if not result.get('success'): - error_title = f"⚠️ 资产监测失败: {monitor_name}" if language.startswith('zh') else f"⚠️ Monitor Failed: {monitor_name}" + error_title = f"⚠️ Asset monitoring failed: {monitor_name}" if language.startswith('zh') else f"⚠️ Monitor Failed: {monitor_name}" if len(error_title) > 255: error_title = error_title[:252] + '...' error_msg = f"分析失败: {result.get('error', 'Unknown error')}" if language.startswith('zh') else f"Analysis failed: {result.get('error', 'Unknown error')}" @@ -1549,7 +1549,7 @@ def _check_position_alerts(): db.commit() cur.close() - # Send notification(合并个人中心通知配置,与资产监控任务一致) + # Send notification (merge personal center notification configuration, consistent with asset monitoring tasks) resolved = _resolve_notification_delivery(alert_user_id, notification_config) channels = resolved.get('channels') or ['browser'] targets = resolved.get('targets', {}) @@ -1639,16 +1639,16 @@ def notify_strategy_signal_for_positions(market: str, symbol: str, signal_type: quantity = float(pos.get('quantity') or 0) entry_price = float(pos.get('entry_price') or 0) - title = f"🔗 策略信号联动: {pos_name}" - message = f"""策略发出 {signal_type} 信号! + title = f"🔗Strategy signal linkage: {pos_name}" + message = f"""The strategy emits {signal_type} signal! -标的: {market}/{symbol} -您的持仓: {pos_side.upper()} {quantity} @ {entry_price:.4f} +Target: {market}/{symbol} +Your position: {pos_side.upper()} {quantity} @ {entry_price:.4f} -信号详情: +Signal details: {signal_detail} -请注意检查您的持仓是否需要调整。""" +Please check whether your position needs adjustment. """ # Save browser notification with get_db_connection() as db: diff --git a/backend_api_python/app/services/search.py b/backend_api_python/app/services/search.py index c37aeda..2b5808e 100644 --- a/backend_api_python/app/services/search.py +++ b/backend_api_python/app/services/search.py @@ -1,13 +1,13 @@ """ -Search service v2.0 - 增强版搜索服务 -整合多个搜索引擎,支持 API Key 轮换和故障转移 +Search service v2.0 - Enhanced version of search service +Integrate multiple search engines and support API Key rotation and failover -支持的搜索引擎(按优先级): -1. Tavily - 专为AI设计,免费1000次/月 -2. SerpAPI - Google/Bing 结果抓取 -3. Google CSE - 自定义搜索引擎 +Supported search engines (in order of priority): +1. Tavily - specially designed for AI, free 1000 times/month +2. SerpAPI - Google/Bing result crawling +3. Google CSE – Custom Search Engine 4. Bing Search API -5. DuckDuckGo - 免费兜底 +5. DuckDuckGo – Get the scoop for free 参考:daily_stock_analysis-main/src/search_service.py """ @@ -34,21 +34,21 @@ _google_quota_reset_time = 0 @dataclass class SearchResult: - """搜索结果数据类""" + """Search result data class""" title: str - snippet: str # 摘要 + snippet: str # summary url: str - source: str # 来源网站 + source: str # Source website published_date: Optional[str] = None - sentiment: str = 'neutral' # 情绪标签 + sentiment: str = 'neutral' # Emotion tags def to_text(self) -> str: - """转换为文本格式""" + """Convert to text format""" date_str = f" ({self.published_date})" if self.published_date else "" return f"【{self.source}】{self.title}{date_str}\n{self.snippet}" def to_dict(self) -> Dict[str, Any]: - """转换为字典""" + """Convert to dictionary""" return { 'title': self.title, 'link': self.url, @@ -61,16 +61,16 @@ class SearchResult: @dataclass class SearchResponse: - """搜索响应""" + """search response""" query: str results: List[SearchResult] - provider: str # 使用的搜索引擎 + provider: str # search engine used success: bool = True error_message: Optional[str] = None - search_time: float = 0.0 # 搜索耗时(秒) + search_time: float = 0.0 # Search time (seconds) def to_context(self, max_results: int = 5) -> str: - """将搜索结果转换为可用于 AI 分析的上下文""" + """Transform search results into context that can be used for AI analysis""" if not self.success or not self.results: return f"搜索 '{self.query}' 未找到相关结果。" @@ -81,20 +81,20 @@ class SearchResponse: return "\n".join(lines) def to_list(self) -> List[Dict[str, Any]]: - """转换为列表格式(兼容旧接口)""" + """Convert to list format (compatible with old interface)""" return [r.to_dict() for r in self.results] class BaseSearchProvider(ABC): - """搜索引擎基类""" + """Search engine base class""" def __init__(self, api_keys: List[str], name: str): """ - 初始化搜索引擎 + Initialize search engine Args: - api_keys: API Key 列表(支持多个 key 负载均衡) - name: 搜索引擎名称 + api_keys: API Key list (supports multiple key load balancing) + name: search engine name """ self._api_keys = api_keys self._name = name @@ -108,58 +108,58 @@ class BaseSearchProvider(ABC): @property def is_available(self) -> bool: - """检查是否有可用的 API Key""" + """Check if there is an available API Key""" return bool(self._api_keys) def _get_next_key(self) -> Optional[str]: """ - 获取下一个可用的 API Key(负载均衡) + Get the next available API Key (load balancing) - 策略:轮询 + 跳过错误过多的 key + Strategy: polling + skip keys with too many errors """ if not self._key_cycle: return None - # 最多尝试所有 key + # Try at most all keys for _ in range(len(self._api_keys)): key = next(self._key_cycle) - # 跳过错误次数过多的 key(超过 3 次) + # Skip keys with too many errors (more than 3 times) if self._key_errors.get(key, 0) < 3: return key - # 所有 key 都有问题,重置错误计数并返回第一个 + # There is a problem with all keys, reset the error count and return the first one logger.warning(f"[{self._name}] 所有 API Key 都有错误记录,重置错误计数") self._key_errors = {key: 0 for key in self._api_keys} return self._api_keys[0] if self._api_keys else None def _record_success(self, key: str) -> None: - """记录成功使用""" + """Record successful use""" self._key_usage[key] = self._key_usage.get(key, 0) + 1 - # 成功后减少错误计数 + # Decrement error count on success if key in self._key_errors and self._key_errors[key] > 0: self._key_errors[key] -= 1 def _record_error(self, key: str) -> None: - """记录错误""" + """Log errors""" self._key_errors[key] = self._key_errors.get(key, 0) + 1 logger.warning(f"[{self._name}] API Key {key[:8]}... 错误计数: {self._key_errors[key]}") @abstractmethod def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse: - """执行搜索(子类实现)""" + """Perform a search (subclass implementation)""" pass def search(self, query: str, max_results: int = 5, days: int = 7) -> SearchResponse: """ - 执行搜索 + Perform a search Args: - query: 搜索关键词 - max_results: 最大返回结果数 - days: 搜索最近几天的时间范围(默认7天) + query: search keyword + max_results: Maximum number of results returned + days: Search the time range of the last few days (default 7 days) Returns: - SearchResponse 对象 + SearchResponse object """ api_key = self._get_next_key() if not api_key: @@ -199,7 +199,7 @@ class BaseSearchProvider(ABC): @staticmethod def _extract_domain(url: str) -> str: - """从 URL 提取域名作为来源""" + """Extract domain name from URL as source""" try: parsed = urlparse(url) domain = parsed.netloc.replace('www.', '') @@ -210,31 +210,31 @@ class BaseSearchProvider(ABC): class TavilySearchProvider(BaseSearchProvider): """ - Tavily 搜索引擎 + Tavily search engine - 特点: - - 专为 AI/LLM 优化的搜索 API - - 免费版每月 1000 次请求 - - 返回结构化的搜索结果 + Features: + - Search API optimized for AI/LLM + - Free version 1000 requests per month + - Return structured search results - 文档:https://docs.tavily.com/ + Documentation: https://docs.tavily.com/ """ def __init__(self, api_keys: List[str]): super().__init__(api_keys, "Tavily") def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse: - """执行 Tavily 搜索""" + """Perform a Tavily search""" try: from tavily import TavilyClient except ImportError: - # 如果未安装 tavily-python,使用 REST API + # If tavily-python is not installed, use the REST API return self._do_search_rest(query, api_key, max_results, days) try: client = TavilyClient(api_key=api_key) - # 执行搜索 + # Perform a search response = client.search( query=query, search_depth="advanced", @@ -244,7 +244,7 @@ class TavilySearchProvider(BaseSearchProvider): days=days, ) - # 解析结果 + # Parse results results = [] for item in response.get('results', []): results.append(SearchResult( @@ -276,7 +276,7 @@ class TavilySearchProvider(BaseSearchProvider): ) def _do_search_rest(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse: - """使用 REST API 执行 Tavily 搜索(备选方案)""" + """Performing Tavily searches using the REST API (alternative)""" try: url = "https://api.tavily.com/search" headers = { @@ -332,20 +332,20 @@ class TavilySearchProvider(BaseSearchProvider): class SerpAPISearchProvider(BaseSearchProvider): """ - SerpAPI 搜索引擎 + SerpAPI search engine - 特点: - - 支持 Google、Bing、百度等多种搜索引擎 - - 免费版每月 100 次请求 + Features: + -Support multiple search engines such as Google, Bing, Baidu, etc. + - Free version 100 requests per month - 文档:https://serpapi.com/ + Documentation: https://serpapi.com/ """ def __init__(self, api_keys: List[str]): super().__init__(api_keys, "SerpAPI") def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse: - """执行 SerpAPI 搜索""" + """Perform a SerpAPI search""" try: from serpapi import GoogleSearch except ImportError: @@ -405,7 +405,7 @@ class SerpAPISearchProvider(BaseSearchProvider): ) def _do_search_rest(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse: - """使用 REST API 执行 SerpAPI 搜索""" + """Performing SerpAPI searches using the REST API""" try: tbs = "qdr:w" if days <= 1: @@ -467,14 +467,14 @@ class SerpAPISearchProvider(BaseSearchProvider): class GoogleSearchProvider(BaseSearchProvider): - """Google Custom Search (CSE) 搜索引擎""" + """Google Custom Search (CSE) search engine""" def __init__(self, api_key: str, cx: str): super().__init__([api_key] if api_key else [], "Google") self._cx = cx def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse: - """执行 Google 搜索""" + """Perform a Google search""" global _google_quota_exhausted, _google_quota_reset_time if not self._cx: @@ -495,7 +495,7 @@ class GoogleSearchProvider(BaseSearchProvider): 'num': min(max_results, 10), } - # 添加时间限制 + # Add time limit if days <= 1: params['dateRestrict'] = 'd1' elif days <= 7: @@ -550,13 +550,13 @@ class GoogleSearchProvider(BaseSearchProvider): class BingSearchProvider(BaseSearchProvider): - """Bing Search API 搜索引擎""" + """Bing Search API search engine""" def __init__(self, api_key: str): super().__init__([api_key] if api_key else [], "Bing") def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse: - """执行 Bing 搜索""" + """Perform a Bing search""" try: url = "https://api.bing.microsoft.com/v7.0/search" headers = {"Ocp-Apim-Subscription-Key": api_key} @@ -600,15 +600,15 @@ class BingSearchProvider(BaseSearchProvider): class DuckDuckGoSearchProvider(BaseSearchProvider): - """DuckDuckGo 搜索引擎(免费,无需 API Key)""" + """DuckDuckGo search engine (free, no API Key required)""" def __init__(self): super().__init__(['free'], "DuckDuckGo") def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse: - """执行 DuckDuckGo 搜索""" + """Perform a DuckDuckGo search""" try: - # 使用 DuckDuckGo Instant Answer API + # Using DuckDuckGo Instant Answer API url = "https://api.duckduckgo.com/" params = { 'q': query, @@ -623,7 +623,7 @@ class DuckDuckGoSearchProvider(BaseSearchProvider): results = [] - # 获取 RelatedTopics + # Get RelatedTopics related_topics = data.get('RelatedTopics', []) for topic in related_topics[:max_results]: if isinstance(topic, dict): @@ -646,7 +646,7 @@ class DuckDuckGoSearchProvider(BaseSearchProvider): source='DuckDuckGo', )) - # 检查 AbstractURL + # Check AbstractURL if data.get('AbstractURL') and len(results) < max_results: results.insert(0, SearchResult( title=data.get('Heading', query), @@ -655,7 +655,7 @@ class DuckDuckGoSearchProvider(BaseSearchProvider): source='DuckDuckGo', )) - # 如果没有结果,尝试 HTML 版本 + # If no results, try the HTML version if not results: results = self._search_html(query, max_results) @@ -676,7 +676,7 @@ class DuckDuckGoSearchProvider(BaseSearchProvider): ) def _search_html(self, query: str, max_results: int) -> List[SearchResult]: - """DuckDuckGo HTML 搜索备选""" + """DuckDuckGo HTML search alternatives""" try: url = "https://lite.duckduckgo.com/lite/" headers = { @@ -715,12 +715,12 @@ class DuckDuckGoSearchProvider(BaseSearchProvider): class SearchService: """ - 搜索服务 + Search service - 功能: - 1. 管理多个搜索引擎 - 2. 自动故障转移 - 3. 结果聚合和格式化 + Function: + 1. Manage multiple search engines + 2. Automatic failover + 3. Result aggregation and formatting """ def __init__(self): @@ -730,17 +730,17 @@ class SearchService: self._init_providers() def _load_config(self): - """加载配置""" + """Load configuration""" config = load_addon_config() self._config = config.get('search', {}) self.provider = self._config.get('provider', 'google') self.max_results = int(self._config.get('max_results', 10)) def _init_providers(self): - """初始化搜索引擎(按优先级排序)""" + """Initialize search engines (sorted by priority)""" from app.config import APIKeys - # 1. Tavily(AI优化搜索) + # 1. Tavily (AI optimized search) tavily_keys = APIKeys.TAVILY_API_KEYS if tavily_keys: self._providers.append(TavilySearchProvider(tavily_keys)) @@ -765,7 +765,7 @@ class SearchService: self._providers.append(BingSearchProvider(bing_api_key)) logger.info("已配置 Bing 搜索") - # 5. DuckDuckGo(免费兜底) + # 5. DuckDuckGo (Free Tips) self._providers.append(DuckDuckGoSearchProvider()) logger.info("已配置 DuckDuckGo 搜索(免费兜底)") @@ -774,25 +774,25 @@ class SearchService: @property def is_available(self) -> bool: - """检查是否有可用的搜索引擎""" + """Check if a search engine is available""" return any(p.is_available for p in self._providers) def search(self, query: str, num_results: int = None, date_restrict: str = None, days: int = 7) -> List[Dict[str, Any]]: """ - 执行搜索(兼容旧接口) + Perform a search (compatible with old interface) Args: - query: 搜索关键词 - num_results: 最大返回结果数 - date_restrict: 时间限制(Google 格式,如 'd7') - days: 搜索最近几天(优先级高于 date_restrict) + query: search keyword + num_results: Maximum number of results returned + date_restrict: time restriction (Google format, such as 'd7') + days: Search the last few days (priority higher than date_restrict) Returns: - 搜索结果列表 + Search result list """ limit = num_results if num_results else self.max_results - # 解析 date_restrict 为 days + # Parse date_restrict to days if date_restrict and not days: if date_restrict.startswith('d'): days = int(date_restrict[1:]) @@ -806,17 +806,17 @@ class SearchService: def search_with_fallback(self, query: str, max_results: int = 5, days: int = 7) -> SearchResponse: """ - 执行搜索(带自动故障转移) + Perform a search (with automatic failover) Args: - query: 搜索关键词 - max_results: 最大返回结果数 - days: 搜索最近几天 + query: search keyword + max_results: Maximum number of results returned + days: Search the last few days Returns: - SearchResponse 对象 + SearchResponse object """ - # 依次尝试各个搜索引擎 + # Try each search engine in sequence for provider in self._providers: if not provider.is_available: continue @@ -828,7 +828,7 @@ class SearchService: else: logger.warning(f"{provider.name} 搜索失败: {response.error_message},尝试下一个引擎") - # 所有引擎都失败 + # all engines fail return SearchResponse( query=query, results=[], @@ -845,27 +845,27 @@ class SearchService: max_results: int = 5 ) -> SearchResponse: """ - 搜索股票相关新闻 + Search stock related news Args: - stock_code: 股票代码 - stock_name: 股票名称 - market: 市场类型 - max_results: 最大返回结果数 + stock_code: stock code + stock_name: stock name + market: market type + max_results: Maximum number of results returned Returns: - SearchResponse 对象 + SearchResponse object """ - # 智能确定搜索时间范围 + # Intelligently determine search time range today_weekday = datetime.now().weekday() - if today_weekday == 0: # 周一 + if today_weekday == 0: # on Monday search_days = 3 - elif today_weekday >= 5: # 周末 + elif today_weekday >= 5: # weekend search_days = 2 else: search_days = 1 - # 根据市场类型构建搜索查询 + # Build search queries based on market type if market == "USStock": query = f"{stock_name} {stock_code} stock news latest" elif market == "Crypto": @@ -886,7 +886,7 @@ class SearchService: event_types: Optional[List[str]] = None ) -> SearchResponse: """ - 搜索股票特定事件(年报预告、减持等) + Search for specific stock events (annual report preview, shareholding reduction, etc.) """ if event_types is None: event_types = ["年报预告", "减持公告", "业绩快报"] @@ -897,12 +897,12 @@ class SearchService: return self.search_with_fallback(query, max_results=5, days=30) -# 单例实例 +# Singleton instance _search_service: Optional[SearchService] = None def get_search_service() -> SearchService: - """获取搜索服务单例""" + """Get the search service singleton""" global _search_service if _search_service is None: _search_service = SearchService() @@ -910,6 +910,6 @@ def get_search_service() -> SearchService: def reset_search_service() -> None: - """重置搜索服务(用于测试或配置更新后)""" + """Reset the search service (for testing or after configuration updates)""" global _search_service _search_service = None diff --git a/backend_api_python/app/services/signal_notifier.py b/backend_api_python/app/services/signal_notifier.py index 852c9ec..e1aaedc 100644 --- a/backend_api_python/app/services/signal_notifier.py +++ b/backend_api_python/app/services/signal_notifier.py @@ -92,17 +92,17 @@ class SignalNotifier: """ Notify signal events across channels. - 通知配置说明: - - 用户在个人中心配置自己的通知设置(telegram_bot_token, telegram_chat_id, email 等) - - 创建策略/监控时,系统自动使用用户配置的通知目标 + Notification configuration instructions: + - Users configure their own notification settings (telegram_bot_token, telegram_chat_id, email, etc.) in the personal center + - When creating policies/monitors, the system automatically uses user-configured notification targets - 公共服务配置(管理员在系统设置中配置): + Public service configuration (configured by administrator in system settings): - SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, SMTP_USE_TLS - (邮件服务,所有用户共用) + (Mail service, shared by all users) - TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER - (短信服务,所有用户共用) + (SMS service, shared by all users) - 可选的环境变量: + Optional environment variables: - SIGNAL_NOTIFY_TIMEOUT_SEC: HTTP timeout (default: 6) """ @@ -112,7 +112,7 @@ class SignalNotifier: except Exception: self.timeout_sec = 6.0 - # 公共 SMTP 配置(管理员在系统设置中配置) + # Public SMTP configuration (configured by administrator in system settings) self.smtp_host = (os.getenv("SMTP_HOST") or "").strip() try: self.smtp_port = int(os.getenv("SMTP_PORT") or "587") @@ -663,10 +663,10 @@ class SignalNotifier: token_override: str = "", parse_mode: str = "", ) -> Tuple[bool, str]: - # 用户必须在个人中心配置自己的 telegram_bot_token + # Users must configure their own telegram_bot_token in the personal center token = (token_override or "").strip() if not token: - return False, "missing_telegram_bot_token (请在个人中心配置 Telegram Bot Token)" + return False, "missing_telegram_bot_token (please configure Telegram Bot Token in the personal center)" if not chat_id: return False, "missing_telegram_chat_id" url = f"https://api.telegram.org/bot{token}/sendMessage" @@ -860,4 +860,3 @@ class SignalNotifier: return results - diff --git a/backend_api_python/app/services/trading_executor.py b/backend_api_python/app/services/trading_executor.py index 0c5b9ca..0779c0a 100644 --- a/backend_api_python/app/services/trading_executor.py +++ b/backend_api_python/app/services/trading_executor.py @@ -1,5 +1,5 @@ """ -实时交易执行服务 +Real-time trade execution services """ import time import threading @@ -27,10 +27,10 @@ logger = get_logger(__name__) class TradingExecutor: - """实时交易执行器 (Signal Provider Mode)""" + """Real-time transaction executor (Signal Provider Mode)""" def __init__(self): - # 不再使用全局连接,改为每次使用时从连接池获取 + # Instead of using a global connection, obtain it from the connection pool each time it is used. self.running_strategies = {} # {strategy_id: thread} self.lock = threading.Lock() # Local-only lightweight in-memory price cache (symbol -> (price, expiry_ts)). @@ -44,22 +44,22 @@ class TradingExecutor: # Keyed by (strategy_id, symbol, signal_type, signal_timestamp). self._signal_dedup = {} # type: Dict[int, Dict[str, float]] self._signal_dedup_lock = threading.Lock() - self.kline_service = KlineService() # K线服务(带缓存) + self.kline_service = KlineService() # K-line service (with cache) - # 单实例线程上限,避免无限制创建线程导致 can't start new thread/OOM + # The upper limit of single-instance threads to avoid unlimited thread creation causing can't start new thread/OOM self.max_threads = int(os.getenv('STRATEGY_MAX_THREADS', '64')) - # 确保数据库字段存在 + # Make sure the database field exists self._ensure_db_columns() def _ensure_db_columns(self): - """确保必要的数据库字段存在(PostgreSQL)""" + """Ensure necessary database fields exist (PostgreSQL)""" try: with get_db_connection() as db: cursor = db.cursor() col_names = set() - # PostgreSQL: 使用 information_schema 查询列 + # PostgreSQL: Query columns using information_schema try: cursor.execute(""" SELECT column_name FROM information_schema.columns @@ -93,7 +93,7 @@ class TradingExecutor: 典型场景:OKX 永续统一符号通常是 `BNB/USDT:USDT`,但前端/数据库可能传 `BNB/USDT`。 """ try: - # 新系统:仅支持 swap(合约永续) / spot(现货) + # New system: only supports swap (perpetual contract) / spot (spot) if market_type != 'swap': return symbol if not symbol or ':' in symbol: @@ -101,7 +101,7 @@ class TradingExecutor: if not getattr(exchange, 'markets', None): return symbol - # 如果 symbol 本身就是合约市场,直接返回 + # If symbol itself is a contract market, return it directly try: m = exchange.market(symbol) if m and (m.get('swap') or m.get('future') or m.get('contract')): @@ -109,7 +109,7 @@ class TradingExecutor: except Exception: pass - # OKX/部分交易所:永续常见为 BASE/QUOTE:QUOTE 或 BASE/QUOTE:USDT + # OKX/some exchanges: Perpetual is usually BASE/QUOTE:QUOTE or BASE/QUOTE:USDT if '/' not in symbol: return symbol base, quote = symbol.split('/', 1) @@ -133,7 +133,7 @@ class TradingExecutor: def _log_resource_status(self, prefix: str = ""): """调试:记录线程/内存使用,便于定位 can't start new thread 根因""" try: - import psutil # 如果有安装则使用更精确的指标 + import psutil # Use more precise metrics if installed p = psutil.Process() mem = p.memory_info().rss / 1024 / 1024 th = p.num_threads() @@ -142,7 +142,7 @@ class TradingExecutor: except Exception: try: th = threading.active_count() - # 从 /proc/self/status 读取 VmRSS(适用于 Linux 容器) + # Read VmRSS from /proc/self/status (for Linux containers) vmrss = None try: with open('/proc/self/status') as f: @@ -378,17 +378,17 @@ class TradingExecutor: def start_strategy(self, strategy_id: int) -> bool: """ - 启动策略 + launch strategy Args: - strategy_id: 策略ID + strategy_id: Strategy ID Returns: - 是否成功 + Is it successful? """ try: with self.lock: - # 清理已退出的线程,防止计数膨胀 + # Clean up exited threads to prevent count inflation stale_ids = [sid for sid, th in self.running_strategies.items() if not th.is_alive()] for sid in stale_ids: del self.running_strategies[sid] @@ -405,7 +405,7 @@ class TradingExecutor: logger.warning(f"Strategy {strategy_id} is already running") return False - # 创建并启动线程 + # Create and start threads thread = threading.Thread( target=self._run_strategy_loop, args=(strategy_id,), @@ -414,7 +414,7 @@ class TradingExecutor: try: thread.start() except Exception as e: - # 捕获 can't start new thread 等异常,记录资源状态 + # Capture exceptions such as can't start new thread and record resource status self._log_resource_status(prefix="启动异常") raise e self.running_strategies[strategy_id] = thread @@ -430,13 +430,13 @@ class TradingExecutor: def stop_strategy(self, strategy_id: int) -> bool: """ - 停止策略 + stopping strategy Args: - strategy_id: 策略ID + strategy_id: Strategy ID Returns: - 是否成功 + Is it successful? """ try: with self.lock: @@ -444,7 +444,7 @@ class TradingExecutor: logger.warning(f"Strategy {strategy_id} is not running") return False - # 标记策略为停止状态 + # Mark policy as stopped with get_db_connection() as db: cursor = db.cursor() cursor.execute( @@ -454,7 +454,7 @@ class TradingExecutor: db.commit() cursor.close() - # 从运行列表中移除(线程会在下次循环检查状态时退出) + # Removed from the run list (the thread will exit the next time the loop checks status) del self.running_strategies[strategy_id] logger.info(f"Strategy {strategy_id} stopped") @@ -468,16 +468,16 @@ class TradingExecutor: def _run_strategy_loop(self, strategy_id: int): """ - 策略运行循环 + strategy run loop Args: - strategy_id: 策略ID + strategy_id: Strategy ID """ logger.info(f"Strategy {strategy_id} loop starting") self._console_print(f"[strategy:{strategy_id}] loop initializing") try: - # 加载策略配置 + # Load policy configuration strategy = self._load_strategy(strategy_id) if not strategy: logger.error(f"Strategy {strategy_id} not found") @@ -487,7 +487,7 @@ class TradingExecutor: logger.error(f"Strategy {strategy_id} has unsupported strategy_type for realtime execution: {strategy['strategy_type']}") return - # 初始化策略状态 + # Initialize policy state trading_config = strategy['trading_config'] indicator_config = strategy['indicator_config'] ai_model_config = strategy.get('ai_model_config') or {} @@ -499,7 +499,7 @@ class TradingExecutor: symbol = trading_config.get('symbol', '') timeframe = trading_config.get('timeframe', '1H') - # 安全获取 leverage 和 trade_direction + # Secure access to leverage and trade_direction try: leverage_val = trading_config.get('leverage', 1) if isinstance(leverage_val, (list, tuple)): @@ -509,39 +509,39 @@ class TradingExecutor: logger.warning(f"Strategy {strategy_id} invalid leverage format, reset to 1: {trading_config.get('leverage')}") leverage = 1.0 - # 获取市场类型,默认为合约 - # 根据杠杆自动判断:杠杆=1为现货,杠杆>1为合约 + # Get the market type, default is contract + # Automatic judgment based on leverage: leverage = 1 for spot, leverage > 1 for contract market_type = trading_config.get('market_type', 'swap') if market_type not in ['swap', 'spot']: logger.error(f"Strategy {strategy_id} invalid market_type={market_type} (only swap/spot supported); refusing to start") return - # 根据杠杆自动调整市场类型 + # Automatically adjust market type based on leverage if leverage == 1.0: - market_type = 'spot' # 现货固定1倍杠杆 + market_type = 'spot' # Spot fixed 1x leverage logger.info(f"Strategy {strategy_id} leverage=1; auto-switch market_type to spot") else: - # 合约市场:统一使用 swap(永续),避免 futures/delivery 混淆导致持仓/下单查错市场 + # Contract market: uniformly use swap (perpetual) to avoid futures/delivery confusion that may lead to position/order checking in the wrong market. market_type = 'swap' logger.info(f"Strategy {strategy_id} derivatives trading; normalize market_type to: {market_type}") - # 根据市场类型限制杠杆 + # Limit leverage based on market type if market_type == 'spot': - leverage = 1.0 # 现货固定1倍杠杆 + leverage = 1.0 # Spot fixed 1x leverage elif leverage < 1: leverage = 1.0 elif leverage > 125: leverage = 125.0 logger.warning(f"Strategy {strategy_id} leverage > 125; capped to 125") - # 获取交易方向,现货只能做多 + # Get the trading direction, spot can only go long trade_direction = trading_config.get('trade_direction', 'long') if market_type == 'spot': - trade_direction = 'long' # 现货只能做多 + trade_direction = 'long' # Spot prices can only be long logger.info(f"Strategy {strategy_id} spot trading; force trade_direction=long") - # 获取市场类别(Crypto, USStock, Forex, Futures) - # 这决定了使用哪个数据源来获取价格和K线数据 + # Get market category (Crypto, USStock, Forex, Futures) + # This determines which data source to use to obtain price and K-line data market_category = (strategy.get('market_category') or 'Crypto').strip() logger.info(f"Strategy {strategy_id} market_category: {market_category}") @@ -557,10 +557,10 @@ class TradingExecutor: ) return - # 初始化交易所连接(信号模式下无需真实连接) + # Initialize exchange connection (no real connection required in signal mode) exchange = None - # 安全获取 initial_capital + # Safely obtain initial_capital try: initial_capital_val = strategy.get('initial_capital', 1000) if isinstance(initial_capital_val, (list, tuple)): @@ -570,13 +570,13 @@ class TradingExecutor: logger.warning(f"Strategy {strategy_id} invalid initial_capital format, reset to 1000: {strategy.get('initial_capital')}") initial_capital = 1000.0 - # 净值会在首次更新持仓时自动计算和更新 + # Equity is automatically calculated and updated the first time a position is updated - # 获取指标代码 + # Get indicator code indicator_id = indicator_config.get('indicator_id') indicator_code = indicator_config.get('indicator_code', '') - # 如果代码为空,尝试从数据库获取 + # If the code is empty, try to get it from the database if not indicator_code and indicator_id: indicator_code = self._get_indicator_code_from_db(indicator_id) @@ -584,11 +584,11 @@ class TradingExecutor: logger.error(f"Strategy {strategy_id} indicator_code is empty") return - # 确保 indicator_code 是字符串(处理 JSON 转义问题) + # Make sure indicator_code is a string (to handle JSON escaping issues) if not isinstance(indicator_code, str): indicator_code = str(indicator_code) - # 处理可能的 JSON 转义问题 + # Handle possible JSON escaping issues if '\\n' in indicator_code and '\n' not in indicator_code: try: import json @@ -609,9 +609,9 @@ class TradingExecutor: ) # ============================================ - # 初始化阶段:获取历史K线并计算指标 + # Initialization phase: Obtain historical K-lines and calculate indicators # ============================================ - # logger.info(f"策略 {strategy_id} 初始化:获取历史K线数据...") + # logger.info(f"Strategy {strategy_id} initialization: Get historical K-line data...") history_limit = int(os.getenv('K_LINE_HISTORY_GET_NUMBER', 500)) klines = self._fetch_latest_kline(symbol, timeframe, limit=history_limit, market_category=market_category) if not klines or len(klines) < 2: @@ -619,20 +619,20 @@ class TradingExecutor: return logger.info(rf'Strategy {strategy_id} history kline number: {len(klines)}') - # 转换为DataFrame + # Convert to DataFrame df = self._klines_to_dataframe(klines) if len(df) == 0: logger.error(f"Strategy {strategy_id} K-lines are empty after normalization") return # ============================================ - # 启动时:同步持仓状态,清理"幽灵持仓" + # At startup: synchronize position status and clean up "ghost positions" # ============================================ - # 即使信号模式下,也要在启动时检查并清理用户在交易所手动平仓但数据库记录还在的情况 - # 这样可以避免策略认为还有持仓而无法执行新的开仓信号 + # Even in signal mode, it is necessary to check and clean up the situation when the user manually closes the position on the exchange but the database record is still there. + # This can prevent the strategy from thinking that there are still positions and being unable to execute a new opening signal. try: logger.info(f"策略 {strategy_id} 启动时检查持仓同步...") - # 调用持仓同步逻辑(即使signal模式也要检查) + # Call position synchronization logic (check even in signal mode) from app import get_pending_order_worker worker = get_pending_order_worker() if worker and hasattr(worker, '_sync_positions_best_effort'): @@ -641,30 +641,30 @@ class TradingExecutor: except Exception as e: logger.warning(f"策略 {strategy_id} 启动时持仓同步失败(不影响启动): {e}") - # 获取当前持仓最高价(从本地数据库读取) + # Get the current highest position price (read from local database) current_pos_list = self._get_current_positions(strategy_id, symbol) initial_highest = 0.0 - initial_position = 0 # 0=无持仓, 1=多头, -1=空头 + initial_position = 0 # 0=No position, 1=Long position, -1=Short position initial_avg_entry_price = 0.0 initial_position_count = 0 initial_last_add_price = 0.0 if current_pos_list: - pos = current_pos_list[0] # 取第一个持仓(单向持仓模式) + pos = current_pos_list[0] # Take the first position (one-way position mode) initial_highest = float(pos.get('highest_price', 0) or 0) pos_side = pos.get('side', 'long') initial_position = 1 if pos_side == 'long' else -1 initial_avg_entry_price = float(pos.get('entry_price', 0) or 0) - initial_position_count = 1 # 简化处理,假设是单笔持仓 + initial_position_count = 1 # To simplify the process, assume it is a single position initial_last_add_price = initial_avg_entry_price - # 关键诊断日志:确认指标是否拿到了持仓状态 + # Key diagnostic log: Confirm whether the indicator has obtained the position status logger.info( f"策略 {strategy_id} 指标注入持仓状态: count={len(current_pos_list)}, " f"position={initial_position}, entry_price={initial_avg_entry_price}, highest={initial_highest}" ) - # 执行指标代码,获取信号和触发价格 + # Execute indicator code, get signals and trigger prices indicator_result = self._execute_indicator_with_prices( indicator_code, df, trading_config, initial_highest_price=initial_highest, @@ -677,9 +677,9 @@ class TradingExecutor: logger.error(f"Strategy {strategy_id} indicator execution failed") return - # 提取信号和触发价格 - pending_signals = indicator_result.get('pending_signals', []) # 待触发的信号列表 - last_kline_time = indicator_result.get('last_kline_time', 0) # 最后一根K线的时间 + # Extract signals and trigger prices + pending_signals = indicator_result.get('pending_signals', []) # List of signals to be triggered + last_kline_time = indicator_result.get('last_kline_time', 0) # The time of the last K-line logger.info(f"Strategy {strategy_id} initialized; pending_signals={len(pending_signals)}") if pending_signals: @@ -701,14 +701,14 @@ class TradingExecutor: last_tick_time = 0.0 last_kline_update_time = time.time() - # 计算K线周期(秒) + # Calculate K-line period (seconds) from app.data_sources.base import TIMEFRAME_SECONDS timeframe_seconds = TIMEFRAME_SECONDS.get(timeframe, 3600) - kline_update_interval = timeframe_seconds # 每个K线周期更新一次 + kline_update_interval = timeframe_seconds # Updated once every K-line cycle while True: try: - # 检查策略状态 + # Check policy status if not self._is_strategy_running(strategy_id): logger.info(f"Strategy {strategy_id} stopped") break @@ -724,7 +724,7 @@ class TradingExecutor: last_tick_time = current_time # ============================================ - # 0. 虚拟持仓模式,无需同步交易所 + # 0. Virtual position mode, no need to synchronize exchanges # ============================================ # pass @@ -737,7 +737,7 @@ class TradingExecutor: continue # ============================================ - # 2. 检查是否需要更新K线(每个K线周期更新一次,从API拉取) + # 2. Check whether the K-line needs to be updated (updated once every K-line cycle, pulled from API) # ============================================ if current_time - last_kline_update_time >= kline_update_interval: klines = self._fetch_latest_kline(symbol, timeframe, limit=history_limit, market_category=market_category) @@ -775,7 +775,7 @@ class TradingExecutor: last_kline_update_time = current_time - # 更新 highest_price(使用最新 close 作为 current_price 的近似) + # Update highest_price (using latest close as an approximation of current_price) if new_hp > 0 and current_pos_list: current_close = float(df['close'].iloc[-1]) for p in current_pos_list: @@ -787,7 +787,7 @@ class TradingExecutor: ) else: # ============================================ - # 3. 非K线更新tick:用当前价更新最后一根K线并重算指标(统一tick节奏) + # 3. Non-K-line update tick: update the last K-line with the current price and recalculate the indicator (unify the tick rhythm) # ============================================ if 'df' in locals() and df is not None and len(df) > 0: try: @@ -836,7 +836,7 @@ class TradingExecutor: # ============================================ # 4. Evaluate triggers once per tick # ============================================ - # 优化点4: 信号有效期清理 (Signal Expiration) + # Optimization point 4: Signal expiration cleanup (Signal Expiration) current_ts = int(time.time()) if pending_signals: expiration_threshold = timeframe_seconds * 2 @@ -854,7 +854,7 @@ class TradingExecutor: if pending_signals: logger.info(f"[monitoring] strategy={strategy_id} price={current_price}, pending_signals={len(pending_signals)}") - # 检查是否有待触发的信号 + # Check if there is a signal to be triggered triggered_signals = [] signals_to_remove = [] @@ -862,15 +862,15 @@ class TradingExecutor: signal_type = signal_info.get('type') # 'open_long', 'close_long', 'open_short', 'close_short' trigger_price = signal_info.get('trigger_price', 0) - # 检查价格是否触发 + # Check if price triggers triggered = False - # 【关键修复】平仓/止损止盈信号默认“立即触发” + # [Key Fix] Position closing/stop loss and take profit signals default to "trigger immediately" exit_trigger_mode = trading_config.get('exit_trigger_mode', 'immediate') # 'immediate' or 'price' if signal_type in ['close_long', 'close_short'] and exit_trigger_mode == 'immediate': triggered = True - # 【可选】开仓/加仓信号是否“立即触发” + # [Optional] Whether the signal to open/increase a position is "triggered immediately" entry_trigger_mode = trading_config.get('entry_trigger_mode', 'price') # 'price' or 'immediate' if signal_type in ['open_long', 'open_short', 'add_long', 'add_short'] and entry_trigger_mode == 'immediate': triggered = True @@ -917,12 +917,12 @@ class TradingExecutor: if risk_sl: triggered_signals.append(risk_sl) - # 从待触发列表中移除已触发的信号 + # Remove a triggered signal from the pending trigger list for signal_info in signals_to_remove: if signal_info in pending_signals: pending_signals.remove(signal_info) - # 执行触发的信号 + # Execution trigger signal if triggered_signals: logger.info(f"Strategy {strategy_id} triggered signals: {triggered_signals}") @@ -1031,7 +1031,7 @@ class TradingExecutor: logger.error(traceback.format_exc()) self._console_print(f"[strategy:{strategy_id}] fatal error: {e}") finally: - # 清理 + # clean up with self.lock: if strategy_id in self.running_strategies: del self.running_strategies[strategy_id] @@ -1040,7 +1040,7 @@ class TradingExecutor: def _sync_positions_with_exchange(self, strategy_id: int, exchange: Any, symbol: str, market_type: str): """ - [Depracated] 信号模式下无需同步交易所持仓 + [Depracated] No need to synchronize exchange positions in signal mode """ pass @@ -1064,7 +1064,7 @@ class TradingExecutor: cursor.close() if strategy: - # 解析JSON字段 + # Parse JSON fields for field in ['indicator_config', 'trading_config', 'notification_config', 'ai_model_config']: if isinstance(strategy.get(field), str): try: @@ -1079,7 +1079,7 @@ class TradingExecutor: strategy['exchange_config'] = json.loads(exchange_config_str) except Exception as e: logger.error(f"Strategy {strategy_id} failed to parse exchange_config: {str(e)}") - # 尝试直接解析 JSON(向后兼容) + # Try parsing JSON directly (backwards compatible) try: strategy['exchange_config'] = json.loads(exchange_config_str) except: @@ -1099,7 +1099,7 @@ class TradingExecutor: 同时检查数据库状态和线程状态,避免重启后状态不一致 """ try: - # 1. 检查数据库状态 + # 1. Check database status with get_db_connection() as db: cursor = db.cursor() cursor.execute( @@ -1110,15 +1110,15 @@ class TradingExecutor: cursor.close() db_status = result and result.get('status') == 'running' - # 2. 检查线程是否真的在运行 + # 2. Check if the thread is actually running with self.lock: thread = self.running_strategies.get(strategy_id) thread_running = thread is not None and thread.is_alive() - # 3. 如果数据库状态是running但线程不在运行,说明状态不一致(可能是重启后恢复失败) + # 3. If the database status is running but the thread is not running, it means the status is inconsistent (may be recovery failure after restart) if db_status and not thread_running: logger.warning(f"Strategy {strategy_id} status mismatch: DB=running but thread not running. Updating DB status to stopped.") - # 更新数据库状态为stopped,避免策略"僵尸"状态 + # Update the database status to stopped to avoid the policy "zombie" state try: with get_db_connection() as db: cursor = db.cursor() @@ -1132,7 +1132,7 @@ class TradingExecutor: logger.error(f"Failed to update strategy {strategy_id} status to stopped: {e}") return False - # 4. 只有数据库状态和线程状态都一致时才返回True + # 4. Return True only if the database status and thread status are consistent return db_status and thread_running except Exception as e: logger.error(f"Error checking strategy {strategy_id} running status: {e}") @@ -1149,16 +1149,16 @@ class TradingExecutor: return None def _fetch_latest_kline(self, symbol: str, timeframe: str, limit: int = 500, market_category: str = 'Crypto') -> List[Dict[str, Any]]: - """获取最新K线数据(优先从缓存获取) + """Get the latest K-line data (get it from cache first) Args: - symbol: 交易对/代码 - timeframe: 时间周期 - limit: 数据条数 - market_category: 市场类型 (Crypto, USStock, Forex, Futures) + symbol: trading pair/symbol + timeframe: time period + limit: number of data items + market_category: Market type (Crypto, USStock, Forex, Futures) """ try: - # 使用 KlineService 获取K线数据(自动处理缓存) + # Use KlineService to obtain K-line data (automatically handle cache) return self.kline_service.get_kline( market=market_category, symbol=symbol, @@ -1171,13 +1171,13 @@ class TradingExecutor: return [] def _fetch_current_price(self, exchange: Any, symbol: str, market_type: str = None, market_category: str = 'Crypto') -> Optional[float]: - """获取当前价格 (根据 market_category 选择正确的数据源) + """Get the current price (select the correct data source based on market_category) Args: - exchange: 交易所实例(信号模式下为 None) - symbol: 交易对/代码 - market_type: 交易类型 (swap/spot) - market_category: 市场类型 (Crypto, USStock, Forex, Futures) + exchange: exchange instance (None in signal mode) + symbol: trading pair/symbol + market_type: transaction type (swap/spot) + market_category: Market type (Crypto, USStock, Forex, Futures) """ # Local in-memory cache first cache_key = f"{market_category}:{(symbol or '').strip().upper()}" @@ -1196,8 +1196,8 @@ class TradingExecutor: pass try: - # 根据 market_category 选择正确的数据源 - # 支持: Crypto, USStock, Forex, Futures + # Select the correct data source based on market_category + # Support: Crypto, USStock, Forex, Futures ticker = DataSourceFactory.get_ticker(market_category, symbol) if ticker: price = float(ticker.get('last') or ticker.get('close') or 0) @@ -1225,9 +1225,9 @@ class TradingExecutor: timeframe_seconds: int, ) -> Optional[Dict[str, Any]]: """ - 服务端兜底止损:当价格穿透止损线时,直接生成 close_long/close_short 信号。 + Server-side stop loss: when the price penetrates the stop loss line, close_long/close_short signals are directly generated. - 目的:防止“指标回放逻辑导致最后一根K线没有 close_* 信号”或“插针反弹导致二次触发条件不满足”时不止损。 + Purpose: To prevent non-stop loss when "the indicator replay logic causes the last K-line to have no close_* signal" or "the pin rebound causes the secondary trigger condition to be unsatisfied". """ try: if trading_config is None: @@ -1237,7 +1237,7 @@ class TradingExecutor: if str(enabled).lower() in ['0', 'false', 'no', 'off']: return None - # 获取当前持仓(使用本地数据库记录作为风控依据) + # Get the current position (use local database records as risk control basis) current_positions = self._get_current_positions(strategy_id, symbol) if not current_positions: return None @@ -1276,19 +1276,19 @@ class TradingExecutor: tf = int(timeframe_seconds or 60) candle_ts = int(now_ts // tf) * tf - # 多头:跌破止损线 + # Bulls: Falling below the stop loss line if side == 'long': stop_line = entry_price * (1 - sl) if current_price <= stop_line: return { 'type': 'close_long', - 'trigger_price': 0, # 立即触发(由 exit_trigger_mode 控制) + 'trigger_price': 0, # Trigger immediately (controlled by exit_trigger_mode) 'position_size': 0, 'timestamp': candle_ts, 'reason': 'server_stop_loss', 'stop_loss_price': stop_line, } - # 空头:突破止损线 + # Short: Stop loss line broken elif side == 'short': stop_line = entry_price * (1 + sl) if current_price >= stop_line: @@ -1462,12 +1462,12 @@ class TradingExecutor: return None def _klines_to_dataframe(self, klines: List[Dict[str, Any]]) -> pd.DataFrame: - """将K线数据转换为DataFrame""" + """Convert K-line data to DataFrame""" if not klines: - # 返回空的 DataFrame,包含正确的列 + # Returns an empty DataFrame with the correct columns return pd.DataFrame(columns=['open', 'high', 'low', 'close', 'volume']) - # 创建 DataFrame + # Create DataFrame df = pd.DataFrame(klines) # Convert time column. @@ -1479,7 +1479,7 @@ class TradingExecutor: df['timestamp'] = pd.to_datetime(df['timestamp'], unit='s', utc=True) df = df.set_index('timestamp') - # 确保只包含需要的列 + # Make sure to include only the columns you need required_columns = ['open', 'high', 'low', 'close', 'volume'] available_columns = [col for col in required_columns if col in df.columns] if not available_columns: @@ -1488,29 +1488,29 @@ class TradingExecutor: df = df[available_columns] - # 强制转换所有数值列为 float64 类型 + # Cast all numeric columns to float64 type for col in ['open', 'high', 'low', 'close', 'volume']: if col in df.columns: - # 先转换为数值类型,然后强制转换为 float64 + # Convert to numeric type first, then cast to float64 df[col] = pd.to_numeric(df[col], errors='coerce').astype('float64') - # 删除包含 NaN 的行 + # Delete rows containing NaN df = df.dropna() return df def _update_dataframe_with_current_price(self, df: pd.DataFrame, current_price: float, timeframe: str) -> pd.DataFrame: """ - 使用当前价格更新DataFrame的最后一根K线(用于实时计算) + Update the last bar of the DataFrame using the current price (for real-time calculations) """ if df is None or len(df) == 0: return df try: - # 获取最后一根K线的时间 + # Get the time of the last K-line last_time = df.index[-1] - # 计算当前时间对应的K线起始时间 + # Calculate the K-line starting time corresponding to the current time from app.data_sources.base import TIMEFRAME_SECONDS timeframe_key = timeframe if timeframe_key not in TIMEFRAME_SECONDS: @@ -1523,17 +1523,17 @@ class TradingExecutor: last_ts = float(last_time.timestamp()) now_ts = float(time.time()) - # 计算当前价格所属的 K 线开始时间 + # Calculate the starting time of the K-line to which the current price belongs current_period_start = int(now_ts // tf_seconds) * tf_seconds - # 检查最后一根K线是否就是当前周期的 + # Check whether the last K-line is for the current cycle if abs(last_ts - current_period_start) < 2: - # 更新最后一根 + # Update the last one df.iloc[-1, df.columns.get_loc('close')] = current_price df.iloc[-1, df.columns.get_loc('high')] = max(df.iloc[-1]['high'], current_price) df.iloc[-1, df.columns.get_loc('low')] = min(df.iloc[-1]['low'], current_price) elif current_period_start > last_ts: - # 追加新行 + # Added new bank new_row = pd.DataFrame({ 'open': [current_price], 'high': [current_price], @@ -1559,10 +1559,10 @@ class TradingExecutor: initial_last_add_price: float = 0.0 ) -> Optional[Dict[str, Any]]: """ - 执行指标代码并提取待触发的信号和价格 + Execute the indicator code and extract the signal and price to be triggered """ try: - # 执行指标代码 + # Execution indicator code executed_df, exec_env = self._execute_indicator_df( indicator_code, df, trading_config, initial_highest_price=initial_highest_price, @@ -1574,13 +1574,13 @@ class TradingExecutor: if executed_df is None: return None - # 提取最新的 highest_price + # Extract the latest highest_price new_highest_price = exec_env.get('highest_price', 0.0) - # 提取最后一根K线的时间 + # Extract the time of the last K-line last_kline_time = int(df.index[-1].timestamp()) if hasattr(df.index[-1], 'timestamp') else int(time.time()) - # 提取待触发的信号 + # Extract the signal to be triggered pending_signals = [] # Supported indicator signal formats: @@ -1615,7 +1615,7 @@ class TradingExecutor: # Check for 4-way columns after normalization if all(col in executed_df.columns for col in ['open_long', 'close_long', 'open_short', 'close_short']): - # 优化点3: 防“信号闪烁” (Repainting) + # Optimization point 3: Prevent “signal flicker” (Repainting) signal_mode = trading_config.get('signal_mode', 'confirmed') # 'confirmed' or 'aggressive' exit_signal_mode = trading_config.get('exit_signal_mode', 'aggressive') # 'confirmed' or 'aggressive' @@ -1623,7 +1623,7 @@ class TradingExecutor: exit_check_set = set() if len(executed_df) > 1: - # 始终检查上一根已完成K线 + # Always check the last completed K-line entry_check_set.add(len(executed_df) - 2) exit_check_set.add(len(executed_df) - 2) @@ -1633,16 +1633,16 @@ class TradingExecutor: if exit_signal_mode == 'aggressive' and len(executed_df) > 0: exit_check_set.add(len(executed_df) - 1) - # 统一遍历索引(保持确定性排序) + # Traverse the index uniformly (preserving deterministic ordering) check_indices = sorted(entry_check_set.union(exit_check_set), reverse=True) for idx in check_indices: - # 获取该K线的收盘价(作为默认触发价) + # Get the closing price of the K-line (as the default trigger price) close_price = float(executed_df['close'].iloc[idx]) - # 该信号的时间戳 + # The timestamp of the signal signal_timestamp = int(executed_df.index[idx].timestamp()) if hasattr(executed_df.index[idx], 'timestamp') else last_kline_time - # 开多信号(仅在 entry_check_set 中检查) + # Open long signal (only checked in entry_check_set) if idx in entry_check_set and executed_df['open_long'].iloc[idx]: trigger_price = close_price position_size = 0.08 @@ -1659,7 +1659,7 @@ class TradingExecutor: 'timestamp': signal_timestamp }) - # 平多信号 + # Hirata signal if idx in exit_check_set and executed_df['close_long'].iloc[idx]: trigger_price = close_price if not any(s['type'] == 'close_long' and s.get('timestamp') == signal_timestamp for s in pending_signals): @@ -1670,7 +1670,7 @@ class TradingExecutor: 'timestamp': signal_timestamp }) - # 开空信号 + # Open short signal if idx in entry_check_set and executed_df['open_short'].iloc[idx]: trigger_price = close_price position_size = 0.08 @@ -1687,7 +1687,7 @@ class TradingExecutor: 'timestamp': signal_timestamp }) - # 平空信号 + # flat signal if idx in exit_check_set and executed_df['close_short'].iloc[idx]: trigger_price = close_price if not any(s['type'] == 'close_short' and s.get('timestamp') == signal_timestamp for s in pending_signals): @@ -1698,7 +1698,7 @@ class TradingExecutor: 'timestamp': signal_timestamp }) - # 加多信号 + # add bull signal if idx in entry_check_set and 'add_long' in executed_df.columns and executed_df['add_long'].iloc[idx]: trigger_price = close_price position_size = 0.06 @@ -1715,7 +1715,7 @@ class TradingExecutor: 'timestamp': signal_timestamp }) - # 加空信号 + # Air conditioning signal if idx in entry_check_set and 'add_short' in executed_df.columns and executed_df['add_short'].iloc[idx]: trigger_price = close_price position_size = 0.06 @@ -1799,9 +1799,9 @@ class TradingExecutor: initial_position_count: int = 0, initial_last_add_price: float = 0.0 ) -> tuple[Optional[pd.DataFrame], dict]: - """执行指标代码,返回执行后的DataFrame和执行环境""" + """Execute the indicator code and return the executed DataFrame and execution environment.""" try: - # 确保 DataFrame 的所有数值列都是 float64 类型 + # Ensure that all numeric columns of the DataFrame are of type float64 df = df.copy() for col in ['open', 'high', 'low', 'close', 'volume']: if col in df.columns: @@ -1810,33 +1810,33 @@ class TradingExecutor: else: df[col] = df[col].astype('float64') - # 删除包含 NaN 的行 + # Delete rows containing NaN df = df.dropna() if len(df) == 0: logger.warning("DataFrame is empty; cannot execute indicator script") return None, {} - # 初始化信号Series + # Initialize signal Series signals = pd.Series(0, index=df.index, dtype='float64') - # 准备执行环境 + # Prepare execution environment # Expose the full trading config to indicator scripts so frontend parameters # (scale-in/out, position sizing, risk params) can be used directly. # Also provide a backtest-modal compatible nested config object: cfg.risk/cfg.scale/cfg.position. tc = dict(trading_config or {}) cfg = self._build_cfg_from_trading_config(tc) - # === 指标参数支持 === - # 从 trading_config 获取用户设置的指标参数 + # === Indicator parameter support === + # Get user-set indicator parameters from trading_config user_indicator_params = tc.get('indicator_params', {}) - # 解析指标代码中声明的参数 + # Parse the parameters declared in the indicator code declared_params = IndicatorParamsParser.parse_params(indicator_code) - # 合并参数(用户值优先,否则使用默认值) + # Merge parameters (user values ​​take precedence, otherwise default values ​​are used) merged_params = IndicatorParamsParser.merge_params(declared_params, user_indicator_params) - # === 指标调用器支持 === - # 获取用户ID和指标ID(用于 call_indicator 权限检查) + # === Indicator caller support === + # Get user ID and indicator ID (for call_indicator permission check) user_id = tc.get('user_id', 1) indicator_id = tc.get('indicator_id') indicator_caller = IndicatorCaller(user_id, indicator_id) @@ -1854,8 +1854,8 @@ class TradingExecutor: 'trading_config': tc, 'config': tc, # alias 'cfg': cfg, # normalized nested config - 'params': merged_params, # 指标参数 (新增) - 'call_indicator': indicator_caller.call_indicator, # 调用其他指标 (新增) + 'params': merged_params, # Indicator parameters (new) + 'call_indicator': indicator_caller.call_indicator, # Call other indicators (new) 'leverage': float(trading_config.get('leverage', 1)), 'initial_capital': float(trading_config.get('initial_capital', 1000)), 'commission': 0.001, @@ -1888,26 +1888,26 @@ class TradingExecutor: pre_import_code = "import numpy as np\nimport pandas as pd\n" exec(pre_import_code, exec_env) - # 兼容性修复:将旧版pandas的fillna(method=...)语法转换为新版语法 - # pandas 2.0+ 移除了 fillna() 的 method 参数,需要使用 ffill() 或 bfill() - # 旧语法: df.fillna(method='ffill') 或 df.fillna(method="ffill") - # 新语法: df.ffill() + # Compatibility fix: Convert the fillna(method=...) syntax of the old version of pandas to the new version of the syntax + # pandas 2.0+ removes the method parameter of fillna(), you need to use ffill() or bfill() + # Old syntax: df.fillna(method='ffill') or df.fillna(method="ffill") + # New syntax: df.ffill() import re compatibility_fixed_code = indicator_code - # 替换 fillna(method='ffill') 或 fillna(method="ffill") 为 ffill() + # Replace fillna(method='ffill') or fillna(method="ffill") with ffill() compatibility_fixed_code = re.sub( r'\.fillna\(\s*method\s*=\s*["\']ffill["\']\s*\)', '.ffill()', compatibility_fixed_code ) - # 替换 fillna(method='bfill') 或 fillna(method="bfill") 为 bfill() + # Replace fillna(method='bfill') or fillna(method="bfill") with bfill() compatibility_fixed_code = re.sub( r'\.fillna\(\s*method\s*=\s*["\']bfill["\']\s*\)', '.bfill()', compatibility_fixed_code ) - # 这里的 safe_exec_code 假设已存在 + # safe_exec_code here is assumed to already exist exec(compatibility_fixed_code, exec_env) executed_df = exec_env.get('df', df) @@ -1929,14 +1929,14 @@ class TradingExecutor: return None, {} def _execute_indicator(self, indicator_code: str, df: pd.DataFrame, trading_config: Dict[str, Any]) -> Optional[Any]: - """兼容旧版本""" + """Compatible with older versions""" executed_df, _ = self._execute_indicator_df(indicator_code, df, trading_config) if executed_df is None: return None return 0 def _get_current_positions(self, strategy_id: int, symbol: str) -> List[Dict[str, Any]]: - """获取当前持仓(支持symbol规范化匹配)""" + """Get the current position (supports symbol normalization matching)""" try: with get_db_connection() as db: cursor = db.cursor() @@ -1950,7 +1950,7 @@ class TradingExecutor: matched_positions = [] for pos in all_positions: - # 简化匹配逻辑:只匹配前缀 + # Simplify matching logic: only match prefixes if pos['symbol'].split(':')[0] == symbol.split(':')[0]: matched_positions.append(pos) @@ -1988,20 +1988,20 @@ class TradingExecutor: ai_model_config: Optional[Dict[str, Any]] = None, signal_ts: int = 0, ): - """执行具体的交易信号""" + """Execute specific trading signals""" try: # Hard state-machine guard (double safety in addition to loop-level filtering). state = self._position_state(current_positions) if not self._is_signal_allowed(state, signal_type): return False - # 1. 检查交易方向限制 + # 1. Check trading direction restrictions if market_type == 'spot' and 'short' in signal_type: return False sig = (signal_type or "").strip().lower() - # 1.1 开仓 AI 过滤(仅 open_*) + # 1.1 Open position AI filtering (only open_*) if sig in ("open_long", "open_short") and self._is_entry_ai_filter_enabled(ai_model_config=ai_model_config, trading_config=trading_config): ok_ai, ai_info = self._entry_ai_filter_allows( strategy_id=strategy_id, @@ -2038,7 +2038,7 @@ class TradingExecutor: ) return False - # 2. 计算下单数量 + # 2. Calculate order quantity available_capital = self._get_available_capital( strategy_id, initial_capital, @@ -2087,12 +2087,12 @@ class TradingExecutor: else: amount = reduce_amount - # 3. 检查反向持仓(单向持仓逻辑) - # ... (简化处理,假设无反向或由用户处理) ... + # 3. Check reverse positions (one-way position logic) + # ... (Simplified processing, assuming no reverse or processing by the user) ... # 4. Execute order enqueue (PendingOrderWorker will dispatch notifications in signal mode) if 'close' in sig: - # 平仓逻辑:找到对应持仓大小 + # Position closing logic: find the corresponding position size pos = next((p for p in current_positions if p.get('side') and p['side'] in signal_type), None) if not pos: return False @@ -2124,7 +2124,7 @@ class TradingExecutor: if str(execution_mode or "").strip().lower() == "live": return True - # 更新数据库状态 (signal mode / local simulation) + # Update database status (signal mode / local simulation) if 'open' in sig or 'add' in sig: self._record_trade( strategy_id=strategy_id, symbol=symbol, type=signal_type, @@ -2132,7 +2132,7 @@ class TradingExecutor: ) side = 'short' if 'short' in signal_type else 'long' - # 查找现有持仓以计算均价 + # Find existing positions to calculate average price old_pos = next((p for p in current_positions if p['side'] == side), None) new_size = amount new_entry = current_price @@ -2148,7 +2148,7 @@ class TradingExecutor: ) elif sig.startswith("reduce_"): # Partial scale-out: reduce position size, keep entry price unchanged. - # 信号模式下计算部分平仓盈亏 + # Calculate partial closing profit and loss in signal mode side = 'short' if 'short' in signal_type else 'long' old_pos = next((p for p in current_positions if p.get('side') == side), None) if not old_pos: @@ -2156,7 +2156,7 @@ class TradingExecutor: old_size = float(old_pos.get('size') or 0.0) old_entry = float(old_pos.get('entry_price') or 0.0) - # 计算减仓部分的盈亏(信号模式下,不含手续费) + # Calculate the profit and loss of the position reduction part (in signal mode, excluding handling fees) reduce_profit = None if old_entry > 0 and amount > 0: if side == 'long': @@ -2179,11 +2179,11 @@ class TradingExecutor: size=new_size, entry_price=old_entry, current_price=current_price ) elif 'close' in sig: - # 信号模式下计算平仓盈亏 + # Calculate closing profit and loss in signal mode side = 'short' if 'short' in signal_type else 'long' old_pos = next((p for p in current_positions if p.get('side') == side), None) - # 计算盈亏(信号模式下,不含手续费) + # Calculate profit and loss (in signal mode, excluding handling fees) close_profit = None if old_pos: entry_price = float(old_pos.get('entry_price') or 0) @@ -2304,7 +2304,7 @@ class TradingExecutor: if isinstance(result, dict) and result.get("error"): return False, {"ai_decision": "", "reason": "analysis_error", "analysis_error": str(result.get("error") or "")} - # FastAnalysisService 直接返回 decision 字段 + # FastAnalysisService directly returns the decision field ai_dec = str(result.get("decision", "")).strip().upper() if not ai_dec or ai_dec not in ("BUY", "SELL", "HOLD"): return False, {"ai_decision": ai_dec, "reason": "missing_ai_decision"} @@ -2669,7 +2669,7 @@ class TradingExecutor: current_price: Optional[float] = None, symbol: str = "", ) -> float: - """获取当前策略可用于仓位计算的净值口径资金。""" + """Get the equity capital that the current strategy can use for position calculation.""" return self._calculate_current_equity( strategy_id, initial_capital, @@ -2740,7 +2740,7 @@ class TradingExecutor: return max(0.0, equity) def _record_trade(self, strategy_id: int, symbol: str, type: str, price: float, amount: float, value: float, profit: float = None, commission: float = None): - """记录交易到数据库""" + """Record transactions to database""" try: # Get user_id from strategy user_id = 1 @@ -2788,7 +2788,7 @@ class TradingExecutor: user_id = int((row or {}).get('user_id') or 1) except Exception: pass - # 简化:直接 Update 或 Insert + # Simplification: direct Update or Insert upsert_query = """ INSERT INTO qd_strategy_positions ( user_id, strategy_id, symbol, side, size, entry_price, current_price, highest_price, lowest_price, updated_at @@ -2825,7 +2825,7 @@ class TradingExecutor: pass def _update_positions(self, strategy_id: int, symbol: str, current_price: float): - """更新所有持仓的当前价格""" + """Update current prices for all positions""" try: with get_db_connection() as db: cursor = db.cursor() @@ -2846,7 +2846,7 @@ class TradingExecutor: return None def _get_all_positions(self, strategy_id: int) -> List[Dict[str, Any]]: - """获取策略的所有持仓(截面策略使用)""" + """Get all positions of the strategy (used by cross-section strategy)""" try: with get_db_connection() as db: cursor = db.cursor() @@ -2923,7 +2923,7 @@ class TradingExecutor: 执行截面策略指标,返回所有标的的评分和排序 """ try: - # 获取所有标的的K线数据 + # Get K-line data of all targets all_data = {} for symbol in symbols: try: @@ -2940,19 +2940,19 @@ class TradingExecutor: logger.error("No data available for cross-sectional strategy") return None - # 准备执行环境 + # Prepare execution environment exec_env = { 'symbols': list(all_data.keys()), 'data': all_data, # {symbol: df} - 'scores': {}, # 用于存储评分 - 'rankings': [], # 用于存储排序 + 'scores': {}, # used to store scores + 'rankings': [], # used to store rankings 'np': np, 'pd': pd, 'trading_config': trading_config, 'config': trading_config, } - # 执行指标代码 + # Execution indicator code import builtins safe_builtins = {k: getattr(builtins, k) for k in dir(builtins) if not k.startswith('_') and k not in [ @@ -2968,7 +2968,7 @@ class TradingExecutor: scores = exec_env.get('scores', {}) rankings = exec_env.get('rankings', []) - # 如果没有提供rankings,根据scores排序 + # If rankings are not provided, sort according to scores if not rankings and scores: rankings = sorted(scores.keys(), key=lambda x: scores.get(x, 0), reverse=True) @@ -2994,26 +2994,26 @@ class TradingExecutor: portfolio_size = trading_config.get('portfolio_size', 10) long_ratio = float(trading_config.get('long_ratio', 0.5)) - # 选择持仓标的 + # Select the position target long_count = int(portfolio_size * long_ratio) short_count = portfolio_size - long_count long_symbols = set(rankings[:long_count]) if long_count > 0 else set() short_symbols = set(rankings[-short_count:]) if short_count > 0 and len(rankings) >= short_count else set() - # 获取当前持仓 + # Get the current position current_positions = self._get_all_positions(strategy_id) current_long = {p['symbol'] for p in current_positions if p.get('side') == 'long'} current_short = {p['symbol'] for p in current_positions if p.get('side') == 'short'} signals = [] - # 生成做多信号 + # Generate long signal for symbol in long_symbols: if symbol not in current_long: - # 如果当前没有多仓,开多 + # If there is currently no long position, open a long position if symbol in current_short: - # 如果当前是空仓,先平空再开多 + # If the current position is a short position, close the short position first and then open a long position signals.append({ 'symbol': symbol, 'type': 'close_short', @@ -3025,7 +3025,7 @@ class TradingExecutor: 'score': scores.get(symbol, 0) }) - # 平掉不在做多列表中的多仓 + # Close long positions that are not in the long list for symbol in current_long: if symbol not in long_symbols: signals.append({ @@ -3034,12 +3034,12 @@ class TradingExecutor: 'score': scores.get(symbol, 0) }) - # 生成做空信号 + # Generate short signal for symbol in short_symbols: if symbol not in current_short: - # 如果当前没有空仓,开空 + # If there is currently no short position, open a short position if symbol in current_long: - # 如果当前是多仓,先平多再开空 + # If you are currently in a long position, close the long position first and then open a short position. signals.append({ 'symbol': symbol, 'type': 'close_long', @@ -3051,7 +3051,7 @@ class TradingExecutor: 'score': scores.get(symbol, 0) }) - # 平掉不在做空列表中的空仓 + # Close short positions that are not in the short list for symbol in current_short: if symbol not in short_symbols: signals.append({ @@ -3098,7 +3098,7 @@ class TradingExecutor: while True: try: - # 检查策略状态 + # Check policy status if not self._is_strategy_running(strategy_id): logger.info(f"Cross-sectional strategy {strategy_id} stopped") break @@ -3113,13 +3113,13 @@ class TradingExecutor: continue last_tick_time = current_time - # 检查是否需要调仓 + # Check whether position adjustment is needed if not self._should_rebalance(strategy_id, rebalance_frequency): continue logger.info(f"Cross-sectional strategy {strategy_id} rebalancing...") - # 执行截面指标 + #Execution cross-section indicators result = self._execute_cross_sectional_indicator( indicator_code, symbol_list, trading_config, market_category, timeframe ) @@ -3128,7 +3128,7 @@ class TradingExecutor: logger.warning(f"Cross-sectional indicator returned no result") continue - # 生成信号 + # Generate signal signals = self._generate_cross_sectional_signals( strategy_id, result['rankings'], result['scores'], trading_config ) @@ -3140,7 +3140,7 @@ class TradingExecutor: logger.info(f"Generated {len(signals)} signals for cross-sectional strategy {strategy_id}") - # 批量执行交易 + # Execute transactions in batches from concurrent.futures import ThreadPoolExecutor, as_completed with ThreadPoolExecutor(max_workers=min(10, len(signals))) as executor: futures = {} @@ -3171,7 +3171,7 @@ class TradingExecutor: ) futures[future] = signal - # 等待所有交易完成 + # Wait for all transactions to complete for future in as_completed(futures): signal = futures[future] try: @@ -3181,7 +3181,7 @@ class TradingExecutor: except Exception as e: logger.error(f"Failed to execute signal {signal['symbol']} {signal['type']}: {e}") - # 更新调仓时间 + # Update position rebalancing time self._update_last_rebalance(strategy_id) last_rebalance_time = current_time diff --git a/backend_api_python/app/services/usdt_payment_service.py b/backend_api_python/app/services/usdt_payment_service.py index ee80749..040bff3 100644 --- a/backend_api_python/app/services/usdt_payment_service.py +++ b/backend_api_python/app/services/usdt_payment_service.py @@ -1,10 +1,10 @@ """ -USDT Payment Service (方案B:每单独立地址 + 自动对账) +USDT Payment Service (Plan B: independent address for each order + automatic reconciliation) MVP: -- 只支持 USDT-TRC20 -- 使用 XPUB 派生地址(服务端只保存 xpub,不保存私钥) -- 后台 Worker 线程自动轮询链上到账 + 前端轮询双保险 +- Only supports USDT-TRC20 +- Use XPUB derived address (the server only saves xpub, not the private key) +- The background Worker thread automatically polls the on-chain account + front-end polling for double insurance """ import os diff --git a/backend_api_python/app/services/user_service.py b/backend_api_python/app/services/user_service.py index f818a59..7bd90ed 100644 --- a/backend_api_python/app/services/user_service.py +++ b/backend_api_python/app/services/user_service.py @@ -189,13 +189,13 @@ class UserService: def get_token_version(self, user_id: int) -> int: """ - 获取用户当前的 token 版本号。 + Get the user's current token version number. Args: - user_id: 用户ID + user_id: user ID Returns: - 当前 token 版本号,默认为 1 + Current token version number, default is 1 """ try: with get_db_connection() as db: @@ -215,19 +215,19 @@ class UserService: def increment_token_version(self, user_id: int) -> int: """ - 递增用户的 token 版本号,使旧的 token 失效。 - 用于实现单一客户端登录(踢出其他设备)。 + Increment the user's token version number and invalidate the old token. + Used to implement single client login (kick out other devices). Args: - user_id: 用户ID + user_id: user ID Returns: - 新的 token 版本号 + New token version number """ try: with get_db_connection() as db: cur = db.cursor() - # 递增 token_version + # Increment token_version cur.execute( """ UPDATE qd_users @@ -238,7 +238,7 @@ class UserService: ) db.commit() - # 获取新的 token_version + # Get new token_version cur.execute( "SELECT token_version FROM qd_users WHERE id = ?", (user_id,) diff --git a/backend_api_python/app/utils/__init__.py b/backend_api_python/app/utils/__init__.py index 2fc2761..3520690 100644 --- a/backend_api_python/app/utils/__init__.py +++ b/backend_api_python/app/utils/__init__.py @@ -1,5 +1,5 @@ """ -工具模块 +tool module """ from app.utils.logger import get_logger from app.utils.cache import CacheManager diff --git a/backend_api_python/app/utils/auth.py b/backend_api_python/app/utils/auth.py index 44c466a..dece1db 100644 --- a/backend_api_python/app/utils/auth.py +++ b/backend_api_python/app/utils/auth.py @@ -35,7 +35,7 @@ def generate_token(user_id: int, username: str, role: str = 'user', token_versio 'sub': username, 'user_id': user_id, 'role': role, - 'token_version': token_version, # 用于单一客户端登录控制 + 'token_version': token_version, # For single client login control } return jwt.encode( payload, @@ -60,12 +60,12 @@ def verify_token(token: str) -> dict: try: payload = jwt.decode(token, Config.SECRET_KEY, algorithms=['HS256']) - # 验证 token_version(单一客户端登录控制) + # Verify token_version (single client login control) user_id = payload.get('user_id') token_version = payload.get('token_version') if user_id and token_version is not None: - # 检查数据库中的 token_version 是否匹配 + # Check if token_version in database matches if not _verify_token_version(user_id, token_version): logger.debug(f"Token version mismatch for user {user_id}: expected current, got {token_version}") return None @@ -81,12 +81,12 @@ def verify_token(token: str) -> dict: def _verify_token_version(user_id: int, token_version: int) -> bool: """ - 验证 token 版本是否与数据库中存储的版本匹配。 - 用于实现单一客户端登录(踢出重复登录)。 + Verify that the token version matches the version stored in the database. + Used to implement single client login (kick out duplicate logins). Args: - user_id: 用户ID - token_version: Token中的版本号 + user_id: user ID + token_version: version number in Token Returns: True if version matches, False otherwise @@ -109,7 +109,7 @@ def _verify_token_version(user_id: int, token_version: int) -> bool: return int(token_version) == int(db_token_version) except Exception as e: logger.error(f"_verify_token_version failed: {e}") - # 如果验证失败,为了安全起见,返回 False + # If validation fails, for security reasons, return False return False diff --git a/backend_api_python/app/utils/cache.py b/backend_api_python/app/utils/cache.py index 2e03380..389f0a3 100644 --- a/backend_api_python/app/utils/cache.py +++ b/backend_api_python/app/utils/cache.py @@ -15,7 +15,7 @@ logger = get_logger(__name__) class MemoryCache: - """内存缓存(Redis 不可用时的备选方案)""" + """In-memory cache (an alternative if Redis is unavailable)""" def __init__(self): self._cache = {} @@ -47,7 +47,7 @@ class MemoryCache: class CacheManager: - """缓存管理器""" + """cache manager""" _instance = None _lock = threading.Lock() @@ -98,7 +98,7 @@ class CacheManager: self._use_redis = False def get(self, key: str) -> Optional[Any]: - """获取缓存""" + """Get cache""" try: data = self._client.get(key) if data: @@ -109,14 +109,14 @@ class CacheManager: return None def set(self, key: str, value: Any, ttl: int = 300): - """设置缓存""" + """Set up cache""" try: self._client.setex(key, ttl, json.dumps(value)) except Exception as e: logger.error(f"Cache write failed: {e}") def delete(self, key: str): - """删除缓存""" + """Delete cache""" try: self._client.delete(key) except Exception as e: diff --git a/backend_api_python/app/utils/config_loader.py b/backend_api_python/app/utils/config_loader.py index be33aed..a42319b 100644 --- a/backend_api_python/app/utils/config_loader.py +++ b/backend_api_python/app/utils/config_loader.py @@ -17,7 +17,7 @@ from app.utils.logger import get_logger logger = get_logger(__name__) -# 配置缓存 +# Configure cache _config_cache: Optional[Dict[str, Any]] = None @@ -32,7 +32,7 @@ def load_addon_config() -> Dict[str, Any]: """ global _config_cache - # 如果缓存存在,直接返回 + # If the cache exists, return directly if _config_cache is not None: return _config_cache @@ -147,16 +147,16 @@ def load_addon_config() -> Dict[str, Any]: def _convert_config_value(value: str, value_type: str) -> Any: """ - 根据类型转换配置值(与PHP端convertConfigValue方法保持一致) + Convert configuration values ​​according to type (consistent with the convertConfigValue method on the PHP side) Args: - value: 配置值字符串(可能为None) - value_type: 配置类型 + value: configuration value string (may be None) + value_type: configuration type Returns: - 转换后的配置值 + Converted configuration value """ - # 处理 None 或空值 + # Handling None or null values if value is None or value == '': if value_type == 'int': return 0 @@ -186,7 +186,7 @@ def _convert_config_value(value: str, value_type: str) -> Any: return str(value) if value is not None else '' except (ValueError, TypeError) as e: logger.warning(f"Config value type conversion failed: value={value}, type={value_type}, error={str(e)}") - # 转换失败时返回默认值 + # Returns default value if conversion fails if value_type == 'int': return 0 elif value_type == 'float': @@ -201,10 +201,10 @@ def _convert_config_value(value: str, value_type: str) -> Any: def get_internal_api_key() -> Optional[str]: """ - 获取内部API密钥(优先从环境变量读取) + Get the internal API key (preferably read from environment variables) Returns: - 内部API密钥,如果未配置则返回None + Internal API key, returns None if not configured """ try: env_val = os.getenv('INTERNAL_API_KEY', '').strip() @@ -227,7 +227,7 @@ def get_internal_api_key() -> Optional[str]: def clear_config_cache(): """ - 清除配置缓存(配置更新后调用) + Clear configuration cache (called after configuration update) """ global _config_cache _config_cache = None diff --git a/backend_api_python/app/utils/db_postgres.py b/backend_api_python/app/utils/db_postgres.py index 6c575c2..599b546 100644 --- a/backend_api_python/app/utils/db_postgres.py +++ b/backend_api_python/app/utils/db_postgres.py @@ -223,7 +223,7 @@ class PostgresConnection: def _ensure_session_utc(conn) -> None: - """每条连接检出后固定为 UTC,与 API 序列化约定一致。""" + """Each connection is fixed to UTC after checkout, consistent with the API serialization convention.""" try: cur = conn.cursor() cur.execute("SET TIME ZONE 'UTC'") @@ -250,7 +250,7 @@ def get_pg_connection(): conn.rollback() except Exception: pass - # 记录更详细的错误信息 + # Log more detailed error information error_msg = str(e) if e else repr(e) error_type = type(e).__name__ logger.error(f"PostgreSQL operation error ({error_type}): {error_msg}", exc_info=True) diff --git a/backend_api_python/app/utils/http.py b/backend_api_python/app/utils/http.py index df60f53..5f96d11 100644 --- a/backend_api_python/app/utils/http.py +++ b/backend_api_python/app/utils/http.py @@ -1,5 +1,5 @@ """ -HTTP 工具模块 +HTTP tool module """ import requests from requests.adapters import HTTPAdapter @@ -12,15 +12,15 @@ def get_retry_session( status_forcelist: tuple = (500, 502, 503, 504) ) -> requests.Session: """ - 获取带重试机制的 HTTP Session + Get HTTP Session with retry mechanism Args: - retries: 重试次数 - backoff_factor: 重试间隔因子 - status_forcelist: 需要重试的 HTTP 状态码 + retries: number of retries + backoff_factor: retry interval factor + status_forcelist: HTTP status codes that need to be retried Returns: - 配置好的 Session 实例 + Configured Session instance """ session = requests.Session() retry = Retry( @@ -36,6 +36,6 @@ def get_retry_session( return session -# 全局共享 Session +# Global shared session global_session = get_retry_session() diff --git a/backend_api_python/app/utils/logger.py b/backend_api_python/app/utils/logger.py index 638e19b..6d568de 100644 --- a/backend_api_python/app/utils/logger.py +++ b/backend_api_python/app/utils/logger.py @@ -7,7 +7,7 @@ from logging.handlers import RotatingFileHandler def setup_logger(): - """配置全局日志""" + """Configure global logs""" log_level = os.getenv('LOG_LEVEL', 'INFO') log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' @@ -16,22 +16,22 @@ def setup_logger(): format=log_format ) - # 过滤 werkzeug 的 INFO 级别日志(减少噪音) - # 只保留 WARNING 及以上级别 + # Filter werkzeug's INFO level logs (reduce noise) + # Only keep WARNING and above levels werkzeug_logger = logging.getLogger('werkzeug') werkzeug_logger.setLevel(logging.WARNING) - # 过滤 kline 路由的 INFO 级别日志(减少噪音) - # 只保留 WARNING 及以上级别 + # Filter INFO level logs for kline routes (reduce noise) + # Only keep WARNING and above levels kline_logger = logging.getLogger('app.routes.kline') kline_logger.setLevel(logging.WARNING) - # 创建日志目录 + # Create log directory log_dir = 'logs' if not os.path.exists(log_dir): os.makedirs(log_dir) - # 添加文件处理器 + # Add file handler file_handler = RotatingFileHandler( os.path.join(log_dir, 'app.log'), maxBytes=10*1024*1024, # 10MB @@ -44,13 +44,13 @@ def setup_logger(): def get_logger(name: str) -> logging.Logger: """ - 获取指定名称的日志记录器 + Get the logger with the specified name Args: - name: 日志记录器名称 + name: logger name Returns: - Logger 实例 + Logger instance """ return logging.getLogger(name) diff --git a/backend_api_python/app/utils/safe_exec.py b/backend_api_python/app/utils/safe_exec.py index 3410161..bafed10 100644 --- a/backend_api_python/app/utils/safe_exec.py +++ b/backend_api_python/app/utils/safe_exec.py @@ -1,6 +1,6 @@ """ -安全的代码执行工具 -提供超时、资源限制和沙箱环境 +Secure code execution tools +Provides timeouts, resource limits and a sandbox environment """ import signal import sys @@ -16,36 +16,36 @@ logger = get_logger(__name__) class TimeoutError(Exception): - """代码执行超时异常""" + """Code execution timeout exception""" pass @contextmanager def timeout_context(seconds: int): """ - 代码执行超时上下文管理器 + Code execution timeout context manager - 注意: - - 仅在Unix/Linux系统上有效 - - 仅在主线程中有效,非主线程会降级为不限制超时 - - Windows上会降级为不限制超时 + Notice: + - Only works on Unix/Linux systems + - Only valid in the main thread, non-main threads will be downgraded to unlimited timeout + - Will downgrade to unlimited timeout on Windows Args: - seconds: 超时时间(秒) + seconds: timeout (seconds) """ - # 检查是否在主线程中 + # Check if in main thread is_main_thread = threading.current_thread() is threading.main_thread() if sys.platform == 'win32': - # Windows不支持signal.alarm,只能记录警告 + # signal.alarm is not supported on Windows, only warnings can be logged logger.warning("Windows does not support signal-based timeouts; execution time limits may not work") yield return if not is_main_thread: - # 非主线程不能使用signal,记录警告但不限制超时 - # logger.warning(f"当前在非主线程中运行(线程: {threading.current_thread().name})," - # f"signal超时不可用,代码执行可能无法限制时间") + # Non-main threads cannot use signal. Warnings are logged but timeouts are not limited. + # logger.warning(f"Currently running in a non-main thread (thread: {threading.current_thread().name})," + # f"signal timeout is not available, code execution may not limit the time") yield return @@ -53,18 +53,18 @@ def timeout_context(seconds: int): raise TimeoutError(f"代码执行超时(超过{seconds}秒)") try: - # 设置信号处理器 + # Set up signal handler old_handler = signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(seconds) try: yield finally: - # 恢复原来的信号处理器 + # Restore original signal handler signal.alarm(0) signal.signal(signal.SIGALRM, old_handler) except ValueError as e: - # 如果signal设置失败(比如在某些环境中),记录警告但不中断执行 + # If signal setup fails (such as in some environments), log a warning but do not interrupt execution logger.warning(f"Failed to set signal timeout: {str(e)}; execution will continue without timeout enforcement") yield @@ -77,35 +77,35 @@ def safe_exec_code( max_memory_mb: Optional[int] = None ) -> Dict[str, Any]: """ - 安全执行Python代码 + Safe execution of Python code Args: - code: 要执行的Python代码 - exec_globals: 全局变量字典 - exec_locals: 局部变量字典(如果为None,则使用exec_globals) - timeout: 超时时间(秒),默认30秒 - max_memory_mb: 最大内存限制(MB),默认500MB + code: Python code to execute + exec_globals: dictionary of global variables + exec_locals: dictionary of local variables (if None, exec_globals is used) + timeout: timeout (seconds), default 30 seconds + max_memory_mb: Maximum memory limit (MB), default 500MB Returns: - 执行结果字典,包含: - - success: bool,是否执行成功 - - error: str,错误信息(如果失败) - - result: Any,执行结果(如果有) + Execution result dictionary, including: + - success: bool, whether the execution was successful + - error: str, error message (if failure) + - result: Any, execution result (if any) Raises: - TimeoutError: 如果代码执行超时 + TimeoutError: If the code execution times out """ if exec_locals is None: exec_locals = exec_globals - # 设置内存限制(如果支持) + # Set memory limit (if supported) if max_memory_mb is None: - max_memory_mb = 500 # 默认500MB + max_memory_mb = 500 # Default 500MB try: - # 注意:resource.setrlimit 是进程级别,会影响整个 API 进程。 - # 之前全局限制为 500MB 可能导致并行策略/线程无法分配内存。 - # 仅当显式开启 SAFE_EXEC_ENABLE_RLIMIT 时才设置。 + # Note: resource.setrlimit is process level and affects the entire API process. + # The previous global limit of 500MB could prevent parallel strategies/threads from allocating memory. + # Only set if SAFE_EXEC_ENABLE_RLIMIT is explicitly turned on. if sys.platform != 'win32' and os.getenv('SAFE_EXEC_ENABLE_RLIMIT', 'false').lower() == 'true': try: import resource @@ -117,8 +117,8 @@ def safe_exec_code( else: logger.debug("No resource memory limit (SAFE_EXEC_ENABLE_RLIMIT disabled or unsupported platform)") - # 在Windows上,timeout_context不会真正限制时间 - # 但会记录警告 + # On Windows, timeout_context doesn't really limit the time + # But a warning will be logged with timeout_context(timeout): exec(code, exec_globals, exec_locals) @@ -157,12 +157,12 @@ def safe_exec_code( def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: """ - 验证代码安全性(基本检查) + Verify code security (basic check) - 检查代码中是否包含危险的函数调用或导入 + Check your code for dangerous function calls or imports Args: - code: 要检查的Python代码 + code: Python code to check Returns: (is_safe: bool, error_message: Optional[str]) @@ -170,9 +170,9 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: import ast import re - # 危险的关键字和函数名 + # Dangerous keywords and function names dangerous_patterns = [ - # 系统命令执行 + # System command execution r'\bos\.system\b', r'\bos\.popen\b', r'\bos\.spawn\b', @@ -180,16 +180,16 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: r'\bos\.fork\b', r'\bsubprocess\b', r'\bcommands\b', - # 代码执行 + # code execution r'\b__import__\s*\(', r'\beval\s*\(', r'\bexec\s*\(', r'\bcompile\s*\(', - # 文件操作 + # File operations r'\bopen\s*\(', r'\bfile\s*\(', r'\b__builtins__\b', - # 模块导入 + # module import r'\bimport\s+os\b', r'\bimport\s+sys\b', r'\bimport\s+subprocess\b', @@ -208,7 +208,7 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: r'\bimport\s+multiprocessing\b', r'\bimport\s+threading\b', r'\bimport\s+concurrent\b', - # 反射和元编程(可能用于绕过限制) + # Reflection and metaprogramming (possibly used to bypass restrictions) r'\bgetattr\s*\(.*__import__', r'\bgetattr\s*\(.*eval', r'\bgetattr\s*\(.*exec', @@ -219,14 +219,14 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: r'\bglobals\s*\(', r'\blocals\s*\(', r'\bdir\s*\(', - r'\btype\s*\(.*\)\s*\(', # type() 可能用于创建新类型 + r'\btype\s*\(.*\)\s*\(', # type() may be used to create new types r'\b__class__\b', r'\b__bases__\b', r'\b__subclasses__\b', r'\b__mro__\b', r'\b__init__\b.*__import__', r'\b__new__\b.*__import__', - # 其他危险操作 + # Other dangerous operations r'\b__builtins__\s*\[', r'\b__builtins__\s*\.', r'\b__import__\s*\(', @@ -234,16 +234,16 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: r'\bimp\b', ] - # 检查代码中是否包含危险模式 + # Check your code for dangerous patterns for pattern in dangerous_patterns: if re.search(pattern, code): return False, f"检测到危险代码模式: {pattern}" - # 尝试解析AST,检查是否有危险的节点 + # Try parsing the AST, checking for dangerous nodes try: tree = ast.parse(code) - # 危险模块列表(扩展) + # List of dangerous modules (extended) dangerous_modules = [ 'os', 'sys', 'subprocess', 'pymysql', 'sqlite3', 'requests', 'urllib', 'http', 'socket', 'ftplib', 'telnetlib', @@ -252,38 +252,38 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: 'importlib', 'imp', 'builtins' ] - # 危险函数列表(扩展) - # 注意:hasattr 是安全的,只用于检查属性,不用于访问 + # List of dangerous functions (extended) + # Note: hasattr is safe and is only used to check attributes, not to access dangerous_functions = [ 'eval', 'exec', 'compile', '__import__', - 'getattr', 'setattr', 'delattr', # hasattr 已移除,它是安全的 + 'getattr', 'setattr', 'delattr', # hasattr has been removed, it is safe 'globals', 'locals', 'vars', 'dir', 'type' ] - # 检查是否有危险的函数调用 + # Check for dangerous function calls for node in ast.walk(tree): - # 检查是否有对危险函数的调用 + # Check for calls to dangerous functions if isinstance(node, ast.Call): if isinstance(node.func, ast.Name): func_name = node.func.id if func_name in dangerous_functions: return False, f"检测到危险函数调用: {func_name}()" - # 检查是否有os.system等调用 + # Check if there are calls to os.system etc. if isinstance(node.func, ast.Attribute): if isinstance(node.func.value, ast.Name): if node.func.value.id in dangerous_modules: return False, f"检测到危险模块调用: {node.func.value.id}.{node.func.attr}" - # 检查是否有 getattr(builtins, '__import__') 等绕过方式 + # Check if there are bypass methods such as getattr(builtins, '__import__') if isinstance(node.func, ast.Name) and node.func.id == 'getattr': - # 检查 getattr 的参数 + # Check the parameters of getattr if len(node.args) >= 2: if isinstance(node.args[0], ast.Name) and node.args[0].id in ['builtins', '__builtins__']: if isinstance(node.args[1], ast.Constant) and node.args[1].value in dangerous_functions: return False, f"检测到通过 getattr 绕过限制: getattr({node.args[0].id}, '{node.args[1].value}')" - # 检查导入语句:用户脚本中一律禁止使用 import(统一由平台注入安全依赖) + # Check the import statement: the use of import is prohibited in user scripts (security dependencies are uniformly injected by the platform) for node in ast.walk(tree): if isinstance(node, ast.Import): return False, "不允许在脚本中使用 import 语句,请直接使用平台提供的 pd/np 等对象" @@ -291,19 +291,19 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: if isinstance(node, ast.ImportFrom): return False, "不允许在脚本中使用 import 语句,请直接使用平台提供的 pd/np 等对象" - # 检查是否有访问 __builtins__ 的尝试 + # Check if there is an attempt to access __builtins__ for node in ast.walk(tree): if isinstance(node, ast.Attribute): if isinstance(node.attr, str) and node.attr.startswith('__') and node.attr.endswith('__'): if node.attr in ['__builtins__', '__import__', '__class__', '__bases__', '__subclasses__', '__mro__']: - # 检查是否在危险上下文中使用 + # Check if used in dangerous context if isinstance(node.value, ast.Name) and node.value.id in ['builtins', '__builtins__']: return False, f"检测到访问危险属性: {node.value.id}.{node.attr}" except SyntaxError as e: return False, f"代码语法错误: {str(e)}" except Exception as e: - # 如果AST解析失败,记录警告但允许继续(可能是代码不完整) + # If AST parsing fails, log a warning but allow to continue (possibly incomplete code) logger.warning(f"AST parse failed; skipping safety checks: {str(e)}") return True, None diff --git a/backend_api_python/gunicorn_config.py b/backend_api_python/gunicorn_config.py index 2d3ecc7..1ec4194 100644 --- a/backend_api_python/gunicorn_config.py +++ b/backend_api_python/gunicorn_config.py @@ -1,29 +1,29 @@ """ -Gunicorn 配置文件(生产环境) +Gunicorn configuration file (production environment) """ import multiprocessing -# 服务器 socket +# server socket bind = "0.0.0.0:5000" backlog = 2048 -# Worker 进程 +# Worker process workers = multiprocessing.cpu_count() * 2 + 1 worker_class = "sync" worker_connections = 1000 timeout = 600 # 10 minutes for long-running backtests keepalive = 5 -# 日志 +# log accesslog = "logs/access.log" errorlog = "logs/error.log" loglevel = "info" access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' -# 进程命名 +# Process naming proc_name = "quantdinger_python_api" -# 服务器机制 +# Server mechanism daemon = False pidfile = "logs/gunicorn.pid" umask = 0 @@ -31,7 +31,7 @@ user = None group = None tmp_upload_dir = None -# SSL(如果需要) +# SSL (if required) # keyfile = None # certfile = None diff --git a/backend_api_python/run.py b/backend_api_python/run.py index 22b3c60..c69125a 100644 --- a/backend_api_python/run.py +++ b/backend_api_python/run.py @@ -69,7 +69,7 @@ app = create_app() def main(): - """启动应用""" + """Start application""" # Keep startup messages ASCII-only and short. print("QuantDinger Python API v2.2.2") diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6b0c29a..1fe6b26 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,9 +6,9 @@ This document records version updates, new features, bug fixes, and database mig ## V3.0.1 (2026-04-05) — Frontend / docs -- **前端版本**:`QuantDinger-Vue-src/package.json`、页脚展示与 `frontend/VERSION` 统一为 **3.0.1**。 -- **文档**:根目录 `README.md` 与 `docs/README_CN.md` 补充 QuantDinger 专属交易所邀请注册链接表(与个人中心「开户」一致),版本徽章更新为 3.0.1。 -- **回测中心**:暗黑主题下图标与「添加标的」等弹窗样式对齐(`a-icon`、图表标题区、Modal 挂载层)。 +- **Front-end version**: `QuantDinger-Vue-src/package.json`, footer display and `frontend/VERSION` are unified to **3.0.1**. +- **Documentation**: The root directories `README.md` and `docs/README_CN.md` are added to the QuantDinger exclusive exchange invitation registration link table (consistent with the personal center "Account Opening"), and the version badge is updated to 3.0.1. +- **Backtest Center**: Under the dark theme, icons are aligned with pop-up window styles such as "Add Target" (`a-icon`, chart title area, Modal mounting layer). --- @@ -16,25 +16,25 @@ This document records version updates, new features, bug fixes, and database mig ### 🚀 New Features -- **真实策略回测主链路**: 新增基于 `strategyId` 的策略回测入口,支持已保存的 `IndicatorStrategy` 与 `ScriptStrategy`,不再只是“取指标再跑一次指标回测”。 -- **策略快照解析层**: 后端新增统一策略快照解析逻辑,把 `indicator_config`、`trading_config`、`strategy_code` 解析为可回测的标准输入。 -- **策略回测历史与详情**: 回测记录现在可区分 `indicator` / `strategy_indicator` / `strategy_script`,并支持策略回测历史、详情查看和 AI 修正建议链路。 -- **交易助手联动回测中心**: 交易助手中的策略项新增回测跳转入口,可直接带 `strategy_id` 进入回测中心。 +- **Real strategy backtest main link**: Added new strategy backtest entrance based on `strategyId`, supporting saved `IndicatorStrategy` and `ScriptStrategy`, no longer just "get the indicator and run another indicator backtest". +- **Strategy Snapshot Parsing Layer**: New unified strategy snapshot parsing logic is added to the backend to parse `indicator_config`, `trading_config`, and `strategy_code` into backtestable standard input. +- **Strategy backtest history and details**: Backtest records can now distinguish `indicator` / `strategy_indicator` / `strategy_script`, and support strategy backtest history, detailed viewing and AI correction suggestion links. +- **Trading Assistant Linked Backtesting Center**: A new backtesting jump entrance is added to the strategy items in the Trading Assistant, and you can directly enter the backtesting center with `strategy_id`. ### 🐛 Bug Fixes -- Fixed the previous “策略回测” pseudo-flow that only reused `/api/indicator/backtest` and could not faithfully replay stored strategies. +- Fixed the previous “strategy backtest” pseudo-flow that only reused `/api/indicator/backtest` and could not faithfully replay stored strategies. - Fixed strategy backtest history semantics so records can be linked to concrete strategies instead of only relying on `indicator_id`. - Fixed strategy backtest UI entry restoration in Backtest Center and wired the strategy selector/history drawer to real backend endpoints. ### 🎨 UI/UX Improvements -- Restored the `回测中心 -> 策略回测` tab with strategy summary cards and environment override controls. +- Restored the `Backtest Center -> Strategy Backtest` tab with strategy summary cards and environment override controls. - Unified strategy backtest history display with the existing run viewer and AI suggestion modal. ### 📋 Database Migration -**在已有 PostgreSQL 库上执行(新库若已通过更新后的 `migrations/init.sql` 初始化则无需再执行):** +**Execute on the existing PostgreSQL library (if the new library has been initialized through the updated `migrations/init.sql`, there is no need to execute it again): ** ```sql -- ============================================================ @@ -97,15 +97,15 @@ CREATE INDEX IF NOT EXISTS idx_backtest_equity_points_run_id ON qd_backtest_equi ### 🚀 New Features -- **User profile IANA timezone (`qd_users.timezone`)**: 个人资料可保存时区(IANA 标识,如 `Asia/Shanghai`);为空表示跟随浏览器。登录态 `/api/auth/info`、资料接口与前端 AI 分析页等时间展示会按该时区调用 `toLocaleString(..., { timeZone })`(非法或空则回退本机时区)。 +- **User profile IANA timezone (`qd_users.timezone`)**: Profile can save time zone (IANA identifier, such as `Asia/Shanghai`); empty means following the browser. Login state `/api/auth/info`, data interface and front-end AI analysis page and other time display will call `toLocaleString(..., { timeZone })` according to the time zone (if illegal or empty, it will fall back to the local time zone). ### 📋 Database Migration -**在已有 PostgreSQL 库上执行(新库若已通过更新后的 `migrations/init.sql` 初始化则无需再执行):** +**Execute on the existing PostgreSQL library (if the new library has been initialized through the updated `migrations/init.sql`, there is no need to execute it again): ** ```sql -- ============================================================ --- QuantDinger V2.2.3 — qd_users.timezone(用户资料时区) +-- QuantDinger V2.2.3 — `qd_users.timezone` (user profile timezone) -- ============================================================ DO $$ @@ -122,13 +122,13 @@ BEGIN END $$; ``` -**仅当列不存在时的一行式写法(自行确认无列后再执行):** +**One-line writing method only when the column does not exist (confirm that there is no column before executing): ** ```sql ALTER TABLE qd_users ADD COLUMN IF NOT EXISTS timezone VARCHAR(64) DEFAULT ''; ``` -> 说明:`ALTER TABLE ... ADD COLUMN IF NOT EXISTS` 需 **PostgreSQL 11+**(本仓库 Docker 默认 `postgres:16` 可用);与上面 `DO` 块二选一即可。 +> Note: `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` requires **PostgreSQL 11+** (the default `postgres:16` is available for Docker in this warehouse); just choose one of the two above `DO` blocks. --- @@ -167,24 +167,24 @@ ALTER TABLE qd_users ADD COLUMN IF NOT EXISTS timezone VARCHAR(64) DEFAULT ''; -- Polymarket Prediction Markets Integration -- ============================================================ --- 预测市场表(缓存) +-- Prediction market table (cache) CREATE TABLE IF NOT EXISTS qd_polymarket_markets ( id SERIAL PRIMARY KEY, market_id VARCHAR(255) UNIQUE NOT NULL, question TEXT, category VARCHAR(100), -- crypto, politics, economics, sports - current_probability DECIMAL(5,2), -- YES概率(0-100) + current_probability DECIMAL(5,2), -- YES probability (0-100) volume_24h DECIMAL(20,2), liquidity DECIMAL(20,2), end_date_iso TIMESTAMP, status VARCHAR(50), -- active, closed, resolved - outcome_tokens JSONB, -- YES/NO价格和交易量 - slug VARCHAR(255), -- Polymarket事件slug,用于构建URL + outcome_tokens JSONB, -- YES/NO price and volume + slug VARCHAR(255), -- Polymarket event slug used to build the URL created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); --- 添加slug字段(如果表已存在但字段不存在) +-- Add the slug field if the table exists but the column does not DO $$ BEGIN IF NOT EXISTS ( @@ -200,20 +200,20 @@ CREATE INDEX IF NOT EXISTS idx_polymarket_category ON qd_polymarket_markets(cate CREATE INDEX IF NOT EXISTS idx_polymarket_status ON qd_polymarket_markets(status); CREATE INDEX IF NOT EXISTS idx_polymarket_updated ON qd_polymarket_markets(updated_at DESC); --- AI分析记录表 +-- AI analysis records table CREATE TABLE IF NOT EXISTS qd_polymarket_ai_analysis ( id SERIAL PRIMARY KEY, market_id VARCHAR(255) NOT NULL, - user_id INTEGER, -- 可选:用户特定的分析 + user_id INTEGER, -- Optional: user-specific analysis ai_predicted_probability DECIMAL(5,2), market_probability DECIMAL(5,2), - divergence DECIMAL(5,2), -- AI - 市场 + divergence DECIMAL(5,2), -- AI minus market recommendation VARCHAR(20), -- YES/NO/HOLD confidence_score DECIMAL(5,2), opportunity_score DECIMAL(5,2), reasoning TEXT, key_factors JSONB, - related_assets TEXT[], -- 相关资产列表 + related_assets TEXT[], -- Related asset list created_at TIMESTAMP DEFAULT NOW() ); @@ -221,7 +221,7 @@ CREATE INDEX IF NOT EXISTS idx_polymarket_analysis_market ON qd_polymarket_ai_an CREATE INDEX IF NOT EXISTS idx_polymarket_analysis_opportunity ON qd_polymarket_ai_analysis(opportunity_score DESC); CREATE INDEX IF NOT EXISTS idx_polymarket_analysis_user ON qd_polymarket_ai_analysis(user_id); --- 资产交易机会表(基于预测市场生成) +-- Asset trading opportunities table (generated from prediction markets) CREATE TABLE IF NOT EXISTS qd_polymarket_asset_opportunities ( id SERIAL PRIMARY KEY, market_id VARCHAR(255) NOT NULL, @@ -230,7 +230,7 @@ CREATE TABLE IF NOT EXISTS qd_polymarket_asset_opportunities ( signal VARCHAR(20), -- BUY/SELL/HOLD confidence DECIMAL(5,2), reasoning TEXT, - entry_suggestion JSONB, -- 入场建议 + entry_suggestion JSONB, -- Entry suggestion created_at TIMESTAMP DEFAULT NOW() ); @@ -295,7 +295,7 @@ END $$; - **Market Order Default**: Changed default order mode to market order for reliable execution - **Billing Config i18n**: All billing configuration items fully multi-language supported -#### Quick Trade Panel (闪电交易) 🆕 +#### Quick Trade Panel (Lightning Trading) 🆕 - **Side-Sliding Drawer**: Professional trading panel slides in from the right, allowing instant order placement without leaving the analysis page - **Multi-Exchange Support**: Select from saved exchange credentials (Binance, OKX, Bitget, Bybit, etc.) with real-time balance display - **Long/Short Toggle**: Color-coded direction buttons with one-click switching diff --git a/docs/FRONTEND_FAST_ANALYSIS.md b/docs/FRONTEND_FAST_ANALYSIS.md index 163e879..865d747 100644 --- a/docs/FRONTEND_FAST_ANALYSIS.md +++ b/docs/FRONTEND_FAST_ANALYSIS.md @@ -1,62 +1,62 @@ -# 快速分析(Fast Analysis)前端对接说明 +# Fast Analysis Frontend Integration Notes -本开源仓库中的 **`frontend/` 仅包含构建产物 `dist/`**,Vue 源码在**私有前端仓库**(见根目录 `.github/workflows/update-frontend.yml`)。因此在本仓库内**无法直接修改** `FastAnalysisReport.vue` 等组件,需要在私有仓库中按下列说明调整。 +**`frontend/` in this open source warehouse only contains the build product `dist/`**, and the Vue source code is in the **private frontend warehouse** (see the root directory `.github/workflows/update-frontend.yml`). Therefore, components such as `FastAnalysisReport.vue` cannot be modified directly in this warehouse. They need to be adjusted in the private warehouse according to the following instructions. -## 1. 止盈 / 止损显示反了(BUY/SELL) +## 1. Take profit/stop loss display is reversed (BUY/SELL) -### 后端约定(`/api/fast-analysis/analyze`) +### Backend convention (`/api/fast-analysis/analyze`) -- **`trading_plan.stop_loss` / `trading_plan.take_profit`** 已与 `decision` 对齐几何关系: - - **BUY**:`stop_loss < 现价 < take_profit` - - **SELL(空)**:`take_profit < 现价 < stop_loss`(止损在上方,止盈在下方) -- **`indicators.trading_levels.suggested_stop_loss / suggested_take_profit`** 在采集器里是**多单(做多)参考价**,**不能**在 SELL 时直接拿来当界面上的「止损/止盈」两行,否则会和空单几何相反。 +- **`trading_plan.stop_loss` / `trading_plan.take_profit`** Already aligned with `decision`: + - **BUY**: `stop_loss < current price < take_profit` + - **SELL (empty)**: `take_profit < current price < stop_loss` (stop loss is above, take profit is below) +- **`indicators.trading_levels.suggested_stop_loss / suggested_take_profit`** is the **reference price for long orders** in the collector. It cannot be used directly as the "stop loss/take profit" lines on the interface during SELL, otherwise it will be geometrically opposite to the short order. -### 前端常见错误 +### Common front-end errors -- 第一行写死绑定 `trading_levels.suggested_stop_loss`、第二行绑定 `suggested_take_profit`。 -- 或把 `stop_loss` / `take_profit` **标签写反**(模板里「止损」绑了 `take_profit`)。 +- The first line is hardcoded to bind `trading_levels.suggested_stop_loss`, and the second line is bound to `suggested_take_profit`. +- Or reverse the `stop_loss` / `take_profit` ** tags ** ("stop loss" in the template is tied to `take_profit`). -### 推荐写法(私有仓库中修改) +### Recommended writing method (modify in private warehouse) -只使用接口返回的 **`data.trading_plan`**(或兼容字段 **`stopLoss` / `takeProfit`**): +Only use **`data.trading_plan`** (or compatible fields **`stopLoss` / `takeProfit`**) returned by the interface: ```text -止损(亏损离场价): trading_plan.stop_loss (或 stopLoss) -止盈(盈利目标价): trading_plan.take_profit (或 takeProfit) +Stop loss (loss exit price): trading_plan.stop_loss (or stopLoss) +Take profit (profit target price): trading_plan.take_profit (or takeProfit) ``` -可选:根据 `trading_plan.decision === 'SELL'` 在文案旁加一句「空单:止损在现价上方,止盈在现价下方」。 +Optional: According to `trading_plan.decision === 'SELL'`, add a sentence "Short order: Stop loss is above the current price, take profit is below the current price" next to the copy. -### API 兼容字段(后端已加) +### API compatible fields (added in backend) -`trading_plan` 内额外包含: +`trading_plan` additionally contains: - `entryPrice`, `stopLoss`, `takeProfit`, `positionSizePct` -- `loss_exit_price`, `profit_target_price`(与止损/止盈数值一致,语义更清晰) -- `decision`:与主结果 `decision` 一致,便于组件内判断 +- `loss_exit_price`, `profit_target_price` (consistent with stop loss/take profit values, with clearer semantics) +- `decision`: consistent with the main result `decision`, which facilitates judgment within the component -根级另有驼峰:`trendOutlook`, `trendOutlookSummary`(与 `trend_outlook` 等相同内容)。 +There is another camel case at the root level: `trendOutlook`, `trendOutlookSummary` (the same content as `trend_outlook`, etc.). -## 2. 「未来时间段预判」不显示 +## 2. "Future time period prediction" is not displayed -后端字段: +Backend fields: -- **`trend_outlook`**:对象,含 `next_24h`, `next_3d`, `next_1w`, `next_1m`(每项含 `score`, `trend`, `strength`)。 -- **`trend_outlook_summary`**:一行可读摘要(中文/英文随 `language`)。 -- 若走 **`/api/fast-analysis/analyze-legacy`**:`fast_analysis` 内同样有上述字段;`overview.report` 会追加 **【周期预判】** 段落;顶层也有 `trend_outlook` / `trend_outlook_summary`。 +- **`trend_outlook`**: Object, including `next_24h`, `next_3d`, `next_1w`, `next_1m` (each item contains `score`, `trend`, `strength`). +- **`trend_outlook_summary`**: One line readable summary (Chinese/English with `language`). +- If you go to **`/api/fast-analysis/analyze-legacy`**: `fast_analysis` also has the above fields; `overview.report` will append the **[Period Prediction]** paragraph; the top level also has `trend_outlook` / `trend_outlook_summary`. -### 前端需要做的 +### What the front end needs to do -- 在快速分析结果页**单独渲染** `trend_outlook` 或 `trend_outlook_summary`(不要只读 `summary` 正文)。 -- 若请求的是 legacy 接口,请读 **`data.fast_analysis.trend_outlook`** 或顶层 **`data.trend_outlook`**,不要假设只在某一嵌套路径下。 +- **Single rendering** of `trend_outlook` or `trend_outlook_summary` in the quick analysis results page (do not read only the `summary` text). +- If the request is for the legacy interface, please read **`data.fast_analysis.trend_outlook`** or the top-level **`data.trend_outlook`**. Do not assume that it is only under a certain nested path. -## 3. 自检清单 +## 3. Self-check list -| 检查项 | 说明 | +| Check items | Description | |--------|------| -| 接口路径 | 确认用的是 `/analyze` 还是 `/analyze-legacy`,字段路径一致 | -| 绑定来源 | 止损/止盈是否来自 `trading_plan`,而非 `trading_levels` | -| SELL 几何 | 空单位:`take_profit < current < stop_loss` | -| 周期预判 | 模板是否包含 `trend_outlook` 或 `trend_outlook_summary` | +| Interface path | Confirm whether you are using `/analyze` or `/analyze-legacy`, and the field paths are consistent | +| Binding source | Whether stop loss/take profit comes from `trading_plan` instead of `trading_levels` | +| SELL Geometry | Empty units: `take_profit < current < stop_loss` | +| Cycle prediction | Whether the template contains `trend_outlook` or `trend_outlook_summary` | -更新私有前端仓库后,通过 CI 或手动打包替换本仓库的 `frontend/dist/`(参见 `update-frontend.yml`)。 +After updating the private front-end repository, replace `frontend/dist/` of this repository through CI or manual packaging (see `update-frontend.yml`).