update
This commit is contained in:
parent
24a77796d1
commit
5a270542ea
@ -340,6 +340,7 @@ async def get_service_status():
|
|||||||
if service:
|
if service:
|
||||||
account = service.get_account_status()
|
account = service.get_account_status()
|
||||||
status["account"] = account
|
status["account"] = account
|
||||||
|
status["auto_trading_enabled"] = service.get_auto_trading_status()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@ -348,3 +349,60 @@ async def get_service_status():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取实盘交易服务状态失败: {e}")
|
logger.error(f"获取实盘交易服务状态失败: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/auto-trading")
|
||||||
|
async def set_auto_trading(enabled: bool = Query(..., description="是否启用自动交易")):
|
||||||
|
"""
|
||||||
|
设置实盘自动交易开关
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled: true=启用自动交易,false=禁用自动交易
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
service = get_real_trading_service()
|
||||||
|
|
||||||
|
if not service:
|
||||||
|
raise HTTPException(status_code=404, detail="实盘交易服务未初始化,请检查 API 配置")
|
||||||
|
|
||||||
|
success = service.set_auto_trading(enabled)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
status_text = "启用" if enabled else "禁用"
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"实盘自动交易已{status_text}",
|
||||||
|
"auto_trading_enabled": enabled
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=500, detail="设置自动交易失败")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"设置自动交易失败: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/auto-trading")
|
||||||
|
async def get_auto_trading_status():
|
||||||
|
"""获取实盘自动交易状态"""
|
||||||
|
try:
|
||||||
|
service = get_real_trading_service()
|
||||||
|
|
||||||
|
if not service:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "实盘交易服务未初始化",
|
||||||
|
"auto_trading_enabled": False
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled = service.get_auto_trading_status()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"auto_trading_enabled": enabled
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取自动交易状态失败: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|||||||
@ -43,12 +43,16 @@ class CryptoAgent:
|
|||||||
self.llm_analyzer = LLMSignalAnalyzer()
|
self.llm_analyzer = LLMSignalAnalyzer()
|
||||||
self.signal_db = get_signal_db_service() # 信号数据库服务
|
self.signal_db = get_signal_db_service() # 信号数据库服务
|
||||||
|
|
||||||
# 模拟交易服务
|
# 模拟交易服务(始终启用)
|
||||||
self.paper_trading_enabled = self.settings.paper_trading_enabled
|
self.paper_trading = get_paper_trading_service()
|
||||||
if self.paper_trading_enabled:
|
|
||||||
self.paper_trading = get_paper_trading_service()
|
# 实盘交易服务(如果配置了 API)
|
||||||
else:
|
self.real_trading = None
|
||||||
self.paper_trading = None
|
try:
|
||||||
|
from app.services.real_trading_service import get_real_trading_service
|
||||||
|
self.real_trading = get_real_trading_service()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"实盘交易服务初始化失败: {e}")
|
||||||
|
|
||||||
# 状态管理
|
# 状态管理
|
||||||
self.last_signals: Dict[str, Dict[str, Any]] = {}
|
self.last_signals: Dict[str, Dict[str, Any]] = {}
|
||||||
@ -75,8 +79,13 @@ class CryptoAgent:
|
|||||||
})
|
})
|
||||||
|
|
||||||
logger.info(f"加密货币智能体初始化完成(LLM 驱动),监控交易对: {self.symbols}")
|
logger.info(f"加密货币智能体初始化完成(LLM 驱动),监控交易对: {self.symbols}")
|
||||||
if self.paper_trading_enabled:
|
logger.info(f"模拟交易: 始终启用")
|
||||||
logger.info(f"模拟交易已启用")
|
|
||||||
|
if self.real_trading:
|
||||||
|
auto_status = "启用" if self.real_trading.get_auto_trading_status() else "禁用"
|
||||||
|
logger.info(f"实盘交易: 已配置 (自动交易: {auto_status})")
|
||||||
|
else:
|
||||||
|
logger.info(f"实盘交易: 未配置")
|
||||||
|
|
||||||
def _on_price_update(self, symbol: str, price: float):
|
def _on_price_update(self, symbol: str, price: float):
|
||||||
"""处理实时价格更新(用于模拟交易)"""
|
"""处理实时价格更新(用于模拟交易)"""
|
||||||
@ -495,8 +504,8 @@ class CryptoAgent:
|
|||||||
self.last_signals[symbol] = best_signal
|
self.last_signals[symbol] = best_signal
|
||||||
self.signal_cooldown[symbol] = datetime.now()
|
self.signal_cooldown[symbol] = datetime.now()
|
||||||
|
|
||||||
# 5. 创建模拟订单
|
# 5. 创建模拟订单(始终执行)
|
||||||
if self.paper_trading_enabled and self.paper_trading:
|
if self.paper_trading:
|
||||||
grade = best_signal.get('grade', 'D')
|
grade = best_signal.get('grade', 'D')
|
||||||
position_size = best_signal.get('position_size', 'light')
|
position_size = best_signal.get('position_size', 'light')
|
||||||
if grade != 'D':
|
if grade != 'D':
|
||||||
@ -513,6 +522,24 @@ class CryptoAgent:
|
|||||||
order = result.get('order')
|
order = result.get('order')
|
||||||
if order:
|
if order:
|
||||||
logger.info(f" 📝 已创建模拟订单: {order.order_id} | 仓位: {position_size}")
|
logger.info(f" 📝 已创建模拟订单: {order.order_id} | 仓位: {position_size}")
|
||||||
|
|
||||||
|
# 6. 创建实盘订单(如果启用了自动交易)
|
||||||
|
if self.real_trading and self.real_trading.get_auto_trading_status():
|
||||||
|
grade = best_signal.get('grade', 'D')
|
||||||
|
position_size = best_signal.get('position_size', 'light')
|
||||||
|
if grade != 'D':
|
||||||
|
# 转换信号格式以兼容 real_trading
|
||||||
|
real_signal = self._convert_to_real_signal(symbol, best_signal, current_price)
|
||||||
|
result = self.real_trading.create_order_from_signal(real_signal, current_price)
|
||||||
|
|
||||||
|
if result.get('success'):
|
||||||
|
logger.info(f" 💰 已创建实盘订单: {result.get('order_id')} | 仓位: {position_size} | 数量: ${result.get('quantity', 0):.2f}")
|
||||||
|
|
||||||
|
# 发送实盘订单成交通知
|
||||||
|
await self._notify_real_order_created(symbol, best_signal, result)
|
||||||
|
elif not result.get('skipped'):
|
||||||
|
# 只有非跳过的情况才记录错误
|
||||||
|
logger.warning(f" ⚠️ 实盘订单创建失败: {result.get('message')}")
|
||||||
else:
|
else:
|
||||||
if best_signal:
|
if best_signal:
|
||||||
logger.info(f"\n⏸️ 信号冷却中或置信度不足,不发送通知")
|
logger.info(f"\n⏸️ 信号冷却中或置信度不足,不发送通知")
|
||||||
@ -698,6 +725,62 @@ class CryptoAgent:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"持仓回顾失败: {e}", exc_info=True)
|
logger.error(f"持仓回顾失败: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def _convert_to_real_signal(self, symbol: str, signal: Dict[str, Any],
|
||||||
|
current_price: float) -> Dict[str, Any]:
|
||||||
|
"""转换 LLM 信号格式为实盘交易格式"""
|
||||||
|
signal_type = signal.get('type', 'medium_term')
|
||||||
|
type_map = {'short_term': 'short_term', 'medium_term': 'swing', 'long_term': 'swing'}
|
||||||
|
|
||||||
|
# 获取入场类型和入场价
|
||||||
|
entry_type = signal.get('entry_type', 'market')
|
||||||
|
entry_price = signal.get('entry_price', current_price)
|
||||||
|
|
||||||
|
# 映射 action: buy -> long, sell -> short
|
||||||
|
action = signal.get('action', 'hold')
|
||||||
|
side_map = {'buy': 'long', 'sell': 'short'}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'symbol': symbol,
|
||||||
|
'side': side_map.get(action, 'long'),
|
||||||
|
'entry_type': entry_type,
|
||||||
|
'entry_price': entry_price,
|
||||||
|
'stop_loss': signal.get('stop_loss', 0),
|
||||||
|
'take_profit': signal.get('take_profit', 0),
|
||||||
|
'confidence': signal.get('confidence', 0),
|
||||||
|
'grade': signal.get('grade', 'D'),
|
||||||
|
'signal_type': type_map.get(signal_type, 'swing'),
|
||||||
|
'position_size': signal.get('position_size', 'light'), # LLM 建议的仓位大小
|
||||||
|
'trend': signal.get('trend')
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _notify_real_order_created(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
signal: Dict[str, Any],
|
||||||
|
result: Dict[str, Any]
|
||||||
|
):
|
||||||
|
"""发送实盘订单创建通知"""
|
||||||
|
side = signal.get('action', 'buy')
|
||||||
|
side_text = "做多" if side == 'buy' else "做空"
|
||||||
|
grade = signal.get('grade', 'N/A')
|
||||||
|
position_size = result.get('position_size', 'light')
|
||||||
|
quantity = result.get('quantity', 0)
|
||||||
|
|
||||||
|
message = f"""💰 实盘订单已创建
|
||||||
|
|
||||||
|
交易对: {symbol}
|
||||||
|
方向: {side_text}
|
||||||
|
等级: {grade}
|
||||||
|
仓位: {position_size}
|
||||||
|
数量: ${quantity:.2f}
|
||||||
|
订单 ID: {result.get('order_id', '')[:12]}...
|
||||||
|
|
||||||
|
⚠️ 真实资金交易中"""
|
||||||
|
|
||||||
|
await self.feishu.send_text(message)
|
||||||
|
await self.telegram.send_message(message)
|
||||||
|
logger.info(f"已发送实盘订单创建通知: {result.get('order_id')}")
|
||||||
|
|
||||||
async def _notify_position_adjustment(
|
async def _notify_position_adjustment(
|
||||||
self,
|
self,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
|
|||||||
198
backend/app/services/position_manager.py
Normal file
198
backend/app/services/position_manager.py
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
"""
|
||||||
|
仓位管理服务 - 供模拟盘和实盘共用
|
||||||
|
基于 LLM 建议动态计算仓位大小
|
||||||
|
"""
|
||||||
|
from typing import Dict, Any, Optional, Tuple
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from app.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class PositionCalculator(ABC):
|
||||||
|
"""仓位计算器抽象基类"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_account_status(self) -> Dict[str, Any]:
|
||||||
|
"""获取账户状态"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_max_leverage(self) -> int:
|
||||||
|
"""获取最大杠杆"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PositionManager:
|
||||||
|
"""
|
||||||
|
仓位管理器
|
||||||
|
|
||||||
|
根据 LLM 建议的仓位大小(heavy/medium/light)动态计算实际保证金和持仓价值
|
||||||
|
模拟盘和实盘共用同一套仓位管理逻辑
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, calculator: PositionCalculator):
|
||||||
|
"""
|
||||||
|
初始化仓位管理器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
calculator: 仓位计算器(模拟盘或实盘)
|
||||||
|
"""
|
||||||
|
self.calculator = calculator
|
||||||
|
|
||||||
|
def calculate_position(
|
||||||
|
self,
|
||||||
|
position_size: str,
|
||||||
|
symbol: str,
|
||||||
|
custom_ratios: Optional[Dict[str, float]] = None
|
||||||
|
) -> Tuple[float, float]:
|
||||||
|
"""
|
||||||
|
根据 LLM 建议计算仓位
|
||||||
|
|
||||||
|
Args:
|
||||||
|
position_size: LLM 建议的仓位大小 ('heavy', 'medium', 'light')
|
||||||
|
symbol: 交易对
|
||||||
|
custom_ratios: 自定义仓位比例,可选
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(margin, position_value) 元组
|
||||||
|
"""
|
||||||
|
account = self.calculator.get_account_status()
|
||||||
|
max_leverage = self.calculator.get_max_leverage()
|
||||||
|
|
||||||
|
balance = account['current_balance']
|
||||||
|
current_position_value = account.get('total_position_value', 0)
|
||||||
|
|
||||||
|
# 计算可用保证金空间
|
||||||
|
max_position_value = balance * max_leverage
|
||||||
|
available_position_value = max_position_value - current_position_value
|
||||||
|
|
||||||
|
if available_position_value <= 0:
|
||||||
|
logger.warning(f"已达最大杠杆限制,无法开仓")
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
# 使用自定义比例或默认比例
|
||||||
|
ratios = custom_ratios or {
|
||||||
|
'heavy': 0.30,
|
||||||
|
'medium': 0.15,
|
||||||
|
'light': 0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
size_ratio = ratios.get(position_size, 0.05)
|
||||||
|
|
||||||
|
# 计算目标持仓价值
|
||||||
|
target_position_value = available_position_value * size_ratio
|
||||||
|
|
||||||
|
# 设置最小和最大限制
|
||||||
|
min_position_value = 1000 # 最小持仓价值
|
||||||
|
max_single_position = balance * 5 # 单笔最大不超过 5x 杠杆
|
||||||
|
|
||||||
|
position_value = max(min_position_value, min(target_position_value, max_single_position))
|
||||||
|
position_value = min(position_value, available_position_value)
|
||||||
|
position_value = round(position_value, 2)
|
||||||
|
|
||||||
|
# 计算对应的保证金
|
||||||
|
margin = round(position_value / max_leverage, 2)
|
||||||
|
|
||||||
|
logger.info(f"动态仓位计算: {position_size} | 可用空间: ${available_position_value:,.0f} | "
|
||||||
|
f"目标仓位: ${position_value:,.0f} | 保证金: ${margin:,.0f}")
|
||||||
|
|
||||||
|
return margin, position_value
|
||||||
|
|
||||||
|
|
||||||
|
class PaperPositionCalculator(PositionCalculator):
|
||||||
|
"""模拟盘仓位计算器"""
|
||||||
|
|
||||||
|
def __init__(self, account_status_getter, max_leverage: int = 20):
|
||||||
|
"""
|
||||||
|
初始化模拟盘计算器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
account_status_getter: 获取账户状态的函数
|
||||||
|
max_leverage: 最大杠杆倍数
|
||||||
|
"""
|
||||||
|
self.account_status_getter = account_status_getter
|
||||||
|
self._max_leverage = max_leverage
|
||||||
|
|
||||||
|
def get_account_status(self) -> Dict[str, Any]:
|
||||||
|
return self.account_status_getter()
|
||||||
|
|
||||||
|
def get_max_leverage(self) -> int:
|
||||||
|
return self._max_leverage
|
||||||
|
|
||||||
|
|
||||||
|
class RealPositionCalculator(PositionCalculator):
|
||||||
|
"""实盘仓位计算器"""
|
||||||
|
|
||||||
|
def __init__(self, balance: float, used_margin: float, total_position_value: float, max_leverage: int = 10):
|
||||||
|
"""
|
||||||
|
初始化实盘计算器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
balance: 账户余额
|
||||||
|
used_margin: 已用保证金
|
||||||
|
total_position_value: 总持仓价值
|
||||||
|
max_leverage: 最大杠杆倍数
|
||||||
|
"""
|
||||||
|
self._account_status = {
|
||||||
|
'current_balance': balance,
|
||||||
|
'used_margin': used_margin,
|
||||||
|
'total_position_value': total_position_value
|
||||||
|
}
|
||||||
|
self._max_leverage = max_leverage
|
||||||
|
|
||||||
|
def get_account_status(self) -> Dict[str, Any]:
|
||||||
|
return self._account_status
|
||||||
|
|
||||||
|
def get_max_leverage(self) -> int:
|
||||||
|
return self._max_leverage
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_paper_position(
|
||||||
|
account_status_getter,
|
||||||
|
position_size: str,
|
||||||
|
symbol: str,
|
||||||
|
max_leverage: int = 20
|
||||||
|
) -> Tuple[float, float]:
|
||||||
|
"""
|
||||||
|
计算模拟盘仓位(快捷方法)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
account_status_getter: 获取账户状态的函数
|
||||||
|
position_size: LLM 建议的仓位大小
|
||||||
|
symbol: 交易对
|
||||||
|
max_leverage: 最大杠杆倍数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(margin, position_value) 元组
|
||||||
|
"""
|
||||||
|
calculator = PaperPositionCalculator(account_status_getter, max_leverage)
|
||||||
|
manager = PositionManager(calculator)
|
||||||
|
return manager.calculate_position(position_size, symbol)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_real_position(
|
||||||
|
balance: float,
|
||||||
|
used_margin: float,
|
||||||
|
total_position_value: float,
|
||||||
|
position_size: str,
|
||||||
|
symbol: str,
|
||||||
|
max_leverage: int = 10,
|
||||||
|
custom_ratios: Optional[Dict[str, float]] = None
|
||||||
|
) -> Tuple[float, float]:
|
||||||
|
"""
|
||||||
|
计算实盘仓位(快捷方法)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
balance: 账户余额
|
||||||
|
used_margin: 已用保证金
|
||||||
|
total_position_value: 总持仓价值
|
||||||
|
position_size: LLM 建议的仓位大小
|
||||||
|
symbol: 交易对
|
||||||
|
max_leverage: 最大杠杆倍数
|
||||||
|
custom_ratios: 自定义仓位比例(实盘可用更保守的配置)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(margin, position_value) 元组
|
||||||
|
"""
|
||||||
|
calculator = RealPositionCalculator(balance, used_margin, total_position_value, max_leverage)
|
||||||
|
manager = PositionManager(calculator)
|
||||||
|
return manager.calculate_position(position_size, symbol, custom_ratios)
|
||||||
@ -2,6 +2,7 @@
|
|||||||
实盘交易服务 - Bitget 合约交易
|
实盘交易服务 - Bitget 合约交易
|
||||||
|
|
||||||
提供与模拟交易服务类似的接口,但执行的是真实交易
|
提供与模拟交易服务类似的接口,但执行的是真实交易
|
||||||
|
集成 LLM 仓位管理决策
|
||||||
"""
|
"""
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@ -10,6 +11,7 @@ from typing import Dict, Any, List, Optional
|
|||||||
from app.models.real_trading import RealOrder
|
from app.models.real_trading import RealOrder
|
||||||
from app.models.paper_trading import OrderStatus, OrderSide, SignalGrade, EntryType
|
from app.models.paper_trading import OrderStatus, OrderSide, SignalGrade, EntryType
|
||||||
from app.services.db_service import db_service
|
from app.services.db_service import db_service
|
||||||
|
from app.services.position_manager import calculate_real_position
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
|
|
||||||
@ -29,6 +31,9 @@ class RealTradingService:
|
|||||||
self.risk_per_trade = self.settings.real_trading_risk_per_trade
|
self.risk_per_trade = self.settings.real_trading_risk_per_trade
|
||||||
self.max_orders = self.settings.real_trading_max_orders
|
self.max_orders = self.settings.real_trading_max_orders
|
||||||
|
|
||||||
|
# 自动交易开关(从数据库加载)
|
||||||
|
self.auto_trading_enabled = self._load_auto_trading_status()
|
||||||
|
|
||||||
# 获取交易 API (使用 CCXT SDK 版本)
|
# 获取交易 API (使用 CCXT SDK 版本)
|
||||||
from app.services.bitget_trading_api_sdk import get_bitget_trading_api
|
from app.services.bitget_trading_api_sdk import get_bitget_trading_api
|
||||||
self.trading_api = get_bitget_trading_api()
|
self.trading_api = get_bitget_trading_api()
|
||||||
@ -44,14 +49,81 @@ class RealTradingService:
|
|||||||
self._load_active_orders()
|
self._load_active_orders()
|
||||||
|
|
||||||
logger.info(f"实盘交易服务初始化完成(最大单笔: ${self.max_single_position},"
|
logger.info(f"实盘交易服务初始化完成(最大单笔: ${self.max_single_position},"
|
||||||
f"杠杆: {self.default_leverage}x,最大持仓: {self.max_orders})")
|
f"杠杆: {self.default_leverage}x,最大持仓: {self.max_orders},"
|
||||||
|
f"自动交易: {'启用' if self.auto_trading_enabled else '禁用'})")
|
||||||
|
|
||||||
def _ensure_table_exists(self):
|
def _ensure_table_exists(self):
|
||||||
"""确保数据表已创建"""
|
"""确保数据表已创建"""
|
||||||
from app.models.real_trading import RealOrder
|
from app.models.real_trading import RealOrder
|
||||||
from app.models.database import Base
|
from app.models.database import Base
|
||||||
|
from sqlalchemy import text
|
||||||
Base.metadata.create_all(bind=db_service.engine)
|
Base.metadata.create_all(bind=db_service.engine)
|
||||||
|
|
||||||
|
# 创建自动交易开关表
|
||||||
|
db = db_service.get_session()
|
||||||
|
try:
|
||||||
|
db.execute(text("""
|
||||||
|
CREATE TABLE IF NOT EXISTS real_trading_settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# 初始化自动交易开关
|
||||||
|
db.execute(text("""
|
||||||
|
INSERT OR IGNORE INTO real_trading_settings (key, value)
|
||||||
|
VALUES ('auto_trading_enabled', '0')
|
||||||
|
"""))
|
||||||
|
db.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"创建设置表失败: {e}")
|
||||||
|
db.rollback()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def _load_auto_trading_status(self) -> bool:
|
||||||
|
"""从数据库加载自动交易开关状态"""
|
||||||
|
db = db_service.get_session()
|
||||||
|
try:
|
||||||
|
from sqlalchemy import text
|
||||||
|
result = db.execute(text("SELECT value FROM real_trading_settings WHERE key = 'auto_trading_enabled'")).fetchone()
|
||||||
|
if result:
|
||||||
|
return result[0] == '1'
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"加载自动交易状态失败: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def set_auto_trading(self, enabled: bool) -> bool:
|
||||||
|
"""设置自动交易开关"""
|
||||||
|
db = db_service.get_session()
|
||||||
|
try:
|
||||||
|
from sqlalchemy import text
|
||||||
|
db.execute(text("""
|
||||||
|
UPDATE real_trading_settings
|
||||||
|
SET value = :value, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE key = 'auto_trading_enabled'
|
||||||
|
"""), {'value': '1' if enabled else '0'})
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
self.auto_trading_enabled = enabled
|
||||||
|
logger.info(f"实盘自动交易已{'启用' if enabled else '禁用'}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"设置自动交易失败: {e}")
|
||||||
|
db.rollback()
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def get_auto_trading_status(self) -> bool:
|
||||||
|
"""获取自动交易状态"""
|
||||||
|
return self.auto_trading_enabled
|
||||||
|
|
||||||
def _load_active_orders(self):
|
def _load_active_orders(self):
|
||||||
"""从数据库加载活跃订单"""
|
"""从数据库加载活跃订单"""
|
||||||
db = db_service.get_session()
|
db = db_service.get_session()
|
||||||
@ -70,15 +142,33 @@ class RealTradingService:
|
|||||||
|
|
||||||
def create_order_from_signal(self, signal: Dict[str, Any], current_price: float = None) -> Dict[str, Any]:
|
def create_order_from_signal(self, signal: Dict[str, Any], current_price: float = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
从信号创建实盘订单
|
从信号创建实盘订单(集成 LLM 仓位管理)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
signal: LLM 分析信号
|
signal: LLM 分析信号
|
||||||
|
- symbol: 交易对
|
||||||
|
- side: 'long' or 'short'
|
||||||
|
- entry_type: 'market' or 'limit'
|
||||||
|
- entry_price: 入场价
|
||||||
|
- stop_loss: 止损价
|
||||||
|
- take_profit: 止盈价
|
||||||
|
- grade: 信号等级
|
||||||
|
- confidence: 置信度
|
||||||
|
- position_size: LLM 建议的仓位大小 ('heavy', 'medium', 'light')
|
||||||
current_price: 当前价格
|
current_price: 当前价格
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
创建结果
|
创建结果
|
||||||
"""
|
"""
|
||||||
|
# 检查自动交易开关
|
||||||
|
if not self.auto_trading_enabled:
|
||||||
|
logger.info(f"实盘自动交易已禁用,跳过信号执行")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': '实盘自动交易已禁用',
|
||||||
|
'skipped': True
|
||||||
|
}
|
||||||
|
|
||||||
if not self.trading_api:
|
if not self.trading_api:
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
@ -109,6 +199,7 @@ class RealTradingService:
|
|||||||
take_profit = signal.get('take_profit')
|
take_profit = signal.get('take_profit')
|
||||||
grade = signal.get('grade', 'D')
|
grade = signal.get('grade', 'D')
|
||||||
confidence = signal.get('confidence', 0)
|
confidence = signal.get('confidence', 0)
|
||||||
|
position_size = signal.get('position_size', 'light') # LLM 建议的仓位大小
|
||||||
|
|
||||||
# 验证必需参数
|
# 验证必需参数
|
||||||
if not all([symbol, side, stop_loss, take_profit]):
|
if not all([symbol, side, stop_loss, take_profit]):
|
||||||
@ -141,12 +232,52 @@ class RealTradingService:
|
|||||||
'message': f'已达最大持仓数 {self.max_orders}'
|
'message': f'已达最大持仓数 {self.max_orders}'
|
||||||
}
|
}
|
||||||
|
|
||||||
# 风险检查 - 检查账户余额
|
# 获取账户状态
|
||||||
balance_info = self._check_balance_risk(symbol, entry_price, stop_loss)
|
account = self.get_account_status()
|
||||||
if not balance_info['can_trade']:
|
balance = account['current_balance']
|
||||||
|
available = account['available']
|
||||||
|
used_margin = account['used_margin']
|
||||||
|
total_position_value = account['total_position_value']
|
||||||
|
|
||||||
|
if available < 10:
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': balance_info['reason']
|
'message': f'可用余额不足 (${available:.2f})'
|
||||||
|
}
|
||||||
|
|
||||||
|
# === 使用 LLM 建议的仓位大小计算仓位 ===
|
||||||
|
# 实盘使用更保守的比例配置
|
||||||
|
custom_ratios = {
|
||||||
|
'heavy': 0.15, # 激进方案:heavy 15%(模拟盘是 30%)
|
||||||
|
'medium': 0.08, # 激进方案:medium 8%(模拟盘是 15%)
|
||||||
|
'light': 0.03 # 激进方案:light 3%(模拟盘是 5%)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 计算仓位(使用统一的仓位管理器)
|
||||||
|
margin, position_value = calculate_real_position(
|
||||||
|
balance=balance,
|
||||||
|
used_margin=used_margin,
|
||||||
|
total_position_value=total_position_value,
|
||||||
|
position_size=position_size,
|
||||||
|
symbol=symbol,
|
||||||
|
max_leverage=self.default_leverage,
|
||||||
|
custom_ratios=custom_ratios
|
||||||
|
)
|
||||||
|
|
||||||
|
if margin <= 0 or position_value <= 0:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': '无法开仓:仓位计算失败或已达杠杆限制'
|
||||||
|
}
|
||||||
|
|
||||||
|
quantity = position_value # 订单数量(以 USDT 计价)
|
||||||
|
|
||||||
|
# 最小仓位限制(100 美金测试,最小 5 USDT)
|
||||||
|
min_quantity = 5
|
||||||
|
if quantity < min_quantity:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f'计算仓位 ${quantity:.2f} 小于最小值 ${min_quantity}'
|
||||||
}
|
}
|
||||||
|
|
||||||
# 创建订单对象
|
# 创建订单对象
|
||||||
@ -158,14 +289,14 @@ class RealTradingService:
|
|||||||
entry_price=entry_price,
|
entry_price=entry_price,
|
||||||
stop_loss=stop_loss,
|
stop_loss=stop_loss,
|
||||||
take_profit=take_profit,
|
take_profit=take_profit,
|
||||||
quantity=balance_info['quantity'],
|
quantity=quantity,
|
||||||
leverage=self.default_leverage,
|
leverage=self.default_leverage,
|
||||||
signal_grade=SignalGrade[grade.upper()] if grade.upper() in ['A', 'B', 'C', 'D'] else SignalGrade.D,
|
signal_grade=SignalGrade[grade.upper()] if grade.upper() in ['A', 'B', 'C', 'D'] else SignalGrade.D,
|
||||||
signal_type=signal.get('signal_type', 'swing'),
|
signal_type=signal.get('signal_type', 'swing'),
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
trend=signal.get('trend'),
|
trend=signal.get('trend'),
|
||||||
entry_type=EntryType.MARKET if entry_type == 'market' else EntryType.LIMIT,
|
entry_type=EntryType.MARKET if entry_type == 'market' else EntryType.LIMIT,
|
||||||
status=OrderStatus.PENDING
|
status=OrderStatus.OPEN if entry_type == 'market' else OrderStatus.PENDING
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(order)
|
db.add(order)
|
||||||
@ -189,11 +320,15 @@ class RealTradingService:
|
|||||||
self.active_orders[order_id] = order
|
self.active_orders[order_id] = order
|
||||||
|
|
||||||
result['success'] = True
|
result['success'] = True
|
||||||
result['message'] = '实盘订单创建成功'
|
result['message'] = f'实盘订单创建成功 (仓位: {position_size})'
|
||||||
result['order_id'] = order_id
|
result['order_id'] = order_id
|
||||||
result['exchange_order_id'] = exchange_result.get('order_id')
|
result['exchange_order_id'] = exchange_result.get('order_id')
|
||||||
|
result['position_size'] = position_size
|
||||||
|
result['quantity'] = quantity
|
||||||
|
|
||||||
logger.info(f"✅ 实盘订单创建成功: {symbol} {side} ${entry_price} -> {exchange_result.get('order_id')}")
|
logger.info(f"✅ 实盘订单创建成功: {symbol} {side} ${entry_price} | "
|
||||||
|
f"仓位: {position_size} | 数量: ${quantity:.2f} | "
|
||||||
|
f"-> {exchange_result.get('order_id')}")
|
||||||
else:
|
else:
|
||||||
# 下单失败,删除记录
|
# 下单失败,删除记录
|
||||||
db.delete(order)
|
db.delete(order)
|
||||||
@ -213,59 +348,6 @@ class RealTradingService:
|
|||||||
|
|
||||||
return result
|
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:
|
def _place_bitget_order(self, order: RealOrder, current_price: float) -> Dict:
|
||||||
"""
|
"""
|
||||||
调用 Bitget API 下单 (使用 CCXT SDK)
|
调用 Bitget API 下单 (使用 CCXT SDK)
|
||||||
@ -401,6 +483,43 @@ class RealTradingService:
|
|||||||
'available': 0
|
'available': 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_position_info(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
获取当前持仓信息(供 LLM 分析使用)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
持仓信息字典
|
||||||
|
"""
|
||||||
|
account = self.get_account_status()
|
||||||
|
active_orders = self.get_active_orders()
|
||||||
|
|
||||||
|
# 计算当前杠杆
|
||||||
|
balance = account['current_balance']
|
||||||
|
total_position_value = account['total_position_value']
|
||||||
|
current_leverage = total_position_value / balance if balance > 0 else 0
|
||||||
|
|
||||||
|
# 格式化持仓列表
|
||||||
|
positions = []
|
||||||
|
for order in active_orders:
|
||||||
|
positions.append({
|
||||||
|
'symbol': order.get('symbol'),
|
||||||
|
'side': order.get('side'),
|
||||||
|
'status': order.get('status'),
|
||||||
|
'entry_price': order.get('filled_price') or order.get('entry_price'),
|
||||||
|
'quantity': order.get('quantity'),
|
||||||
|
'pnl_percent': order.get('pnl_percent', 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'account_balance': balance,
|
||||||
|
'total_position_value': total_position_value,
|
||||||
|
'current_leverage': current_leverage,
|
||||||
|
'max_leverage': self.default_leverage,
|
||||||
|
'active_order_count': len(active_orders),
|
||||||
|
'max_orders': self.max_orders,
|
||||||
|
'positions': positions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# 全局实例
|
# 全局实例
|
||||||
_real_trading_service: Optional[RealTradingService] = None
|
_real_trading_service: Optional[RealTradingService] = None
|
||||||
@ -410,8 +529,11 @@ def get_real_trading_service() -> Optional[RealTradingService]:
|
|||||||
"""
|
"""
|
||||||
获取实盘交易服务实例(单例)
|
获取实盘交易服务实例(单例)
|
||||||
|
|
||||||
|
注意:不再检查 REAL_TRADING_ENABLED 配置
|
||||||
|
只要 API 配置了就初始化服务,自动交易可以单独控制
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
RealTradingService 实例或 None(如果未配置或未启用)
|
RealTradingService 实例或 None(如果未配置 API)
|
||||||
"""
|
"""
|
||||||
global _real_trading_service
|
global _real_trading_service
|
||||||
|
|
||||||
@ -420,11 +542,7 @@ def get_real_trading_service() -> Optional[RealTradingService]:
|
|||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
||||||
# 检查是否启用实盘交易
|
# 检查是否配置了 API Key(不再检查 REAL_TRADING_ENABLED)
|
||||||
if not settings.real_trading_enabled:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 检查是否配置了 API Key
|
|
||||||
if not settings.bitget_api_key or not settings.bitget_api_secret:
|
if not settings.bitget_api_key or not settings.bitget_api_secret:
|
||||||
logger.warning("Bitget API Key 未配置,实盘交易功能不可用")
|
logger.warning("Bitget API Key 未配置,实盘交易功能不可用")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -362,6 +362,81 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switch-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #444;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.enabled {
|
||||||
|
color: #00ff41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.disabled {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -374,9 +449,26 @@
|
|||||||
实盘交易
|
实盘交易
|
||||||
<span class="real-badge">LIVE</span>
|
<span class="real-badge">LIVE</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="refresh-btn" @click="refreshData">
|
<div style="display: flex; align-items: center; gap: 16px;">
|
||||||
刷新
|
<!-- 自动交易开关 -->
|
||||||
</button>
|
<div class="switch-container" v-if="apiConfigured">
|
||||||
|
<span class="switch-label">自动交易</span>
|
||||||
|
<label class="switch" :class="{ 'switch-disabled': !serviceEnabled }">
|
||||||
|
<input type="checkbox"
|
||||||
|
v-model="autoTradingEnabled"
|
||||||
|
@change="toggleAutoTrading"
|
||||||
|
:disabled="!serviceEnabled || switching">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="status-text"
|
||||||
|
:class="{ enabled: autoTradingEnabled, disabled: !autoTradingEnabled }">
|
||||||
|
{{ autoTradingEnabled ? '已启用' : '已禁用' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="refresh-btn" @click="refreshData">
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -560,6 +652,8 @@
|
|||||||
serviceEnabled: false,
|
serviceEnabled: false,
|
||||||
apiConfigured: false,
|
apiConfigured: false,
|
||||||
useTestnet: true,
|
useTestnet: true,
|
||||||
|
autoTradingEnabled: false,
|
||||||
|
switching: false,
|
||||||
account: {
|
account: {
|
||||||
current_balance: 0,
|
current_balance: 0,
|
||||||
available: 0,
|
available: 0,
|
||||||
@ -622,6 +716,8 @@
|
|||||||
this.serviceEnabled = status.enabled;
|
this.serviceEnabled = status.enabled;
|
||||||
this.apiConfigured = status.api_configured;
|
this.apiConfigured = status.api_configured;
|
||||||
this.useTestnet = status.use_testnet;
|
this.useTestnet = status.use_testnet;
|
||||||
|
// 获取自动交易状态
|
||||||
|
this.autoTradingEnabled = status.auto_trading_enabled || false;
|
||||||
if (status.account) {
|
if (status.account) {
|
||||||
this.account = status.account;
|
this.account = status.account;
|
||||||
}
|
}
|
||||||
@ -631,6 +727,34 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async toggleAutoTrading() {
|
||||||
|
if (this.switching) return;
|
||||||
|
|
||||||
|
this.switching = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/real-trading/auto-trading', null, {
|
||||||
|
params: { enabled: this.autoTradingEnabled }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
// 刷新状态
|
||||||
|
await this.fetchServiceStatus();
|
||||||
|
alert(response.data.message);
|
||||||
|
} else {
|
||||||
|
// 恢复原状态
|
||||||
|
this.autoTradingEnabled = !this.autoTradingEnabled;
|
||||||
|
alert('设置失败: ' + (response.data.message || '未知错误'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置自动交易失败:', error);
|
||||||
|
// 恢复原状态
|
||||||
|
this.autoTradingEnabled = !this.autoTradingEnabled;
|
||||||
|
alert('设置失败: ' + (error.response?.data?.detail || error.message));
|
||||||
|
} finally {
|
||||||
|
this.switching = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async fetchAccountStatus() {
|
async fetchAccountStatus() {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/real-trading/account');
|
const response = await axios.get('/api/real-trading/account');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user