Refactor: restructure market module with services, stores, and utils
This commit is contained in:
@@ -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'
|
||||
]
|
||||
@@ -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', '')
|
||||
)
|
||||
@@ -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', [])
|
||||
)
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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', '')
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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'),
|
||||
)
|
||||
Reference in New Issue
Block a user