feat: improve backtest system and add OpenRouter balance query

This commit is contained in:
TIANHE
2026-01-12 04:01:17 +08:00
parent 5a4c770279
commit bd5db753df
11 changed files with 1996 additions and 413 deletions
+85 -15
View File
@@ -82,6 +82,46 @@ def _normalize_lang(lang: str | None) -> str:
return l2 if l2 in supported else "zh-CN"
@backtest_bp.route('/backtest/precision-info', methods=['POST'])
def get_precision_info():
"""
获取回测精度信息(用于前端提示)
Params:
market: 市场类型
startDate: 开始日期 (YYYY-MM-DD)
endDate: 结束日期 (YYYY-MM-DD)
Returns:
精度信息,包含推荐的执行时间框架和预估K线数量
"""
try:
data = request.get_json()
if not data:
return jsonify({'code': 0, 'msg': 'Request body is required'}), 400
market = data.get('market', 'crypto')
start_date_str = data.get('startDate', '')
end_date_str = data.get('endDate', '')
if not start_date_str or not end_date_str:
return jsonify({'code': 0, 'msg': 'startDate and endDate are required'}), 400
start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
exec_tf, precision_info = backtest_service.get_execution_timeframe(start_date, end_date, market)
return jsonify({
'code': 1,
'msg': 'success',
'data': precision_info
})
except Exception as e:
logger.error(f"Get precision info failed: {e}")
return jsonify({'code': 0, 'msg': str(e)}), 400
@backtest_bp.route('/backtest', methods=['POST'])
def run_backtest():
"""
@@ -97,6 +137,7 @@ def run_backtest():
endDate: End date (YYYY-MM-DD)
initialCapital: Initial capital (default 10000)
commission: Commission rate (default 0.001)
enableMtf: Enable multi-timeframe backtest (default true, only for crypto)
"""
try:
data = request.get_json()
@@ -122,6 +163,10 @@ def run_backtest():
leverage = int(data.get('leverage', 1))
trade_direction = data.get('tradeDirection', 'long') # long, short, both
strategy_config = data.get('strategyConfig') or {}
# 多时间框架回测开关(默认开启,仅加密货币市场有效)
enable_mtf = data.get('enableMtf', True)
if isinstance(enable_mtf, str):
enable_mtf = enable_mtf.lower() in ['true', '1', 'yes']
# (Debug) log received params if needed
@@ -178,21 +223,46 @@ def run_backtest():
}), 400
# 执行回测
result = backtest_service.run(
indicator_code=indicator_code,
market=market,
symbol=symbol,
timeframe=timeframe,
start_date=start_date,
end_date=end_date,
initial_capital=initial_capital,
commission=commission,
slippage=slippage,
leverage=leverage,
trade_direction=trade_direction,
strategy_config=strategy_config
)
# 执行回测(支持多时间框架高精度回测)
# 加密货币市场且启用MTF时,使用多时间框架回测
if enable_mtf and market.lower() in ['crypto', 'cryptocurrency']:
result = backtest_service.run_multi_timeframe(
indicator_code=indicator_code,
market=market,
symbol=symbol,
timeframe=timeframe,
start_date=start_date,
end_date=end_date,
initial_capital=initial_capital,
commission=commission,
slippage=slippage,
leverage=leverage,
trade_direction=trade_direction,
strategy_config=strategy_config,
enable_mtf=True
)
else:
result = backtest_service.run(
indicator_code=indicator_code,
market=market,
symbol=symbol,
timeframe=timeframe,
start_date=start_date,
end_date=end_date,
initial_capital=initial_capital,
commission=commission,
slippage=slippage,
leverage=leverage,
trade_direction=trade_direction,
strategy_config=strategy_config
)
# 添加标准回测的精度信息
result['precision_info'] = {
'enabled': False,
'timeframe': timeframe,
'precision': 'standard',
'message': '使用标准K线回测'
}
# Persist backtest run for AI optimization / history
run_id = None
+76
View File
@@ -877,6 +877,82 @@ def save_settings():
return jsonify({'code': 0, 'msg': f'Save failed: {str(e)}'})
@settings_bp.route('/openrouter-balance', methods=['GET'])
def get_openrouter_balance():
"""查询 OpenRouter 账户余额"""
try:
import requests
from app.config.api_keys import APIKeys
api_key = APIKeys.OPENROUTER_API_KEY
if not api_key:
return jsonify({
'code': 0,
'msg': 'OpenRouter API Key 未配置',
'data': None
})
# 调用 OpenRouter API 查询余额
# https://openrouter.ai/docs#limits
resp = requests.get(
'https://openrouter.ai/api/v1/auth/key',
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
},
timeout=10
)
if resp.status_code == 200:
data = resp.json()
# OpenRouter 返回格式: {"data": {"label": "...", "usage": 0.0, "limit": null, ...}}
key_data = data.get('data', {})
usage = key_data.get('usage', 0) # 已使用金额
limit = key_data.get('limit') # 限额(可能为null表示无限制)
limit_remaining = key_data.get('limit_remaining') # 剩余额度
is_free_tier = key_data.get('is_free_tier', False)
rate_limit = key_data.get('rate_limit', {})
return jsonify({
'code': 1,
'msg': 'success',
'data': {
'usage': round(usage, 4), # 已使用(美元)
'limit': limit, # 总限额
'limit_remaining': round(limit_remaining, 4) if limit_remaining is not None else None, # 剩余额度
'is_free_tier': is_free_tier,
'rate_limit': rate_limit,
'label': key_data.get('label', '')
}
})
elif resp.status_code == 401:
return jsonify({
'code': 0,
'msg': 'API Key 无效或已过期',
'data': None
})
else:
return jsonify({
'code': 0,
'msg': f'查询失败: HTTP {resp.status_code}',
'data': None
})
except requests.exceptions.Timeout:
return jsonify({
'code': 0,
'msg': '请求超时,请检查网络连接',
'data': None
})
except Exception as e:
logger.error(f"Get OpenRouter balance failed: {e}")
return jsonify({
'code': 0,
'msg': f'查询失败: {str(e)}',
'data': None
})
@settings_bp.route('/test-connection', methods=['POST'])
def test_connection():
"""测试API连接"""
File diff suppressed because it is too large Load Diff
@@ -202,7 +202,7 @@ class PendingOrderWorker:
# instId: BTC-USDT-SWAP -> BTC/USDT
hb_sym = inst_id.replace("-SWAP", "").replace("-", "/")
side = "long" if pos_side == "long" else ("short" if pos_side == "short" else ("long" if pos > 0 else "short"))
# IMPORTANT: OKX swap positions `pos` is in contracts (张数), but our system uses base-asset quantity.
# IMPORTANT: OKX swap positions `pos` is in contracts, but our system uses base-asset quantity.
# Convert contracts -> base using ctVal when available.
qty_base = abs(float(pos))
try:
+32 -32
View File
@@ -14,7 +14,7 @@ logger = get_logger(__name__)
class StrategyService:
"""Strategy service."""
# 类变量:限制连接测试并发数
# Class variable: limit connection test concurrency
_connection_test_semaphore = threading.Semaphore(5)
def __init__(self):
@@ -22,7 +22,7 @@ class StrategyService:
pass
def get_running_strategies(self) -> List[Dict[str, Any]]:
"""获取所有运行中的策略(仅ID"""
"""Get all running strategies (ID only)"""
try:
with get_db_connection() as db:
cursor = db.cursor()
@@ -36,13 +36,13 @@ class StrategyService:
return []
def get_running_strategies_with_type(self) -> List[Dict[str, Any]]:
"""获取所有运行中的策略(包含类型信息)"""
"""Get all running strategies (with type info)"""
try:
with get_db_connection() as db:
cursor = db.cursor()
# 假设 qd_strategies_trading 表中有 strategy_type 字段
# 如果没有,可能需要关联查询或者根据其他字段判断
# 这里假设表结构已更新
# Assume qd_strategies_trading table has strategy_type field
# If not, may need join query or determine from other fields
# Here we assume table structure is updated
query = "SELECT id, strategy_type FROM qd_strategies_trading WHERE status = 'running'"
cursor.execute(query)
results = cursor.fetchall()
@@ -58,14 +58,14 @@ class StrategyService:
def get_exchange_symbols(self, exchange_config: Dict[str, Any]) -> Dict[str, Any]:
"""
获取交易所交易对列表 (无需API Key)
Get exchange trading pairs (no API Key required)
"""
try:
exchange_id = exchange_config.get('exchange_id', '')
proxies = exchange_config.get('proxies')
if not exchange_id:
return {'success': False, 'message': '请选择交易所', 'symbols': []}
return {'success': False, 'message': 'Please select an exchange', 'symbols': []}
# For these exchanges, prefer direct REST (no ccxt), aligned with local live-trading design.
ex = str(exchange_id or "").strip().lower()
@@ -97,7 +97,7 @@ class StrategyService:
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}
return {'success': True, 'message': f'Success, {len(symbols)} trading pairs', '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("/")
@@ -113,7 +113,7 @@ class StrategyService:
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}
return {'success': True, 'message': f'Success, {len(symbols)} trading pairs', 'symbols': symbols}
if ex == "kraken":
if market_type == "spot":
@@ -142,7 +142,7 @@ class StrategyService:
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}
return {'success': True, 'message': f'Success, {len(symbols)} trading pairs', 'symbols': symbols}
if ex == "kucoin":
if market_type == "spot":
@@ -177,7 +177,7 @@ class StrategyService:
if base_ccy:
symbols.append(f"{base_ccy}/USDT")
symbols = sorted(list(set(symbols)))
return {'success': True, 'message': f'获取成功,共 {len(symbols)} 个交易对', 'symbols': symbols}
return {'success': True, 'message': f'Success, {len(symbols)} trading pairs', 'symbols': symbols}
if ex == "gate":
base = str(exchange_config.get("base_url") or exchange_config.get("baseUrl") or "https://api.gateio.ws").rstrip("/")
@@ -203,7 +203,7 @@ class StrategyService:
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}
return {'success': True, 'message': f'Success, {len(symbols)} trading pairs', '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(
@@ -225,19 +225,19 @@ class StrategyService:
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}
return {'success': True, 'message': f'Success, {len(symbols)} trading pairs', 'symbols': symbols}
return {'success': True, 'message': 'Success', 'symbols': symbols}
import ccxt
# 创建交易所实例 (public only)
# Create exchange instance (public only)
exchange_class = getattr(ccxt, exchange_id, None)
if not exchange_class:
return {'success': False, 'message': f'不支持的交易所: {exchange_id}', 'symbols': []}
return {'success': False, 'message': f'Unsupported exchange: {exchange_id}', 'symbols': []}
exchange_config_dict = {
'enableRateLimit': True,
'options': {'defaultType': 'swap'} # 默认为 swap
'options': {'defaultType': 'swap'} # Default to swap
}
if proxies:
exchange_config_dict['proxies'] = proxies
@@ -251,11 +251,11 @@ class StrategyService:
symbols.append(symbol)
symbols.sort()
return {'success': True, 'message': f'获取成功,共 {len(symbols)} 个交易对', 'symbols': symbols}
return {'success': True, 'message': f'Success, {len(symbols)} trading pairs', 'symbols': symbols}
except Exception as e:
logger.error(f"Failed to fetch symbols: {str(e)}")
return {'success': False, 'message': f'获取交易对失败: {str(e)}', 'symbols': []}
return {'success': False, 'message': f'Failed to get trading pairs: {str(e)}', 'symbols': []}
def test_exchange_connection(self, exchange_config: Dict[str, Any]) -> Dict[str, Any]:
"""
@@ -535,7 +535,7 @@ class StrategyService:
trading_config = payload.get('trading_config') or {}
exchange_config = payload.get('exchange_config') or {}
# 策略组字段
# Strategy group fields
strategy_group_id = payload.get('strategy_group_id') or ''
group_base_name = payload.get('group_base_name') or ''
@@ -588,10 +588,10 @@ class StrategyService:
def batch_create_strategies(self, payload: Dict[str, Any]) -> Dict[str, Any]:
"""
批量创建策略多币种
Batch create strategies (multi-symbol)
Args:
payload: 包含 symbols数组和其他策略配置
payload: Contains symbols (array) and other strategy config
Returns:
{
@@ -609,7 +609,7 @@ class StrategyService:
if not base_name:
raise ValueError("strategy_name is required")
# 生成策略组ID
# Generate strategy group ID
strategy_group_id = str(uuid.uuid4())[:8]
created_ids = []
@@ -617,10 +617,10 @@ class StrategyService:
for symbol in symbols:
try:
# 为每个币种创建单独的策略
# Create individual strategy for each symbol
single_payload = dict(payload)
# 解析 symbol(可能是 "Market:SYMBOL" 格式)
# Parse symbol (may be "Market:SYMBOL" format)
if isinstance(symbol, str) and ':' in symbol:
parts = symbol.split(':', 1)
market_category = parts[0]
@@ -629,13 +629,13 @@ class StrategyService:
market_category = payload.get('market_category') or 'Crypto'
symbol_name = symbol
# 策略名称加币种后缀
# Strategy name with symbol suffix
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
# Update symbol in trading_config
trading_config = dict(single_payload.get('trading_config') or {})
trading_config['symbol'] = symbol_name
single_payload['trading_config'] = trading_config
@@ -658,7 +658,7 @@ class StrategyService:
}
def batch_start_strategies(self, strategy_ids: List[int]) -> Dict[str, Any]:
"""批量启动策略"""
"""Batch start strategies"""
success_ids = []
failed_ids = []
@@ -677,7 +677,7 @@ class StrategyService:
}
def batch_stop_strategies(self, strategy_ids: List[int]) -> Dict[str, Any]:
"""批量停止策略"""
"""Batch stop strategies"""
success_ids = []
failed_ids = []
@@ -696,7 +696,7 @@ class StrategyService:
}
def batch_delete_strategies(self, strategy_ids: List[int]) -> Dict[str, Any]:
"""批量删除策略"""
"""Batch delete strategies"""
success_ids = []
failed_ids = []
@@ -715,7 +715,7 @@ class StrategyService:
}
def get_strategies_by_group(self, strategy_group_id: str) -> List[Dict[str, Any]]:
"""获取策略组内的所有策略"""
"""Get all strategies in a group"""
try:
with get_db_connection() as db:
cur = db.cursor()