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:
@@ -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'
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
# =========================
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -28,15 +28,13 @@
|
||||
<a-spin :spinning="loading">
|
||||
<div class="settings-content">
|
||||
<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">
|
||||
<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>
|
||||
</template>
|
||||
<template slot="extra">
|
||||
<a-icon :type="getGroupIcon(groupKey)" class="panel-icon" />
|
||||
</template>
|
||||
|
||||
<a-form :form="form" layout="vertical" class="settings-form">
|
||||
<a-row :gutter="24">
|
||||
@@ -49,8 +47,14 @@
|
||||
:key="item.key">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<span class="form-label-with-link">
|
||||
<span>{{ getItemLabel(groupKey, item) }}</span>
|
||||
<span class="form-label-with-tooltip">
|
||||
<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
|
||||
v-if="item.link"
|
||||
:href="item.link"
|
||||
@@ -158,7 +162,7 @@ export default {
|
||||
saving: false,
|
||||
schema: {},
|
||||
values: {},
|
||||
activeKeys: ['ai', 'data_source', 'app', 'auth'],
|
||||
activeKeys: ['server', 'auth', 'ai', 'trading'],
|
||||
passwordVisible: {},
|
||||
showRestartTip: false
|
||||
}
|
||||
@@ -166,6 +170,20 @@ export default {
|
||||
computed: {
|
||||
isDarkTheme () {
|
||||
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 () {
|
||||
@@ -203,15 +221,16 @@ export default {
|
||||
server: 'cloud-server',
|
||||
worker: 'schedule',
|
||||
notification: 'notification',
|
||||
smtp: 'mail',
|
||||
twilio: 'phone',
|
||||
email: 'mail',
|
||||
sms: 'phone',
|
||||
strategy: 'fund',
|
||||
proxy: 'global',
|
||||
network: 'global',
|
||||
app: 'appstore',
|
||||
ai: 'robot',
|
||||
market: 'stock',
|
||||
trading: 'stock',
|
||||
data_source: 'database',
|
||||
search: 'search'
|
||||
search: 'search',
|
||||
agent: 'experiment'
|
||||
}
|
||||
return icons[groupKey] || 'setting'
|
||||
},
|
||||
@@ -228,6 +247,17 @@ export default {
|
||||
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) {
|
||||
if (!linkText) return this.$t('settings.getApi')
|
||||
// 如果是翻译键(以 settings.link. 开头),则翻译
|
||||
@@ -400,17 +430,18 @@ export default {
|
||||
.panel-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
|
||||
.panel-icon-left {
|
||||
font-size: 18px;
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-icon {
|
||||
font-size: 18px;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
@@ -432,10 +463,27 @@ export default {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-label-with-link {
|
||||
.form-label-with-tooltip {
|
||||
display: flex;
|
||||
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 {
|
||||
font-size: 12px;
|
||||
@@ -449,6 +497,7 @@ export default {
|
||||
background: rgba(24, 144, 255, 0.08);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(24, 144, 255, 0.15);
|
||||
@@ -540,8 +589,13 @@ export default {
|
||||
color: #e0e6ed;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
|
||||
.panel-header .panel-title {
|
||||
color: #e0e6ed;
|
||||
.panel-header {
|
||||
.panel-icon-left {
|
||||
color: #58a6ff;
|
||||
}
|
||||
.panel-title {
|
||||
color: #e0e6ed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,12 +615,26 @@ export default {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.form-label-with-link .api-link {
|
||||
background: rgba(24, 144, 255, 0.15);
|
||||
color: #58a6ff;
|
||||
.form-label-with-tooltip {
|
||||
.label-text {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(24, 144, 255, 0.25);
|
||||
.help-icon {
|
||||
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,
|
||||
timeframe: values.timeframe,
|
||||
market_type: marketType,
|
||||
// Preset order params (advanced settings removed from UI)
|
||||
order_mode: 'maker',
|
||||
// Order execution settings moved to backend env config (ORDER_MODE, MAKER_WAIT_SEC, MAKER_OFFSET_BPS)
|
||||
margin_mode: 'cross',
|
||||
signal_mode: 'confirmed',
|
||||
// Backtest-like configs
|
||||
|
||||
Reference in New Issue
Block a user