feat: improve backtest system and add OpenRouter balance query
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user