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
|
||||
|
||||
|
||||
@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'])
|
||||
def update_strategy():
|
||||
try:
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
import time
|
||||
import json
|
||||
import threading
|
||||
import uuid
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
@@ -534,6 +535,10 @@ class StrategyService:
|
||||
trading_config = payload.get('trading_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
|
||||
symbol = (trading_config or {}).get('symbol')
|
||||
timeframe = (trading_config or {}).get('timeframe')
|
||||
@@ -549,8 +554,9 @@ class StrategyService:
|
||||
(strategy_name, strategy_type, market_category, execution_mode, notification_config,
|
||||
status, symbol, timeframe, initial_capital, leverage, market_type,
|
||||
exchange_config, indicator_config, trading_config, ai_model_config, decide_interval,
|
||||
strategy_group_id, group_base_name,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
name,
|
||||
@@ -569,6 +575,8 @@ class StrategyService:
|
||||
self._dump_json_or_encrypt(trading_config, encrypt=False),
|
||||
self._dump_json_or_encrypt(payload.get('ai_model_config') or {}, encrypt=False),
|
||||
int(payload.get('decide_interval') or 300),
|
||||
strategy_group_id,
|
||||
group_base_name,
|
||||
now,
|
||||
now
|
||||
)
|
||||
@@ -578,6 +586,150 @@ class StrategyService:
|
||||
cur.close()
|
||||
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:
|
||||
now = int(time.time())
|
||||
existing = self.get_strategy(strategy_id)
|
||||
|
||||
@@ -104,7 +104,9 @@ def _init_db_schema(conn):
|
||||
ensure_columns("qd_strategies_trading", {
|
||||
"market_category": "TEXT DEFAULT 'Crypto'",
|
||||
"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. 持仓表
|
||||
|
||||
@@ -5,10 +5,14 @@ const api = {
|
||||
strategies: '/api/strategies',
|
||||
strategyDetail: '/api/strategies/detail',
|
||||
createStrategy: '/api/strategies/create',
|
||||
batchCreateStrategies: '/api/strategies/batch-create',
|
||||
updateStrategy: '/api/strategies/update',
|
||||
stopStrategy: '/api/strategies/stop',
|
||||
startStrategy: '/api/strategies/start',
|
||||
deleteStrategy: '/api/strategies/delete',
|
||||
batchStartStrategies: '/api/strategies/batch-start',
|
||||
batchStopStrategies: '/api/strategies/batch-stop',
|
||||
batchDeleteStrategies: '/api/strategies/batch-delete',
|
||||
testConnection: '/api/strategies/test-connection',
|
||||
trades: '/api/strategies/trades',
|
||||
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
|
||||
@@ -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 - 交易所配置
|
||||
|
||||
@@ -1170,6 +1170,10 @@ const locale = {
|
||||
'trading-assistant.stopStrategy': 'Stop Strategy',
|
||||
'trading-assistant.editStrategy': 'Edit 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.stopped': 'Stopped',
|
||||
'trading-assistant.status.error': 'Error',
|
||||
@@ -1202,7 +1206,9 @@ const locale = {
|
||||
'trading-assistant.form.testConnection': 'Test Connection',
|
||||
'trading-assistant.form.strategyName': 'Strategy Name',
|
||||
'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.symbolsHint': 'Select multiple pairs to create strategies for each',
|
||||
'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.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.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.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.selectExchange': 'Please select an exchange',
|
||||
'trading-assistant.placeholders.selectMarketCategory': 'Please select market category',
|
||||
@@ -1320,6 +1334,7 @@ const locale = {
|
||||
'trading-assistant.placeholders.inputPassphrase': 'Please enter Passphrase',
|
||||
'trading-assistant.placeholders.inputStrategyName': 'Please enter strategy name',
|
||||
'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.selectTimeframe': 'Please select timeframe',
|
||||
'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.strategyNameRequired': 'Please enter strategy name',
|
||||
'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.leverageRequired': 'Please enter leverage',
|
||||
'trading-assistant.validation.emailInvalid': 'Invalid email address',
|
||||
|
||||
@@ -1079,6 +1079,10 @@ const locale = {
|
||||
'trading-assistant.stopStrategy': '停止策略',
|
||||
'trading-assistant.editStrategy': '编辑策略',
|
||||
'trading-assistant.deleteStrategy': '删除策略',
|
||||
'trading-assistant.startAll': '全部启动',
|
||||
'trading-assistant.stopAll': '全部停止',
|
||||
'trading-assistant.deleteAll': '全部删除',
|
||||
'trading-assistant.symbolCount': '个币种',
|
||||
'trading-assistant.status.running': '运行中',
|
||||
'trading-assistant.status.stopped': '已停止',
|
||||
'trading-assistant.status.error': '错误',
|
||||
@@ -1091,6 +1095,8 @@ const locale = {
|
||||
'trading-assistant.form.step1': '选择指标',
|
||||
'trading-assistant.form.step2': '交易所配置',
|
||||
'trading-assistant.form.step3': '策略参数',
|
||||
'trading-assistant.form.step2Params': '策略参数',
|
||||
'trading-assistant.form.step3Signal': '信号通知',
|
||||
'trading-assistant.form.indicator': '选择指标',
|
||||
'trading-assistant.form.indicatorHint': '只能选择您已购买或创建的技术指标',
|
||||
'trading-assistant.form.qdtCostHints': '使用策略将消耗QDT,请确保账户有足够的QDT余额',
|
||||
@@ -1103,7 +1109,10 @@ const locale = {
|
||||
'trading-assistant.form.testConnection': '测试连接',
|
||||
'trading-assistant.form.strategyName': '策略名称',
|
||||
'trading-assistant.form.symbol': '交易对',
|
||||
'trading-assistant.form.symbols': '交易对(多选)',
|
||||
'trading-assistant.form.symbolHint': '目前仅支持加密货币交易对',
|
||||
'trading-assistant.form.symbolHintCrypto': '加密货币:使用BTC/USDT等交易对格式',
|
||||
'trading-assistant.form.symbolsHint': '选择多个交易对,将自动创建多个策略',
|
||||
'trading-assistant.form.initialCapital': '投入金额',
|
||||
'trading-assistant.form.marketType': '市场类型',
|
||||
'trading-assistant.form.marketTypeFutures': '合约',
|
||||
@@ -1135,6 +1144,23 @@ const locale = {
|
||||
'trading-assistant.form.enableAiFilterHint': '启用后,指标信号将经过AI智能过滤,提高交易质量',
|
||||
'trading-assistant.form.aiFilterPrompt': '自定义提示词',
|
||||
'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.form.advancedSettings': '高级设置',
|
||||
'trading-assistant.form.orderMode': '下单模式',
|
||||
@@ -1183,6 +1209,20 @@ const locale = {
|
||||
'trading-assistant.messages.loadIndicatorsFailed': '加载指标列表失败,请稍后重试',
|
||||
'trading-assistant.messages.spotLimitations': '现货交易已自动设置为仅做多、1倍杠杆',
|
||||
'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.selectExchange': '请选择交易所',
|
||||
'trading-assistant.placeholders.inputApiKey': '请输入API Key',
|
||||
@@ -1190,9 +1230,17 @@ const locale = {
|
||||
'trading-assistant.placeholders.inputPassphrase': '请输入Passphrase',
|
||||
'trading-assistant.placeholders.inputStrategyName': '请输入策略名称',
|
||||
'trading-assistant.placeholders.selectSymbol': '请选择交易对',
|
||||
'trading-assistant.placeholders.selectSymbols': '请选择交易对(支持多选)',
|
||||
'trading-assistant.placeholders.selectTimeframe': '请选择时间周期',
|
||||
'trading-assistant.placeholders.selectKlinePeriod': '请选择K线周期',
|
||||
'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.exchangeRequired': '请选择交易所',
|
||||
'trading-assistant.validation.apiKeyRequired': '请输入API Key',
|
||||
@@ -1203,6 +1251,9 @@ const locale = {
|
||||
'trading-assistant.validation.testConnectionFailed': '连接测试失败,请检查配置后重新测试',
|
||||
'trading-assistant.validation.strategyNameRequired': '请输入策略名称',
|
||||
'trading-assistant.validation.symbolRequired': '请选择交易对',
|
||||
'trading-assistant.validation.symbolsRequired': '请至少选择一个交易对',
|
||||
'trading-assistant.validation.emailInvalid': '请输入有效的邮箱地址',
|
||||
'trading-assistant.validation.notifyChannelRequired': '请至少选择一个通知渠道',
|
||||
'trading-assistant.validation.initialCapitalRequired': '请输入投入金额',
|
||||
'trading-assistant.validation.leverageRequired': '请输入杠杆倍数',
|
||||
'trading-assistant.table.time': '时间',
|
||||
|
||||
@@ -1079,6 +1079,10 @@ const locale = {
|
||||
'trading-assistant.stopStrategy': '停止策略',
|
||||
'trading-assistant.editStrategy': '編輯策略',
|
||||
'trading-assistant.deleteStrategy': '刪除策略',
|
||||
'trading-assistant.startAll': '全部啟動',
|
||||
'trading-assistant.stopAll': '全部停止',
|
||||
'trading-assistant.deleteAll': '全部刪除',
|
||||
'trading-assistant.symbolCount': '個幣種',
|
||||
'trading-assistant.status.running': '運行中',
|
||||
'trading-assistant.status.stopped': '已停止',
|
||||
'trading-assistant.status.error': '錯誤',
|
||||
@@ -1091,6 +1095,8 @@ const locale = {
|
||||
'trading-assistant.form.step1': '選擇指標',
|
||||
'trading-assistant.form.step2': '交易所配置',
|
||||
'trading-assistant.form.step3': '策略參數',
|
||||
'trading-assistant.form.step2Params': '策略參數',
|
||||
'trading-assistant.form.step3Signal': '信號通知',
|
||||
'trading-assistant.form.indicator': '選擇指標',
|
||||
'trading-assistant.form.indicatorHint': '只能選擇您已購買或創建的技術指標',
|
||||
'trading-assistant.form.qdtCostHints': '使用策略將消耗QDT,請確保賬戶有足夠的QDT餘額',
|
||||
@@ -1103,7 +1109,10 @@ const locale = {
|
||||
'trading-assistant.form.testConnection': '測試連接',
|
||||
'trading-assistant.form.strategyName': '策略名稱',
|
||||
'trading-assistant.form.symbol': '交易對',
|
||||
'trading-assistant.form.symbols': '交易對(多選)',
|
||||
'trading-assistant.form.symbolHint': '目前僅支持加密貨幣交易對',
|
||||
'trading-assistant.form.symbolHintCrypto': '加密貨幣:使用BTC/USDT等交易對格式',
|
||||
'trading-assistant.form.symbolsHint': '選擇多個交易對,將自動創建多個策略',
|
||||
'trading-assistant.form.initialCapital': '投入金額',
|
||||
'trading-assistant.form.marketType': '市場類型',
|
||||
'trading-assistant.form.marketTypeFutures': '合約',
|
||||
@@ -1135,6 +1144,23 @@ const locale = {
|
||||
'trading-assistant.form.enableAiFilterHint': '啟用後,指標信號將經過AI智能過濾,提高交易質量',
|
||||
'trading-assistant.form.aiFilterPrompt': '自定義提示詞',
|
||||
'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.form.advancedSettings': '高級設置',
|
||||
'trading-assistant.form.orderMode': '下單模式',
|
||||
@@ -1183,6 +1209,20 @@ const locale = {
|
||||
'trading-assistant.messages.loadIndicatorsFailed': '加載指標列表失敗,請稍後重試',
|
||||
'trading-assistant.messages.spotLimitations': '現貨交易已自動設置爲僅做多、1倍槓杆',
|
||||
'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.selectExchange': '請選擇交易所',
|
||||
'trading-assistant.placeholders.inputApiKey': '請輸入API Key',
|
||||
@@ -1190,9 +1230,17 @@ const locale = {
|
||||
'trading-assistant.placeholders.inputPassphrase': '請輸入Passphrase',
|
||||
'trading-assistant.placeholders.inputStrategyName': '請輸入策略名稱',
|
||||
'trading-assistant.placeholders.selectSymbol': '請選擇交易對',
|
||||
'trading-assistant.placeholders.selectSymbols': '請選擇交易對(支持多選)',
|
||||
'trading-assistant.placeholders.selectTimeframe': '請選擇時間周期',
|
||||
'trading-assistant.placeholders.selectKlinePeriod': '請選擇K線周期',
|
||||
'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.exchangeRequired': '請選擇交易所',
|
||||
'trading-assistant.validation.apiKeyRequired': '請輸入API Key',
|
||||
@@ -1203,6 +1251,9 @@ const locale = {
|
||||
'trading-assistant.validation.testConnectionFailed': '連接測試失敗,請檢查配置後重新測試',
|
||||
'trading-assistant.validation.strategyNameRequired': '請輸入策略名稱',
|
||||
'trading-assistant.validation.symbolRequired': '請選擇交易對',
|
||||
'trading-assistant.validation.symbolsRequired': '請至少選擇一個交易對',
|
||||
'trading-assistant.validation.emailInvalid': '請輸入有效的郵箱地址',
|
||||
'trading-assistant.validation.notifyChannelRequired': '請至少選擇一個通知渠道',
|
||||
'trading-assistant.validation.initialCapitalRequired': '請輸入投入金額',
|
||||
'trading-assistant.validation.leverageRequired': '請輸入槓杆倍數',
|
||||
'trading-assistant.table.time': '時間',
|
||||
|
||||
@@ -20,69 +20,153 @@
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<a-empty v-if="!loading && strategies.length === 0" :description="$t('trading-assistant.noStrategy')" />
|
||||
<a-list
|
||||
v-else
|
||||
:data-source="strategies"
|
||||
size="small"
|
||||
>
|
||||
<a-list-item
|
||||
slot="renderItem"
|
||||
slot-scope="item"
|
||||
<div v-else class="strategy-grouped-list">
|
||||
<!-- 策略组列表 -->
|
||||
<div
|
||||
v-for="group in groupedStrategies.groups"
|
||||
:key="group.id"
|
||||
class="strategy-group"
|
||||
>
|
||||
<!-- 策略组头部 -->
|
||||
<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 }]"
|
||||
@click="handleSelectStrategy(item)"
|
||||
>
|
||||
<a-list-item-meta>
|
||||
<template slot="title">
|
||||
<div class="strategy-item-header">
|
||||
<div class="strategy-name-wrapper">
|
||||
<a-tag
|
||||
v-if="item.exchange_config && item.exchange_config.exchange_id"
|
||||
:color="getExchangeTagColor(item.exchange_config.exchange_id)"
|
||||
size="small"
|
||||
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' }
|
||||
]"
|
||||
<div class="strategy-item-content">
|
||||
<div class="strategy-item-header">
|
||||
<div class="strategy-name-wrapper">
|
||||
<a-tag
|
||||
v-if="item.exchange_config && item.exchange_config.exchange_id"
|
||||
:color="getExchangeTagColor(item.exchange_config.exchange_id)"
|
||||
size="small"
|
||||
class="exchange-tag"
|
||||
>
|
||||
{{ getStatusText(item.status) }}
|
||||
</span>
|
||||
<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>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template slot="actions">
|
||||
<a-dropdown
|
||||
:getPopupContainer="getDropdownContainer"
|
||||
:trigger="['click']">
|
||||
</div>
|
||||
<div class="strategy-item-info">
|
||||
<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 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" />
|
||||
@@ -105,9 +189,9 @@
|
||||
</a-menu>
|
||||
<a-button type="link" icon="more" size="small" />
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</a-col>
|
||||
@@ -312,8 +396,10 @@
|
||||
/>
|
||||
</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
|
||||
v-if="isEditMode"
|
||||
v-decorator="['symbol', { rules: [{ required: true, message: $t('trading-assistant.validation.symbolRequired') }] }]"
|
||||
:placeholder="$t('trading-assistant.placeholders.selectSymbol')"
|
||||
show-search
|
||||
@@ -336,8 +422,35 @@
|
||||
</div>
|
||||
</a-select-option>
|
||||
</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">
|
||||
{{ $t('trading-assistant.form.symbolHintCrypto') }}
|
||||
{{ isEditMode ? $t('trading-assistant.form.symbolHintCrypto') : $t('trading-assistant.form.symbolsHint') }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
@@ -991,7 +1104,7 @@
|
||||
</template>
|
||||
|
||||
<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 { listExchangeCredentials, getExchangeCredential, createExchangeCredential } from '@/api/credentials'
|
||||
import { baseMixin } from '@/store/app-mixin'
|
||||
@@ -1086,6 +1199,44 @@ export default {
|
||||
// Always depend on selectedMarketCategory to make UI reactive.
|
||||
const cat = this.selectedMarketCategory || '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 () {
|
||||
@@ -1129,7 +1280,11 @@ export default {
|
||||
loadingExchangeCredentials: false,
|
||||
exchangeCredentials: [],
|
||||
saveCredentialUi: false,
|
||||
suppressApiClearOnce: false
|
||||
suppressApiClearOnce: false,
|
||||
// 多币种选择(创建模式)
|
||||
selectedSymbols: [],
|
||||
// 策略组折叠状态
|
||||
collapsedGroups: {}
|
||||
// Market category is inferred from Step 1 watchlist symbol ("Market:SYMBOL").
|
||||
}
|
||||
},
|
||||
@@ -1191,6 +1346,28 @@ export default {
|
||||
} 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 () {
|
||||
this.loadingExchangeCredentials = true
|
||||
try {
|
||||
@@ -1344,6 +1521,7 @@ export default {
|
||||
this.entryPctMaxUi = 100
|
||||
this.aiFilterEnabledUi = false
|
||||
this.selectedMarketCategory = 'Crypto'
|
||||
this.selectedSymbols = [] // 重置多币种选择
|
||||
|
||||
this.form.resetFields()
|
||||
this.form.setFieldsValue({
|
||||
@@ -1681,6 +1859,83 @@ export default {
|
||||
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) {
|
||||
try {
|
||||
const res = await startStrategy(id)
|
||||
@@ -2165,18 +2420,31 @@ export default {
|
||||
handleNext () {
|
||||
if (this.currentStep === 0) {
|
||||
// Step 1: basic config (strategy name/symbol/capital/leverage/market type/timeframe)
|
||||
// 编辑模式验证 symbol,创建模式验证 selectedSymbols(多选)
|
||||
const fieldsToValidate = [
|
||||
'indicator_id',
|
||||
'strategy_name',
|
||||
'symbol',
|
||||
'initial_capital',
|
||||
'market_type',
|
||||
'leverage',
|
||||
'trade_direction',
|
||||
'timeframe'
|
||||
]
|
||||
// 编辑模式需要验证 symbol 字段
|
||||
if (this.isEditMode) {
|
||||
fieldsToValidate.push('symbol')
|
||||
}
|
||||
this.form.validateFields(fieldsToValidate, (err, values) => {
|
||||
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
|
||||
try {
|
||||
const marketType = (values && values.market_type) || this.form.getFieldValue('market_type')
|
||||
@@ -2278,18 +2546,10 @@ export default {
|
||||
if (leverage > 125) leverage = 125
|
||||
}
|
||||
|
||||
// Watchlist-style symbol value: "Market:SYMBOL"
|
||||
let parsedMarketCategory = (values.market_category || this.selectedMarketCategory || 'Crypto')
|
||||
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 = {
|
||||
// 构建基础 payload
|
||||
const basePayload = {
|
||||
strategy_name: values.strategy_name,
|
||||
market_category: parsedMarketCategory,
|
||||
market_category: this.selectedMarketCategory || 'Crypto',
|
||||
execution_mode: values.execution_mode || 'signal',
|
||||
notification_config: notificationConfig,
|
||||
indicator_config: {
|
||||
@@ -2305,7 +2565,6 @@ export default {
|
||||
passphrase: this.needsPassphrase ? values.passphrase : undefined
|
||||
} : undefined,
|
||||
trading_config: {
|
||||
symbol: parsedSymbol,
|
||||
initial_capital: values.initial_capital,
|
||||
leverage: leverage,
|
||||
trade_direction: tradeDirection,
|
||||
@@ -2347,15 +2606,31 @@ export default {
|
||||
|
||||
let res
|
||||
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 {
|
||||
payload.user_id = 1
|
||||
payload.strategy_type = 'IndicatorStrategy'
|
||||
res = await createStrategy(payload)
|
||||
// 创建模式:批量创建策略
|
||||
basePayload.user_id = 1
|
||||
basePayload.strategy_type = 'IndicatorStrategy'
|
||||
basePayload.symbols = this.selectedSymbols // 多币种数组
|
||||
|
||||
res = await batchCreateStrategies(basePayload)
|
||||
}
|
||||
|
||||
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) {
|
||||
// Save credential to vault (best-effort)
|
||||
try {
|
||||
@@ -2806,6 +3081,121 @@ export default {
|
||||
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 {
|
||||
cursor: pointer;
|
||||
padding: 14px 16px;
|
||||
|
||||
Reference in New Issue
Block a user