From 5a4c770279a894717d9785a8169bb75e400d7058 Mon Sep 17 00:00:00 2001 From: TIANHE Date: Mon, 12 Jan 2026 00:15:52 +0800 Subject: [PATCH] feat: Refactor settings UI and fix commission fee recording Settings improvements: - Reorganize config groups with logical ordering (server, auth, ai, trading, etc.) - Add description/tooltip for each config item with question mark icon - Add icon to each group header - Support i18n for descriptions (zh-CN, zh-TW, en-US) - Move order execution config (ORDER_MODE, MAKER_WAIT_SEC, MAKER_OFFSET_BPS) to env Commission fee fixes: - Fix fee extraction in exchange clients: Bybit, Coinbase, Kraken, Gate, Kucoin, Bitfinex - Properly accumulate and record commission fees in pending_order_worker - Add fee/fee_ccy fields to wait_for_fill returns Frontend updates: - Remove order_mode config from trading-assistant frontend (now uses env config) - Add sorted schema display by order field - Add tooltip with description on hover --- backend_api_python/app/routes/settings.py | 790 +++++++++++++++--- .../app/services/live_trading/bitfinex.py | 11 +- .../app/services/live_trading/bybit.py | 16 +- .../live_trading/coinbase_exchange.py | 16 +- .../app/services/live_trading/gate.py | 30 +- .../app/services/live_trading/kraken.py | 16 +- .../services/live_trading/kraken_futures.py | 16 +- .../app/services/live_trading/kucoin.py | 16 +- .../app/services/pending_order_worker.py | 35 +- .../app/services/trading_executor.py | 13 +- backend_api_python/env.example | 16 + quantdinger_vue/src/locales/lang/en-US.js | 35 +- quantdinger_vue/src/locales/lang/zh-CN.js | 111 ++- quantdinger_vue/src/locales/lang/zh-TW.js | 110 ++- quantdinger_vue/src/views/settings/index.vue | 120 ++- .../src/views/trading-assistant/index.vue | 3 +- 16 files changed, 1121 insertions(+), 233 deletions(-) diff --git a/backend_api_python/app/routes/settings.py b/backend_api_python/app/routes/settings.py index 76e7c39..36e28f2 100644 --- a/backend_api_python/app/routes/settings.py +++ b/backend_api_python/app/routes/settings.py @@ -14,155 +14,687 @@ settings_bp = Blueprint('settings', __name__) # .env 文件路径 ENV_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env') -# 配置项定义(分组)- 完整对照 env.example +# 配置项定义(分组)- 按功能模块划分,每个配置项包含描述 CONFIG_SCHEMA = { - 'auth': { - 'title': '认证配置', - 'items': [ - {'key': 'SECRET_KEY', 'label': 'Secret Key', 'type': 'password', 'default': 'quantdinger-secret-key-change-me'}, - {'key': 'ADMIN_USER', 'label': '管理员用户名', 'type': 'text', 'default': 'quantdinger'}, - {'key': 'ADMIN_PASSWORD', 'label': '管理员密码', 'type': 'password', 'default': '123456'}, - ] - }, + # ==================== 1. 服务配置 ==================== 'server': { - 'title': '服务器配置', + 'title': 'Server Configuration', + 'icon': 'cloud-server', + 'order': 1, 'items': [ - {'key': 'PYTHON_API_HOST', 'label': '监听地址', 'type': 'text', 'default': '0.0.0.0'}, - {'key': 'PYTHON_API_PORT', 'label': '端口', 'type': 'number', 'default': '5000'}, - {'key': 'PYTHON_API_DEBUG', 'label': '调试模式', 'type': 'boolean', 'default': 'False'}, + { + 'key': 'PYTHON_API_HOST', + 'label': 'Listen Address', + 'type': 'text', + 'default': '0.0.0.0', + 'description': 'Server listen address. 0.0.0.0 allows external access, 127.0.0.1 for local only' + }, + { + 'key': 'PYTHON_API_PORT', + 'label': 'Port', + 'type': 'number', + 'default': '5000', + 'description': 'Server listen port, default 5000' + }, + { + 'key': 'PYTHON_API_DEBUG', + 'label': 'Debug Mode', + 'type': 'boolean', + 'default': 'False', + 'description': 'Enable debug mode for development. Disable in production' + }, ] }, - 'worker': { - 'title': '订单处理配置', + + # ==================== 2. 安全认证 ==================== + 'auth': { + 'title': 'Security & Authentication', + 'icon': 'lock', + 'order': 2, 'items': [ - {'key': 'ENABLE_PENDING_ORDER_WORKER', 'label': '启用订单处理Worker', 'type': 'boolean', 'default': 'True'}, - {'key': 'PENDING_ORDER_STALE_SEC', 'label': '订单超时时间(秒)', 'type': 'number', 'default': '90'}, - ] - }, - 'notification': { - 'title': '信号通知配置', - 'items': [ - {'key': 'SIGNAL_WEBHOOK_URL', 'label': 'Webhook URL', 'type': 'text', 'required': False}, - {'key': 'SIGNAL_WEBHOOK_TOKEN', 'label': 'Webhook Token', 'type': 'password', 'required': False}, - {'key': 'SIGNAL_NOTIFY_TIMEOUT_SEC', 'label': '通知超时(秒)', 'type': 'number', 'default': '6'}, - {'key': 'TELEGRAM_BOT_TOKEN', 'label': 'Telegram Bot Token', 'type': 'password', 'required': False, 'link': 'https://t.me/BotFather', 'link_text': 'settings.link.createBot'}, - ] - }, - 'smtp': { - 'title': '邮件SMTP配置', - 'items': [ - {'key': 'SMTP_HOST', 'label': 'SMTP服务器', 'type': 'text', 'required': False}, - {'key': 'SMTP_PORT', 'label': 'SMTP端口', 'type': 'number', 'default': '587'}, - {'key': 'SMTP_USER', 'label': 'SMTP用户名', 'type': 'text', 'required': False}, - {'key': 'SMTP_PASSWORD', 'label': 'SMTP密码', 'type': 'password', 'required': False}, - {'key': 'SMTP_FROM', 'label': '发件人地址', 'type': 'text', 'required': False}, - {'key': 'SMTP_USE_TLS', 'label': '使用TLS', 'type': 'boolean', 'default': 'True'}, - {'key': 'SMTP_USE_SSL', 'label': '使用SSL', 'type': 'boolean', 'default': 'False'}, - ] - }, - 'twilio': { - 'title': 'Twilio短信配置', - 'items': [ - {'key': 'TWILIO_ACCOUNT_SID', 'label': 'Account SID', 'type': 'password', 'required': False, 'link': 'https://console.twilio.com/', 'link_text': 'settings.link.getApi'}, - {'key': 'TWILIO_AUTH_TOKEN', 'label': 'Auth Token', 'type': 'password', 'required': False}, - {'key': 'TWILIO_FROM_NUMBER', 'label': '发送号码', 'type': 'text', 'required': False}, - ] - }, - 'strategy': { - 'title': '策略执行配置', - 'items': [ - {'key': 'DISABLE_RESTORE_RUNNING_STRATEGIES', 'label': '禁用自动恢复策略', 'type': 'boolean', 'default': 'False'}, - {'key': 'STRATEGY_TICK_INTERVAL_SEC', 'label': '策略Tick间隔(秒)', 'type': 'number', 'default': '10'}, - {'key': 'PRICE_CACHE_TTL_SEC', 'label': '价格缓存TTL(秒)', 'type': 'number', 'default': '10'}, - ] - }, - 'proxy': { - 'title': '代理配置', - 'items': [ - {'key': 'PROXY_PORT', 'label': '代理端口', 'type': 'text', 'required': False}, - {'key': 'PROXY_HOST', 'label': '代理主机', 'type': 'text', 'default': '127.0.0.1'}, - {'key': 'PROXY_SCHEME', 'label': '代理协议', 'type': 'select', 'options': ['socks5h', 'socks5', 'http', 'https'], 'default': 'socks5h'}, - {'key': 'PROXY_URL', 'label': '完整代理URL', 'type': 'text', 'required': False}, - ] - }, - 'app': { - 'title': '应用配置', - 'items': [ - {'key': 'CORS_ORIGINS', 'label': 'CORS来源', 'type': 'text', 'default': '*'}, - {'key': 'RATE_LIMIT', 'label': '速率限制(每分钟)', 'type': 'number', 'default': '100'}, - {'key': 'ENABLE_CACHE', 'label': '启用缓存', 'type': 'boolean', 'default': 'False'}, - {'key': 'ENABLE_REQUEST_LOG', 'label': '启用请求日志', 'type': 'boolean', 'default': 'True'}, - {'key': 'ENABLE_AI_ANALYSIS', 'label': '启用AI分析', 'type': 'boolean', 'default': 'True'}, - ] - }, - 'agent_memory': { - 'title': '记忆/反思配置', - 'items': [ - {'key': 'ENABLE_AGENT_MEMORY', 'label': '启用Agent记忆', 'type': 'boolean', 'default': 'True'}, - {'key': 'AGENT_MEMORY_ENABLE_VECTOR', 'label': '启用向量检索(本地)', 'type': 'boolean', 'default': 'True'}, - {'key': 'AGENT_MEMORY_EMBEDDING_DIM', 'label': 'Embedding维度', 'type': 'number', 'default': '256'}, - {'key': 'AGENT_MEMORY_TOP_K', 'label': '召回数量TopK', 'type': 'number', 'default': '5'}, - {'key': 'AGENT_MEMORY_CANDIDATE_LIMIT', 'label': '候选窗口大小', 'type': 'number', 'default': '500'}, - {'key': 'AGENT_MEMORY_HALF_LIFE_DAYS', 'label': '时间衰减半衰期(天)', 'type': 'number', 'default': '30'}, - {'key': 'AGENT_MEMORY_W_SIM', 'label': '相似度权重', 'type': 'number', 'default': '0.75'}, - {'key': 'AGENT_MEMORY_W_RECENCY', 'label': '时间权重', 'type': 'number', 'default': '0.20'}, - {'key': 'AGENT_MEMORY_W_RETURNS', 'label': '收益权重', 'type': 'number', 'default': '0.05'}, - ] - }, - 'reflection_worker': { - 'title': '自动反思验证Worker', - 'items': [ - {'key': 'ENABLE_REFLECTION_WORKER', 'label': '启用自动验证', 'type': 'boolean', 'default': 'False'}, - {'key': 'REFLECTION_WORKER_INTERVAL_SEC', 'label': '验证周期间隔(秒)', 'type': 'number', 'default': '86400'}, + { + 'key': 'SECRET_KEY', + 'label': 'Secret Key', + 'type': 'password', + 'default': 'quantdinger-secret-key-change-me', + 'description': 'JWT signing secret key. MUST change in production for security' + }, + { + 'key': 'ADMIN_USER', + 'label': 'Admin Username', + 'type': 'text', + 'default': 'quantdinger', + 'description': 'Administrator login username' + }, + { + 'key': 'ADMIN_PASSWORD', + 'label': 'Admin Password', + 'type': 'password', + 'default': '123456', + 'description': 'Administrator login password. MUST change in production' + }, ] }, + + # ==================== 3. AI/LLM 配置 ==================== 'ai': { - 'title': 'AI/LLM配置', + 'title': 'AI / LLM Configuration', + 'icon': 'robot', + 'order': 3, 'items': [ - {'key': 'OPENROUTER_API_KEY', 'label': 'OpenRouter API Key', 'type': 'password', 'required': False, 'link': 'https://openrouter.ai/keys', 'link_text': 'settings.link.getApiKey'}, - {'key': 'OPENROUTER_API_URL', 'label': 'OpenRouter API URL', 'type': 'text', 'default': 'https://openrouter.ai/api/v1/chat/completions'}, - {'key': 'OPENROUTER_MODEL', 'label': '默认模型', 'type': 'text', 'default': 'openai/gpt-4o', 'link': 'https://openrouter.ai/models', 'link_text': 'settings.link.viewModels'}, - {'key': 'OPENROUTER_TEMPERATURE', 'label': 'Temperature', 'type': 'number', 'default': '0.7'}, - {'key': 'OPENROUTER_MAX_TOKENS', 'label': 'Max Tokens', 'type': 'number', 'default': '4000'}, - {'key': 'OPENROUTER_TIMEOUT', 'label': '超时时间(秒)', 'type': 'number', 'default': '300'}, - {'key': 'OPENROUTER_CONNECT_TIMEOUT', 'label': '连接超时(秒)', 'type': 'number', 'default': '30'}, - {'key': 'AI_MODELS_JSON', 'label': '模型列表(JSON)', 'type': 'text', 'default': '{}', 'required': False}, + { + 'key': 'OPENROUTER_API_KEY', + 'label': 'OpenRouter API Key', + 'type': 'password', + 'required': False, + 'link': 'https://openrouter.ai/keys', + 'link_text': 'settings.link.getApiKey', + 'description': 'OpenRouter API key for AI model access. Supports multiple LLM providers' + }, + { + 'key': 'OPENROUTER_API_URL', + 'label': 'OpenRouter API URL', + 'type': 'text', + 'default': 'https://openrouter.ai/api/v1/chat/completions', + 'description': 'OpenRouter API endpoint URL' + }, + { + 'key': 'OPENROUTER_MODEL', + 'label': 'Default Model', + 'type': 'text', + 'default': 'openai/gpt-4o', + 'link': 'https://openrouter.ai/models', + 'link_text': 'settings.link.viewModels', + 'description': 'Default LLM model ID, e.g. openai/gpt-4o, anthropic/claude-3.5-sonnet' + }, + { + 'key': 'OPENROUTER_TEMPERATURE', + 'label': 'Temperature', + 'type': 'number', + 'default': '0.7', + 'description': 'Model creativity (0-1). Lower = more deterministic, Higher = more creative' + }, + { + 'key': 'OPENROUTER_MAX_TOKENS', + 'label': 'Max Tokens', + 'type': 'number', + 'default': '4000', + 'description': 'Maximum output tokens per request' + }, + { + 'key': 'OPENROUTER_TIMEOUT', + 'label': 'Request Timeout (sec)', + 'type': 'number', + 'default': '300', + 'description': 'API request timeout in seconds' + }, + { + 'key': 'OPENROUTER_CONNECT_TIMEOUT', + 'label': 'Connect Timeout (sec)', + 'type': 'number', + 'default': '30', + 'description': 'Connection establishment timeout in seconds' + }, + { + 'key': 'AI_MODELS_JSON', + 'label': 'Custom Models (JSON)', + 'type': 'text', + 'default': '{}', + 'required': False, + 'description': 'Custom model list in JSON format for model selector' + }, ] }, - 'market': { - 'title': '市场预设', + + # ==================== 4. 实盘交易 ==================== + 'trading': { + 'title': 'Live Trading', + 'icon': 'stock', + 'order': 4, 'items': [ - {'key': 'MARKET_TYPES_JSON', 'label': '市场类型(JSON)', 'type': 'text', 'default': '[]', 'required': False}, - {'key': 'TRADING_SUPPORTED_SYMBOLS_JSON', 'label': '支持的交易对(JSON)', 'type': 'text', 'default': '[]', 'required': False}, + { + 'key': 'ENABLE_PENDING_ORDER_WORKER', + 'label': 'Enable Order Worker', + 'type': 'boolean', + 'default': 'True', + 'description': 'Enable background order processing worker for live trading' + }, + { + 'key': 'PENDING_ORDER_STALE_SEC', + 'label': 'Order Stale Timeout (sec)', + 'type': 'number', + 'default': '90', + 'description': 'Mark pending order as stale after this many seconds' + }, + { + 'key': 'ORDER_MODE', + 'label': 'Order Execution Mode', + 'type': 'select', + 'options': ['maker', 'market'], + 'default': 'maker', + 'description': 'maker: Limit order first (lower fees), market: Market order (instant fill)' + }, + { + 'key': 'MAKER_WAIT_SEC', + 'label': 'Limit Order Wait (sec)', + 'type': 'number', + 'default': '10', + 'description': 'Wait time for limit order fill before switching to market order' + }, + { + 'key': 'MAKER_OFFSET_BPS', + 'label': 'Limit Order Offset (bps)', + 'type': 'number', + 'default': '2', + 'description': 'Price offset in basis points (1bps=0.01%). Buy: price*(1-offset), Sell: price*(1+offset)' + }, ] }, + + # ==================== 5. 策略执行 ==================== + 'strategy': { + 'title': 'Strategy Execution', + 'icon': 'fund', + 'order': 5, + 'items': [ + { + 'key': 'DISABLE_RESTORE_RUNNING_STRATEGIES', + 'label': 'Disable Auto Restore', + 'type': 'boolean', + 'default': 'False', + 'description': 'Disable automatic restore of running strategies on server restart' + }, + { + 'key': 'STRATEGY_TICK_INTERVAL_SEC', + 'label': 'Tick Interval (sec)', + 'type': 'number', + 'default': '10', + 'description': 'Strategy main loop tick interval in seconds' + }, + { + 'key': 'PRICE_CACHE_TTL_SEC', + 'label': 'Price Cache TTL (sec)', + 'type': 'number', + 'default': '10', + 'description': 'Time-to-live for cached price data in seconds' + }, + { + 'key': 'MARKET_TYPES_JSON', + 'label': 'Market Types (JSON)', + 'type': 'text', + 'default': '[]', + 'required': False, + 'description': 'Custom market type definitions in JSON format' + }, + { + 'key': 'TRADING_SUPPORTED_SYMBOLS_JSON', + 'label': 'Supported Symbols (JSON)', + 'type': 'text', + 'default': '[]', + 'required': False, + 'description': 'List of supported trading symbols in JSON format' + }, + ] + }, + + # ==================== 6. 数据源配置 ==================== 'data_source': { - 'title': '数据源配置', + 'title': 'Data Sources', + 'icon': 'database', + 'order': 6, 'items': [ - {'key': 'DATA_SOURCE_TIMEOUT', 'label': '数据源超时(秒)', 'type': 'number', 'default': '30'}, - {'key': 'DATA_SOURCE_RETRY', 'label': '重试次数', 'type': 'number', 'default': '3'}, - {'key': 'DATA_SOURCE_RETRY_BACKOFF', 'label': '重试退避(秒)', 'type': 'number', 'default': '0.5'}, - {'key': 'FINNHUB_API_KEY', 'label': 'Finnhub API Key', 'type': 'password', 'required': False, 'link': 'https://finnhub.io/register', 'link_text': 'settings.link.freeRegister'}, - {'key': 'FINNHUB_TIMEOUT', 'label': 'Finnhub超时(秒)', 'type': 'number', 'default': '10'}, - {'key': 'FINNHUB_RATE_LIMIT', 'label': 'Finnhub速率限制', 'type': 'number', 'default': '60'}, - {'key': 'CCXT_DEFAULT_EXCHANGE', 'label': 'CCXT默认交易所', 'type': 'text', 'default': 'coinbase', 'link': 'https://github.com/ccxt/ccxt#supported-cryptocurrency-exchange-markets', 'link_text': 'settings.link.supportedExchanges'}, - {'key': 'CCXT_TIMEOUT', 'label': 'CCXT超时(ms)', 'type': 'number', 'default': '10000'}, - {'key': 'CCXT_PROXY', 'label': 'CCXT代理', 'type': 'text', 'required': False}, - {'key': 'AKSHARE_TIMEOUT', 'label': 'Akshare超时(秒)', 'type': 'number', 'default': '30'}, - {'key': 'YFINANCE_TIMEOUT', 'label': 'YFinance超时(秒)', 'type': 'number', 'default': '30'}, - {'key': 'TIINGO_API_KEY', 'label': 'Tiingo API Key', 'type': 'password', 'required': False, 'link': 'https://www.tiingo.com/account/api/token', 'link_text': 'settings.link.getToken'}, - {'key': 'TIINGO_TIMEOUT', 'label': 'Tiingo超时(秒)', 'type': 'number', 'default': '10'}, + { + 'key': 'DATA_SOURCE_TIMEOUT', + 'label': 'Default Timeout (sec)', + 'type': 'number', + 'default': '30', + 'description': 'Default timeout for all data source requests' + }, + { + 'key': 'DATA_SOURCE_RETRY', + 'label': 'Retry Count', + 'type': 'number', + 'default': '3', + 'description': 'Number of retry attempts on data source failure' + }, + { + 'key': 'DATA_SOURCE_RETRY_BACKOFF', + 'label': 'Retry Backoff (sec)', + 'type': 'number', + 'default': '0.5', + 'description': 'Backoff time between retry attempts' + }, + { + 'key': 'CCXT_DEFAULT_EXCHANGE', + 'label': 'CCXT Default Exchange', + 'type': 'text', + 'default': 'coinbase', + 'link': 'https://github.com/ccxt/ccxt#supported-cryptocurrency-exchange-markets', + 'link_text': 'settings.link.supportedExchanges', + 'description': 'Default exchange for CCXT crypto data (binance, coinbase, okx, etc.)' + }, + { + 'key': 'CCXT_TIMEOUT', + 'label': 'CCXT Timeout (ms)', + 'type': 'number', + 'default': '10000', + 'description': 'CCXT request timeout in milliseconds' + }, + { + 'key': 'CCXT_PROXY', + 'label': 'CCXT Proxy', + 'type': 'text', + 'required': False, + 'description': 'Proxy URL for CCXT requests (e.g. socks5h://127.0.0.1:1080)' + }, + { + 'key': 'FINNHUB_API_KEY', + 'label': 'Finnhub API Key', + 'type': 'password', + 'required': False, + 'link': 'https://finnhub.io/register', + 'link_text': 'settings.link.freeRegister', + 'description': 'Finnhub API key for US stock data (free tier available)' + }, + { + 'key': 'FINNHUB_TIMEOUT', + 'label': 'Finnhub Timeout (sec)', + 'type': 'number', + 'default': '10', + 'description': 'Finnhub API request timeout' + }, + { + 'key': 'FINNHUB_RATE_LIMIT', + 'label': 'Finnhub Rate Limit', + 'type': 'number', + 'default': '60', + 'description': 'Finnhub API rate limit (requests per minute)' + }, + { + 'key': 'TIINGO_API_KEY', + 'label': 'Tiingo API Key', + 'type': 'password', + 'required': False, + 'link': 'https://www.tiingo.com/account/api/token', + 'link_text': 'settings.link.getToken', + 'description': 'Tiingo API key for US stock data (free tier available)' + }, + { + 'key': 'TIINGO_TIMEOUT', + 'label': 'Tiingo Timeout (sec)', + 'type': 'number', + 'default': '10', + 'description': 'Tiingo API request timeout' + }, + { + 'key': 'AKSHARE_TIMEOUT', + 'label': 'Akshare Timeout (sec)', + 'type': 'number', + 'default': '30', + 'description': 'Akshare API timeout for China A-share data' + }, + { + 'key': 'YFINANCE_TIMEOUT', + 'label': 'YFinance Timeout (sec)', + 'type': 'number', + 'default': '30', + 'description': 'Yahoo Finance API timeout' + }, ] }, - 'search': { - 'title': '搜索配置', + + # ==================== 7. 通知推送 ==================== + 'notification': { + 'title': 'Notifications', + 'icon': 'notification', + 'order': 7, 'items': [ - {'key': 'SEARCH_PROVIDER', 'label': '搜索提供商', 'type': 'select', 'options': ['google', 'bing', 'none'], 'default': 'google'}, - {'key': 'SEARCH_MAX_RESULTS', 'label': '最大结果数', 'type': 'number', 'default': '10'}, - {'key': 'SEARCH_GOOGLE_API_KEY', 'label': 'Google API Key', 'type': 'password', 'required': False, 'link': 'https://developers.google.com/custom-search/v1/introduction', 'link_text': 'settings.link.applyApi'}, - {'key': 'SEARCH_GOOGLE_CX', 'label': 'Google CX', 'type': 'text', 'required': False, 'link': 'https://programmablesearchengine.google.com/controlpanel/all', 'link_text': 'settings.link.createSearchEngine'}, - {'key': 'SEARCH_BING_API_KEY', 'label': 'Bing API Key', 'type': 'password', 'required': False, 'link': 'https://www.microsoft.com/en-us/bing/apis/bing-web-search-api', 'link_text': 'settings.link.applyApi'}, - {'key': 'INTERNAL_API_KEY', 'label': '内部API Key', 'type': 'password', 'required': False}, + { + 'key': 'SIGNAL_WEBHOOK_URL', + 'label': 'Webhook URL', + 'type': 'text', + 'required': False, + 'description': 'Custom webhook URL for signal notifications (POST JSON)' + }, + { + 'key': 'SIGNAL_WEBHOOK_TOKEN', + 'label': 'Webhook Token', + 'type': 'password', + 'required': False, + 'description': 'Authentication token sent in webhook header' + }, + { + 'key': 'SIGNAL_NOTIFY_TIMEOUT_SEC', + 'label': 'Notify Timeout (sec)', + 'type': 'number', + 'default': '6', + 'description': 'Notification request timeout' + }, + { + 'key': 'TELEGRAM_BOT_TOKEN', + 'label': 'Telegram Bot Token', + 'type': 'password', + 'required': False, + 'link': 'https://t.me/BotFather', + 'link_text': 'settings.link.createBot', + 'description': 'Telegram bot token from @BotFather for signal notifications' + }, + ] + }, + + # ==================== 8. 邮件配置 ==================== + 'email': { + 'title': 'Email (SMTP)', + 'icon': 'mail', + 'order': 8, + 'items': [ + { + 'key': 'SMTP_HOST', + 'label': 'SMTP Server', + 'type': 'text', + 'required': False, + 'description': 'SMTP server hostname (e.g. smtp.gmail.com)' + }, + { + 'key': 'SMTP_PORT', + 'label': 'SMTP Port', + 'type': 'number', + 'default': '587', + 'description': 'SMTP port (587 for TLS, 465 for SSL, 25 for plain)' + }, + { + 'key': 'SMTP_USER', + 'label': 'SMTP Username', + 'type': 'text', + 'required': False, + 'description': 'SMTP authentication username (usually email address)' + }, + { + 'key': 'SMTP_PASSWORD', + 'label': 'SMTP Password', + 'type': 'password', + 'required': False, + 'description': 'SMTP authentication password or app-specific password' + }, + { + 'key': 'SMTP_FROM', + 'label': 'Sender Address', + 'type': 'text', + 'required': False, + 'description': 'Email sender address (From header)' + }, + { + 'key': 'SMTP_USE_TLS', + 'label': 'Use TLS', + 'type': 'boolean', + 'default': 'True', + 'description': 'Enable STARTTLS encryption (recommended for port 587)' + }, + { + 'key': 'SMTP_USE_SSL', + 'label': 'Use SSL', + 'type': 'boolean', + 'default': 'False', + 'description': 'Enable SSL encryption (for port 465)' + }, + ] + }, + + # ==================== 9. 短信配置 ==================== + 'sms': { + 'title': 'SMS (Twilio)', + 'icon': 'phone', + 'order': 9, + 'items': [ + { + 'key': 'TWILIO_ACCOUNT_SID', + 'label': 'Account SID', + 'type': 'password', + 'required': False, + 'link': 'https://console.twilio.com/', + 'link_text': 'settings.link.getApi', + 'description': 'Twilio Account SID from console dashboard' + }, + { + 'key': 'TWILIO_AUTH_TOKEN', + 'label': 'Auth Token', + 'type': 'password', + 'required': False, + 'description': 'Twilio Auth Token from console dashboard' + }, + { + 'key': 'TWILIO_FROM_NUMBER', + 'label': 'Sender Number', + 'type': 'text', + 'required': False, + 'description': 'Twilio phone number for sending SMS (e.g. +1234567890)' + }, + ] + }, + + # ==================== 10. AI Agent 配置 ==================== + 'agent': { + 'title': 'AI Agent', + 'icon': 'experiment', + 'order': 10, + 'items': [ + { + 'key': 'ENABLE_AGENT_MEMORY', + 'label': 'Enable Agent Memory', + 'type': 'boolean', + 'default': 'True', + 'description': 'Enable AI agent memory for learning from past trades' + }, + { + 'key': 'AGENT_MEMORY_ENABLE_VECTOR', + 'label': 'Enable Vector Search', + 'type': 'boolean', + 'default': 'True', + 'description': 'Enable local vector similarity search for memory retrieval' + }, + { + 'key': 'AGENT_MEMORY_EMBEDDING_DIM', + 'label': 'Embedding Dimension', + 'type': 'number', + 'default': '256', + 'description': 'Vector embedding dimension for memory storage' + }, + { + 'key': 'AGENT_MEMORY_TOP_K', + 'label': 'Retrieval Top-K', + 'type': 'number', + 'default': '5', + 'description': 'Number of similar memories to retrieve' + }, + { + 'key': 'AGENT_MEMORY_CANDIDATE_LIMIT', + 'label': 'Candidate Limit', + 'type': 'number', + 'default': '500', + 'description': 'Maximum candidates for similarity search' + }, + { + 'key': 'AGENT_MEMORY_HALF_LIFE_DAYS', + 'label': 'Recency Half-life (days)', + 'type': 'number', + 'default': '30', + 'description': 'Time decay half-life for memory recency scoring' + }, + { + 'key': 'AGENT_MEMORY_W_SIM', + 'label': 'Similarity Weight', + 'type': 'number', + 'default': '0.75', + 'description': 'Weight for similarity score in memory ranking (0-1)' + }, + { + 'key': 'AGENT_MEMORY_W_RECENCY', + 'label': 'Recency Weight', + 'type': 'number', + 'default': '0.20', + 'description': 'Weight for recency score in memory ranking (0-1)' + }, + { + 'key': 'AGENT_MEMORY_W_RETURNS', + 'label': 'Returns Weight', + 'type': 'number', + 'default': '0.05', + 'description': 'Weight for returns score in memory ranking (0-1)' + }, + { + 'key': 'ENABLE_REFLECTION_WORKER', + 'label': 'Enable Auto Reflection', + 'type': 'boolean', + 'default': 'False', + 'description': 'Enable background worker for automatic trade reflection' + }, + { + 'key': 'REFLECTION_WORKER_INTERVAL_SEC', + 'label': 'Reflection Interval (sec)', + 'type': 'number', + 'default': '86400', + 'description': 'Interval between automatic reflection runs (default: 24h)' + }, + ] + }, + + # ==================== 11. 网络代理 ==================== + 'network': { + 'title': 'Network & Proxy', + 'icon': 'global', + 'order': 11, + 'items': [ + { + 'key': 'PROXY_HOST', + 'label': 'Proxy Host', + 'type': 'text', + 'default': '127.0.0.1', + 'description': 'Proxy server hostname or IP' + }, + { + 'key': 'PROXY_PORT', + 'label': 'Proxy Port', + 'type': 'text', + 'required': False, + 'description': 'Proxy server port (leave empty to disable proxy)' + }, + { + 'key': 'PROXY_SCHEME', + 'label': 'Proxy Protocol', + 'type': 'select', + 'options': ['socks5h', 'socks5', 'http', 'https'], + 'default': 'socks5h', + 'description': 'Proxy protocol type. socks5h: SOCKS5 with DNS resolution' + }, + { + 'key': 'PROXY_URL', + 'label': 'Full Proxy URL', + 'type': 'text', + 'required': False, + 'description': 'Complete proxy URL (overrides above settings if set)' + }, + ] + }, + + # ==================== 12. 搜索配置 ==================== + 'search': { + 'title': 'Web Search', + 'icon': 'search', + 'order': 12, + 'items': [ + { + 'key': 'SEARCH_PROVIDER', + 'label': 'Search Provider', + 'type': 'select', + 'options': ['google', 'bing', 'none'], + 'default': 'google', + 'description': 'Web search provider for AI research features' + }, + { + 'key': 'SEARCH_MAX_RESULTS', + 'label': 'Max Results', + 'type': 'number', + 'default': '10', + 'description': 'Maximum search results to return' + }, + { + 'key': 'SEARCH_GOOGLE_API_KEY', + 'label': 'Google API Key', + 'type': 'password', + 'required': False, + 'link': 'https://developers.google.com/custom-search/v1/introduction', + 'link_text': 'settings.link.applyApi', + 'description': 'Google Custom Search JSON API key' + }, + { + 'key': 'SEARCH_GOOGLE_CX', + 'label': 'Google Search Engine ID', + 'type': 'text', + 'required': False, + 'link': 'https://programmablesearchengine.google.com/controlpanel/all', + 'link_text': 'settings.link.createSearchEngine', + 'description': 'Google Programmable Search Engine ID (CX)' + }, + { + 'key': 'SEARCH_BING_API_KEY', + 'label': 'Bing API Key', + 'type': 'password', + 'required': False, + 'link': 'https://www.microsoft.com/en-us/bing/apis/bing-web-search-api', + 'link_text': 'settings.link.applyApi', + 'description': 'Microsoft Bing Web Search API key' + }, + { + 'key': 'INTERNAL_API_KEY', + 'label': 'Internal API Key', + 'type': 'password', + 'required': False, + 'description': 'Internal API authentication key for service-to-service calls' + }, + ] + }, + + # ==================== 13. 应用配置 ==================== + 'app': { + 'title': 'Application', + 'icon': 'appstore', + 'order': 13, + 'items': [ + { + 'key': 'CORS_ORIGINS', + 'label': 'CORS Origins', + 'type': 'text', + 'default': '*', + 'description': 'Allowed CORS origins (* for all, or comma-separated list)' + }, + { + 'key': 'RATE_LIMIT', + 'label': 'Rate Limit (req/min)', + 'type': 'number', + 'default': '100', + 'description': 'API rate limit per IP per minute' + }, + { + 'key': 'ENABLE_CACHE', + 'label': 'Enable Cache', + 'type': 'boolean', + 'default': 'False', + 'description': 'Enable response caching for improved performance' + }, + { + 'key': 'ENABLE_REQUEST_LOG', + 'label': 'Enable Request Log', + 'type': 'boolean', + 'default': 'True', + 'description': 'Log all API requests for debugging' + }, + { + 'key': 'ENABLE_AI_ANALYSIS', + 'label': 'Enable AI Analysis', + 'type': 'boolean', + 'default': 'True', + 'description': 'Enable AI-powered market analysis features' + }, ] }, } diff --git a/backend_api_python/app/services/live_trading/bitfinex.py b/backend_api_python/app/services/live_trading/bitfinex.py index d5c695f..46323af 100644 --- a/backend_api_python/app/services/live_trading/bitfinex.py +++ b/backend_api_python/app/services/live_trading/bitfinex.py @@ -234,8 +234,11 @@ class BitfinexDerivativesClient(BitfinexClient): last = last or [] filled = 0.0 avg_price = 0.0 + fee = 0.0 + fee_ccy = "" status = "" # best-effort parsing from array fields + # Bitfinex order response format: [ID, GID, CID, SYMBOL, MTS_CREATE, MTS_UPDATE, AMOUNT, AMOUNT_ORIG, TYPE, TYPE_PREV, MTS_TIF, _, FLAGS, STATUS, _, PRICE_AVG, ...] try: if isinstance(last, list) and len(last) >= 15: status = str(last[13] or "") @@ -245,12 +248,14 @@ class BitfinexDerivativesClient(BitfinexClient): avg_price = float(last[14] or 0.0) except Exception: pass + # Note: Bitfinex order response doesn't include fee; fee is typically in trades. + # We return 0.0 here; actual fee can be fetched via trades endpoint if needed. if filled > 0 and avg_price > 0: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if isinstance(status, str) and ("EXECUTED" in status.upper() or "CANCELED" in status.upper()): - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if time.time() >= end_ts: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} time.sleep(float(poll_interval_sec or 0.5)) diff --git a/backend_api_python/app/services/live_trading/bybit.py b/backend_api_python/app/services/live_trading/bybit.py index 001d161..5a59d31 100644 --- a/backend_api_python/app/services/live_trading/bybit.py +++ b/backend_api_python/app/services/live_trading/bybit.py @@ -315,12 +315,22 @@ class BybitClient(BaseRestClient): avg_price = float(last.get("avgPrice") or 0.0) except Exception: avg_price = 0.0 + # Extract fee from cumExecFee (Bybit API field for cumulative execution fee) + fee = 0.0 + fee_ccy = "" + try: + fee = abs(float(last.get("cumExecFee") or 0.0)) + except Exception: + fee = 0.0 + # Bybit linear contracts are settled in USDT + if fee > 0: + fee_ccy = "USDT" if filled > 0 and avg_price > 0: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if status.lower() in ("filled", "cancelled", "canceled", "rejected"): - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if time.time() >= end_ts: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} time.sleep(float(poll_interval_sec or 0.5)) def get_positions(self) -> Dict[str, Any]: diff --git a/backend_api_python/app/services/live_trading/coinbase_exchange.py b/backend_api_python/app/services/live_trading/coinbase_exchange.py index 74c566d..47dc383 100644 --- a/backend_api_python/app/services/live_trading/coinbase_exchange.py +++ b/backend_api_python/app/services/live_trading/coinbase_exchange.py @@ -170,6 +170,8 @@ class CoinbaseExchangeClient(BaseRestClient): status = str(last.get("status") or "") filled = 0.0 avg_price = 0.0 + fee = 0.0 + fee_ccy = "" try: filled = float(last.get("filled_size") or 0.0) except Exception: @@ -180,12 +182,20 @@ class CoinbaseExchangeClient(BaseRestClient): avg_price = executed_value / filled except Exception: avg_price = 0.0 + # Extract fee from fill_fees (Coinbase API field) + try: + fee = abs(float(last.get("fill_fees") or 0.0)) + except Exception: + fee = 0.0 + # Coinbase fees are typically in the quote currency (e.g., USD) + if fee > 0: + fee_ccy = "USD" if filled > 0 and avg_price > 0: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if status.lower() in ("done", "rejected", "canceled", "cancelled"): - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if time.time() >= end_ts: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} time.sleep(float(poll_interval_sec or 0.5)) diff --git a/backend_api_python/app/services/live_trading/gate.py b/backend_api_python/app/services/live_trading/gate.py index 02f59a5..6a8d594 100644 --- a/backend_api_python/app/services/live_trading/gate.py +++ b/backend_api_python/app/services/live_trading/gate.py @@ -134,6 +134,8 @@ class GateSpotClient(_GateBase): status = str(last.get("status") or "") filled = 0.0 avg_price = 0.0 + fee = 0.0 + fee_ccy = "" try: filled = float(last.get("filled_amount") or 0.0) except Exception: @@ -144,12 +146,18 @@ class GateSpotClient(_GateBase): avg_price = filled_total / filled except Exception: avg_price = 0.0 + # Extract fee from Gate API + try: + fee = abs(float(last.get("fee") or 0.0)) + except Exception: + fee = 0.0 + fee_ccy = str(last.get("fee_currency") or "").strip() if filled > 0 and avg_price > 0: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if status.lower() in ("closed", "cancelled", "canceled"): - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if time.time() >= end_ts: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} time.sleep(float(poll_interval_sec or 0.5)) @@ -321,6 +329,8 @@ class GateUsdtFuturesClient(_GateBase): status = str(last.get("status") or "") filled = 0.0 avg_price = 0.0 + fee = 0.0 + fee_ccy = "" try: # Gate futures often returns "filled_size" in contracts. filled_ct = abs(float(last.get("filled_size") or last.get("filledSize") or 0.0)) @@ -331,12 +341,20 @@ class GateUsdtFuturesClient(_GateBase): avg_price = float(last.get("fill_price") or last.get("fillPrice") or last.get("price") or 0.0) except Exception: avg_price = 0.0 + # Extract fee from Gate Futures API + try: + fee = abs(float(last.get("fee") or 0.0)) + except Exception: + fee = 0.0 + # Gate USDT futures fees are in USDT + if fee > 0: + fee_ccy = "USDT" if filled > 0 and avg_price > 0: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if str(status).lower() in ("finished", "cancelled", "canceled"): - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if time.time() >= end_ts: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} time.sleep(float(poll_interval_sec or 0.5)) diff --git a/backend_api_python/app/services/live_trading/kraken.py b/backend_api_python/app/services/live_trading/kraken.py index 28e66fe..8d25401 100644 --- a/backend_api_python/app/services/live_trading/kraken.py +++ b/backend_api_python/app/services/live_trading/kraken.py @@ -153,6 +153,8 @@ class KrakenClient(BaseRestClient): status = str(last.get("status") or "") filled = 0.0 avg_price = 0.0 + fee = 0.0 + fee_ccy = "" try: filled = float(last.get("vol_exec") or 0.0) except Exception: @@ -164,12 +166,20 @@ class KrakenClient(BaseRestClient): avg_price = cost / filled except Exception: avg_price = 0.0 + # Extract fee from Kraken API + try: + fee = abs(float(last.get("fee") or 0.0)) + except Exception: + fee = 0.0 + # Kraken fees are typically in the quote currency (e.g., USD) + if fee > 0: + fee_ccy = "USD" if filled > 0 and avg_price > 0: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if status.lower() in ("closed", "canceled", "cancelled", "expired"): - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if time.time() >= end_ts: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} time.sleep(float(poll_interval_sec or 0.5)) diff --git a/backend_api_python/app/services/live_trading/kraken_futures.py b/backend_api_python/app/services/live_trading/kraken_futures.py index ddfaeb0..e0c6728 100644 --- a/backend_api_python/app/services/live_trading/kraken_futures.py +++ b/backend_api_python/app/services/live_trading/kraken_futures.py @@ -186,6 +186,8 @@ class KrakenFuturesClient(BaseRestClient): status = str(last.get("status") or last.get("orderStatus") or "") filled = 0.0 avg_price = 0.0 + fee = 0.0 + fee_ccy = "" try: filled = float(last.get("filledSize") or last.get("filled_size") or 0.0) except Exception: @@ -194,12 +196,20 @@ class KrakenFuturesClient(BaseRestClient): avg_price = float(last.get("avgFillPrice") or last.get("avg_fill_price") or 0.0) except Exception: avg_price = 0.0 + # Extract fee from Kraken Futures API (if available) + try: + fee = abs(float(last.get("fee") or 0.0)) + except Exception: + fee = 0.0 + # Kraken Futures fees are typically in USD + if fee > 0: + fee_ccy = "USD" if filled > 0 and avg_price > 0: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if status.lower() in ("filled", "cancelled", "canceled", "rejected"): - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if time.time() >= end_ts: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} time.sleep(float(poll_interval_sec or 0.5)) diff --git a/backend_api_python/app/services/live_trading/kucoin.py b/backend_api_python/app/services/live_trading/kucoin.py index 1bad9a7..c66b91d 100644 --- a/backend_api_python/app/services/live_trading/kucoin.py +++ b/backend_api_python/app/services/live_trading/kucoin.py @@ -475,6 +475,8 @@ class KucoinFuturesClient(BaseRestClient): status = str(od.get("status") or "") filled = 0.0 avg_price = 0.0 + fee = 0.0 + fee_ccy = "" try: # dealSize is in contracts; convert back to base using multiplier best-effort. deal_ct = float(od.get("dealSize") or 0.0) @@ -497,12 +499,20 @@ class KucoinFuturesClient(BaseRestClient): filled = abs(float(deal_ct or 0.0)) * float(mult) if filled > 0 and deal_value > 0: avg_price = float(deal_value) / float(filled) + # Extract fee from KuCoin Futures API (orderMargin contains fee info in some cases) + try: + fee = abs(float(od.get("fee") or od.get("orderFee") or 0.0)) + except Exception: + fee = 0.0 + # KuCoin Futures fees are typically in USDT + if fee > 0: + fee_ccy = "USDT" if filled > 0 and avg_price > 0: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if status.lower() in ("done", "canceled", "cancelled", "filled"): - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} if time.time() >= end_ts: - return {"filled": filled, "avg_price": avg_price, "status": status, "order": last} + return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last} time.sleep(float(poll_interval_sec or 0.5)) diff --git a/backend_api_python/app/services/pending_order_worker.py b/backend_api_python/app/services/pending_order_worker.py index 0318924..fb3cd42 100644 --- a/backend_api_python/app/services/pending_order_worker.py +++ b/backend_api_python/app/services/pending_order_worker.py @@ -12,7 +12,7 @@ import json import os import threading import time -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from app.services.signal_notifier import SignalNotifier from app.services.exchange_execution import load_strategy_configs, resolve_exchange_config, safe_exchange_config_for_log @@ -722,12 +722,17 @@ class PendingOrderWorker: _notify_live_best_effort(status="failed", error="spot_market_does_not_support_short_signals") return - # Unified maker->market fallback settings (defaults: 10 seconds) - order_mode = str(payload.get("order_mode") or payload.get("orderMode") or "maker").strip().lower() - maker_wait_sec = float(payload.get("maker_wait_sec") or payload.get("makerWaitSec") or 10.0) - maker_offset_bps = float(payload.get("maker_offset_bps") or payload.get("makerOffsetBps") or 2.0) + # Unified maker->market fallback settings + # Priority: payload config > environment variable > default value + _default_order_mode = os.getenv("ORDER_MODE", "maker").strip().lower() + _default_maker_wait_sec = float(os.getenv("MAKER_WAIT_SEC", "10")) + _default_maker_offset_bps = float(os.getenv("MAKER_OFFSET_BPS", "2")) + + order_mode = str(payload.get("order_mode") or payload.get("orderMode") or _default_order_mode).strip().lower() + maker_wait_sec = float(payload.get("maker_wait_sec") or payload.get("makerWaitSec") or _default_maker_wait_sec) + maker_offset_bps = float(payload.get("maker_offset_bps") or payload.get("makerOffsetBps") or _default_maker_offset_bps) if maker_wait_sec <= 0: - maker_wait_sec = 10.0 + maker_wait_sec = _default_maker_wait_sec if _default_maker_wait_sec > 0 else 10.0 if maker_offset_bps < 0: maker_offset_bps = 0.0 maker_offset = maker_offset_bps / 10000.0 @@ -1070,18 +1075,22 @@ class PendingOrderWorker: q = client.wait_for_fill(symbol=str(symbol), order_id=limit_order_id, client_order_id=limit_client_oid, max_wait_sec=maker_wait_sec) phases["limit_query"] = q _apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) + _apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or "")) elif isinstance(client, CoinbaseExchangeClient): q = client.wait_for_fill(order_id=limit_order_id, client_order_id=limit_client_oid, max_wait_sec=maker_wait_sec) phases["limit_query"] = q _apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) + _apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or "")) elif isinstance(client, KrakenClient): q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) phases["limit_query"] = q _apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) + _apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or "")) elif isinstance(client, KrakenFuturesClient): q = client.wait_for_fill(order_id=limit_order_id, client_order_id=limit_client_oid, max_wait_sec=maker_wait_sec) phases["limit_query"] = q _apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) + _apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or "")) elif isinstance(client, KucoinSpotClient): q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) phases["limit_query"] = q @@ -1091,22 +1100,27 @@ class PendingOrderWorker: q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) phases["limit_query"] = q _apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) + _apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or "")) elif isinstance(client, GateSpotClient): q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) phases["limit_query"] = q _apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) + _apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or "")) elif isinstance(client, GateUsdtFuturesClient): q = client.wait_for_fill(order_id=limit_order_id, contract=to_gate_currency_pair(str(symbol)), max_wait_sec=maker_wait_sec) phases["limit_query"] = q _apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) + _apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or "")) elif isinstance(client, BitfinexClient): q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) phases["limit_query"] = q _apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) + _apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or "")) elif isinstance(client, BitfinexDerivativesClient): q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) phases["limit_query"] = q _apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) + _apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or "")) remaining = max(0.0, float(amount or 0.0) - total_base) @@ -1386,18 +1400,22 @@ class PendingOrderWorker: q2 = client.wait_for_fill(symbol=str(symbol), order_id=market_order_id, client_order_id=market_client_oid, max_wait_sec=3.0) phases["market_query"] = q2 _apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) + _apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or "")) elif isinstance(client, CoinbaseExchangeClient): q2 = client.wait_for_fill(order_id=market_order_id, client_order_id=market_client_oid, max_wait_sec=3.0) phases["market_query"] = q2 _apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) + _apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or "")) elif isinstance(client, KrakenClient): q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) phases["market_query"] = q2 _apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) + _apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or "")) elif isinstance(client, KrakenFuturesClient): q2 = client.wait_for_fill(order_id=market_order_id, client_order_id=market_client_oid, max_wait_sec=3.0) phases["market_query"] = q2 _apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) + _apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or "")) elif isinstance(client, KucoinSpotClient): q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) phases["market_query"] = q2 @@ -1407,22 +1425,27 @@ class PendingOrderWorker: q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) phases["market_query"] = q2 _apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) + _apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or "")) elif isinstance(client, GateSpotClient): q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) phases["market_query"] = q2 _apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) + _apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or "")) elif isinstance(client, GateUsdtFuturesClient): q2 = client.wait_for_fill(order_id=market_order_id, contract=to_gate_currency_pair(str(symbol)), max_wait_sec=3.0) phases["market_query"] = q2 _apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) + _apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or "")) elif isinstance(client, BitfinexClient): q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) phases["market_query"] = q2 _apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) + _apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or "")) elif isinstance(client, BitfinexDerivativesClient): q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) phases["market_query"] = q2 _apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) + _apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or "")) except LiveTradingError as e: logger.warning(f"live market phase failed: pending_id={order_id}, strategy_id={strategy_id}, cfg={safe_cfg}, err={e}") phases["market_error"] = str(e) diff --git a/backend_api_python/app/services/trading_executor.py b/backend_api_python/app/services/trading_executor.py index 8cebb52..c701b95 100644 --- a/backend_api_python/app/services/trading_executor.py +++ b/backend_api_python/app/services/trading_executor.py @@ -2221,8 +2221,11 @@ class TradingExecutor: margin_mode: str = 'cross', stop_loss_price: float = None, take_profit_price: float = None, - order_mode: str = 'maker', - maker_wait_sec: float = 8.0, + # Order execution params (order_mode, maker_wait_sec, maker_offset_bps) are now + # configured via environment variables: ORDER_MODE, MAKER_WAIT_SEC, MAKER_OFFSET_BPS + # These parameters are kept for backward compatibility but will be ignored. + order_mode: str = None, + maker_wait_sec: float = None, maker_retries: int = 3, close_fallback_to_market: bool = True, open_fallback_to_market: bool = True, @@ -2236,6 +2239,9 @@ class TradingExecutor: A separate worker will poll `pending_orders` and dispatch: - execution_mode='signal': dispatch notifications (no real trading). - execution_mode='live': reserved for future live trading execution (not implemented). + + Note: Order execution settings (order_mode, maker_wait_sec, maker_offset_bps) are now + configured via environment variables and not passed from strategy config. """ try: # Reference price at enqueue time: use current tick price if provided to avoid extra fetch. @@ -2249,8 +2255,7 @@ class TradingExecutor: "stop_loss_price": float(stop_loss_price or 0.0) if stop_loss_price is not None else 0.0, "take_profit_price": float(take_profit_price or 0.0) if take_profit_price is not None else 0.0, "margin_mode": str(margin_mode or "cross"), - "order_mode": str(order_mode or "maker"), - "maker_wait_sec": float(maker_wait_sec or 0.0), + # Order execution params moved to env config (ORDER_MODE, MAKER_WAIT_SEC, MAKER_OFFSET_BPS) "maker_retries": int(maker_retries or 0), "close_fallback_to_market": bool(close_fallback_to_market), "open_fallback_to_market": bool(open_fallback_to_market), diff --git a/backend_api_python/env.example b/backend_api_python/env.example index 251fa15..68dbe56 100644 --- a/backend_api_python/env.example +++ b/backend_api_python/env.example @@ -34,6 +34,22 @@ ENABLE_PENDING_ORDER_WORKER=true # Reclaim orders stuck in status=processing after worker crashes (seconds). PENDING_ORDER_STALE_SEC=90 +# ========================= +# Live trading order execution settings +# ========================= +# Order execution mode: +# - "maker": Limit order first, then market order for remaining (default, lower fees) +# - "market": Market order only (immediate execution, higher fees) +ORDER_MODE=maker + +# How long to wait for limit order to fill before switching to market order (seconds) +MAKER_WAIT_SEC=10 + +# Price offset for limit orders in basis points (1 bps = 0.01%) +# Buy orders: price = market_price * (1 - offset) +# Sell orders: price = market_price * (1 + offset) +MAKER_OFFSET_BPS=2 + # ========================= # Strategy signal notifications (optional) # ========================= diff --git a/quantdinger_vue/src/locales/lang/en-US.js b/quantdinger_vue/src/locales/lang/en-US.js index f9681d4..ec148db 100644 --- a/quantdinger_vue/src/locales/lang/en-US.js +++ b/quantdinger_vue/src/locales/lang/en-US.js @@ -1911,21 +1911,20 @@ const locale = { 'settings.copySuccess': 'Copied', 'settings.copyFailed': 'Copy failed', // Settings groups - 'settings.group.auth': 'Authentication', + // Settings groups (ordered by backend order) 'settings.group.server': 'Server Configuration', - 'settings.group.worker': 'Order Worker', - 'settings.group.notification': 'Signal Notification', - 'settings.group.smtp': 'Email SMTP', - 'settings.group.twilio': 'Twilio SMS', + 'settings.group.auth': 'Security & Authentication', + 'settings.group.ai': 'AI / LLM Configuration', + 'settings.group.trading': 'Live Trading', 'settings.group.strategy': 'Strategy Execution', - 'settings.group.proxy': 'Proxy Configuration', - 'settings.group.app': 'Application', - 'settings.group.ai': 'AI/LLM Configuration', - 'settings.group.market': 'Market Presets', 'settings.group.data_source': 'Data Sources', - 'settings.group.search': 'Search Configuration', - 'settings.group.agent_memory': 'Memory/Reflection', - 'settings.group.reflection_worker': 'Auto Reflection Verification Worker', + 'settings.group.notification': 'Notifications', + 'settings.group.email': 'Email (SMTP)', + 'settings.group.sms': 'SMS (Twilio)', + 'settings.group.agent': 'AI Agent', + 'settings.group.network': 'Network & Proxy', + 'settings.group.search': 'Web Search', + 'settings.group.app': 'Application', // Settings fields - Auth 'settings.field.SECRET_KEY': 'Secret Key', 'settings.field.ADMIN_USER': 'Admin Username', @@ -1937,6 +1936,10 @@ const locale = { // Settings fields - Worker 'settings.field.ENABLE_PENDING_ORDER_WORKER': 'Enable Order Worker', 'settings.field.PENDING_ORDER_STALE_SEC': 'Order Stale Timeout (sec)', + // Settings fields - Trading + 'settings.field.ORDER_MODE': 'Order Mode', + 'settings.field.MAKER_WAIT_SEC': 'Limit Order Wait Time (sec)', + 'settings.field.MAKER_OFFSET_BPS': 'Limit Order Price Offset (bps)', // Settings fields - Notification 'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL', 'settings.field.SIGNAL_WEBHOOK_TOKEN': 'Webhook Token', @@ -2014,7 +2017,13 @@ const locale = { 'settings.field.SEARCH_GOOGLE_API_KEY': 'Google API Key', 'settings.field.SEARCH_GOOGLE_CX': 'Google CX', 'settings.field.SEARCH_BING_API_KEY': 'Bing API Key', - 'settings.field.INTERNAL_API_KEY': 'Internal API Key' + 'settings.field.INTERNAL_API_KEY': 'Internal API Key', + + // Settings descriptions (config item descriptions) + // Note: These are optional since backend already provides English descriptions + 'settings.desc.ORDER_MODE': 'maker: Limit order first (lower fees), market: Market order (instant fill)', + 'settings.desc.MAKER_WAIT_SEC': 'Wait time for limit order fill before switching to market order', + 'settings.desc.MAKER_OFFSET_BPS': 'Price offset in basis points. Buy: price*(1-offset), Sell: price*(1+offset)' } export default { diff --git a/quantdinger_vue/src/locales/lang/zh-CN.js b/quantdinger_vue/src/locales/lang/zh-CN.js index d8944d1..0007973 100644 --- a/quantdinger_vue/src/locales/lang/zh-CN.js +++ b/quantdinger_vue/src/locales/lang/zh-CN.js @@ -1649,22 +1649,20 @@ const locale = { 'settings.copyRestartCmd': '复制重启命令', 'settings.copySuccess': '复制成功', 'settings.copyFailed': '复制失败', -// Settings groups -'settings.group.auth': '认证配置', -'settings.group.server': '服务器配置', -'settings.group.worker': '订单处理配置', -'settings.group.notification': '信号通知配置', -'settings.group.smtp': '邮件SMTP配置', -'settings.group.twilio': 'Twilio短信配置', -'settings.group.strategy': '策略执行配置', -'settings.group.proxy': '代理配置', -'settings.group.app': '应用配置', +// Settings groups (按后端 order 排序) +'settings.group.server': '服务配置', +'settings.group.auth': '安全认证', 'settings.group.ai': 'AI/LLM配置', -'settings.group.market': '市场预设', -'settings.group.data_source': '数据源配置', +'settings.group.trading': '实盘交易', +'settings.group.strategy': '策略执行', +'settings.group.data_source': '数据源', +'settings.group.notification': '通知推送', +'settings.group.email': '邮件配置', +'settings.group.sms': '短信配置', +'settings.group.agent': 'AI Agent', +'settings.group.network': '网络代理', 'settings.group.search': '搜索配置', -'settings.group.agent_memory': '记忆/反思配置', -'settings.group.reflection_worker': '自动反思验证Worker', +'settings.group.app': '应用配置', // Settings fields - Auth 'settings.field.SECRET_KEY': 'Secret Key', 'settings.field.ADMIN_USER': '管理员用户名', @@ -1676,6 +1674,10 @@ const locale = { // Settings fields - Worker 'settings.field.ENABLE_PENDING_ORDER_WORKER': '启用订单处理Worker', 'settings.field.PENDING_ORDER_STALE_SEC': '订单超时时间(秒)', +// Settings fields - Trading +'settings.field.ORDER_MODE': '下单模式', +'settings.field.MAKER_WAIT_SEC': '限价单等待时间(秒)', +'settings.field.MAKER_OFFSET_BPS': '限价单价格偏移(基点)', // Settings fields - Notification 'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL', 'settings.field.SIGNAL_WEBHOOK_TOKEN': 'Webhook Token', @@ -1753,7 +1755,86 @@ const locale = { 'settings.field.SEARCH_GOOGLE_API_KEY': 'Google API Key', 'settings.field.SEARCH_GOOGLE_CX': 'Google CX', 'settings.field.SEARCH_BING_API_KEY': 'Bing API Key', -'settings.field.INTERNAL_API_KEY': '内部API Key' +'settings.field.INTERNAL_API_KEY': '内部API Key', + +// Settings descriptions (配置项说明) +'settings.desc.PYTHON_API_HOST': '服务监听地址。0.0.0.0 允许外部访问,127.0.0.1 仅本地访问', +'settings.desc.PYTHON_API_PORT': '服务监听端口,默认5000', +'settings.desc.PYTHON_API_DEBUG': '启用调试模式,开发时使用。生产环境请关闭', +'settings.desc.SECRET_KEY': 'JWT签名密钥。生产环境必须修改以确保安全', +'settings.desc.ADMIN_USER': '管理员登录用户名', +'settings.desc.ADMIN_PASSWORD': '管理员登录密码。生产环境必须修改', +'settings.desc.OPENROUTER_API_KEY': 'OpenRouter API密钥,用于访问多种AI模型', +'settings.desc.OPENROUTER_API_URL': 'OpenRouter API端点地址', +'settings.desc.OPENROUTER_MODEL': '默认使用的AI模型,如 openai/gpt-4o, anthropic/claude-3.5-sonnet', +'settings.desc.OPENROUTER_TEMPERATURE': '模型创造性(0-1)。越低越确定性,越高越有创意', +'settings.desc.OPENROUTER_MAX_TOKENS': '每次请求最大输出token数', +'settings.desc.OPENROUTER_TIMEOUT': 'API请求超时时间(秒)', +'settings.desc.OPENROUTER_CONNECT_TIMEOUT': '连接建立超时时间(秒)', +'settings.desc.AI_MODELS_JSON': '自定义模型列表,JSON格式,用于模型选择器', +'settings.desc.ENABLE_PENDING_ORDER_WORKER': '启用后台订单处理Worker,实盘交易必需', +'settings.desc.PENDING_ORDER_STALE_SEC': '等待订单超时时间,超时后标记为过期', +'settings.desc.ORDER_MODE': 'maker: 限价单优先(手续费低),market: 市价单(立即成交)', +'settings.desc.MAKER_WAIT_SEC': '限价单等待成交时间,超时后自动切换为市价单', +'settings.desc.MAKER_OFFSET_BPS': '限价单价格偏移(基点)。买入价=市价*(1-偏移),卖出价=市价*(1+偏移)', +'settings.desc.DISABLE_RESTORE_RUNNING_STRATEGIES': '禁止服务重启时自动恢复运行中的策略', +'settings.desc.STRATEGY_TICK_INTERVAL_SEC': '策略主循环检查间隔(秒)', +'settings.desc.PRICE_CACHE_TTL_SEC': '价格数据缓存有效期(秒)', +'settings.desc.MARKET_TYPES_JSON': '自定义市场类型配置,JSON格式', +'settings.desc.TRADING_SUPPORTED_SYMBOLS_JSON': '支持的交易对列表,JSON格式', +'settings.desc.DATA_SOURCE_TIMEOUT': '数据源请求默认超时时间', +'settings.desc.DATA_SOURCE_RETRY': '数据源请求失败时的重试次数', +'settings.desc.DATA_SOURCE_RETRY_BACKOFF': '重试间隔时间(秒)', +'settings.desc.CCXT_DEFAULT_EXCHANGE': 'CCXT默认交易所(binance, coinbase, okx等)', +'settings.desc.CCXT_TIMEOUT': 'CCXT请求超时时间(毫秒)', +'settings.desc.CCXT_PROXY': 'CCXT请求代理地址(如 socks5h://127.0.0.1:1080)', +'settings.desc.FINNHUB_API_KEY': 'Finnhub API密钥,用于美股数据(有免费额度)', +'settings.desc.FINNHUB_TIMEOUT': 'Finnhub API请求超时时间', +'settings.desc.FINNHUB_RATE_LIMIT': 'Finnhub API速率限制(每分钟请求数)', +'settings.desc.TIINGO_API_KEY': 'Tiingo API密钥,用于美股数据(有免费额度)', +'settings.desc.TIINGO_TIMEOUT': 'Tiingo API请求超时时间', +'settings.desc.AKSHARE_TIMEOUT': 'Akshare API超时时间,用于A股数据', +'settings.desc.YFINANCE_TIMEOUT': 'Yahoo Finance API超时时间', +'settings.desc.SIGNAL_WEBHOOK_URL': '信号通知Webhook地址(POST JSON)', +'settings.desc.SIGNAL_WEBHOOK_TOKEN': 'Webhook认证令牌,通过请求头发送', +'settings.desc.SIGNAL_NOTIFY_TIMEOUT_SEC': '通知请求超时时间', +'settings.desc.TELEGRAM_BOT_TOKEN': 'Telegram机器人Token,从@BotFather获取', +'settings.desc.SMTP_HOST': 'SMTP邮件服务器地址(如 smtp.gmail.com)', +'settings.desc.SMTP_PORT': 'SMTP端口(TLS用587,SSL用465,明文用25)', +'settings.desc.SMTP_USER': 'SMTP认证用户名(通常是邮箱地址)', +'settings.desc.SMTP_PASSWORD': 'SMTP认证密码或应用专用密码', +'settings.desc.SMTP_FROM': '邮件发件人地址', +'settings.desc.SMTP_USE_TLS': '启用STARTTLS加密(推荐端口587)', +'settings.desc.SMTP_USE_SSL': '启用SSL加密(端口465)', +'settings.desc.TWILIO_ACCOUNT_SID': 'Twilio账户SID,从控制台获取', +'settings.desc.TWILIO_AUTH_TOKEN': 'Twilio认证Token,从控制台获取', +'settings.desc.TWILIO_FROM_NUMBER': 'Twilio发送短信的号码(如 +1234567890)', +'settings.desc.ENABLE_AGENT_MEMORY': '启用AI Agent记忆功能,用于学习历史交易', +'settings.desc.AGENT_MEMORY_ENABLE_VECTOR': '启用本地向量相似度搜索进行记忆检索', +'settings.desc.AGENT_MEMORY_EMBEDDING_DIM': '记忆向量嵌入维度', +'settings.desc.AGENT_MEMORY_TOP_K': '检索时返回的相似记忆数量', +'settings.desc.AGENT_MEMORY_CANDIDATE_LIMIT': '相似度搜索的候选记忆上限', +'settings.desc.AGENT_MEMORY_HALF_LIFE_DAYS': '记忆时间衰减的半衰期(天)', +'settings.desc.AGENT_MEMORY_W_SIM': '记忆排序中相似度分数的权重(0-1)', +'settings.desc.AGENT_MEMORY_W_RECENCY': '记忆排序中时间新近度的权重(0-1)', +'settings.desc.AGENT_MEMORY_W_RETURNS': '记忆排序中收益表现的权重(0-1)', +'settings.desc.ENABLE_REFLECTION_WORKER': '启用后台自动交易反思Worker', +'settings.desc.REFLECTION_WORKER_INTERVAL_SEC': '自动反思运行间隔(默认24小时)', +'settings.desc.PROXY_HOST': '代理服务器主机名或IP', +'settings.desc.PROXY_PORT': '代理服务器端口(留空则禁用代理)', +'settings.desc.PROXY_SCHEME': '代理协议类型。socks5h 表示DNS也走代理', +'settings.desc.PROXY_URL': '完整代理URL(设置后覆盖上面的配置)', +'settings.desc.SEARCH_PROVIDER': '网页搜索提供商,用于AI研究功能', +'settings.desc.SEARCH_MAX_RESULTS': '搜索返回的最大结果数', +'settings.desc.SEARCH_GOOGLE_API_KEY': 'Google自定义搜索API密钥', +'settings.desc.SEARCH_GOOGLE_CX': 'Google可编程搜索引擎ID (CX)', +'settings.desc.SEARCH_BING_API_KEY': 'Microsoft Bing网页搜索API密钥', +'settings.desc.INTERNAL_API_KEY': '内部API认证密钥,用于服务间调用', +'settings.desc.CORS_ORIGINS': '允许的CORS来源(* 表示全部,或逗号分隔的列表)', +'settings.desc.RATE_LIMIT': '每IP每分钟的API请求限制', +'settings.desc.ENABLE_CACHE': '启用响应缓存以提高性能', +'settings.desc.ENABLE_REQUEST_LOG': '记录所有API请求日志,用于调试', +'settings.desc.ENABLE_AI_ANALYSIS': '启用AI驱动的市场分析功能' } export default { diff --git a/quantdinger_vue/src/locales/lang/zh-TW.js b/quantdinger_vue/src/locales/lang/zh-TW.js index 9155b8d..bea7f95 100644 --- a/quantdinger_vue/src/locales/lang/zh-TW.js +++ b/quantdinger_vue/src/locales/lang/zh-TW.js @@ -1650,21 +1650,20 @@ const locale = { 'settings.copySuccess': '複製成功', 'settings.copyFailed': '複製失敗', // Settings groups - 'settings.group.auth': '認證配置', - 'settings.group.server': '服務器配置', - 'settings.group.worker': '訂單處理配置', - 'settings.group.notification': '信號通知配置', - 'settings.group.smtp': '郵件SMTP配置', - 'settings.group.twilio': 'Twilio短信配置', - 'settings.group.strategy': '策略執行配置', - 'settings.group.proxy': '代理配置', - 'settings.group.app': '應用配置', + // Settings groups (按後端 order 排序) + 'settings.group.server': '服務配置', + 'settings.group.auth': '安全認證', 'settings.group.ai': 'AI/LLM配置', - 'settings.group.market': '市場預設', - 'settings.group.data_source': '數據源配置', + 'settings.group.trading': '實盤交易', + 'settings.group.strategy': '策略執行', + 'settings.group.data_source': '數據源', + 'settings.group.notification': '通知推送', + 'settings.group.email': '郵件配置', + 'settings.group.sms': '短信配置', + 'settings.group.agent': 'AI Agent', + 'settings.group.network': '網絡代理', 'settings.group.search': '搜索配置', - 'settings.group.agent_memory': '記憶/反思配置', - 'settings.group.reflection_worker': '自動反思驗證Worker', + 'settings.group.app': '應用配置', // Settings fields - Auth 'settings.field.SECRET_KEY': 'Secret Key', 'settings.field.ADMIN_USER': '管理員用戶名', @@ -1676,6 +1675,10 @@ const locale = { // Settings fields - Worker 'settings.field.ENABLE_PENDING_ORDER_WORKER': '啟用訂單處理Worker', 'settings.field.PENDING_ORDER_STALE_SEC': '訂單超時時間(秒)', + // Settings fields - Trading + 'settings.field.ORDER_MODE': '下單模式', + 'settings.field.MAKER_WAIT_SEC': '限價單等待時間(秒)', + 'settings.field.MAKER_OFFSET_BPS': '限價單價格偏移(基點)', // Settings fields - Notification 'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL', 'settings.field.SIGNAL_WEBHOOK_TOKEN': 'Webhook Token', @@ -1753,7 +1756,86 @@ const locale = { 'settings.field.SEARCH_GOOGLE_API_KEY': 'Google API Key', 'settings.field.SEARCH_GOOGLE_CX': 'Google CX', 'settings.field.SEARCH_BING_API_KEY': 'Bing API Key', - 'settings.field.INTERNAL_API_KEY': '內部API Key' + 'settings.field.INTERNAL_API_KEY': '內部API Key', + + // Settings descriptions (配置項說明) + 'settings.desc.PYTHON_API_HOST': '服務監聽地址。0.0.0.0 允許外部訪問,127.0.0.1 僅本地訪問', + 'settings.desc.PYTHON_API_PORT': '服務監聽端口,默認5000', + 'settings.desc.PYTHON_API_DEBUG': '啟用調試模式,開發時使用。生產環境請關閉', + 'settings.desc.SECRET_KEY': 'JWT簽名密鑰。生產環境必須修改以確保安全', + 'settings.desc.ADMIN_USER': '管理員登錄用戶名', + 'settings.desc.ADMIN_PASSWORD': '管理員登錄密碼。生產環境必須修改', + 'settings.desc.OPENROUTER_API_KEY': 'OpenRouter API密鑰,用於訪問多種AI模型', + 'settings.desc.OPENROUTER_API_URL': 'OpenRouter API端點地址', + 'settings.desc.OPENROUTER_MODEL': '默認使用的AI模型,如 openai/gpt-4o, anthropic/claude-3.5-sonnet', + 'settings.desc.OPENROUTER_TEMPERATURE': '模型創造性(0-1)。越低越確定性,越高越有創意', + 'settings.desc.OPENROUTER_MAX_TOKENS': '每次請求最大輸出token數', + 'settings.desc.OPENROUTER_TIMEOUT': 'API請求超時時間(秒)', + 'settings.desc.OPENROUTER_CONNECT_TIMEOUT': '連接建立超時時間(秒)', + 'settings.desc.AI_MODELS_JSON': '自定義模型列表,JSON格式,用於模型選擇器', + 'settings.desc.ENABLE_PENDING_ORDER_WORKER': '啟用後台訂單處理Worker,實盤交易必需', + 'settings.desc.PENDING_ORDER_STALE_SEC': '等待訂單超時時間,超時後標記為過期', + 'settings.desc.ORDER_MODE': 'maker: 限價單優先(手續費低),market: 市價單(立即成交)', + 'settings.desc.MAKER_WAIT_SEC': '限價單等待成交時間,超時後自動切換為市價單', + 'settings.desc.MAKER_OFFSET_BPS': '限價單價格偏移(基點)。買入價=市價*(1-偏移),賣出價=市價*(1+偏移)', + 'settings.desc.DISABLE_RESTORE_RUNNING_STRATEGIES': '禁止服務重啟時自動恢復運行中的策略', + 'settings.desc.STRATEGY_TICK_INTERVAL_SEC': '策略主循環檢查間隔(秒)', + 'settings.desc.PRICE_CACHE_TTL_SEC': '價格數據緩存有效期(秒)', + 'settings.desc.MARKET_TYPES_JSON': '自定義市場類型配置,JSON格式', + 'settings.desc.TRADING_SUPPORTED_SYMBOLS_JSON': '支持的交易對列表,JSON格式', + 'settings.desc.DATA_SOURCE_TIMEOUT': '數據源請求默認超時時間', + 'settings.desc.DATA_SOURCE_RETRY': '數據源請求失敗時的重試次數', + 'settings.desc.DATA_SOURCE_RETRY_BACKOFF': '重試間隔時間(秒)', + 'settings.desc.CCXT_DEFAULT_EXCHANGE': 'CCXT默認交易所(binance, coinbase, okx等)', + 'settings.desc.CCXT_TIMEOUT': 'CCXT請求超時時間(毫秒)', + 'settings.desc.CCXT_PROXY': 'CCXT請求代理地址(如 socks5h://127.0.0.1:1080)', + 'settings.desc.FINNHUB_API_KEY': 'Finnhub API密鑰,用於美股數據(有免費額度)', + 'settings.desc.FINNHUB_TIMEOUT': 'Finnhub API請求超時時間', + 'settings.desc.FINNHUB_RATE_LIMIT': 'Finnhub API速率限制(每分鐘請求數)', + 'settings.desc.TIINGO_API_KEY': 'Tiingo API密鑰,用於美股數據(有免費額度)', + 'settings.desc.TIINGO_TIMEOUT': 'Tiingo API請求超時時間', + 'settings.desc.AKSHARE_TIMEOUT': 'Akshare API超時時間,用於A股數據', + 'settings.desc.YFINANCE_TIMEOUT': 'Yahoo Finance API超時時間', + 'settings.desc.SIGNAL_WEBHOOK_URL': '信號通知Webhook地址(POST JSON)', + 'settings.desc.SIGNAL_WEBHOOK_TOKEN': 'Webhook認證令牌,通過請求頭發送', + 'settings.desc.SIGNAL_NOTIFY_TIMEOUT_SEC': '通知請求超時時間', + 'settings.desc.TELEGRAM_BOT_TOKEN': 'Telegram機器人Token,從@BotFather獲取', + 'settings.desc.SMTP_HOST': 'SMTP郵件服務器地址(如 smtp.gmail.com)', + 'settings.desc.SMTP_PORT': 'SMTP端口(TLS用587,SSL用465,明文用25)', + 'settings.desc.SMTP_USER': 'SMTP認證用戶名(通常是郵箱地址)', + 'settings.desc.SMTP_PASSWORD': 'SMTP認證密碼或應用專用密碼', + 'settings.desc.SMTP_FROM': '郵件發件人地址', + 'settings.desc.SMTP_USE_TLS': '啟用STARTTLS加密(推薦端口587)', + 'settings.desc.SMTP_USE_SSL': '啟用SSL加密(端口465)', + 'settings.desc.TWILIO_ACCOUNT_SID': 'Twilio賬戶SID,從控制台獲取', + 'settings.desc.TWILIO_AUTH_TOKEN': 'Twilio認證Token,從控制台獲取', + 'settings.desc.TWILIO_FROM_NUMBER': 'Twilio發送短信的號碼(如 +1234567890)', + 'settings.desc.ENABLE_AGENT_MEMORY': '啟用AI Agent記憶功能,用於學習歷史交易', + 'settings.desc.AGENT_MEMORY_ENABLE_VECTOR': '啟用本地向量相似度搜索進行記憶檢索', + 'settings.desc.AGENT_MEMORY_EMBEDDING_DIM': '記憶向量嵌入維度', + 'settings.desc.AGENT_MEMORY_TOP_K': '檢索時返回的相似記憶數量', + 'settings.desc.AGENT_MEMORY_CANDIDATE_LIMIT': '相似度搜索的候選記憶上限', + 'settings.desc.AGENT_MEMORY_HALF_LIFE_DAYS': '記憶時間衰減的半衰期(天)', + 'settings.desc.AGENT_MEMORY_W_SIM': '記憶排序中相似度分數的權重(0-1)', + 'settings.desc.AGENT_MEMORY_W_RECENCY': '記憶排序中時間新近度的權重(0-1)', + 'settings.desc.AGENT_MEMORY_W_RETURNS': '記憶排序中收益表現的權重(0-1)', + 'settings.desc.ENABLE_REFLECTION_WORKER': '啟用後台自動交易反思Worker', + 'settings.desc.REFLECTION_WORKER_INTERVAL_SEC': '自動反思運行間隔(默認24小時)', + 'settings.desc.PROXY_HOST': '代理服務器主機名或IP', + 'settings.desc.PROXY_PORT': '代理服務器端口(留空則禁用代理)', + 'settings.desc.PROXY_SCHEME': '代理協議類型。socks5h 表示DNS也走代理', + 'settings.desc.PROXY_URL': '完整代理URL(設置後覆蓋上面的配置)', + 'settings.desc.SEARCH_PROVIDER': '網頁搜索提供商,用於AI研究功能', + 'settings.desc.SEARCH_MAX_RESULTS': '搜索返回的最大結果數', + 'settings.desc.SEARCH_GOOGLE_API_KEY': 'Google自定義搜索API密鑰', + 'settings.desc.SEARCH_GOOGLE_CX': 'Google可編程搜索引擎ID (CX)', + 'settings.desc.SEARCH_BING_API_KEY': 'Microsoft Bing網頁搜索API密鑰', + 'settings.desc.INTERNAL_API_KEY': '內部API認證密鑰,用於服務間調用', + 'settings.desc.CORS_ORIGINS': '允許的CORS來源(* 表示全部,或逗號分隔的列表)', + 'settings.desc.RATE_LIMIT': '每IP每分鐘的API請求限制', + 'settings.desc.ENABLE_CACHE': '啟用響應緩存以提高性能', + 'settings.desc.ENABLE_REQUEST_LOG': '記錄所有API請求日誌,用於調試', + 'settings.desc.ENABLE_AI_ANALYSIS': '啟用AI驅動的市場分析功能' } export default { diff --git a/quantdinger_vue/src/views/settings/index.vue b/quantdinger_vue/src/views/settings/index.vue index e11936b..863fbf3 100644 --- a/quantdinger_vue/src/views/settings/index.vue +++ b/quantdinger_vue/src/views/settings/index.vue @@ -28,15 +28,13 @@
- + - @@ -49,8 +47,14 @@ :key="item.key">