808 lines
29 KiB
Python
808 lines
29 KiB
Python
"""
|
||
交易执行器基类
|
||
|
||
为不同平台提供统一的交易执行接口,各平台可根据自身特性实现具体逻辑。
|
||
"""
|
||
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
|
||
|
||
# 初始化飞书通知服务
|
||
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
|
||
|
||
# ==================== 飞书通知 ====================
|
||
|
||
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:
|
||
success = result.get('success', False)
|
||
order_id = result.get('order_id', '')
|
||
error_msg = result.get('error', result.get('message', ''))
|
||
|
||
# 根据操作类型选择通知方法
|
||
if operation == 'OPEN':
|
||
await self._send_open_notification(symbol, result, details)
|
||
elif operation == 'CLOSE':
|
||
await self._send_close_notification(symbol, result, details)
|
||
elif operation == 'CANCEL':
|
||
await self._send_cancel_notification(symbol, result, details)
|
||
elif operation == 'TP_SL':
|
||
await self._send_tp_sl_notification(symbol, result, details)
|
||
elif operation == 'POSITION_MANAGEMENT':
|
||
await self._send_position_management_notification(symbol, result, details)
|
||
else:
|
||
# 通用通知
|
||
await self._send_generic_notification(operation, symbol, result, details)
|
||
|
||
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):
|
||
"""发送开仓通知"""
|
||
success = result.get('success', False)
|
||
order_id = result.get('order_id', '')
|
||
error_msg = result.get('error', result.get('message', ''))
|
||
|
||
if success:
|
||
# 成功开仓
|
||
title = f"✅ [{self.platform_name}] 开仓成功 - {symbol}"
|
||
|
||
content_parts = [
|
||
f"**平台**: {self.platform_name}",
|
||
f"**交易对**: {symbol}",
|
||
f"**订单ID**: {order_id}",
|
||
]
|
||
|
||
# 添加详情
|
||
if details:
|
||
if 'size' in details:
|
||
content_parts.append(f"**数量**: {details['size']}")
|
||
if 'price' in details:
|
||
content_parts.append(f"**价格**: ${details['price']:,.2f}")
|
||
if 'margin' in details:
|
||
content_parts.append(f"**保证金**: ${details['margin']:,.2f}")
|
||
if 'leverage' in details:
|
||
content_parts.append(f"**杠杆**: {details['leverage']}x")
|
||
if 'stop_loss' in details and details['stop_loss']:
|
||
content_parts.append(f"**止损**: ${details['stop_loss']:,.2f}")
|
||
if 'take_profit' in details and details['take_profit']:
|
||
content_parts.append(f"**止盈**: ${details['take_profit']:,.2f}")
|
||
if 'order_type' in details:
|
||
content_parts.append(f"**订单类型**: {details['order_type']}")
|
||
|
||
content = "\n".join(content_parts)
|
||
color = "green"
|
||
else:
|
||
# 开仓失败
|
||
title = f"❌ [{self.platform_name}] 开仓失败 - {symbol}"
|
||
|
||
content_parts = [
|
||
f"**平台**: {self.platform_name}",
|
||
f"**交易对**: {symbol}",
|
||
f"**错误**: {error_msg}",
|
||
]
|
||
|
||
if details and 'reason' in details:
|
||
content_parts.append(f"**原因**: {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):
|
||
"""发送平仓通知"""
|
||
success = result.get('success', False)
|
||
error_msg = result.get('error', result.get('message', ''))
|
||
|
||
if success:
|
||
title = f"✅ [{self.platform_name}] 平仓成功 - {symbol}"
|
||
|
||
content_parts = [
|
||
f"**平台**: {self.platform_name}",
|
||
f"**交易对**: {symbol}",
|
||
]
|
||
|
||
if details:
|
||
if 'pnl' in details:
|
||
pnl = details['pnl']
|
||
pnl_color = "盈利" if pnl >= 0 else "亏损"
|
||
content_parts.append(f"**{pnl_color}**: ${pnl:,.2f}")
|
||
if 'pnl_percent' in details:
|
||
content_parts.append(f"**收益率**: {details['pnl_percent']:.2f}%")
|
||
if 'exit_reason' in details:
|
||
content_parts.append(f"**平仓原因**: {details['exit_reason']}")
|
||
|
||
content = "\n".join(content_parts)
|
||
color = "green"
|
||
else:
|
||
title = f"❌ [{self.platform_name}] 平仓失败 - {symbol}"
|
||
|
||
content_parts = [
|
||
f"**平台**: {self.platform_name}",
|
||
f"**交易对**: {symbol}",
|
||
f"**错误**: {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):
|
||
"""发送撤单通知"""
|
||
success = result.get('success', False)
|
||
order_id = result.get('order_id', '')
|
||
error_msg = result.get('error', result.get('message', ''))
|
||
|
||
if success:
|
||
title = f"✅ [{self.platform_name}] 撤单成功 - {symbol}"
|
||
|
||
content_parts = [
|
||
f"**平台**: {self.platform_name}",
|
||
f"**交易对**: {symbol}",
|
||
f"**订单ID**: {order_id}",
|
||
]
|
||
|
||
if details and 'reason' in details:
|
||
content_parts.append(f"**撤单原因**: {details['reason']}")
|
||
|
||
content = "\n".join(content_parts)
|
||
color = "green"
|
||
else:
|
||
title = f"❌ [{self.platform_name}] 撤单失败 - {symbol}"
|
||
|
||
content_parts = [
|
||
f"**平台**: {self.platform_name}",
|
||
f"**交易对**: {symbol}",
|
||
f"**订单ID**: {order_id}",
|
||
f"**错误**: {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):
|
||
"""发送止盈止损设置通知"""
|
||
success = result.get('success', False)
|
||
message = result.get('message', '')
|
||
|
||
if success:
|
||
title = f"✅ [{self.platform_name}] 止盈止损设置成功 - {symbol}"
|
||
|
||
content_parts = [
|
||
f"**平台**: {self.platform_name}",
|
||
f"**交易对**: {symbol}",
|
||
]
|
||
|
||
if details:
|
||
if 'stop_loss' in details and details['stop_loss']:
|
||
content_parts.append(f"**止损**: ${details['stop_loss']:,.2f}")
|
||
if 'take_profit' in details and details['take_profit']:
|
||
content_parts.append(f"**止盈**: ${details['take_profit']:,.2f}")
|
||
if 'move_sl_reason' in details:
|
||
content_parts.append(f"**移动止损**: {details['move_sl_reason']}")
|
||
|
||
content = "\n".join(content_parts)
|
||
color = "green"
|
||
else:
|
||
title = f"⚠️ [{self.platform_name}] 止盈止损设置失败 - {symbol}"
|
||
|
||
content_parts = [
|
||
f"**平台**: {self.platform_name}",
|
||
f"**交易对**: {symbol}",
|
||
f"**错误**: {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):
|
||
"""发送持仓管理通知"""
|
||
action = result.get('action', '')
|
||
reason = result.get('reason', '')
|
||
|
||
title = f"📊 [{self.platform_name}] 持仓管理 - {symbol}"
|
||
|
||
content_parts = [
|
||
f"**平台**: {self.platform_name}",
|
||
f"**交易对**: {symbol}",
|
||
f"**操作**: {action}",
|
||
f"**原因**: {reason}",
|
||
]
|
||
|
||
if details:
|
||
if 'pnl_percent' in details:
|
||
content_parts.append(f"**盈亏**: {details['pnl_percent']:.2f}%")
|
||
if 'hold_hours' in details:
|
||
content_parts.append(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):
|
||
"""发送通用通知"""
|
||
success = result.get('success', False)
|
||
message = result.get('message', result.get('error', ''))
|
||
|
||
title = f"[{self.platform_name}] {operation} - {symbol}"
|
||
|
||
content_parts = [
|
||
f"**平台**: {self.platform_name}",
|
||
f"**操作**: {operation}",
|
||
f"**交易对**: {symbol}",
|
||
f"**状态**: {'成功' if success else '失败'}",
|
||
]
|
||
|
||
if message:
|
||
content_parts.append(f"**信息**: {message}")
|
||
|
||
if details:
|
||
for key, value in details.items():
|
||
content_parts.append(f"**{key}**: {value}")
|
||
|
||
content = "\n".join(content_parts)
|
||
color = "green" if success else "red"
|
||
|
||
await self.feishu.send_card(title, content, color)
|