Refactor: restructure market module with services, stores, and utils

This commit is contained in:
guaiwoluo2020
2026-03-19 09:47:41 +08:00
parent ef15b92342
commit b7ea37ad09
78 changed files with 10594 additions and 7645 deletions
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
策略服务模块
"""
from .strategy_service import StrategyService
from .risk_manager import RiskManager
__all__ = [
'StrategyService',
'RiskManager',
]
+257
View File
@@ -0,0 +1,257 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
风险管理服务
"""
from typing import Dict, Optional
from datetime import datetime
from ...models import TradingStrategy
class RiskManager:
"""风险管理服务"""
def __init__(self):
# 账户信息(从外部更新)
self._account_balance: float = 0.0
self._account_equity: float = 0.0
self._free_margin: float = 0.0
# 每日风险限制
self._daily_risk_limit: float = 5.0 # 每日最大风险百分比
self._daily_risk_used: float = 0.0 # 今日已使用风险
# 品种配置(点值、最小手数等)
self._symbol_config: Dict[str, Dict] = {}
# 统计服务引用(用于获取账户信息)
self._statistics_service = None
print("[RiskManager] 风险管理服务已初始化")
def set_statistics_service(self, service) -> None:
"""设置统计服务引用"""
self._statistics_service = service
def _refresh_account_info(self) -> None:
"""从统计服务刷新账户信息"""
if not self._statistics_service:
return
try:
account_info = self._statistics_service.get_account_info()
if account_info:
self._account_balance = account_info.get('balance', 0.0)
self._account_equity = account_info.get('equity', 0.0)
# free_margin 通常等于 equity - used_margin,这里用 equity 近似
self._free_margin = account_info.get('equity', 0.0)
except Exception as e:
print(f"[RiskManager] 刷新账户信息失败: {e}")
# ==================== 账户信息 ====================
def update_account_info(self, balance: float, equity: float, free_margin: float) -> None:
"""更新账户信息"""
self._account_balance = balance
self._account_equity = equity
self._free_margin = free_margin
def get_account_balance(self) -> float:
"""获取账户余额"""
return self._account_balance
def get_account_equity(self) -> float:
"""获取账户权益"""
return self._account_equity
# ==================== 品种配置 ====================
def set_symbol_config(self, symbol: str, config: Dict) -> None:
"""设置品种配置"""
self._symbol_config[symbol] = config
def get_symbol_config(self, symbol: str) -> Dict:
"""获取品种配置"""
return self._symbol_config.get(symbol, {
"point_value": 1.0, # 点值
"min_volume": 0.01, # 最小手数
"max_volume": 10.0, # 最大手数
"volume_step": 0.01, # 手数步长
})
# ==================== 手数计算 ====================
def calculate_volume(self, symbol: str, risk_points: float,
strategy: TradingStrategy) -> float:
"""
计算交易手数
Args:
symbol: 品种
risk_points: 风险点数
strategy: 策略配置
Returns:
计算的手数
"""
config = self.get_symbol_config(symbol)
point_value = config.get('point_value', 1.0)
min_volume = config.get('min_volume', 0.01)
max_volume = config.get('max_volume', 10.0)
volume_step = config.get('volume_step', 0.01)
if strategy.volume_mode == "fixed":
volume = strategy.fixed_volume
elif strategy.volume_mode == "risk_percent":
# 根据风险百分比计算手数
risk_amount = self._account_balance * (strategy.risk_percent / 100)
# 手数 = 风险金额 / (风险点数 * 点值)
if risk_points > 0 and point_value > 0:
volume = risk_amount / (risk_points * point_value)
else:
volume = min_volume
else:
volume = strategy.fixed_volume
# 应用最大风险点数限制
if risk_points > strategy.max_risk_points:
print(f"[RiskManager] 风险点数 {risk_points} 超过最大限制 {strategy.max_risk_points}")
return 0.0
# 限制手数范围
volume = max(min_volume, min(volume, max_volume))
# 按步长取整
volume = round(volume / volume_step) * volume_step
return volume
# ==================== 风险检查 ====================
def check_risk(self, symbol: str, volume: float, risk_points: float) -> Dict:
"""
检查交易风险
Args:
symbol: 品种
volume: 手数
risk_points: 风险点数
Returns:
检查结果
"""
# 刷新账户信息
self._refresh_account_info()
config = self.get_symbol_config(symbol)
point_value = config.get('point_value', 1.0)
# 计算风险金额
risk_amount = volume * risk_points * point_value
risk_percent = (risk_amount / self._account_balance * 100) if self._account_balance > 0 else 0
# 检查每日风险限制
remaining_risk = self._daily_risk_limit - self._daily_risk_used
allowed = True
warnings = []
# 账户信息是否已初始化
account_initialized = self._account_balance > 0 or self._free_margin > 0
if risk_percent > 5:
allowed = False
warnings.append(f"单笔风险 {risk_percent:.2f}% 超过5%")
if risk_percent + self._daily_risk_used > self._daily_risk_limit:
allowed = False
warnings.append(f"将超过每日风险限制 {self._daily_risk_limit}%")
# 只有账户信息已初始化时才检查保证金
if account_initialized and self._free_margin < risk_amount:
allowed = False
warnings.append(f"保证金不足 (可用: {self._free_margin:.2f}, 需要: {risk_amount:.2f})")
if not account_initialized:
warnings.append("账户信息未初始化,跳过保证金检查")
return {
"allowed": allowed,
"risk_amount": risk_amount,
"risk_percent": round(risk_percent, 2),
"daily_risk_used": self._daily_risk_used,
"daily_risk_limit": self._daily_risk_limit,
"remaining_risk": remaining_risk,
"warnings": warnings,
"account_initialized": account_initialized,
}
# ==================== 持仓检查 ====================
def check_position_limit(self, symbol: str, strategy: TradingStrategy,
current_positions: int, same_direction: int,
opposite_direction: int, action: str) -> Dict:
"""
检查持仓限制
Args:
symbol: 品种
strategy: 策略配置
current_positions: 当前持仓数
same_direction: 同向持仓数
opposite_direction: 反向持仓数
action: 交易方向 buy/sell
Returns:
检查结果
"""
allowed = True
warnings = []
# 检查最大持仓数
if current_positions >= strategy.max_positions:
allowed = False
warnings.append(f"已达到最大持仓数 {strategy.max_positions}")
# 检查同向持仓
new_same_direction = same_direction + 1
if new_same_direction > strategy.max_same_direction:
allowed = False
warnings.append(f"同向持仓将超过限制 {strategy.max_same_direction}")
# 检查持仓冲突策略
if opposite_direction > 0:
if strategy.position_conflict == "block":
allowed = False
warnings.append("有反向持仓,策略禁止新开仓")
elif strategy.position_conflict == "allow_same":
allowed = False
warnings.append("有反向持仓,策略只允许同向加仓")
elif strategy.position_conflict == "allow_opposite":
# 允许反向
pass
return {
"allowed": allowed,
"current_positions": current_positions,
"same_direction": same_direction,
"opposite_direction": opposite_direction,
"max_positions": strategy.max_positions,
"max_same_direction": strategy.max_same_direction,
"warnings": warnings,
}
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取状态"""
return {
"account_balance": self._account_balance,
"account_equity": self._account_equity,
"free_margin": self._free_margin,
"daily_risk_limit": self._daily_risk_limit,
"daily_risk_used": self._daily_risk_used,
"symbol_count": len(self._symbol_config),
}
@@ -0,0 +1,456 @@
#!/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(),
}