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
This commit is contained in:
TIANHE
2026-01-12 00:15:52 +08:00
parent e7cb9c6493
commit 5a4c770279
16 changed files with 1121 additions and 233 deletions
+661 -129
View File
@@ -14,155 +14,687 @@ settings_bp = Blueprint('settings', __name__)
# .env 文件路径 # .env 文件路径
ENV_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env') ENV_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# 配置项定义(分组)- 完整对照 env.example # 配置项定义(分组)- 按功能模块划分,每个配置项包含描述
CONFIG_SCHEMA = { CONFIG_SCHEMA = {
'auth': { # ==================== 1. 服务配置 ====================
'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'},
]
},
'server': { 'server': {
'title': '服务器配置', 'title': 'Server Configuration',
'icon': 'cloud-server',
'order': 1,
'items': [ '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_HOST',
{'key': 'PYTHON_API_DEBUG', 'label': '调试模式', 'type': 'boolean', 'default': 'False'}, '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': [ 'items': [
{'key': 'ENABLE_PENDING_ORDER_WORKER', 'label': '启用订单处理Worker', 'type': 'boolean', 'default': 'True'}, {
{'key': 'PENDING_ORDER_STALE_SEC', 'label': '订单超时时间(秒)', 'type': 'number', 'default': '90'}, 'key': 'SECRET_KEY',
] 'label': 'Secret Key',
}, 'type': 'password',
'notification': { 'default': 'quantdinger-secret-key-change-me',
'title': '信号通知配置', 'description': 'JWT signing secret key. MUST change in production for security'
'items': [ },
{'key': 'SIGNAL_WEBHOOK_URL', 'label': 'Webhook URL', 'type': 'text', 'required': False}, {
{'key': 'SIGNAL_WEBHOOK_TOKEN', 'label': 'Webhook Token', 'type': 'password', 'required': False}, 'key': 'ADMIN_USER',
{'key': 'SIGNAL_NOTIFY_TIMEOUT_SEC', 'label': '通知超时(秒)', 'type': 'number', 'default': '6'}, 'label': 'Admin Username',
{'key': 'TELEGRAM_BOT_TOKEN', 'label': 'Telegram Bot Token', 'type': 'password', 'required': False, 'link': 'https://t.me/BotFather', 'link_text': 'settings.link.createBot'}, 'type': 'text',
] 'default': 'quantdinger',
}, 'description': 'Administrator login username'
'smtp': { },
'title': '邮件SMTP配置', {
'items': [ 'key': 'ADMIN_PASSWORD',
{'key': 'SMTP_HOST', 'label': 'SMTP服务器', 'type': 'text', 'required': False}, 'label': 'Admin Password',
{'key': 'SMTP_PORT', 'label': 'SMTP端口', 'type': 'number', 'default': '587'}, 'type': 'password',
{'key': 'SMTP_USER', 'label': 'SMTP用户名', 'type': 'text', 'required': False}, 'default': '123456',
{'key': 'SMTP_PASSWORD', 'label': 'SMTP密码', 'type': 'password', 'required': False}, 'description': 'Administrator login password. MUST change in production'
{'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'},
] ]
}, },
# ==================== 3. AI/LLM 配置 ====================
'ai': { 'ai': {
'title': 'AI/LLM配置', 'title': 'AI / LLM Configuration',
'icon': 'robot',
'order': 3,
'items': [ '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_API_KEY',
{'key': 'OPENROUTER_MODEL', 'label': '默认模型', 'type': 'text', 'default': 'openai/gpt-4o', 'link': 'https://openrouter.ai/models', 'link_text': 'settings.link.viewModels'}, 'label': 'OpenRouter API Key',
{'key': 'OPENROUTER_TEMPERATURE', 'label': 'Temperature', 'type': 'number', 'default': '0.7'}, 'type': 'password',
{'key': 'OPENROUTER_MAX_TOKENS', 'label': 'Max Tokens', 'type': 'number', 'default': '4000'}, 'required': False,
{'key': 'OPENROUTER_TIMEOUT', 'label': '超时时间(秒)', 'type': 'number', 'default': '300'}, 'link': 'https://openrouter.ai/keys',
{'key': 'OPENROUTER_CONNECT_TIMEOUT', 'label': '连接超时(秒)', 'type': 'number', 'default': '30'}, 'link_text': 'settings.link.getApiKey',
{'key': 'AI_MODELS_JSON', 'label': '模型列表(JSON)', 'type': 'text', 'default': '{}', 'required': False}, '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': [ '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': { 'data_source': {
'title': '数据源配置', 'title': 'Data Sources',
'icon': 'database',
'order': 6,
'items': [ 'items': [
{'key': 'DATA_SOURCE_TIMEOUT', 'label': '数据源超时(秒)', 'type': 'number', 'default': '30'}, {
{'key': 'DATA_SOURCE_RETRY', 'label': '重试次数', 'type': 'number', 'default': '3'}, 'key': 'DATA_SOURCE_TIMEOUT',
{'key': 'DATA_SOURCE_RETRY_BACKOFF', 'label': '重试退避(秒)', 'type': 'number', 'default': '0.5'}, 'label': 'Default Timeout (sec)',
{'key': 'FINNHUB_API_KEY', 'label': 'Finnhub API Key', 'type': 'password', 'required': False, 'link': 'https://finnhub.io/register', 'link_text': 'settings.link.freeRegister'}, 'type': 'number',
{'key': 'FINNHUB_TIMEOUT', 'label': 'Finnhub超时(秒)', 'type': 'number', 'default': '10'}, 'default': '30',
{'key': 'FINNHUB_RATE_LIMIT', 'label': 'Finnhub速率限制', 'type': 'number', 'default': '60'}, 'description': 'Default timeout for all data source requests'
{'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': 'DATA_SOURCE_RETRY',
{'key': 'AKSHARE_TIMEOUT', 'label': 'Akshare超时(秒)', 'type': 'number', 'default': '30'}, 'label': 'Retry Count',
{'key': 'YFINANCE_TIMEOUT', 'label': 'YFinance超时(秒)', 'type': 'number', 'default': '30'}, 'type': 'number',
{'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'}, 'default': '3',
{'key': 'TIINGO_TIMEOUT', 'label': 'Tiingo超时(秒)', 'type': 'number', 'default': '10'}, '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': [ 'items': [
{'key': 'SEARCH_PROVIDER', 'label': '搜索提供商', 'type': 'select', 'options': ['google', 'bing', 'none'], 'default': 'google'}, {
{'key': 'SEARCH_MAX_RESULTS', 'label': '最大结果数', 'type': 'number', 'default': '10'}, 'key': 'SIGNAL_WEBHOOK_URL',
{'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'}, 'label': 'Webhook URL',
{'key': 'SEARCH_GOOGLE_CX', 'label': 'Google CX', 'type': 'text', 'required': False, 'link': 'https://programmablesearchengine.google.com/controlpanel/all', 'link_text': 'settings.link.createSearchEngine'}, 'type': 'text',
{'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'}, 'required': False,
{'key': 'INTERNAL_API_KEY', 'label': '内部API Key', 'type': 'password', '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'
},
] ]
}, },
} }
@@ -234,8 +234,11 @@ class BitfinexDerivativesClient(BitfinexClient):
last = last or [] last = last or []
filled = 0.0 filled = 0.0
avg_price = 0.0 avg_price = 0.0
fee = 0.0
fee_ccy = ""
status = "" status = ""
# best-effort parsing from array fields # 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: try:
if isinstance(last, list) and len(last) >= 15: if isinstance(last, list) and len(last) >= 15:
status = str(last[13] or "") status = str(last[13] or "")
@@ -245,12 +248,14 @@ class BitfinexDerivativesClient(BitfinexClient):
avg_price = float(last[14] or 0.0) avg_price = float(last[14] or 0.0)
except Exception: except Exception:
pass 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: 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()): 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: 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)) time.sleep(float(poll_interval_sec or 0.5))
@@ -315,12 +315,22 @@ class BybitClient(BaseRestClient):
avg_price = float(last.get("avgPrice") or 0.0) avg_price = float(last.get("avgPrice") or 0.0)
except Exception: except Exception:
avg_price = 0.0 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: 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"): 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: 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)) time.sleep(float(poll_interval_sec or 0.5))
def get_positions(self) -> Dict[str, Any]: def get_positions(self) -> Dict[str, Any]:
@@ -170,6 +170,8 @@ class CoinbaseExchangeClient(BaseRestClient):
status = str(last.get("status") or "") status = str(last.get("status") or "")
filled = 0.0 filled = 0.0
avg_price = 0.0 avg_price = 0.0
fee = 0.0
fee_ccy = ""
try: try:
filled = float(last.get("filled_size") or 0.0) filled = float(last.get("filled_size") or 0.0)
except Exception: except Exception:
@@ -180,12 +182,20 @@ class CoinbaseExchangeClient(BaseRestClient):
avg_price = executed_value / filled avg_price = executed_value / filled
except Exception: except Exception:
avg_price = 0.0 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: 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"): 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: 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)) time.sleep(float(poll_interval_sec or 0.5))
@@ -134,6 +134,8 @@ class GateSpotClient(_GateBase):
status = str(last.get("status") or "") status = str(last.get("status") or "")
filled = 0.0 filled = 0.0
avg_price = 0.0 avg_price = 0.0
fee = 0.0
fee_ccy = ""
try: try:
filled = float(last.get("filled_amount") or 0.0) filled = float(last.get("filled_amount") or 0.0)
except Exception: except Exception:
@@ -144,12 +146,18 @@ class GateSpotClient(_GateBase):
avg_price = filled_total / filled avg_price = filled_total / filled
except Exception: except Exception:
avg_price = 0.0 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: 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"): 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: 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)) time.sleep(float(poll_interval_sec or 0.5))
@@ -321,6 +329,8 @@ class GateUsdtFuturesClient(_GateBase):
status = str(last.get("status") or "") status = str(last.get("status") or "")
filled = 0.0 filled = 0.0
avg_price = 0.0 avg_price = 0.0
fee = 0.0
fee_ccy = ""
try: try:
# Gate futures often returns "filled_size" in contracts. # Gate futures often returns "filled_size" in contracts.
filled_ct = abs(float(last.get("filled_size") or last.get("filledSize") or 0.0)) 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) avg_price = float(last.get("fill_price") or last.get("fillPrice") or last.get("price") or 0.0)
except Exception: except Exception:
avg_price = 0.0 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: 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"): 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: 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)) time.sleep(float(poll_interval_sec or 0.5))
@@ -153,6 +153,8 @@ class KrakenClient(BaseRestClient):
status = str(last.get("status") or "") status = str(last.get("status") or "")
filled = 0.0 filled = 0.0
avg_price = 0.0 avg_price = 0.0
fee = 0.0
fee_ccy = ""
try: try:
filled = float(last.get("vol_exec") or 0.0) filled = float(last.get("vol_exec") or 0.0)
except Exception: except Exception:
@@ -164,12 +166,20 @@ class KrakenClient(BaseRestClient):
avg_price = cost / filled avg_price = cost / filled
except Exception: except Exception:
avg_price = 0.0 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: 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"): 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: 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)) time.sleep(float(poll_interval_sec or 0.5))
@@ -186,6 +186,8 @@ class KrakenFuturesClient(BaseRestClient):
status = str(last.get("status") or last.get("orderStatus") or "") status = str(last.get("status") or last.get("orderStatus") or "")
filled = 0.0 filled = 0.0
avg_price = 0.0 avg_price = 0.0
fee = 0.0
fee_ccy = ""
try: try:
filled = float(last.get("filledSize") or last.get("filled_size") or 0.0) filled = float(last.get("filledSize") or last.get("filled_size") or 0.0)
except Exception: except Exception:
@@ -194,12 +196,20 @@ class KrakenFuturesClient(BaseRestClient):
avg_price = float(last.get("avgFillPrice") or last.get("avg_fill_price") or 0.0) avg_price = float(last.get("avgFillPrice") or last.get("avg_fill_price") or 0.0)
except Exception: except Exception:
avg_price = 0.0 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: 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"): 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: 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)) time.sleep(float(poll_interval_sec or 0.5))
@@ -475,6 +475,8 @@ class KucoinFuturesClient(BaseRestClient):
status = str(od.get("status") or "") status = str(od.get("status") or "")
filled = 0.0 filled = 0.0
avg_price = 0.0 avg_price = 0.0
fee = 0.0
fee_ccy = ""
try: try:
# dealSize is in contracts; convert back to base using multiplier best-effort. # dealSize is in contracts; convert back to base using multiplier best-effort.
deal_ct = float(od.get("dealSize") or 0.0) 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) filled = abs(float(deal_ct or 0.0)) * float(mult)
if filled > 0 and deal_value > 0: if filled > 0 and deal_value > 0:
avg_price = float(deal_value) / float(filled) 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: 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"): 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: 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)) time.sleep(float(poll_interval_sec or 0.5))
@@ -12,7 +12,7 @@ import json
import os import os
import threading import threading
import time 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.signal_notifier import SignalNotifier
from app.services.exchange_execution import load_strategy_configs, resolve_exchange_config, safe_exchange_config_for_log 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") _notify_live_best_effort(status="failed", error="spot_market_does_not_support_short_signals")
return return
# Unified maker->market fallback settings (defaults: 10 seconds) # Unified maker->market fallback settings
order_mode = str(payload.get("order_mode") or payload.get("orderMode") or "maker").strip().lower() # Priority: payload config > environment variable > default value
maker_wait_sec = float(payload.get("maker_wait_sec") or payload.get("makerWaitSec") or 10.0) _default_order_mode = os.getenv("ORDER_MODE", "maker").strip().lower()
maker_offset_bps = float(payload.get("maker_offset_bps") or payload.get("makerOffsetBps") or 2.0) _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: 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: if maker_offset_bps < 0:
maker_offset_bps = 0.0 maker_offset_bps = 0.0
maker_offset = maker_offset_bps / 10000.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) 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 phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) _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): 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) 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 phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) _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): elif isinstance(client, KrakenClient):
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) _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): 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) 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 phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) _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): elif isinstance(client, KucoinSpotClient):
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q 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) q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) _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): elif isinstance(client, GateSpotClient):
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) _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): 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) 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 phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) _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): elif isinstance(client, BitfinexClient):
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) _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): elif isinstance(client, BitfinexDerivativesClient):
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec) q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0)) _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) 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) 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 phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) _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): 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) 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 phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) _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): elif isinstance(client, KrakenClient):
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2 phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) _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): 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) 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 phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) _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): elif isinstance(client, KucoinSpotClient):
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2 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) q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2 phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) _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): elif isinstance(client, GateSpotClient):
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2 phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) _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): 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) 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 phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) _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): elif isinstance(client, BitfinexClient):
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2 phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) _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): elif isinstance(client, BitfinexDerivativesClient):
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0) q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2 phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0)) _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: except LiveTradingError as e:
logger.warning(f"live market phase failed: pending_id={order_id}, strategy_id={strategy_id}, cfg={safe_cfg}, err={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) phases["market_error"] = str(e)
@@ -2221,8 +2221,11 @@ class TradingExecutor:
margin_mode: str = 'cross', margin_mode: str = 'cross',
stop_loss_price: float = None, stop_loss_price: float = None,
take_profit_price: float = None, take_profit_price: float = None,
order_mode: str = 'maker', # Order execution params (order_mode, maker_wait_sec, maker_offset_bps) are now
maker_wait_sec: float = 8.0, # 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, maker_retries: int = 3,
close_fallback_to_market: bool = True, close_fallback_to_market: bool = True,
open_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: A separate worker will poll `pending_orders` and dispatch:
- execution_mode='signal': dispatch notifications (no real trading). - execution_mode='signal': dispatch notifications (no real trading).
- execution_mode='live': reserved for future live trading execution (not implemented). - 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: try:
# Reference price at enqueue time: use current tick price if provided to avoid extra fetch. # 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, "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, "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"), "margin_mode": str(margin_mode or "cross"),
"order_mode": str(order_mode or "maker"), # Order execution params moved to env config (ORDER_MODE, MAKER_WAIT_SEC, MAKER_OFFSET_BPS)
"maker_wait_sec": float(maker_wait_sec or 0.0),
"maker_retries": int(maker_retries or 0), "maker_retries": int(maker_retries or 0),
"close_fallback_to_market": bool(close_fallback_to_market), "close_fallback_to_market": bool(close_fallback_to_market),
"open_fallback_to_market": bool(open_fallback_to_market), "open_fallback_to_market": bool(open_fallback_to_market),
+16
View File
@@ -34,6 +34,22 @@ ENABLE_PENDING_ORDER_WORKER=true
# Reclaim orders stuck in status=processing after worker crashes (seconds). # Reclaim orders stuck in status=processing after worker crashes (seconds).
PENDING_ORDER_STALE_SEC=90 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) # Strategy signal notifications (optional)
# ========================= # =========================
+22 -13
View File
@@ -1911,21 +1911,20 @@ const locale = {
'settings.copySuccess': 'Copied', 'settings.copySuccess': 'Copied',
'settings.copyFailed': 'Copy failed', 'settings.copyFailed': 'Copy failed',
// Settings groups // Settings groups
'settings.group.auth': 'Authentication', // Settings groups (ordered by backend order)
'settings.group.server': 'Server Configuration', 'settings.group.server': 'Server Configuration',
'settings.group.worker': 'Order Worker', 'settings.group.auth': 'Security & Authentication',
'settings.group.notification': 'Signal Notification', 'settings.group.ai': 'AI / LLM Configuration',
'settings.group.smtp': 'Email SMTP', 'settings.group.trading': 'Live Trading',
'settings.group.twilio': 'Twilio SMS',
'settings.group.strategy': 'Strategy Execution', '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.data_source': 'Data Sources',
'settings.group.search': 'Search Configuration', 'settings.group.notification': 'Notifications',
'settings.group.agent_memory': 'Memory/Reflection', 'settings.group.email': 'Email (SMTP)',
'settings.group.reflection_worker': 'Auto Reflection Verification Worker', '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 fields - Auth
'settings.field.SECRET_KEY': 'Secret Key', 'settings.field.SECRET_KEY': 'Secret Key',
'settings.field.ADMIN_USER': 'Admin Username', 'settings.field.ADMIN_USER': 'Admin Username',
@@ -1937,6 +1936,10 @@ const locale = {
// Settings fields - Worker // Settings fields - Worker
'settings.field.ENABLE_PENDING_ORDER_WORKER': 'Enable Order Worker', 'settings.field.ENABLE_PENDING_ORDER_WORKER': 'Enable Order Worker',
'settings.field.PENDING_ORDER_STALE_SEC': 'Order Stale Timeout (sec)', '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 fields - Notification
'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL', 'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL',
'settings.field.SIGNAL_WEBHOOK_TOKEN': 'Webhook Token', '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_API_KEY': 'Google API Key',
'settings.field.SEARCH_GOOGLE_CX': 'Google CX', 'settings.field.SEARCH_GOOGLE_CX': 'Google CX',
'settings.field.SEARCH_BING_API_KEY': 'Bing API Key', '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 { export default {
+96 -15
View File
@@ -1649,22 +1649,20 @@ const locale = {
'settings.copyRestartCmd': '复制重启命令', 'settings.copyRestartCmd': '复制重启命令',
'settings.copySuccess': '复制成功', 'settings.copySuccess': '复制成功',
'settings.copyFailed': '复制失败', 'settings.copyFailed': '复制失败',
// Settings groups // Settings groups (按后端 order 排序)
'settings.group.auth': '认证配置', 'settings.group.server': '服务配置',
'settings.group.server': '服务器配置', 'settings.group.auth': '安全认证',
'settings.group.worker': '订单处理配置',
'settings.group.notification': '信号通知配置',
'settings.group.smtp': '邮件SMTP配置',
'settings.group.twilio': 'Twilio短信配置',
'settings.group.strategy': '策略执行配置',
'settings.group.proxy': '代理配置',
'settings.group.app': '应用配置',
'settings.group.ai': 'AI/LLM配置', 'settings.group.ai': 'AI/LLM配置',
'settings.group.market': '市场预设', 'settings.group.trading': '实盘交易',
'settings.group.data_source': '数据源配置', '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.search': '搜索配置',
'settings.group.agent_memory': '记忆/反思配置', 'settings.group.app': '应用配置',
'settings.group.reflection_worker': '自动反思验证Worker',
// Settings fields - Auth // Settings fields - Auth
'settings.field.SECRET_KEY': 'Secret Key', 'settings.field.SECRET_KEY': 'Secret Key',
'settings.field.ADMIN_USER': '管理员用户名', 'settings.field.ADMIN_USER': '管理员用户名',
@@ -1676,6 +1674,10 @@ const locale = {
// Settings fields - Worker // Settings fields - Worker
'settings.field.ENABLE_PENDING_ORDER_WORKER': '启用订单处理Worker', 'settings.field.ENABLE_PENDING_ORDER_WORKER': '启用订单处理Worker',
'settings.field.PENDING_ORDER_STALE_SEC': '订单超时时间(秒)', '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 fields - Notification
'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL', 'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL',
'settings.field.SIGNAL_WEBHOOK_TOKEN': 'Webhook Token', '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_API_KEY': 'Google API Key',
'settings.field.SEARCH_GOOGLE_CX': 'Google CX', 'settings.field.SEARCH_GOOGLE_CX': 'Google CX',
'settings.field.SEARCH_BING_API_KEY': 'Bing API Key', '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用587SSL用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 { export default {
+96 -14
View File
@@ -1650,21 +1650,20 @@ const locale = {
'settings.copySuccess': '複製成功', 'settings.copySuccess': '複製成功',
'settings.copyFailed': '複製失敗', 'settings.copyFailed': '複製失敗',
// Settings groups // Settings groups
'settings.group.auth': '認證配置', // Settings groups (按後端 order 排序)
'settings.group.server': '服務配置', 'settings.group.server': '服務配置',
'settings.group.worker': '訂單處理配置', 'settings.group.auth': '安全認證',
'settings.group.notification': '信號通知配置',
'settings.group.smtp': '郵件SMTP配置',
'settings.group.twilio': 'Twilio短信配置',
'settings.group.strategy': '策略執行配置',
'settings.group.proxy': '代理配置',
'settings.group.app': '應用配置',
'settings.group.ai': 'AI/LLM配置', 'settings.group.ai': 'AI/LLM配置',
'settings.group.market': '市場預設', 'settings.group.trading': '實盤交易',
'settings.group.data_source': '數據源配置', '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.search': '搜索配置',
'settings.group.agent_memory': '記憶/反思配置', 'settings.group.app': '應用配置',
'settings.group.reflection_worker': '自動反思驗證Worker',
// Settings fields - Auth // Settings fields - Auth
'settings.field.SECRET_KEY': 'Secret Key', 'settings.field.SECRET_KEY': 'Secret Key',
'settings.field.ADMIN_USER': '管理員用戶名', 'settings.field.ADMIN_USER': '管理員用戶名',
@@ -1676,6 +1675,10 @@ const locale = {
// Settings fields - Worker // Settings fields - Worker
'settings.field.ENABLE_PENDING_ORDER_WORKER': '啟用訂單處理Worker', 'settings.field.ENABLE_PENDING_ORDER_WORKER': '啟用訂單處理Worker',
'settings.field.PENDING_ORDER_STALE_SEC': '訂單超時時間(秒)', '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 fields - Notification
'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL', 'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL',
'settings.field.SIGNAL_WEBHOOK_TOKEN': 'Webhook Token', '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_API_KEY': 'Google API Key',
'settings.field.SEARCH_GOOGLE_CX': 'Google CX', 'settings.field.SEARCH_GOOGLE_CX': 'Google CX',
'settings.field.SEARCH_BING_API_KEY': 'Bing API Key', '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用587SSL用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 { export default {
+94 -26
View File
@@ -28,15 +28,13 @@
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="settings-content"> <div class="settings-content">
<a-collapse v-model="activeKeys" :bordered="false" class="settings-collapse"> <a-collapse v-model="activeKeys" :bordered="false" class="settings-collapse">
<a-collapse-panel v-for="(group, groupKey) in schema" :key="groupKey"> <a-collapse-panel v-for="(group, groupKey) in sortedSchema" :key="groupKey">
<template slot="header"> <template slot="header">
<span class="panel-header"> <span class="panel-header">
<a-icon :type="group.icon || getGroupIcon(groupKey)" class="panel-icon-left" />
<span class="panel-title">{{ getGroupTitle(groupKey, group.title) }}</span> <span class="panel-title">{{ getGroupTitle(groupKey, group.title) }}</span>
</span> </span>
</template> </template>
<template slot="extra">
<a-icon :type="getGroupIcon(groupKey)" class="panel-icon" />
</template>
<a-form :form="form" layout="vertical" class="settings-form"> <a-form :form="form" layout="vertical" class="settings-form">
<a-row :gutter="24"> <a-row :gutter="24">
@@ -49,8 +47,14 @@
:key="item.key"> :key="item.key">
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
<span class="form-label-with-link"> <span class="form-label-with-tooltip">
<span>{{ getItemLabel(groupKey, item) }}</span> <span class="label-text">{{ getItemLabel(groupKey, item) }}</span>
<a-tooltip v-if="item.description" placement="top">
<template slot="title">
{{ getItemDescription(groupKey, item) }}
</template>
<a-icon type="question-circle" class="help-icon" />
</a-tooltip>
<a <a
v-if="item.link" v-if="item.link"
:href="item.link" :href="item.link"
@@ -158,7 +162,7 @@ export default {
saving: false, saving: false,
schema: {}, schema: {},
values: {}, values: {},
activeKeys: ['ai', 'data_source', 'app', 'auth'], activeKeys: ['server', 'auth', 'ai', 'trading'],
passwordVisible: {}, passwordVisible: {},
showRestartTip: false showRestartTip: false
} }
@@ -166,6 +170,20 @@ export default {
computed: { computed: {
isDarkTheme () { isDarkTheme () {
return this.navTheme === 'dark' || this.navTheme === 'realdark' return this.navTheme === 'dark' || this.navTheme === 'realdark'
},
// order schema
sortedSchema () {
const entries = Object.entries(this.schema)
entries.sort((a, b) => {
const orderA = a[1].order || 999
const orderB = b[1].order || 999
return orderA - orderB
})
const sorted = {}
for (const [key, value] of entries) {
sorted[key] = value
}
return sorted
} }
}, },
beforeCreate () { beforeCreate () {
@@ -203,15 +221,16 @@ export default {
server: 'cloud-server', server: 'cloud-server',
worker: 'schedule', worker: 'schedule',
notification: 'notification', notification: 'notification',
smtp: 'mail', email: 'mail',
twilio: 'phone', sms: 'phone',
strategy: 'fund', strategy: 'fund',
proxy: 'global', network: 'global',
app: 'appstore', app: 'appstore',
ai: 'robot', ai: 'robot',
market: 'stock', trading: 'stock',
data_source: 'database', data_source: 'database',
search: 'search' search: 'search',
agent: 'experiment'
} }
return icons[groupKey] || 'setting' return icons[groupKey] || 'setting'
}, },
@@ -228,6 +247,17 @@ export default {
return translated !== key ? translated : item.label return translated !== key ? translated : item.label
}, },
getItemDescription (groupKey, item) {
//
const key = `settings.desc.${item.key}`
const translated = this.$t(key)
if (translated !== key) {
return translated
}
// 退
return item.description || ''
},
getLinkText (linkText) { getLinkText (linkText) {
if (!linkText) return this.$t('settings.getApi') if (!linkText) return this.$t('settings.getApi')
// settings.link. // settings.link.
@@ -400,17 +430,18 @@ export default {
.panel-header { .panel-header {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 10px;
flex: 1; flex: 1;
.panel-icon-left {
font-size: 18px;
color: @primary-color;
}
.panel-title { .panel-title {
font-size: 16px; font-size: 16px;
} }
} }
.panel-icon {
font-size: 18px;
color: @primary-color;
}
} }
.ant-collapse-content { .ant-collapse-content {
@@ -432,10 +463,27 @@ export default {
font-weight: 500; font-weight: 500;
} }
.form-label-with-link { .form-label-with-tooltip {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 6px;
flex-wrap: wrap;
.label-text {
color: #475569;
font-weight: 500;
}
.help-icon {
font-size: 14px;
color: #94a3b8;
cursor: help;
transition: color 0.2s;
&:hover {
color: @primary-color;
}
}
.api-link { .api-link {
font-size: 12px; font-size: 12px;
@@ -449,6 +497,7 @@ export default {
background: rgba(24, 144, 255, 0.08); background: rgba(24, 144, 255, 0.08);
border-radius: 4px; border-radius: 4px;
transition: all 0.2s; transition: all 0.2s;
margin-left: 4px;
&:hover { &:hover {
background: rgba(24, 144, 255, 0.15); background: rgba(24, 144, 255, 0.15);
@@ -540,8 +589,13 @@ export default {
color: #e0e6ed; color: #e0e6ed;
border-bottom-color: rgba(255, 255, 255, 0.06); border-bottom-color: rgba(255, 255, 255, 0.06);
.panel-header .panel-title { .panel-header {
color: #e0e6ed; .panel-icon-left {
color: #58a6ff;
}
.panel-title {
color: #e0e6ed;
}
} }
} }
@@ -561,12 +615,26 @@ export default {
color: #c9d1d9; color: #c9d1d9;
} }
.form-label-with-link .api-link { .form-label-with-tooltip {
background: rgba(24, 144, 255, 0.15); .label-text {
color: #58a6ff; color: #c9d1d9;
}
&:hover { .help-icon {
background: rgba(24, 144, 255, 0.25); color: #6e7681;
&:hover {
color: #58a6ff;
}
}
.api-link {
background: rgba(24, 144, 255, 0.15);
color: #58a6ff;
&:hover {
background: rgba(24, 144, 255, 0.25);
}
} }
} }
} }
@@ -2570,8 +2570,7 @@ export default {
trade_direction: tradeDirection, trade_direction: tradeDirection,
timeframe: values.timeframe, timeframe: values.timeframe,
market_type: marketType, market_type: marketType,
// Preset order params (advanced settings removed from UI) // Order execution settings moved to backend env config (ORDER_MODE, MAKER_WAIT_SEC, MAKER_OFFSET_BPS)
order_mode: 'maker',
margin_mode: 'cross', margin_mode: 'cross',
signal_mode: 'confirmed', signal_mode: 'confirmed',
// Backtest-like configs // Backtest-like configs