stock-ai-agent/backend/app/crypto_agent/executor/base_executor.py
2026-04-25 13:20:28 +08:00

825 lines
32 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.

"""
交易执行器基类
为不同平台提供统一的交易执行接口,各平台可根据自身特性实现具体逻辑。
"""
from abc import ABC, abstractmethod
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta
from app.utils.logger import logger
class BaseExecutor(ABC):
"""交易执行器基类"""
MIN_EFFECTIVE_LEVERAGE_BY_SIGNAL_TYPE = {
'short_term': 4.0,
'medium_term': 2.0,
'long_term': 2.0,
}
def __init__(self, platform_name: str):
self.platform_name = platform_name
self.account_id = "default"
# 初始化飞书通知服务
try:
from app.services.feishu_service import get_feishu_paper_trading_service
self.feishu = get_feishu_paper_trading_service()
except Exception as e:
logger.warning(f"[{self.platform_name}] 飞书服务初始化失败: {e}")
self.feishu = None
# 延迟导入飞书服务,避免循环依赖
self._feishu_service = None
# ==================== 核心执行方法 ====================
@abstractmethod
async def execute_open(self, decision: Dict[str, Any],
current_price: float) -> Dict[str, Any]:
"""
执行开仓
Args:
decision: 决策字典(包含 symbol, action, margin, stop_loss, take_profit 等)
current_price: 当前价格
Returns:
执行结果 {'success': bool, 'order_id': str, 'message': str, ...}
"""
pass
@abstractmethod
async def execute_close(self, decision: Dict[str, Any],
current_price: float) -> Dict[str, Any]:
"""执行平仓"""
pass
@abstractmethod
async def execute_cancel(self, order_id: str, symbol: str) -> Dict[str, Any]:
"""执行撤单"""
pass
# ==================== 订单类型决策 ====================
def decide_order_type(self, signal: Dict[str, Any],
current_price: float) -> tuple:
"""
决定订单类型(市价/限价)
Returns:
(order_type, reason) - 'market''limit'
"""
entry_price = signal.get('entry_price', current_price)
if not entry_price or entry_price == 0:
return 'market', "无入场价,使用市价单"
price_diff_pct = abs(entry_price - current_price) / current_price * 100
# 平台特定的阈值
threshold = self.get_market_order_threshold()
if price_diff_pct < threshold:
return 'market', f"价格差 {price_diff_pct:.3f}% < {threshold}%,使用市价单"
else:
return 'limit', f"价格差 {price_diff_pct:.3f}% >= {threshold}%,使用限价单 @ ${entry_price:.2f}"
@abstractmethod
def get_market_order_threshold(self) -> float:
"""
获取市价单阈值(百分比)
价格差小于此阈值时使用市价单
"""
pass
# ==================== 止盈止损设置 ====================
@abstractmethod
async def set_stop_loss_take_profit(self,
symbol: str,
order_id: str,
stop_loss: Optional[float],
take_profit: Optional[float],
position_size: float) -> Dict[str, Any]:
"""
设置止盈止损
Args:
symbol: 交易对
order_id: 订单ID或持仓ID
stop_loss: 止损价
take_profit: 止盈价
position_size: 持仓数量
Returns:
{'success': bool, 'message': str}
"""
pass
def should_set_tp_sl_on_order(self) -> bool:
"""
是否在下单时设置止盈止损
Returns:
True: 下单参数中携带 TP/SL
False: 成交后单独设置
"""
return False # 默认成交后设置
# ==================== 挂单超时管理 ====================
def check_pending_order_timeout(self,
pending_orders: List[Dict],
timeout_hours: Optional[float] = None) -> List[Dict[str, Any]]:
"""
检查挂单超时
Returns:
需要取消的订单列表
"""
if timeout_hours is None:
timeout_hours = self.get_pending_order_timeout()
timeout_orders = []
now = datetime.now()
for order in pending_orders:
created_at = order.get('created_at')
if not created_at:
continue
# 解析时间
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
age_hours = (now - created_at).total_seconds() / 3600
if age_hours > timeout_hours:
timeout_orders.append({
'order_id': order.get('order_id'),
'symbol': order.get('symbol'),
'age_hours': age_hours,
'action': 'CANCEL',
'reason': f"挂单超时 {age_hours:.1f}h > {timeout_hours}h"
})
return timeout_orders
@abstractmethod
def get_pending_order_timeout(self) -> float:
"""
获取挂单超时时间(小时)
Returns:
超时小时数
"""
pass
# ==================== 持仓管理 ====================
def check_position_management(self,
positions: List[Dict],
current_prices: Dict[str, float],
volatility_data: Optional[Dict[str, float]] = None) -> List[Dict[str, Any]]:
"""
持仓管理检查
Args:
positions: 持仓列表
current_prices: 当前价格 {symbol: price}
volatility_data: 波动率数据 {symbol: atr_pct}1h ATR / price
Returns:
建议的操作列表
"""
actions = []
now = datetime.now()
for pos in positions:
symbol = pos.get('symbol')
current_price = current_prices.get(symbol, pos.get('entry_price', 0))
if current_price <= 0:
continue
# 计算盈亏百分比
entry_price = pos.get('entry_price', 0)
side = pos.get('side')
if side == 'buy':
pnl_pct = (current_price - entry_price) / entry_price * 100
else:
pnl_pct = (entry_price - current_price) / entry_price * 100
# 计算持仓时长
opened_at = pos.get('opened_at')
if opened_at:
if isinstance(opened_at, str):
opened_at = datetime.fromisoformat(opened_at.replace('Z', '+00:00'))
hold_hours = (now - opened_at).total_seconds() / 3600
else:
hold_hours = 0
# 获取平台特定规则
target_profit_pct, max_hold_hours = self.get_position_exit_rules()
# 规则1: 达到目标盈利
if pnl_pct >= target_profit_pct:
actions.append({
'symbol': symbol,
'action': 'TAKE_PROFIT',
'reason': f"盈利 {pnl_pct:.1f}% >= {target_profit_pct}%",
'priority': 1
})
# 规则2: 持仓超时
if hold_hours > max_hold_hours:
actions.append({
'symbol': symbol,
'action': 'TIME_EXIT',
'reason': f"持仓 {hold_hours:.1f}h > {max_hold_hours}h",
'priority': 2
})
# 规则3: 动态移动止损(基于 ATR 波动率)
current_sl = pos.get('stop_loss')
if not current_sl:
continue
# 获取波动率ATR 占价格的百分比(如 0.015 = 1.5%
atr_pct = (volatility_data.get(symbol) if volatility_data else None) or 0.02
# 波动率倍数:低波动(震荡)给更多空间,高波动(趋势)收得更紧
atr_multiplier = max(1.5, 2.0 / atr_pct)
# 两级移动止损
# 第一级:盈利 >= 0.6 * ATR% * multiplier → SL 移到入场价 ± 1倍ATR给足震荡空间
# 第二级:盈利 >= 1.0 * ATR% * multiplier → SL 移到保本
step1_threshold = atr_pct * 100 * atr_multiplier * 0.6
step2_threshold = atr_pct * 100 * atr_multiplier * 1.0
if pnl_pct >= step2_threshold:
# 盈利充足,移到保本
if (side == 'buy' and current_sl < entry_price) or \
(side == 'sell' and current_sl > entry_price):
actions.append({
'symbol': symbol,
'action': 'MOVE_SL',
'new_sl': entry_price,
'pnl_pct': pnl_pct,
'reason': f"盈利 {pnl_pct:.1f}% >= {step2_threshold:.1f}%,移动止损到保本",
'priority': 3
})
elif pnl_pct >= step1_threshold:
# 盈利初步,移到入场价 ± 1倍ATR保留震荡空间不急于保本
if side == 'buy' and current_sl < entry_price:
new_sl = entry_price * (1 - atr_pct)
if new_sl > current_sl:
actions.append({
'symbol': symbol,
'action': 'MOVE_SL',
'new_sl': new_sl,
'pnl_pct': pnl_pct,
'reason': f"盈利 {pnl_pct:.1f}% >= {step1_threshold:.1f}%,移动止损到入场价-1ATR (${new_sl:.2f})",
'priority': 3
})
elif side == 'sell' and current_sl > entry_price:
new_sl = entry_price * (1 + atr_pct)
if new_sl < current_sl:
actions.append({
'symbol': symbol,
'action': 'MOVE_SL',
'new_sl': new_sl,
'pnl_pct': pnl_pct,
'reason': f"盈利 {pnl_pct:.1f}% >= {step1_threshold:.1f}%,移动止损到入场价+1ATR (${new_sl:.2f})",
'priority': 3
})
# 按优先级排序
actions.sort(key=lambda x: x.get('priority', 99))
return actions
@abstractmethod
def get_position_exit_rules(self) -> tuple:
"""
获取持仓退出规则
Returns:
(target_profit_pct, max_hold_hours)
"""
pass
# ==================== 交易成本管理 ====================
def calculate_effective_margin(self,
available: float,
margin: float) -> float:
"""
计算实际可用保证金(预留手续费)
Args:
available: 可用余额
margin: 计算出的保证金
Returns:
调整后的保证金
"""
# 获取平台手续费率
fee_rate = self.get_fee_rate()
# 预留开仓 + 平仓手续费
fee_reserve = margin * fee_rate * 2
# 调整保证金
adjusted_margin = margin + fee_reserve
# 不超过可用余额的 99%
max_usable = available * 0.99
adjusted_margin = min(adjusted_margin, max_usable)
if adjusted_margin < margin:
logger.info(f"[{self.platform_name}] 保证金调整: ${margin:.2f} → ${adjusted_margin:.2f} "
f"(预留手续费 ${(fee_reserve):.2f})")
return adjusted_margin
def get_min_effective_leverage(self, decision: Dict[str, Any]) -> float:
signal_type = decision.get('timeframe') or decision.get('type') or 'medium_term'
return float(self.MIN_EFFECTIVE_LEVERAGE_BY_SIGNAL_TYPE.get(signal_type, 2.0))
def validate_effective_leverage(self,
decision: Dict[str, Any],
margin: float,
actual_position_value: float) -> tuple[bool, str, float]:
if margin <= 0:
return False, "保证金无效", 0.0
effective_leverage = actual_position_value / margin if margin > 0 else 0.0
min_effective_leverage = self.get_min_effective_leverage(decision)
if effective_leverage + 1e-9 < min_effective_leverage:
signal_type = decision.get('timeframe') or decision.get('type') or 'medium_term'
return (
False,
f"{signal_type} 实际有效杠杆 {effective_leverage:.2f}x < 最小要求 {min_effective_leverage:.1f}x",
effective_leverage,
)
return True, "", effective_leverage
@abstractmethod
def get_fee_rate(self) -> float:
"""
获取手续费率
Returns:
手续费率(如 0.0006 = 0.06%
"""
pass
# ==================== API 重试机制 ====================
async def execute_with_retry(self,
func,
max_retries: int = None,
delay: float = 1.0) -> Any:
"""
带 API 限流感知的重试机制
Args:
func: 要执行的异步函数
max_retries: 最大重试次数None 则使用平台默认)
delay: 初始延迟秒数
Returns:
函数返回值
"""
if max_retries is None:
max_retries = self.get_max_retries()
last_error = None
for attempt in range(max_retries):
try:
return await func()
except Exception as e:
last_error = e
error_msg = str(e)
# 检查是否是限流错误
if self.is_rate_limit_error(error_msg):
wait_time = self.get_rate_limit_wait_time(error_msg, attempt)
logger.warning(f"[{self.platform_name}] API 限流,等待 {wait_time}s 后重试 "
f"(尝试 {attempt + 1}/{max_retries})")
import asyncio
await asyncio.sleep(wait_time)
# 其他错误
else:
if attempt < max_retries - 1:
wait_time = delay * (attempt + 1)
logger.warning(f"[{self.platform_name}] 执行失败: {error_msg}"
f"{wait_time}s 后重试 (尝试 {attempt + 1}/{max_retries})")
import asyncio
await asyncio.sleep(wait_time)
else:
logger.error(f"[{self.platform_name}] 执行失败,已达最大重试次数: {error_msg}")
raise last_error
@abstractmethod
def get_max_retries(self) -> int:
"""获取最大重试次数"""
pass
@abstractmethod
def is_rate_limit_error(self, error_msg: str) -> bool:
"""判断是否是限流错误"""
pass
@abstractmethod
def get_rate_limit_wait_time(self, error_msg: str, attempt: int) -> float:
"""
获取限流等待时间
Args:
error_msg: 错误信息
attempt: 当前尝试次数
Returns:
等待秒数
"""
pass
# ==================== 挂单价格优化 ====================
def should_update_pending_order(self,
new_price: float,
old_price: float,
side: str) -> tuple:
"""
是否需要更新挂单价格
Args:
new_price: 新价格
old_price: 旧价格
side: 方向 (buy/sell)
Returns:
(should_update, reason)
"""
price_diff_pct = abs(new_price - old_price) / old_price * 100
threshold = self.get_price_update_threshold()
if price_diff_pct < threshold:
return False, f"价格差 {price_diff_pct:.3f}% < {threshold}%,保持原挂单"
# 检查是否更优
if side == 'buy':
# 做多:价格更低更优
is_better = new_price < old_price
if is_better:
return True, f"新价格更低(更优),更新挂单 ${old_price:.2f} → ${new_price:.2f}"
else:
return False, "新价格更高(更差),保持原挂单"
else:
# 做空:价格更高更优
is_better = new_price > old_price
if is_better:
return True, f"新价格更高(更优),更新挂单 ${old_price:.2f} → ${new_price:.2f}"
else:
return False, "新价格更低(更差),保持原挂单"
@abstractmethod
def get_price_update_threshold(self) -> float:
"""
获取价格更新阈值(百分比)
Returns:
价格差异阈值(如 0.5 = 0.5%
"""
pass
# ==================== 移动止损 ====================
@abstractmethod
async def move_stop_loss(self,
symbol: str,
new_stop_loss: float,
current_stop_loss: Optional[float] = None) -> Dict[str, Any]:
"""
移动止损
Args:
symbol: 交易对
new_stop_loss: 新的止损价
current_stop_loss: 当前止损价(可选)
Returns:
{'success': bool, 'message': str}
"""
pass
# ==================== 飞书通知 ====================
def _normalize_notification_context(self,
details: Optional[Dict[str, Any]] = None,
account_id: str = "default",
target_key: str = "") -> Dict[str, str]:
resolved_account_id = str(account_id or getattr(self, 'account_id', 'default') or 'default')
resolved_target_key = target_key or (
f"{self.platform_name}:{resolved_account_id}" if resolved_account_id and resolved_account_id != "default" else self.platform_name
)
return {
"account_id": resolved_account_id,
"target_key": resolved_target_key,
"platform_label": self.platform_name,
}
def _append_notification_detail(self, content_parts: List[str], label: str, value: Any):
if value is None or value == "":
return
content_parts.append(f"**{label}**: {value}")
def _build_notification_header(self,
symbol: str,
account_id: str,
target_key: str) -> List[str]:
return [
f"**执行目标**: {target_key}",
f"**平台**: {self.platform_name}",
f"**账号**: {account_id}",
f"**交易对**: {symbol}",
]
async def send_execution_notification(self,
operation: str,
symbol: str,
result: Dict[str, Any],
details: Optional[Dict[str, Any]] = None):
"""
发送执行结果通知(统一入口)
Args:
operation: 操作类型 ('OPEN', 'CLOSE', 'CANCEL', 'TP_SL')
symbol: 交易对
result: 执行结果 {'success': bool, 'order_id': str, ...}
details: 额外详情
"""
if not self.feishu:
return
try:
details = dict(details or {})
account_id = details.get('account_id') or getattr(self, 'account_id', 'default')
target_key = details.get('target_key') or (
f"{self.platform_name}:{account_id}" if account_id and account_id != "default" else self.platform_name
)
# 根据操作类型选择通知方法
if operation == 'OPEN':
await self._send_open_notification(symbol, result, details, account_id, target_key)
elif operation == 'CLOSE':
await self._send_close_notification(symbol, result, details, account_id, target_key)
elif operation == 'CANCEL':
await self._send_cancel_notification(symbol, result, details, account_id, target_key)
elif operation == 'TP_SL':
await self._send_tp_sl_notification(symbol, result, details, account_id, target_key)
elif operation == 'POSITION_MANAGEMENT':
await self._send_position_management_notification(symbol, result, details, account_id, target_key)
else:
# 通用通知
await self._send_generic_notification(operation, symbol, result, details, account_id, target_key)
except Exception as e:
logger.error(f"[{self.platform_name}] 发送执行通知失败: {e}")
async def _send_open_notification(self,
symbol: str,
result: Dict[str, Any],
details: Optional[Dict[str, Any]] = None,
account_id: str = "default",
target_key: str = ""):
"""发送开仓通知"""
success = result.get('success', False)
order_id = result.get('order_id', '')
error_msg = result.get('error', result.get('message', ''))
if success:
# 成功开仓
title = f"✅ [{target_key or self.platform_name}] 开仓成功 - {symbol}"
content_parts = self._build_notification_header(symbol, account_id, target_key)
self._append_notification_detail(content_parts, "订单ID", order_id)
# 添加详情
if details:
self._append_notification_detail(content_parts, "数量", details.get('size'))
if details.get('price') is not None:
self._append_notification_detail(content_parts, "价格", f"${details['price']:,.2f}")
if details.get('margin') is not None:
self._append_notification_detail(content_parts, "保证金", f"${details['margin']:,.2f}")
if details.get('notional') is not None:
self._append_notification_detail(content_parts, "名义仓位", f"${details['notional']:,.2f}")
if details.get('leverage') is not None:
self._append_notification_detail(content_parts, "杠杆", f"{details['leverage']}x")
if details.get('stop_loss') is not None:
self._append_notification_detail(content_parts, "止损", f"${details['stop_loss']:,.2f}")
if details.get('take_profit') is not None:
self._append_notification_detail(content_parts, "止盈", f"${details['take_profit']:,.2f}")
self._append_notification_detail(content_parts, "订单类型", details.get('order_type'))
content = "\n".join(content_parts)
color = "green"
else:
# 开仓失败
title = f"❌ [{target_key or self.platform_name}] 开仓失败 - {symbol}"
content_parts = self._build_notification_header(symbol, account_id, target_key)
self._append_notification_detail(content_parts, "错误", error_msg)
if details and 'reason' in details:
self._append_notification_detail(content_parts, "原因", details['reason'])
content = "\n".join(content_parts)
color = "red"
await self.feishu.send_card(title, content, color)
async def _send_close_notification(self,
symbol: str,
result: Dict[str, Any],
details: Optional[Dict[str, Any]] = None,
account_id: str = "default",
target_key: str = ""):
"""发送平仓通知"""
success = result.get('success', False)
error_msg = result.get('error', result.get('message', ''))
if success:
title = f"✅ [{target_key or self.platform_name}] 平仓成功 - {symbol}"
content_parts = self._build_notification_header(symbol, account_id, target_key)
if details:
if 'pnl' in details:
pnl = details['pnl']
pnl_color = "盈利" if pnl >= 0 else "亏损"
self._append_notification_detail(content_parts, pnl_color, f"${pnl:,.2f}")
if 'pnl_percent' in details:
self._append_notification_detail(content_parts, "收益率", f"{details['pnl_percent']:.2f}%")
if 'exit_reason' in details:
self._append_notification_detail(content_parts, "平仓原因", details['exit_reason'])
content = "\n".join(content_parts)
color = "green"
else:
title = f"❌ [{target_key or self.platform_name}] 平仓失败 - {symbol}"
content_parts = self._build_notification_header(symbol, account_id, target_key)
self._append_notification_detail(content_parts, "错误", error_msg)
content = "\n".join(content_parts)
color = "red"
await self.feishu.send_card(title, content, color)
async def _send_cancel_notification(self,
symbol: str,
result: Dict[str, Any],
details: Optional[Dict[str, Any]] = None,
account_id: str = "default",
target_key: str = ""):
"""发送撤单通知"""
success = result.get('success', False)
order_id = result.get('order_id', '')
error_msg = result.get('error', result.get('message', ''))
if success:
title = f"✅ [{target_key or self.platform_name}] 撤单成功 - {symbol}"
content_parts = self._build_notification_header(symbol, account_id, target_key)
self._append_notification_detail(content_parts, "订单ID", order_id)
if details and 'reason' in details:
self._append_notification_detail(content_parts, "撤单原因", details['reason'])
content = "\n".join(content_parts)
color = "green"
else:
title = f"❌ [{target_key or self.platform_name}] 撤单失败 - {symbol}"
content_parts = self._build_notification_header(symbol, account_id, target_key)
self._append_notification_detail(content_parts, "订单ID", order_id)
self._append_notification_detail(content_parts, "错误", error_msg)
content = "\n".join(content_parts)
color = "red"
await self.feishu.send_card(title, content, color)
async def _send_tp_sl_notification(self,
symbol: str,
result: Dict[str, Any],
details: Optional[Dict[str, Any]] = None,
account_id: str = "default",
target_key: str = ""):
"""发送止盈止损设置通知"""
success = result.get('success', False)
message = result.get('message', '')
if success:
title = f"✅ [{target_key or self.platform_name}] 止盈止损设置成功 - {symbol}"
content_parts = self._build_notification_header(symbol, account_id, target_key)
if details:
if 'stop_loss' in details and details['stop_loss'] is not None:
self._append_notification_detail(content_parts, "止损", f"${details['stop_loss']:,.2f}")
if 'take_profit' in details and details['take_profit'] is not None:
self._append_notification_detail(content_parts, "止盈", f"${details['take_profit']:,.2f}")
if 'move_sl_reason' in details:
self._append_notification_detail(content_parts, "移动止损", details['move_sl_reason'])
content = "\n".join(content_parts)
color = "green"
else:
title = f"⚠️ [{target_key or self.platform_name}] 止盈止损设置失败 - {symbol}"
content_parts = self._build_notification_header(symbol, account_id, target_key)
self._append_notification_detail(content_parts, "错误", message)
content = "\n".join(content_parts)
color = "orange"
await self.feishu.send_card(title, content, color)
async def _send_position_management_notification(self,
symbol: str,
result: Dict[str, Any],
details: Optional[Dict[str, Any]] = None,
account_id: str = "default",
target_key: str = ""):
"""发送持仓管理通知"""
action = result.get('action', '')
reason = result.get('reason', '')
title = f"📊 [{target_key or self.platform_name}] 持仓管理 - {symbol}"
content_parts = self._build_notification_header(symbol, account_id, target_key)
self._append_notification_detail(content_parts, "操作", action)
self._append_notification_detail(content_parts, "原因", reason)
if details:
if 'pnl_percent' in details:
self._append_notification_detail(content_parts, "盈亏", f"{details['pnl_percent']:.2f}%")
if 'hold_hours' in details:
self._append_notification_detail(content_parts, "持仓时长", f"{details['hold_hours']:.1f}h")
content = "\n".join(content_parts)
# 根据操作类型选择颜色
if action == 'TAKE_PROFIT':
color = "green"
elif action == 'TIME_EXIT':
color = "orange"
elif action == 'MOVE_SL':
color = "blue"
else:
color = "blue"
await self.feishu.send_card(title, content, color)
async def _send_generic_notification(self,
operation: str,
symbol: str,
result: Dict[str, Any],
details: Optional[Dict[str, Any]] = None,
account_id: str = "default",
target_key: str = ""):
"""发送通用通知"""
success = result.get('success', False)
message = result.get('message', result.get('error', ''))
title = f"[{target_key or self.platform_name}] {operation} - {symbol}"
content_parts = self._build_notification_header(symbol, account_id, target_key)
self._append_notification_detail(content_parts, "操作", operation)
self._append_notification_detail(content_parts, "状态", '成功' if success else '失败')
if message:
self._append_notification_detail(content_parts, "信息", message)
if details:
for key, value in details.items():
if key in {'account_id', 'target_key'}:
continue
self._append_notification_detail(content_parts, key, value)
content = "\n".join(content_parts)
color = "green" if success else "red"
await self.feishu.send_card(title, content, color)