From c3cf230104cb98e0a5b85819a75e471995df53da Mon Sep 17 00:00:00 2001 From: dienakdz Date: Wed, 8 Apr 2026 07:27:26 +0700 Subject: [PATCH] feat(trading-assistant): refactor strategy creation and enhance script mode functionality - Removed conditional rendering for the assistant guide bar. - Simplified strategy overview and strategy list item components. - Introduced a new modal for selecting strategy mode. - Enhanced strategy creation modal to support script strategies with a dedicated editor. - Updated form handling for script strategies, including validation and submission logic. - Improved user experience with better messaging and streamlined UI components. - Updated translations for better clarity in Chinese. --- backend_api_python/.dockerignore | 4 +- backend_api_python/.gitignore | 4 +- .../app/data_sources/cache_manager.py | 14 +- .../app/data_sources/circuit_breaker.py | 18 +- .../app/data_sources/factory.py | 2 +- .../app/data_sources/polymarket.py | 4 +- backend_api_python/app/routes/indicator.py | 2 +- backend_api_python/app/routes/settings.py | 10 +- .../app/services/analysis_memory.py | 4 +- backend_api_python/app/services/backtest.py | 6 +- .../app/services/community_service.py | 38 +- .../app/services/indicator_params.py | 6 +- .../app/services/live_trading/okx.py | 4 +- backend_api_python/app/services/llm.py | 6 +- .../app/services/market_data_collector.py | 42 +- backend_api_python/app/services/search.py | 44 +- .../app/services/signal_notifier.py | 14 +- backend_api_python/app/services/strategy.py | 6 + .../app/services/trading_executor.py | 51 +- backend_api_python/app/utils/safe_exec.py | 22 +- backend_api_python/env.example | 2 +- .../scripts/backfill_zero_trades.py | 38 +- .../scripts/run_reflection_task.py | 8 +- backend_api_python/start.sh | 21 +- frontend_vue/babel.config.js | 4 +- frontend_vue/commitlint.config.js | 22 +- frontend_vue/config/plugin.config.js | 6 +- frontend_vue/public/index.html | 74 +- frontend_vue/src/api/ai-trading.js | 18 +- frontend_vue/src/api/market.js | 36 +- frontend_vue/src/api/settings.js | 16 +- frontend_vue/src/api/strategy.js | 26 + frontend_vue/src/assets/background.svg | 4 +- .../ArticleListContent/ArticleListContent.vue | 2 +- .../src/components/AvatarList/List.jsx | 2 +- frontend_vue/src/components/Charts/Bar.vue | 2 +- .../src/components/Charts/MiniArea.vue | 2 +- .../src/components/Charts/MiniBar.vue | 2 +- frontend_vue/src/components/Charts/Radar.vue | 2 +- .../src/components/Charts/TransferBar.vue | 8 +- .../src/components/Editor/QuillEditor.vue | 4 +- .../src/components/GlobalFooter/index.vue | 24 +- .../GlobalHeader/AvatarDropdown.vue | 2 +- .../components/GlobalHeader/RightContent.vue | 18 +- .../src/components/IconSelector/icons.js | 14 +- .../src/components/NoticeIcon/NoticeIcon.vue | 76 +- .../src/components/Other/CarbonAds.vue | 2 +- .../PolymarketAnalysisWorkspace.vue | 2 +- .../src/components/SelectLang/index.jsx | 6 +- .../src/components/SelectLang/index.less | 2 +- .../SettingDrawer/SettingDrawer.vue | 8 +- .../components/SettingDrawer/themeColor.js | 4 +- frontend_vue/src/components/Table/index.js | 51 +- frontend_vue/src/components/Tree/Tree.jsx | 6 +- frontend_vue/src/components/_util/util.js | 6 +- frontend_vue/src/config/defaultSettings.js | 22 +- frontend_vue/src/core/bootstrap.js | 4 +- frontend_vue/src/core/directives/action.js | 16 +- frontend_vue/src/core/icons.js | 4 +- frontend_vue/src/core/lazy_use.js | 2 +- .../src/core/permission/permission.js | 18 +- frontend_vue/src/global.less | 10 +- frontend_vue/src/layouts/BasicLayout.less | 87 +- frontend_vue/src/layouts/BasicLayout.vue | 218 +- frontend_vue/src/layouts/RouteView.vue | 6 +- frontend_vue/src/locales/index.js | 2 +- frontend_vue/src/locales/lang/en-US.js | 120 +- frontend_vue/src/permission.js | 38 +- frontend_vue/src/router/index.js | 2 +- .../components/FastAnalysisReport.vue | 82 +- .../views/ai-analysis/components/index.vue | 410 ++-- frontend_vue/src/views/ai-analysis/index.vue | 1932 +++++++++++++++-- .../src/views/backtest-center/index.vue | 18 +- frontend_vue/src/views/billing/index.vue | 2 +- frontend_vue/src/views/dashboard/index.vue | 98 +- .../components/IndicatorEditor.vue | 1 - .../components/KlineChart.vue | 180 +- .../src/views/indicator-analysis/index.vue | 81 +- .../components/CommentList.vue | 30 +- .../components/IndicatorCard.vue | 24 +- .../components/IndicatorDetail.vue | 24 +- .../src/views/indicator-community/index.vue | 54 +- frontend_vue/src/views/portfolio/index.vue | 196 +- frontend_vue/src/views/profile/index.vue | 166 +- frontend_vue/src/views/settings/index.vue | 60 +- .../components/ScriptStrategyEditor.vue | 915 ++++++++ .../components/StrategyTypeSelector.vue | 61 + .../src/views/trading-assistant/index.vue | 678 +++--- frontend_vue/vue.config.js | 6 +- 89 files changed, 4653 insertions(+), 1735 deletions(-) create mode 100644 frontend_vue/src/views/trading-assistant/components/ScriptStrategyEditor.vue create mode 100644 frontend_vue/src/views/trading-assistant/components/StrategyTypeSelector.vue diff --git a/backend_api_python/.dockerignore b/backend_api_python/.dockerignore index 0ac2652..9068dcb 100644 --- a/backend_api_python/.dockerignore +++ b/backend_api_python/.dockerignore @@ -19,14 +19,14 @@ ENV/ logs/ *.log -# Database (在 docker-compose 中挂载) +# Database (mounted in docker-compose) *.db # Environment .env .env.local -# Data (在 docker-compose 中挂载) +# Data (mounted in docker-compose) data/memory/*.db # Git diff --git a/backend_api_python/.gitignore b/backend_api_python/.gitignore index 8067098..196630c 100644 --- a/backend_api_python/.gitignore +++ b/backend_api_python/.gitignore @@ -30,10 +30,10 @@ wheels/ *.swo *~ -# 日志 +# Logs *.log -# 环境变量 +# Environment variables .env .env.local diff --git a/backend_api_python/app/data_sources/cache_manager.py b/backend_api_python/app/data_sources/cache_manager.py index 2be9a65..a04d650 100644 --- a/backend_api_python/app/data_sources/cache_manager.py +++ b/backend_api_python/app/data_sources/cache_manager.py @@ -86,7 +86,7 @@ class DataCache: if entry.is_expired(): del self._cache[key] self._misses += 1 - logger.debug(f"[缓存] {self.name}:{key} 已过期,删除") + logger.debug(f"[cache] {self.name}:{key} expired and was removed") return None # Update access order (LRU) @@ -94,7 +94,7 @@ class DataCache: entry.hit_count += 1 self._hits += 1 - logger.debug(f"[缓存命中] {self.name}:{key} (年龄: {entry.age():.0f}s/{entry.ttl:.0f}s)") + logger.debug(f"[cache hit] {self.name}:{key} (age: {entry.age():.0f}s/{entry.ttl:.0f}s)") return entry.data def set( @@ -115,7 +115,7 @@ class DataCache: # 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}") + logger.debug(f"[cache] {self.name} reached capacity, evicted: {oldest_key}") actual_ttl = ttl if ttl is not None else self.default_ttl self._cache[key] = CacheEntry( @@ -124,14 +124,14 @@ class DataCache: ttl=actual_ttl ) - logger.debug(f"[缓存更新] {self.name}:{key} TTL={actual_ttl}s") + logger.debug(f"[cache update] {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] - logger.debug(f"[缓存] {self.name}:{key} 已删除") + logger.debug(f"[cache] {self.name}:{key} deleted") return True return False @@ -140,7 +140,7 @@ class DataCache: with self._lock: count = len(self._cache) self._cache.clear() - logger.info(f"[缓存] {self.name} 已清空 {count} 条记录") + logger.info(f"[cache] {self.name} cleared {count} records") return count def cleanup_expired(self) -> int: @@ -154,7 +154,7 @@ class DataCache: del self._cache[key] if expired_keys: - logger.debug(f"[缓存] {self.name} 清理 {len(expired_keys)} 条过期记录") + logger.debug(f"[cache] {self.name} cleaned {len(expired_keys)} expired records") return len(expired_keys) def stats(self) -> Dict[str, Any]: diff --git a/backend_api_python/app/data_sources/circuit_breaker.py b/backend_api_python/app/data_sources/circuit_breaker.py index c5a0206..c5db6ae 100644 --- a/backend_api_python/app/data_sources/circuit_breaker.py +++ b/backend_api_python/app/data_sources/circuit_breaker.py @@ -84,11 +84,11 @@ class CircuitBreaker: # Cooling is completed and enters the half-open state state['state'] = CircuitState.HALF_OPEN state['half_open_calls'] = 0 - logger.info(f"[熔断器] {source} 冷却完成,进入半开状态") + logger.info(f"[circuit breaker] {source} cooldown finished, entering half-open state") return True else: remaining = self.cooldown_seconds - time_since_failure - logger.debug(f"[熔断器] {source} 处于熔断状态,剩余冷却时间: {remaining:.0f}s") + logger.debug(f"[circuit breaker] {source} is open, remaining cooldown: {remaining:.0f}s") return False if state['state'] == CircuitState.HALF_OPEN: @@ -105,7 +105,7 @@ class CircuitBreaker: if state['state'] == CircuitState.HALF_OPEN: # Successful in half-open state, full recovery - logger.info(f"[熔断器] {source} 半开状态请求成功,恢复正常") + logger.info(f"[circuit breaker] {source} request succeeded in half-open state, fully recovered") # reset state state['state'] = CircuitState.CLOSED @@ -126,14 +126,14 @@ class CircuitBreaker: # 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") + logger.warning(f"[circuit breaker] {source} request failed in half-open state, staying open for {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)") + logger.warning(f"[circuit breaker] {source} failed {state['failures']} times consecutively and is now open " + f"(cooldown {self.cooldown_seconds}s)") if error: - logger.warning(f"[熔断器] 最后错误: {error}") + logger.warning(f"[circuit breaker] last error: {error}") def get_status(self) -> Dict[str, Dict[str, Any]]: """Get all data source status""" @@ -151,10 +151,10 @@ class CircuitBreaker: if source: if source in self._states: del self._states[source] - logger.info(f"[熔断器] 已重置 {source} 的熔断状态") + logger.info(f"[circuit breaker] reset breaker state for {source}") else: self._states.clear() - logger.info("[熔断器] 已重置所有数据源的熔断状态") + logger.info("[circuit breaker] reset breaker state for all data sources") # ============================================ diff --git a/backend_api_python/app/data_sources/factory.py b/backend_api_python/app/data_sources/factory.py index ff71397..565c6ad 100644 --- a/backend_api_python/app/data_sources/factory.py +++ b/backend_api_python/app/data_sources/factory.py @@ -62,7 +62,7 @@ class DataSourceFactory: from app.data_sources.futures import FuturesDataSource return FuturesDataSource() else: - raise ValueError(f"不支持的市场类型: {market}") + raise ValueError(f"Unsupported market type: {market}") @classmethod def get_kline( diff --git a/backend_api_python/app/data_sources/polymarket.py b/backend_api_python/app/data_sources/polymarket.py index ff75fb0..b540f90 100644 --- a/backend_api_python/app/data_sources/polymarket.py +++ b/backend_api_python/app/data_sources/polymarket.py @@ -160,7 +160,7 @@ class PolymarketDataSource: return None def get_market_history(self, market_id: str, days: int = 30) -> List[Dict]: - """获取市场历史价格数据""" + """Get historical market price data.""" # Here you need to implement historical data acquisition logic # Temporarily returns an empty list return [] @@ -374,7 +374,7 @@ class PolymarketDataSource: return [] def _get_cached_markets(self, category: str = None, limit: int = 50) -> Optional[List[Dict]]: - """从数据库缓存读取市场数据""" + """Read market data from the database cache.""" try: with get_db_connection() as db: cur = db.cursor() diff --git a/backend_api_python/app/routes/indicator.py b/backend_api_python/app/routes/indicator.py index 2e58de1..d5b83e1 100644 --- a/backend_api_python/app/routes/indicator.py +++ b/backend_api_python/app/routes/indicator.py @@ -709,7 +709,7 @@ IMPORTANT: Output Python code directly, without explanations, without descriptio reference_id=f"ai_code_gen_{user_id}_{int(time.time())}" ) if not ok: - yield "data: " + json.dumps({"error": f"积分不足: {msg}"}, ensure_ascii=False) + "\n\n" + yield "data: " + json.dumps({"error": f"Insufficient credits: {msg}"}, ensure_ascii=False) + "\n\n" yield "data: [DONE]\n\n" return diff --git a/backend_api_python/app/routes/settings.py b/backend_api_python/app/routes/settings.py index 723a01e..8bc3de4 100644 --- a/backend_api_python/app/routes/settings.py +++ b/backend_api_python/app/routes/settings.py @@ -1010,7 +1010,7 @@ def get_openrouter_balance(): if not api_key: return jsonify({ 'code': 0, - 'msg': 'OpenRouter API Key 未配置', + 'msg': 'OpenRouter API key is not configured', 'data': None }) @@ -1050,27 +1050,27 @@ def get_openrouter_balance(): elif resp.status_code == 401: return jsonify({ 'code': 0, - 'msg': 'API Key 无效或已过期', + 'msg': 'API key is invalid or expired', 'data': None }) else: return jsonify({ 'code': 0, - 'msg': f'查询失败: HTTP {resp.status_code}', + 'msg': f'Query failed: HTTP {resp.status_code}', 'data': None }) except requests.exceptions.Timeout: return jsonify({ 'code': 0, - 'msg': '请求超时,请检查网络连接', + 'msg': 'Request timed out. Please check the network connection.', 'data': None }) except Exception as e: logger.error(f"Get OpenRouter balance failed: {e}") return jsonify({ 'code': 0, - 'msg': f'查询失败: {str(e)}', + 'msg': f'Query failed: {str(e)}', 'data': None }) diff --git a/backend_api_python/app/services/analysis_memory.py b/backend_api_python/app/services/analysis_memory.py index 4b0cb1a..5ca81b2 100644 --- a/backend_api_python/app/services/analysis_memory.py +++ b/backend_api_python/app/services/analysis_memory.py @@ -84,7 +84,7 @@ class AnalysisMemory: cur.execute(""" DO $$ BEGIN - -- 添加 user_id 列(如果不存在) + -- Add the user_id column if it does not exist IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'qd_analysis_memory' AND column_name = 'user_id' @@ -92,7 +92,7 @@ class AnalysisMemory: ALTER TABLE qd_analysis_memory ADD COLUMN user_id INT; END IF; - -- 添加 raw_result 列(如果不存在) + -- Add the raw_result column if it does not exist IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'qd_analysis_memory' AND column_name = 'raw_result' diff --git a/backend_api_python/app/services/backtest.py b/backend_api_python/app/services/backtest.py index 3d32e9c..4e6f1fe 100644 --- a/backend_api_python/app/services/backtest.py +++ b/backend_api_python/app/services/backtest.py @@ -3378,7 +3378,7 @@ import pandas as pd else: # SL not strict enough, liquidation triggered logger.warning(f"Long liquidation! entry={entry_price:.2f}, low={low:.2f}, " - f"爆仓线={liquidation_price:.2f}, 止损价={stop_loss_price:.2f}") + f"liq_price={liquidation_price:.2f}, stop_loss_price={stop_loss_price:.2f}") is_liquidated = True liquidation_loss = self._liquidation_loss(capital) capital = 0 @@ -3402,7 +3402,7 @@ import pandas as pd stop_loss_price = close_short_price_arr[i] if has_stop_loss else 0 logger.warning(f"[candle {i}] Short hit liquidation! entry={entry_price:.2f}, high={high:.2f}, liq_price={liquidation_price:.2f}, " - f"止损信号={close_short_arr[i]}, 止损价={stop_loss_price:.4f}, 时间={timestamp}") + f"stop_loss_signal={close_short_arr[i]}, stop_loss_price={stop_loss_price:.4f}, time={timestamp}") # Determine SL or liquidation first if has_stop_loss and stop_loss_price < liquidation_price: @@ -3425,7 +3425,7 @@ import pandas as pd else: # SL not strict enough, liquidation triggered logger.warning(f"Short liquidation! entry={entry_price:.2f}, high={high:.2f}, " - f"爆仓线={liquidation_price:.2f}, 止损价={stop_loss_price:.2f}") + f"liq_price={liquidation_price:.2f}, stop_loss_price={stop_loss_price:.2f}") is_liquidated = True liquidation_loss = self._liquidation_loss(capital) capital = 0 diff --git a/backend_api_python/app/services/community_service.py b/backend_api_python/app/services/community_service.py index c3470e1..f218e96 100644 --- a/backend_api_python/app/services/community_service.py +++ b/backend_api_python/app/services/community_service.py @@ -155,7 +155,7 @@ class CommunityService: return {'items': [], 'total': 0, 'page': 1, 'page_size': page_size, 'total_pages': 0} def get_indicator_detail(self, indicator_id: int, user_id: int = None) -> Optional[Dict[str, Any]]: - """获取指标详情""" + """Get indicator details.""" try: with get_db_connection() as db: cur = db.cursor() @@ -235,8 +235,8 @@ class CommunityService: def purchase_indicator(self, buyer_id: int, indicator_id: int) -> Tuple[bool, str, Dict[str, Any]]: """ - 购买指标 - + Purchase an indicator. + Returns: (success, message, data) """ @@ -367,7 +367,7 @@ class CommunityService: return False, f'error: {str(e)}', {} def get_my_purchases(self, user_id: int, page: int = 1, page_size: int = 20) -> Dict[str, Any]: - """获取用户购买的指标列表""" + """Get the list of indicators purchased by the user.""" offset = (page - 1) * page_size try: @@ -433,7 +433,7 @@ class CommunityService: # ========================================== def get_comments(self, indicator_id: int, page: int = 1, page_size: int = 20) -> Dict[str, Any]: - """获取指标评论列表""" + """Get the list of indicator comments.""" offset = (page - 1) * page_size try: @@ -495,7 +495,9 @@ class CommunityService: content: str ) -> Tuple[bool, str, Dict[str, Any]]: """ - 添加评论(只有购买过的用户可以评论,且只能评论一次) + Add a comment. + + Only users who purchased the indicator can comment, and only once. """ try: # Verify score range @@ -577,7 +579,9 @@ class CommunityService: content: str ) -> Tuple[bool, str, Dict[str, Any]]: """ - 更新评论(只能修改自己的评论) + Update a comment. + + Users can only edit their own comments. """ try: rating = max(1, min(5, int(rating))) @@ -628,7 +632,7 @@ class CommunityService: return False, f'error: {str(e)}', {} def get_user_comment(self, user_id: int, indicator_id: int) -> Optional[Dict[str, Any]]: - """获取用户对某个指标的评论""" + """Get the user's comment for a specific indicator.""" try: with get_db_connection() as db: cur = db.cursor() @@ -665,7 +669,7 @@ class CommunityService: page_size: int = 20, review_status: str = 'pending' # 'pending' / 'approved' / 'rejected' / 'all' ) -> Dict[str, Any]: - """获取待审核的指标列表(管理员用)""" + """Get the list of indicators pending review for admins.""" offset = (page - 1) * page_size try: @@ -753,7 +757,7 @@ class CommunityService: action: str, # 'approve' / 'reject' note: str = '' ) -> Tuple[bool, str]: - """审核指标""" + """Review an indicator.""" try: new_status = 'approved' if action == 'approve' else 'rejected' note = (note or '').strip()[:500] @@ -790,7 +794,7 @@ class CommunityService: return False, f'error: {str(e)}' def unpublish_indicator(self, admin_id: int, indicator_id: int, note: str = '') -> Tuple[bool, str]: - """下架指标(取消发布)""" + """Unpublish an indicator.""" try: note = (note or '').strip()[:500] @@ -826,7 +830,7 @@ class CommunityService: return False, f'error: {str(e)}' def admin_delete_indicator(self, admin_id: int, indicator_id: int) -> Tuple[bool, str]: - """管理员删除指标""" + """Delete an indicator as an admin.""" try: with get_db_connection() as db: cur = db.cursor() @@ -889,11 +893,11 @@ class CommunityService: def get_indicator_performance(self, indicator_id: int) -> Dict[str, Any]: """ - 获取指标的实盘表现统计 + Get live performance statistics for an indicator. - 数据来源: - 1. qd_backtest_runs - 回测记录(result_json 内含 totalReturn / winRate 等) - 2. qd_strategy_trades + qd_strategies_trading - 真实实盘交易记录 + Data sources: + 1. qd_backtest_runs - Backtest records (result_json contains totalReturn, winRate, etc.) + 2. qd_strategy_trades + qd_strategies_trading - Real live trading records """ default_result = { 'strategy_count': 0, @@ -1032,7 +1036,7 @@ _community_service = None def get_community_service() -> CommunityService: - """获取社区服务单例""" + """Get the community service singleton.""" global _community_service if _community_service is None: _community_service = CommunityService() diff --git a/backend_api_python/app/services/indicator_params.py b/backend_api_python/app/services/indicator_params.py index 4d5a23a..442cfdb 100644 --- a/backend_api_python/app/services/indicator_params.py +++ b/backend_api_python/app/services/indicator_params.py @@ -272,13 +272,13 @@ class IndicatorCaller: def get_indicator_params(indicator_id: int) -> List[Dict[str, Any]]: """ - 获取指标的参数声明(供API调用) + Get the indicator parameter declarations for API usage Args: - indicator_id: 指标ID + indicator_id: Indicator ID Returns: - 参数声明列表 + A list of parameter declarations """ try: with get_db_connection() as db: diff --git a/backend_api_python/app/services/live_trading/okx.py b/backend_api_python/app/services/live_trading/okx.py index ae5e855..fda5ad4 100644 --- a/backend_api_python/app/services/live_trading/okx.py +++ b/backend_api_python/app/services/live_trading/okx.py @@ -722,8 +722,8 @@ class OkxClient(BaseRestClient): """ mt = (market_type or "swap").strip().lower() inst_id = to_okx_spot_inst_id(symbol) if mt == "spot" else to_okx_swap_inst_id(symbol) - # IMPORTANT: For OKX SWAP, fillSz/accFillSz are in "contracts" (张数), not base-asset quantity. - # Our system standardizes on base-asset quantity everywhere ("币数"), so we convert using ctVal. + # IMPORTANT: For OKX SWAP, fillSz/accFillSz are in contract counts, not base-asset quantity. + # Our system standardizes on base-asset quantity everywhere, so we convert using ctVal. ct_val = Decimal("0") if mt != "spot": try: diff --git a/backend_api_python/app/services/llm.py b/backend_api_python/app/services/llm.py index 0d06a77..797bd2c 100644 --- a/backend_api_python/app/services/llm.py +++ b/backend_api_python/app/services/llm.py @@ -212,11 +212,11 @@ class LLMService: if "openrouter" in (base_url or "").lower(): from app.config.api_keys import APIKeys if not APIKeys.OPENROUTER_API_KEY: - error_msg += ". OPENROUTER_API_KEY 未配置,请在 backend_api_python/.env 中设置" + error_msg += ". OPENROUTER_API_KEY is not configured. Set it in backend_api_python/.env" elif response.status_code == 403: - error_msg += ". 可能原因:API 密钥无效/过期、余额不足、或无模型权限。请检查 https://openrouter.ai/keys" + error_msg += ". Possible causes: API key is invalid or expired, account balance is insufficient, or the model is not permitted. Check https://openrouter.ai/keys" elif response.status_code == 404: - error_msg += ". 可能原因:模型不可用或账户隐私/数据策略限制。请检查 https://openrouter.ai/settings/privacy" + error_msg += ". Possible causes: the model is unavailable or blocked by account privacy/data-policy settings. Check https://openrouter.ai/settings/privacy" raise ValueError(error_msg) diff --git a/backend_api_python/app/services/market_data_collector.py b/backend_api_python/app/services/market_data_collector.py index 68d5fba..d2a7424 100644 --- a/backend_api_python/app/services/market_data_collector.py +++ b/backend_api_python/app/services/market_data_collector.py @@ -534,7 +534,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。 + MACD(12,26,9): DIF = EMA12(close) - EMA26(close), DEA = EMA9(DIF), histogram = DIF - DEA. 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) @@ -866,32 +866,32 @@ class MarketDataCollector: crypto_info = { 'BTC': { 'name': 'Bitcoin', - 'description': '比特币,数字黄金,市值第一的加密货币,作为价值存储和避险资产', + 'description': 'Bitcoin, the leading cryptocurrency by market cap, often viewed as digital gold and a store-of-value asset.', 'category': 'Store of Value', }, 'ETH': { 'name': 'Ethereum', - 'description': '以太坊,智能合约平台,DeFi和NFT生态的基础设施', + 'description': 'Ethereum, a smart-contract platform and core infrastructure for the DeFi and NFT ecosystem.', 'category': 'Smart Contract Platform', }, 'BNB': { 'name': 'Binance Coin', - 'description': '币安币,全球最大交易所的平台代币', + 'description': 'Binance Coin, the platform token of one of the world\'s largest exchanges.', 'category': 'Exchange Token', }, 'SOL': { 'name': 'Solana', - 'description': '高性能公链,主打高TPS和低Gas费', + 'description': 'A high-performance public chain focused on high throughput and low transaction fees.', 'category': 'Smart Contract Platform', }, 'XRP': { 'name': 'Ripple', - 'description': '瑞波币,专注跨境支付解决方案', + 'description': 'Ripple, focused on cross-border payment solutions.', 'category': 'Payment', }, 'DOGE': { 'name': 'Dogecoin', - 'description': '狗狗币,Meme币代表,社区驱动', + 'description': 'Dogecoin, a community-driven meme coin.', 'category': 'Meme', }, } @@ -905,7 +905,7 @@ class MarketDataCollector: return { 'name': base, - 'description': f'{base} 是一种加密货币', + 'description': f'{base} is a cryptocurrency.', 'category': 'Unknown', } @@ -960,7 +960,7 @@ class MarketDataCollector: if cached_sentiment.get('vix'): vix = cached_sentiment['vix'] result['VIX'] = { - 'name': 'VIX恐慌指数', + 'name': 'VIX Fear Index', 'description': vix.get('interpretation', ''), 'price': vix.get('value', 0), 'change': vix.get('change', 0), @@ -971,7 +971,7 @@ class MarketDataCollector: if cached_sentiment.get('dxy'): dxy = cached_sentiment['dxy'] result['DXY'] = { - 'name': '美元指数', + 'name': 'US Dollar Index', 'description': dxy.get('interpretation', ''), 'price': dxy.get('value', 0), 'change': dxy.get('change', 0), @@ -982,7 +982,7 @@ class MarketDataCollector: if cached_sentiment.get('yield_curve'): yc = cached_sentiment['yield_curve'] result['TNX'] = { - 'name': '美债10年收益率', + 'name': 'US 10Y Treasury Yield', 'description': yc.get('interpretation', ''), 'price': yc.get('yield_10y', 0), 'change': yc.get('change', 0), @@ -994,7 +994,7 @@ class MarketDataCollector: if cached_sentiment.get('fear_greed'): fg = cached_sentiment['fear_greed'] result['FEAR_GREED'] = { - 'name': '恐惧贪婪指数', + 'name': 'Fear & Greed Index', 'description': fg.get('classification', 'Neutral'), 'price': fg.get('value', 50), 'change': 0, @@ -1024,7 +1024,7 @@ class MarketDataCollector: # Convert to unified format if key == 'VIX': result[key] = { - 'name': 'VIX恐慌指数', + 'name': 'VIX Fear Index', 'description': data.get('interpretation', ''), 'price': data.get('value', 0), 'change': data.get('change', 0), @@ -1033,7 +1033,7 @@ class MarketDataCollector: } elif key == 'DXY': result[key] = { - 'name': '美元指数', + 'name': 'US Dollar Index', 'description': data.get('interpretation', ''), 'price': data.get('value', 0), 'change': data.get('change', 0), @@ -1042,7 +1042,7 @@ class MarketDataCollector: } elif key == 'TNX': result[key] = { - 'name': '美债10年收益率', + 'name': 'US 10Y Treasury Yield', 'description': data.get('interpretation', ''), 'price': data.get('yield_10y', 0), 'change': data.get('change', 0), @@ -1052,7 +1052,7 @@ class MarketDataCollector: } elif key == 'FEAR_GREED': result[key] = { - 'name': '恐惧贪婪指数', + 'name': 'Fear & Greed Index', 'description': data.get('classification', 'Neutral'), 'price': data.get('value', 50), 'change': 0, @@ -1122,7 +1122,7 @@ class MarketDataCollector: "url": item.get('url', ''), "sentiment": item.get('sentiment', 'neutral'), }) - logger.info(f"Finnhub 新闻获取成功: {len(news_list)} 条") + logger.info(f"Finnhub news fetched successfully: {len(news_list)} items") except Exception as e: logger.debug(f"Finnhub news fetch failed: {e}") @@ -1199,13 +1199,13 @@ class MarketDataCollector: "datetime": result.published_date or datetime.now().strftime('%Y-%m-%d'), "headline": result.title, "summary": result.snippet[:200] if result.snippet else '', - "source": f"搜索:{result.source}", + "source": f"Search:{result.source}", "url": result.url, "sentiment": result.sentiment, }) - logger.info(f"搜索引擎新闻补充: {len(news_list)} 条 (来源: {response.provider})") + logger.info(f"Search-engine news supplement: {len(news_list)} items (provider: {response.provider})") except Exception as e: - logger.debug(f"搜索引擎新闻获取失败: {e}") + logger.debug(f"Search-engine news fetch failed: {e}") return news_list @@ -1260,7 +1260,7 @@ class MarketDataCollector: "datetime": result.published_date or datetime.now().strftime('%Y-%m-%d %H:%M'), "headline": result.title, "summary": result.snippet[:300] if result.snippet else '', - "source": f"全球事件:{result.source}", + "source": f"Global event:{result.source}", "url": result.url, "sentiment": "negative" if any(kw in text for kw in ["war", "conflict", "attack", "战争", "冲突", "袭击"]) else "neutral", "is_global_event": True # Flag as global event diff --git a/backend_api_python/app/services/search.py b/backend_api_python/app/services/search.py index 2b5808e..769dbc6 100644 --- a/backend_api_python/app/services/search.py +++ b/backend_api_python/app/services/search.py @@ -9,7 +9,7 @@ Supported search engines (in order of priority): 4. Bing Search API 5. DuckDuckGo – Get the scoop for free -参考:daily_stock_analysis-main/src/search_service.py +Reference: daily_stock_analysis-main/src/search_service.py """ import requests import json @@ -72,9 +72,9 @@ class SearchResponse: def to_context(self, max_results: int = 5) -> str: """Transform search results into context that can be used for AI analysis""" if not self.success or not self.results: - return f"搜索 '{self.query}' 未找到相关结果。" + return f"No relevant results were found for '{self.query}'." - lines = [f"【{self.query} 搜索结果】(来源:{self.provider})"] + lines = [f"[Search results for {self.query}] (source: {self.provider})"] for i, result in enumerate(self.results[:max_results], 1): lines.append(f"\n{i}. {result.to_text()}") @@ -128,7 +128,7 @@ class BaseSearchProvider(ABC): return key # There is a problem with all keys, reset the error count and return the first one - logger.warning(f"[{self._name}] 所有 API Key 都有错误记录,重置错误计数") + logger.warning(f"[{self._name}] all API keys have recorded errors, resetting error counters") self._key_errors = {key: 0 for key in self._api_keys} return self._api_keys[0] if self._api_keys else None @@ -142,7 +142,7 @@ class BaseSearchProvider(ABC): 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]}") + logger.warning(f"[{self._name}] API key {key[:8]}... error count: {self._key_errors[key]}") @abstractmethod def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse: @@ -168,7 +168,7 @@ class BaseSearchProvider(ABC): results=[], provider=self._name, success=False, - error_message=f"{self._name} 未配置 API Key" + error_message=f"{self._name} API key is not configured" ) start_time = time.time() @@ -178,7 +178,7 @@ class BaseSearchProvider(ABC): if response.success: self._record_success(api_key) - logger.info(f"[{self._name}] 搜索 '{query}' 成功,返回 {len(response.results)} 条结果,耗时 {response.search_time:.2f}s") + logger.info(f"[{self._name}] search '{query}' succeeded, returned {len(response.results)} results in {response.search_time:.2f}s") else: self._record_error(api_key) @@ -187,7 +187,7 @@ class BaseSearchProvider(ABC): except Exception as e: self._record_error(api_key) elapsed = time.time() - start_time - logger.error(f"[{self._name}] 搜索 '{query}' 失败: {e}") + logger.error(f"[{self._name}] search '{query}' failed: {e}") return SearchResponse( query=query, results=[], @@ -203,9 +203,9 @@ class BaseSearchProvider(ABC): try: parsed = urlparse(url) domain = parsed.netloc.replace('www.', '') - return domain or '未知来源' + return domain or 'Unknown source' except: - return '未知来源' + return 'Unknown source' class TavilySearchProvider(BaseSearchProvider): @@ -265,7 +265,7 @@ class TavilySearchProvider(BaseSearchProvider): except Exception as e: error_msg = str(e) if 'rate limit' in error_msg.lower() or 'quota' in error_msg.lower(): - error_msg = f"API 配额已用尽: {error_msg}" + error_msg = f"API quota exhausted: {error_msg}" return SearchResponse( query=query, @@ -483,7 +483,7 @@ class GoogleSearchProvider(BaseSearchProvider): results=[], provider=self.name, success=False, - error_message="Google Search 未配置 CX" + error_message="Google Search CX is not configured" ) try: @@ -515,7 +515,7 @@ class GoogleSearchProvider(BaseSearchProvider): results=[], provider=self.name, success=False, - error_message="Google API 配额已用尽" + error_message="Google API quota exhausted" ) response.raise_for_status() @@ -744,33 +744,33 @@ class SearchService: tavily_keys = APIKeys.TAVILY_API_KEYS if tavily_keys: self._providers.append(TavilySearchProvider(tavily_keys)) - logger.info(f"已配置 Tavily 搜索,共 {len(tavily_keys)} 个 API Key") + logger.info(f"Configured Tavily search with {len(tavily_keys)} API keys") # 2. SerpAPI serpapi_keys = APIKeys.SERPAPI_KEYS if serpapi_keys: self._providers.append(SerpAPISearchProvider(serpapi_keys)) - logger.info(f"已配置 SerpAPI 搜索,共 {len(serpapi_keys)} 个 API Key") + logger.info(f"Configured SerpAPI search with {len(serpapi_keys)} API keys") # 3. Google CSE google_api_key = self._config.get('google', {}).get('api_key') google_cx = self._config.get('google', {}).get('cx') if google_api_key and google_cx: self._providers.append(GoogleSearchProvider(google_api_key, google_cx)) - logger.info("已配置 Google CSE 搜索") + logger.info("Configured Google CSE search") # 4. Bing bing_api_key = self._config.get('bing', {}).get('api_key') if bing_api_key: self._providers.append(BingSearchProvider(bing_api_key)) - logger.info("已配置 Bing 搜索") + logger.info("Configured Bing search") # 5. DuckDuckGo (Free Tips) self._providers.append(DuckDuckGoSearchProvider()) - logger.info("已配置 DuckDuckGo 搜索(免费兜底)") + logger.info("Configured DuckDuckGo search as the free fallback") if len(self._providers) == 1: - logger.warning("仅有 DuckDuckGo 可用,建议配置更多搜索引擎 API Key") + logger.warning("Only DuckDuckGo is available. Configure more search-engine API keys for better coverage.") @property def is_available(self) -> bool: @@ -826,7 +826,7 @@ class SearchService: if response.success and response.results: return response else: - logger.warning(f"{provider.name} 搜索失败: {response.error_message},尝试下一个引擎") + logger.warning(f"{provider.name} search failed: {response.error_message}. Trying the next engine.") # all engines fail return SearchResponse( @@ -834,7 +834,7 @@ class SearchService: results=[], provider="None", success=False, - error_message="所有搜索引擎都不可用或搜索失败" + error_message="All search engines are unavailable or failed" ) def search_stock_news( @@ -875,7 +875,7 @@ class SearchService: else: query = f"{stock_name} {stock_code} latest news" - logger.info(f"搜索股票新闻: {stock_name}({stock_code}), market={market}, days={search_days}") + logger.info(f"Searching stock news: {stock_name}({stock_code}), market={market}, days={search_days}") return self.search_with_fallback(query, max_results, search_days) diff --git a/backend_api_python/app/services/signal_notifier.py b/backend_api_python/app/services/signal_notifier.py index e1aaedc..a77c1e8 100644 --- a/backend_api_python/app/services/signal_notifier.py +++ b/backend_api_python/app/services/signal_notifier.py @@ -501,15 +501,15 @@ class SignalNotifier: """ Generic webhook delivery. - 用户在个人中心配置: - - webhook_url: Webhook 地址 - - webhook_token: Bearer Token(可选) + User configuration in the profile center: + - webhook_url: webhook URL + - webhook_token: optional Bearer token - 支持功能: - - 自定义 headers: notification_config.targets.webhook_headers + Supported features: + - Custom headers: notification_config.targets.webhook_headers - Bearer Token: notification_config.targets.webhook_token - - 签名验证: notification_config.targets.webhook_signing_secret - - 自动重试: 429/5xx 时重试一次 + - Signature verification: notification_config.targets.webhook_signing_secret + - Automatic retry: retry once on 429/5xx """ if not url: return False, "missing_webhook_url" diff --git a/backend_api_python/app/services/strategy.py b/backend_api_python/app/services/strategy.py index 71c6d93..ade08c5 100644 --- a/backend_api_python/app/services/strategy.py +++ b/backend_api_python/app/services/strategy.py @@ -933,6 +933,8 @@ class StrategyService: trading_config = payload.get('trading_config') if payload.get('trading_config') is not None else (existing.get('trading_config') or {}) exchange_config = payload.get('exchange_config') if payload.get('exchange_config') is not None else (existing.get('exchange_config') or {}) ai_model_config = payload.get('ai_model_config') if payload.get('ai_model_config') is not None else (existing.get('ai_model_config') or {}) + strategy_mode = payload.get('strategy_mode') if payload.get('strategy_mode') is not None else (existing.get('strategy_mode') or 'signal') + strategy_code = payload.get('strategy_code') if payload.get('strategy_code') is not None else (existing.get('strategy_code') or '') # When credential_id is present, strip raw API keys to avoid # storing secrets in the strategy record — they live in qd_exchange_credentials. @@ -964,6 +966,8 @@ class StrategyService: """ UPDATE qd_strategies_trading SET strategy_name = ?, + strategy_mode = ?, + strategy_code = ?, market_category = ?, execution_mode = ?, notification_config = ?, @@ -981,6 +985,8 @@ class StrategyService: """, ( name, + strategy_mode, + strategy_code, market_category, execution_mode, self._dump_json_or_encrypt(notification_config, encrypt=False), diff --git a/backend_api_python/app/services/trading_executor.py b/backend_api_python/app/services/trading_executor.py index 0779c0a..dd5ede2 100644 --- a/backend_api_python/app/services/trading_executor.py +++ b/backend_api_python/app/services/trading_executor.py @@ -88,9 +88,10 @@ class TradingExecutor: def _normalize_trade_symbol(self, exchange: Any, symbol: str, market_type: str, exchange_id: str) -> str: """ - 将数据库/配置里的 symbol 规范化为交易所合约可用的 CCXT symbol。 + Normalize symbols from the database/config into the CCXT symbol format accepted by the exchange. - 典型场景:OKX 永续统一符号通常是 `BNB/USDT:USDT`,但前端/数据库可能传 `BNB/USDT`。 + Typical case: the normalized OKX perpetual symbol is usually `BNB/USDT:USDT`, + while the frontend or database may pass `BNB/USDT`. """ try: # New system: only supports swap (perpetual contract) / spot (spot) @@ -131,7 +132,7 @@ class TradingExecutor: return symbol def _log_resource_status(self, prefix: str = ""): - """调试:记录线程/内存使用,便于定位 can't start new thread 根因""" + """Debug helper: record thread and memory usage to diagnose the root cause of 'can't start new thread'.""" try: import psutil # Use more precise metrics if installed p = psutil.Process() @@ -415,7 +416,7 @@ class TradingExecutor: thread.start() except Exception as e: # Capture exceptions such as can't start new thread and record resource status - self._log_resource_status(prefix="启动异常") + self._log_resource_status(prefix="startup exception") raise e self.running_strategies[strategy_id] = thread @@ -631,15 +632,15 @@ class TradingExecutor: # 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} 启动时检查持仓同步...") + logger.info(f"Checking position synchronization during startup for strategy {strategy_id}...") # 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'): worker._sync_positions_best_effort(target_strategy_id=strategy_id) - logger.info(f"策略 {strategy_id} 启动时持仓同步完成") + logger.info(f"Position synchronization completed during startup for strategy {strategy_id}") except Exception as e: - logger.warning(f"策略 {strategy_id} 启动时持仓同步失败(不影响启动): {e}") + logger.warning(f"Position synchronization failed during startup for strategy {strategy_id} (startup continues): {e}") # Get the current highest position price (read from local database) current_pos_list = self._get_current_positions(strategy_id, symbol) @@ -660,7 +661,7 @@ class TradingExecutor: # Key diagnostic log: Confirm whether the indicator has obtained the position status logger.info( - f"策略 {strategy_id} 指标注入持仓状态: count={len(current_pos_list)}, " + f"Injected position state for strategy {strategy_id}: count={len(current_pos_list)}, " f"position={initial_position}, entry_price={initial_avg_entry_price}, highest={initial_highest}" ) @@ -1005,7 +1006,7 @@ class TradingExecutor: market=market_type or 'Crypto', symbol=symbol, signal_type=signal_type, - signal_detail=f"策略: {strategy_name}\n信号: {signal_type}\n价格: {execute_price:.4f}" + signal_detail=f"Strategy: {strategy_name}\nSignal: {signal_type}\nPrice: {execute_price:.4f}" ) except Exception as link_e: logger.warning(f"Strategy signal linkage notification failed: {link_e}") @@ -1095,8 +1096,8 @@ class TradingExecutor: def _is_strategy_running(self, strategy_id: int) -> bool: """ - 检查策略是否在运行 - 同时检查数据库状态和线程状态,避免重启后状态不一致 + Check whether the strategy is running. + This checks both the database state and the thread state to avoid mismatches after restart. """ try: # 1. Check database status @@ -1145,7 +1146,7 @@ class TradingExecutor: leverage: float = None, strategy_id: int = None ) -> Optional[ccxt.Exchange]: - """(Mock) 信号模式不需要真实交易所连接""" + """(Mock) Signal mode does not require a real exchange connection.""" return None def _fetch_latest_kline(self, symbol: str, timeframe: str, limit: int = 500, market_category: str = 'Crypto') -> List[Dict[str, Any]]: @@ -1872,7 +1873,7 @@ class TradingExecutor: allowed_modules = ['numpy', 'pandas', 'math', 'json', 'time'] if name in allowed_modules or name.split('.')[0] in allowed_modules: return builtins.__import__(name, *args, **kwargs) - raise ImportError(f"不允许导入模块: {name}") + raise ImportError(f"Importing modules is not allowed: {name}") safe_builtins = {k: getattr(builtins, k) for k in dir(builtins) if not k.startswith('_') and k not in [ @@ -1961,7 +1962,7 @@ class TradingExecutor: return [] def _execute_trading_logic(self, *args, **kwargs): - """已废弃""" + """Deprecated.""" pass def _execute_signal( @@ -2014,8 +2015,8 @@ class TradingExecutor: # Best-effort persist a browser notification so UI can show "HOLD due to AI filter". reason = (ai_info or {}).get("reason") or "ai_filter_rejected" ai_decision = (ai_info or {}).get("ai_decision") or "" - title = f"AI过滤拦截开仓 | {symbol}" - msg = f"策略信号={sig},AI决策={ai_decision or 'UNKNOWN'},原因={reason};已HOLD(不下单)" + title = f"AI filter blocked opening a position | {symbol}" + msg = f"Strategy signal={sig}, AI decision={ai_decision or 'UNKNOWN'}, reason={reason}; action forced to HOLD (no order placed)" self._persist_browser_notification( strategy_id=strategy_id, symbol=symbol, @@ -2369,7 +2370,7 @@ class TradingExecutor: payload: Optional[Dict[str, Any]] = None, user_id: int = None, ) -> None: - """Best-effort persist notification row for the frontend '通知' panel (browser channel).""" + """Best-effort persistence of a notification row for the frontend notifications panel (browser channel).""" try: now = int(time.time()) # Get user_id from strategy if not provided @@ -2776,7 +2777,7 @@ class TradingExecutor: highest_price: float = 0.0, lowest_price: float = 0.0, ): - """更新持仓状态""" + """Update position state.""" try: # Get user_id from strategy user_id = 1 @@ -2811,7 +2812,7 @@ class TradingExecutor: logger.error(f"Failed to update position: {e}") def _close_position(self, strategy_id: int, symbol: str, side: str): - """平仓:删除持仓记录""" + """Close a position by deleting the position record.""" try: with get_db_connection() as db: cursor = db.cursor() @@ -2861,7 +2862,7 @@ class TradingExecutor: return [] def _should_rebalance(self, strategy_id: int, rebalance_frequency: str) -> bool: - """检查是否应该调仓""" + """Check whether rebalancing should run.""" try: with get_db_connection() as db: cursor = db.cursor() @@ -2892,7 +2893,7 @@ class TradingExecutor: return True def _update_last_rebalance(self, strategy_id: int): - """更新上次调仓时间""" + """Update the last rebalance timestamp.""" try: with get_db_connection() as db: cursor = db.cursor() @@ -2920,7 +2921,7 @@ class TradingExecutor: timeframe: str ) -> Optional[Dict[str, Any]]: """ - 执行截面策略指标,返回所有标的的评分和排序 + Execute cross-sectional strategy indicators and return scores and ranking for all instruments. """ try: # Get K-line data of all targets @@ -2989,7 +2990,7 @@ class TradingExecutor: trading_config: Dict[str, Any] ) -> List[Dict[str, Any]]: """ - 根据排序结果生成截面策略信号 + Generate cross-sectional strategy signals from the ranking results. """ portfolio_size = trading_config.get('portfolio_size', 10) long_ratio = float(trading_config.get('long_ratio', 0.5)) @@ -3080,7 +3081,7 @@ class TradingExecutor: indicator_id: Optional[int] ): """ - 截面策略执行循环 + Cross-sectional strategy execution loop. """ logger.info(f"Starting cross-sectional strategy loop for strategy {strategy_id}") @@ -3188,4 +3189,4 @@ class TradingExecutor: except Exception as e: logger.error(f"Cross-sectional strategy loop error: {e}") logger.error(traceback.format_exc()) - time.sleep(5) # Wait before retrying \ No newline at end of file + time.sleep(5) # Wait before retrying diff --git a/backend_api_python/app/utils/safe_exec.py b/backend_api_python/app/utils/safe_exec.py index bafed10..fb7e895 100644 --- a/backend_api_python/app/utils/safe_exec.py +++ b/backend_api_python/app/utils/safe_exec.py @@ -50,7 +50,7 @@ def timeout_context(seconds: int): return def timeout_handler(signum, frame): - raise TimeoutError(f"代码执行超时(超过{seconds}秒)") + raise TimeoutError(f"Code execution timed out after {seconds} seconds") try: # Set up signal handler @@ -129,7 +129,7 @@ def safe_exec_code( } except MemoryError as e: - error_msg = f"代码执行内存不足(超过{max_memory_mb}MB限制)" + error_msg = f"Code execution exceeded the memory limit of {max_memory_mb}MB" logger.error(f"Code execution out of memory (limit={max_memory_mb}MB)") return { 'success': False, @@ -145,7 +145,7 @@ def safe_exec_code( 'result': None } except Exception as e: - error_msg = f"代码执行错误: {str(e)}\n{traceback.format_exc()}" + error_msg = f"Code execution error: {str(e)}\n{traceback.format_exc()}" logger.error(f"Code execution error: {str(e)}") logger.error(traceback.format_exc()) return { @@ -237,7 +237,7 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: # Check your code for dangerous patterns for pattern in dangerous_patterns: if re.search(pattern, code): - return False, f"检测到危险代码模式: {pattern}" + return False, f"Dangerous code pattern detected: {pattern}" # Try parsing the AST, checking for dangerous nodes try: @@ -267,13 +267,13 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: if isinstance(node.func, ast.Name): func_name = node.func.id if func_name in dangerous_functions: - return False, f"检测到危险函数调用: {func_name}()" + return False, f"Dangerous function call detected: {func_name}()" # 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}" + return False, f"Dangerous module call detected: {node.func.value.id}.{node.func.attr}" # Check if there are bypass methods such as getattr(builtins, '__import__') if isinstance(node.func, ast.Name) and node.func.id == 'getattr': @@ -281,15 +281,15 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: 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}')" + return False, f"Detected an attempt to bypass restrictions via getattr: getattr({node.args[0].id}, '{node.args[1].value}')" # 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 等对象" + return False, "Import statements are not allowed in scripts. Use the platform-provided objects such as pd and np directly." if isinstance(node, ast.ImportFrom): - return False, "不允许在脚本中使用 import 语句,请直接使用平台提供的 pd/np 等对象" + return False, "Import statements are not allowed in scripts. Use the platform-provided objects such as pd and np directly." # Check if there is an attempt to access __builtins__ for node in ast.walk(tree): @@ -298,10 +298,10 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]: 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}" + return False, f"Dangerous attribute access detected: {node.value.id}.{node.attr}" except SyntaxError as e: - return False, f"代码语法错误: {str(e)}" + return False, f"Code syntax error: {str(e)}" except Exception as e: # 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)}") diff --git a/backend_api_python/env.example b/backend_api_python/env.example index b9004e0..f746370 100644 --- a/backend_api_python/env.example +++ b/backend_api_python/env.example @@ -88,7 +88,7 @@ GITHUB_REDIRECT_URI=http://localhost:5000/api/auth/oauth/github/callback # ========================= BILLING_ENABLED=false -# 积分单价 +# Credit unit cost BILLING_COST_AI_ANALYSIS=10 BILLING_COST_AI_CODE_GEN=30 diff --git a/backend_api_python/scripts/backfill_zero_trades.py b/backend_api_python/scripts/backfill_zero_trades.py index 0c3c5c6..9c073a5 100644 --- a/backend_api_python/scripts/backfill_zero_trades.py +++ b/backend_api_python/scripts/backfill_zero_trades.py @@ -1,19 +1,19 @@ """ -回填历史 qd_strategy_trades 中 price/amount/value 为 0 的记录。 +Backfill historical qd_strategy_trades rows whose price/amount/value are 0. -背景: -- 某些交易所/订单类型下,执行器回报里 filled_price/filled_amount 可能为 0, - 但交易所实际已成交,导致交易纪律/交易记录显示为 0。 -- 我们现在在 OrderProcessor 中增加了“fetch_order/fetch_my_trades 回补”逻辑,避免新数据再出现该问题。 -- 对历史脏数据,可用 qd_pending_orders 中的 executed_at/filled_price/filled_amount/fee 做近似匹配回填。 +Background: +- For some exchanges and order types, the executor may report filled_price/filled_amount as 0 + even though the exchange has actually filled the order, causing trade records to show 0. +- We have now added fetch_order/fetch_my_trades backfill logic in OrderProcessor to avoid this for new data. +- For historical dirty data, use executed_at/filled_price/filled_amount/fee in qd_pending_orders for approximate backfilling. -使用: +Usage: python backend_api_python/scripts/backfill_zero_trades.py --strategy-id 43 --since 2025-12-24 --until 2025-12-25 python backend_api_python/scripts/backfill_zero_trades.py --strategy-id 43 --since 2025-12-24 --until 2025-12-25 --apply -注意: -- 该脚本按 (strategy_id, symbol, type) + 时间窗口(默认 ±600s) 匹配 qd_pending_orders。 -- 若同一条 trade 匹配到多个候选订单,将选择 executed_at 最接近的那条;若仍不唯一会跳过。 +Notes: +- The script matches qd_pending_orders by (strategy_id, symbol, type) plus a time window of ±600s by default. +- If one trade matches multiple candidate orders, it chooses the one with the closest executed_at; if still ambiguous, it skips the row. """ from __future__ import annotations @@ -28,15 +28,15 @@ from app.utils.db import get_db_connection def _parse_date_to_ts(s: str) -> int: s = (s or "").strip() - # 支持 YYYY-MM-DD 或 YYYY/MM/DD + # Support YYYY-MM-DD or YYYY/MM/DD for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S"): try: dt = datetime.strptime(s, fmt) - # 服务器通常用本地时间写入 int(time.time());这里按本地时间解析 + # The server usually writes int(time.time()) in local time; parse it as local time here return int(dt.replace(tzinfo=None).timestamp()) except Exception: pass - raise ValueError(f"无法解析日期: {s}") + raise ValueError(f"Unable to parse date: {s}") def _fetch_bad_trades(strategy_id: int, since_ts: int, until_ts: int, limit: int) -> List[Dict[str, Any]]: @@ -95,7 +95,7 @@ def _find_best_order_match( cursor.close() if not cand: return None - # 若最接近的有并列(比如 executed_at 相同),认为不唯一,跳过以免误回填 + # If the closest candidates are tied, such as identical executed_at, treat it as ambiguous and skip it if len(cand) >= 2 and abs(int(cand[0]["executed_at"]) - trade_ts) == abs(int(cand[1]["executed_at"]) - trade_ts): return None return cand[0] @@ -128,11 +128,11 @@ def _update_trade( def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--strategy-id", type=int, required=True) - ap.add_argument("--since", type=str, required=True, help="YYYY-MM-DD 或 YYYY/MM/DD") - ap.add_argument("--until", type=str, required=True, help="YYYY-MM-DD 或 YYYY/MM/DD") - ap.add_argument("--window-sec", type=int, default=600, help="匹配窗口,默认±600秒") - ap.add_argument("--limit", type=int, default=500, help="最多处理多少条 trade") - ap.add_argument("--apply", action="store_true", help="真正写库;默认 dry-run 仅打印") + ap.add_argument("--since", type=str, required=True, help="YYYY-MM-DD or YYYY/MM/DD") + ap.add_argument("--until", type=str, required=True, help="YYYY-MM-DD or YYYY/MM/DD") + ap.add_argument("--window-sec", type=int, default=600, help="Match window, default ±600 seconds") + ap.add_argument("--limit", type=int, default=500, help="Maximum number of trades to process") + ap.add_argument("--apply", action="store_true", help="Actually write to the database; default is dry-run output only") args = ap.parse_args() since_ts = _parse_date_to_ts(args.since) diff --git a/backend_api_python/scripts/run_reflection_task.py b/backend_api_python/scripts/run_reflection_task.py index bb2842b..dce539e 100644 --- a/backend_api_python/scripts/run_reflection_task.py +++ b/backend_api_python/scripts/run_reflection_task.py @@ -2,8 +2,8 @@ import sys import os from dotenv import load_dotenv -# 添加后端目录到 Python 路径(使得可以 import app.*) -# 由于 app 包位于 backend_api_python/app 下,而脚本位于 backend_api_python/scripts +# Add the backend directory to the Python path so app.* can be imported +# The app package lives under backend_api_python/app while this script is under backend_api_python/scripts sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from app.services.reflection import ReflectionService @@ -19,8 +19,8 @@ def main(): load_dotenv(backend_env_path, override=False) """ - 运行自动反思验证任务 - 建议通过 cron 或 定时任务调度器 每天运行一次 + Run the automated reflection verification task. + It is intended to run once per day via cron or another task scheduler. """ print("Running Automated Reflection Verification Task...") service = ReflectionService() diff --git a/backend_api_python/start.sh b/backend_api_python/start.sh index c088464..3b67f66 100644 --- a/backend_api_python/start.sh +++ b/backend_api_python/start.sh @@ -1,26 +1,25 @@ #!/bin/bash -# QuantDinger Python API 启动脚本 +# QuantDinger Python API startup script -# 激活虚拟环境(如果使用虚拟环境) +# Activate the virtual environment if one is used # source venv/bin/activate -# 检查依赖是否安装 +# Check whether dependencies are installed if ! python -c "import flask" 2>/dev/null; then - echo "正在安装依赖..." + echo "Installing dependencies..." pip install -r requirements.txt fi -# 启动服务 -echo "启动 QuantDinger Python API 服务..." -echo "服务地址: http://0.0.0.0:5000" +# Start the service +echo "Starting QuantDinger Python API service..." +echo "Service address: http://0.0.0.0:5000" -# 创建日志目录 +# Create the log directory mkdir -p logs -# 开发环境(使用新的入口文件) +# Development environment (uses the new entry file) python run.py -# 生产环境(使用 gunicorn) +# Production environment (uses gunicorn) # gunicorn -w 4 -b 0.0.0.0:5000 --timeout 120 --access-logfile logs/access.log --error-logfile logs/error.log "run:create_app()" - diff --git a/frontend_vue/babel.config.js b/frontend_vue/babel.config.js index 4fe6229..8d93070 100644 --- a/frontend_vue/babel.config.js +++ b/frontend_vue/babel.config.js @@ -3,7 +3,7 @@ const IS_PREVIEW = process.env.VUE_APP_PREVIEW === 'true' const plugins = [] if (IS_PROD && !IS_PREVIEW) { - // 去除日志的插件, + // Plugin used to strip logs plugins.push('transform-remove-console') } @@ -12,7 +12,7 @@ if (IS_PROD && !IS_PREVIEW) { plugins.push(['import', { 'libraryName': 'ant-design-vue', 'libraryDirectory': 'es', - 'style': true // `style: true` 会加载 less 文件 + 'style': true // `style: true` loads the LESS files }]) module.exports = { diff --git a/frontend_vue/commitlint.config.js b/frontend_vue/commitlint.config.js index 7197cc6..9f99e34 100644 --- a/frontend_vue/commitlint.config.js +++ b/frontend_vue/commitlint.config.js @@ -1,15 +1,15 @@ /** - * feat:新增功能 - * fix:bug 修复 - * docs:文档更新 - * style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑) - * refactor:重构代码(既没有新增功能,也没有修复 bug) - * perf:性能, 体验优化 - * test:新增测试用例或是更新现有测试 - * build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交 - * ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交 - * chore:不属于以上类型的其他类型,比如构建流程, 依赖管理 - * revert:回滚某个更早之前的提交 + * feat: add a new feature + * fix: bug fix + * docs: documentation update + * style: code changes that do not affect program logic, such as whitespace, formatting, or missing semicolons + * refactor: code refactoring that neither adds a feature nor fixes a bug + * perf: performance or experience optimization + * test: add new test cases or update existing ones + * build: changes mainly related to the project build system, such as gulp, webpack, or rollup config + * ci: changes mainly related to the continuous integration flow, such as Travis, Jenkins, GitLab CI, or Circle + * chore: other changes that do not fit the categories above, such as build flow or dependency management + * revert: revert an earlier commit */ module.exports = { diff --git a/frontend_vue/config/plugin.config.js b/frontend_vue/config/plugin.config.js index bb51b98..a8bf93a 100644 --- a/frontend_vue/config/plugin.config.js +++ b/frontend_vue/config/plugin.config.js @@ -2,7 +2,7 @@ const ThemeColorReplacer = require('webpack-theme-color-replacer') const generate = require('@ant-design/colors/lib/generate').default const getAntdSerials = (color) => { - // 淡化(即less的tint) + // Lighten (the LESS tint equivalent) const lightens = new Array(9).fill().map((t, i) => { return ThemeColorReplacer.varyColor.lighten(color, i / 10) }) @@ -13,8 +13,8 @@ const getAntdSerials = (color) => { const themePluginOption = { fileName: 'css/theme-colors-[contenthash:8].css', - matchColors: getAntdSerials('#1890ff'), // 主色系列 - // 改变样式选择器,解决样式覆盖问题 + matchColors: getAntdSerials('#1890ff'), // Primary color palette + // Change the style selector to avoid style override issues changeSelector (selector) { switch (selector) { case '.ant-calendar-today .ant-calendar-date': diff --git a/frontend_vue/public/index.html b/frontend_vue/public/index.html index 3ecf915..8b8b263 100644 --- a/frontend_vue/public/index.html +++ b/frontend_vue/public/index.html @@ -37,7 +37,7 @@ max-width: 600px; } - /* 像素风格小猫奔跑动画容器 */ + /* Pixel-style running cat animation container */ .pixel-cat-container { position: relative; width: 100%; @@ -46,7 +46,7 @@ overflow: hidden; } - /* 像素小猫主体 */ + /* Pixel cat body */ .pixel-cat { position: absolute; left: 0; @@ -59,14 +59,14 @@ image-rendering: crisp-edges; } - /* 所有像素元素都使用锐利边缘 */ + /* All pixel elements use sharp edges */ .pixel-cat * { image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges; } - /* 猫头 - 像素方块组成 */ + /* Cat head - built from pixel blocks */ .cat-head { position: absolute; left: 2px; @@ -74,16 +74,16 @@ width: 16px; height: 14px; background: - /* 头部主体白色 */ + /* White base of the head */ linear-gradient(#fff, #fff) 2px 4px / 12px 10px no-repeat, - /* 左半脸黑色斑块 */ + /* Black patch on the left side of the face */ linear-gradient(#000, #000) 2px 4px / 6px 10px no-repeat, - /* 头顶 */ + /* Top of the head */ linear-gradient(#fff, #fff) 4px 2px / 8px 2px no-repeat; animation: headBob 0.4s steps(2) infinite; } - /* 左耳 - 黑色尖耳朵 */ + /* Left ear - black pointed ear */ .cat-ear-left { position: absolute; left: 0px; @@ -96,7 +96,7 @@ linear-gradient(#000, #000) 1px 0px / 2px 2px no-repeat; } - /* 右耳 - 白色尖耳朵 */ + /* Right ear - white pointed ear */ .cat-ear-right { position: absolute; right: 0px; @@ -120,7 +120,7 @@ box-sizing: border-box; } - /* 左眼 - 黑底白眼(在黑色区域) */ + /* Left eye - white eye on black background */ .cat-eye-left { position: absolute; left: 4px; @@ -140,7 +140,7 @@ background: #000; } - /* 右眼 - 白底黑眼(在白色区域) */ + /* Right eye - black eye on white background */ .cat-eye-right { position: absolute; right: 2px; @@ -162,7 +162,7 @@ background: #000; } - /* 鼻子 - 小粉色方块 */ + /* Nose - small pink square */ .cat-nose { position: absolute; left: 7px; @@ -172,7 +172,7 @@ background: #000; } - /* 胡须 - 像素线条 */ + /* Whiskers - pixel lines */ .cat-whiskers { position: absolute; left: 0; @@ -203,7 +203,7 @@ box-shadow: 0 2px 0 #000; } - /* 身体 - 黑白相间像素块 */ + /* Body - black and white pixel blocks */ .cat-body { position: absolute; left: 4px; @@ -211,15 +211,15 @@ width: 14px; height: 10px; background: - /* 白色部分 */ + /* White section */ linear-gradient(#fff, #fff) 6px 0 / 8px 10px no-repeat, - /* 黑色部分 */ + /* Black section */ linear-gradient(#000, #000) 0 0 / 8px 10px no-repeat; border: 1px solid #000; box-sizing: border-box; } - /* 前腿 - 左 (黑色) */ + /* Front leg - left, black */ .cat-leg-front-left { position: absolute; left: 6px; @@ -230,7 +230,7 @@ animation: legFront 0.2s steps(2) infinite; } - /* 前腿 - 右 (白色带边框) */ + /* Front leg - right, white with border */ .cat-leg-front-right { position: absolute; left: 12px; @@ -243,7 +243,7 @@ animation: legFront 0.2s steps(2) infinite 0.1s; } - /* 后腿 - 左 (白色带边框) */ + /* Back leg - left, white with border */ .cat-leg-back-left { position: absolute; left: 3px; @@ -256,7 +256,7 @@ animation: legBack 0.2s steps(2) infinite 0.1s; } - /* 后腿 - 右 (黑色) */ + /* Back leg - right, black */ .cat-leg-back-right { position: absolute; left: 15px; @@ -267,7 +267,7 @@ animation: legBack 0.2s steps(2) infinite; } - /* 尾巴 - 长且弯曲的像素尾巴 */ + /* Tail - a long curved pixel tail */ .cat-tail { position: absolute; right: -10px; @@ -275,55 +275,55 @@ width: 12px; height: 10px; background: - /* 尾巴根部 */ + /* Tail base */ linear-gradient(#000, #000) 0 6px / 3px 3px no-repeat, - /* 尾巴中部 */ + /* Middle of the tail */ linear-gradient(#000, #000) 3px 4px / 3px 3px no-repeat, - /* 尾巴弯曲 */ + /* Tail curve */ linear-gradient(#000, #000) 6px 2px / 3px 3px no-repeat, - /* 尾巴尖端 */ + /* Tail tip */ linear-gradient(#000, #000) 9px 0 / 3px 3px no-repeat; animation: tailWag 0.3s steps(2) infinite alternate; transform-origin: left bottom; } - /* 奔跑动画 - 轻微上下跳动 */ + /* Running animation - slight vertical bounce */ @keyframes catRun { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } } - /* 移动动画 - 左右移动 */ + /* Movement animation - move left and right */ @keyframes catMove { 0% { left: -40px; } 100% { left: calc(100% + 40px); } } - /* 前腿动画 */ + /* Front leg animation */ @keyframes legFront { 0%, 100% { transform: rotate(-15deg); } 50% { transform: rotate(15deg); } } - /* 后腿动画 */ + /* Back leg animation */ @keyframes legBack { 0%, 100% { transform: rotate(15deg); } 50% { transform: rotate(-15deg); } } - /* 尾巴摆动 */ + /* Tail swing */ @keyframes tailWag { 0% { transform: rotate(-10deg); } 100% { transform: rotate(10deg); } } - /* 头部轻微摆动 */ + /* Subtle head sway */ @keyframes headBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1px); } } - /* 地面效果 - 像素风格 */ + /* Ground effect - pixel style */ .ground { position: absolute; bottom: 0; @@ -346,7 +346,7 @@ 100% { background-position: 12px 0; } } - /* 品牌文字 */ + /* Brand text */ .brand-text { display: flex; justify-content: center; @@ -358,7 +358,7 @@ letter-spacing: 2px; } - /* 暗色主题适配 */ + /* Dark theme adaptation */ @media (prefers-color-scheme: dark) { .first-loading-wrp { background: #141414; @@ -382,7 +382,7 @@ ); } - /* 暗色模式下白色部分改成浅灰 */ + /* Use light gray instead of pure white in dark mode */ .cat-head { background: linear-gradient(#ddd, #ddd) 2px 4px / 12px 10px no-repeat, @@ -407,7 +407,7 @@ } } - /* 确保像素风格在所有浏览器中正确显示 */ + /* Ensure the pixel style renders correctly in all browsers */ .pixel-cat, .pixel-cat * { image-rendering: -moz-crisp-edges; @@ -416,7 +416,7 @@ image-rendering: crisp-edges; } - /* 手机端适配 */ + /* Mobile adaptation */ @media (max-width: 768px) { .pixel-cat-container { transform: scale(1.5); diff --git a/frontend_vue/src/api/ai-trading.js b/frontend_vue/src/api/ai-trading.js index 0961230..e80141f 100644 --- a/frontend_vue/src/api/ai-trading.js +++ b/frontend_vue/src/api/ai-trading.js @@ -13,7 +13,7 @@ const api = { } /** - * 获取AI交易策略列表 + * Get AI trading strategy list */ export function getStrategies () { return request({ @@ -23,7 +23,7 @@ export function getStrategies () { } /** - * 创建AI交易策略 + * Create AI trading strategy */ export function createAIStrategy (data) { return request({ @@ -34,7 +34,7 @@ export function createAIStrategy (data) { } /** - * 更新AI交易策略 + * Update AI trading strategy */ export function updateAIStrategy (data) { return request({ @@ -45,7 +45,7 @@ export function updateAIStrategy (data) { } /** - * 删除策略 + * Delete strategy */ export function deleteStrategy (strategyId) { return request({ @@ -56,7 +56,7 @@ export function deleteStrategy (strategyId) { } /** - * 启动策略 + * Start strategy */ export function startStrategy (strategyId) { return request({ @@ -67,7 +67,7 @@ export function startStrategy (strategyId) { } /** - * 停止策略 + * Stop strategy */ export function stopStrategy (strategyId) { return request({ @@ -78,7 +78,7 @@ export function stopStrategy (strategyId) { } /** - * 测试交易所连接 + * Test exchange connection */ export function testConnection (data) { return request({ @@ -89,7 +89,7 @@ export function testConnection (data) { } /** - * 获取AI决策记录 + * Get AI decision records */ export function getAIDecisions (strategyId, params) { return request({ @@ -103,7 +103,7 @@ export function getAIDecisions (strategyId, params) { } /** - * 获取系统支持的交易对列表 + * Get supported trading pairs */ export function getCryptoSymbols () { return request({ diff --git a/frontend_vue/src/api/market.js b/frontend_vue/src/api/market.js index 01ada51..93f77c8 100644 --- a/frontend_vue/src/api/market.js +++ b/frontend_vue/src/api/market.js @@ -28,7 +28,7 @@ const marketApi = { } /** - * 获取自选股列表 + * Get watchlist * @param parameter { userid: number } * @returns {*} */ @@ -41,7 +41,7 @@ export function getWatchlist (parameter) { } /** - * 添加自选股 + * Add watchlist item * @param parameter { userid: number, market: string, symbol: string } * @returns {*} */ @@ -54,7 +54,7 @@ export function addWatchlist (parameter) { } /** - * 删除自选股 + * Remove watchlist item * @param parameter { userid: number, symbol: string } * @returns {*} */ @@ -67,8 +67,8 @@ export function removeWatchlist (parameter) { } /** - * 获取自选股价格 - * @param parameter { watchlist: array } watchlist格式:[{market: 'USStock', symbol: 'AAPL'}, ...] + * Get watchlist prices + * @param parameter { watchlist: array } watchlist format: [{market: 'USStock', symbol: 'AAPL'}, ...] * @returns {*} */ export function getWatchlistPrices (parameter) { @@ -82,7 +82,7 @@ export function getWatchlistPrices (parameter) { } /** - * 发送 AI 聊天消息 + * Send AI chat message * @param parameter { userid: number, message: string, chatId?: string } * @returns {*} */ @@ -95,7 +95,7 @@ export function chatMessage (parameter) { } /** - * 获取聊天历史 + * Get chat history * @param parameter { userid: number } * @returns {*} */ @@ -108,7 +108,7 @@ export function getChatHistory (parameter) { } /** - * 保存聊天历史 + * Save chat history * @param parameter { userid: number, chatHistory: array } * @returns {*} */ @@ -121,7 +121,7 @@ export function saveChatHistory (parameter) { } /** - * 执行多维度分析 + * Run multi-dimensional analysis * @param parameter { userid: number, market: string, symbol: string } * @returns {*} */ @@ -135,7 +135,7 @@ export function multiAnalysis (parameter) { } /** - * 创建分析任务 + * Create analysis task * @param parameter { userid: number, market: string, symbol: string } * @returns {*} */ @@ -148,7 +148,7 @@ export function createAnalysisTask (parameter) { } /** - * 获取分析任务状态 + * Get analysis task status * @param parameter { task_id: number } * @returns {*} */ @@ -161,7 +161,7 @@ export function getAnalysisTaskStatus (parameter) { } /** - * 获取历史分析列表 + * Get analysis history list * @param parameter { userid: number, page?: number, pagesize?: number } * @returns {*} */ @@ -187,7 +187,7 @@ export function deleteAnalysisTask (parameter) { } /** - * 反思学习 + * Reflection learning * @param parameter { market: string, symbol: string, decision: string, returns?: number, result?: string } * @returns {*} */ @@ -200,7 +200,7 @@ export function reflectAnalysis (parameter) { } /** - * 获取插件配置 + * Get plugin configuration * @returns {*} */ export function getConfig () { @@ -211,7 +211,7 @@ export function getConfig () { } /** - * 获取菜单底部配置 + * Get footer menu configuration * @returns {*} */ export function getMenuFooterConfig () { @@ -222,7 +222,7 @@ export function getMenuFooterConfig () { } /** - * 获取股票类型列表 + * Get stock type list * @returns {*} */ export function getMarketTypes () { @@ -233,7 +233,7 @@ export function getMarketTypes () { } /** - * 搜索金融产品 + * Search financial instruments * @param parameter { market: string, keyword: string, limit?: number } * @returns {*} */ @@ -246,7 +246,7 @@ export function searchSymbols (parameter) { } /** - * 获取热门标的 + * Get popular instruments * @param parameter { market: string, limit?: number } * @returns {*} */ diff --git a/frontend_vue/src/api/settings.js b/frontend_vue/src/api/settings.js index f721e94..5538c5a 100644 --- a/frontend_vue/src/api/settings.js +++ b/frontend_vue/src/api/settings.js @@ -1,7 +1,7 @@ import request from '@/utils/request' /** - * 获取配置项定义 + * Get setting schema definitions */ export function getSettingsSchema () { return request({ @@ -11,7 +11,7 @@ export function getSettingsSchema () { } /** - * 获取当前配置值 + * Get current setting values */ export function getSettingsValues () { return request({ @@ -21,8 +21,8 @@ export function getSettingsValues () { } /** - * 保存配置 - * @param {Object} data - 配置数据 + * Save settings + * @param {Object} data - settings data */ export function saveSettings (data) { return request({ @@ -33,9 +33,9 @@ export function saveSettings (data) { } /** - * 测试API连接 - * @param {string} service - 服务名称 (openrouter, finnhub, etc.) - * @param {Object} params - 额外参数 + * Test API connection + * @param {string} service - service name (openrouter, finnhub, etc.) + * @param {Object} params - extra parameters */ export function testConnection (service, params = {}) { return request({ @@ -46,7 +46,7 @@ export function testConnection (service, params = {}) { } /** - * 查询 OpenRouter 账户余额 + * Query OpenRouter account balance */ export function getOpenRouterBalance () { return request({ diff --git a/frontend_vue/src/api/strategy.js b/frontend_vue/src/api/strategy.js index 5889ea5..ba597fb 100644 --- a/frontend_vue/src/api/strategy.js +++ b/frontend_vue/src/api/strategy.js @@ -17,6 +17,8 @@ const api = { batchStopStrategies: '/api/strategies/batch-stop', batchDeleteStrategies: '/api/strategies/batch-delete', testConnection: '/api/strategies/test-connection', + verifyCode: '/api/strategies/verify-code', + aiGenerate: '/api/strategies/ai-generate', trades: '/api/strategies/trades', positions: '/api/strategies/positions', equityCurve: '/api/strategies/equityCurve', @@ -235,6 +237,30 @@ export function testExchangeConnection (exchangeConfig) { }) } +/** + * Verify script strategy code. + * @param {string} code - Python strategy source. + */ +export function verifyStrategyCode (code) { + return request({ + url: api.verifyCode, + method: 'post', + data: { code } + }) +} + +/** + * Ask the backend LLM worker to generate script strategy code. + * @param {string} prompt - Natural language strategy description. + */ +export function aiGenerateStrategyCode (prompt) { + return request({ + url: api.aiGenerate, + method: 'post', + data: { prompt } + }) +} + /** * Get strategy trade records. * @param {number} id - Strategy ID. diff --git a/frontend_vue/src/assets/background.svg b/frontend_vue/src/assets/background.svg index 89c2597..f6ad926 100644 --- a/frontend_vue/src/assets/background.svg +++ b/frontend_vue/src/assets/background.svg @@ -5,7 +5,7 @@ Created with Sketch. - + @@ -66,4 +66,4 @@ - \ No newline at end of file + diff --git a/frontend_vue/src/components/ArticleListContent/ArticleListContent.vue b/frontend_vue/src/components/ArticleListContent/ArticleListContent.vue index 25a6820..ad91c78 100644 --- a/frontend_vue/src/components/ArticleListContent/ArticleListContent.vue +++ b/frontend_vue/src/components/ArticleListContent/ArticleListContent.vue @@ -7,7 +7,7 @@
- {{ owner }} 发布在 {{ href }} + {{ owner }} published on {{ href }} {{ updateAt | moment }}
diff --git a/frontend_vue/src/components/AvatarList/List.jsx b/frontend_vue/src/components/AvatarList/List.jsx index bff7092..5c5e449 100644 --- a/frontend_vue/src/components/AvatarList/List.jsx +++ b/frontend_vue/src/components/AvatarList/List.jsx @@ -6,7 +6,7 @@ import Item from './Item.jsx' import { filterEmpty } from '@/components/_util/util' /** - * size: `number`、 `large`、`small`、`default` 默认值: default + * size: `number`, `large`, `small`, `default`; default: default * maxLength: number * excessItemsStyle: CSSProperties */ diff --git a/frontend_vue/src/components/Charts/Bar.vue b/frontend_vue/src/components/Charts/Bar.vue index 4482845..286581e 100644 --- a/frontend_vue/src/components/Charts/Bar.vue +++ b/frontend_vue/src/components/Charts/Bar.vue @@ -35,7 +35,7 @@ export default { min: 2 }, { dataKey: 'y', - title: '时间', + title: 'Time', min: 1, max: 22 }] diff --git a/frontend_vue/src/components/Charts/MiniArea.vue b/frontend_vue/src/components/Charts/MiniArea.vue index 58fe92c..15c0f5f 100644 --- a/frontend_vue/src/components/Charts/MiniArea.vue +++ b/frontend_vue/src/components/Charts/MiniArea.vue @@ -33,7 +33,7 @@ const scale = [{ min: 2 }, { dataKey: 'y', - title: '时间', + title: 'Time', min: 1, max: 22 }] diff --git a/frontend_vue/src/components/Charts/MiniBar.vue b/frontend_vue/src/components/Charts/MiniBar.vue index beac404..090bd91 100644 --- a/frontend_vue/src/components/Charts/MiniBar.vue +++ b/frontend_vue/src/components/Charts/MiniBar.vue @@ -34,7 +34,7 @@ const scale = [{ min: 2 }, { dataKey: 'y', - title: '时间', + title: 'Time', min: 1, max: 30 }] diff --git a/frontend_vue/src/components/Charts/Radar.vue b/frontend_vue/src/components/Charts/Radar.vue index 5ee88ad..3187a66 100644 --- a/frontend_vue/src/components/Charts/Radar.vue +++ b/frontend_vue/src/components/Charts/Radar.vue @@ -41,7 +41,7 @@ const scale = [ max: 80 }, { dataKey: 'user', - alias: '类型' + alias: 'Type' } ] diff --git a/frontend_vue/src/components/Charts/TransferBar.vue b/frontend_vue/src/components/Charts/TransferBar.vue index 7f96f0b..5e8befa 100644 --- a/frontend_vue/src/components/Charts/TransferBar.vue +++ b/frontend_vue/src/components/Charts/TransferBar.vue @@ -24,13 +24,13 @@ const tooltip = [ ] const scale = [{ dataKey: 'x', - title: '日期(天)', - alias: '日期(天)', + title: 'Date (day)', + alias: 'Date (day)', min: 2 }, { dataKey: 'y', - title: '流量(Gb)', - alias: '流量(Gb)', + title: 'Traffic (Gb)', + alias: 'Traffic (Gb)', min: 1 }] diff --git a/frontend_vue/src/components/Editor/QuillEditor.vue b/frontend_vue/src/components/Editor/QuillEditor.vue index a9610ee..3391b37 100644 --- a/frontend_vue/src/components/Editor/QuillEditor.vue +++ b/frontend_vue/src/components/Editor/QuillEditor.vue @@ -30,7 +30,7 @@ export default { type: String, default: 'ant-editor-quill' }, - // 表单校验用字段 + // Field used for form validation // eslint-disable-next-line value: { type: String @@ -66,7 +66,7 @@ export default { diff --git a/frontend_vue/src/views/trading-assistant/components/StrategyTypeSelector.vue b/frontend_vue/src/views/trading-assistant/components/StrategyTypeSelector.vue new file mode 100644 index 0000000..069d4f0 --- /dev/null +++ b/frontend_vue/src/views/trading-assistant/components/StrategyTypeSelector.vue @@ -0,0 +1,61 @@ + + + diff --git a/frontend_vue/src/views/trading-assistant/index.vue b/frontend_vue/src/views/trading-assistant/index.vue index dd439de..51e29e8 100644 --- a/frontend_vue/src/views/trading-assistant/index.vue +++ b/frontend_vue/src/views/trading-assistant/index.vue @@ -1,6 +1,6 @@