Files

497 lines
18 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易策略数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional
import json
import os
import uuid
class ConsistencyRequirement:
"""一致性要求"""
ANY = "any" # 任一信号即可
MAJORITY = "majority" # 多数信号一致
ALL = "all" # 所有信号一致
class ConflictResolution:
"""冲突解决策略"""
HIGHEST_CONFIDENCE = "highest_confidence" # 最高置信度
HIGHEST_WEIGHT = "highest_weight" # 最高权重
SKIP = "skip" # 跳过冲突
class VolumeMode:
"""手数模式"""
FIXED = "fixed" # 固定手数
RISK_PERCENT = "risk_percent" # 风险百分比
class StopLossMode:
"""止损模式"""
SIGNAL = "signal" # 使用信号建议
FIXED_POINTS = "fixed_points" # 固定点数
ATR_PERCENT = "atr_percent" # ATR百分比
class TakeProfitMode:
"""止盈模式"""
SIGNAL = "signal" # 使用信号建议
FIXED_POINTS = "fixed_points" # 固定点数
RISK_REWARD = "risk_reward" # 风险回报比
class PositionConflict:
"""持仓冲突处理"""
ALLOW_OPPOSITE = "allow_opposite" # 允许反向
ALLOW_SAME = "allow_same" # 允许同向
ALLOW_BOTH = "allow_both" # 都允许
BLOCK = "block" # 有持仓则阻止
@dataclass
class TradingStrategy:
"""交易策略 - 绑定品种,配置信号权重和决策规则"""
# ==================== 基本信息 ====================
symbol: str # 绑定的品种
strategy_name: str = "" # 策略名称
# ==================== 启用状态 ====================
enabled: bool = True # 是否启用
# ==================== 信号源配置(新版:支持周期级别控制)====================
# 信号源配置结构:
# {
# "pivot": {
# "enabled": true,
# "periods": {"M1": {"enabled": true, "weight": 15}, "M5": {"enabled": true, "weight": 20}, ...}
# },
# "key_level": {"enabled": true, "weight": 40}, # key_level 不区分周期
# "ai_entry": {
# "enabled": true,
# "periods": {"M5": {"enabled": true, "weight": 20}, ...}
# }
# }
signal_config: Dict = field(default_factory=lambda: {
"pivot": {
"enabled": True,
"periods": {
"M1": {"enabled": True, "weight": 15},
"M5": {"enabled": True, "weight": 20},
"M15": {"enabled": False, "weight": 25},
"H1": {"enabled": False, "weight": 20},
"H4": {"enabled": False, "weight": 20}
}
},
"key_level": {
"enabled": True,
"weight": 40
},
"ai_entry": {
"enabled": True,
"periods": {
"M1": {"enabled": False, "weight": 15},
"M5": {"enabled": True, "weight": 20},
"M15": {"enabled": True, "weight": 30},
"H1": {"enabled": True, "weight": 25},
"H4": {"enabled": False, "weight": 20}
}
}
})
# ==================== 信号权重配置(兼容旧版,已废弃)===================
signal_weights: Dict[str, int] = field(default_factory=lambda: {
"pivot": 30,
"key_level": 40,
"ai_entry": 30,
})
period_weights: Dict[str, int] = field(default_factory=lambda: {
"H4": 20,
"H1": 20,
"M15": 25,
"M5": 20,
"M1": 15,
})
# ==================== 信号过滤规则 ====================
min_confidence: int = 50
consistency_requirement: str = ConsistencyRequirement.MAJORITY
conflict_resolution: str = ConflictResolution.HIGHEST_WEIGHT
# ==================== 仓位管理 ====================
fixed_volume: float = 0.01
volume_mode: str = VolumeMode.FIXED
risk_percent: float = 1.0
max_risk_points: float = 50.0
max_positions: int = 3
max_same_direction: int = 2
# ==================== 止损止盈规则 ====================
sl_mode: str = StopLossMode.SIGNAL
sl_fixed_points: float = 20.0
sl_atr_multiplier: float = 1.5
tp_mode: str = TakeProfitMode.SIGNAL
tp_fixed_points: float = 40.0
tp_risk_reward: float = 2.0
# ==================== 过滤条件 ====================
min_risk_reward: float = 1.0
max_risk_reward: float = 5.0
min_sl_points: float = 5.0
max_sl_points: float = 100.0
# ==================== 时间过滤 ====================
trading_hours: Dict = field(default_factory=lambda: {
"start": "00:00",
"end": "23:59",
"exclude_hours": []
})
# ==================== 持仓冲突处理 ====================
position_conflict: str = PositionConflict.ALLOW_OPPOSITE
# ==================== 自动生成字段 ====================
strategy_id: str = ""
created_at: datetime = None
updated_at: datetime = None
def __post_init__(self):
if not self.strategy_id:
self.strategy_id = str(uuid.uuid4())[:8]
if not self.created_at:
self.created_at = datetime.now()
if not self.updated_at:
self.updated_at = self.created_at
if not self.strategy_name:
self.strategy_name = f"Strategy_{self.symbol}"
def update(self, data: Dict) -> None:
"""更新配置"""
if "enabled" in data:
self.enabled = bool(data["enabled"])
if "signal_config" in data:
self.signal_config = data["signal_config"]
if "signal_weights" in data:
self.signal_weights = data["signal_weights"]
if "period_weights" in data:
self.period_weights = data["period_weights"]
if "min_confidence" in data:
self.min_confidence = int(data["min_confidence"])
if "consistency_requirement" in data:
self.consistency_requirement = data["consistency_requirement"]
if "conflict_resolution" in data:
self.conflict_resolution = data["conflict_resolution"]
if "fixed_volume" in data:
self.fixed_volume = float(data["fixed_volume"])
if "volume_mode" in data:
self.volume_mode = data["volume_mode"]
if "risk_percent" in data:
self.risk_percent = float(data["risk_percent"])
if "max_positions" in data:
self.max_positions = int(data["max_positions"])
if "max_same_direction" in data:
self.max_same_direction = int(data["max_same_direction"])
if "sl_mode" in data:
self.sl_mode = data["sl_mode"]
if "tp_mode" in data:
self.tp_mode = data["tp_mode"]
if "min_risk_reward" in data:
self.min_risk_reward = float(data["min_risk_reward"])
if "max_risk_reward" in data:
self.max_risk_reward = float(data["max_risk_reward"])
if "position_conflict" in data:
self.position_conflict = data["position_conflict"]
if "trading_hours" in data:
self.trading_hours = data["trading_hours"]
self.updated_at = datetime.now()
def get_signal_weight(self, source: str, period: str = None) -> int:
"""
获取信号源权重(支持周期级别)
Args:
source: 信号源 (pivot/key_level/ai_entry)
period: 周期 (M1/M5/M15/H1/H4)key_level 不需要周期
Returns:
权重值
"""
# 优先使用新的 signal_config
if self.signal_config and source in self.signal_config:
config = self.signal_config[source]
if not config.get("enabled", True):
return 0
# key_level 不区分周期
if source == "key_level":
return config.get("weight", 0)
# 其他信号源区分周期
if period and "periods" in config:
period_config = config["periods"].get(period, {})
if not period_config.get("enabled", False):
return 0
return period_config.get("weight", 0)
# 如果没有 period 配置,返回 0
return 0
# 兼容旧版 signal_weights
return self.signal_weights.get(source, 0)
def is_signal_enabled(self, source: str, period: str = None) -> bool:
"""
检查信号源是否启用
Args:
source: 信号源
period: 周期(key_level 不需要)
Returns:
是否启用
"""
if not self.signal_config or source not in self.signal_config:
# 兼容旧版:signal_weights 中有配置就认为启用
return source in self.signal_weights and self.signal_weights[source] > 0
config = self.signal_config[source]
if not config.get("enabled", True):
return False
# key_level 不区分周期
if source == "key_level":
return True
# 其他信号源需要检查周期
if period and "periods" in config:
period_config = config["periods"].get(period, {})
return period_config.get("enabled", False)
return False
def get_period_weight(self, period: str) -> int:
"""获取周期权重(兼容旧版)"""
return self.period_weights.get(period, 0)
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"strategy_id": self.strategy_id,
"strategy_name": self.strategy_name,
"symbol": self.symbol,
"enabled": self.enabled,
"signal_config": self.signal_config,
"signal_weights": self.signal_weights,
"period_weights": self.period_weights,
"min_confidence": self.min_confidence,
"consistency_requirement": self.consistency_requirement,
"conflict_resolution": self.conflict_resolution,
"fixed_volume": self.fixed_volume,
"volume_mode": self.volume_mode,
"risk_percent": self.risk_percent,
"max_risk_points": self.max_risk_points,
"max_positions": self.max_positions,
"max_same_direction": self.max_same_direction,
"sl_mode": self.sl_mode,
"sl_fixed_points": self.sl_fixed_points,
"sl_atr_multiplier": self.sl_atr_multiplier,
"tp_mode": self.tp_mode,
"tp_fixed_points": self.tp_fixed_points,
"tp_risk_reward": self.tp_risk_reward,
"min_risk_reward": self.min_risk_reward,
"max_risk_reward": self.max_risk_reward,
"min_sl_points": self.min_sl_points,
"max_sl_points": self.max_sl_points,
"trading_hours": self.trading_hours,
"position_conflict": self.position_conflict,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
@classmethod
def from_dict(cls, data: Dict) -> 'TradingStrategy':
"""从字典创建"""
created_at = data.get('created_at')
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
updated_at = data.get('updated_at')
if isinstance(updated_at, str):
updated_at = datetime.fromisoformat(updated_at)
# 默认 signal_config
default_signal_config = {
"pivot": {
"enabled": True,
"periods": {
"M1": {"enabled": True, "weight": 15},
"M5": {"enabled": True, "weight": 20},
"M15": {"enabled": False, "weight": 25},
"H1": {"enabled": False, "weight": 20},
"H4": {"enabled": False, "weight": 20}
}
},
"key_level": {
"enabled": True,
"weight": 40
},
"ai_entry": {
"enabled": True,
"periods": {
"M1": {"enabled": False, "weight": 15},
"M5": {"enabled": True, "weight": 20},
"M15": {"enabled": True, "weight": 30},
"H1": {"enabled": True, "weight": 25},
"H4": {"enabled": False, "weight": 20}
}
}
}
return cls(
symbol=data.get('symbol', ''),
strategy_name=data.get('strategy_name', ''),
enabled=data.get('enabled', True),
signal_config=data.get('signal_config', default_signal_config),
signal_weights=data.get('signal_weights', {"pivot": 30, "key_level": 40, "ai_entry": 30}),
period_weights=data.get('period_weights', {"H4": 20, "H1": 20, "M15": 25, "M5": 20, "M1": 15}),
min_confidence=data.get('min_confidence', 50),
consistency_requirement=data.get('consistency_requirement', ConsistencyRequirement.MAJORITY),
conflict_resolution=data.get('conflict_resolution', ConflictResolution.HIGHEST_WEIGHT),
fixed_volume=data.get('fixed_volume', 0.01),
volume_mode=data.get('volume_mode', VolumeMode.FIXED),
risk_percent=data.get('risk_percent', 1.0),
max_risk_points=data.get('max_risk_points', 50.0),
max_positions=data.get('max_positions', 3),
max_same_direction=data.get('max_same_direction', 2),
sl_mode=data.get('sl_mode', StopLossMode.SIGNAL),
sl_fixed_points=data.get('sl_fixed_points', 20.0),
sl_atr_multiplier=data.get('sl_atr_multiplier', 1.5),
tp_mode=data.get('tp_mode', TakeProfitMode.SIGNAL),
tp_fixed_points=data.get('tp_fixed_points', 40.0),
tp_risk_reward=data.get('tp_risk_reward', 2.0),
min_risk_reward=data.get('min_risk_reward', 1.0),
max_risk_reward=data.get('max_risk_reward', 5.0),
min_sl_points=data.get('min_sl_points', 5.0),
max_sl_points=data.get('max_sl_points', 100.0),
trading_hours=data.get('trading_hours', {"start": "00:00", "end": "23:59", "exclude_hours": []}),
position_conflict=data.get('position_conflict', PositionConflict.ALLOW_OPPOSITE),
strategy_id=data.get('strategy_id', ''),
created_at=created_at,
updated_at=updated_at,
)
@dataclass
class TradingDecision:
"""交易决策 - 策略层输出"""
# ==================== 基本信息 ====================
symbol: str # 品种
strategy_id: str # 来源策略ID
# ==================== 决策结果 ====================
action: str = "" # buy/sell/none
decision_type: str = "" # signal_combined / single_signal / manual
# ==================== 信号汇总 ====================
signals: List[Dict] = field(default_factory=list)
signal_summary: Dict = field(default_factory=dict)
# ==================== 执行参数 ====================
entry_price: float = 0.0
sl: float = 0.0
tp: float = 0.0
volume: float = 0.01
risk_points: float = 0.0
reward_points: float = 0.0
risk_reward_ratio: float = 0.0
# ==================== 决策理由 ====================
decision_reason: str = ""
confidence_score: float = 0.0
# ==================== 检查结果 ====================
position_check: Dict = field(default_factory=dict)
risk_check: Dict = field(default_factory=dict)
# ==================== 状态 ====================
decision_id: str = ""
status: str = "pending" # pending/confirmed/rejected/expired
created_at: datetime = None
# ==================== 关联 ====================
order_id: Optional[str] = None
def __post_init__(self):
if not self.decision_id:
self.decision_id = str(uuid.uuid4())[:8]
if not self.created_at:
self.created_at = datetime.now()
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"decision_id": self.decision_id,
"symbol": self.symbol,
"strategy_id": self.strategy_id,
"action": self.action,
"decision_type": self.decision_type,
"signals": self.signals,
"signal_summary": self.signal_summary,
"entry_price": self.entry_price,
"sl": self.sl,
"tp": self.tp,
"volume": self.volume,
"risk_points": self.risk_points,
"reward_points": self.reward_points,
"risk_reward_ratio": self.risk_reward_ratio,
"decision_reason": self.decision_reason,
"confidence_score": self.confidence_score,
"position_check": self.position_check,
"risk_check": self.risk_check,
"status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None,
"order_id": self.order_id,
}
@classmethod
def from_dict(cls, data: Dict) -> 'TradingDecision':
"""从字典创建"""
created_at = data.get('created_at')
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
return cls(
symbol=data.get('symbol', ''),
strategy_id=data.get('strategy_id', ''),
action=data.get('action', ''),
decision_type=data.get('decision_type', ''),
signals=data.get('signals', []),
signal_summary=data.get('signal_summary', {}),
entry_price=data.get('entry_price', 0.0),
sl=data.get('sl', 0.0),
tp=data.get('tp', 0.0),
volume=data.get('volume', 0.01),
risk_points=data.get('risk_points', 0.0),
reward_points=data.get('reward_points', 0.0),
risk_reward_ratio=data.get('risk_reward_ratio', 0.0),
decision_reason=data.get('decision_reason', ''),
confidence_score=data.get('confidence_score', 0.0),
position_check=data.get('position_check', {}),
risk_check=data.get('risk_check', {}),
decision_id=data.get('decision_id', ''),
status=data.get('status', 'pending'),
created_at=created_at,
order_id=data.get('order_id'),
)