stock-ai-agent/backend/app/services/real_trading_service.py

434 lines
15 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.

"""
实盘交易服务 - Bitget 合约交易
提供与模拟交易服务类似的接口,但执行的是真实交易
"""
import uuid
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
from app.models.real_trading import RealOrder
from app.models.paper_trading import OrderStatus, OrderSide, SignalGrade, EntryType
from app.services.db_service import db_service
from app.config import get_settings
from app.utils.logger import logger
class RealTradingService:
"""实盘交易服务"""
def __init__(self):
"""初始化实盘交易服务"""
self.settings = get_settings()
self.active_orders: Dict[str, RealOrder] = {} # 内存缓存活跃订单
# 实盘交易配置
self.max_single_position = self.settings.real_trading_max_single_position
self.max_total_ratio = self.settings.real_trading_max_total_ratio
self.default_leverage = self.settings.real_trading_default_leverage
self.risk_per_trade = self.settings.real_trading_risk_per_trade
self.max_orders = self.settings.real_trading_max_orders
# 获取交易 API (使用 CCXT SDK 版本)
from app.services.bitget_trading_api_sdk import get_bitget_trading_api
self.trading_api = get_bitget_trading_api()
if not self.trading_api:
logger.error("Bitget 交易 API 未初始化,实盘交易功能不可用")
return
# 确保表已创建
self._ensure_table_exists()
# 加载活跃订单
self._load_active_orders()
logger.info(f"实盘交易服务初始化完成(最大单笔: ${self.max_single_position}"
f"杠杆: {self.default_leverage}x最大持仓: {self.max_orders}")
def _ensure_table_exists(self):
"""确保数据表已创建"""
from app.models.real_trading import RealOrder
from app.models.database import Base
Base.metadata.create_all(bind=db_service.engine)
def _load_active_orders(self):
"""从数据库加载活跃订单"""
db = db_service.get_session()
try:
orders = db.query(RealOrder).filter(
RealOrder.status.in_([OrderStatus.PENDING, OrderStatus.OPEN])
).all()
for order in orders:
self.active_orders[order.order_id] = order
logger.info(f"实盘交易: 加载了 {len(orders)} 个活跃订单")
finally:
db.close()
def create_order_from_signal(self, signal: Dict[str, Any], current_price: float = None) -> Dict[str, Any]:
"""
从信号创建实盘订单
Args:
signal: LLM 分析信号
current_price: 当前价格
Returns:
创建结果
"""
if not self.trading_api:
return {
'success': False,
'message': '交易 API 未初始化'
}
db = db_service.get_session()
result = {
'success': False,
'message': '',
'order_id': None
}
try:
# 检查实盘交易是否启用
if not self.settings.real_trading_enabled:
return {
'success': False,
'message': '实盘交易未启用,请检查配置 REAL_TRADING_ENABLED=true'
}
# 获取信号信息
symbol = signal.get('symbol')
side = signal.get('side') # 'long' or 'short'
entry_type = signal.get('entry_type', 'market') # 'market' or 'limit'
entry_price = signal.get('entry_price')
stop_loss = signal.get('stop_loss')
take_profit = signal.get('take_profit')
grade = signal.get('grade', 'D')
confidence = signal.get('confidence', 0)
# 验证必需参数
if not all([symbol, side, stop_loss, take_profit]):
return {
'success': False,
'message': '信号缺少必需参数'
}
# 获取当前价格
if not current_price:
from app.services.bitget_service import bitget_service
current_price = bitget_service.get_current_price(symbol)
if not current_price:
return {
'success': False,
'message': '无法获取当前价格'
}
# 设置入场价
if entry_type == 'market':
entry_price = current_price
elif not entry_price:
entry_price = current_price
# 风险检查 - 检查订单数量
if len(self.active_orders) >= self.max_orders:
return {
'success': False,
'message': f'已达最大持仓数 {self.max_orders}'
}
# 风险检查 - 检查账户余额
balance_info = self._check_balance_risk(symbol, entry_price, stop_loss)
if not balance_info['can_trade']:
return {
'success': False,
'message': balance_info['reason']
}
# 创建订单对象
order_id = str(uuid.uuid4())
order = RealOrder(
order_id=order_id,
symbol=symbol,
side=OrderSide.LONG if side == 'long' else OrderSide.SHORT,
entry_price=entry_price,
stop_loss=stop_loss,
take_profit=take_profit,
quantity=balance_info['quantity'],
leverage=self.default_leverage,
signal_grade=SignalGrade[grade.upper()] if grade.upper() in ['A', 'B', 'C', 'D'] else SignalGrade.D,
signal_type=signal.get('signal_type', 'swing'),
confidence=confidence,
trend=signal.get('trend'),
entry_type=EntryType.MARKET if entry_type == 'market' else EntryType.LIMIT,
status=OrderStatus.PENDING
)
db.add(order)
db.commit()
db.refresh(order)
# 调用 Bitget API 下单
exchange_result = self._place_bitget_order(order, current_price)
if exchange_result['success']:
# 更新订单状态
order.exchange_order_id = exchange_result.get('order_id')
order.client_order_id = exchange_result.get('client_order_id')
order.filled_price = exchange_result.get('filled_price', entry_price)
order.filled_at = datetime.now()
order.status = OrderStatus.OPEN
db.commit()
# 添加到内存缓存
self.active_orders[order_id] = order
result['success'] = True
result['message'] = '实盘订单创建成功'
result['order_id'] = order_id
result['exchange_order_id'] = exchange_result.get('order_id')
logger.info(f"✅ 实盘订单创建成功: {symbol} {side} ${entry_price} -> {exchange_result.get('order_id')}")
else:
# 下单失败,删除记录
db.delete(order)
db.commit()
result['message'] = f"下单失败: {exchange_result.get('message', '未知错误')}"
logger.error(f"❌ 实盘订单下单失败: {exchange_result.get('message')}")
except Exception as e:
db.rollback()
result['message'] = f"创建订单失败: {str(e)}"
logger.error(f"创建实盘订单失败: {e}")
finally:
db.close()
return result
def _check_balance_risk(self, symbol: str, entry_price: float, stop_loss: float) -> Dict:
"""
检查余额和风险
Returns:
{'can_trade': bool, 'reason': str, 'quantity': float}
"""
try:
# 获取账户余额
balance_info = self.trading_api.get_balance()
usdt_balance = balance_info.get('USDT', {})
available = float(usdt_balance.get('available', 0))
if available < 10:
return {
'can_trade': False,
'reason': f'可用余额不足 (${available:.2f})',
'quantity': 0
}
# 计算风险
risk_amount = entry_price - stop_loss
risk_percent = (risk_amount / entry_price) * 100
# 根据风险计算仓位
max_quantity = (available * self.risk_per_trade) / (risk_percent / 100)
# 限制单笔最大仓位
max_quantity = min(max_quantity, self.max_single_position)
# 最小仓位限制
min_quantity = 10
if max_quantity < min_quantity:
return {
'can_trade': False,
'reason': f'风险过高,计算仓位 ${max_quantity:.2f} 小于最小值 ${min_quantity}',
'quantity': 0
}
return {
'can_trade': True,
'reason': '',
'quantity': round(max_quantity, 2)
}
except Exception as e:
logger.error(f"检查余额风险失败: {e}")
return {
'can_trade': False,
'reason': f'风险检查失败: {str(e)}',
'quantity': 0
}
def _place_bitget_order(self, order: RealOrder, current_price: float) -> Dict:
"""
调用 Bitget API 下单 (使用 CCXT SDK)
Returns:
{'success': bool, 'order_id': str, 'client_order_id': str, 'filled_price': float, 'message': str}
"""
try:
# CCXT 使用标准的 buy/sell 方向
# 对于合约buy = 开多/平空sell = 开空/平多
# 这里我们简化为long = buy, short = sell
side_map = {
OrderSide.LONG: 'buy',
OrderSide.SHORT: 'sell'
}
# 映射订单类型
order_type = 'market' if order.entry_type == EntryType.MARKET else 'limit'
# 计算合约数量(张数)
# Bitget U本位合约1张 = 1 USDT大多数情况
size = order.quantity # 简化处理,实际应该根据合约规格计算
# 生成自定义订单ID
client_order_id = f"real_{order.order_id[:8]}"
# 调用 API 下单
result = self.trading_api.place_order(
symbol=order.symbol,
side=side_map[order.side],
order_type=order_type,
size=size,
price=order.entry_price if order_type == 'limit' else None,
client_order_id=client_order_id
)
if result:
# CCXT 返回的订单对象格式
# result['id'] 是交易所订单ID
# result['price'] 是委托价格
# result['average'] 是成交均价(如果已成交)
order_id = result.get('id')
filled_price = float(result.get('average', 0)) or current_price
return {
'success': True,
'order_id': order_id,
'client_order_id': client_order_id,
'filled_price': filled_price
}
else:
return {
'success': False,
'message': 'API 调用失败'
}
except Exception as e:
logger.error(f"Bitget 下单失败: {e}")
return {
'success': False,
'message': str(e)
}
def get_active_orders(self) -> List[Dict]:
"""获取活跃订单列表"""
return [order.to_dict() for order in self.active_orders.values()]
def get_order(self, order_id: str) -> Optional[Dict]:
"""获取指定订单"""
order = self.active_orders.get(order_id)
if order:
return order.to_dict()
return None
def sync_positions_from_exchange(self) -> List[Dict]:
"""
从交易所同步持仓状态
Returns:
同步后的持仓列表
"""
if not self.trading_api:
return []
try:
# 获取交易所实际持仓
positions = self.trading_api.get_position()
logger.info(f"从交易所同步了 {len(positions)} 个持仓")
return positions
except Exception as e:
logger.error(f"同步持仓失败: {e}")
return []
def get_account_status(self) -> Dict:
"""获取账户状态"""
if not self.trading_api:
return {
'current_balance': 0,
'used_margin': 0,
'total_position_value': 0,
'available': 0
}
try:
balance_info = self.trading_api.get_balance()
usdt_info = balance_info.get('USDT', {})
available = float(usdt_info.get('available', 0))
frozen = float(usdt_info.get('frozen', 0))
locked = float(usdt_info.get('locked', 0))
# 计算持仓价值
total_position_value = 0
for order in self.active_orders.values():
if order.status == OrderStatus.OPEN:
total_position_value += order.quantity
return {
'current_balance': available + frozen + locked,
'available': available,
'used_margin': frozen + locked,
'total_position_value': total_position_value
}
except Exception as e:
logger.error(f"获取账户状态失败: {e}")
return {
'current_balance': 0,
'used_margin': 0,
'total_position_value': 0,
'available': 0
}
# 全局实例
_real_trading_service: Optional[RealTradingService] = None
def get_real_trading_service() -> Optional[RealTradingService]:
"""
获取实盘交易服务实例(单例)
Returns:
RealTradingService 实例或 None如果未配置或未启用
"""
global _real_trading_service
if _real_trading_service:
return _real_trading_service
settings = get_settings()
# 检查是否启用实盘交易
if not settings.real_trading_enabled:
return None
# 检查是否配置了 API Key
if not settings.bitget_api_key or not settings.bitget_api_secret:
logger.warning("Bitget API Key 未配置,实盘交易功能不可用")
return None
_real_trading_service = RealTradingService()
return _real_trading_service