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
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据模型模块
"""
from .kline import KlineData
from .pivot import PivotPoint
from .llm_config import LLMConfig
from .llm_analysis import LLMAnalysisResult
from .tech_analysis import TechTrendState, TechTrendChange, TechResonanceResult, TechTradeSuggestion
from .calendar_event import CalendarEvent
from .flash_news import FlashNews
from .pending_order import PendingOrder
from .trading_instruction import TradingInstruction
from .trading_signal import TradingSignal, SignalSource, SignalStatus
from .trading_strategy import (
TradingStrategy, TradingDecision,
ConsistencyRequirement, ConflictResolution, VolumeMode,
StopLossMode, TakeProfitMode, PositionConflict
)
from .statistics import StatisticsData
from .position import PositionData
from .trade_history import TradeDeal
__all__ = [
'KlineData', 'PivotPoint',
'LLMConfig', 'LLMAnalysisResult',
'TechTrendState', 'TechTrendChange', 'TechResonanceResult', 'TechTradeSuggestion',
'CalendarEvent', 'FlashNews',
'PendingOrder', 'TradingInstruction',
'TradingSignal', 'SignalSource', 'SignalStatus',
'TradingStrategy', 'TradingDecision',
'ConsistencyRequirement', 'ConflictResolution', 'VolumeMode',
'StopLossMode', 'TakeProfitMode', 'PositionConflict',
'StatisticsData', 'PositionData', 'TradeDeal'
]
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
财经日历事件数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import re
# MT5中表示无效值的特殊数值
MT5_INVALID_VALUE = -9223372036854775808.0
# MT5时区偏移(相对于北京时间)
# MT5服务器通常是 GMT+2,北京时间是 GMT+8
# 所以 MT5时间 + 6小时 = 北京时间
MT5_TIMEZONE_OFFSET_HOURS = 6
def clean_invalid_value(value: str) -> str:
"""清理MT5返回的无效值"""
if not value:
return ""
try:
num = float(value)
if num == MT5_INVALID_VALUE or num < -1e15:
return ""
if num == int(num):
return str(int(num))
return value
except (ValueError, TypeError):
return value
def clean_text(text: str) -> str:
"""清理文本中的控制字符和无效Unicode"""
if not text:
return ""
cleaned = re.sub(r'[\x00-\x1f\x7f]', '', text)
result = []
for char in cleaned:
code = ord(char)
if (0x20 <= code <= 0x7E or
0x4E00 <= code <= 0x9FFF or
0x3000 <= code <= 0x303F or
0xFF00 <= code <= 0xFFEF or
code > 0x9FFF):
result.append(char)
return ''.join(result)
def is_valid_name(name: str) -> bool:
"""检查名称是否有效(不是乱码)"""
if not name or len(name) < 2:
return False
printable_count = 0
for char in name:
code = ord(char)
if (0x20 <= code <= 0x7E or
0x4E00 <= code <= 0x9FFF or
0x3000 <= code <= 0x303F or
0xFF00 <= code <= 0xFFEF):
printable_count += 1
ratio = printable_count / len(name) if name else 0
return ratio >= 0.7
@dataclass
class CalendarEvent:
"""财经日历事件"""
id: str
name: str
name_en: str = ""
country: str = ""
currency: str = ""
importance: int = 0 # 0-3
publish_time: Optional[datetime] = None
forecast: str = ""
previous: str = ""
actual: str = ""
unit: str = ""
symbols: List[str] = field(default_factory=list)
event_type: str = ""
# 发布后填充
result: str = "" # better/worse/in_line
impact: Dict = field(default_factory=dict)
analyzed: bool = False
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"id": self.id,
"name": self.name,
"name_en": self.name_en,
"country": self.country,
"currency": self.currency,
"importance": self.importance,
"publish_time": self.publish_time.isoformat() if self.publish_time else None,
"forecast": self.forecast,
"previous": self.previous,
"actual": self.actual,
"unit": self.unit,
"symbols": self.symbols,
"event_type": self.event_type,
"result": self.result,
"impact": self.impact,
"analyzed": self.analyzed
}
@classmethod
def from_mt5_data(cls, event_data: Dict) -> Optional['CalendarEvent']:
"""
从MT5数据创建事件对象
Args:
event_data: MT5返回的事件数据
Returns:
CalendarEvent 或 None(如果数据无效)
"""
event_id = str(event_data.get('id', ''))
if not event_id:
return None
# 解析发布时间
publish_time = event_data.get('publish_time')
if isinstance(publish_time, str):
try:
publish_time = datetime.fromisoformat(publish_time.replace('Z', '+00:00'))
except:
try:
publish_time = datetime.strptime(publish_time, '%Y.%m.%d %H:%M:%S')
except:
return None
elif not isinstance(publish_time, datetime):
return None
# 将 MT5 时间转换为北京时间(GMT+8)
# MT5服务器时间通常是 GMT+2,北京时间是 GMT+8,差6小时
publish_time = publish_time + timedelta(hours=MT5_TIMEZONE_OFFSET_HOURS)
# 清理并检查名称有效性
cleaned_name = clean_text(event_data.get('name', ''))
if not is_valid_name(cleaned_name):
return None
return cls(
id=event_id,
name=cleaned_name,
name_en=clean_text(event_data.get('name_en', '')),
country=clean_text(event_data.get('country', '')),
currency=event_data.get('currency', ''),
importance=event_data.get('importance', 0),
publish_time=publish_time,
forecast=clean_invalid_value(event_data.get('forecast', '')),
previous=clean_invalid_value(event_data.get('previous', '')),
actual=clean_invalid_value(event_data.get('actual', '')),
unit=event_data.get('unit', ''),
symbols=event_data.get('symbols', []),
event_type=event_data.get('event_type', '')
)
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快讯数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional
@dataclass
class FlashNews:
"""快讯数据"""
id: str
content: str
source: str = ""
time: Optional[datetime] = None
importance: int = 0
keywords: List[str] = field(default_factory=list)
related_symbols: List[str] = field(default_factory=list)
# 分析后填充
speaker: str = ""
speaker_title: str = ""
impact: Dict = field(default_factory=dict)
analyzed: bool = False
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"id": self.id,
"content": self.content,
"source": self.source,
"time": self.time.isoformat() if self.time else None,
"importance": self.importance,
"keywords": self.keywords,
"related_symbols": self.related_symbols,
"speaker": self.speaker,
"speaker_title": self.speaker_title,
"impact": self.impact,
"analyzed": self.analyzed
}
@classmethod
def from_jin10_data(cls, data: Dict) -> 'FlashNews':
"""
从金十数据创建快讯对象
Args:
data: 金十API返回的快讯数据
Returns:
FlashNews
"""
# 解析时间
time = None
time_str = data.get('time') or data.get('publish_time')
if time_str:
if isinstance(time_str, datetime):
time = time_str
elif isinstance(time_str, (int, float)):
time = datetime.fromtimestamp(time_str)
else:
try:
time = datetime.fromisoformat(str(time_str).replace('Z', '+00:00'))
except:
pass
return cls(
id=str(data.get('id', '')),
content=data.get('content', ''),
source=data.get('source', 'jin10'),
time=time,
importance=data.get('importance', 0),
keywords=data.get('keywords', []),
related_symbols=data.get('related_symbols', [])
)
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
K线数据结构
"""
from datetime import datetime
from typing import Dict
class KlineData:
"""K线数据结构"""
def __init__(self, symbol: str, period: str, timestamp, open_price: float,
high: float, low: float, close: float, volume: float = 0):
self.symbol = symbol
self.period = period # H4, H1, M15, M5, M1
self.timestamp = timestamp
self.open = open_price
self.high = high
self.low = low
self.close = close
self.volume = volume
def to_dict(self) -> Dict:
"""转换为字典"""
ts = self.timestamp
if isinstance(ts, datetime):
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = str(ts)
return {
"symbol": self.symbol,
"period": self.period,
"timestamp": ts_str,
"open": self.open,
"high": self.high,
"low": self.low,
"close": self.close,
"volume": self.volume
}
@classmethod
def from_dict(cls, data: Dict) -> 'KlineData':
"""从字典创建"""
return cls(
symbol=data.get('symbol', ''),
period=data.get('period', ''),
timestamp=data.get('timestamp') or data.get('time'),
open_price=float(data.get('open', 0)),
high=float(data.get('high', 0)),
low=float(data.get('low', 0)),
close=float(data.get('close', 0)),
volume=float(data.get('volume', 0))
)
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LLM 分析结果数据结构
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional
@dataclass
class LLMAnalysisResult:
"""LLM 分析结果"""
symbol: str
trend_analysis: Dict = field(default_factory=dict)
overall_trend: Optional[Dict] = None
key_levels: Optional[Dict] = None
trade_suggestions: List[Dict] = field(default_factory=list)
analyzed_at: Optional[str] = None
data_stale: bool = False
market_status: str = "active" # active, stale, closed
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"symbol": self.symbol,
"trend_analysis": self.trend_analysis,
"overall_trend": self.overall_trend,
"key_levels": self.key_levels,
"trade_suggestions": self.trade_suggestions,
"analyzed_at": self.analyzed_at,
"data_stale": self.data_stale,
"market_status": self.market_status
}
@classmethod
def from_api_response(cls, symbol: str, data: Dict) -> 'LLMAnalysisResult':
"""从 LLM API 返回的字典创建"""
return cls(
symbol=symbol,
trend_analysis=data.get("trend_analysis", {}),
overall_trend=data.get("overall_trend"),
key_levels=data.get("key_levels"),
trade_suggestions=data.get("trade_suggestions", []),
analyzed_at=datetime.now().isoformat(),
data_stale=False,
market_status="active"
)
def get_trade_suggestion(self, period: str) -> Optional[Dict]:
"""获取指定周期的交易建议"""
for ts in self.trade_suggestions:
if ts.get("period") == period:
return ts
return None
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LLM 配置数据结构
"""
from dataclasses import dataclass
from typing import Dict
@dataclass
class LLMConfig:
"""LLM 配置"""
api_key: str = ""
api_base: str = "https://api.openai.com/v1"
model: str = "gpt-4o-mini"
@property
def enabled(self) -> bool:
"""是否启用(有 API Key 才启用)"""
return bool(self.api_key)
def to_dict(self) -> Dict:
"""转换为字典(API Key 脱敏)"""
masked_key = ""
if self.api_key:
if len(self.api_key) > 8:
masked_key = self.api_key[:4] + "****" + self.api_key[-4:]
else:
masked_key = "****"
return {
"api_key": masked_key,
"api_key_set": bool(self.api_key),
"api_base": self.api_base,
"model": self.model,
"enabled": self.enabled
}
@classmethod
def from_dict(cls, data: Dict) -> 'LLMConfig':
"""从字典创建"""
return cls(
api_key=data.get("api_key", ""),
api_base=data.get("api_base", "https://api.openai.com/v1"),
model=data.get("model", "gpt-4o-mini")
)
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
待确认订单数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Dict, Optional
import uuid
@dataclass
class PendingOrder:
"""待确认订单"""
# 必填字段
symbol: str
action: str # b=买入, s=卖出
price: float # 入场价
mount: float # 手数
sl: float # 止损
tp: float # 止盈
# 可选字段
reason: str = ""
description: str = ""
source: str = "" # auto_pivot_m1/key_level/ai_entry_nearby/manual
# 策略相关字段
pivot_price: Optional[float] = None # 转折点价格
key_level: Optional[float] = None # 关键点位
ai_period: Optional[str] = None # AI分析周期
ai_entry_price: Optional[float] = None # AI入场价
ai_direction: Optional[str] = None # AI方向
# AI方向一致性分析
ai_directions: Optional[Dict] = None
direction_consistent: bool = False
consistent_periods: list = field(default_factory=list)
inconsistent_periods: list = field(default_factory=list)
recommendation: str = ""
recommendation_color: str = ""
# 自动生成字段
order_id: str = ""
status: str = "pending" # pending/confirmed/rejected/expired
created_at: Optional[datetime] = None
expires_at: Optional[datetime] = None
confirmed_at: Optional[datetime] = None
# 超时时间(秒)
TIMEOUT_SECONDS: int = field(default=180, repr=False)
def __post_init__(self):
if not self.order_id:
self.order_id = str(uuid.uuid4())[:8]
if not self.created_at:
self.created_at = datetime.now()
if not self.expires_at:
self.expires_at = self.created_at + timedelta(seconds=self.TIMEOUT_SECONDS)
def is_expired(self) -> bool:
"""检查是否已过期"""
return datetime.now() > self.expires_at
def is_pending(self) -> bool:
"""检查是否待处理"""
return self.status == "pending" and not self.is_expired()
def confirm(self) -> None:
"""确认订单"""
self.status = "confirmed"
self.confirmed_at = datetime.now()
def reject(self) -> None:
"""拒绝订单"""
self.status = "rejected"
def mark_expired(self) -> None:
"""标记为过期"""
self.status = "expired"
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"order_id": self.order_id,
"symbol": self.symbol,
"action": self.action,
"price": self.price,
"mount": self.mount,
"sl": self.sl,
"tp": self.tp,
"reason": self.reason,
"description": self.description,
"source": self.source,
"pivot_price": self.pivot_price,
"key_level": self.key_level,
"ai_period": self.ai_period,
"ai_entry_price": self.ai_entry_price,
"ai_direction": self.ai_direction,
"ai_directions": self.ai_directions,
"direction_consistent": self.direction_consistent,
"consistent_periods": self.consistent_periods,
"inconsistent_periods": self.inconsistent_periods,
"recommendation": self.recommendation,
"recommendation_color": self.recommendation_color,
"status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None,
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
"confirmed_at": self.confirmed_at.isoformat() if self.confirmed_at else None,
}
@classmethod
def from_dict(cls, data: Dict) -> 'PendingOrder':
"""从字典创建"""
# 处理时间字段
created_at = data.get('created_at')
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
elif created_at is None:
created_at = datetime.now()
expires_at = data.get('expires_at')
if isinstance(expires_at, str):
expires_at = datetime.fromisoformat(expires_at)
confirmed_at = data.get('confirmed_at')
if isinstance(confirmed_at, str):
confirmed_at = datetime.fromisoformat(confirmed_at)
return cls(
symbol=data.get('symbol', ''),
action=data.get('action', ''),
price=data.get('price', 0.0),
mount=data.get('mount', 0.0),
sl=data.get('sl', 0.0),
tp=data.get('tp', 0.0),
reason=data.get('reason', ''),
description=data.get('description', ''),
source=data.get('source', ''),
pivot_price=data.get('pivot_price'),
key_level=data.get('key_level'),
ai_period=data.get('ai_period'),
ai_entry_price=data.get('ai_entry_price'),
ai_direction=data.get('ai_direction'),
ai_directions=data.get('ai_directions'),
direction_consistent=data.get('direction_consistent', False),
consistent_periods=data.get('consistent_periods', []),
inconsistent_periods=data.get('inconsistent_periods', []),
recommendation=data.get('recommendation', ''),
recommendation_color=data.get('recommendation_color', ''),
order_id=data.get('order_id', ''),
status=data.get('status', 'pending'),
created_at=created_at,
expires_at=expires_at,
confirmed_at=confirmed_at,
)
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
转折点数据结构
"""
from datetime import datetime
from typing import Dict
class PivotPoint:
"""转折点数据结构"""
def __init__(self, symbol: str, period: str, timestamp, price: float,
direction: str, strength: int = 3):
self.symbol = symbol
self.period = period
self.timestamp = timestamp
self.price = price
self.direction = direction # "high" 或 "low"
self.strength = strength # 转折强度(左右各N根K线)
def to_dict(self) -> Dict:
"""转换为字典"""
ts = self.timestamp
if isinstance(ts, datetime):
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = str(ts)
return {
"symbol": self.symbol,
"period": self.period,
"timestamp": ts_str,
"price": self.price,
"direction": self.direction,
"strength": self.strength
}
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
持仓数据模型
"""
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, Optional
@dataclass
class PositionData:
"""
持仓数据
EA通过 /ea/positions 上报
"""
ticket: int
symbol: str
volume: float
price_open: float
position_type: str # "BUY" / "SELL"
profit: float
# 止损止盈
sl: float = 0.0
tp: float = 0.0
distance_sl: float = 0.0 # 距离止损的点数
distance_tp: float = 0.0 # 距离止盈的点数
# 元数据
updated_at: datetime = None
def __post_init__(self):
if self.updated_at is None:
self.updated_at = datetime.now()
@property
def is_buy(self) -> bool:
"""是否为买单"""
return self.position_type.upper() == "BUY"
@property
def is_sell(self) -> bool:
"""是否为卖单"""
return self.position_type.upper() == "SELL"
@property
def direction(self) -> str:
"""方向:buy / sell"""
return "buy" if self.is_buy else "sell"
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"ticket": self.ticket,
"symbol": self.symbol,
"volume": self.volume,
"price_open": self.price_open,
"type": self.position_type,
"profit": self.profit,
"sl": self.sl,
"tp": self.tp,
"distance_sl": self.distance_sl,
"distance_tp": self.distance_tp,
"direction": self.direction,
"updated_at": self.updated_at.isoformat() if self.updated_at else None
}
@classmethod
def from_ea_data(cls, data: Dict, symbol: str = None) -> 'PositionData':
"""从EA上报数据创建"""
return cls(
ticket=int(data.get('ticket', 0)),
symbol=data.get('symbol', symbol or ''),
volume=float(data.get('volume', 0)),
price_open=float(data.get('priceOpen', 0)),
position_type=data.get('type', 'BUY').upper(),
profit=float(data.get('profit', 0)),
sl=float(data.get('sl', 0)),
tp=float(data.get('tp', 0)),
distance_sl=float(data.get('distanceSL', 0)),
distance_tp=float(data.get('distanceTP', 0))
)
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
统计数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, Dict
@dataclass
class StatisticsData:
"""
EA上报的统计数据
用途
1. 获取品种价差TechService
2. 获取账户信息前端展示
3. 检查数据时效性
"""
symbol: str
timestamp: datetime
# 价格信息
bid_price: float
ask_price: float
spread: float
spread_points: float
# 账户信息
balance: float
equity: float
margin_level: float
# 其他
tick_count: int = 0
@property
def mid_price(self) -> float:
"""中间价"""
return (self.bid_price + self.ask_price) / 2
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"symbol": self.symbol,
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
"bid_price": self.bid_price,
"ask_price": self.ask_price,
"spread": self.spread,
"spread_points": self.spread_points,
"balance": self.balance,
"equity": self.equity,
"margin_level": self.margin_level,
"tick_count": self.tick_count,
"mid_price": self.mid_price
}
@classmethod
def from_ea_data(cls, data: Dict) -> 'StatisticsData':
"""从EA上报数据创建"""
# 解析时间戳
timestamp = data.get('timestamp')
if isinstance(timestamp, str):
try:
timestamp = datetime.fromisoformat(timestamp)
except:
timestamp = datetime.now()
elif not isinstance(timestamp, datetime):
timestamp = datetime.now()
return cls(
symbol=data.get('symbol', ''),
timestamp=timestamp,
bid_price=float(data.get('bidPrice', 0)),
ask_price=float(data.get('askPrice', 0)),
spread=float(data.get('spread', 0)),
spread_points=float(data.get('spreadPoints', 0)),
balance=float(data.get('balance', 0)),
equity=float(data.get('equity', 0)),
margin_level=float(data.get('marginLevel', 0)),
tick_count=int(data.get('tickCount', 0))
)
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
技术分析相关数据结构
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional
@dataclass
class TechTrendState:
"""单周期趋势状态"""
symbol: str
period: str
trend: str = "unknown" # "up" / "down" / "sideways" / "unknown"
strength: int = 0 # 0-100
adx: float = 0.0
ma_fast: float = 0.0
ma_slow: float = 0.0
price: float = 0.0
reason: str = ""
timestamp: Optional[str] = None
previous_trend: Optional[str] = None
change_signal: bool = False
def to_dict(self) -> Dict:
return {
"trend": self.trend,
"strength": self.strength,
"adx": self.adx,
"ma_fast": self.ma_fast,
"ma_slow": self.ma_slow,
"price": self.price,
"reason": self.reason,
"timestamp": self.timestamp,
"previous_trend": self.previous_trend,
"change_signal": self.change_signal
}
@dataclass
class TechTrendChange:
"""趋势转换记录"""
period: str
from_trend: str
to_trend: str
price: float
timestamp: str
def to_dict(self) -> Dict:
return {
"period": self.period,
"from_trend": self.from_trend,
"to_trend": self.to_trend,
"price": self.price,
"timestamp": self.timestamp
}
@dataclass
class TechResonanceResult:
"""多周期共振结果"""
symbol: str
resonance: str = "none" # "up" / "down" / "none"
strength: int = 0
aligned_count: int = 0
up_count: int = 0
down_count: int = 0
sideways_count: int = 0
signal: str = "等待数据"
periods: Dict = field(default_factory=dict)
def to_dict(self) -> Dict:
return {
"resonance": self.resonance,
"strength": self.strength,
"aligned_count": self.aligned_count,
"up_count": self.up_count,
"down_count": self.down_count,
"sideways_count": self.sideways_count,
"signal": self.signal,
"periods": {p: s.to_dict() if hasattr(s, 'to_dict') else s for p, s in self.periods.items()}
}
@dataclass
class TechTradeSuggestion:
"""技术分析交易建议"""
symbol: str
action: str # "b" / "s"
price: float
sl: float
tp: float
reason: str
trend_strength: int
resonance_periods: int
generated_at: str
def to_dict(self) -> Dict:
return {
"symbol": self.symbol,
"action": self.action,
"price": self.price,
"mount": 0.01, # 默认手数
"sl": self.sl,
"tp": self.tp,
"reason": self.reason,
"trend_strength": self.trend_strength,
"resonance_periods": self.resonance_periods,
"generated_at": self.generated_at
}
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易历史数据模型
"""
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, Optional
import re
@dataclass
class TradeDeal:
"""
成交记录
EA通过 /trade_history 上报
"""
ticket: int
order: int
symbol: str
deal_type: int # 0=买入, 1=卖出
entry_type: int # 0=开仓, 1=平仓, 2=反向
volume: float
price: float
profit: float
swap: float
commission: float
time: datetime
comment: str
@property
def is_buy(self) -> bool:
"""是否为买入"""
return self.deal_type == 0
@property
def is_sell(self) -> bool:
"""是否为卖出"""
return self.deal_type == 1
@property
def is_entry(self) -> bool:
"""是否为开仓"""
return self.entry_type == 0
@property
def is_exit(self) -> bool:
"""是否为平仓"""
return self.entry_type == 1
@property
def deal_type_text(self) -> str:
"""成交类型文本"""
return "买入" if self.is_buy else "卖出"
@property
def entry_type_text(self) -> str:
"""入场类型文本"""
if self.entry_type == 0:
return "开仓"
elif self.entry_type == 1:
return "平仓"
elif self.entry_type == 2:
return "反向"
return "未知"
@property
def order_source(self) -> str:
"""订单来源"""
if not self.comment or not self.comment.strip():
return "手动"
comment = self.comment.strip()
if comment.startswith('[sl'):
return "止损触发"
if comment.startswith('[tp'):
return "止盈触发"
if comment.startswith('[so'):
return "强制平仓"
return "自动"
@property
def is_auto(self) -> bool:
"""是否为自动订单"""
if not self.comment or not self.comment.strip():
return False
comment = self.comment.strip()
# 排除MT5系统标记
if comment.startswith('[sl') or comment.startswith('[tp') or comment.startswith('[so'):
return False
return True
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"ticket": self.ticket,
"order": self.order,
"symbol": self.symbol,
"type": self.deal_type,
"type_text": self.deal_type_text,
"entry": self.entry_type,
"entry_text": self.entry_type_text,
"volume": self.volume,
"price": self.price,
"profit": self.profit,
"swap": self.swap,
"commission": self.commission,
"time": self.time.strftime("%Y-%m-%d %H:%M:%S") if self.time else None,
"comment": self.comment,
"is_auto": self.is_auto,
"order_source": self.order_source
}
@classmethod
def from_ea_data(cls, data: Dict) -> 'TradeDeal':
"""从EA上报数据创建"""
# 解析时间
deal_time = data.get('time')
if isinstance(deal_time, str):
for fmt in ["%Y.%m.%d %H:%M:%S", "%Y-%m-%d %H:%M:%S"]:
try:
deal_time = datetime.strptime(deal_time, fmt)
break
except:
pass
if not isinstance(deal_time, datetime):
deal_time = datetime.now()
return cls(
ticket=int(data.get('ticket', 0)),
order=int(data.get('order', 0)),
symbol=data.get('symbol', ''),
deal_type=int(data.get('type', 0)),
entry_type=int(data.get('entry', 0)),
volume=float(data.get('volume', 0)),
price=float(data.get('price', 0)),
profit=float(data.get('profit', 0)),
swap=float(data.get('swap', 0)),
commission=float(data.get('commission', 0)),
time=deal_time,
comment=data.get('comment', '')
)
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易指令数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, Optional
import uuid
@dataclass
class TradingInstruction:
"""交易指令"""
# 必填字段
symbol: str
action: str # b=买入, s=卖出
price: float # 指令执行价格
mount: float # 手数
# 可选字段
sl: float = 0.0 # 止损
tp: float = 0.005 # 止盈(默认值)
reason: str = ""
description: str = ""
source: str = "" # manual/pending_order_confirm/key_level/ai_entry
# 来源追踪
order_id: Optional[str] = None # 来源订单ID(如果是确认订单转入)
# 自动生成字段
instruction_id: str = ""
status: str = "pending" # pending/sent/executed/cancelled
created_at: Optional[datetime] = None
sent_at: Optional[datetime] = None # 发送给EA的时间
executed_at: Optional[datetime] = None
def __post_init__(self):
if not self.instruction_id:
self.instruction_id = str(uuid.uuid4())[:8]
if not self.created_at:
self.created_at = datetime.now()
# 确保tp有默认值
if self.tp is None or self.tp <= 0:
self.tp = 0.005
def to_dict(self) -> Dict:
"""转换为字典(用于返回给EA"""
return {
"symbol": self.symbol.lower(),
"action": self.action.lower(),
"mount": self.mount,
"price": self.price,
"sl": self.sl,
"tp": self.tp,
}
def to_full_dict(self) -> Dict:
"""转换为完整字典(用于内部存储和查询)"""
return {
"instruction_id": self.instruction_id,
"symbol": self.symbol,
"action": self.action,
"price": self.price,
"mount": self.mount,
"sl": self.sl,
"tp": self.tp,
"reason": self.reason,
"description": self.description,
"source": self.source,
"order_id": self.order_id,
"status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None,
"sent_at": self.sent_at.isoformat() if self.sent_at else None,
"executed_at": self.executed_at.isoformat() if self.executed_at else None,
}
@classmethod
def from_dict(cls, data: Dict) -> 'TradingInstruction':
"""从字典创建"""
created_at = data.get('created_at')
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
elif created_at is None:
created_at = datetime.now()
sent_at = data.get('sent_at')
if isinstance(sent_at, str):
sent_at = datetime.fromisoformat(sent_at)
executed_at = data.get('executed_at')
if isinstance(executed_at, str):
executed_at = datetime.fromisoformat(executed_at)
return cls(
symbol=data.get('symbol', ''),
action=data.get('action', ''),
price=data.get('price', 0.0),
mount=data.get('mount', 0.0),
sl=data.get('sl', 0.0),
tp=data.get('tp', 0.005),
reason=data.get('reason', ''),
description=data.get('description', ''),
source=data.get('source', ''),
order_id=data.get('order_id'),
instruction_id=data.get('instruction_id', ''),
status=data.get('status', 'pending'),
created_at=created_at,
sent_at=sent_at,
executed_at=executed_at,
)
@classmethod
def from_pending_order(cls, order: 'PendingOrder') -> 'TradingInstruction':
"""从待确认订单创建"""
return cls(
symbol=order.symbol,
action=order.action,
price=order.price,
mount=order.mount,
sl=order.sl,
tp=order.tp,
reason=order.reason,
description=order.description,
source=f"pending_order_{order.source}",
order_id=order.order_id,
)
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易信号数据模型
纯分析结果不含仓位资金
"""
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Dict, Optional
import uuid
class SignalSource:
"""信号来源"""
PIVOT = "pivot" # 转折点信号
KEY_LEVEL = "key_level" # 关键点位信号
AI_ENTRY = "ai_entry" # AI入场信号
class SignalStatus:
"""信号状态"""
ACTIVE = "active" # 活跃
EXPIRED = "expired" # 已过期
USED = "used" # 已被使用(生成决策)
@dataclass
class TradingSignal:
"""交易信号 - 纯分析结果,不含仓位资金"""
# ==================== 基本信息 ====================
symbol: str # 品种
action: str # 方向: buy/sell
confidence: int = 50 # 置信度 0-100
# ==================== 来源 ====================
source: str = "" # pivot/key_level/ai_entry
source_period: str = "" # 来源周期 (H4/H1/M15/M5/M1)
# ==================== 触发信息 ====================
trigger_price: float = 0.0 # 触发价格
trigger_time: datetime = None # 触发时间
trigger_reason: str = "" # 触发原因
# ==================== 建议参数 ====================
suggested_entry: float = 0.0 # 建议入场价
suggested_sl: float = 0.0 # 建议止损
suggested_tp: float = 0.0 # 建议止盈
risk_reward_ratio: float = 0.0 # 风险回报比
# ==================== 来源特有参数 ====================
# Pivot信号
pivot_price: Optional[float] = None
pivot_type: Optional[str] = None # high/low
# KeyLevel信号
key_level: Optional[float] = None
distance_pct: Optional[float] = None
# AI Entry信号
ai_analysis_period: Optional[str] = None
# ==================== 自动生成字段 ====================
signal_id: str = ""
status: str = SignalStatus.ACTIVE
created_at: datetime = None
expires_at: datetime = None
# 默认信号有效期(秒)
DEFAULT_TTL: int = field(default=300, repr=False) # 5分钟
def __post_init__(self):
if not self.signal_id:
self.signal_id = str(uuid.uuid4())[:8]
if not self.created_at:
self.created_at = datetime.now()
if not self.trigger_time:
self.trigger_time = self.created_at
if not self.expires_at:
self.expires_at = self.created_at + timedelta(seconds=self.DEFAULT_TTL)
def is_expired(self) -> bool:
"""检查是否已过期"""
return datetime.now() > self.expires_at
def is_active(self) -> bool:
"""检查是否活跃"""
return self.status == SignalStatus.ACTIVE and not self.is_expired()
def mark_used(self) -> None:
"""标记为已使用"""
self.status = SignalStatus.USED
def mark_expired(self) -> None:
"""标记为已过期"""
self.status = SignalStatus.EXPIRED
def get_risk_points(self) -> float:
"""获取风险点数"""
if self.action == "buy":
return abs(self.suggested_entry - self.suggested_sl)
else:
return abs(self.suggested_sl - self.suggested_entry)
def get_reward_points(self) -> float:
"""获取回报点数"""
if self.action == "buy":
return abs(self.suggested_tp - self.suggested_entry)
else:
return abs(self.suggested_entry - self.suggested_tp)
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"signal_id": self.signal_id,
"symbol": self.symbol,
"action": self.action,
"confidence": self.confidence,
"source": self.source,
"source_period": self.source_period,
"trigger_price": self.trigger_price,
"trigger_time": self.trigger_time.isoformat() if self.trigger_time else None,
"trigger_reason": self.trigger_reason,
"suggested_entry": self.suggested_entry,
"suggested_sl": self.suggested_sl,
"suggested_tp": self.suggested_tp,
"risk_reward_ratio": self.risk_reward_ratio,
"pivot_price": self.pivot_price,
"pivot_type": self.pivot_type,
"key_level": self.key_level,
"distance_pct": self.distance_pct,
"ai_analysis_period": self.ai_analysis_period,
"status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None,
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
"risk_points": self.get_risk_points(),
"reward_points": self.get_reward_points(),
}
@classmethod
def from_dict(cls, data: Dict) -> 'TradingSignal':
"""从字典创建"""
trigger_time = data.get('trigger_time')
if isinstance(trigger_time, str):
trigger_time = datetime.fromisoformat(trigger_time)
created_at = data.get('created_at')
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
elif created_at is None:
created_at = datetime.now()
expires_at = data.get('expires_at')
if isinstance(expires_at, str):
expires_at = datetime.fromisoformat(expires_at)
return cls(
symbol=data.get('symbol', ''),
action=data.get('action', ''),
confidence=data.get('confidence', 50),
source=data.get('source', ''),
source_period=data.get('source_period', ''),
trigger_price=data.get('trigger_price', 0.0),
trigger_time=trigger_time,
trigger_reason=data.get('trigger_reason', ''),
suggested_entry=data.get('suggested_entry', 0.0),
suggested_sl=data.get('suggested_sl', 0.0),
suggested_tp=data.get('suggested_tp', 0.0),
risk_reward_ratio=data.get('risk_reward_ratio', 0.0),
pivot_price=data.get('pivot_price'),
pivot_type=data.get('pivot_type'),
key_level=data.get('key_level'),
distance_pct=data.get('distance_pct'),
ai_analysis_period=data.get('ai_analysis_period'),
signal_id=data.get('signal_id', ''),
status=data.get('status', SignalStatus.ACTIVE),
created_at=created_at,
expires_at=expires_at,
)
+497
View File
@@ -0,0 +1,497 @@
#!/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'),
)