feat(i18n): add missing translations for trading-assistant modal
- Add step2Params and step3Signal translations - Add execution mode related translations (signal/live) - Add notification channel translations (browser/email/phone/telegram/discord/webhook) - Add live trading config translations (savedCredential, credentialName, etc.) - Add batch operation message translations (batchCreateSuccess, batchStart/Stop/Delete) - Add placeholders for email, phone, telegram, discord, webhook inputs - Add validation messages for email and notify channel - Add strategy group translations (startAll, stopAll, deleteAll, symbolCount) - Support both zh-CN (Simplified Chinese) and zh-TW (Traditional Chinese)
This commit is contained in:
@@ -74,6 +74,171 @@ def create_strategy():
|
|||||||
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
|
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@strategy_bp.route('/strategies/batch-create', methods=['POST'])
|
||||||
|
def batch_create_strategies():
|
||||||
|
"""
|
||||||
|
批量创建策略(多币种)
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
strategy_name: 策略基础名称
|
||||||
|
symbols: 币种数组,如 ["Crypto:BTC/USDT", "Crypto:ETH/USDT"]
|
||||||
|
... 其他策略配置
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = request.get_json() or {}
|
||||||
|
payload['user_id'] = int(payload.get('user_id') or 1)
|
||||||
|
payload['strategy_type'] = payload.get('strategy_type') or 'IndicatorStrategy'
|
||||||
|
|
||||||
|
result = get_strategy_service().batch_create_strategies(payload)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
return jsonify({
|
||||||
|
'code': 1,
|
||||||
|
'msg': f"成功创建 {result['total_created']} 个策略",
|
||||||
|
'data': result
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '批量创建失败',
|
||||||
|
'data': result
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"batch_create_strategies failed: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@strategy_bp.route('/strategies/batch-start', methods=['POST'])
|
||||||
|
def batch_start_strategies():
|
||||||
|
"""
|
||||||
|
批量启动策略
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
strategy_ids: 策略ID数组
|
||||||
|
或
|
||||||
|
strategy_group_id: 策略组ID
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = request.get_json() or {}
|
||||||
|
strategy_ids = payload.get('strategy_ids') or []
|
||||||
|
strategy_group_id = payload.get('strategy_group_id')
|
||||||
|
|
||||||
|
# 如果提供了策略组ID,获取组内所有策略
|
||||||
|
if strategy_group_id and not strategy_ids:
|
||||||
|
strategy_ids = get_strategy_service().get_strategies_by_group(strategy_group_id)
|
||||||
|
|
||||||
|
if not strategy_ids:
|
||||||
|
return jsonify({'code': 0, 'msg': '请提供策略ID', 'data': None}), 400
|
||||||
|
|
||||||
|
# 先更新数据库状态
|
||||||
|
result = get_strategy_service().batch_start_strategies(strategy_ids)
|
||||||
|
|
||||||
|
# 然后启动执行器
|
||||||
|
executor = get_trading_executor()
|
||||||
|
for sid in result.get('success_ids', []):
|
||||||
|
try:
|
||||||
|
executor.start_strategy(sid)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start executor for strategy {sid}: {e}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'code': 1 if result['success'] else 0,
|
||||||
|
'msg': f"成功启动 {len(result.get('success_ids', []))} 个策略",
|
||||||
|
'data': result
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"batch_start_strategies failed: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@strategy_bp.route('/strategies/batch-stop', methods=['POST'])
|
||||||
|
def batch_stop_strategies():
|
||||||
|
"""
|
||||||
|
批量停止策略
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
strategy_ids: 策略ID数组
|
||||||
|
或
|
||||||
|
strategy_group_id: 策略组ID
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = request.get_json() or {}
|
||||||
|
strategy_ids = payload.get('strategy_ids') or []
|
||||||
|
strategy_group_id = payload.get('strategy_group_id')
|
||||||
|
|
||||||
|
if strategy_group_id and not strategy_ids:
|
||||||
|
strategy_ids = get_strategy_service().get_strategies_by_group(strategy_group_id)
|
||||||
|
|
||||||
|
if not strategy_ids:
|
||||||
|
return jsonify({'code': 0, 'msg': '请提供策略ID', 'data': None}), 400
|
||||||
|
|
||||||
|
# 先停止执行器
|
||||||
|
executor = get_trading_executor()
|
||||||
|
for sid in strategy_ids:
|
||||||
|
try:
|
||||||
|
executor.stop_strategy(sid)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to stop executor for strategy {sid}: {e}")
|
||||||
|
|
||||||
|
# 然后更新数据库状态
|
||||||
|
result = get_strategy_service().batch_stop_strategies(strategy_ids)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'code': 1 if result['success'] else 0,
|
||||||
|
'msg': f"成功停止 {len(result.get('success_ids', []))} 个策略",
|
||||||
|
'data': result
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"batch_stop_strategies failed: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@strategy_bp.route('/strategies/batch-delete', methods=['DELETE'])
|
||||||
|
def batch_delete_strategies():
|
||||||
|
"""
|
||||||
|
批量删除策略
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
strategy_ids: 策略ID数组
|
||||||
|
或
|
||||||
|
strategy_group_id: 策略组ID
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = request.get_json() or {}
|
||||||
|
strategy_ids = payload.get('strategy_ids') or []
|
||||||
|
strategy_group_id = payload.get('strategy_group_id')
|
||||||
|
|
||||||
|
if strategy_group_id and not strategy_ids:
|
||||||
|
strategy_ids = get_strategy_service().get_strategies_by_group(strategy_group_id)
|
||||||
|
|
||||||
|
if not strategy_ids:
|
||||||
|
return jsonify({'code': 0, 'msg': '请提供策略ID', 'data': None}), 400
|
||||||
|
|
||||||
|
# 先停止执行器
|
||||||
|
executor = get_trading_executor()
|
||||||
|
for sid in strategy_ids:
|
||||||
|
try:
|
||||||
|
executor.stop_strategy(sid)
|
||||||
|
except Exception as e:
|
||||||
|
pass # 忽略停止错误
|
||||||
|
|
||||||
|
# 然后删除
|
||||||
|
result = get_strategy_service().batch_delete_strategies(strategy_ids)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'code': 1 if result['success'] else 0,
|
||||||
|
'msg': f"成功删除 {len(result.get('success_ids', []))} 个策略",
|
||||||
|
'data': result
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"batch_delete_strategies failed: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
|
||||||
|
|
||||||
|
|
||||||
@strategy_bp.route('/strategies/update', methods=['PUT'])
|
@strategy_bp.route('/strategies/update', methods=['PUT'])
|
||||||
def update_strategy():
|
def update_strategy():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
|
import uuid
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -534,6 +535,10 @@ class StrategyService:
|
|||||||
trading_config = payload.get('trading_config') or {}
|
trading_config = payload.get('trading_config') or {}
|
||||||
exchange_config = payload.get('exchange_config') or {}
|
exchange_config = payload.get('exchange_config') or {}
|
||||||
|
|
||||||
|
# 策略组字段
|
||||||
|
strategy_group_id = payload.get('strategy_group_id') or ''
|
||||||
|
group_base_name = payload.get('group_base_name') or ''
|
||||||
|
|
||||||
# Denormalized fields for quick list rendering
|
# Denormalized fields for quick list rendering
|
||||||
symbol = (trading_config or {}).get('symbol')
|
symbol = (trading_config or {}).get('symbol')
|
||||||
timeframe = (trading_config or {}).get('timeframe')
|
timeframe = (trading_config or {}).get('timeframe')
|
||||||
@@ -549,8 +554,9 @@ class StrategyService:
|
|||||||
(strategy_name, strategy_type, market_category, execution_mode, notification_config,
|
(strategy_name, strategy_type, market_category, execution_mode, notification_config,
|
||||||
status, symbol, timeframe, initial_capital, leverage, market_type,
|
status, symbol, timeframe, initial_capital, leverage, market_type,
|
||||||
exchange_config, indicator_config, trading_config, ai_model_config, decide_interval,
|
exchange_config, indicator_config, trading_config, ai_model_config, decide_interval,
|
||||||
|
strategy_group_id, group_base_name,
|
||||||
created_at, updated_at)
|
created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
name,
|
name,
|
||||||
@@ -569,6 +575,8 @@ class StrategyService:
|
|||||||
self._dump_json_or_encrypt(trading_config, encrypt=False),
|
self._dump_json_or_encrypt(trading_config, encrypt=False),
|
||||||
self._dump_json_or_encrypt(payload.get('ai_model_config') or {}, encrypt=False),
|
self._dump_json_or_encrypt(payload.get('ai_model_config') or {}, encrypt=False),
|
||||||
int(payload.get('decide_interval') or 300),
|
int(payload.get('decide_interval') or 300),
|
||||||
|
strategy_group_id,
|
||||||
|
group_base_name,
|
||||||
now,
|
now,
|
||||||
now
|
now
|
||||||
)
|
)
|
||||||
@@ -578,6 +586,150 @@ class StrategyService:
|
|||||||
cur.close()
|
cur.close()
|
||||||
return int(new_id)
|
return int(new_id)
|
||||||
|
|
||||||
|
def batch_create_strategies(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
批量创建策略(多币种)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
payload: 包含 symbols(数组)和其他策略配置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
'success': True/False,
|
||||||
|
'strategy_group_id': '...',
|
||||||
|
'created_ids': [1, 2, 3],
|
||||||
|
'failed_symbols': []
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
symbols = payload.get('symbols') or []
|
||||||
|
if not symbols or not isinstance(symbols, list):
|
||||||
|
raise ValueError("symbols array is required")
|
||||||
|
|
||||||
|
base_name = (payload.get('strategy_name') or '').strip()
|
||||||
|
if not base_name:
|
||||||
|
raise ValueError("strategy_name is required")
|
||||||
|
|
||||||
|
# 生成策略组ID
|
||||||
|
strategy_group_id = str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
|
created_ids = []
|
||||||
|
failed_symbols = []
|
||||||
|
|
||||||
|
for symbol in symbols:
|
||||||
|
try:
|
||||||
|
# 为每个币种创建单独的策略
|
||||||
|
single_payload = dict(payload)
|
||||||
|
|
||||||
|
# 解析 symbol(可能是 "Market:SYMBOL" 格式)
|
||||||
|
if isinstance(symbol, str) and ':' in symbol:
|
||||||
|
parts = symbol.split(':', 1)
|
||||||
|
market_category = parts[0]
|
||||||
|
symbol_name = parts[1]
|
||||||
|
else:
|
||||||
|
market_category = payload.get('market_category') or 'Crypto'
|
||||||
|
symbol_name = symbol
|
||||||
|
|
||||||
|
# 策略名称加币种后缀
|
||||||
|
single_payload['strategy_name'] = f"{base_name}-{symbol_name}"
|
||||||
|
single_payload['strategy_group_id'] = strategy_group_id
|
||||||
|
single_payload['group_base_name'] = base_name
|
||||||
|
single_payload['market_category'] = market_category
|
||||||
|
|
||||||
|
# 更新 trading_config 中的 symbol
|
||||||
|
trading_config = dict(single_payload.get('trading_config') or {})
|
||||||
|
trading_config['symbol'] = symbol_name
|
||||||
|
single_payload['trading_config'] = trading_config
|
||||||
|
|
||||||
|
new_id = self.create_strategy(single_payload)
|
||||||
|
created_ids.append(new_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create strategy for symbol {symbol}: {e}")
|
||||||
|
failed_symbols.append({'symbol': symbol, 'error': str(e)})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': len(created_ids) > 0,
|
||||||
|
'strategy_group_id': strategy_group_id,
|
||||||
|
'group_base_name': base_name,
|
||||||
|
'created_ids': created_ids,
|
||||||
|
'failed_symbols': failed_symbols,
|
||||||
|
'total_created': len(created_ids),
|
||||||
|
'total_failed': len(failed_symbols)
|
||||||
|
}
|
||||||
|
|
||||||
|
def batch_start_strategies(self, strategy_ids: List[int]) -> Dict[str, Any]:
|
||||||
|
"""批量启动策略"""
|
||||||
|
success_ids = []
|
||||||
|
failed_ids = []
|
||||||
|
|
||||||
|
for sid in strategy_ids:
|
||||||
|
try:
|
||||||
|
self.update_strategy_status(sid, 'running')
|
||||||
|
success_ids.append(sid)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start strategy {sid}: {e}")
|
||||||
|
failed_ids.append({'id': sid, 'error': str(e)})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': len(success_ids) > 0,
|
||||||
|
'success_ids': success_ids,
|
||||||
|
'failed_ids': failed_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
def batch_stop_strategies(self, strategy_ids: List[int]) -> Dict[str, Any]:
|
||||||
|
"""批量停止策略"""
|
||||||
|
success_ids = []
|
||||||
|
failed_ids = []
|
||||||
|
|
||||||
|
for sid in strategy_ids:
|
||||||
|
try:
|
||||||
|
self.update_strategy_status(sid, 'stopped')
|
||||||
|
success_ids.append(sid)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to stop strategy {sid}: {e}")
|
||||||
|
failed_ids.append({'id': sid, 'error': str(e)})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': len(success_ids) > 0,
|
||||||
|
'success_ids': success_ids,
|
||||||
|
'failed_ids': failed_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
def batch_delete_strategies(self, strategy_ids: List[int]) -> Dict[str, Any]:
|
||||||
|
"""批量删除策略"""
|
||||||
|
success_ids = []
|
||||||
|
failed_ids = []
|
||||||
|
|
||||||
|
for sid in strategy_ids:
|
||||||
|
try:
|
||||||
|
self.delete_strategy(sid)
|
||||||
|
success_ids.append(sid)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to delete strategy {sid}: {e}")
|
||||||
|
failed_ids.append({'id': sid, 'error': str(e)})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': len(success_ids) > 0,
|
||||||
|
'success_ids': success_ids,
|
||||||
|
'failed_ids': failed_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_strategies_by_group(self, strategy_group_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""获取策略组内的所有策略"""
|
||||||
|
try:
|
||||||
|
with get_db_connection() as db:
|
||||||
|
cur = db.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id FROM qd_strategies_trading WHERE strategy_group_id = ?",
|
||||||
|
(strategy_group_id,)
|
||||||
|
)
|
||||||
|
rows = cur.fetchall() or []
|
||||||
|
cur.close()
|
||||||
|
return [row['id'] for row in rows]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"get_strategies_by_group failed: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
def update_strategy(self, strategy_id: int, payload: Dict[str, Any]) -> bool:
|
def update_strategy(self, strategy_id: int, payload: Dict[str, Any]) -> bool:
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
existing = self.get_strategy(strategy_id)
|
existing = self.get_strategy(strategy_id)
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ def _init_db_schema(conn):
|
|||||||
ensure_columns("qd_strategies_trading", {
|
ensure_columns("qd_strategies_trading", {
|
||||||
"market_category": "TEXT DEFAULT 'Crypto'",
|
"market_category": "TEXT DEFAULT 'Crypto'",
|
||||||
"execution_mode": "TEXT DEFAULT 'signal'",
|
"execution_mode": "TEXT DEFAULT 'signal'",
|
||||||
"notification_config": "TEXT DEFAULT ''"
|
"notification_config": "TEXT DEFAULT ''",
|
||||||
|
"strategy_group_id": "TEXT DEFAULT ''", # 策略组ID,批量创建的策略共享同一个组ID
|
||||||
|
"group_base_name": "TEXT DEFAULT ''" # 策略组基础名称(用于显示)
|
||||||
})
|
})
|
||||||
|
|
||||||
# 2. 持仓表
|
# 2. 持仓表
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ const api = {
|
|||||||
strategies: '/api/strategies',
|
strategies: '/api/strategies',
|
||||||
strategyDetail: '/api/strategies/detail',
|
strategyDetail: '/api/strategies/detail',
|
||||||
createStrategy: '/api/strategies/create',
|
createStrategy: '/api/strategies/create',
|
||||||
|
batchCreateStrategies: '/api/strategies/batch-create',
|
||||||
updateStrategy: '/api/strategies/update',
|
updateStrategy: '/api/strategies/update',
|
||||||
stopStrategy: '/api/strategies/stop',
|
stopStrategy: '/api/strategies/stop',
|
||||||
startStrategy: '/api/strategies/start',
|
startStrategy: '/api/strategies/start',
|
||||||
deleteStrategy: '/api/strategies/delete',
|
deleteStrategy: '/api/strategies/delete',
|
||||||
|
batchStartStrategies: '/api/strategies/batch-start',
|
||||||
|
batchStopStrategies: '/api/strategies/batch-stop',
|
||||||
|
batchDeleteStrategies: '/api/strategies/batch-delete',
|
||||||
testConnection: '/api/strategies/test-connection',
|
testConnection: '/api/strategies/test-connection',
|
||||||
trades: '/api/strategies/trades',
|
trades: '/api/strategies/trades',
|
||||||
positions: '/api/strategies/positions',
|
positions: '/api/strategies/positions',
|
||||||
@@ -59,6 +63,20 @@ export function createStrategy (data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建策略(多币种)
|
||||||
|
* @param {Object} data - 策略数据
|
||||||
|
* @param {string} data.strategy_name - 策略基础名称
|
||||||
|
* @param {Array} data.symbols - 币种数组,如 ["Crypto:BTC/USDT", "Crypto:ETH/USDT"]
|
||||||
|
*/
|
||||||
|
export function batchCreateStrategies (data) {
|
||||||
|
return request({
|
||||||
|
url: api.batchCreateStrategies,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新策略
|
* 更新策略
|
||||||
* @param {number} id - 策略ID
|
* @param {number} id - 策略ID
|
||||||
@@ -113,6 +131,48 @@ export function deleteStrategy (id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量启动策略
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {Array} data.strategy_ids - 策略ID数组
|
||||||
|
* @param {string} data.strategy_group_id - 策略组ID(可选,与strategy_ids二选一)
|
||||||
|
*/
|
||||||
|
export function batchStartStrategies (data) {
|
||||||
|
return request({
|
||||||
|
url: api.batchStartStrategies,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量停止策略
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {Array} data.strategy_ids - 策略ID数组
|
||||||
|
* @param {string} data.strategy_group_id - 策略组ID(可选,与strategy_ids二选一)
|
||||||
|
*/
|
||||||
|
export function batchStopStrategies (data) {
|
||||||
|
return request({
|
||||||
|
url: api.batchStopStrategies,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除策略
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {Array} data.strategy_ids - 策略ID数组
|
||||||
|
* @param {string} data.strategy_group_id - 策略组ID(可选,与strategy_ids二选一)
|
||||||
|
*/
|
||||||
|
export function batchDeleteStrategies (data) {
|
||||||
|
return request({
|
||||||
|
url: api.batchDeleteStrategies,
|
||||||
|
method: 'delete',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试交易所连接
|
* 测试交易所连接
|
||||||
* @param {Object} exchangeConfig - 交易所配置
|
* @param {Object} exchangeConfig - 交易所配置
|
||||||
|
|||||||
@@ -1170,6 +1170,10 @@ const locale = {
|
|||||||
'trading-assistant.stopStrategy': 'Stop Strategy',
|
'trading-assistant.stopStrategy': 'Stop Strategy',
|
||||||
'trading-assistant.editStrategy': 'Edit Strategy',
|
'trading-assistant.editStrategy': 'Edit Strategy',
|
||||||
'trading-assistant.deleteStrategy': 'Delete Strategy',
|
'trading-assistant.deleteStrategy': 'Delete Strategy',
|
||||||
|
'trading-assistant.startAll': 'Start All',
|
||||||
|
'trading-assistant.stopAll': 'Stop All',
|
||||||
|
'trading-assistant.deleteAll': 'Delete All',
|
||||||
|
'trading-assistant.symbolCount': 'symbols',
|
||||||
'trading-assistant.status.running': 'Running',
|
'trading-assistant.status.running': 'Running',
|
||||||
'trading-assistant.status.stopped': 'Stopped',
|
'trading-assistant.status.stopped': 'Stopped',
|
||||||
'trading-assistant.status.error': 'Error',
|
'trading-assistant.status.error': 'Error',
|
||||||
@@ -1202,7 +1206,9 @@ const locale = {
|
|||||||
'trading-assistant.form.testConnection': 'Test Connection',
|
'trading-assistant.form.testConnection': 'Test Connection',
|
||||||
'trading-assistant.form.strategyName': 'Strategy Name',
|
'trading-assistant.form.strategyName': 'Strategy Name',
|
||||||
'trading-assistant.form.symbol': 'Trading Pair',
|
'trading-assistant.form.symbol': 'Trading Pair',
|
||||||
|
'trading-assistant.form.symbols': 'Trading Pairs (Multi-select)',
|
||||||
'trading-assistant.form.symbolHint': 'Symbol format depends on the selected market',
|
'trading-assistant.form.symbolHint': 'Symbol format depends on the selected market',
|
||||||
|
'trading-assistant.form.symbolsHint': 'Select multiple pairs to create strategies for each',
|
||||||
'trading-assistant.form.symbolHintCrypto': 'Crypto: use trading pairs like BTC/USDT',
|
'trading-assistant.form.symbolHintCrypto': 'Crypto: use trading pairs like BTC/USDT',
|
||||||
'trading-assistant.form.symbolHintGeneral': 'Enter the symbol for the selected market (e.g. 600519, AAPL, EURUSD).',
|
'trading-assistant.form.symbolHintGeneral': 'Enter the symbol for the selected market (e.g. 600519, AAPL, EURUSD).',
|
||||||
'trading-assistant.form.initialCapital': 'Initial Capital',
|
'trading-assistant.form.initialCapital': 'Initial Capital',
|
||||||
@@ -1312,6 +1318,14 @@ const locale = {
|
|||||||
'trading-assistant.messages.loadIndicatorsFailed': 'Failed to load indicator list, please try again later',
|
'trading-assistant.messages.loadIndicatorsFailed': 'Failed to load indicator list, please try again later',
|
||||||
'trading-assistant.messages.spotLimitations': 'Spot trading has been automatically set to long only with 1x leverage',
|
'trading-assistant.messages.spotLimitations': 'Spot trading has been automatically set to long only with 1x leverage',
|
||||||
'trading-assistant.messages.autoFillApiConfig': 'Auto-filled API configuration for this exchange from history',
|
'trading-assistant.messages.autoFillApiConfig': 'Auto-filled API configuration for this exchange from history',
|
||||||
|
'trading-assistant.messages.batchCreateSuccess': 'Successfully created {count} strategies',
|
||||||
|
'trading-assistant.messages.batchStartSuccess': 'Successfully started {count} strategies',
|
||||||
|
'trading-assistant.messages.batchStartFailed': 'Failed to start strategies',
|
||||||
|
'trading-assistant.messages.batchStopSuccess': 'Successfully stopped {count} strategies',
|
||||||
|
'trading-assistant.messages.batchStopFailed': 'Failed to stop strategies',
|
||||||
|
'trading-assistant.messages.batchDeleteSuccess': 'Successfully deleted {count} strategies',
|
||||||
|
'trading-assistant.messages.batchDeleteFailed': 'Failed to delete strategies',
|
||||||
|
'trading-assistant.messages.batchDeleteConfirm': 'Are you sure you want to delete {count} strategies in group "{name}"? This action cannot be undone.',
|
||||||
'trading-assistant.placeholders.selectIndicator': 'Please select an indicator',
|
'trading-assistant.placeholders.selectIndicator': 'Please select an indicator',
|
||||||
'trading-assistant.placeholders.selectExchange': 'Please select an exchange',
|
'trading-assistant.placeholders.selectExchange': 'Please select an exchange',
|
||||||
'trading-assistant.placeholders.selectMarketCategory': 'Please select market category',
|
'trading-assistant.placeholders.selectMarketCategory': 'Please select market category',
|
||||||
@@ -1320,6 +1334,7 @@ const locale = {
|
|||||||
'trading-assistant.placeholders.inputPassphrase': 'Please enter Passphrase',
|
'trading-assistant.placeholders.inputPassphrase': 'Please enter Passphrase',
|
||||||
'trading-assistant.placeholders.inputStrategyName': 'Please enter strategy name',
|
'trading-assistant.placeholders.inputStrategyName': 'Please enter strategy name',
|
||||||
'trading-assistant.placeholders.selectSymbol': 'Please select trading pair',
|
'trading-assistant.placeholders.selectSymbol': 'Please select trading pair',
|
||||||
|
'trading-assistant.placeholders.selectSymbols': 'Please select trading pairs (multi-select)',
|
||||||
'trading-assistant.placeholders.inputSymbol': 'Please enter symbol',
|
'trading-assistant.placeholders.inputSymbol': 'Please enter symbol',
|
||||||
'trading-assistant.placeholders.selectTimeframe': 'Please select timeframe',
|
'trading-assistant.placeholders.selectTimeframe': 'Please select timeframe',
|
||||||
'trading-assistant.placeholders.selectKlinePeriod': 'Please select K-line period',
|
'trading-assistant.placeholders.selectKlinePeriod': 'Please select K-line period',
|
||||||
@@ -1341,6 +1356,7 @@ const locale = {
|
|||||||
'trading-assistant.validation.testConnectionFailed': 'Connection test failed, please check the configuration and test again',
|
'trading-assistant.validation.testConnectionFailed': 'Connection test failed, please check the configuration and test again',
|
||||||
'trading-assistant.validation.strategyNameRequired': 'Please enter strategy name',
|
'trading-assistant.validation.strategyNameRequired': 'Please enter strategy name',
|
||||||
'trading-assistant.validation.symbolRequired': 'Please select trading pair',
|
'trading-assistant.validation.symbolRequired': 'Please select trading pair',
|
||||||
|
'trading-assistant.validation.symbolsRequired': 'Please select at least one trading pair',
|
||||||
'trading-assistant.validation.initialCapitalRequired': 'Please enter initial capital',
|
'trading-assistant.validation.initialCapitalRequired': 'Please enter initial capital',
|
||||||
'trading-assistant.validation.leverageRequired': 'Please enter leverage',
|
'trading-assistant.validation.leverageRequired': 'Please enter leverage',
|
||||||
'trading-assistant.validation.emailInvalid': 'Invalid email address',
|
'trading-assistant.validation.emailInvalid': 'Invalid email address',
|
||||||
|
|||||||
@@ -1079,6 +1079,10 @@ const locale = {
|
|||||||
'trading-assistant.stopStrategy': '停止策略',
|
'trading-assistant.stopStrategy': '停止策略',
|
||||||
'trading-assistant.editStrategy': '编辑策略',
|
'trading-assistant.editStrategy': '编辑策略',
|
||||||
'trading-assistant.deleteStrategy': '删除策略',
|
'trading-assistant.deleteStrategy': '删除策略',
|
||||||
|
'trading-assistant.startAll': '全部启动',
|
||||||
|
'trading-assistant.stopAll': '全部停止',
|
||||||
|
'trading-assistant.deleteAll': '全部删除',
|
||||||
|
'trading-assistant.symbolCount': '个币种',
|
||||||
'trading-assistant.status.running': '运行中',
|
'trading-assistant.status.running': '运行中',
|
||||||
'trading-assistant.status.stopped': '已停止',
|
'trading-assistant.status.stopped': '已停止',
|
||||||
'trading-assistant.status.error': '错误',
|
'trading-assistant.status.error': '错误',
|
||||||
@@ -1091,6 +1095,8 @@ const locale = {
|
|||||||
'trading-assistant.form.step1': '选择指标',
|
'trading-assistant.form.step1': '选择指标',
|
||||||
'trading-assistant.form.step2': '交易所配置',
|
'trading-assistant.form.step2': '交易所配置',
|
||||||
'trading-assistant.form.step3': '策略参数',
|
'trading-assistant.form.step3': '策略参数',
|
||||||
|
'trading-assistant.form.step2Params': '策略参数',
|
||||||
|
'trading-assistant.form.step3Signal': '信号通知',
|
||||||
'trading-assistant.form.indicator': '选择指标',
|
'trading-assistant.form.indicator': '选择指标',
|
||||||
'trading-assistant.form.indicatorHint': '只能选择您已购买或创建的技术指标',
|
'trading-assistant.form.indicatorHint': '只能选择您已购买或创建的技术指标',
|
||||||
'trading-assistant.form.qdtCostHints': '使用策略将消耗QDT,请确保账户有足够的QDT余额',
|
'trading-assistant.form.qdtCostHints': '使用策略将消耗QDT,请确保账户有足够的QDT余额',
|
||||||
@@ -1103,7 +1109,10 @@ const locale = {
|
|||||||
'trading-assistant.form.testConnection': '测试连接',
|
'trading-assistant.form.testConnection': '测试连接',
|
||||||
'trading-assistant.form.strategyName': '策略名称',
|
'trading-assistant.form.strategyName': '策略名称',
|
||||||
'trading-assistant.form.symbol': '交易对',
|
'trading-assistant.form.symbol': '交易对',
|
||||||
|
'trading-assistant.form.symbols': '交易对(多选)',
|
||||||
'trading-assistant.form.symbolHint': '目前仅支持加密货币交易对',
|
'trading-assistant.form.symbolHint': '目前仅支持加密货币交易对',
|
||||||
|
'trading-assistant.form.symbolHintCrypto': '加密货币:使用BTC/USDT等交易对格式',
|
||||||
|
'trading-assistant.form.symbolsHint': '选择多个交易对,将自动创建多个策略',
|
||||||
'trading-assistant.form.initialCapital': '投入金额',
|
'trading-assistant.form.initialCapital': '投入金额',
|
||||||
'trading-assistant.form.marketType': '市场类型',
|
'trading-assistant.form.marketType': '市场类型',
|
||||||
'trading-assistant.form.marketTypeFutures': '合约',
|
'trading-assistant.form.marketTypeFutures': '合约',
|
||||||
@@ -1135,6 +1144,23 @@ const locale = {
|
|||||||
'trading-assistant.form.enableAiFilterHint': '启用后,指标信号将经过AI智能过滤,提高交易质量',
|
'trading-assistant.form.enableAiFilterHint': '启用后,指标信号将经过AI智能过滤,提高交易质量',
|
||||||
'trading-assistant.form.aiFilterPrompt': '自定义提示词',
|
'trading-assistant.form.aiFilterPrompt': '自定义提示词',
|
||||||
'trading-assistant.form.aiFilterPromptHint': '为AI过滤提供自定义指令,留空则使用系统默认',
|
'trading-assistant.form.aiFilterPromptHint': '为AI过滤提供自定义指令,留空则使用系统默认',
|
||||||
|
'trading-assistant.form.executionMode': '执行模式',
|
||||||
|
'trading-assistant.form.executionModeSignal': '仅信号通知',
|
||||||
|
'trading-assistant.form.executionModeLive': '实盘自动交易',
|
||||||
|
'trading-assistant.form.liveTradingCryptoOnlyHint': '实盘交易功能仅支持加密货币市场',
|
||||||
|
'trading-assistant.form.notifyChannels': '通知渠道',
|
||||||
|
'trading-assistant.form.notifyChannelsHint': '选择信号触发时的通知方式',
|
||||||
|
'trading-assistant.form.notifyEmail': '邮箱地址',
|
||||||
|
'trading-assistant.form.notifyPhone': '手机号码',
|
||||||
|
'trading-assistant.form.notifyTelegram': 'Telegram ID',
|
||||||
|
'trading-assistant.form.notifyDiscord': 'Discord Webhook',
|
||||||
|
'trading-assistant.form.notifyWebhook': 'Webhook 地址',
|
||||||
|
'trading-assistant.form.liveTradingConfigTitle': '实盘交易配置',
|
||||||
|
'trading-assistant.form.liveTradingConfigHint': '请填写交易所API信息,实盘交易将使用您的API进行真实交易',
|
||||||
|
'trading-assistant.form.savedCredential': '已保存的凭证',
|
||||||
|
'trading-assistant.form.savedCredentialHint': '选择已保存的交易所凭证,自动填充API配置',
|
||||||
|
'trading-assistant.form.saveCredential': '保存此凭证以便下次使用',
|
||||||
|
'trading-assistant.form.credentialName': '凭证名称',
|
||||||
'trading-assistant.validation.strategyTypeRequired': '请选择策略类型',
|
'trading-assistant.validation.strategyTypeRequired': '请选择策略类型',
|
||||||
'trading-assistant.form.advancedSettings': '高级设置',
|
'trading-assistant.form.advancedSettings': '高级设置',
|
||||||
'trading-assistant.form.orderMode': '下单模式',
|
'trading-assistant.form.orderMode': '下单模式',
|
||||||
@@ -1183,6 +1209,20 @@ const locale = {
|
|||||||
'trading-assistant.messages.loadIndicatorsFailed': '加载指标列表失败,请稍后重试',
|
'trading-assistant.messages.loadIndicatorsFailed': '加载指标列表失败,请稍后重试',
|
||||||
'trading-assistant.messages.spotLimitations': '现货交易已自动设置为仅做多、1倍杠杆',
|
'trading-assistant.messages.spotLimitations': '现货交易已自动设置为仅做多、1倍杠杆',
|
||||||
'trading-assistant.messages.autoFillApiConfig': '已自动填充该交易所的历史API配置',
|
'trading-assistant.messages.autoFillApiConfig': '已自动填充该交易所的历史API配置',
|
||||||
|
'trading-assistant.messages.batchCreateSuccess': '成功创建 {count} 个策略',
|
||||||
|
'trading-assistant.messages.batchStartSuccess': '成功启动 {count} 个策略',
|
||||||
|
'trading-assistant.messages.batchStartFailed': '批量启动策略失败',
|
||||||
|
'trading-assistant.messages.batchStopSuccess': '成功停止 {count} 个策略',
|
||||||
|
'trading-assistant.messages.batchStopFailed': '批量停止策略失败',
|
||||||
|
'trading-assistant.messages.batchDeleteSuccess': '成功删除 {count} 个策略',
|
||||||
|
'trading-assistant.messages.batchDeleteFailed': '批量删除策略失败',
|
||||||
|
'trading-assistant.messages.batchDeleteConfirm': '确定要删除策略组"{name}"下的 {count} 个策略吗?此操作不可恢复。',
|
||||||
|
'trading-assistant.notify.browser': '浏览器通知',
|
||||||
|
'trading-assistant.notify.email': '邮箱',
|
||||||
|
'trading-assistant.notify.phone': '短信',
|
||||||
|
'trading-assistant.notify.telegram': 'Telegram',
|
||||||
|
'trading-assistant.notify.discord': 'Discord',
|
||||||
|
'trading-assistant.notify.webhook': 'Webhook',
|
||||||
'trading-assistant.placeholders.selectIndicator': '请选择指标',
|
'trading-assistant.placeholders.selectIndicator': '请选择指标',
|
||||||
'trading-assistant.placeholders.selectExchange': '请选择交易所',
|
'trading-assistant.placeholders.selectExchange': '请选择交易所',
|
||||||
'trading-assistant.placeholders.inputApiKey': '请输入API Key',
|
'trading-assistant.placeholders.inputApiKey': '请输入API Key',
|
||||||
@@ -1190,9 +1230,17 @@ const locale = {
|
|||||||
'trading-assistant.placeholders.inputPassphrase': '请输入Passphrase',
|
'trading-assistant.placeholders.inputPassphrase': '请输入Passphrase',
|
||||||
'trading-assistant.placeholders.inputStrategyName': '请输入策略名称',
|
'trading-assistant.placeholders.inputStrategyName': '请输入策略名称',
|
||||||
'trading-assistant.placeholders.selectSymbol': '请选择交易对',
|
'trading-assistant.placeholders.selectSymbol': '请选择交易对',
|
||||||
|
'trading-assistant.placeholders.selectSymbols': '请选择交易对(支持多选)',
|
||||||
'trading-assistant.placeholders.selectTimeframe': '请选择时间周期',
|
'trading-assistant.placeholders.selectTimeframe': '请选择时间周期',
|
||||||
'trading-assistant.placeholders.selectKlinePeriod': '请选择K线周期',
|
'trading-assistant.placeholders.selectKlinePeriod': '请选择K线周期',
|
||||||
'trading-assistant.placeholders.inputAiFilterPrompt': '请输入自定义提示词(可选)',
|
'trading-assistant.placeholders.inputAiFilterPrompt': '请输入自定义提示词(可选)',
|
||||||
|
'trading-assistant.placeholders.selectSavedCredential': '请选择已保存的凭证',
|
||||||
|
'trading-assistant.placeholders.inputEmail': '请输入邮箱地址',
|
||||||
|
'trading-assistant.placeholders.inputPhone': '请输入手机号码',
|
||||||
|
'trading-assistant.placeholders.inputTelegram': '请输入Telegram ID或Chat ID',
|
||||||
|
'trading-assistant.placeholders.inputDiscord': '请输入Discord Webhook地址',
|
||||||
|
'trading-assistant.placeholders.inputWebhook': '请输入Webhook地址',
|
||||||
|
'trading-assistant.placeholders.inputCredentialName': '请输入凭证名称(如:我的OKX账户)',
|
||||||
'trading-assistant.validation.indicatorRequired': '请选择指标',
|
'trading-assistant.validation.indicatorRequired': '请选择指标',
|
||||||
'trading-assistant.validation.exchangeRequired': '请选择交易所',
|
'trading-assistant.validation.exchangeRequired': '请选择交易所',
|
||||||
'trading-assistant.validation.apiKeyRequired': '请输入API Key',
|
'trading-assistant.validation.apiKeyRequired': '请输入API Key',
|
||||||
@@ -1203,6 +1251,9 @@ const locale = {
|
|||||||
'trading-assistant.validation.testConnectionFailed': '连接测试失败,请检查配置后重新测试',
|
'trading-assistant.validation.testConnectionFailed': '连接测试失败,请检查配置后重新测试',
|
||||||
'trading-assistant.validation.strategyNameRequired': '请输入策略名称',
|
'trading-assistant.validation.strategyNameRequired': '请输入策略名称',
|
||||||
'trading-assistant.validation.symbolRequired': '请选择交易对',
|
'trading-assistant.validation.symbolRequired': '请选择交易对',
|
||||||
|
'trading-assistant.validation.symbolsRequired': '请至少选择一个交易对',
|
||||||
|
'trading-assistant.validation.emailInvalid': '请输入有效的邮箱地址',
|
||||||
|
'trading-assistant.validation.notifyChannelRequired': '请至少选择一个通知渠道',
|
||||||
'trading-assistant.validation.initialCapitalRequired': '请输入投入金额',
|
'trading-assistant.validation.initialCapitalRequired': '请输入投入金额',
|
||||||
'trading-assistant.validation.leverageRequired': '请输入杠杆倍数',
|
'trading-assistant.validation.leverageRequired': '请输入杠杆倍数',
|
||||||
'trading-assistant.table.time': '时间',
|
'trading-assistant.table.time': '时间',
|
||||||
|
|||||||
@@ -1079,6 +1079,10 @@ const locale = {
|
|||||||
'trading-assistant.stopStrategy': '停止策略',
|
'trading-assistant.stopStrategy': '停止策略',
|
||||||
'trading-assistant.editStrategy': '編輯策略',
|
'trading-assistant.editStrategy': '編輯策略',
|
||||||
'trading-assistant.deleteStrategy': '刪除策略',
|
'trading-assistant.deleteStrategy': '刪除策略',
|
||||||
|
'trading-assistant.startAll': '全部啟動',
|
||||||
|
'trading-assistant.stopAll': '全部停止',
|
||||||
|
'trading-assistant.deleteAll': '全部刪除',
|
||||||
|
'trading-assistant.symbolCount': '個幣種',
|
||||||
'trading-assistant.status.running': '運行中',
|
'trading-assistant.status.running': '運行中',
|
||||||
'trading-assistant.status.stopped': '已停止',
|
'trading-assistant.status.stopped': '已停止',
|
||||||
'trading-assistant.status.error': '錯誤',
|
'trading-assistant.status.error': '錯誤',
|
||||||
@@ -1091,6 +1095,8 @@ const locale = {
|
|||||||
'trading-assistant.form.step1': '選擇指標',
|
'trading-assistant.form.step1': '選擇指標',
|
||||||
'trading-assistant.form.step2': '交易所配置',
|
'trading-assistant.form.step2': '交易所配置',
|
||||||
'trading-assistant.form.step3': '策略參數',
|
'trading-assistant.form.step3': '策略參數',
|
||||||
|
'trading-assistant.form.step2Params': '策略參數',
|
||||||
|
'trading-assistant.form.step3Signal': '信號通知',
|
||||||
'trading-assistant.form.indicator': '選擇指標',
|
'trading-assistant.form.indicator': '選擇指標',
|
||||||
'trading-assistant.form.indicatorHint': '只能選擇您已購買或創建的技術指標',
|
'trading-assistant.form.indicatorHint': '只能選擇您已購買或創建的技術指標',
|
||||||
'trading-assistant.form.qdtCostHints': '使用策略將消耗QDT,請確保賬戶有足夠的QDT餘額',
|
'trading-assistant.form.qdtCostHints': '使用策略將消耗QDT,請確保賬戶有足夠的QDT餘額',
|
||||||
@@ -1103,7 +1109,10 @@ const locale = {
|
|||||||
'trading-assistant.form.testConnection': '測試連接',
|
'trading-assistant.form.testConnection': '測試連接',
|
||||||
'trading-assistant.form.strategyName': '策略名稱',
|
'trading-assistant.form.strategyName': '策略名稱',
|
||||||
'trading-assistant.form.symbol': '交易對',
|
'trading-assistant.form.symbol': '交易對',
|
||||||
|
'trading-assistant.form.symbols': '交易對(多選)',
|
||||||
'trading-assistant.form.symbolHint': '目前僅支持加密貨幣交易對',
|
'trading-assistant.form.symbolHint': '目前僅支持加密貨幣交易對',
|
||||||
|
'trading-assistant.form.symbolHintCrypto': '加密貨幣:使用BTC/USDT等交易對格式',
|
||||||
|
'trading-assistant.form.symbolsHint': '選擇多個交易對,將自動創建多個策略',
|
||||||
'trading-assistant.form.initialCapital': '投入金額',
|
'trading-assistant.form.initialCapital': '投入金額',
|
||||||
'trading-assistant.form.marketType': '市場類型',
|
'trading-assistant.form.marketType': '市場類型',
|
||||||
'trading-assistant.form.marketTypeFutures': '合約',
|
'trading-assistant.form.marketTypeFutures': '合約',
|
||||||
@@ -1135,6 +1144,23 @@ const locale = {
|
|||||||
'trading-assistant.form.enableAiFilterHint': '啟用後,指標信號將經過AI智能過濾,提高交易質量',
|
'trading-assistant.form.enableAiFilterHint': '啟用後,指標信號將經過AI智能過濾,提高交易質量',
|
||||||
'trading-assistant.form.aiFilterPrompt': '自定義提示詞',
|
'trading-assistant.form.aiFilterPrompt': '自定義提示詞',
|
||||||
'trading-assistant.form.aiFilterPromptHint': '為AI過濾提供自定義指令,留空則使用系統默認',
|
'trading-assistant.form.aiFilterPromptHint': '為AI過濾提供自定義指令,留空則使用系統默認',
|
||||||
|
'trading-assistant.form.executionMode': '執行模式',
|
||||||
|
'trading-assistant.form.executionModeSignal': '僅信號通知',
|
||||||
|
'trading-assistant.form.executionModeLive': '實盤自動交易',
|
||||||
|
'trading-assistant.form.liveTradingCryptoOnlyHint': '實盤交易功能僅支持加密貨幣市場',
|
||||||
|
'trading-assistant.form.notifyChannels': '通知渠道',
|
||||||
|
'trading-assistant.form.notifyChannelsHint': '選擇信號觸發時的通知方式',
|
||||||
|
'trading-assistant.form.notifyEmail': '郵箱地址',
|
||||||
|
'trading-assistant.form.notifyPhone': '手機號碼',
|
||||||
|
'trading-assistant.form.notifyTelegram': 'Telegram ID',
|
||||||
|
'trading-assistant.form.notifyDiscord': 'Discord Webhook',
|
||||||
|
'trading-assistant.form.notifyWebhook': 'Webhook 地址',
|
||||||
|
'trading-assistant.form.liveTradingConfigTitle': '實盤交易配置',
|
||||||
|
'trading-assistant.form.liveTradingConfigHint': '請填寫交易所API資訊,實盤交易將使用您的API進行真實交易',
|
||||||
|
'trading-assistant.form.savedCredential': '已儲存的憑證',
|
||||||
|
'trading-assistant.form.savedCredentialHint': '選擇已儲存的交易所憑證,自動填充API配置',
|
||||||
|
'trading-assistant.form.saveCredential': '儲存此憑證以便下次使用',
|
||||||
|
'trading-assistant.form.credentialName': '憑證名稱',
|
||||||
'trading-assistant.validation.strategyTypeRequired': '請選擇策略類型',
|
'trading-assistant.validation.strategyTypeRequired': '請選擇策略類型',
|
||||||
'trading-assistant.form.advancedSettings': '高級設置',
|
'trading-assistant.form.advancedSettings': '高級設置',
|
||||||
'trading-assistant.form.orderMode': '下單模式',
|
'trading-assistant.form.orderMode': '下單模式',
|
||||||
@@ -1183,6 +1209,20 @@ const locale = {
|
|||||||
'trading-assistant.messages.loadIndicatorsFailed': '加載指標列表失敗,請稍後重試',
|
'trading-assistant.messages.loadIndicatorsFailed': '加載指標列表失敗,請稍後重試',
|
||||||
'trading-assistant.messages.spotLimitations': '現貨交易已自動設置爲僅做多、1倍槓杆',
|
'trading-assistant.messages.spotLimitations': '現貨交易已自動設置爲僅做多、1倍槓杆',
|
||||||
'trading-assistant.messages.autoFillApiConfig': '已自動填充該交易所的歷史API配置',
|
'trading-assistant.messages.autoFillApiConfig': '已自動填充該交易所的歷史API配置',
|
||||||
|
'trading-assistant.messages.batchCreateSuccess': '成功創建 {count} 個策略',
|
||||||
|
'trading-assistant.messages.batchStartSuccess': '成功啟動 {count} 個策略',
|
||||||
|
'trading-assistant.messages.batchStartFailed': '批量啟動策略失敗',
|
||||||
|
'trading-assistant.messages.batchStopSuccess': '成功停止 {count} 個策略',
|
||||||
|
'trading-assistant.messages.batchStopFailed': '批量停止策略失敗',
|
||||||
|
'trading-assistant.messages.batchDeleteSuccess': '成功刪除 {count} 個策略',
|
||||||
|
'trading-assistant.messages.batchDeleteFailed': '批量刪除策略失敗',
|
||||||
|
'trading-assistant.messages.batchDeleteConfirm': '確定要刪除策略組"{name}"下的 {count} 個策略嗎?此操作不可恢復。',
|
||||||
|
'trading-assistant.notify.browser': '瀏覽器通知',
|
||||||
|
'trading-assistant.notify.email': '郵箱',
|
||||||
|
'trading-assistant.notify.phone': '簡訊',
|
||||||
|
'trading-assistant.notify.telegram': 'Telegram',
|
||||||
|
'trading-assistant.notify.discord': 'Discord',
|
||||||
|
'trading-assistant.notify.webhook': 'Webhook',
|
||||||
'trading-assistant.placeholders.selectIndicator': '請選擇指標',
|
'trading-assistant.placeholders.selectIndicator': '請選擇指標',
|
||||||
'trading-assistant.placeholders.selectExchange': '請選擇交易所',
|
'trading-assistant.placeholders.selectExchange': '請選擇交易所',
|
||||||
'trading-assistant.placeholders.inputApiKey': '請輸入API Key',
|
'trading-assistant.placeholders.inputApiKey': '請輸入API Key',
|
||||||
@@ -1190,9 +1230,17 @@ const locale = {
|
|||||||
'trading-assistant.placeholders.inputPassphrase': '請輸入Passphrase',
|
'trading-assistant.placeholders.inputPassphrase': '請輸入Passphrase',
|
||||||
'trading-assistant.placeholders.inputStrategyName': '請輸入策略名稱',
|
'trading-assistant.placeholders.inputStrategyName': '請輸入策略名稱',
|
||||||
'trading-assistant.placeholders.selectSymbol': '請選擇交易對',
|
'trading-assistant.placeholders.selectSymbol': '請選擇交易對',
|
||||||
|
'trading-assistant.placeholders.selectSymbols': '請選擇交易對(支持多選)',
|
||||||
'trading-assistant.placeholders.selectTimeframe': '請選擇時間周期',
|
'trading-assistant.placeholders.selectTimeframe': '請選擇時間周期',
|
||||||
'trading-assistant.placeholders.selectKlinePeriod': '請選擇K線周期',
|
'trading-assistant.placeholders.selectKlinePeriod': '請選擇K線周期',
|
||||||
'trading-assistant.placeholders.inputAiFilterPrompt': '請輸入自定義提示詞(可選)',
|
'trading-assistant.placeholders.inputAiFilterPrompt': '請輸入自定義提示詞(可選)',
|
||||||
|
'trading-assistant.placeholders.selectSavedCredential': '請選擇已儲存的憑證',
|
||||||
|
'trading-assistant.placeholders.inputEmail': '請輸入郵箱地址',
|
||||||
|
'trading-assistant.placeholders.inputPhone': '請輸入手機號碼',
|
||||||
|
'trading-assistant.placeholders.inputTelegram': '請輸入Telegram ID或Chat ID',
|
||||||
|
'trading-assistant.placeholders.inputDiscord': '請輸入Discord Webhook地址',
|
||||||
|
'trading-assistant.placeholders.inputWebhook': '請輸入Webhook地址',
|
||||||
|
'trading-assistant.placeholders.inputCredentialName': '請輸入憑證名稱(如:我的OKX帳戶)',
|
||||||
'trading-assistant.validation.indicatorRequired': '請選擇指標',
|
'trading-assistant.validation.indicatorRequired': '請選擇指標',
|
||||||
'trading-assistant.validation.exchangeRequired': '請選擇交易所',
|
'trading-assistant.validation.exchangeRequired': '請選擇交易所',
|
||||||
'trading-assistant.validation.apiKeyRequired': '請輸入API Key',
|
'trading-assistant.validation.apiKeyRequired': '請輸入API Key',
|
||||||
@@ -1203,6 +1251,9 @@ const locale = {
|
|||||||
'trading-assistant.validation.testConnectionFailed': '連接測試失敗,請檢查配置後重新測試',
|
'trading-assistant.validation.testConnectionFailed': '連接測試失敗,請檢查配置後重新測試',
|
||||||
'trading-assistant.validation.strategyNameRequired': '請輸入策略名稱',
|
'trading-assistant.validation.strategyNameRequired': '請輸入策略名稱',
|
||||||
'trading-assistant.validation.symbolRequired': '請選擇交易對',
|
'trading-assistant.validation.symbolRequired': '請選擇交易對',
|
||||||
|
'trading-assistant.validation.symbolsRequired': '請至少選擇一個交易對',
|
||||||
|
'trading-assistant.validation.emailInvalid': '請輸入有效的郵箱地址',
|
||||||
|
'trading-assistant.validation.notifyChannelRequired': '請至少選擇一個通知渠道',
|
||||||
'trading-assistant.validation.initialCapitalRequired': '請輸入投入金額',
|
'trading-assistant.validation.initialCapitalRequired': '請輸入投入金額',
|
||||||
'trading-assistant.validation.leverageRequired': '請輸入槓杆倍數',
|
'trading-assistant.validation.leverageRequired': '請輸入槓杆倍數',
|
||||||
'trading-assistant.table.time': '時間',
|
'trading-assistant.table.time': '時間',
|
||||||
|
|||||||
@@ -20,69 +20,153 @@
|
|||||||
|
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
<a-empty v-if="!loading && strategies.length === 0" :description="$t('trading-assistant.noStrategy')" />
|
<a-empty v-if="!loading && strategies.length === 0" :description="$t('trading-assistant.noStrategy')" />
|
||||||
<a-list
|
<div v-else class="strategy-grouped-list">
|
||||||
v-else
|
<!-- 策略组列表 -->
|
||||||
:data-source="strategies"
|
<div
|
||||||
size="small"
|
v-for="group in groupedStrategies.groups"
|
||||||
>
|
:key="group.id"
|
||||||
<a-list-item
|
class="strategy-group"
|
||||||
slot="renderItem"
|
>
|
||||||
slot-scope="item"
|
<!-- 策略组头部 -->
|
||||||
|
<div class="strategy-group-header" @click="toggleGroup(group.id)">
|
||||||
|
<div class="group-header-left">
|
||||||
|
<a-icon :type="collapsedGroups[group.id] ? 'right' : 'down'" class="collapse-icon" />
|
||||||
|
<a-icon type="folder" class="group-icon" />
|
||||||
|
<span class="group-name">{{ group.baseName }}</span>
|
||||||
|
<a-tag size="small" color="blue">{{ group.strategies.length }} {{ $t('trading-assistant.symbolCount') }}</a-tag>
|
||||||
|
</div>
|
||||||
|
<div class="group-header-right" @click.stop>
|
||||||
|
<span v-if="group.runningCount > 0" class="group-status running">
|
||||||
|
{{ group.runningCount }} {{ $t('trading-assistant.status.running') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="group.stoppedCount > 0" class="group-status stopped">
|
||||||
|
{{ group.stoppedCount }} {{ $t('trading-assistant.status.stopped') }}
|
||||||
|
</span>
|
||||||
|
<a-dropdown :getPopupContainer="getDropdownContainer" :trigger="['click']">
|
||||||
|
<a-menu slot="overlay" @click="({ key }) => handleGroupMenuClick(key, group)">
|
||||||
|
<a-menu-item key="startAll">
|
||||||
|
<a-icon type="play-circle" />
|
||||||
|
{{ $t('trading-assistant.startAll') }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="stopAll">
|
||||||
|
<a-icon type="pause-circle" />
|
||||||
|
{{ $t('trading-assistant.stopAll') }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-divider />
|
||||||
|
<a-menu-item key="deleteAll" class="danger-item">
|
||||||
|
<a-icon type="delete" />
|
||||||
|
{{ $t('trading-assistant.deleteAll') }}
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
<a-button type="link" icon="more" size="small" />
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 策略组内的策略列表(可折叠) -->
|
||||||
|
<div v-show="!collapsedGroups[group.id]" class="strategy-group-content">
|
||||||
|
<div
|
||||||
|
v-for="item in group.strategies"
|
||||||
|
:key="item.id"
|
||||||
|
:class="['strategy-list-item', { active: selectedStrategy && selectedStrategy.id === item.id }]"
|
||||||
|
@click="handleSelectStrategy(item)"
|
||||||
|
>
|
||||||
|
<div class="strategy-item-content">
|
||||||
|
<div class="strategy-item-header">
|
||||||
|
<div class="strategy-name-wrapper">
|
||||||
|
<span class="info-item" v-if="item.trading_config && item.trading_config.symbol">
|
||||||
|
<a-icon type="dollar" />
|
||||||
|
{{ item.trading_config.symbol }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="status-label"
|
||||||
|
:class="[
|
||||||
|
item.status ? `status-${item.status}` : '',
|
||||||
|
{ 'status-stopped': item.status === 'stopped' }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ getStatusText(item.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="strategy-item-actions" @click.stop>
|
||||||
|
<a-dropdown :getPopupContainer="getDropdownContainer" :trigger="['click']">
|
||||||
|
<a-menu slot="overlay" @click="({ key }) => handleMenuClick(key, item)">
|
||||||
|
<a-menu-item v-if="item.status === 'stopped'" key="start">
|
||||||
|
<a-icon type="play-circle" />
|
||||||
|
{{ $t('trading-assistant.startStrategy') }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item v-if="item.status === 'running'" key="stop">
|
||||||
|
<a-icon type="pause-circle" />
|
||||||
|
{{ $t('trading-assistant.stopStrategy') }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-divider />
|
||||||
|
<a-menu-item key="edit">
|
||||||
|
<a-icon type="edit" />
|
||||||
|
{{ $t('trading-assistant.editStrategy') }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-divider />
|
||||||
|
<a-menu-item key="delete" class="danger-item">
|
||||||
|
<a-icon type="delete" />
|
||||||
|
{{ $t('trading-assistant.deleteStrategy') }}
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
<a-button type="link" icon="more" size="small" />
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未分组的策略列表 -->
|
||||||
|
<div
|
||||||
|
v-for="item in groupedStrategies.ungrouped"
|
||||||
|
:key="item.id"
|
||||||
:class="['strategy-list-item', { active: selectedStrategy && selectedStrategy.id === item.id }]"
|
:class="['strategy-list-item', { active: selectedStrategy && selectedStrategy.id === item.id }]"
|
||||||
@click="handleSelectStrategy(item)"
|
@click="handleSelectStrategy(item)"
|
||||||
>
|
>
|
||||||
<a-list-item-meta>
|
<div class="strategy-item-content">
|
||||||
<template slot="title">
|
<div class="strategy-item-header">
|
||||||
<div class="strategy-item-header">
|
<div class="strategy-name-wrapper">
|
||||||
<div class="strategy-name-wrapper">
|
<a-tag
|
||||||
<a-tag
|
v-if="item.exchange_config && item.exchange_config.exchange_id"
|
||||||
v-if="item.exchange_config && item.exchange_config.exchange_id"
|
:color="getExchangeTagColor(item.exchange_config.exchange_id)"
|
||||||
:color="getExchangeTagColor(item.exchange_config.exchange_id)"
|
size="small"
|
||||||
size="small"
|
class="exchange-tag"
|
||||||
class="exchange-tag"
|
|
||||||
>
|
|
||||||
<a-icon type="bank" style="margin-right: 4px;" />
|
|
||||||
{{ getExchangeDisplayName(item.exchange_config.exchange_id) }}
|
|
||||||
</a-tag>
|
|
||||||
<span class="strategy-name">{{ item.strategy_name }}</span>
|
|
||||||
<a-tag
|
|
||||||
v-if="item.strategy_type === 'PromptBasedStrategy'"
|
|
||||||
color="purple"
|
|
||||||
size="small"
|
|
||||||
class="strategy-type-tag"
|
|
||||||
>
|
|
||||||
<a-icon type="robot" style="margin-right: 2px;" />
|
|
||||||
AI
|
|
||||||
</a-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template slot="description">
|
|
||||||
<div class="strategy-item-info">
|
|
||||||
<!-- <span class="info-item">
|
|
||||||
<a-icon type="line-chart" />
|
|
||||||
{{ getStrategyTypeText(item.strategy_type) }}
|
|
||||||
</span> -->
|
|
||||||
<span class="info-item" v-if="item.trading_config && item.trading_config.symbol">
|
|
||||||
<a-icon type="dollar" />
|
|
||||||
{{ item.trading_config.symbol }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="status-label"
|
|
||||||
:class="[
|
|
||||||
item.status ? `status-${item.status}` : '',
|
|
||||||
{ 'status-stopped': item.status === 'stopped' }
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
{{ getStatusText(item.status) }}
|
<a-icon type="bank" style="margin-right: 4px;" />
|
||||||
</span>
|
{{ getExchangeDisplayName(item.exchange_config.exchange_id) }}
|
||||||
|
</a-tag>
|
||||||
|
<span class="strategy-name">{{ item.strategy_name }}</span>
|
||||||
|
<a-tag
|
||||||
|
v-if="item.strategy_type === 'PromptBasedStrategy'"
|
||||||
|
color="purple"
|
||||||
|
size="small"
|
||||||
|
class="strategy-type-tag"
|
||||||
|
>
|
||||||
|
<a-icon type="robot" style="margin-right: 2px;" />
|
||||||
|
AI
|
||||||
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</a-list-item-meta>
|
<div class="strategy-item-info">
|
||||||
<template slot="actions">
|
<span class="info-item" v-if="item.trading_config && item.trading_config.symbol">
|
||||||
<a-dropdown
|
<a-icon type="dollar" />
|
||||||
:getPopupContainer="getDropdownContainer"
|
{{ item.trading_config.symbol }}
|
||||||
:trigger="['click']">
|
</span>
|
||||||
|
<span
|
||||||
|
class="status-label"
|
||||||
|
:class="[
|
||||||
|
item.status ? `status-${item.status}` : '',
|
||||||
|
{ 'status-stopped': item.status === 'stopped' }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ getStatusText(item.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="strategy-item-actions" @click.stop>
|
||||||
|
<a-dropdown :getPopupContainer="getDropdownContainer" :trigger="['click']">
|
||||||
<a-menu slot="overlay" @click="({ key }) => handleMenuClick(key, item)">
|
<a-menu slot="overlay" @click="({ key }) => handleMenuClick(key, item)">
|
||||||
<a-menu-item v-if="item.status === 'stopped'" key="start">
|
<a-menu-item v-if="item.status === 'stopped'" key="start">
|
||||||
<a-icon type="play-circle" />
|
<a-icon type="play-circle" />
|
||||||
@@ -105,9 +189,9 @@
|
|||||||
</a-menu>
|
</a-menu>
|
||||||
<a-button type="link" icon="more" size="small" />
|
<a-button type="link" icon="more" size="small" />
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</template>
|
</div>
|
||||||
</a-list-item>
|
</div>
|
||||||
</a-list>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -312,8 +396,10 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item :label="$t('trading-assistant.form.symbol')">
|
<a-form-item :label="isEditMode ? $t('trading-assistant.form.symbol') : $t('trading-assistant.form.symbols')">
|
||||||
|
<!-- 编辑模式:单选 -->
|
||||||
<a-select
|
<a-select
|
||||||
|
v-if="isEditMode"
|
||||||
v-decorator="['symbol', { rules: [{ required: true, message: $t('trading-assistant.validation.symbolRequired') }] }]"
|
v-decorator="['symbol', { rules: [{ required: true, message: $t('trading-assistant.validation.symbolRequired') }] }]"
|
||||||
:placeholder="$t('trading-assistant.placeholders.selectSymbol')"
|
:placeholder="$t('trading-assistant.placeholders.selectSymbol')"
|
||||||
show-search
|
show-search
|
||||||
@@ -336,8 +422,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
<!-- 创建模式:多选 -->
|
||||||
|
<a-select
|
||||||
|
v-else
|
||||||
|
v-model="selectedSymbols"
|
||||||
|
mode="multiple"
|
||||||
|
:placeholder="$t('trading-assistant.placeholders.selectSymbols')"
|
||||||
|
show-search
|
||||||
|
:filter-option="filterWatchlistOption"
|
||||||
|
:loading="loadingWatchlist"
|
||||||
|
@change="handleMultiSymbolChange"
|
||||||
|
:getPopupContainer="(triggerNode) => triggerNode.parentNode"
|
||||||
|
:maxTagCount="3"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="item in watchlist"
|
||||||
|
:key="`${item.market}:${item.symbol}`"
|
||||||
|
:value="`${item.market}:${item.symbol}`"
|
||||||
|
>
|
||||||
|
<div class="symbol-option">
|
||||||
|
<a-tag :color="getMarketColor(item.market)" style="margin-right: 8px; margin-bottom: 0;">
|
||||||
|
{{ item.market }}
|
||||||
|
</a-tag>
|
||||||
|
<span class="symbol-name">{{ item.symbol }}</span>
|
||||||
|
<span v-if="item.name" class="symbol-name-extra">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
<div class="form-item-hint">
|
<div class="form-item-hint">
|
||||||
{{ $t('trading-assistant.form.symbolHintCrypto') }}
|
{{ isEditMode ? $t('trading-assistant.form.symbolHintCrypto') : $t('trading-assistant.form.symbolsHint') }}
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
@@ -991,7 +1104,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getStrategyList, startStrategy, stopStrategy, deleteStrategy, createStrategy, updateStrategy, testExchangeConnection, getStrategyEquityCurve } from '@/api/strategy'
|
import { getStrategyList, startStrategy, stopStrategy, deleteStrategy, updateStrategy, testExchangeConnection, getStrategyEquityCurve, batchCreateStrategies, batchStartStrategies, batchStopStrategies, batchDeleteStrategies } from '@/api/strategy'
|
||||||
import { getWatchlist } from '@/api/market'
|
import { getWatchlist } from '@/api/market'
|
||||||
import { listExchangeCredentials, getExchangeCredential, createExchangeCredential } from '@/api/credentials'
|
import { listExchangeCredentials, getExchangeCredential, createExchangeCredential } from '@/api/credentials'
|
||||||
import { baseMixin } from '@/store/app-mixin'
|
import { baseMixin } from '@/store/app-mixin'
|
||||||
@@ -1086,6 +1199,44 @@ export default {
|
|||||||
// Always depend on selectedMarketCategory to make UI reactive.
|
// Always depend on selectedMarketCategory to make UI reactive.
|
||||||
const cat = this.selectedMarketCategory || 'Crypto'
|
const cat = this.selectedMarketCategory || 'Crypto'
|
||||||
return String(cat).toLowerCase() === 'crypto'
|
return String(cat).toLowerCase() === 'crypto'
|
||||||
|
},
|
||||||
|
// 策略分组显示
|
||||||
|
groupedStrategies () {
|
||||||
|
const groups = {}
|
||||||
|
const ungrouped = []
|
||||||
|
|
||||||
|
for (const s of this.strategies) {
|
||||||
|
const groupId = s.strategy_group_id
|
||||||
|
if (groupId && groupId.trim()) {
|
||||||
|
if (!groups[groupId]) {
|
||||||
|
groups[groupId] = {
|
||||||
|
id: groupId,
|
||||||
|
baseName: s.group_base_name || s.strategy_name.split('-')[0],
|
||||||
|
strategies: [],
|
||||||
|
// 统计信息
|
||||||
|
runningCount: 0,
|
||||||
|
stoppedCount: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groups[groupId].strategies.push(s)
|
||||||
|
if (s.status === 'running') {
|
||||||
|
groups[groupId].runningCount++
|
||||||
|
} else {
|
||||||
|
groups[groupId].stoppedCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ungrouped.push(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为数组,按创建时间排序
|
||||||
|
const groupList = Object.values(groups).sort((a, b) => {
|
||||||
|
const aTime = Math.max(...a.strategies.map(s => s.created_at || 0))
|
||||||
|
const bTime = Math.max(...b.strategies.map(s => s.created_at || 0))
|
||||||
|
return bTime - aTime
|
||||||
|
})
|
||||||
|
|
||||||
|
return { groups: groupList, ungrouped }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@@ -1129,7 +1280,11 @@ export default {
|
|||||||
loadingExchangeCredentials: false,
|
loadingExchangeCredentials: false,
|
||||||
exchangeCredentials: [],
|
exchangeCredentials: [],
|
||||||
saveCredentialUi: false,
|
saveCredentialUi: false,
|
||||||
suppressApiClearOnce: false
|
suppressApiClearOnce: false,
|
||||||
|
// 多币种选择(创建模式)
|
||||||
|
selectedSymbols: [],
|
||||||
|
// 策略组折叠状态
|
||||||
|
collapsedGroups: {}
|
||||||
// Market category is inferred from Step 1 watchlist symbol ("Market:SYMBOL").
|
// Market category is inferred from Step 1 watchlist symbol ("Market:SYMBOL").
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1191,6 +1346,28 @@ export default {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleMultiSymbolChange (vals) {
|
||||||
|
// vals: 数组,如 ["Crypto:BTC/USDT", "Crypto:ETH/USDT"]
|
||||||
|
this.selectedSymbols = vals || []
|
||||||
|
|
||||||
|
// 根据选中的币种更新市场类型
|
||||||
|
if (vals && vals.length > 0) {
|
||||||
|
const firstVal = vals[0]
|
||||||
|
if (typeof firstVal === 'string' && firstVal.includes(':')) {
|
||||||
|
const idx = firstVal.indexOf(':')
|
||||||
|
const market = firstVal.slice(0, idx)
|
||||||
|
this.selectedMarketCategory = market || 'Crypto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-crypto markets cannot use live trading
|
||||||
|
if (this.selectedMarketCategory !== 'Crypto') {
|
||||||
|
this.executionModeUi = 'signal'
|
||||||
|
try {
|
||||||
|
this.form && this.form.setFieldsValue && this.form.setFieldsValue({ execution_mode: 'signal' })
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
},
|
||||||
async loadExchangeCredentials () {
|
async loadExchangeCredentials () {
|
||||||
this.loadingExchangeCredentials = true
|
this.loadingExchangeCredentials = true
|
||||||
try {
|
try {
|
||||||
@@ -1344,6 +1521,7 @@ export default {
|
|||||||
this.entryPctMaxUi = 100
|
this.entryPctMaxUi = 100
|
||||||
this.aiFilterEnabledUi = false
|
this.aiFilterEnabledUi = false
|
||||||
this.selectedMarketCategory = 'Crypto'
|
this.selectedMarketCategory = 'Crypto'
|
||||||
|
this.selectedSymbols = [] // 重置多币种选择
|
||||||
|
|
||||||
this.form.resetFields()
|
this.form.resetFields()
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
@@ -1681,6 +1859,83 @@ export default {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
toggleGroup (groupId) {
|
||||||
|
this.$set(this.collapsedGroups, groupId, !this.collapsedGroups[groupId])
|
||||||
|
},
|
||||||
|
async handleGroupMenuClick (key, group) {
|
||||||
|
const strategyIds = group.strategies.map(s => s.id)
|
||||||
|
switch (key) {
|
||||||
|
case 'startAll':
|
||||||
|
await this.handleBatchStartStrategies(strategyIds, group.baseName)
|
||||||
|
break
|
||||||
|
case 'stopAll':
|
||||||
|
await this.handleBatchStopStrategies(strategyIds, group.baseName)
|
||||||
|
break
|
||||||
|
case 'deleteAll':
|
||||||
|
await this.handleBatchDeleteStrategies(strategyIds, group.baseName)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleBatchStartStrategies (strategyIds, groupName) {
|
||||||
|
try {
|
||||||
|
const res = await batchStartStrategies({ strategy_ids: strategyIds })
|
||||||
|
if (res.code === 1) {
|
||||||
|
const count = res.data?.success_ids?.length || strategyIds.length
|
||||||
|
this.$message.success(this.$t('trading-assistant.messages.batchStartSuccess', { count }))
|
||||||
|
this.loadStrategies()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || this.$t('trading-assistant.messages.batchStartFailed'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error(this.$t('trading-assistant.messages.batchStartFailed'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleBatchStopStrategies (strategyIds, groupName) {
|
||||||
|
try {
|
||||||
|
const res = await batchStopStrategies({ strategy_ids: strategyIds })
|
||||||
|
if (res.code === 1) {
|
||||||
|
const count = res.data?.success_ids?.length || strategyIds.length
|
||||||
|
this.$message.success(this.$t('trading-assistant.messages.batchStopSuccess', { count }))
|
||||||
|
this.loadStrategies()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || this.$t('trading-assistant.messages.batchStopFailed'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error(this.$t('trading-assistant.messages.batchStopFailed'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleBatchDeleteStrategies (strategyIds, groupName) {
|
||||||
|
const confirmText = this.$t('trading-assistant.messages.batchDeleteConfirm', {
|
||||||
|
count: strategyIds.length,
|
||||||
|
name: groupName
|
||||||
|
})
|
||||||
|
this.$confirm({
|
||||||
|
title: this.$t('trading-assistant.deleteAll'),
|
||||||
|
content: confirmText,
|
||||||
|
okText: this.$t('trading-assistant.deleteAll'),
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: this.$t('trading-assistant.form.cancel'),
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const res = await batchDeleteStrategies({ strategy_ids: strategyIds })
|
||||||
|
if (res.code === 1) {
|
||||||
|
const count = res.data?.success_ids?.length || strategyIds.length
|
||||||
|
this.$message.success(this.$t('trading-assistant.messages.batchDeleteSuccess', { count }))
|
||||||
|
// 如果删除的策略包含当前选中的策略,清空选中状态
|
||||||
|
if (this.selectedStrategy && strategyIds.includes(this.selectedStrategy.id)) {
|
||||||
|
this.selectedStrategy = null
|
||||||
|
this.stopEquityPolling()
|
||||||
|
}
|
||||||
|
this.loadStrategies()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || this.$t('trading-assistant.messages.batchDeleteFailed'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error(this.$t('trading-assistant.messages.batchDeleteFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
async handleStartStrategy (id) {
|
async handleStartStrategy (id) {
|
||||||
try {
|
try {
|
||||||
const res = await startStrategy(id)
|
const res = await startStrategy(id)
|
||||||
@@ -2165,18 +2420,31 @@ export default {
|
|||||||
handleNext () {
|
handleNext () {
|
||||||
if (this.currentStep === 0) {
|
if (this.currentStep === 0) {
|
||||||
// Step 1: basic config (strategy name/symbol/capital/leverage/market type/timeframe)
|
// Step 1: basic config (strategy name/symbol/capital/leverage/market type/timeframe)
|
||||||
|
// 编辑模式验证 symbol,创建模式验证 selectedSymbols(多选)
|
||||||
const fieldsToValidate = [
|
const fieldsToValidate = [
|
||||||
'indicator_id',
|
'indicator_id',
|
||||||
'strategy_name',
|
'strategy_name',
|
||||||
'symbol',
|
|
||||||
'initial_capital',
|
'initial_capital',
|
||||||
'market_type',
|
'market_type',
|
||||||
'leverage',
|
'leverage',
|
||||||
'trade_direction',
|
'trade_direction',
|
||||||
'timeframe'
|
'timeframe'
|
||||||
]
|
]
|
||||||
|
// 编辑模式需要验证 symbol 字段
|
||||||
|
if (this.isEditMode) {
|
||||||
|
fieldsToValidate.push('symbol')
|
||||||
|
}
|
||||||
this.form.validateFields(fieldsToValidate, (err, values) => {
|
this.form.validateFields(fieldsToValidate, (err, values) => {
|
||||||
if (err) return
|
if (err) return
|
||||||
|
|
||||||
|
// 创建模式:验证多币种选择
|
||||||
|
if (!this.isEditMode) {
|
||||||
|
if (!this.selectedSymbols || this.selectedSymbols.length === 0) {
|
||||||
|
this.$message.warning(this.$t('trading-assistant.validation.symbolsRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Enforce spot limitations
|
// Enforce spot limitations
|
||||||
try {
|
try {
|
||||||
const marketType = (values && values.market_type) || this.form.getFieldValue('market_type')
|
const marketType = (values && values.market_type) || this.form.getFieldValue('market_type')
|
||||||
@@ -2278,18 +2546,10 @@ export default {
|
|||||||
if (leverage > 125) leverage = 125
|
if (leverage > 125) leverage = 125
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watchlist-style symbol value: "Market:SYMBOL"
|
// 构建基础 payload
|
||||||
let parsedMarketCategory = (values.market_category || this.selectedMarketCategory || 'Crypto')
|
const basePayload = {
|
||||||
let parsedSymbol = values.symbol
|
|
||||||
if (typeof parsedSymbol === 'string' && parsedSymbol.includes(':')) {
|
|
||||||
const idx = parsedSymbol.indexOf(':')
|
|
||||||
parsedMarketCategory = parsedSymbol.slice(0, idx) || parsedMarketCategory
|
|
||||||
parsedSymbol = parsedSymbol.slice(idx + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
strategy_name: values.strategy_name,
|
strategy_name: values.strategy_name,
|
||||||
market_category: parsedMarketCategory,
|
market_category: this.selectedMarketCategory || 'Crypto',
|
||||||
execution_mode: values.execution_mode || 'signal',
|
execution_mode: values.execution_mode || 'signal',
|
||||||
notification_config: notificationConfig,
|
notification_config: notificationConfig,
|
||||||
indicator_config: {
|
indicator_config: {
|
||||||
@@ -2305,7 +2565,6 @@ export default {
|
|||||||
passphrase: this.needsPassphrase ? values.passphrase : undefined
|
passphrase: this.needsPassphrase ? values.passphrase : undefined
|
||||||
} : undefined,
|
} : undefined,
|
||||||
trading_config: {
|
trading_config: {
|
||||||
symbol: parsedSymbol,
|
|
||||||
initial_capital: values.initial_capital,
|
initial_capital: values.initial_capital,
|
||||||
leverage: leverage,
|
leverage: leverage,
|
||||||
trade_direction: tradeDirection,
|
trade_direction: tradeDirection,
|
||||||
@@ -2347,15 +2606,31 @@ export default {
|
|||||||
|
|
||||||
let res
|
let res
|
||||||
if (this.editingStrategy) {
|
if (this.editingStrategy) {
|
||||||
res = await updateStrategy(this.editingStrategy.id, payload)
|
// 编辑模式:更新单个策略
|
||||||
|
let parsedSymbol = values.symbol
|
||||||
|
if (typeof parsedSymbol === 'string' && parsedSymbol.includes(':')) {
|
||||||
|
const idx = parsedSymbol.indexOf(':')
|
||||||
|
basePayload.market_category = parsedSymbol.slice(0, idx) || basePayload.market_category
|
||||||
|
parsedSymbol = parsedSymbol.slice(idx + 1)
|
||||||
|
}
|
||||||
|
basePayload.trading_config.symbol = parsedSymbol
|
||||||
|
res = await updateStrategy(this.editingStrategy.id, basePayload)
|
||||||
} else {
|
} else {
|
||||||
payload.user_id = 1
|
// 创建模式:批量创建策略
|
||||||
payload.strategy_type = 'IndicatorStrategy'
|
basePayload.user_id = 1
|
||||||
res = await createStrategy(payload)
|
basePayload.strategy_type = 'IndicatorStrategy'
|
||||||
|
basePayload.symbols = this.selectedSymbols // 多币种数组
|
||||||
|
|
||||||
|
res = await batchCreateStrategies(basePayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.code === 1) {
|
if (res.code === 1) {
|
||||||
this.$message.success(this.isEditMode ? this.$t('trading-assistant.messages.updateSuccess') : this.$t('trading-assistant.messages.createSuccess'))
|
if (this.isEditMode) {
|
||||||
|
this.$message.success(this.$t('trading-assistant.messages.updateSuccess'))
|
||||||
|
} else {
|
||||||
|
const totalCreated = res.data?.total_created || this.selectedSymbols.length
|
||||||
|
this.$message.success(this.$t('trading-assistant.messages.batchCreateSuccess', { count: totalCreated }))
|
||||||
|
}
|
||||||
if (isLive && values.save_credential) {
|
if (isLive && values.save_credential) {
|
||||||
// Save credential to vault (best-effort)
|
// Save credential to vault (best-effort)
|
||||||
try {
|
try {
|
||||||
@@ -2806,6 +3081,121 @@ export default {
|
|||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 策略分组列表
|
||||||
|
.strategy-grouped-list {
|
||||||
|
.strategy-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: @border-radius-md;
|
||||||
|
border: 1px solid #e8ecf1;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.strategy-group-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #e8f4fd 0%, #e3f0fc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: @primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1e3a5f;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.group-status {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
&.running {
|
||||||
|
background: rgba(14, 203, 129, 0.1);
|
||||||
|
color: @success-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stopped {
|
||||||
|
background: rgba(246, 70, 93, 0.1);
|
||||||
|
color: @danger-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-group-content {
|
||||||
|
padding: 4px 8px 8px;
|
||||||
|
|
||||||
|
.strategy-list-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
margin-left: 20px;
|
||||||
|
border-left: 2px solid #e8ecf1;
|
||||||
|
background: #fafbfc;
|
||||||
|
border-radius: 0 @border-radius-sm @border-radius-sm 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f0f7ff;
|
||||||
|
border-left-color: @primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #e6f4ff;
|
||||||
|
border-left-color: @primary-color;
|
||||||
|
border-left-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-item-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-item-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.strategy-list-item {
|
.strategy-list-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
|
|||||||
Reference in New Issue
Block a user