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:
TIANHE
2026-01-11 22:15:54 +08:00
parent 74138b4cb2
commit e7cb9c6493
8 changed files with 972 additions and 85 deletions
+165
View File
@@ -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:
+153 -1
View File
@@ -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)
+3 -1
View File
@@ -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. 持仓表
+60
View File
@@ -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 - 交易所配置
+16
View File
@@ -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',
+51
View File
@@ -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': '时间',
+51
View File
@@ -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;