497 lines
18 KiB
Python
497 lines
18 KiB
Python
#!/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'),
|
||
) |