456 lines
16 KiB
Python
456 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
策略决策服务
|
|
综合信号、持仓、资金等做出交易决策
|
|
"""
|
|
|
|
from typing import List, Dict, Optional
|
|
from datetime import datetime
|
|
import threading
|
|
|
|
from ...models import TradingSignal, TradingStrategy, TradingDecision
|
|
from ...models import ConsistencyRequirement, ConflictResolution
|
|
from ...models import StopLossMode, TakeProfitMode
|
|
from ...store import StrategyStore
|
|
from ..signal import SignalService
|
|
from .risk_manager import RiskManager
|
|
|
|
|
|
class StrategyService:
|
|
"""策略决策服务"""
|
|
|
|
def __init__(self, strategy_store: StrategyStore = None,
|
|
signal_service: SignalService = None,
|
|
risk_manager: RiskManager = None):
|
|
self.strategy_store = strategy_store or StrategyStore()
|
|
self.signal_service = signal_service or SignalService()
|
|
self.risk_manager = risk_manager or RiskManager()
|
|
|
|
# 持仓服务引用(外部设置)
|
|
self._position_service = None
|
|
|
|
# 待确认订单服务引用(外部设置)
|
|
self._pending_order_service = None
|
|
|
|
# 决策冷却
|
|
self._decision_cooldowns: Dict[str, datetime] = {}
|
|
self._cooldown_lock = threading.Lock()
|
|
self.decision_cooldown = 60 # 60秒冷却
|
|
|
|
print("[StrategyService] 策略决策服务已初始化")
|
|
|
|
def set_position_service(self, service) -> None:
|
|
"""设置持仓服务"""
|
|
self._position_service = service
|
|
|
|
def set_pending_order_service(self, service) -> None:
|
|
"""设置待确认订单服务"""
|
|
self._pending_order_service = service
|
|
|
|
# ==================== 策略配置 ====================
|
|
|
|
def get_strategy(self, symbol: str) -> TradingStrategy:
|
|
"""获取品种策略配置"""
|
|
return self.strategy_store.get_or_create_strategy(symbol)
|
|
|
|
def update_strategy(self, symbol: str, data: Dict) -> TradingStrategy:
|
|
"""更新策略配置"""
|
|
return self.strategy_store.update_strategy(symbol, data)
|
|
|
|
def get_all_strategies(self) -> List[TradingStrategy]:
|
|
"""获取所有策略"""
|
|
return self.strategy_store.get_all_strategies()
|
|
|
|
# ==================== 信号综合分析 ====================
|
|
|
|
def analyze_signals(self, symbol: str, signals: List[TradingSignal],
|
|
strategy: TradingStrategy) -> Dict:
|
|
"""
|
|
综合分析信号
|
|
|
|
Args:
|
|
symbol: 品种
|
|
signals: 信号列表
|
|
strategy: 策略配置
|
|
|
|
Returns:
|
|
分析结果
|
|
"""
|
|
if not signals:
|
|
return {
|
|
"total_count": 0,
|
|
"buy_count": 0,
|
|
"sell_count": 0,
|
|
"buy_weighted_score": 0,
|
|
"sell_weighted_score": 0,
|
|
"consistency": 0,
|
|
"direction": None,
|
|
"action": "none",
|
|
}
|
|
|
|
# 过滤掉未启用的信号
|
|
filtered_signals = []
|
|
for s in signals:
|
|
period = s.source_period if s.source != "key_level" else None
|
|
if strategy.is_signal_enabled(s.source, period):
|
|
filtered_signals.append(s)
|
|
|
|
if not filtered_signals:
|
|
return {
|
|
"total_count": 0,
|
|
"buy_count": 0,
|
|
"sell_count": 0,
|
|
"buy_weighted_score": 0,
|
|
"sell_weighted_score": 0,
|
|
"consistency": 0,
|
|
"direction": None,
|
|
"action": "none",
|
|
"filtered_out": len(signals),
|
|
}
|
|
|
|
buy_signals = [s for s in filtered_signals if s.action == "buy"]
|
|
sell_signals = [s for s in filtered_signals if s.action == "sell"]
|
|
|
|
# 计算加权分数(使用新的周期级别权重)
|
|
buy_score = sum(
|
|
s.confidence * strategy.get_signal_weight(s.source, s.source_period) / 100
|
|
for s in buy_signals
|
|
)
|
|
sell_score = sum(
|
|
s.confidence * strategy.get_signal_weight(s.source, s.source_period) / 100
|
|
for s in sell_signals
|
|
)
|
|
|
|
# 计算一致性
|
|
total = len(filtered_signals)
|
|
majority_count = max(len(buy_signals), len(sell_signals))
|
|
consistency = majority_count / total if total > 0 else 0
|
|
|
|
# 确定方向
|
|
direction = None
|
|
if buy_score > sell_score:
|
|
direction = "buy"
|
|
elif sell_score > buy_score:
|
|
direction = "sell"
|
|
|
|
# 检查一致性要求
|
|
action = "none"
|
|
if direction:
|
|
if strategy.consistency_requirement == ConsistencyRequirement.ANY:
|
|
action = direction
|
|
elif strategy.consistency_requirement == ConsistencyRequirement.MAJORITY:
|
|
if consistency >= 0.5:
|
|
action = direction
|
|
elif strategy.consistency_requirement == ConsistencyRequirement.ALL:
|
|
if consistency == 1.0:
|
|
action = direction
|
|
|
|
return {
|
|
"total_count": total,
|
|
"buy_count": len(buy_signals),
|
|
"sell_count": len(sell_signals),
|
|
"buy_weighted_score": round(buy_score, 2),
|
|
"sell_weighted_score": round(sell_score, 2),
|
|
"consistency": round(consistency, 2),
|
|
"direction": direction,
|
|
"action": action,
|
|
"buy_signals": [s.signal_id for s in buy_signals],
|
|
"sell_signals": [s.signal_id for s in sell_signals],
|
|
"filtered_out": len(signals) - len(filtered_signals),
|
|
}
|
|
|
|
# ==================== 决策生成 ====================
|
|
|
|
def make_decision(self, symbol: str, current_price: float,
|
|
force_signals: List[TradingSignal] = None) -> Optional[TradingDecision]:
|
|
"""
|
|
做出交易决策
|
|
|
|
Args:
|
|
symbol: 品种
|
|
current_price: 当前价格
|
|
force_signals: 强制使用的信号(用于测试)
|
|
|
|
Returns:
|
|
TradingDecision 或 None
|
|
"""
|
|
# 检查决策冷却
|
|
if self._is_in_cooldown(symbol):
|
|
return None
|
|
|
|
# 获取策略配置
|
|
strategy = self.get_strategy(symbol)
|
|
if not strategy.enabled:
|
|
return None
|
|
|
|
# 获取信号
|
|
signals = force_signals if force_signals else self.signal_service.get_active_signals(symbol)
|
|
|
|
# 过滤低置信度信号
|
|
signals = [s for s in signals if s.confidence >= strategy.min_confidence]
|
|
|
|
if not signals:
|
|
return None
|
|
|
|
# 分析信号
|
|
analysis = self.analyze_signals(symbol, signals, strategy)
|
|
|
|
if analysis["action"] == "none":
|
|
return None
|
|
|
|
action = analysis["action"]
|
|
|
|
# 选择最佳信号(用于止损止盈)
|
|
best_signal = self._select_best_signal(signals, action, strategy)
|
|
if not best_signal:
|
|
return None
|
|
|
|
# 计算止损止盈
|
|
entry_price = current_price
|
|
sl, tp = self._calculate_sl_tp(entry_price, best_signal, strategy)
|
|
|
|
if not sl or not tp or sl == 0 or tp == 0:
|
|
print(f"[StrategyService] 无效的止损止盈: sl={sl}, tp={tp}")
|
|
return None
|
|
|
|
# 计算风险
|
|
risk_points = abs(entry_price - sl)
|
|
reward_points = abs(tp - entry_price)
|
|
rr_ratio = reward_points / risk_points if risk_points > 0 else 0
|
|
|
|
# 检查风险回报比
|
|
if rr_ratio < strategy.min_risk_reward:
|
|
print(f"[StrategyService] 风险回报比 {rr_ratio:.2f} 低于最小要求 {strategy.min_risk_reward}")
|
|
return None
|
|
|
|
# 动态止损范围(根据价格调整)
|
|
# 最小止损 = 价格的 0.05% 或 5 点(取较大)
|
|
# 最大止损 = 价格的 2% 或 100 点(取较小)
|
|
price_min_sl = entry_price * 0.0005 # 价格的 0.05%
|
|
price_max_sl = entry_price * 0.02 # 价格的 2%
|
|
|
|
# 确保 min <= max
|
|
dynamic_min_sl = max(1.0, price_min_sl) # 最小至少 1 点
|
|
dynamic_max_sl = max(dynamic_min_sl, price_max_sl) # 最大至少等于最小
|
|
|
|
# 如果动态范围不合理,跳过
|
|
if dynamic_min_sl > dynamic_max_sl:
|
|
print(f"[StrategyService] 动态止损范围无效: [{dynamic_min_sl:.2f}, {dynamic_max_sl:.2f}], 跳过决策")
|
|
return None
|
|
|
|
# 检查止损点数
|
|
if risk_points < dynamic_min_sl or risk_points > dynamic_max_sl:
|
|
print(f"[StrategyService] 止损点数 {risk_points:.2f} 不在动态范围 [{dynamic_min_sl:.2f}, {dynamic_max_sl:.2f}] (价格={entry_price:.2f})")
|
|
return None
|
|
|
|
# 计算手数
|
|
volume = self.risk_manager.calculate_volume(symbol, risk_points, strategy)
|
|
if volume <= 0:
|
|
return None
|
|
|
|
# 检查持仓限制
|
|
position_check = self._check_position_limits(symbol, strategy, action)
|
|
|
|
# 检查风险限制
|
|
risk_check = self.risk_manager.check_risk(symbol, volume, risk_points)
|
|
|
|
# 如果检查不通过,返回拒绝的决策
|
|
if not position_check.get("allowed", True) or not risk_check.get("allowed", True):
|
|
# 即使被拒绝也要设置冷却,避免频繁推送
|
|
self._set_cooldown(symbol)
|
|
decision = TradingDecision(
|
|
symbol=symbol,
|
|
strategy_id=strategy.strategy_id,
|
|
action="none",
|
|
decision_type="rejected",
|
|
signals=[s.to_dict() for s in signals],
|
|
signal_summary=analysis,
|
|
decision_reason="风控检查未通过",
|
|
confidence_score=0,
|
|
position_check=position_check,
|
|
risk_check=risk_check,
|
|
status="rejected",
|
|
)
|
|
return decision
|
|
|
|
# 设置决策冷却
|
|
self._set_cooldown(symbol)
|
|
|
|
# 生成决策理由
|
|
decision_reason = self._generate_decision_reason(analysis, best_signal)
|
|
|
|
# 创建决策
|
|
decision = TradingDecision(
|
|
symbol=symbol,
|
|
strategy_id=strategy.strategy_id,
|
|
action=action,
|
|
decision_type="signal_combined" if len(signals) > 1 else "single_signal",
|
|
signals=[s.to_dict() for s in signals],
|
|
signal_summary=analysis,
|
|
entry_price=entry_price,
|
|
sl=round(sl, 2),
|
|
tp=round(tp, 2),
|
|
volume=volume,
|
|
risk_points=round(risk_points, 2),
|
|
reward_points=round(reward_points, 2),
|
|
risk_reward_ratio=round(rr_ratio, 2),
|
|
decision_reason=decision_reason,
|
|
confidence_score=analysis["buy_weighted_score"] if action == "buy" else analysis["sell_weighted_score"],
|
|
position_check=position_check,
|
|
risk_check=risk_check,
|
|
)
|
|
|
|
print(f"[StrategyService] 生成决策: {decision.decision_id} {action} {symbol} @ {entry_price}")
|
|
|
|
return decision
|
|
|
|
def _select_best_signal(self, signals: List[TradingSignal],
|
|
action: str, strategy: TradingStrategy) -> Optional[TradingSignal]:
|
|
"""选择最佳信号"""
|
|
filtered = [s for s in signals if s.action == action]
|
|
if not filtered:
|
|
return None
|
|
|
|
if strategy.conflict_resolution == ConflictResolution.HIGHEST_CONFIDENCE:
|
|
return max(filtered, key=lambda s: s.confidence)
|
|
elif strategy.conflict_resolution == ConflictResolution.HIGHEST_WEIGHT:
|
|
return max(filtered, key=lambda s: s.confidence * strategy.get_signal_weight(s.source, s.source_period))
|
|
else:
|
|
return filtered[0]
|
|
|
|
def _calculate_sl_tp(self, entry_price: float, signal: TradingSignal,
|
|
strategy: TradingStrategy) -> tuple:
|
|
"""计算止损止盈"""
|
|
# 止损
|
|
if strategy.sl_mode == StopLossMode.SIGNAL:
|
|
sl = signal.suggested_sl
|
|
elif strategy.sl_mode == StopLossMode.FIXED_POINTS:
|
|
if signal.action == "buy":
|
|
sl = entry_price - strategy.sl_fixed_points
|
|
else:
|
|
sl = entry_price + strategy.sl_fixed_points
|
|
else:
|
|
sl = signal.suggested_sl
|
|
|
|
# 止盈
|
|
if strategy.tp_mode == TakeProfitMode.SIGNAL:
|
|
tp = signal.suggested_tp
|
|
elif strategy.tp_mode == TakeProfitMode.FIXED_POINTS:
|
|
if signal.action == "buy":
|
|
tp = entry_price + strategy.tp_fixed_points
|
|
else:
|
|
tp = entry_price - strategy.tp_fixed_points
|
|
elif strategy.tp_mode == TakeProfitMode.RISK_REWARD:
|
|
risk = abs(entry_price - sl)
|
|
if signal.action == "buy":
|
|
tp = entry_price + risk * strategy.tp_risk_reward
|
|
else:
|
|
tp = entry_price - risk * strategy.tp_risk_reward
|
|
else:
|
|
tp = signal.suggested_tp
|
|
|
|
return sl, tp
|
|
|
|
def _check_position_limits(self, symbol: str, strategy: TradingStrategy,
|
|
action: str) -> Dict:
|
|
"""检查持仓限制"""
|
|
current_positions = 0
|
|
same_direction = 0
|
|
opposite_direction = 0
|
|
|
|
if self._position_service:
|
|
positions = self._position_service.get_positions(symbol)
|
|
current_positions = len(positions)
|
|
for pos in positions:
|
|
# PositionData.to_dict() 返回 direction 字段
|
|
pos_direction = pos.get('direction', '')
|
|
if pos_direction == action:
|
|
same_direction += 1
|
|
else:
|
|
opposite_direction += 1
|
|
|
|
return self.risk_manager.check_position_limit(
|
|
symbol, strategy, current_positions, same_direction, opposite_direction, action
|
|
)
|
|
|
|
def _generate_decision_reason(self, analysis: Dict, signal: TradingSignal) -> str:
|
|
"""生成决策理由"""
|
|
reasons = []
|
|
|
|
total = analysis["total_count"]
|
|
buy_count = analysis["buy_count"]
|
|
sell_count = analysis["sell_count"]
|
|
direction = analysis["direction"]
|
|
|
|
if total == 1:
|
|
reasons.append(f"单一信号({signal.source})建议{direction}")
|
|
else:
|
|
reasons.append(f"{total}个信号中{buy_count}个买入、{sell_count}个卖出")
|
|
|
|
reasons.append(f"综合判断: {direction}")
|
|
reasons.append(f"风险回报比: {signal.risk_reward_ratio:.2f}")
|
|
|
|
return " | ".join(reasons)
|
|
|
|
def _is_in_cooldown(self, symbol: str) -> bool:
|
|
"""检查是否在冷却期"""
|
|
with self._cooldown_lock:
|
|
if symbol in self._decision_cooldowns:
|
|
last_time = self._decision_cooldowns[symbol]
|
|
elapsed = (datetime.now() - last_time).total_seconds()
|
|
return elapsed < self.decision_cooldown
|
|
return False
|
|
|
|
def _set_cooldown(self, symbol: str) -> None:
|
|
"""设置冷却"""
|
|
with self._cooldown_lock:
|
|
self._decision_cooldowns[symbol] = datetime.now()
|
|
|
|
# ==================== 执行决策 ====================
|
|
|
|
def execute_decision(self, decision: TradingDecision) -> Optional[str]:
|
|
"""
|
|
执行决策(生成待确认订单)
|
|
|
|
Args:
|
|
decision: 交易决策
|
|
|
|
Returns:
|
|
订单ID 或 None
|
|
"""
|
|
if decision.action == "none":
|
|
return None
|
|
|
|
if not self._pending_order_service:
|
|
print("[StrategyService] 待确认订单服务未设置")
|
|
return None
|
|
|
|
# 创建订单
|
|
order_id = self._pending_order_service.create_order(
|
|
symbol=decision.symbol,
|
|
action=decision.action,
|
|
price=decision.entry_price,
|
|
mount=decision.volume,
|
|
sl=decision.sl,
|
|
tp=decision.tp,
|
|
reason=decision.decision_reason,
|
|
description=f"Strategy: {decision.strategy_id}",
|
|
source="strategy_decision",
|
|
)
|
|
|
|
decision.order_id = order_id
|
|
decision.status = "confirmed"
|
|
|
|
print(f"[StrategyService] 决策已执行,订单ID: {order_id}")
|
|
return order_id
|
|
|
|
# ==================== 状态 ====================
|
|
|
|
def get_status(self) -> Dict:
|
|
"""获取服务状态"""
|
|
return {
|
|
"strategy_store": self.strategy_store.get_status(),
|
|
"signal_service": self.signal_service.get_status(),
|
|
"risk_manager": self.risk_manager.get_status(),
|
|
} |