Files
DinQuant/backend_api_python/app/services/strategy.py
T
TIANHE e7cb9c6493 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)
2026-01-11 22:15:54 +08:00

810 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import time
import json
import threading
import uuid
from typing import List, Dict, Any, Optional
from datetime import datetime
from app.utils.logger import get_logger
from app.utils.db import get_db_connection
logger = get_logger(__name__)
class StrategyService:
"""Strategy service."""
# 类变量:限制连接测试并发数
_connection_test_semaphore = threading.Semaphore(5)
def __init__(self):
# Local deployment: do not use encryption/decryption.
pass
def get_running_strategies(self) -> List[Dict[str, Any]]:
"""获取所有运行中的策略(仅ID"""
try:
with get_db_connection() as db:
cursor = db.cursor()
query = "SELECT id FROM qd_strategies_trading WHERE status = 'running'"
cursor.execute(query)
results = cursor.fetchall()
cursor.close()
return [row['id'] for row in results]
except Exception as e:
logger.error(f"Failed to fetch running strategies: {str(e)}")
return []
def get_running_strategies_with_type(self) -> List[Dict[str, Any]]:
"""获取所有运行中的策略(包含类型信息)"""
try:
with get_db_connection() as db:
cursor = db.cursor()
# 假设 qd_strategies_trading 表中有 strategy_type 字段
# 如果没有,可能需要关联查询或者根据其他字段判断
# 这里假设表结构已更新
query = "SELECT id, strategy_type FROM qd_strategies_trading WHERE status = 'running'"
cursor.execute(query)
results = cursor.fetchall()
cursor.close()
strategies = [{'id': row['id'], 'strategy_type': row.get('strategy_type', '')} for row in results]
logger.info(f"Found {len(strategies)} running strategies: {strategies}")
return strategies
except Exception as e:
logger.error(f"Failed to fetch running strategies: {str(e)}")
return []
def get_exchange_symbols(self, exchange_config: Dict[str, Any]) -> Dict[str, Any]:
"""
获取交易所交易对列表 (无需API Key)
"""
try:
exchange_id = exchange_config.get('exchange_id', '')
proxies = exchange_config.get('proxies')
if not exchange_id:
return {'success': False, 'message': '请选择交易所', 'symbols': []}
# For these exchanges, prefer direct REST (no ccxt), aligned with local live-trading design.
ex = str(exchange_id or "").strip().lower()
if ex in ("bybit", "coinbaseexchange", "coinbase_exchange", "kraken", "kucoin", "gate", "bitfinex"):
import requests
def _req_json(url: str) -> Any:
r = requests.get(url, timeout=15, proxies=proxies)
r.raise_for_status()
return r.json()
symbols: List[str] = []
market_type = str(exchange_config.get("market_type") or exchange_config.get("defaultType") or "spot").strip().lower()
if market_type in ("futures", "future", "perp", "perpetual"):
market_type = "swap"
if ex == "bybit":
base = str(exchange_config.get("base_url") or exchange_config.get("baseUrl") or "https://api.bybit.com").rstrip("/")
cat = "spot" if market_type == "spot" else "linear"
j = _req_json(f"{base}/v5/market/instruments-info?category={cat}")
lst = (((j.get("result") or {}).get("list")) if isinstance(j, dict) else None) or []
if isinstance(lst, list):
for it in lst:
if not isinstance(it, dict):
continue
sym = str(it.get("symbol") or "")
status = str(it.get("status") or "").lower()
if not sym or (status and status not in ("trading", "tradable", "online")):
continue
if sym.endswith("USDT") and len(sym) > 4:
symbols.append(f"{sym[:-4]}/USDT")
symbols = sorted(list(set(symbols)))
return {'success': True, 'message': f'获取成功,共 {len(symbols)} 个交易对', 'symbols': symbols}
if ex in ("coinbaseexchange", "coinbase_exchange"):
base = str(exchange_config.get("base_url") or exchange_config.get("baseUrl") or "https://api.exchange.coinbase.com").rstrip("/")
j = _req_json(f"{base}/products")
if isinstance(j, list):
for it in j:
if not isinstance(it, dict):
continue
if str(it.get("status") or "").lower() not in ("online", ""):
continue
base_ccy = str(it.get("base_currency") or "").upper()
quote_ccy = str(it.get("quote_currency") or "").upper()
if quote_ccy == "USDT" and base_ccy:
symbols.append(f"{base_ccy}/USDT")
symbols = sorted(list(set(symbols)))
return {'success': True, 'message': f'获取成功,共 {len(symbols)} 个交易对', 'symbols': symbols}
if ex == "kraken":
if market_type == "spot":
j = _req_json("https://api.kraken.com/0/public/AssetPairs")
res = (j.get("result") if isinstance(j, dict) else None) or {}
if isinstance(res, dict):
for _k, v in res.items():
if not isinstance(v, dict):
continue
wsname = str(v.get("wsname") or "")
if not wsname or "/" not in wsname:
continue
base_ccy, quote_ccy = wsname.split("/", 1)
if str(quote_ccy).upper() == "USDT":
symbols.append(f"{str(base_ccy).upper()}/USDT")
else:
base = str(exchange_config.get("futures_base_url") or exchange_config.get("futuresBaseUrl") or "https://futures.kraken.com").rstrip("/")
j = _req_json(f"{base}/derivatives/api/v3/instruments")
instruments = j.get("instruments") if isinstance(j, dict) else None
if isinstance(instruments, list):
for it in instruments:
if not isinstance(it, dict):
continue
sym = str(it.get("symbol") or "")
typ = str(it.get("type") or "").lower()
if sym and ("perpetual" in typ or typ.startswith("pf") or sym.startswith("PF_")):
symbols.append(sym)
symbols = sorted(list(set(symbols)))
return {'success': True, 'message': f'获取成功,共 {len(symbols)} 个交易对', 'symbols': symbols}
if ex == "kucoin":
if market_type == "spot":
base = str(exchange_config.get("base_url") or exchange_config.get("baseUrl") or "https://api.kucoin.com").rstrip("/")
j = _req_json(f"{base}/api/v1/symbols")
data = (j.get("data") if isinstance(j, dict) else None) or []
if isinstance(data, list):
for it in data:
if not isinstance(it, dict):
continue
if not bool(it.get("enableTrading", True)):
continue
if str(it.get("quoteCurrency") or "").upper() != "USDT":
continue
b = str(it.get("baseCurrency") or "").upper()
if b:
symbols.append(f"{b}/USDT")
else:
base = str(exchange_config.get("futures_base_url") or exchange_config.get("futuresBaseUrl") or "https://api-futures.kucoin.com").rstrip("/")
j = _req_json(f"{base}/api/v1/contracts/active")
data = (j.get("data") if isinstance(j, dict) else None) or []
if isinstance(data, list):
for it in data:
if not isinstance(it, dict):
continue
sym = str(it.get("symbol") or "")
if not sym or not sym.upper().endswith("USDTM"):
continue
base_ccy = sym[:-5].upper()
if base_ccy == "XBT":
base_ccy = "BTC"
if base_ccy:
symbols.append(f"{base_ccy}/USDT")
symbols = sorted(list(set(symbols)))
return {'success': True, 'message': f'获取成功,共 {len(symbols)} 个交易对', 'symbols': symbols}
if ex == "gate":
base = str(exchange_config.get("base_url") or exchange_config.get("baseUrl") or "https://api.gateio.ws").rstrip("/")
if market_type == "spot":
j = _req_json(f"{base}/api/v4/spot/currency_pairs")
if isinstance(j, list):
for it in j:
if not isinstance(it, dict):
continue
if str(it.get("trade_status") or "").lower() not in ("tradable", "trading", ""):
continue
base_ccy = str(it.get("base") or "").upper()
quote_ccy = str(it.get("quote") or "").upper()
if quote_ccy == "USDT" and base_ccy:
symbols.append(f"{base_ccy}/USDT")
else:
j = _req_json(f"{base}/api/v4/futures/usdt/contracts")
if isinstance(j, list):
for it in j:
if not isinstance(it, dict):
continue
name = str(it.get("name") or it.get("contract") or "")
if name and name.upper().endswith("_USDT"):
symbols.append(name.replace("_", "/"))
symbols = sorted(list(set(symbols)))
return {'success': True, 'message': f'获取成功,共 {len(symbols)} 个交易对', 'symbols': symbols}
if ex == "bitfinex":
j = _req_json("https://api-pub.bitfinex.com/v2/conf/pub:list:pair:exchange") if market_type == "spot" else _req_json(
"https://api-pub.bitfinex.com/v2/conf/pub:list:pair:futures"
)
pairs = []
if isinstance(j, list) and j and isinstance(j[0], list):
pairs = j[0]
for p in pairs:
s = str(p or "").upper()
if not s:
continue
if market_type != "spot":
symbols.append(s)
continue
# Focus USDT (Bitfinex uses UST)
if s.endswith("UST") and len(s) > 3:
symbols.append(f"{s[:-3]}/USDT")
elif s.endswith("USDT") and len(s) > 4:
symbols.append(f"{s[:-4]}/USDT")
symbols = sorted(list(set(symbols)))
return {'success': True, 'message': f'获取成功,共 {len(symbols)} 个交易对', 'symbols': symbols}
return {'success': True, 'message': '获取成功', 'symbols': symbols}
import ccxt
# 创建交易所实例 (public only)
exchange_class = getattr(ccxt, exchange_id, None)
if not exchange_class:
return {'success': False, 'message': f'不支持的交易所: {exchange_id}', 'symbols': []}
exchange_config_dict = {
'enableRateLimit': True,
'options': {'defaultType': 'swap'} # 默认为 swap
}
if proxies:
exchange_config_dict['proxies'] = proxies
exchange = exchange_class(exchange_config_dict)
markets = exchange.load_markets()
symbols = []
for symbol, market in markets.items():
if market.get('active', False) and market.get('quote') == 'USDT':
symbols.append(symbol)
symbols.sort()
return {'success': True, 'message': f'获取成功,共 {len(symbols)} 个交易对', 'symbols': symbols}
except Exception as e:
logger.error(f"Failed to fetch symbols: {str(e)}")
return {'success': False, 'message': f'获取交易对失败: {str(e)}', 'symbols': []}
def test_exchange_connection(self, exchange_config: Dict[str, Any]) -> Dict[str, Any]:
"""
Test exchange connection via direct REST clients (no ccxt).
Notes:
- This is local-only; failures are returned as user-friendly messages.
- We do not log secrets.
"""
# Limit concurrency to protect CPU / rate limits
with StrategyService._connection_test_semaphore:
try:
from app.services.exchange_execution import resolve_exchange_config, safe_exchange_config_for_log
from app.services.live_trading.factory import create_client
from app.services.live_trading.binance import BinanceFuturesClient
from app.services.live_trading.binance_spot import BinanceSpotClient
from app.services.live_trading.okx import OkxClient
from app.services.live_trading.bitget import BitgetMixClient
from app.services.live_trading.bybit import BybitClient
from app.services.live_trading.coinbase_exchange import CoinbaseExchangeClient
from app.services.live_trading.kraken import KrakenClient
from app.services.live_trading.kraken_futures import KrakenFuturesClient
from app.services.live_trading.kucoin import KucoinSpotClient
from app.services.live_trading.kucoin import KucoinFuturesClient
from app.services.live_trading.gate import GateSpotClient, GateUsdtFuturesClient
from app.services.live_trading.bitfinex import BitfinexClient, BitfinexDerivativesClient
resolved = resolve_exchange_config(exchange_config or {})
safe_cfg = safe_exchange_config_for_log(resolved)
exchange_id = (resolved.get("exchange_id") or "").strip().lower()
if not exchange_id:
return {'success': False, 'message': 'Missing exchange_id', 'data': None}
# IMPORTANT:
# Test connection should respect configured market_type (spot vs swap).
# Otherwise Binance will default to futures endpoints (fapi) and spot-only keys will fail with -2015.
market_type = str(resolved.get("market_type") or resolved.get("defaultType") or "swap").strip().lower()
client = create_client(resolved, market_type=market_type)
client_kind = type(client).__name__
# Best-effort detect current egress IP (for Binance IP whitelist debugging).
egress_ip = ""
try:
import requests as _rq
egress_ip = str(_rq.get("https://ifconfig.me/ip", timeout=5).text or "").strip()
except Exception:
egress_ip = ""
# 1) Public connectivity
ok_public = False
try:
ok_public = bool(getattr(client, "ping")())
except Exception:
ok_public = False
if not ok_public:
return {
'success': False,
'message': f'Public ping failed: {exchange_id}',
'data': {'exchange': safe_cfg, 'client': client_kind, 'market_type': market_type, 'egress_ip': egress_ip},
}
# 2) Private credential validation (best-effort)
priv_data = None
try:
if isinstance(client, BinanceFuturesClient):
priv_data = client.get_account()
elif isinstance(client, BinanceSpotClient):
priv_data = client.get_account()
elif isinstance(client, OkxClient):
priv_data = client.get_balance()
elif isinstance(client, BitgetMixClient):
product_type = str(resolved.get("product_type") or resolved.get("productType") or "USDT-FUTURES")
priv_data = client.get_accounts(product_type=product_type)
elif isinstance(client, BybitClient):
priv_data = client.get_wallet_balance()
elif isinstance(client, CoinbaseExchangeClient):
priv_data = client.get_accounts()
elif isinstance(client, KrakenClient):
priv_data = client.get_balance()
elif isinstance(client, KrakenFuturesClient):
priv_data = client.get_accounts()
elif isinstance(client, KucoinSpotClient):
priv_data = client.get_accounts()
elif isinstance(client, KucoinFuturesClient):
priv_data = client.get_accounts()
elif isinstance(client, GateSpotClient):
priv_data = client.get_accounts()
elif isinstance(client, GateUsdtFuturesClient):
priv_data = client.get_accounts()
elif isinstance(client, BitfinexClient):
priv_data = client.get_wallets()
elif isinstance(client, BitfinexDerivativesClient):
priv_data = client.get_wallets()
except Exception as e:
msg = str(e)
# Add actionable hints for the most common Binance auth error.
if exchange_id == "binance" and ("-2015" in msg or "Invalid API-key, IP, or permissions" in msg):
# Auto A/B test: try the other market_type once to pinpoint permission mismatch.
alt_market_type = "spot" if market_type != "spot" else "swap"
alt_client_kind = ""
alt_base_url = ""
alt_ok = False
try:
alt_client = create_client(resolved, market_type=alt_market_type)
alt_client_kind = type(alt_client).__name__
alt_base_url = getattr(alt_client, "base_url", "") or ""
if isinstance(alt_client, BinanceFuturesClient) or isinstance(alt_client, BinanceSpotClient):
_ = alt_client.get_account()
alt_ok = True
except Exception:
alt_ok = False
base_url = getattr(client, "base_url", "") or ""
hint = (
f"Binance auth failed (-2015). Verify: "
f"(1) IP whitelist includes this server egress IP={egress_ip or 'unknown'}, "
f"(2) API key permissions match market_type={market_type} "
f"(spot requires Spot permissions; swap requires Futures permissions), "
f"(3) you're using binance.com keys for base_url={base_url or 'unknown'}."
)
if alt_ok:
hint += (
f" Auto-check: your key works for market_type={alt_market_type} "
f"(client={alt_client_kind}, base_url={alt_base_url or 'unknown'}) "
f"but fails for market_type={market_type}. This is almost always a permissions/product mismatch."
)
msg = f"{msg} | {hint}"
return {
'success': False,
'message': f'Auth failed: {msg}',
'data': {
'exchange': safe_cfg,
'client': client_kind,
'market_type': market_type,
'egress_ip': egress_ip,
'base_url': getattr(client, "base_url", "") or "",
},
}
return {
'success': True,
'message': 'Connection OK',
'data': {
'exchange': safe_cfg,
'client': client_kind,
'market_type': market_type,
'egress_ip': egress_ip,
'base_url': getattr(client, "base_url", "") or "",
'private': priv_data,
},
}
except Exception as e:
logger.error(f"test_exchange_connection failed: {str(e)}")
return {'success': False, 'message': f'Connection failed: {str(e)}', 'data': None}
def get_strategy_type(self, strategy_id: int) -> str:
"""Get strategy type from DB."""
try:
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"SELECT strategy_type FROM qd_strategies_trading WHERE id = ?",
(strategy_id,)
)
row = cur.fetchone()
cur.close()
return (row or {}).get('strategy_type') or 'IndicatorStrategy'
except Exception:
return 'IndicatorStrategy'
def update_strategy_status(self, strategy_id: int, status: str) -> bool:
"""Update strategy status."""
try:
now = int(time.time())
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"UPDATE qd_strategies_trading SET status = ?, updated_at = ? WHERE id = ?",
(status, now, strategy_id)
)
db.commit()
cur.close()
return True
except Exception as e:
logger.error(f"update_strategy_status failed: {e}")
return False
def _safe_json_loads(self, value: Any, default: Any):
"""Load JSON string into Python object (local deployment: plaintext only)."""
if value is None:
return default
if isinstance(value, (dict, list)):
return value
if not isinstance(value, str):
return default
s = value.strip()
if not s:
return default
try:
return json.loads(s)
except Exception:
return default
def _dump_json_or_encrypt(self, obj: Any, encrypt: bool = False) -> str:
if obj is None:
return ''
# Local deployment: always store plaintext JSON.
return json.dumps(obj, ensure_ascii=False)
def list_strategies(self, user_id: int = 1) -> List[Dict[str, Any]]:
"""List strategies for local single-user."""
try:
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
SELECT *
FROM qd_strategies_trading
ORDER BY id DESC
"""
)
rows = cur.fetchall() or []
cur.close()
out = []
for r in rows:
ex = self._safe_json_loads(r.get('exchange_config'), {})
ind = self._safe_json_loads(r.get('indicator_config'), {})
tr = self._safe_json_loads(r.get('trading_config'), {})
ai = self._safe_json_loads(r.get('ai_model_config'), {})
notify = self._safe_json_loads(r.get('notification_config'), {})
out.append({
**r,
'exchange_config': ex,
'indicator_config': ind,
'trading_config': tr,
'ai_model_config': ai,
'notification_config': notify
})
return out
except Exception as e:
logger.error(f"list_strategies failed: {e}")
return []
def get_strategy(self, strategy_id: int) -> Optional[Dict[str, Any]]:
try:
with get_db_connection() as db:
cur = db.cursor()
cur.execute("SELECT * FROM qd_strategies_trading WHERE id = ?", (strategy_id,))
r = cur.fetchone()
cur.close()
if not r:
return None
r['exchange_config'] = self._safe_json_loads(r.get('exchange_config'), {})
r['indicator_config'] = self._safe_json_loads(r.get('indicator_config'), {})
r['trading_config'] = self._safe_json_loads(r.get('trading_config'), {})
r['ai_model_config'] = self._safe_json_loads(r.get('ai_model_config'), {})
r['notification_config'] = self._safe_json_loads(r.get('notification_config'), {})
return r
except Exception as e:
logger.error(f"get_strategy failed: {e}")
return None
def create_strategy(self, payload: Dict[str, Any]) -> int:
now = int(time.time())
name = (payload.get('strategy_name') or '').strip()
if not name:
raise ValueError("strategy_name is required")
strategy_type = payload.get('strategy_type') or 'IndicatorStrategy'
market_category = payload.get('market_category') or 'Crypto'
execution_mode = payload.get('execution_mode') or 'signal'
notification_config = payload.get('notification_config') or {}
indicator_config = payload.get('indicator_config') or {}
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')
initial_capital = (trading_config or {}).get('initial_capital') or payload.get('initial_capital') or 1000
leverage = (trading_config or {}).get('leverage') or 1
market_type = (trading_config or {}).get('market_type') or 'swap'
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
INSERT INTO qd_strategies_trading
(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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
name,
strategy_type,
market_category,
execution_mode,
self._dump_json_or_encrypt(notification_config, encrypt=False),
payload.get('status') or 'stopped',
symbol,
timeframe,
float(initial_capital or 1000),
int(leverage or 1),
market_type,
self._dump_json_or_encrypt(exchange_config, encrypt=False) if exchange_config else '',
self._dump_json_or_encrypt(indicator_config, encrypt=False),
self._dump_json_or_encrypt(trading_config, encrypt=False),
self._dump_json_or_encrypt(payload.get('ai_model_config') or {}, encrypt=False),
int(payload.get('decide_interval') or 300),
strategy_group_id,
group_base_name,
now,
now
)
)
new_id = cur.lastrowid
db.commit()
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)
if not existing:
return False
# Merge: allow partial updates
name = (payload.get('strategy_name') or existing.get('strategy_name') or '').strip()
market_category = payload.get('market_category') or existing.get('market_category') or 'Crypto'
execution_mode = payload.get('execution_mode') or existing.get('execution_mode') or 'signal'
notification_config = payload.get('notification_config') if payload.get('notification_config') is not None else (existing.get('notification_config') or {})
indicator_config = payload.get('indicator_config') if payload.get('indicator_config') is not None else (existing.get('indicator_config') or {})
trading_config = payload.get('trading_config') if payload.get('trading_config') is not None else (existing.get('trading_config') or {})
exchange_config = payload.get('exchange_config') if payload.get('exchange_config') is not None else (existing.get('exchange_config') or {})
ai_model_config = payload.get('ai_model_config') if payload.get('ai_model_config') is not None else (existing.get('ai_model_config') or {})
symbol = (trading_config or {}).get('symbol')
timeframe = (trading_config or {}).get('timeframe')
initial_capital = (trading_config or {}).get('initial_capital') or existing.get('initial_capital') or 1000
leverage = (trading_config or {}).get('leverage') or existing.get('leverage') or 1
market_type = (trading_config or {}).get('market_type') or existing.get('market_type') or 'swap'
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
UPDATE qd_strategies_trading
SET strategy_name = ?,
market_category = ?,
execution_mode = ?,
notification_config = ?,
symbol = ?,
timeframe = ?,
initial_capital = ?,
leverage = ?,
market_type = ?,
exchange_config = ?,
indicator_config = ?,
trading_config = ?,
ai_model_config = ?,
updated_at = ?
WHERE id = ?
""",
(
name,
market_category,
execution_mode,
self._dump_json_or_encrypt(notification_config, encrypt=False),
symbol,
timeframe,
float(initial_capital or 1000),
int(leverage or 1),
market_type,
self._dump_json_or_encrypt(exchange_config, encrypt=False) if exchange_config else '',
self._dump_json_or_encrypt(indicator_config, encrypt=False),
self._dump_json_or_encrypt(trading_config, encrypt=False),
self._dump_json_or_encrypt(ai_model_config, encrypt=False),
now,
strategy_id
)
)
db.commit()
cur.close()
return True
def delete_strategy(self, strategy_id: int) -> bool:
try:
with get_db_connection() as db:
cur = db.cursor()
cur.execute("DELETE FROM qd_strategies_trading WHERE id = ?", (strategy_id,))
db.commit()
cur.close()
return True
except Exception as e:
logger.error(f"delete_strategy failed: {e}")
return False