Refactor: restructure market module with services, stores, and utils
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
服务模块
|
||||
"""
|
||||
|
||||
from .kline_service import KlineService
|
||||
from .pivot_service import PivotService
|
||||
from .llm_service import LLMService
|
||||
from .tech_indicators import calculate_ma, calculate_adx, calculate_rsi, calculate_macd, calculate_bollinger_bands
|
||||
from .tech_service import TechService
|
||||
from .calendar_service import CalendarService
|
||||
from .flash_news_service import FlashNewsService
|
||||
from .pending_order_service import PendingOrderService
|
||||
from .trading_instruction_service import TradingInstructionService
|
||||
|
||||
# 信号服务
|
||||
from .signal import SignalService, PivotSignalGenerator, KeyLevelSignalGenerator, AIEntrySignalGenerator
|
||||
|
||||
# 策略服务
|
||||
from .strategy import StrategyService, RiskManager
|
||||
|
||||
# 统计、持仓、交易历史服务
|
||||
from .statistics_service import StatisticsService
|
||||
from .position_service import PositionService
|
||||
from .trade_history_service import TradeHistoryService
|
||||
|
||||
__all__ = [
|
||||
'KlineService', 'PivotService', 'LLMService', 'TechService',
|
||||
'CalendarService', 'FlashNewsService',
|
||||
'PendingOrderService', 'TradingInstructionService',
|
||||
'SignalService', 'PivotSignalGenerator', 'KeyLevelSignalGenerator', 'AIEntrySignalGenerator',
|
||||
'StrategyService', 'RiskManager',
|
||||
'StatisticsService', 'PositionService', 'TradeHistoryService',
|
||||
'calculate_ma', 'calculate_adx', 'calculate_rsi', 'calculate_macd', 'calculate_bollinger_bands'
|
||||
]
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
财经日历服务模块
|
||||
处理事件影响分析、提醒等业务逻辑
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from ..models import CalendarEvent
|
||||
from ..store import CalendarStore
|
||||
from ..event_config import get_high_impact_event_names, DATA_IMPACT_RULES
|
||||
|
||||
|
||||
class CalendarService:
|
||||
"""财经日历服务(处理业务逻辑)"""
|
||||
|
||||
# 提醒时间(发布前多少秒)
|
||||
REMINDER_SECONDS = 300 # 5分钟
|
||||
|
||||
def __init__(self, calendar_store: CalendarStore):
|
||||
self.store = calendar_store
|
||||
|
||||
# 高影响事件名称
|
||||
self._high_impact_names = get_high_impact_event_names()
|
||||
|
||||
print("[CalendarService] 财经日历服务已初始化")
|
||||
|
||||
# ==================== 事件查询 ====================
|
||||
|
||||
def get_calendar(self, date_str: str = None) -> List[Dict]:
|
||||
"""获取财经日历"""
|
||||
return self.store.get_events(date_str)
|
||||
|
||||
def get_upcoming_events(self, hours: int = 24) -> List[Dict]:
|
||||
"""获取即将发布的重要事件"""
|
||||
events = self.store.get_upcoming_events(hours)
|
||||
return [e.to_dict() for e in events]
|
||||
|
||||
def get_event_by_id(self, event_id: str) -> Optional[CalendarEvent]:
|
||||
"""根据ID获取事件"""
|
||||
return self.store.get_event_by_id(event_id)
|
||||
|
||||
# ==================== 事件提醒 ====================
|
||||
|
||||
def check_upcoming_reminders(self) -> List[CalendarEvent]:
|
||||
"""
|
||||
检查即将发布的事件提醒
|
||||
|
||||
Returns:
|
||||
需要提醒的事件列表
|
||||
"""
|
||||
now = datetime.now()
|
||||
reminders = []
|
||||
|
||||
events = self.store.get_upcoming_events(hours=1, min_importance=2)
|
||||
|
||||
for event in events:
|
||||
if not event.publish_time:
|
||||
continue
|
||||
|
||||
time_to_publish = (event.publish_time - now).total_seconds()
|
||||
|
||||
# 发布前5分钟内
|
||||
if 0 < time_to_publish <= self.REMINDER_SECONDS:
|
||||
reminder_key = f"{event.id}_reminder"
|
||||
|
||||
if not self.store.is_alerted(reminder_key):
|
||||
reminders.append(event)
|
||||
self.store.mark_alerted(reminder_key)
|
||||
|
||||
return reminders
|
||||
|
||||
# ==================== 影响分析 ====================
|
||||
|
||||
def analyze_event_impact(self, event: CalendarEvent) -> Dict:
|
||||
"""
|
||||
分析事件对相关品种的影响
|
||||
|
||||
Args:
|
||||
event: 事件对象
|
||||
|
||||
Returns:
|
||||
影响分析结果 {symbol: {direction, reason}}
|
||||
"""
|
||||
impact = {}
|
||||
|
||||
# 检查是否有结果
|
||||
if not event.result or not event.actual:
|
||||
return impact
|
||||
|
||||
for symbol in event.symbols:
|
||||
# 查找影响规则
|
||||
rules = DATA_IMPACT_RULES.get(symbol, {})
|
||||
event_rules = rules.get(event.name) or rules.get(event.name_en)
|
||||
|
||||
if not event_rules:
|
||||
continue
|
||||
|
||||
direction = event_rules.get(event.result)
|
||||
reason_key = f"reason_{event.result}"
|
||||
reason = event_rules.get(reason_key, "")
|
||||
|
||||
if direction:
|
||||
impact[symbol] = {
|
||||
"direction": direction,
|
||||
"reason": reason,
|
||||
"event_name": event.name,
|
||||
"actual": event.actual,
|
||||
"forecast": event.forecast,
|
||||
"result": event.result
|
||||
}
|
||||
|
||||
return impact
|
||||
|
||||
def update_event_result(self, event_id: str, actual: str, result: str) -> bool:
|
||||
"""
|
||||
更新事件结果并分析影响
|
||||
|
||||
Args:
|
||||
event_id: 事件ID
|
||||
actual: 实际值
|
||||
result: 结果类型 (better/worse/in_line)
|
||||
|
||||
Returns:
|
||||
是否更新成功
|
||||
"""
|
||||
event = self.store.get_event_by_id(event_id)
|
||||
if not event:
|
||||
return False
|
||||
|
||||
# 分析影响
|
||||
event.actual = actual
|
||||
event.result = result
|
||||
impact = self.analyze_event_impact(event)
|
||||
|
||||
# 更新存储
|
||||
return self.store.update_event_result(event_id, actual, result, impact)
|
||||
|
||||
# ==================== 高影响事件判断 ====================
|
||||
|
||||
def is_high_impact_event(self, event: CalendarEvent) -> bool:
|
||||
"""判断是否为高影响事件"""
|
||||
if event.importance >= 3:
|
||||
return True
|
||||
if event.name in self._high_impact_names:
|
||||
return True
|
||||
if event.name_en in self._high_impact_names:
|
||||
return True
|
||||
return False
|
||||
|
||||
# ==================== 状态 ====================
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取服务状态"""
|
||||
return {
|
||||
"store_status": self.store.get_status(),
|
||||
"high_impact_event_names": len(self._high_impact_names)
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
快讯服务模块
|
||||
处理快讯影响分析等业务逻辑
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from ..models import FlashNews
|
||||
from ..store import FlashNewsStore
|
||||
from ..event_config import KEY_SPEAKERS, KEY_EVENTS, WATCH_SYMBOLS
|
||||
|
||||
|
||||
class FlashNewsService:
|
||||
"""快讯服务(处理业务逻辑)"""
|
||||
|
||||
def __init__(self, flash_news_store: FlashNewsStore):
|
||||
self.store = flash_news_store
|
||||
|
||||
print("[FlashNewsService] 快讯服务已初始化")
|
||||
|
||||
# ==================== 快讯查询 ====================
|
||||
|
||||
def get_recent_news(self, count: int = 20) -> List[Dict]:
|
||||
"""获取最近快讯"""
|
||||
return self.store.get_news(count)
|
||||
|
||||
def get_news_by_id(self, news_id: str) -> Optional[FlashNews]:
|
||||
"""根据ID获取快讯"""
|
||||
return self.store.get_news_by_id(news_id)
|
||||
|
||||
# ==================== 影响分析 ====================
|
||||
|
||||
def analyze_news_impact(self, news: FlashNews) -> Dict:
|
||||
"""
|
||||
分析快讯对市场的影响
|
||||
|
||||
Args:
|
||||
news: 快讯对象
|
||||
|
||||
Returns:
|
||||
{
|
||||
"speaker": 发言人名称,
|
||||
"speaker_title": 发言人职位,
|
||||
"impact": {symbol: direction},
|
||||
"topics": [相关话题]
|
||||
}
|
||||
"""
|
||||
result = {
|
||||
"speaker": "",
|
||||
"speaker_title": "",
|
||||
"impact": {},
|
||||
"topics": []
|
||||
}
|
||||
|
||||
content = news.content.lower()
|
||||
|
||||
# 1. 检查关键人物讲话
|
||||
for speaker_config in KEY_SPEAKERS:
|
||||
keywords = speaker_config.get('keywords', [])
|
||||
matched = False
|
||||
|
||||
for keyword in keywords:
|
||||
if keyword.lower() in content:
|
||||
matched = True
|
||||
break
|
||||
|
||||
if matched:
|
||||
result["speaker"] = speaker_config['name']
|
||||
result["speaker_title"] = speaker_config['title']
|
||||
|
||||
# 分析关注话题
|
||||
watch_topics = speaker_config.get('watch_topics', [])
|
||||
impact_symbols = speaker_config.get('impact_symbols', [])
|
||||
default_impact = speaker_config.get('default_impact', {})
|
||||
|
||||
for topic in watch_topics:
|
||||
if topic in content:
|
||||
result["topics"].append(topic)
|
||||
|
||||
# 应用默认影响
|
||||
for symbol in impact_symbols:
|
||||
if symbol in default_impact:
|
||||
topic_impact = default_impact[symbol]
|
||||
if topic in topic_impact:
|
||||
result["impact"][symbol] = {
|
||||
"direction": topic_impact[topic],
|
||||
"reason": f"{speaker_config['name']}提及{topic}"
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
# 2. 检查关键事件
|
||||
for event_config in KEY_EVENTS:
|
||||
watch_keywords = event_config.get('watch_keywords', [])
|
||||
matched = False
|
||||
|
||||
for keyword in watch_keywords:
|
||||
if keyword.lower() in content:
|
||||
matched = True
|
||||
break
|
||||
|
||||
if matched:
|
||||
for symbol in event_config.get('symbols', []):
|
||||
if symbol not in result["impact"]:
|
||||
result["impact"][symbol] = {
|
||||
"direction": "不确定",
|
||||
"reason": f"{event_config['name']}相关新闻"
|
||||
}
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def process_news(self, news: FlashNews) -> Optional[Dict]:
|
||||
"""
|
||||
处理快讯(分析影响并存储)
|
||||
|
||||
Args:
|
||||
news: 快讯对象
|
||||
|
||||
Returns:
|
||||
如果有影响则返回分析结果,否则返回None
|
||||
"""
|
||||
# 分析影响
|
||||
analysis = self.analyze_news_impact(news)
|
||||
|
||||
# 只处理有影响的快讯
|
||||
if not analysis["impact"] and not analysis["speaker"]:
|
||||
return None
|
||||
|
||||
# 更新快讯对象
|
||||
news.speaker = analysis["speaker"]
|
||||
news.speaker_title = analysis["speaker_title"]
|
||||
news.impact = analysis["impact"]
|
||||
news.analyzed = True
|
||||
news.importance = 2 if analysis["speaker"] else 1
|
||||
|
||||
# 添加到存储
|
||||
self.store.add_news(news)
|
||||
|
||||
return analysis
|
||||
|
||||
# ==================== 提醒状态 ====================
|
||||
|
||||
def should_alert(self, news_id: str) -> bool:
|
||||
"""
|
||||
判断是否应该推送提醒
|
||||
|
||||
Args:
|
||||
news_id: 快讯ID
|
||||
|
||||
Returns:
|
||||
是否应该提醒
|
||||
"""
|
||||
if self.store.is_alerted(news_id):
|
||||
return False
|
||||
|
||||
self.store.mark_alerted(news_id)
|
||||
return True
|
||||
|
||||
# ==================== 状态 ====================
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取服务状态"""
|
||||
return {
|
||||
"store_status": self.store.get_status()
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
K线服务模块
|
||||
处理K线相关的业务逻辑:时效性检查、连续性检查、格式转换等
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from ..store import KlineStore
|
||||
from ..models import KlineData
|
||||
|
||||
|
||||
class KlineService:
|
||||
"""K线服务(处理业务逻辑)"""
|
||||
|
||||
def __init__(self, store: KlineStore):
|
||||
self.store = store
|
||||
|
||||
def process_kline_data(self, symbol: str, period: str, klines: List[Dict],
|
||||
is_full: bool = False) -> Dict:
|
||||
"""
|
||||
处理K线数据(包含业务逻辑)
|
||||
|
||||
Args:
|
||||
symbol: 交易品种
|
||||
period: 周期
|
||||
klines: K线数据列表
|
||||
is_full: 是否为全量数据
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
"""
|
||||
period = period.upper()
|
||||
|
||||
# 保存数据
|
||||
result = self.store.save_klines(symbol, period, klines, is_full)
|
||||
|
||||
return result
|
||||
|
||||
def check_staleness(self, symbol: str, period: str, klines: List[Dict],
|
||||
timezone_offset_hours: float = 0) -> Dict:
|
||||
"""
|
||||
检查K线时效性
|
||||
|
||||
Args:
|
||||
symbol: 交易品种
|
||||
period: 周期
|
||||
klines: K线数据
|
||||
timezone_offset_hours: 时区偏移
|
||||
|
||||
Returns:
|
||||
{
|
||||
"is_stale": bool,
|
||||
"latest_kline_time": datetime,
|
||||
"kline_time_local": datetime,
|
||||
"time_diff_seconds": int
|
||||
}
|
||||
"""
|
||||
if not klines:
|
||||
return {"is_stale": False, "message": "无数据"}
|
||||
|
||||
period_interval = self.store.get_period_interval(period)
|
||||
latest_kline = klines[-1] if klines else None
|
||||
|
||||
if not latest_kline:
|
||||
return {"is_stale": False}
|
||||
|
||||
ts = latest_kline.get('timestamp') or latest_kline.get('time')
|
||||
latest_kline_time = self._parse_timestamp(ts)
|
||||
|
||||
if not latest_kline_time:
|
||||
return {"is_stale": False}
|
||||
|
||||
now_local = datetime.now()
|
||||
kline_time_local = latest_kline_time - timedelta(hours=timezone_offset_hours)
|
||||
time_diff = (now_local - kline_time_local).total_seconds()
|
||||
|
||||
return {
|
||||
"is_stale": time_diff > period_interval,
|
||||
"latest_kline_time": latest_kline_time,
|
||||
"kline_time_local": kline_time_local,
|
||||
"time_diff_seconds": int(time_diff),
|
||||
"period_interval": period_interval
|
||||
}
|
||||
|
||||
def check_continuity(self, symbol: str, period: str, new_klines: List[Dict]) -> Dict:
|
||||
"""
|
||||
检查增量K线数据是否连续
|
||||
|
||||
Args:
|
||||
symbol: 品种名称
|
||||
period: 周期
|
||||
new_klines: 新推送的K线数据列表
|
||||
|
||||
Returns:
|
||||
{
|
||||
"is_continuous": bool,
|
||||
"gap_count": int,
|
||||
"last_existing_time": datetime,
|
||||
"first_new_time": datetime
|
||||
}
|
||||
"""
|
||||
period = period.upper()
|
||||
|
||||
if not new_klines:
|
||||
return {"is_continuous": True, "gap_count": 0}
|
||||
|
||||
interval = self.store.get_period_interval(period)
|
||||
|
||||
existing = self.store.get_all_klines(symbol, period)
|
||||
if not existing:
|
||||
return {"is_continuous": True, "gap_count": 0}
|
||||
|
||||
# 获取现有数据最后时间
|
||||
last_existing_time = self._parse_timestamp(
|
||||
existing[-1].get('timestamp') or existing[-1].get('time')
|
||||
)
|
||||
if last_existing_time is None:
|
||||
return {"is_continuous": True, "gap_count": 0}
|
||||
|
||||
# 获取新数据最早时间
|
||||
first_new_time = None
|
||||
for k in new_klines:
|
||||
ts = self._parse_timestamp(k.get('timestamp') or k.get('time'))
|
||||
if ts:
|
||||
if first_new_time is None or ts < first_new_time:
|
||||
first_new_time = ts
|
||||
|
||||
if first_new_time is None:
|
||||
return {"is_continuous": True, "gap_count": 0}
|
||||
|
||||
# 计算时间差
|
||||
time_diff = (first_new_time - last_existing_time).total_seconds()
|
||||
|
||||
if time_diff <= 0:
|
||||
return {
|
||||
"is_continuous": True,
|
||||
"gap_count": 0,
|
||||
"last_existing_time": last_existing_time,
|
||||
"first_new_time": first_new_time
|
||||
}
|
||||
|
||||
gap_periods = int(time_diff / interval)
|
||||
|
||||
return {
|
||||
"is_continuous": gap_periods <= 1,
|
||||
"gap_count": max(0, gap_periods - 1),
|
||||
"last_existing_time": last_existing_time,
|
||||
"first_new_time": first_new_time,
|
||||
"expected_gap": gap_periods
|
||||
}
|
||||
|
||||
def convert_to_kline_objects(self, klines: List[Dict], symbol: str, period: str) -> List[KlineData]:
|
||||
"""
|
||||
将K线字典列表转换为KlineData对象列表
|
||||
|
||||
Args:
|
||||
klines: K线字典列表
|
||||
symbol: 品种
|
||||
period: 周期
|
||||
|
||||
Returns:
|
||||
KlineData对象列表
|
||||
"""
|
||||
return [
|
||||
KlineData(
|
||||
symbol=symbol,
|
||||
period=period,
|
||||
timestamp=k.get('timestamp') or k.get('time'),
|
||||
open_price=float(k.get('open', 0)),
|
||||
high=float(k.get('high', 0)),
|
||||
low=float(k.get('low', 0)),
|
||||
close=float(k.get('close', 0)),
|
||||
volume=float(k.get('volume', 0))
|
||||
)
|
||||
for k in klines
|
||||
]
|
||||
|
||||
def get_klines(self, symbol: str, period: str, count: int = 100) -> List[Dict]:
|
||||
"""获取K线数据"""
|
||||
return self.store.get_klines(symbol, period, count)
|
||||
|
||||
def get_all_klines(self, symbol: str, period: str) -> List[Dict]:
|
||||
"""获取所有K线数据"""
|
||||
return self.store.get_all_klines(symbol, period)
|
||||
|
||||
def get_all_kline_objects(self, symbol: str, period: str) -> List[KlineData]:
|
||||
"""获取所有K线数据并转换为KlineData对象"""
|
||||
klines = self.store.get_all_klines(symbol, period)
|
||||
return self.convert_to_kline_objects(klines, symbol, period)
|
||||
|
||||
def get_latest_price(self, symbol: str) -> Optional[float]:
|
||||
"""获取最新价格"""
|
||||
return self.store.get_latest_price(symbol)
|
||||
|
||||
def is_initialized(self, symbol: str, period: str) -> bool:
|
||||
"""检查是否已初始化"""
|
||||
return self.store.is_initialized(symbol, period)
|
||||
|
||||
def get_symbols(self) -> List[str]:
|
||||
"""获取所有品种"""
|
||||
return self.store.get_symbols()
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取状态"""
|
||||
return self.store.get_status()
|
||||
|
||||
def check_m1_updated_within(self, symbol: str, seconds: int = 180) -> Dict:
|
||||
"""检查M1数据更新情况"""
|
||||
return self.store.check_m1_updated_within(symbol, seconds)
|
||||
|
||||
def get_period_interval(self, period: str) -> int:
|
||||
"""获取周期时间间隔"""
|
||||
return self.store.get_period_interval(period)
|
||||
|
||||
def check_symbols_status(self, symbols: List[str], stale_threshold: int = 180) -> Dict[str, List[str]]:
|
||||
"""
|
||||
检查多个品种的数据更新状态
|
||||
|
||||
Args:
|
||||
symbols: 品种列表
|
||||
stale_threshold: 过期阈值(秒),默认180秒(3分钟)
|
||||
|
||||
Returns:
|
||||
{"active": [...], "stale": [...], "closed": [...]}
|
||||
- active: 指定秒数内有数据更新
|
||||
- stale: 超过指定秒数未更新
|
||||
- closed: 无数据
|
||||
"""
|
||||
result = {"active": [], "stale": [], "closed": []}
|
||||
|
||||
for symbol in symbols:
|
||||
m1_status = self.store.check_m1_updated_within(symbol, stale_threshold)
|
||||
market_status = m1_status.get("market_status", "closed")
|
||||
|
||||
if market_status == "active":
|
||||
result["active"].append(symbol)
|
||||
elif market_status == "stale":
|
||||
result["stale"].append(symbol)
|
||||
else:
|
||||
result["closed"].append(symbol)
|
||||
|
||||
return result
|
||||
|
||||
def _parse_timestamp(self, ts) -> Optional[datetime]:
|
||||
"""解析时间戳"""
|
||||
if ts is None:
|
||||
return None
|
||||
if isinstance(ts, datetime):
|
||||
return ts
|
||||
ts_str = str(ts)
|
||||
for fmt in ["%Y-%m-%d %H:%M:%S", "%Y.%m.%d %H:%M", "%Y.%m.%d %H:%M:%S", "%Y-%m-%d %H:%M"]:
|
||||
try:
|
||||
return datetime.strptime(ts_str, fmt)
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
@@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
LLM 服务模块
|
||||
处理 LLM 分析相关的业务逻辑
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from ..models import LLMConfig, LLMAnalysisResult
|
||||
from ..store import LLMStore
|
||||
from .kline_service import KlineService
|
||||
|
||||
|
||||
class LLMService:
|
||||
"""LLM 服务(处理业务逻辑)"""
|
||||
|
||||
# 分析间隔(秒)
|
||||
ANALYZE_INTERVAL = 300 # 5分钟
|
||||
|
||||
# 各周期K线数量限制
|
||||
KLINE_LIMITS = {
|
||||
'H4': 20,
|
||||
'H1': 24,
|
||||
'M15': 32,
|
||||
'M5': 48,
|
||||
'M1': 60
|
||||
}
|
||||
|
||||
# 数据过期阈值(秒)
|
||||
STALE_THRESHOLD = 180 # 3分钟
|
||||
|
||||
def __init__(self, llm_store: LLMStore, kline_service: KlineService):
|
||||
self.llm_store = llm_store
|
||||
self.kline_service = kline_service
|
||||
|
||||
# 从环境变量补充配置
|
||||
self._load_env_config()
|
||||
|
||||
print("[LLMService] LLM服务已初始化")
|
||||
|
||||
def _load_env_config(self):
|
||||
"""从环境变量加载配置"""
|
||||
config = self.llm_store.get_config()
|
||||
|
||||
if not config.api_key and os.environ.get("LLM_API_KEY"):
|
||||
self.llm_store.update_config(api_key=os.environ.get("LLM_API_KEY"))
|
||||
|
||||
if os.environ.get("LLM_API_BASE"):
|
||||
self.llm_store.update_config(api_base=os.environ.get("LLM_API_BASE"))
|
||||
|
||||
if os.environ.get("LLM_MODEL"):
|
||||
self.llm_store.update_config(model=os.environ.get("LLM_MODEL"))
|
||||
|
||||
# ==================== 配置管理 ====================
|
||||
|
||||
def get_config(self) -> Dict:
|
||||
"""获取配置"""
|
||||
return self.llm_store.get_config().to_dict()
|
||||
|
||||
def configure(self, api_key: str = None, api_base: str = None, model: str = None) -> Dict:
|
||||
"""配置 LLM 参数"""
|
||||
config = self.llm_store.update_config(api_key, api_base, model)
|
||||
return {
|
||||
"status": "ok",
|
||||
"enabled": config.enabled,
|
||||
"model": config.model,
|
||||
"api_base": config.api_base
|
||||
}
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
"""是否启用"""
|
||||
return self.llm_store.get_config().enabled
|
||||
|
||||
# ==================== 数据收集 ====================
|
||||
|
||||
def collect_klines_for_analysis(self, symbols: List[str]) -> Dict[str, Dict]:
|
||||
"""
|
||||
收集指定品种的K线数据用于分析
|
||||
|
||||
Returns:
|
||||
{symbol: {period: [klines]}}
|
||||
"""
|
||||
all_klines = {}
|
||||
|
||||
for symbol in symbols:
|
||||
klines_data = {}
|
||||
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
|
||||
limit = self.KLINE_LIMITS.get(period, 30)
|
||||
klines = self.kline_service.get_klines(symbol, period, limit)
|
||||
if klines:
|
||||
klines_data[period] = klines
|
||||
|
||||
if klines_data:
|
||||
all_klines[symbol] = klines_data
|
||||
|
||||
return all_klines
|
||||
|
||||
# ==================== Prompt 构建 ====================
|
||||
|
||||
def build_analysis_prompt(self, all_klines: Dict[str, Dict]) -> str:
|
||||
"""构建分析提示词"""
|
||||
prompt = """你是一位专业的金融分析师。请分析以下多个交易品种的K线数据,给出每个品种的趋势判断和交易建议。
|
||||
|
||||
## 分析要求
|
||||
|
||||
对于每个品种,请分析:
|
||||
1. 各周期(H4、H1、M15、M5、M1)的趋势判断,包含趋势类型、置信度(0-100)和判断理由
|
||||
2. 整体趋势方向、强度(0-100)和总结
|
||||
3. 关键支撑位和压力位(请根据K线数据自行判断,各列出3个)
|
||||
4. 交易建议:必须包含M1、M5、M15三个周期的具体交易建议
|
||||
|
||||
趋势类型可选值:单边上涨、单边下跌、区间震荡、震荡上升、震荡下跌、震荡收窄、震荡扩大
|
||||
|
||||
请按以下JSON格式输出(必须是有效的JSON格式,包含所有品种):
|
||||
|
||||
```json
|
||||
{
|
||||
"品种1": {
|
||||
"trend_analysis": {
|
||||
"H4": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
|
||||
"H1": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
|
||||
"M15": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
|
||||
"M5": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
|
||||
"M1": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"}
|
||||
},
|
||||
"overall_trend": {
|
||||
"direction": "整体趋势方向",
|
||||
"strength": 强度,
|
||||
"summary": "整体趋势总结"
|
||||
},
|
||||
"key_levels": {
|
||||
"resistance": [压力位1, 压力位2, 压力位3],
|
||||
"support": [支撑位1, 支撑位2, 支撑位3]
|
||||
},
|
||||
"trade_suggestions": [
|
||||
{
|
||||
"period": "M15",
|
||||
"direction": "buy或sell",
|
||||
"entry_price": 入场价格,
|
||||
"stop_loss": 止损价格,
|
||||
"take_profit": 止盈价格,
|
||||
"reason": "交易理由"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## K线数据
|
||||
"""
|
||||
# 添加各品种的K线数据
|
||||
for symbol, klines_data in all_klines.items():
|
||||
prompt += f"\n### {symbol}\n"
|
||||
for period, klines in klines_data.items():
|
||||
prompt += f"\n#### {period} 周期({len(klines)}根K线)\n"
|
||||
prompt += "| 时间 | 开盘 | 最高 | 最低 | 收盘 |\n"
|
||||
prompt += "|------|------|------|------|------|\n"
|
||||
for k in klines:
|
||||
prompt += f"| {k['timestamp']} | {k['open']:.2f} | {k['high']:.2f} | {k['low']:.2f} | {k['close']:.2f} |\n"
|
||||
|
||||
prompt += """
|
||||
|
||||
请确保输出是纯JSON格式,不要有其他文字说明。每个品种的分析结果都要完整,trade_suggestions必须包含M1、M5、M15三个周期的建议。
|
||||
"""
|
||||
return prompt
|
||||
|
||||
# ==================== LLM API 调用 ====================
|
||||
|
||||
def call_llm(self, prompt: str) -> Optional[Dict]:
|
||||
"""调用 LLM API(非流式)"""
|
||||
config = self.llm_store.get_config()
|
||||
if not config.api_key:
|
||||
return None
|
||||
|
||||
try:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {config.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"model": config.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一位专业的金融分析师,擅长技术分析和趋势判断。请用JSON格式输出分析结果,不要有任何额外的文字说明。"},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 4000
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{config.api_base}/chat/completions",
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
content = result["choices"][0]["message"]["content"]
|
||||
return self._parse_llm_response(content)
|
||||
else:
|
||||
print(f"[LLMService] API调用失败: {response.status_code} - {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"[LLMService] 调用异常: {e}")
|
||||
return None
|
||||
|
||||
def call_llm_stream(self, prompt: str, on_chunk: callable = None) -> Optional[Dict]:
|
||||
"""
|
||||
调用 LLM API(流式)
|
||||
|
||||
Args:
|
||||
prompt: 提示词
|
||||
on_chunk: 回调函数,参数为 (chunk_count, full_content)
|
||||
"""
|
||||
config = self.llm_store.get_config()
|
||||
if not config.api_key:
|
||||
return None
|
||||
|
||||
try:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {config.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"model": config.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一位专业的金融分析师,擅长技术分析和趋势判断。请用JSON格式输出分析结果,不要有任何额外的文字说明。"},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 4000,
|
||||
"stream": True
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{config.api_base}/chat/completions",
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=120,
|
||||
stream=True
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"[LLMService] API调用失败: {response.status_code} - {response.text}")
|
||||
return None
|
||||
|
||||
# 收集完整响应
|
||||
full_content = ""
|
||||
chunk_count = 0
|
||||
|
||||
for line in response.iter_lines():
|
||||
if not line:
|
||||
continue
|
||||
|
||||
line = line.decode('utf-8')
|
||||
if line.startswith('data: '):
|
||||
data_str = line[6:]
|
||||
if data_str == '[DONE]':
|
||||
break
|
||||
|
||||
try:
|
||||
chunk_data = json.loads(data_str)
|
||||
if 'choices' in chunk_data and len(chunk_data['choices']) > 0:
|
||||
delta = chunk_data['choices'][0].get('delta', {})
|
||||
content_piece = delta.get('content', '')
|
||||
if content_piece:
|
||||
full_content += content_piece
|
||||
chunk_count += 1
|
||||
|
||||
if on_chunk:
|
||||
on_chunk(chunk_count, full_content)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
print(f"[LLMService] 流式接收完成,共 {chunk_count} 个chunk,{len(full_content)} 字符")
|
||||
return self._parse_llm_response(full_content)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[LLMService] 流式调用异常: {e}")
|
||||
return None
|
||||
|
||||
def _parse_llm_response(self, content: str) -> Optional[Dict]:
|
||||
"""解析 LLM 响应"""
|
||||
try:
|
||||
# 提取JSON部分
|
||||
if "```json" in content:
|
||||
content = content.split("```json")[1].split("```")[0]
|
||||
elif "```" in content:
|
||||
content = content.split("```")[1].split("```")[0]
|
||||
|
||||
return json.loads(content.strip())
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[LLMService] JSON解析失败: {e}")
|
||||
return None
|
||||
|
||||
# ==================== 入场价检测 ====================
|
||||
|
||||
def check_entry_price_nearby(self, symbol: str, current_price: float,
|
||||
threshold: float = 0.0001) -> List[Dict]:
|
||||
"""
|
||||
检查当前价格是否接近 AI 建议的入场价
|
||||
|
||||
Args:
|
||||
symbol: 交易品种
|
||||
current_price: 当前价格
|
||||
threshold: 价格接近阈值,默认万分之一
|
||||
|
||||
Returns:
|
||||
匹配的交易建议列表
|
||||
"""
|
||||
matched = []
|
||||
|
||||
result = self.llm_store.get_analysis_result(symbol)
|
||||
if not result or not result.trade_suggestions:
|
||||
return matched
|
||||
|
||||
for suggestion in result.trade_suggestions:
|
||||
entry_price = suggestion.get('entry_price')
|
||||
period = suggestion.get('period')
|
||||
direction = suggestion.get('direction')
|
||||
stop_loss = suggestion.get('stop_loss')
|
||||
take_profit = suggestion.get('take_profit')
|
||||
|
||||
if not entry_price or entry_price <= 0:
|
||||
continue
|
||||
|
||||
# 验证止损止盈
|
||||
if not stop_loss or not take_profit or stop_loss <= 0 or take_profit <= 0:
|
||||
print(f"[LLMService] 跳过无效建议: {period} sl={stop_loss}, tp={take_profit}")
|
||||
continue
|
||||
|
||||
price_diff_pct = abs(current_price - entry_price) / entry_price
|
||||
|
||||
if price_diff_pct <= threshold:
|
||||
# 检查冷却
|
||||
can_alert = self.llm_store.check_entry_alert_cooldown(
|
||||
symbol, period, direction, entry_price
|
||||
)
|
||||
|
||||
if can_alert:
|
||||
matched.append({
|
||||
"symbol": symbol,
|
||||
"period": period,
|
||||
"direction": direction,
|
||||
"entry_price": entry_price,
|
||||
"current_price": current_price,
|
||||
"price_diff_pct": round(price_diff_pct * 100, 4),
|
||||
"stop_loss": stop_loss,
|
||||
"take_profit": take_profit,
|
||||
"reason": suggestion.get('reason'),
|
||||
"analyzed_at": result.analyzed_at
|
||||
})
|
||||
print(f"[LLMService] 价格接近AI入场价: {symbol} {period} "
|
||||
f"入场价 {entry_price:.2f}, 当前价 {current_price:.2f}")
|
||||
|
||||
# 清理过期记录
|
||||
self.llm_store.cleanup_entry_alerts()
|
||||
|
||||
return matched
|
||||
|
||||
# ==================== 分析执行 ====================
|
||||
|
||||
def run_analysis(self, on_status: callable = None, on_complete: callable = None) -> Dict:
|
||||
"""
|
||||
执行分析
|
||||
|
||||
Args:
|
||||
on_status: 状态回调
|
||||
on_complete: 完成回调
|
||||
|
||||
Returns:
|
||||
分析结果
|
||||
"""
|
||||
if not self.is_enabled():
|
||||
return {"status": "error", "message": "LLM 未启用"}
|
||||
|
||||
# 获取品种列表
|
||||
symbols = self.kline_service.get_symbols()
|
||||
if not symbols:
|
||||
if on_status:
|
||||
on_status("error", "没有品种数据")
|
||||
return {"status": "error", "message": "没有品种数据"}
|
||||
|
||||
if on_status:
|
||||
on_status("analyzing", f"正在检查 {len(symbols)} 个品种...")
|
||||
|
||||
# 检查数据状态
|
||||
status = self.kline_service.check_symbols_status(symbols, self.STALE_THRESHOLD)
|
||||
active_symbols = status["active"]
|
||||
|
||||
# 更新过期和休市品种状态
|
||||
for symbol in status["stale"]:
|
||||
self.llm_store.update_market_status(symbol, "stale", data_stale=True)
|
||||
for symbol in status["closed"]:
|
||||
self.llm_store.update_market_status(symbol, "closed", data_stale=True)
|
||||
|
||||
if not active_symbols:
|
||||
if on_status:
|
||||
on_status("stale", "所有品种数据均未更新")
|
||||
return {"status": "ok", "message": "所有品种数据均未更新"}
|
||||
|
||||
if on_status:
|
||||
on_status("analyzing", f"正在分析 {len(active_symbols)} 个品种...")
|
||||
|
||||
# 收集K线数据
|
||||
all_klines = self.collect_klines_for_analysis(active_symbols)
|
||||
if not all_klines:
|
||||
if on_status:
|
||||
on_status("error", "无K线数据可分析")
|
||||
return {"status": "error", "message": "无K线数据可分析"}
|
||||
|
||||
# 构建提示词
|
||||
prompt = self.build_analysis_prompt(all_klines)
|
||||
|
||||
# 调用 LLM
|
||||
def on_chunk(count, content):
|
||||
if on_status and count % 50 == 0:
|
||||
on_status("streaming", f"正在接收分析结果... ({len(content)} 字符)")
|
||||
|
||||
response = self.call_llm_stream(prompt, on_chunk)
|
||||
|
||||
# 保存结果
|
||||
if response:
|
||||
for symbol, analysis in response.items():
|
||||
if isinstance(analysis, dict):
|
||||
self.llm_store.save_analysis_dict(symbol, analysis)
|
||||
|
||||
if on_complete:
|
||||
on_complete(response)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"analyzed_symbols": list(response.keys()) if response else []
|
||||
}
|
||||
|
||||
# ==================== 查询 ====================
|
||||
|
||||
def get_analysis(self, symbol: str = None) -> Dict:
|
||||
"""获取分析结果"""
|
||||
return self.llm_store.get_analysis(symbol)
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取状态"""
|
||||
return self.llm_store.get_status()
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
待确认订单服务模块
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Callable
|
||||
from datetime import datetime
|
||||
import threading
|
||||
|
||||
from ..models import PendingOrder
|
||||
from ..store import PendingOrderStore
|
||||
|
||||
|
||||
class PendingOrderService:
|
||||
"""待确认订单服务(处理业务逻辑)"""
|
||||
|
||||
def __init__(self, pending_order_store: PendingOrderStore = None):
|
||||
self.store = pending_order_store or PendingOrderStore()
|
||||
|
||||
# 订单确认回调(确认后将指令加入交易队列)
|
||||
self._confirm_callback: Optional[Callable] = None
|
||||
|
||||
# 启动超时清理线程
|
||||
self._start_cleanup_thread()
|
||||
|
||||
print("[PendingOrderService] 待确认订单服务已初始化")
|
||||
|
||||
def set_confirm_callback(self, callback: Callable):
|
||||
"""
|
||||
设置订单确认回调函数
|
||||
|
||||
回调签名: callback(order: PendingOrder) -> None
|
||||
"""
|
||||
self._confirm_callback = callback
|
||||
|
||||
def _start_cleanup_thread(self):
|
||||
"""启动超时清理线程"""
|
||||
def cleanup_loop():
|
||||
while True:
|
||||
try:
|
||||
expired = self.store.cleanup_expired()
|
||||
for order in expired:
|
||||
print(f"[PendingOrderService] 订单超时自动移除: {order.order_id}")
|
||||
except Exception as e:
|
||||
print(f"[PendingOrderService] 清理线程异常: {e}")
|
||||
threading.Event().wait(10) # 每10秒检查一次
|
||||
|
||||
thread = threading.Thread(target=cleanup_loop, daemon=True)
|
||||
thread.start()
|
||||
|
||||
# ==================== 创建订单 ====================
|
||||
|
||||
def create_order(self, symbol: str, action: str, price: float,
|
||||
mount: float, sl: float, tp: float,
|
||||
reason: str = "", description: str = "",
|
||||
source: str = "", **kwargs) -> str:
|
||||
"""
|
||||
创建待确认订单
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
action: 方向 (b/s)
|
||||
price: 入场价
|
||||
mount: 手数
|
||||
sl: 止损
|
||||
tp: 止盈
|
||||
reason: 原因
|
||||
description: 描述
|
||||
source: 来源
|
||||
**kwargs: 其他字段(pivot_price, key_level, ai_period等)
|
||||
|
||||
Returns:
|
||||
订单ID
|
||||
"""
|
||||
order = PendingOrder(
|
||||
symbol=symbol,
|
||||
action=action,
|
||||
price=price,
|
||||
mount=mount,
|
||||
sl=sl,
|
||||
tp=tp,
|
||||
reason=reason,
|
||||
description=description,
|
||||
source=source,
|
||||
**kwargs
|
||||
)
|
||||
return self.store.add_order(order)
|
||||
|
||||
def create_order_from_dict(self, data: Dict) -> str:
|
||||
"""从字典创建订单"""
|
||||
return self.store.add_order_from_dict(data)
|
||||
|
||||
# ==================== 查询订单 ====================
|
||||
|
||||
def get_order(self, order_id: str) -> Optional[PendingOrder]:
|
||||
"""获取订单"""
|
||||
return self.store.get_order_by_id(order_id)
|
||||
|
||||
def get_orders(self, symbol: str = None) -> List[PendingOrder]:
|
||||
"""获取订单列表"""
|
||||
return self.store.get_pending_orders(symbol)
|
||||
|
||||
def get_orders_dict(self, symbol: str = None) -> List[Dict]:
|
||||
"""获取订单字典列表"""
|
||||
return self.store.get_pending_orders_dict(symbol)
|
||||
|
||||
# 兼容旧方法名
|
||||
def get_pending_orders_dict(self, symbol: str = None) -> List[Dict]:
|
||||
"""获取订单字典列表(兼容旧方法名)"""
|
||||
return self.get_orders_dict(symbol)
|
||||
|
||||
def get_pending_count(self, symbol: str = None) -> int:
|
||||
"""获取待确认订单数量"""
|
||||
return self.store.get_pending_count(symbol)
|
||||
|
||||
# ==================== 确认/拒绝订单 ====================
|
||||
|
||||
def confirm_order(self, order_id: str, updates: Dict = None) -> Optional[PendingOrder]:
|
||||
"""
|
||||
确认订单
|
||||
|
||||
Args:
|
||||
order_id: 订单ID
|
||||
updates: 更新字段(如 mount, sl, tp)
|
||||
|
||||
Returns:
|
||||
确认后的订单
|
||||
"""
|
||||
# 先获取订单
|
||||
order = self.store.get_order_by_id(order_id)
|
||||
if not order:
|
||||
return None
|
||||
|
||||
# 应用更新
|
||||
if updates:
|
||||
if 'mount' in updates:
|
||||
order.mount = updates['mount']
|
||||
if 'sl' in updates:
|
||||
order.sl = updates['sl']
|
||||
if 'tp' in updates:
|
||||
order.tp = updates['tp']
|
||||
|
||||
# 确认订单(从存储中移除)
|
||||
confirmed_order = self.store.confirm_order(order_id)
|
||||
if not confirmed_order:
|
||||
return None
|
||||
|
||||
# 调用确认回调
|
||||
if self._confirm_callback:
|
||||
try:
|
||||
self._confirm_callback(confirmed_order)
|
||||
except Exception as e:
|
||||
print(f"[PendingOrderService] 确认回调执行失败: {e}")
|
||||
|
||||
return confirmed_order
|
||||
|
||||
def reject_order(self, order_id: str) -> Optional[PendingOrder]:
|
||||
"""拒绝订单"""
|
||||
return self.store.reject_order(order_id)
|
||||
|
||||
# ==================== 清理 ====================
|
||||
|
||||
def clear_all(self) -> int:
|
||||
"""清空所有待确认订单"""
|
||||
return self.store.clear_all()
|
||||
|
||||
# ==================== 状态 ====================
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取服务状态"""
|
||||
return {
|
||||
"store": self.store.get_status(),
|
||||
"callback_set": self._confirm_callback is not None,
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
转折点服务模块
|
||||
处理转折点相关的业务逻辑:检测、合并、接近检测等
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
import threading
|
||||
|
||||
from ..models import KlineData, PivotPoint
|
||||
from ..store import KlineStore, PivotStore
|
||||
|
||||
|
||||
class PivotService:
|
||||
"""转折点服务(处理业务逻辑)"""
|
||||
|
||||
# 各周期接近阈值(千分比)
|
||||
THRESHOLDS = {
|
||||
'H4': 0.0015, # 千分之1.5
|
||||
'H1': 0.0015, # 千分之1.5
|
||||
'M15': 0.0015, # 千分之1.5
|
||||
'M5': 0.0005, # 千分之0.5
|
||||
'M1': 0.0002 # 千分之0.2
|
||||
}
|
||||
|
||||
# 各周期转折强度(左右各N根K线)
|
||||
PERIOD_STRENGTH = {
|
||||
'M1': 6,
|
||||
'M5': 4,
|
||||
'M15': 3,
|
||||
'H1': 3,
|
||||
'H4': 3
|
||||
}
|
||||
|
||||
def __init__(self, pivot_store: PivotStore, kline_store: KlineStore):
|
||||
self.pivot_store = pivot_store
|
||||
self.kline_store = kline_store
|
||||
self.default_strength = 3
|
||||
|
||||
print("[PivotService] 转折点服务已初始化")
|
||||
print(f"[PivotService] 周期强度配置: {self.PERIOD_STRENGTH}")
|
||||
|
||||
def detect_pivots(self, symbol: str, period: str, klines: List[KlineData],
|
||||
strength: int = None) -> List[PivotPoint]:
|
||||
"""
|
||||
检测转折点
|
||||
|
||||
Args:
|
||||
symbol: 交易品种
|
||||
period: 周期
|
||||
klines: K线数据列表
|
||||
strength: 转折强度,None则使用周期默认值
|
||||
|
||||
Returns:
|
||||
检测到的转折点列表
|
||||
"""
|
||||
if strength is None:
|
||||
strength = self.PERIOD_STRENGTH.get(period, self.default_strength)
|
||||
|
||||
if len(klines) < 2 * strength + 1:
|
||||
return []
|
||||
|
||||
pivots = []
|
||||
|
||||
for i in range(strength, len(klines) - strength):
|
||||
current = klines[i]
|
||||
|
||||
# 检查是否为高点(顶分型)
|
||||
is_high = True
|
||||
for j in range(1, strength + 1):
|
||||
if klines[i - j].high >= current.high or klines[i + j].high >= current.high:
|
||||
is_high = False
|
||||
break
|
||||
|
||||
if is_high:
|
||||
pivot = PivotPoint(
|
||||
symbol=symbol,
|
||||
period=period,
|
||||
timestamp=current.timestamp,
|
||||
price=current.high,
|
||||
direction="high",
|
||||
strength=strength
|
||||
)
|
||||
pivots.append(pivot)
|
||||
|
||||
# 检查是否为低点(底分型)
|
||||
is_low = True
|
||||
for j in range(1, strength + 1):
|
||||
if klines[i - j].low <= current.low or klines[i + j].low <= current.low:
|
||||
is_low = False
|
||||
break
|
||||
|
||||
if is_low:
|
||||
pivot = PivotPoint(
|
||||
symbol=symbol,
|
||||
period=period,
|
||||
timestamp=current.timestamp,
|
||||
price=current.low,
|
||||
direction="low",
|
||||
strength=strength
|
||||
)
|
||||
pivots.append(pivot)
|
||||
|
||||
return pivots
|
||||
|
||||
def merge_pivots(self, pivots: List[PivotPoint]) -> List[PivotPoint]:
|
||||
"""
|
||||
合并相近的转折点
|
||||
|
||||
合并规则:相邻两个同方向转折点价格差距小于万分之四时合并
|
||||
"""
|
||||
if len(pivots) < 2:
|
||||
return pivots
|
||||
|
||||
high_pivots = [p for p in pivots if p.direction == "high"]
|
||||
low_pivots = [p for p in pivots if p.direction == "low"]
|
||||
|
||||
merged_highs = self._merge_same_direction(high_pivots, "high")
|
||||
merged_lows = self._merge_same_direction(low_pivots, "low")
|
||||
|
||||
return merged_highs + merged_lows
|
||||
|
||||
def _merge_same_direction(self, pivots: List[PivotPoint], direction: str) -> List[PivotPoint]:
|
||||
"""合并同方向的转折点"""
|
||||
if len(pivots) < 2:
|
||||
return pivots
|
||||
|
||||
pivots = sorted(pivots, key=lambda p: str(p.timestamp))
|
||||
|
||||
merged = []
|
||||
i = 0
|
||||
|
||||
while i < len(pivots):
|
||||
current = pivots[i]
|
||||
group = [current]
|
||||
|
||||
j = i + 1
|
||||
while j < len(pivots):
|
||||
next_pivot = pivots[j]
|
||||
|
||||
if current.price > 0:
|
||||
price_diff_pct = abs(next_pivot.price - current.price) / current.price
|
||||
if price_diff_pct <= 0.0004:
|
||||
group.append(next_pivot)
|
||||
j += 1
|
||||
continue
|
||||
|
||||
break
|
||||
|
||||
if direction == "high":
|
||||
best = max(group, key=lambda p: p.price)
|
||||
else:
|
||||
best = min(group, key=lambda p: p.price)
|
||||
|
||||
merged.append(best)
|
||||
i = j
|
||||
|
||||
return merged
|
||||
|
||||
def update_pivots(self, symbol: str, period: str, klines: List[KlineData],
|
||||
strength: int = None) -> int:
|
||||
"""
|
||||
更新转折点数据
|
||||
|
||||
Args:
|
||||
symbol: 交易品种
|
||||
period: 周期
|
||||
klines: K线数据列表
|
||||
strength: 转折强度
|
||||
|
||||
Returns:
|
||||
更新后的转折点数量
|
||||
"""
|
||||
if strength is None:
|
||||
strength = self.PERIOD_STRENGTH.get(period, self.default_strength)
|
||||
|
||||
pivots = self.detect_pivots(symbol, period, klines, strength)
|
||||
|
||||
# 保存原始转折点到时间线
|
||||
timeline = sorted(pivots, key=lambda p: self._normalize_timestamp(p.timestamp))
|
||||
|
||||
# 合并相近的转折点
|
||||
merged_pivots = self.merge_pivots(pivots)
|
||||
|
||||
# 存储到 pivot_store
|
||||
self.pivot_store.save_pivots(symbol, period, merged_pivots, timeline)
|
||||
|
||||
original_count = len(pivots)
|
||||
count = len(merged_pivots)
|
||||
|
||||
if original_count != count:
|
||||
print(f"[PivotService] {symbol} {period} 检测到 {original_count} 个转折点,合并后 {count} 个")
|
||||
else:
|
||||
print(f"[PivotService] {symbol} {period} 检测到 {count} 个转折点")
|
||||
|
||||
return count
|
||||
|
||||
def check_near_pivot(self, symbol: str, current_price: float,
|
||||
trend_filter: Dict[str, str] = None) -> List[Dict]:
|
||||
"""
|
||||
检查当前价格是否接近某个转折点
|
||||
|
||||
Args:
|
||||
symbol: 交易品种
|
||||
current_price: 当前价格
|
||||
trend_filter: 趋势过滤
|
||||
|
||||
Returns:
|
||||
接近的转折点列表
|
||||
"""
|
||||
near_pivots = []
|
||||
|
||||
periods = self.pivot_store.get_all_periods(symbol)
|
||||
|
||||
for period in periods:
|
||||
pivots = self.pivot_store.get_pivot_objects(symbol, period)
|
||||
threshold = self.THRESHOLDS.get(period, 0.001)
|
||||
|
||||
trend = trend_filter.get(period) if trend_filter else None
|
||||
|
||||
for pivot in pivots:
|
||||
if pivot.price == 0 or current_price == 0:
|
||||
continue
|
||||
|
||||
if trend == 'up' and pivot.direction != 'high':
|
||||
continue
|
||||
elif trend == 'down' and pivot.direction != 'low':
|
||||
continue
|
||||
|
||||
is_near = False
|
||||
alert_type = ""
|
||||
|
||||
if pivot.direction == "high":
|
||||
if current_price < pivot.price:
|
||||
distance_pct = (pivot.price - current_price) / current_price
|
||||
if distance_pct <= threshold:
|
||||
is_near = True
|
||||
alert_type = "near_high"
|
||||
|
||||
elif pivot.direction == "low":
|
||||
if current_price > pivot.price:
|
||||
distance_pct = (current_price - pivot.price) / current_price
|
||||
if distance_pct <= threshold:
|
||||
is_near = True
|
||||
alert_type = "near_low"
|
||||
|
||||
if is_near:
|
||||
distance_pct = abs(current_price - pivot.price) / current_price
|
||||
near_pivots.append({
|
||||
**pivot.to_dict(),
|
||||
"current_price": current_price,
|
||||
"distance_pct": round(distance_pct * 100, 4),
|
||||
"threshold_pct": round(threshold * 100, 4),
|
||||
"distance": round(current_price - pivot.price, 2),
|
||||
"alert_type": alert_type,
|
||||
"trend": trend
|
||||
})
|
||||
|
||||
near_pivots.sort(key=lambda x: x['distance_pct'])
|
||||
return near_pivots
|
||||
|
||||
def get_trend_direction(self, symbol: str, period: str = None) -> Dict[str, str]:
|
||||
"""
|
||||
根据最近的转折点判断趋势方向
|
||||
|
||||
Returns:
|
||||
{period: "up"/"down"/"unknown"}
|
||||
"""
|
||||
result = {}
|
||||
|
||||
periods_to_check = [period] if period else self.pivot_store.get_all_periods(symbol)
|
||||
|
||||
for p in periods_to_check:
|
||||
timeline = self.pivot_store.get_timeline(symbol, p)
|
||||
|
||||
if not timeline:
|
||||
result[p] = 'unknown'
|
||||
continue
|
||||
|
||||
latest_pivot = timeline[-1]
|
||||
|
||||
if latest_pivot.direction == 'high':
|
||||
result[p] = 'down'
|
||||
else:
|
||||
result[p] = 'up'
|
||||
|
||||
return result
|
||||
|
||||
def get_pivots(self, symbol: str, period: str, direction: str = None,
|
||||
count: int = 50) -> List[Dict]:
|
||||
"""获取转折点数据"""
|
||||
return self.pivot_store.get_pivots(symbol, period, direction, count)
|
||||
|
||||
def find_nearest_pivot_price(self, symbol: str, direction: str,
|
||||
current_price: float) -> Optional[float]:
|
||||
"""找到离当前价格最近的转折点价格"""
|
||||
return self.pivot_store.find_nearest_pivot_price(symbol, direction, current_price)
|
||||
|
||||
def get_threshold(self, period: str) -> float:
|
||||
"""获取某个周期的接近阈值"""
|
||||
return self.THRESHOLDS.get(period, 0.001)
|
||||
|
||||
def get_strength(self, period: str) -> int:
|
||||
"""获取某个周期的转折强度"""
|
||||
return self.PERIOD_STRENGTH.get(period, self.default_strength)
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取状态"""
|
||||
return self.pivot_store.get_status()
|
||||
|
||||
def clear_symbol(self, symbol: str):
|
||||
"""清除某个Symbol的转折点数据"""
|
||||
self.pivot_store.clear_symbol(symbol)
|
||||
|
||||
def _normalize_timestamp(self, ts) -> str:
|
||||
"""标准化时间戳"""
|
||||
if isinstance(ts, datetime):
|
||||
return ts.strftime("%Y-%m-%d %H:%M:%S")
|
||||
return str(ts)
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
持仓数据服务模块
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
from ..models.position import PositionData
|
||||
from ..store.position_store import PositionStore
|
||||
|
||||
|
||||
class PositionService:
|
||||
"""
|
||||
持仓数据服务
|
||||
|
||||
功能:
|
||||
1. 处理EA上报的持仓数据
|
||||
2. 查询持仓信息
|
||||
3. 为风险管理提供持仓数据
|
||||
"""
|
||||
|
||||
def __init__(self, store: PositionStore = None):
|
||||
self.store = store or PositionStore()
|
||||
|
||||
def update_positions(self, symbol: str, positions_data: List[Dict]) -> Dict:
|
||||
"""
|
||||
更新持仓数据
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
positions_data: EA上报的持仓数据列表
|
||||
|
||||
Returns:
|
||||
{"status": "ok", "count": N, "closed": M}
|
||||
"""
|
||||
positions = [
|
||||
PositionData.from_ea_data(data, symbol)
|
||||
for data in positions_data
|
||||
]
|
||||
return self.store.update(symbol, positions)
|
||||
|
||||
def get_positions(self, symbol: str = None) -> List[Dict]:
|
||||
"""获取持仓数据(字典格式)"""
|
||||
return self.store.get_dict(symbol)
|
||||
|
||||
def get_position_objects(self, symbol: str = None) -> List[PositionData]:
|
||||
"""获取持仓数据(对象格式)"""
|
||||
return self.store.get(symbol)
|
||||
|
||||
def get_position(self, symbol: str, ticket: int) -> Optional[Dict]:
|
||||
"""获取单个持仓"""
|
||||
pos = self.store.get_by_ticket(symbol, ticket)
|
||||
return pos.to_dict() if pos else None
|
||||
|
||||
def get_position_count(self, symbol: str) -> int:
|
||||
"""获取持仓数量"""
|
||||
return self.store.get_count(symbol)
|
||||
|
||||
def get_same_direction_count(self, symbol: str, direction: str) -> int:
|
||||
"""获取同向持仓数量"""
|
||||
return self.store.get_count_by_direction(symbol, direction)
|
||||
|
||||
def get_opposite_direction_count(self, symbol: str, direction: str) -> int:
|
||||
"""获取反向持仓数量"""
|
||||
opposite = "sell" if direction.lower() == "buy" else "buy"
|
||||
return self.store.get_count_by_direction(symbol, opposite)
|
||||
|
||||
def get_summary(self, symbol: str = None) -> Dict:
|
||||
"""获取持仓汇总"""
|
||||
return self.store.get_summary(symbol)
|
||||
|
||||
def get_symbols(self) -> List[str]:
|
||||
"""获取所有有持仓的品种"""
|
||||
return self.store.get_symbols()
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取服务状态"""
|
||||
return self.store.get_status()
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
信号生成器模块
|
||||
"""
|
||||
|
||||
from .signal_service import SignalService
|
||||
from .pivot_signal import PivotSignalGenerator
|
||||
from .key_level_signal import KeyLevelSignalGenerator
|
||||
from .ai_entry_signal import AIEntrySignalGenerator
|
||||
|
||||
__all__ = [
|
||||
'SignalService',
|
||||
'PivotSignalGenerator',
|
||||
'KeyLevelSignalGenerator',
|
||||
'AIEntrySignalGenerator',
|
||||
]
|
||||
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
AI入场信号生成器
|
||||
根据AI分析生成交易信号
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
from ...models import TradingSignal, SignalSource
|
||||
|
||||
|
||||
class AIEntrySignalGenerator:
|
||||
"""AI入场信号生成器"""
|
||||
|
||||
def __init__(self):
|
||||
# LLM分析器引用
|
||||
self._llm_analyzer = None
|
||||
|
||||
# 阈值(价格距离AI入场价的百分比)
|
||||
self.threshold = 0.0001 # 万分之一
|
||||
|
||||
# 信号冷却时间(秒)
|
||||
self.cooldown = 300 # 5分钟
|
||||
|
||||
# 冷却记录
|
||||
self._signal_cooldowns: Dict[str, datetime] = {}
|
||||
|
||||
print("[AIEntrySignalGenerator] AI入场信号生成器已初始化")
|
||||
|
||||
def set_llm_analyzer(self, analyzer) -> None:
|
||||
"""设置LLM分析器"""
|
||||
self._llm_analyzer = analyzer
|
||||
|
||||
def _check_cooldown(self, symbol: str, period: str, entry_price: float, direction: str) -> bool:
|
||||
"""检查是否在冷却期"""
|
||||
key = f"{symbol}_{period}_{entry_price}_{direction}"
|
||||
if key in self._signal_cooldowns:
|
||||
last_time = self._signal_cooldowns[key]
|
||||
elapsed = (datetime.now() - last_time).total_seconds()
|
||||
return elapsed < self.cooldown
|
||||
return False
|
||||
|
||||
def _set_cooldown(self, symbol: str, period: str, entry_price: float, direction: str) -> None:
|
||||
"""设置冷却"""
|
||||
key = f"{symbol}_{period}_{entry_price}_{direction}"
|
||||
self._signal_cooldowns[key] = datetime.now()
|
||||
|
||||
def generate_signal(self, symbol: str, current_price: float) -> Optional[TradingSignal]:
|
||||
"""
|
||||
生成AI入场信号
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
current_price: 当前价格
|
||||
|
||||
Returns:
|
||||
TradingSignal 或 None
|
||||
"""
|
||||
if not self._llm_analyzer:
|
||||
return None
|
||||
|
||||
# 检查价格是否接近AI入场价
|
||||
matches = self._llm_analyzer.check_entry_price_nearby(
|
||||
symbol, current_price, threshold=self.threshold
|
||||
)
|
||||
|
||||
if not matches:
|
||||
return None
|
||||
|
||||
# 使用第一个匹配
|
||||
match = matches[0]
|
||||
period = match.get('period', '')
|
||||
entry_price = match.get('entry_price', 0)
|
||||
direction = match.get('direction', 'buy')
|
||||
sl = match.get('stop_loss', 0)
|
||||
tp = match.get('take_profit', 0)
|
||||
reason = match.get('reason', '')
|
||||
|
||||
# 检查冷却
|
||||
if self._check_cooldown(symbol, period, entry_price, direction):
|
||||
return None
|
||||
|
||||
# 设置冷却
|
||||
self._set_cooldown(symbol, period, entry_price, direction)
|
||||
|
||||
# 确定方向
|
||||
action = "buy" if direction == "buy" else "sell"
|
||||
|
||||
# 验证止损止盈
|
||||
if not sl or not tp or sl <= 0 or tp <= 0:
|
||||
print(f"[AIEntrySignalGenerator] 跳过无效信号: sl={sl}, tp={tp}")
|
||||
return None
|
||||
|
||||
# 验证止损方向
|
||||
if action == "buy" and sl >= current_price:
|
||||
print(f"[AIEntrySignalGenerator] 买入止损无效: sl={sl} >= price={current_price}")
|
||||
return None
|
||||
if action == "sell" and sl <= current_price:
|
||||
print(f"[AIEntrySignalGenerator] 卖出止损无效: sl={sl} <= price={current_price}")
|
||||
return None
|
||||
|
||||
# 计算风险回报比
|
||||
risk = abs(current_price - sl) if sl else 0
|
||||
reward = abs(tp - current_price) if tp else 0
|
||||
rr_ratio = reward / risk if risk > 0 else 0
|
||||
|
||||
# 验证风险回报比
|
||||
if rr_ratio < 1.0:
|
||||
print(f"[AIEntrySignalGenerator] 风险回报比过低: {rr_ratio:.2f}, 跳过信号")
|
||||
return None
|
||||
|
||||
# 验证止损点数(最大为价格的 2%)
|
||||
max_risk = current_price * 0.02
|
||||
if risk > max_risk:
|
||||
print(f"[AIEntrySignalGenerator] 止损点数过大: {risk:.2f} > {max_risk:.2f}, 跳过信号")
|
||||
return None
|
||||
|
||||
print(f"[AIEntrySignalGenerator] 生成信号: {action} @ {current_price:.2f}, SL={sl:.2f}, TP={tp:.2f}, risk={risk:.2f}, rr={rr_ratio:.2f}")
|
||||
|
||||
# 创建信号
|
||||
signal = TradingSignal(
|
||||
symbol=symbol,
|
||||
action=action,
|
||||
confidence=75, # AI信号置信度较高
|
||||
source=SignalSource.AI_ENTRY,
|
||||
source_period=period,
|
||||
trigger_price=current_price,
|
||||
trigger_reason=f"AI建议入场: {reason}",
|
||||
suggested_entry=current_price,
|
||||
suggested_sl=sl,
|
||||
suggested_tp=tp,
|
||||
risk_reward_ratio=round(rr_ratio, 2),
|
||||
ai_analysis_period=period,
|
||||
)
|
||||
|
||||
print(f"[AIEntrySignalGenerator] 生成信号: {signal.signal_id} {action} @ {current_price}, AI入场价={entry_price}")
|
||||
return signal
|
||||
|
||||
def generate_signals(self, symbol: str, current_price: float) -> List[TradingSignal]:
|
||||
"""生成所有匹配的信号"""
|
||||
if not self._llm_analyzer:
|
||||
return []
|
||||
|
||||
signals = []
|
||||
matches = self._llm_analyzer.check_entry_price_nearby(
|
||||
symbol, current_price, threshold=self.threshold
|
||||
)
|
||||
|
||||
for match in matches:
|
||||
period = match.get('period', '')
|
||||
entry_price = match.get('entry_price', 0)
|
||||
direction = match.get('direction', 'buy')
|
||||
sl = match.get('stop_loss', 0)
|
||||
tp = match.get('take_profit', 0)
|
||||
reason = match.get('reason', '')
|
||||
|
||||
# 检查冷却
|
||||
if self._check_cooldown(symbol, period, entry_price, direction):
|
||||
continue
|
||||
|
||||
# 设置冷却
|
||||
self._set_cooldown(symbol, period, entry_price, direction)
|
||||
|
||||
action = "buy" if direction == "buy" else "sell"
|
||||
|
||||
# 验证止损止盈
|
||||
if not sl or not tp or sl <= 0 or tp <= 0:
|
||||
print(f"[AIEntrySignalGenerator] 跳过无效信号: sl={sl}, tp={tp}")
|
||||
continue
|
||||
|
||||
# 验证止损方向
|
||||
if action == "buy" and sl >= current_price:
|
||||
continue
|
||||
if action == "sell" and sl <= current_price:
|
||||
continue
|
||||
|
||||
risk = abs(current_price - sl) if sl else 0
|
||||
reward = abs(tp - current_price) if tp else 0
|
||||
rr_ratio = reward / risk if risk > 0 else 0
|
||||
|
||||
# 验证风险回报比
|
||||
if rr_ratio < 1.0:
|
||||
continue
|
||||
|
||||
# 验证止损点数(最大为价格的 2%)
|
||||
max_risk = current_price * 0.02
|
||||
if risk > max_risk:
|
||||
continue
|
||||
|
||||
signal = TradingSignal(
|
||||
symbol=symbol,
|
||||
action=action,
|
||||
confidence=75,
|
||||
source=SignalSource.AI_ENTRY,
|
||||
source_period=period,
|
||||
trigger_price=current_price,
|
||||
trigger_reason=f"AI建议入场: {reason}",
|
||||
suggested_entry=current_price,
|
||||
suggested_sl=sl,
|
||||
suggested_tp=tp,
|
||||
risk_reward_ratio=round(rr_ratio, 2),
|
||||
ai_analysis_period=period,
|
||||
)
|
||||
signals.append(signal)
|
||||
|
||||
return signals
|
||||
|
||||
def __call__(self, symbol: str, current_price: float) -> List[TradingSignal]:
|
||||
"""使对象可调用"""
|
||||
return self.generate_signals(symbol, current_price)
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
关键点位信号生成器
|
||||
根据关键点位分析生成交易信号
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
from ...models import TradingSignal, SignalSource
|
||||
|
||||
|
||||
class KeyLevelSignalGenerator:
|
||||
"""关键点位信号生成器"""
|
||||
|
||||
def __init__(self):
|
||||
# 关键点位配置
|
||||
self._key_levels: Dict[str, List[float]] = {}
|
||||
|
||||
# 阈值(价格距离关键点位的百分比)
|
||||
self.threshold = 0.0008 # 万分之八
|
||||
|
||||
# 信号冷却时间(秒)
|
||||
self.cooldown = 180
|
||||
|
||||
# 冷却记录
|
||||
self._signal_cooldowns: Dict[str, datetime] = {}
|
||||
|
||||
print("[KeyLevelSignalGenerator] 关键点位信号生成器已初始化")
|
||||
|
||||
def set_key_levels(self, symbol: str, levels: List[float]) -> None:
|
||||
"""设置品种的关键点位"""
|
||||
self._key_levels[symbol] = sorted(levels)
|
||||
|
||||
def get_key_levels(self, symbol: str, current_price: float) -> List[float]:
|
||||
"""获取关键点位(如果没有配置则自动计算)"""
|
||||
if symbol in self._key_levels:
|
||||
return self._key_levels[symbol]
|
||||
|
||||
# 自动计算关键点位
|
||||
return self._auto_calculate_key_levels(current_price)
|
||||
|
||||
def _auto_calculate_key_levels(self, current_price: float) -> List[float]:
|
||||
"""自动计算关键点位"""
|
||||
if current_price <= 0:
|
||||
return []
|
||||
|
||||
int_part = int(current_price)
|
||||
num_digits = len(str(int_part)) if int_part > 0 else 1
|
||||
|
||||
# 根据位数确定步长
|
||||
if num_digits == 1:
|
||||
step = 1
|
||||
elif num_digits == 2:
|
||||
step = 5
|
||||
elif num_digits == 3:
|
||||
step = 10
|
||||
elif num_digits == 4:
|
||||
step = 100
|
||||
else:
|
||||
step = 1000
|
||||
|
||||
# 计算基础点位
|
||||
base_level = int(current_price / step) * step
|
||||
|
||||
# 生成上下各3个关键点位
|
||||
levels = []
|
||||
for i in range(-3, 4):
|
||||
level = base_level + i * step
|
||||
if level > 0:
|
||||
levels.append(float(level))
|
||||
|
||||
return sorted(levels)
|
||||
|
||||
def _check_cooldown(self, symbol: str, key_level: float) -> bool:
|
||||
"""检查是否在冷却期"""
|
||||
key = f"{symbol}_{key_level}"
|
||||
if key in self._signal_cooldowns:
|
||||
last_time = self._signal_cooldowns[key]
|
||||
elapsed = (datetime.now() - last_time).total_seconds()
|
||||
return elapsed < self.cooldown
|
||||
return False
|
||||
|
||||
def _set_cooldown(self, symbol: str, key_level: float) -> None:
|
||||
"""设置冷却"""
|
||||
key = f"{symbol}_{key_level}"
|
||||
self._signal_cooldowns[key] = datetime.now()
|
||||
|
||||
def generate_signal(self, symbol: str, current_price: float) -> Optional[TradingSignal]:
|
||||
"""
|
||||
生成关键点位信号
|
||||
|
||||
策略逻辑:
|
||||
- 价格在关键点位上方,向下接近 → 买入(支撑位)
|
||||
- 价格在关键点位下方,向上接近 → 卖出(压力位)
|
||||
"""
|
||||
key_levels = self.get_key_levels(symbol, current_price)
|
||||
if not key_levels:
|
||||
return None
|
||||
|
||||
# 找到最近的关键点位
|
||||
nearest_level = None
|
||||
min_distance_pct = float('inf')
|
||||
|
||||
for level in key_levels:
|
||||
distance_pct = abs(current_price - level) / current_price
|
||||
if distance_pct < min_distance_pct:
|
||||
min_distance_pct = distance_pct
|
||||
nearest_level = level
|
||||
|
||||
if nearest_level is None:
|
||||
return None
|
||||
|
||||
# 检查是否在阈值范围内
|
||||
if min_distance_pct > self.threshold:
|
||||
return None
|
||||
|
||||
# 检查冷却
|
||||
if self._check_cooldown(symbol, nearest_level):
|
||||
return None
|
||||
|
||||
# 设置冷却
|
||||
self._set_cooldown(symbol, nearest_level)
|
||||
|
||||
# 确定方向
|
||||
if current_price > nearest_level:
|
||||
# 价格在关键点位上方 → 支撑位 → 买入
|
||||
action = "buy"
|
||||
sl = nearest_level - (nearest_level * 0.006) # 关键点位下方万分之六
|
||||
risk = current_price - sl
|
||||
tp = current_price + risk * 1.5
|
||||
trigger_reason = f"价格向下接近 {nearest_level}(支撑位)"
|
||||
else:
|
||||
# 价格在关键点位下方 → 压力位 → 卖出
|
||||
action = "sell"
|
||||
sl = nearest_level + (nearest_level * 0.006)
|
||||
risk = sl - current_price
|
||||
tp = current_price - risk * 1.5
|
||||
trigger_reason = f"价格向上接近 {nearest_level}(压力位)"
|
||||
|
||||
# 计算风险回报比
|
||||
reward = abs(tp - current_price)
|
||||
rr_ratio = reward / risk if risk > 0 else 0
|
||||
|
||||
print(f"[KeyLevelSignalGenerator] 生成信号: {action} @ {current_price:.2f}, key_level={nearest_level:.2f}, sl={sl:.2f}, tp={tp:.2f}, risk={risk:.2f}, rr={rr_ratio:.2f}")
|
||||
|
||||
# 创建信号
|
||||
signal = TradingSignal(
|
||||
symbol=symbol,
|
||||
action=action,
|
||||
confidence=65, # 基础置信度
|
||||
source=SignalSource.KEY_LEVEL,
|
||||
source_period="", # 关键点位不区分周期
|
||||
trigger_price=current_price,
|
||||
trigger_reason=trigger_reason,
|
||||
suggested_entry=current_price,
|
||||
suggested_sl=round(sl, 2),
|
||||
suggested_tp=round(tp, 2),
|
||||
risk_reward_ratio=round(rr_ratio, 2),
|
||||
key_level=nearest_level,
|
||||
distance_pct=round(min_distance_pct * 100, 4),
|
||||
)
|
||||
|
||||
print(f"[KeyLevelSignalGenerator] 生成信号: {signal.signal_id} {action} @ {current_price}, 关键位={nearest_level}")
|
||||
return signal
|
||||
|
||||
def __call__(self, symbol: str, current_price: float) -> Optional[TradingSignal]:
|
||||
"""使对象可调用"""
|
||||
signal = self.generate_signal(symbol, current_price)
|
||||
return signal if signal else None
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
转折点信号生成器
|
||||
根据转折点分析生成交易信号
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
from ...models import TradingSignal, SignalSource
|
||||
from ...store import PivotStore, KlineStore
|
||||
from ...services import PivotService
|
||||
|
||||
|
||||
class PivotSignalGenerator:
|
||||
"""转折点信号生成器"""
|
||||
|
||||
def __init__(self, pivot_service: PivotService = None,
|
||||
pivot_store: PivotStore = None,
|
||||
kline_store: KlineStore = None):
|
||||
self.pivot_service = pivot_service
|
||||
self.pivot_store = pivot_store or PivotStore()
|
||||
self.kline_store = kline_store or KlineStore()
|
||||
|
||||
# 信号冷却时间(秒)
|
||||
self.cooldown = 180
|
||||
|
||||
# 已生成的信号冷却记录
|
||||
self._signal_cooldowns: Dict[str, datetime] = {}
|
||||
|
||||
print("[PivotSignalGenerator] 转折点信号生成器已初始化")
|
||||
|
||||
def set_pivot_service(self, service: PivotService) -> None:
|
||||
"""设置转折点服务"""
|
||||
self.pivot_service = service
|
||||
|
||||
def _check_cooldown(self, symbol: str, period: str, pivot_price: float) -> bool:
|
||||
"""检查是否在冷却期"""
|
||||
key = f"{symbol}_{period}_{pivot_price}"
|
||||
if key in self._signal_cooldowns:
|
||||
last_time = self._signal_cooldowns[key]
|
||||
elapsed = (datetime.now() - last_time).total_seconds()
|
||||
return elapsed < self.cooldown
|
||||
return False
|
||||
|
||||
def _set_cooldown(self, symbol: str, period: str, pivot_price: float) -> None:
|
||||
"""设置冷却"""
|
||||
key = f"{symbol}_{period}_{pivot_price}"
|
||||
self._signal_cooldowns[key] = datetime.now()
|
||||
|
||||
def generate_signal(self, symbol: str, current_price: float,
|
||||
period: str = "M1") -> Optional[TradingSignal]:
|
||||
"""
|
||||
生成转折点信号
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
current_price: 当前价格
|
||||
period: 检测周期
|
||||
|
||||
Returns:
|
||||
TradingSignal 或 None
|
||||
"""
|
||||
if not self.pivot_service:
|
||||
return None
|
||||
|
||||
# 检查是否接近转折点
|
||||
near_pivots = self.pivot_service.check_near_pivot(symbol, current_price)
|
||||
|
||||
for pivot in near_pivots:
|
||||
# 只处理指定周期
|
||||
if pivot.get('period') != period:
|
||||
continue
|
||||
|
||||
# 只处理接近类型(不是突破)
|
||||
alert_type = pivot.get('alert_type', '')
|
||||
if not alert_type.startswith('near_'):
|
||||
continue
|
||||
|
||||
pivot_price = pivot.get('price', 0)
|
||||
pivot_type = 'low' if 'low' in alert_type else 'high'
|
||||
|
||||
# 检查冷却
|
||||
if self._check_cooldown(symbol, period, pivot_price):
|
||||
continue
|
||||
|
||||
# 设置冷却
|
||||
self._set_cooldown(symbol, period, pivot_price)
|
||||
|
||||
# 确定方向
|
||||
if pivot_type == 'low':
|
||||
action = "buy"
|
||||
# 止损 = 低点 - 固定偏移
|
||||
sl_offset = 10.0 # TODO: 从配置获取
|
||||
sl = pivot_price - sl_offset
|
||||
# 止盈 = 最近的高点
|
||||
tp = self.pivot_service.find_nearest_pivot_price(symbol, 'high', current_price)
|
||||
print(f"[PivotSignalGenerator] 买入信号: pivot_price={pivot_price:.2f}, sl={sl:.2f}, tp={tp}")
|
||||
else:
|
||||
action = "sell"
|
||||
sl_offset = 10.0
|
||||
sl = pivot_price + sl_offset
|
||||
tp = self.pivot_service.find_nearest_pivot_price(symbol, 'low', current_price)
|
||||
print(f"[PivotSignalGenerator] 卖出信号: pivot_price={pivot_price:.2f}, sl={sl:.2f}, tp={tp}")
|
||||
|
||||
# 如果没有找到反向转折点,或者止盈太近,使用风险回报比
|
||||
risk = abs(current_price - sl)
|
||||
min_reward = risk * 1.5 # 最小回报 = 1.5 倍风险
|
||||
|
||||
if tp is None:
|
||||
if action == "buy":
|
||||
tp = current_price + min_reward
|
||||
else:
|
||||
tp = current_price - min_reward
|
||||
print(f"[PivotSignalGenerator] 未找到反向转折点,使用风险回报比: tp={tp:.2f}, risk={risk:.2f}")
|
||||
else:
|
||||
# 检查止盈是否太近
|
||||
reward = abs(tp - current_price)
|
||||
if reward < min_reward:
|
||||
print(f"[PivotSignalGenerator] 止盈太近: reward={reward:.2f} < min_reward={min_reward:.2f}, 使用风险回报比")
|
||||
if action == "buy":
|
||||
tp = current_price + min_reward
|
||||
else:
|
||||
tp = current_price - min_reward
|
||||
|
||||
# 计算风险回报比
|
||||
risk = abs(current_price - sl)
|
||||
reward = abs(tp - current_price)
|
||||
rr_ratio = reward / risk if risk > 0 else 0
|
||||
|
||||
# 止损点数验证 - 如果止损点数太大,跳过这个 pivot
|
||||
risk_points = abs(current_price - sl)
|
||||
max_allowed_risk = current_price * 0.02 # 最大风险 = 当前价格的 2%
|
||||
if risk_points > max_allowed_risk:
|
||||
print(f"[PivotSignalGenerator] 止损点数过大: {risk_points:.2f} > {max_allowed_risk:.2f}, 跳过信号")
|
||||
continue
|
||||
|
||||
# 止损止盈验证
|
||||
if sl <= 0 or tp <= 0:
|
||||
print(f"[PivotSignalGenerator] 无效的止损止盈: sl={sl}, tp={tp}, 跳过信号")
|
||||
continue
|
||||
|
||||
# 止损方向验证
|
||||
if action == "buy" and sl >= current_price:
|
||||
print(f"[PivotSignalGenerator] 买入止损无效: sl={sl} >= price={current_price}")
|
||||
continue
|
||||
if action == "sell" and sl <= current_price:
|
||||
print(f"[PivotSignalGenerator] 卖出止损无效: sl={sl} <= price={current_price}")
|
||||
continue
|
||||
|
||||
# 创建信号
|
||||
signal = TradingSignal(
|
||||
symbol=symbol,
|
||||
action=action,
|
||||
confidence=60, # 基础置信度
|
||||
source=SignalSource.PIVOT,
|
||||
source_period=period,
|
||||
trigger_price=current_price,
|
||||
trigger_reason=f"{period}接近{pivot_type}点 {pivot_price:.2f}",
|
||||
suggested_entry=current_price,
|
||||
suggested_sl=sl,
|
||||
suggested_tp=tp,
|
||||
risk_reward_ratio=round(rr_ratio, 2),
|
||||
pivot_price=pivot_price,
|
||||
pivot_type=pivot_type,
|
||||
)
|
||||
|
||||
print(f"[PivotSignalGenerator] 生成信号: {signal.signal_id} {action} @ {current_price}, SL={sl:.2f}, TP={tp:.2f}")
|
||||
return signal
|
||||
|
||||
return None
|
||||
|
||||
def generate_signals(self, symbol: str, current_price: float) -> List[TradingSignal]:
|
||||
"""生成所有周期的信号"""
|
||||
signals = []
|
||||
for period in ['M1', 'M5']:
|
||||
signal = self.generate_signal(symbol, current_price, period)
|
||||
if signal:
|
||||
signals.append(signal)
|
||||
return signals
|
||||
|
||||
def __call__(self, symbol: str, current_price: float) -> List[TradingSignal]:
|
||||
"""使对象可调用,用于注册到SignalService"""
|
||||
return self.generate_signals(symbol, current_price)
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
信号服务模块
|
||||
统一管理信号的生成、存储和查询
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
import threading
|
||||
|
||||
from ...models import TradingSignal, SignalSource
|
||||
from ...store import SignalStore
|
||||
|
||||
|
||||
class SignalService:
|
||||
"""信号服务(统一管理信号)"""
|
||||
|
||||
def __init__(self, signal_store: SignalStore = None):
|
||||
self.store = signal_store or SignalStore()
|
||||
|
||||
# 信号生成器(注册后使用)
|
||||
self._generators: Dict[str, callable] = {}
|
||||
|
||||
# 冷却管理(避免重复生成)
|
||||
self._cooldowns: Dict[str, datetime] = {}
|
||||
self._cooldown_lock = threading.Lock()
|
||||
|
||||
# 默认冷却时间(秒)
|
||||
self.default_cooldown = 180 # 3分钟
|
||||
|
||||
# 启动清理线程
|
||||
self._start_cleanup_thread()
|
||||
|
||||
print("[SignalService] 信号服务已初始化")
|
||||
|
||||
def _start_cleanup_thread(self):
|
||||
"""启动清理线程"""
|
||||
def cleanup_loop():
|
||||
while True:
|
||||
try:
|
||||
self.store.cleanup_expired()
|
||||
self._cleanup_cooldowns()
|
||||
except Exception as e:
|
||||
print(f"[SignalService] 清理线程异常: {e}")
|
||||
threading.Event().wait(30)
|
||||
|
||||
thread = threading.Thread(target=cleanup_loop, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def _cleanup_cooldowns(self):
|
||||
"""清理过期的冷却记录"""
|
||||
current_time = datetime.now()
|
||||
with self._cooldown_lock:
|
||||
keys_to_remove = []
|
||||
for key, last_time in self._cooldowns.items():
|
||||
elapsed = (current_time - last_time).total_seconds()
|
||||
if elapsed > self.default_cooldown * 2:
|
||||
keys_to_remove.append(key)
|
||||
for key in keys_to_remove:
|
||||
del self._cooldowns[key]
|
||||
|
||||
def _check_cooldown(self, key: str) -> bool:
|
||||
"""检查是否在冷却期内"""
|
||||
with self._cooldown_lock:
|
||||
if key in self._cooldowns:
|
||||
last_time = self._cooldowns[key]
|
||||
elapsed = (datetime.now() - last_time).total_seconds()
|
||||
return elapsed < self.default_cooldown
|
||||
return False
|
||||
|
||||
def _set_cooldown(self, key: str) -> None:
|
||||
"""设置冷却"""
|
||||
with self._cooldown_lock:
|
||||
self._cooldowns[key] = datetime.now()
|
||||
|
||||
# ==================== 信号生成器注册 ====================
|
||||
|
||||
def register_generator(self, source: str, generator: callable) -> None:
|
||||
"""注册信号生成器"""
|
||||
self._generators[source] = generator
|
||||
print(f"[SignalService] 注册信号生成器: {source}")
|
||||
|
||||
# ==================== 信号生成 ====================
|
||||
|
||||
def generate_signals(self, symbol: str, current_price: float) -> List[TradingSignal]:
|
||||
"""
|
||||
生成信号(调用所有注册的生成器)
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
current_price: 当前价格
|
||||
|
||||
Returns:
|
||||
生成的信号列表
|
||||
"""
|
||||
signals = []
|
||||
|
||||
for source, generator in self._generators.items():
|
||||
try:
|
||||
generated = generator(symbol, current_price)
|
||||
if generated:
|
||||
if isinstance(generated, list):
|
||||
for signal in generated:
|
||||
if isinstance(signal, TradingSignal):
|
||||
signals.append(signal)
|
||||
elif isinstance(generated, TradingSignal):
|
||||
signals.append(generated)
|
||||
except Exception as e:
|
||||
print(f"[SignalService] 信号生成器 {source} 异常: {e}")
|
||||
|
||||
# 存储信号
|
||||
for signal in signals:
|
||||
self.store.add_signal(signal)
|
||||
|
||||
return signals
|
||||
|
||||
def add_signal(self, signal: TradingSignal) -> str:
|
||||
"""添加信号"""
|
||||
return self.store.add_signal(signal)
|
||||
|
||||
# ==================== 信号查询 ====================
|
||||
|
||||
def get_signal(self, signal_id: str) -> Optional[TradingSignal]:
|
||||
"""获取信号"""
|
||||
return self.store.get_signal_by_id(signal_id)
|
||||
|
||||
def get_active_signals(self, symbol: str = None) -> List[TradingSignal]:
|
||||
"""获取活跃信号"""
|
||||
return self.store.get_active_signals(symbol)
|
||||
|
||||
def get_active_signals_by_source(self, symbol: str, source: str) -> List[TradingSignal]:
|
||||
"""获取指定来源的活跃信号"""
|
||||
return self.store.get_active_signals_by_source(symbol, source)
|
||||
|
||||
def get_signals_dict(self, symbol: str = None) -> List[Dict]:
|
||||
"""获取信号字典列表"""
|
||||
return self.store.get_signals_dict(symbol)
|
||||
|
||||
def get_signal_count(self, symbol: str = None, source: str = None) -> int:
|
||||
"""获取信号数量"""
|
||||
return self.store.get_signal_count(symbol, source)
|
||||
|
||||
# ==================== 信号消费 ====================
|
||||
|
||||
def consume_signal(self, signal_id: str) -> Optional[TradingSignal]:
|
||||
"""
|
||||
消费信号(标记为已使用)
|
||||
|
||||
Args:
|
||||
signal_id: 信号ID
|
||||
|
||||
Returns:
|
||||
信号对象
|
||||
"""
|
||||
signal = self.store.get_signal_by_id(signal_id)
|
||||
if signal and signal.is_active():
|
||||
self.store.mark_signal_used(signal_id)
|
||||
return signal
|
||||
return None
|
||||
|
||||
def consume_signals_for_decision(self, symbol: str) -> List[TradingSignal]:
|
||||
"""
|
||||
消费品种的所有活跃信号(用于决策)
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
|
||||
Returns:
|
||||
信号列表
|
||||
"""
|
||||
signals = self.store.get_active_signals(symbol)
|
||||
for signal in signals:
|
||||
self.store.mark_signal_used(signal.signal_id)
|
||||
return signals
|
||||
|
||||
# ==================== 统计 ====================
|
||||
|
||||
def get_signal_stats(self, symbol: str) -> Dict:
|
||||
"""获取信号统计"""
|
||||
return self.store.get_signal_stats(symbol)
|
||||
|
||||
# ==================== 状态 ====================
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取服务状态"""
|
||||
return {
|
||||
"store": self.store.get_status(),
|
||||
"generators": list(self._generators.keys()),
|
||||
}
|
||||
|
||||
def clear_all(self) -> int:
|
||||
"""清空所有信号"""
|
||||
return self.store.clear_all()
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
统计数据服务模块
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
from ..models.statistics import StatisticsData
|
||||
from ..store.statistics_store import StatisticsStore
|
||||
|
||||
|
||||
class StatisticsService:
|
||||
"""
|
||||
统计数据服务
|
||||
|
||||
功能:
|
||||
1. 处理EA上报的统计数据
|
||||
2. 获取品种价差
|
||||
3. 获取账户信息
|
||||
"""
|
||||
|
||||
def __init__(self, store: StatisticsStore = None):
|
||||
self.store = store or StatisticsStore()
|
||||
|
||||
def process_statistics(self, data: Dict) -> None:
|
||||
"""
|
||||
处理EA上报的统计数据
|
||||
|
||||
Args:
|
||||
data: EA上报的JSON数据
|
||||
"""
|
||||
stat = StatisticsData.from_ea_data(data)
|
||||
self.store.add(stat)
|
||||
|
||||
def get_latest(self, symbol: str = None) -> Optional[StatisticsData]:
|
||||
"""获取最新统计数据"""
|
||||
return self.store.get_latest(symbol)
|
||||
|
||||
def get_spread(self, symbol: str) -> Optional[float]:
|
||||
"""获取品种价差"""
|
||||
return self.store.get_spread(symbol)
|
||||
|
||||
def get_mid_price(self, symbol: str) -> Optional[float]:
|
||||
"""获取品种中间价"""
|
||||
latest = self.store.get_latest(symbol)
|
||||
if latest:
|
||||
return latest.mid_price
|
||||
return None
|
||||
|
||||
def get_account_info(self, symbol: str = None) -> Dict:
|
||||
"""获取账户信息"""
|
||||
return self.store.get_account_info(symbol)
|
||||
|
||||
def get_by_symbol(self, symbol: str, count: int = 10) -> List[StatisticsData]:
|
||||
"""获取指定品种的统计数据"""
|
||||
return self.store.get_by_symbol(symbol, count)
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取服务状态"""
|
||||
return self.store.get_status()
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
策略服务模块
|
||||
"""
|
||||
|
||||
from .strategy_service import StrategyService
|
||||
from .risk_manager import RiskManager
|
||||
|
||||
__all__ = [
|
||||
'StrategyService',
|
||||
'RiskManager',
|
||||
]
|
||||
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
风险管理服务
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ...models import TradingStrategy
|
||||
|
||||
|
||||
class RiskManager:
|
||||
"""风险管理服务"""
|
||||
|
||||
def __init__(self):
|
||||
# 账户信息(从外部更新)
|
||||
self._account_balance: float = 0.0
|
||||
self._account_equity: float = 0.0
|
||||
self._free_margin: float = 0.0
|
||||
|
||||
# 每日风险限制
|
||||
self._daily_risk_limit: float = 5.0 # 每日最大风险百分比
|
||||
self._daily_risk_used: float = 0.0 # 今日已使用风险
|
||||
|
||||
# 品种配置(点值、最小手数等)
|
||||
self._symbol_config: Dict[str, Dict] = {}
|
||||
|
||||
# 统计服务引用(用于获取账户信息)
|
||||
self._statistics_service = None
|
||||
|
||||
print("[RiskManager] 风险管理服务已初始化")
|
||||
|
||||
def set_statistics_service(self, service) -> None:
|
||||
"""设置统计服务引用"""
|
||||
self._statistics_service = service
|
||||
|
||||
def _refresh_account_info(self) -> None:
|
||||
"""从统计服务刷新账户信息"""
|
||||
if not self._statistics_service:
|
||||
return
|
||||
|
||||
try:
|
||||
account_info = self._statistics_service.get_account_info()
|
||||
if account_info:
|
||||
self._account_balance = account_info.get('balance', 0.0)
|
||||
self._account_equity = account_info.get('equity', 0.0)
|
||||
# free_margin 通常等于 equity - used_margin,这里用 equity 近似
|
||||
self._free_margin = account_info.get('equity', 0.0)
|
||||
except Exception as e:
|
||||
print(f"[RiskManager] 刷新账户信息失败: {e}")
|
||||
|
||||
# ==================== 账户信息 ====================
|
||||
|
||||
def update_account_info(self, balance: float, equity: float, free_margin: float) -> None:
|
||||
"""更新账户信息"""
|
||||
self._account_balance = balance
|
||||
self._account_equity = equity
|
||||
self._free_margin = free_margin
|
||||
|
||||
def get_account_balance(self) -> float:
|
||||
"""获取账户余额"""
|
||||
return self._account_balance
|
||||
|
||||
def get_account_equity(self) -> float:
|
||||
"""获取账户权益"""
|
||||
return self._account_equity
|
||||
|
||||
# ==================== 品种配置 ====================
|
||||
|
||||
def set_symbol_config(self, symbol: str, config: Dict) -> None:
|
||||
"""设置品种配置"""
|
||||
self._symbol_config[symbol] = config
|
||||
|
||||
def get_symbol_config(self, symbol: str) -> Dict:
|
||||
"""获取品种配置"""
|
||||
return self._symbol_config.get(symbol, {
|
||||
"point_value": 1.0, # 点值
|
||||
"min_volume": 0.01, # 最小手数
|
||||
"max_volume": 10.0, # 最大手数
|
||||
"volume_step": 0.01, # 手数步长
|
||||
})
|
||||
|
||||
# ==================== 手数计算 ====================
|
||||
|
||||
def calculate_volume(self, symbol: str, risk_points: float,
|
||||
strategy: TradingStrategy) -> float:
|
||||
"""
|
||||
计算交易手数
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
risk_points: 风险点数
|
||||
strategy: 策略配置
|
||||
|
||||
Returns:
|
||||
计算的手数
|
||||
"""
|
||||
config = self.get_symbol_config(symbol)
|
||||
point_value = config.get('point_value', 1.0)
|
||||
min_volume = config.get('min_volume', 0.01)
|
||||
max_volume = config.get('max_volume', 10.0)
|
||||
volume_step = config.get('volume_step', 0.01)
|
||||
|
||||
if strategy.volume_mode == "fixed":
|
||||
volume = strategy.fixed_volume
|
||||
elif strategy.volume_mode == "risk_percent":
|
||||
# 根据风险百分比计算手数
|
||||
risk_amount = self._account_balance * (strategy.risk_percent / 100)
|
||||
# 手数 = 风险金额 / (风险点数 * 点值)
|
||||
if risk_points > 0 and point_value > 0:
|
||||
volume = risk_amount / (risk_points * point_value)
|
||||
else:
|
||||
volume = min_volume
|
||||
else:
|
||||
volume = strategy.fixed_volume
|
||||
|
||||
# 应用最大风险点数限制
|
||||
if risk_points > strategy.max_risk_points:
|
||||
print(f"[RiskManager] 风险点数 {risk_points} 超过最大限制 {strategy.max_risk_points}")
|
||||
return 0.0
|
||||
|
||||
# 限制手数范围
|
||||
volume = max(min_volume, min(volume, max_volume))
|
||||
|
||||
# 按步长取整
|
||||
volume = round(volume / volume_step) * volume_step
|
||||
|
||||
return volume
|
||||
|
||||
# ==================== 风险检查 ====================
|
||||
|
||||
def check_risk(self, symbol: str, volume: float, risk_points: float) -> Dict:
|
||||
"""
|
||||
检查交易风险
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
volume: 手数
|
||||
risk_points: 风险点数
|
||||
|
||||
Returns:
|
||||
检查结果
|
||||
"""
|
||||
# 刷新账户信息
|
||||
self._refresh_account_info()
|
||||
|
||||
config = self.get_symbol_config(symbol)
|
||||
point_value = config.get('point_value', 1.0)
|
||||
|
||||
# 计算风险金额
|
||||
risk_amount = volume * risk_points * point_value
|
||||
risk_percent = (risk_amount / self._account_balance * 100) if self._account_balance > 0 else 0
|
||||
|
||||
# 检查每日风险限制
|
||||
remaining_risk = self._daily_risk_limit - self._daily_risk_used
|
||||
|
||||
allowed = True
|
||||
warnings = []
|
||||
|
||||
# 账户信息是否已初始化
|
||||
account_initialized = self._account_balance > 0 or self._free_margin > 0
|
||||
|
||||
if risk_percent > 5:
|
||||
allowed = False
|
||||
warnings.append(f"单笔风险 {risk_percent:.2f}% 超过5%")
|
||||
|
||||
if risk_percent + self._daily_risk_used > self._daily_risk_limit:
|
||||
allowed = False
|
||||
warnings.append(f"将超过每日风险限制 {self._daily_risk_limit}%")
|
||||
|
||||
# 只有账户信息已初始化时才检查保证金
|
||||
if account_initialized and self._free_margin < risk_amount:
|
||||
allowed = False
|
||||
warnings.append(f"保证金不足 (可用: {self._free_margin:.2f}, 需要: {risk_amount:.2f})")
|
||||
|
||||
if not account_initialized:
|
||||
warnings.append("账户信息未初始化,跳过保证金检查")
|
||||
|
||||
return {
|
||||
"allowed": allowed,
|
||||
"risk_amount": risk_amount,
|
||||
"risk_percent": round(risk_percent, 2),
|
||||
"daily_risk_used": self._daily_risk_used,
|
||||
"daily_risk_limit": self._daily_risk_limit,
|
||||
"remaining_risk": remaining_risk,
|
||||
"warnings": warnings,
|
||||
"account_initialized": account_initialized,
|
||||
}
|
||||
|
||||
# ==================== 持仓检查 ====================
|
||||
|
||||
def check_position_limit(self, symbol: str, strategy: TradingStrategy,
|
||||
current_positions: int, same_direction: int,
|
||||
opposite_direction: int, action: str) -> Dict:
|
||||
"""
|
||||
检查持仓限制
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
strategy: 策略配置
|
||||
current_positions: 当前持仓数
|
||||
same_direction: 同向持仓数
|
||||
opposite_direction: 反向持仓数
|
||||
action: 交易方向 buy/sell
|
||||
|
||||
Returns:
|
||||
检查结果
|
||||
"""
|
||||
allowed = True
|
||||
warnings = []
|
||||
|
||||
# 检查最大持仓数
|
||||
if current_positions >= strategy.max_positions:
|
||||
allowed = False
|
||||
warnings.append(f"已达到最大持仓数 {strategy.max_positions}")
|
||||
|
||||
# 检查同向持仓
|
||||
new_same_direction = same_direction + 1
|
||||
if new_same_direction > strategy.max_same_direction:
|
||||
allowed = False
|
||||
warnings.append(f"同向持仓将超过限制 {strategy.max_same_direction}")
|
||||
|
||||
# 检查持仓冲突策略
|
||||
if opposite_direction > 0:
|
||||
if strategy.position_conflict == "block":
|
||||
allowed = False
|
||||
warnings.append("有反向持仓,策略禁止新开仓")
|
||||
elif strategy.position_conflict == "allow_same":
|
||||
allowed = False
|
||||
warnings.append("有反向持仓,策略只允许同向加仓")
|
||||
elif strategy.position_conflict == "allow_opposite":
|
||||
# 允许反向
|
||||
pass
|
||||
|
||||
return {
|
||||
"allowed": allowed,
|
||||
"current_positions": current_positions,
|
||||
"same_direction": same_direction,
|
||||
"opposite_direction": opposite_direction,
|
||||
"max_positions": strategy.max_positions,
|
||||
"max_same_direction": strategy.max_same_direction,
|
||||
"warnings": warnings,
|
||||
}
|
||||
|
||||
# ==================== 状态 ====================
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取状态"""
|
||||
return {
|
||||
"account_balance": self._account_balance,
|
||||
"account_equity": self._account_equity,
|
||||
"free_margin": self._free_margin,
|
||||
"daily_risk_limit": self._daily_risk_limit,
|
||||
"daily_risk_used": self._daily_risk_used,
|
||||
"symbol_count": len(self._symbol_config),
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
策略决策服务
|
||||
综合信号、持仓、资金等做出交易决策
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
import threading
|
||||
|
||||
from ...models import TradingSignal, TradingStrategy, TradingDecision
|
||||
from ...models import ConsistencyRequirement, ConflictResolution
|
||||
from ...models import StopLossMode, TakeProfitMode
|
||||
from ...store import StrategyStore
|
||||
from ..signal import SignalService
|
||||
from .risk_manager import RiskManager
|
||||
|
||||
|
||||
class StrategyService:
|
||||
"""策略决策服务"""
|
||||
|
||||
def __init__(self, strategy_store: StrategyStore = None,
|
||||
signal_service: SignalService = None,
|
||||
risk_manager: RiskManager = None):
|
||||
self.strategy_store = strategy_store or StrategyStore()
|
||||
self.signal_service = signal_service or SignalService()
|
||||
self.risk_manager = risk_manager or RiskManager()
|
||||
|
||||
# 持仓服务引用(外部设置)
|
||||
self._position_service = None
|
||||
|
||||
# 待确认订单服务引用(外部设置)
|
||||
self._pending_order_service = None
|
||||
|
||||
# 决策冷却
|
||||
self._decision_cooldowns: Dict[str, datetime] = {}
|
||||
self._cooldown_lock = threading.Lock()
|
||||
self.decision_cooldown = 60 # 60秒冷却
|
||||
|
||||
print("[StrategyService] 策略决策服务已初始化")
|
||||
|
||||
def set_position_service(self, service) -> None:
|
||||
"""设置持仓服务"""
|
||||
self._position_service = service
|
||||
|
||||
def set_pending_order_service(self, service) -> None:
|
||||
"""设置待确认订单服务"""
|
||||
self._pending_order_service = service
|
||||
|
||||
# ==================== 策略配置 ====================
|
||||
|
||||
def get_strategy(self, symbol: str) -> TradingStrategy:
|
||||
"""获取品种策略配置"""
|
||||
return self.strategy_store.get_or_create_strategy(symbol)
|
||||
|
||||
def update_strategy(self, symbol: str, data: Dict) -> TradingStrategy:
|
||||
"""更新策略配置"""
|
||||
return self.strategy_store.update_strategy(symbol, data)
|
||||
|
||||
def get_all_strategies(self) -> List[TradingStrategy]:
|
||||
"""获取所有策略"""
|
||||
return self.strategy_store.get_all_strategies()
|
||||
|
||||
# ==================== 信号综合分析 ====================
|
||||
|
||||
def analyze_signals(self, symbol: str, signals: List[TradingSignal],
|
||||
strategy: TradingStrategy) -> Dict:
|
||||
"""
|
||||
综合分析信号
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
signals: 信号列表
|
||||
strategy: 策略配置
|
||||
|
||||
Returns:
|
||||
分析结果
|
||||
"""
|
||||
if not signals:
|
||||
return {
|
||||
"total_count": 0,
|
||||
"buy_count": 0,
|
||||
"sell_count": 0,
|
||||
"buy_weighted_score": 0,
|
||||
"sell_weighted_score": 0,
|
||||
"consistency": 0,
|
||||
"direction": None,
|
||||
"action": "none",
|
||||
}
|
||||
|
||||
# 过滤掉未启用的信号
|
||||
filtered_signals = []
|
||||
for s in signals:
|
||||
period = s.source_period if s.source != "key_level" else None
|
||||
if strategy.is_signal_enabled(s.source, period):
|
||||
filtered_signals.append(s)
|
||||
|
||||
if not filtered_signals:
|
||||
return {
|
||||
"total_count": 0,
|
||||
"buy_count": 0,
|
||||
"sell_count": 0,
|
||||
"buy_weighted_score": 0,
|
||||
"sell_weighted_score": 0,
|
||||
"consistency": 0,
|
||||
"direction": None,
|
||||
"action": "none",
|
||||
"filtered_out": len(signals),
|
||||
}
|
||||
|
||||
buy_signals = [s for s in filtered_signals if s.action == "buy"]
|
||||
sell_signals = [s for s in filtered_signals if s.action == "sell"]
|
||||
|
||||
# 计算加权分数(使用新的周期级别权重)
|
||||
buy_score = sum(
|
||||
s.confidence * strategy.get_signal_weight(s.source, s.source_period) / 100
|
||||
for s in buy_signals
|
||||
)
|
||||
sell_score = sum(
|
||||
s.confidence * strategy.get_signal_weight(s.source, s.source_period) / 100
|
||||
for s in sell_signals
|
||||
)
|
||||
|
||||
# 计算一致性
|
||||
total = len(filtered_signals)
|
||||
majority_count = max(len(buy_signals), len(sell_signals))
|
||||
consistency = majority_count / total if total > 0 else 0
|
||||
|
||||
# 确定方向
|
||||
direction = None
|
||||
if buy_score > sell_score:
|
||||
direction = "buy"
|
||||
elif sell_score > buy_score:
|
||||
direction = "sell"
|
||||
|
||||
# 检查一致性要求
|
||||
action = "none"
|
||||
if direction:
|
||||
if strategy.consistency_requirement == ConsistencyRequirement.ANY:
|
||||
action = direction
|
||||
elif strategy.consistency_requirement == ConsistencyRequirement.MAJORITY:
|
||||
if consistency >= 0.5:
|
||||
action = direction
|
||||
elif strategy.consistency_requirement == ConsistencyRequirement.ALL:
|
||||
if consistency == 1.0:
|
||||
action = direction
|
||||
|
||||
return {
|
||||
"total_count": total,
|
||||
"buy_count": len(buy_signals),
|
||||
"sell_count": len(sell_signals),
|
||||
"buy_weighted_score": round(buy_score, 2),
|
||||
"sell_weighted_score": round(sell_score, 2),
|
||||
"consistency": round(consistency, 2),
|
||||
"direction": direction,
|
||||
"action": action,
|
||||
"buy_signals": [s.signal_id for s in buy_signals],
|
||||
"sell_signals": [s.signal_id for s in sell_signals],
|
||||
"filtered_out": len(signals) - len(filtered_signals),
|
||||
}
|
||||
|
||||
# ==================== 决策生成 ====================
|
||||
|
||||
def make_decision(self, symbol: str, current_price: float,
|
||||
force_signals: List[TradingSignal] = None) -> Optional[TradingDecision]:
|
||||
"""
|
||||
做出交易决策
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
current_price: 当前价格
|
||||
force_signals: 强制使用的信号(用于测试)
|
||||
|
||||
Returns:
|
||||
TradingDecision 或 None
|
||||
"""
|
||||
# 检查决策冷却
|
||||
if self._is_in_cooldown(symbol):
|
||||
return None
|
||||
|
||||
# 获取策略配置
|
||||
strategy = self.get_strategy(symbol)
|
||||
if not strategy.enabled:
|
||||
return None
|
||||
|
||||
# 获取信号
|
||||
signals = force_signals if force_signals else self.signal_service.get_active_signals(symbol)
|
||||
|
||||
# 过滤低置信度信号
|
||||
signals = [s for s in signals if s.confidence >= strategy.min_confidence]
|
||||
|
||||
if not signals:
|
||||
return None
|
||||
|
||||
# 分析信号
|
||||
analysis = self.analyze_signals(symbol, signals, strategy)
|
||||
|
||||
if analysis["action"] == "none":
|
||||
return None
|
||||
|
||||
action = analysis["action"]
|
||||
|
||||
# 选择最佳信号(用于止损止盈)
|
||||
best_signal = self._select_best_signal(signals, action, strategy)
|
||||
if not best_signal:
|
||||
return None
|
||||
|
||||
# 计算止损止盈
|
||||
entry_price = current_price
|
||||
sl, tp = self._calculate_sl_tp(entry_price, best_signal, strategy)
|
||||
|
||||
if not sl or not tp or sl == 0 or tp == 0:
|
||||
print(f"[StrategyService] 无效的止损止盈: sl={sl}, tp={tp}")
|
||||
return None
|
||||
|
||||
# 计算风险
|
||||
risk_points = abs(entry_price - sl)
|
||||
reward_points = abs(tp - entry_price)
|
||||
rr_ratio = reward_points / risk_points if risk_points > 0 else 0
|
||||
|
||||
# 检查风险回报比
|
||||
if rr_ratio < strategy.min_risk_reward:
|
||||
print(f"[StrategyService] 风险回报比 {rr_ratio:.2f} 低于最小要求 {strategy.min_risk_reward}")
|
||||
return None
|
||||
|
||||
# 动态止损范围(根据价格调整)
|
||||
# 最小止损 = 价格的 0.05% 或 5 点(取较大)
|
||||
# 最大止损 = 价格的 2% 或 100 点(取较小)
|
||||
price_min_sl = entry_price * 0.0005 # 价格的 0.05%
|
||||
price_max_sl = entry_price * 0.02 # 价格的 2%
|
||||
|
||||
# 确保 min <= max
|
||||
dynamic_min_sl = max(1.0, price_min_sl) # 最小至少 1 点
|
||||
dynamic_max_sl = max(dynamic_min_sl, price_max_sl) # 最大至少等于最小
|
||||
|
||||
# 如果动态范围不合理,跳过
|
||||
if dynamic_min_sl > dynamic_max_sl:
|
||||
print(f"[StrategyService] 动态止损范围无效: [{dynamic_min_sl:.2f}, {dynamic_max_sl:.2f}], 跳过决策")
|
||||
return None
|
||||
|
||||
# 检查止损点数
|
||||
if risk_points < dynamic_min_sl or risk_points > dynamic_max_sl:
|
||||
print(f"[StrategyService] 止损点数 {risk_points:.2f} 不在动态范围 [{dynamic_min_sl:.2f}, {dynamic_max_sl:.2f}] (价格={entry_price:.2f})")
|
||||
return None
|
||||
|
||||
# 计算手数
|
||||
volume = self.risk_manager.calculate_volume(symbol, risk_points, strategy)
|
||||
if volume <= 0:
|
||||
return None
|
||||
|
||||
# 检查持仓限制
|
||||
position_check = self._check_position_limits(symbol, strategy, action)
|
||||
|
||||
# 检查风险限制
|
||||
risk_check = self.risk_manager.check_risk(symbol, volume, risk_points)
|
||||
|
||||
# 如果检查不通过,返回拒绝的决策
|
||||
if not position_check.get("allowed", True) or not risk_check.get("allowed", True):
|
||||
# 即使被拒绝也要设置冷却,避免频繁推送
|
||||
self._set_cooldown(symbol)
|
||||
decision = TradingDecision(
|
||||
symbol=symbol,
|
||||
strategy_id=strategy.strategy_id,
|
||||
action="none",
|
||||
decision_type="rejected",
|
||||
signals=[s.to_dict() for s in signals],
|
||||
signal_summary=analysis,
|
||||
decision_reason="风控检查未通过",
|
||||
confidence_score=0,
|
||||
position_check=position_check,
|
||||
risk_check=risk_check,
|
||||
status="rejected",
|
||||
)
|
||||
return decision
|
||||
|
||||
# 设置决策冷却
|
||||
self._set_cooldown(symbol)
|
||||
|
||||
# 生成决策理由
|
||||
decision_reason = self._generate_decision_reason(analysis, best_signal)
|
||||
|
||||
# 创建决策
|
||||
decision = TradingDecision(
|
||||
symbol=symbol,
|
||||
strategy_id=strategy.strategy_id,
|
||||
action=action,
|
||||
decision_type="signal_combined" if len(signals) > 1 else "single_signal",
|
||||
signals=[s.to_dict() for s in signals],
|
||||
signal_summary=analysis,
|
||||
entry_price=entry_price,
|
||||
sl=round(sl, 2),
|
||||
tp=round(tp, 2),
|
||||
volume=volume,
|
||||
risk_points=round(risk_points, 2),
|
||||
reward_points=round(reward_points, 2),
|
||||
risk_reward_ratio=round(rr_ratio, 2),
|
||||
decision_reason=decision_reason,
|
||||
confidence_score=analysis["buy_weighted_score"] if action == "buy" else analysis["sell_weighted_score"],
|
||||
position_check=position_check,
|
||||
risk_check=risk_check,
|
||||
)
|
||||
|
||||
print(f"[StrategyService] 生成决策: {decision.decision_id} {action} {symbol} @ {entry_price}")
|
||||
|
||||
return decision
|
||||
|
||||
def _select_best_signal(self, signals: List[TradingSignal],
|
||||
action: str, strategy: TradingStrategy) -> Optional[TradingSignal]:
|
||||
"""选择最佳信号"""
|
||||
filtered = [s for s in signals if s.action == action]
|
||||
if not filtered:
|
||||
return None
|
||||
|
||||
if strategy.conflict_resolution == ConflictResolution.HIGHEST_CONFIDENCE:
|
||||
return max(filtered, key=lambda s: s.confidence)
|
||||
elif strategy.conflict_resolution == ConflictResolution.HIGHEST_WEIGHT:
|
||||
return max(filtered, key=lambda s: s.confidence * strategy.get_signal_weight(s.source, s.source_period))
|
||||
else:
|
||||
return filtered[0]
|
||||
|
||||
def _calculate_sl_tp(self, entry_price: float, signal: TradingSignal,
|
||||
strategy: TradingStrategy) -> tuple:
|
||||
"""计算止损止盈"""
|
||||
# 止损
|
||||
if strategy.sl_mode == StopLossMode.SIGNAL:
|
||||
sl = signal.suggested_sl
|
||||
elif strategy.sl_mode == StopLossMode.FIXED_POINTS:
|
||||
if signal.action == "buy":
|
||||
sl = entry_price - strategy.sl_fixed_points
|
||||
else:
|
||||
sl = entry_price + strategy.sl_fixed_points
|
||||
else:
|
||||
sl = signal.suggested_sl
|
||||
|
||||
# 止盈
|
||||
if strategy.tp_mode == TakeProfitMode.SIGNAL:
|
||||
tp = signal.suggested_tp
|
||||
elif strategy.tp_mode == TakeProfitMode.FIXED_POINTS:
|
||||
if signal.action == "buy":
|
||||
tp = entry_price + strategy.tp_fixed_points
|
||||
else:
|
||||
tp = entry_price - strategy.tp_fixed_points
|
||||
elif strategy.tp_mode == TakeProfitMode.RISK_REWARD:
|
||||
risk = abs(entry_price - sl)
|
||||
if signal.action == "buy":
|
||||
tp = entry_price + risk * strategy.tp_risk_reward
|
||||
else:
|
||||
tp = entry_price - risk * strategy.tp_risk_reward
|
||||
else:
|
||||
tp = signal.suggested_tp
|
||||
|
||||
return sl, tp
|
||||
|
||||
def _check_position_limits(self, symbol: str, strategy: TradingStrategy,
|
||||
action: str) -> Dict:
|
||||
"""检查持仓限制"""
|
||||
current_positions = 0
|
||||
same_direction = 0
|
||||
opposite_direction = 0
|
||||
|
||||
if self._position_service:
|
||||
positions = self._position_service.get_positions(symbol)
|
||||
current_positions = len(positions)
|
||||
for pos in positions:
|
||||
# PositionData.to_dict() 返回 direction 字段
|
||||
pos_direction = pos.get('direction', '')
|
||||
if pos_direction == action:
|
||||
same_direction += 1
|
||||
else:
|
||||
opposite_direction += 1
|
||||
|
||||
return self.risk_manager.check_position_limit(
|
||||
symbol, strategy, current_positions, same_direction, opposite_direction, action
|
||||
)
|
||||
|
||||
def _generate_decision_reason(self, analysis: Dict, signal: TradingSignal) -> str:
|
||||
"""生成决策理由"""
|
||||
reasons = []
|
||||
|
||||
total = analysis["total_count"]
|
||||
buy_count = analysis["buy_count"]
|
||||
sell_count = analysis["sell_count"]
|
||||
direction = analysis["direction"]
|
||||
|
||||
if total == 1:
|
||||
reasons.append(f"单一信号({signal.source})建议{direction}")
|
||||
else:
|
||||
reasons.append(f"{total}个信号中{buy_count}个买入、{sell_count}个卖出")
|
||||
|
||||
reasons.append(f"综合判断: {direction}")
|
||||
reasons.append(f"风险回报比: {signal.risk_reward_ratio:.2f}")
|
||||
|
||||
return " | ".join(reasons)
|
||||
|
||||
def _is_in_cooldown(self, symbol: str) -> bool:
|
||||
"""检查是否在冷却期"""
|
||||
with self._cooldown_lock:
|
||||
if symbol in self._decision_cooldowns:
|
||||
last_time = self._decision_cooldowns[symbol]
|
||||
elapsed = (datetime.now() - last_time).total_seconds()
|
||||
return elapsed < self.decision_cooldown
|
||||
return False
|
||||
|
||||
def _set_cooldown(self, symbol: str) -> None:
|
||||
"""设置冷却"""
|
||||
with self._cooldown_lock:
|
||||
self._decision_cooldowns[symbol] = datetime.now()
|
||||
|
||||
# ==================== 执行决策 ====================
|
||||
|
||||
def execute_decision(self, decision: TradingDecision) -> Optional[str]:
|
||||
"""
|
||||
执行决策(生成待确认订单)
|
||||
|
||||
Args:
|
||||
decision: 交易决策
|
||||
|
||||
Returns:
|
||||
订单ID 或 None
|
||||
"""
|
||||
if decision.action == "none":
|
||||
return None
|
||||
|
||||
if not self._pending_order_service:
|
||||
print("[StrategyService] 待确认订单服务未设置")
|
||||
return None
|
||||
|
||||
# 创建订单
|
||||
order_id = self._pending_order_service.create_order(
|
||||
symbol=decision.symbol,
|
||||
action=decision.action,
|
||||
price=decision.entry_price,
|
||||
mount=decision.volume,
|
||||
sl=decision.sl,
|
||||
tp=decision.tp,
|
||||
reason=decision.decision_reason,
|
||||
description=f"Strategy: {decision.strategy_id}",
|
||||
source="strategy_decision",
|
||||
)
|
||||
|
||||
decision.order_id = order_id
|
||||
decision.status = "confirmed"
|
||||
|
||||
print(f"[StrategyService] 决策已执行,订单ID: {order_id}")
|
||||
return order_id
|
||||
|
||||
# ==================== 状态 ====================
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取服务状态"""
|
||||
return {
|
||||
"strategy_store": self.strategy_store.get_status(),
|
||||
"signal_service": self.signal_service.get_status(),
|
||||
"risk_manager": self.risk_manager.get_status(),
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
技术指标计算模块
|
||||
纯函数实现,不依赖外部状态
|
||||
"""
|
||||
|
||||
from typing import List, Dict
|
||||
from ..models import KlineData
|
||||
|
||||
|
||||
def calculate_ma(data: List[float], period: int) -> float:
|
||||
"""
|
||||
计算移动平均线 (MA)
|
||||
|
||||
Args:
|
||||
data: 数据列表(如收盘价)
|
||||
period: 周期
|
||||
|
||||
Returns:
|
||||
MA 值
|
||||
"""
|
||||
if not data:
|
||||
return 0
|
||||
if len(data) < period:
|
||||
return data[-1]
|
||||
return sum(data[-period:]) / period
|
||||
|
||||
|
||||
def calculate_adx(klines: List[KlineData], period: int = 14) -> float:
|
||||
"""
|
||||
计算 ADX (Average Directional Index)
|
||||
|
||||
ADX > 25: 有趋势
|
||||
ADX > 40: 强趋势
|
||||
ADX < 20: 无明显趋势
|
||||
|
||||
Args:
|
||||
klines: K线数据列表
|
||||
period: 计算周期
|
||||
|
||||
Returns:
|
||||
ADX 值
|
||||
"""
|
||||
if len(klines) < period + 1:
|
||||
return 0
|
||||
|
||||
# 计算 +DM 和 -DM
|
||||
plus_dm = []
|
||||
minus_dm = []
|
||||
tr_list = []
|
||||
|
||||
for i in range(1, len(klines)):
|
||||
high = klines[i].high
|
||||
low = klines[i].low
|
||||
prev_high = klines[i - 1].high
|
||||
prev_low = klines[i - 1].low
|
||||
prev_close = klines[i - 1].close
|
||||
|
||||
# +DM
|
||||
up_move = high - prev_high
|
||||
down_move = prev_low - low
|
||||
|
||||
if up_move > down_move and up_move > 0:
|
||||
plus_dm.append(up_move)
|
||||
else:
|
||||
plus_dm.append(0)
|
||||
|
||||
# -DM
|
||||
if down_move > up_move and down_move > 0:
|
||||
minus_dm.append(down_move)
|
||||
else:
|
||||
minus_dm.append(0)
|
||||
|
||||
# True Range
|
||||
tr = max(
|
||||
high - low,
|
||||
abs(high - prev_close),
|
||||
abs(low - prev_close)
|
||||
)
|
||||
tr_list.append(tr)
|
||||
|
||||
if len(tr_list) < period:
|
||||
return 0
|
||||
|
||||
# 计算平滑值
|
||||
atr = sum(tr_list[-period:]) / period
|
||||
smoothed_plus_dm = sum(plus_dm[-period:]) / period
|
||||
smoothed_minus_dm = sum(minus_dm[-period:]) / period
|
||||
|
||||
# 计算 +DI 和 -DI
|
||||
if atr == 0:
|
||||
return 0
|
||||
|
||||
plus_di = (smoothed_plus_dm / atr) * 100
|
||||
minus_di = (smoothed_minus_dm / atr) * 100
|
||||
|
||||
# 计算 DX
|
||||
di_sum = plus_di + minus_di
|
||||
if di_sum == 0:
|
||||
return 0
|
||||
|
||||
dx = abs(plus_di - minus_di) / di_sum * 100
|
||||
|
||||
return dx
|
||||
|
||||
|
||||
def calculate_rsi(data: List[float], period: int = 14) -> float:
|
||||
"""
|
||||
计算 RSI (Relative Strength Index)
|
||||
|
||||
Args:
|
||||
data: 数据列表(如收盘价)
|
||||
period: 计算周期
|
||||
|
||||
Returns:
|
||||
RSI 值 (0-100)
|
||||
"""
|
||||
if len(data) < period + 1:
|
||||
return 50 # 默认中性值
|
||||
|
||||
gains = []
|
||||
losses = []
|
||||
|
||||
for i in range(1, len(data)):
|
||||
change = data[i] - data[i - 1]
|
||||
if change > 0:
|
||||
gains.append(change)
|
||||
losses.append(0)
|
||||
else:
|
||||
gains.append(0)
|
||||
losses.append(abs(change))
|
||||
|
||||
if len(gains) < period:
|
||||
return 50
|
||||
|
||||
avg_gain = sum(gains[-period:]) / period
|
||||
avg_loss = sum(losses[-period:]) / period
|
||||
|
||||
if avg_loss == 0:
|
||||
return 100
|
||||
|
||||
rs = avg_gain / avg_loss
|
||||
rsi = 100 - (100 / (1 + rs))
|
||||
|
||||
return rsi
|
||||
|
||||
|
||||
def calculate_macd(data: List[float], fast: int = 12, slow: int = 26, signal: int = 9) -> Dict:
|
||||
"""
|
||||
计算 MACD (Moving Average Convergence Divergence)
|
||||
|
||||
Args:
|
||||
data: 数据列表
|
||||
fast: 快线周期
|
||||
slow: 慢线周期
|
||||
signal: 信号线周期
|
||||
|
||||
Returns:
|
||||
{"macd": float, "signal": float, "histogram": float}
|
||||
"""
|
||||
if len(data) < slow + signal:
|
||||
return {"macd": 0, "signal": 0, "histogram": 0}
|
||||
|
||||
# 计算快慢 EMA(简化用 SMA 近似)
|
||||
ema_fast = calculate_ema_approx(data, fast)
|
||||
ema_slow = calculate_ema_approx(data, slow)
|
||||
|
||||
# MACD 线
|
||||
macd_line = ema_fast - ema_slow
|
||||
|
||||
# 信号线(MACD 的移动平均)
|
||||
# 简化处理
|
||||
signal_line = macd_line # 简化
|
||||
|
||||
# 柱状图
|
||||
histogram = macd_line - signal_line
|
||||
|
||||
return {
|
||||
"macd": macd_line,
|
||||
"signal": signal_line,
|
||||
"histogram": histogram
|
||||
}
|
||||
|
||||
|
||||
def calculate_ema_approx(data: List[float], period: int) -> float:
|
||||
"""
|
||||
计算指数移动平均线 (EMA) 的近似值
|
||||
|
||||
Args:
|
||||
data: 数据列表
|
||||
period: 周期
|
||||
|
||||
Returns:
|
||||
EMA 值
|
||||
"""
|
||||
if not data:
|
||||
return 0
|
||||
if len(data) < period:
|
||||
return data[-1]
|
||||
|
||||
# 简化:使用 SMA 近似
|
||||
return sum(data[-period:]) / period
|
||||
|
||||
|
||||
def calculate_bollinger_bands(data: List[float], period: int = 20, std_dev: float = 2.0) -> Dict:
|
||||
"""
|
||||
计算布林带
|
||||
|
||||
Args:
|
||||
data: 数据列表
|
||||
period: 周期
|
||||
std_dev: 标准差倍数
|
||||
|
||||
Returns:
|
||||
{"upper": float, "middle": float, "lower": float}
|
||||
"""
|
||||
if len(data) < period:
|
||||
current = data[-1] if data else 0
|
||||
return {"upper": current, "middle": current, "lower": current}
|
||||
|
||||
# 中轨(SMA)
|
||||
middle = sum(data[-period:]) / period
|
||||
|
||||
# 计算标准差
|
||||
subset = data[-period:]
|
||||
variance = sum((x - middle) ** 2 for x in subset) / period
|
||||
std = variance ** 0.5
|
||||
|
||||
# 上下轨
|
||||
upper = middle + std_dev * std
|
||||
lower = middle - std_dev * std
|
||||
|
||||
return {
|
||||
"upper": upper,
|
||||
"middle": middle,
|
||||
"lower": lower
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
技术分析服务模块
|
||||
处理趋势分析、共振分析、交易建议生成等业务逻辑
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from ..models import KlineData, TechTrendState, TechTrendChange, TechResonanceResult, TechTradeSuggestion
|
||||
from ..store import TechStore, KlineStore, PivotStore
|
||||
from .tech_indicators import calculate_ma, calculate_adx
|
||||
|
||||
|
||||
class TechService:
|
||||
"""技术分析服务(处理业务逻辑)"""
|
||||
|
||||
# 支持的周期
|
||||
PERIODS = ['H4', 'H1', 'M15', 'M5', 'M1']
|
||||
|
||||
# ADX 阈值
|
||||
ADX_TREND_THRESHOLD = 25
|
||||
ADX_STRONG_THRESHOLD = 40
|
||||
|
||||
# 均线周期
|
||||
MA_FAST = 10
|
||||
MA_SLOW = 20
|
||||
|
||||
# 最小 K 线数量
|
||||
MIN_KLINES = 30
|
||||
|
||||
def __init__(self, tech_store: TechStore, kline_store: KlineStore, pivot_store: PivotStore):
|
||||
self.tech_store = tech_store
|
||||
self.kline_store = kline_store
|
||||
self.pivot_store = pivot_store
|
||||
|
||||
# 统计服务引用(用于获取价差)
|
||||
self._statistics_service = None
|
||||
|
||||
print("[TechService] 技术分析服务已初始化")
|
||||
|
||||
def set_statistics_service(self, statistics_service):
|
||||
"""设置统计服务引用"""
|
||||
self._statistics_service = statistics_service
|
||||
|
||||
def _get_symbol_spread(self, symbol: str) -> Optional[float]:
|
||||
"""获取品种价差"""
|
||||
if not self._statistics_service:
|
||||
return None
|
||||
return self._statistics_service.get_spread(symbol)
|
||||
|
||||
# ==================== 趋势分析 ====================
|
||||
|
||||
def analyze_trend(self, symbol: str, period: str) -> Dict:
|
||||
"""
|
||||
分析单个周期的趋势
|
||||
|
||||
Args:
|
||||
symbol: 交易品种
|
||||
period: 周期
|
||||
|
||||
Returns:
|
||||
趋势状态字典
|
||||
"""
|
||||
period = period.upper()
|
||||
|
||||
# 从 store 获取 K 线数据
|
||||
klines_dict = self.kline_store.get_all_klines(symbol, period)
|
||||
if not klines_dict:
|
||||
return self._create_unknown_state(symbol, period, "无K线数据")
|
||||
|
||||
# 转换为 KlineData 对象
|
||||
klines = [
|
||||
KlineData(
|
||||
symbol=k.get('symbol', symbol),
|
||||
period=k.get('period', period),
|
||||
timestamp=k.get('timestamp'),
|
||||
open_price=float(k.get('open', 0)),
|
||||
high=float(k.get('high', 0)),
|
||||
low=float(k.get('low', 0)),
|
||||
close=float(k.get('close', 0)),
|
||||
volume=float(k.get('volume', 0))
|
||||
)
|
||||
for k in klines_dict
|
||||
]
|
||||
|
||||
if len(klines) < self.MIN_KLINES:
|
||||
return self._create_unknown_state(symbol, period, f"K线数据不足(需≥{self.MIN_KLINES}根)")
|
||||
|
||||
# 计算技术指标
|
||||
closes = [k.close for k in klines]
|
||||
ma_fast = calculate_ma(closes, self.MA_FAST)
|
||||
ma_slow = calculate_ma(closes, self.MA_SLOW)
|
||||
current_price = closes[-1]
|
||||
adx = calculate_adx(klines)
|
||||
|
||||
# 判断趋势
|
||||
trend, reason = self._determine_trend(adx, ma_fast, ma_slow, current_price)
|
||||
|
||||
# 计算强度
|
||||
strength = self._calculate_strength(adx)
|
||||
|
||||
# 获取之前的状态
|
||||
previous_state = self.tech_store.get_trend_state_object(symbol, period)
|
||||
previous_trend = previous_state.trend if previous_state else None
|
||||
change_signal = previous_trend and previous_trend != "unknown" and previous_trend != trend
|
||||
|
||||
# 创建状态对象
|
||||
state = TechTrendState(
|
||||
symbol=symbol,
|
||||
period=period,
|
||||
trend=trend,
|
||||
strength=strength,
|
||||
adx=round(adx, 2),
|
||||
ma_fast=round(ma_fast, 4),
|
||||
ma_slow=round(ma_slow, 4),
|
||||
price=current_price,
|
||||
reason=reason,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
previous_trend=previous_trend,
|
||||
change_signal=change_signal
|
||||
)
|
||||
|
||||
# 保存状态
|
||||
self.tech_store.save_trend_state(state)
|
||||
|
||||
# 记录趋势转换
|
||||
if change_signal:
|
||||
change = TechTrendChange(
|
||||
period=period,
|
||||
from_trend=previous_trend,
|
||||
to_trend=trend,
|
||||
price=current_price,
|
||||
timestamp=datetime.now().isoformat()
|
||||
)
|
||||
self.tech_store.add_trend_change(symbol, change)
|
||||
|
||||
return state.to_dict()
|
||||
|
||||
def _create_unknown_state(self, symbol: str, period: str, reason: str) -> Dict:
|
||||
"""创建未知状态"""
|
||||
state = TechTrendState(
|
||||
symbol=symbol,
|
||||
period=period,
|
||||
trend="unknown",
|
||||
reason=reason,
|
||||
timestamp=datetime.now().isoformat()
|
||||
)
|
||||
return state.to_dict()
|
||||
|
||||
def _determine_trend(self, adx: float, ma_fast: float, ma_slow: float, price: float) -> tuple:
|
||||
"""判断趋势方向"""
|
||||
reason_parts = []
|
||||
|
||||
if adx < self.ADX_TREND_THRESHOLD:
|
||||
trend = "sideways"
|
||||
reason_parts.append(f"ADX={adx:.1f}<25 无明显趋势")
|
||||
else:
|
||||
if ma_fast > ma_slow and price > ma_fast:
|
||||
trend = "up"
|
||||
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) > MA{self.MA_SLOW}({ma_slow:.2f})")
|
||||
reason_parts.append(f"价格({price:.2f}) > MA{self.MA_FAST}")
|
||||
reason_parts.append(f"ADX={adx:.1f}≥25 确认趋势")
|
||||
elif ma_fast < ma_slow and price < ma_fast:
|
||||
trend = "down"
|
||||
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) < MA{self.MA_SLOW}({ma_slow:.2f})")
|
||||
reason_parts.append(f"价格({price:.2f}) < MA{self.MA_FAST}")
|
||||
reason_parts.append(f"ADX={adx:.1f}≥25 确认趋势")
|
||||
else:
|
||||
trend = "sideways"
|
||||
if ma_fast > ma_slow:
|
||||
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) > MA{self.MA_SLOW}({ma_slow:.2f})")
|
||||
reason_parts.append(f"但价格({price:.2f})低于MA{self.MA_FAST}")
|
||||
else:
|
||||
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) < MA{self.MA_SLOW}({ma_slow:.2f})")
|
||||
reason_parts.append(f"且价格({price:.2f})高于MA{self.MA_FAST}")
|
||||
reason_parts.append("信号矛盾,判定震荡")
|
||||
|
||||
return trend, ";".join(reason_parts)
|
||||
|
||||
def _calculate_strength(self, adx: float) -> int:
|
||||
"""计算趋势强度"""
|
||||
if adx >= self.ADX_STRONG_THRESHOLD:
|
||||
return min(100, int(adx + 20))
|
||||
elif adx >= self.ADX_TREND_THRESHOLD:
|
||||
return int(adx + 10)
|
||||
else:
|
||||
return int(adx)
|
||||
|
||||
# ==================== 共振分析 ====================
|
||||
|
||||
def analyze_resonance(self, symbol: str) -> Dict:
|
||||
"""
|
||||
分析多周期共振
|
||||
|
||||
Args:
|
||||
symbol: 交易品种
|
||||
|
||||
Returns:
|
||||
共振分析结果
|
||||
"""
|
||||
states = self.tech_store.get_all_trend_states(symbol)
|
||||
|
||||
if not states:
|
||||
result = TechResonanceResult(symbol=symbol)
|
||||
return result.to_dict()
|
||||
|
||||
# 统计各趋势数量
|
||||
up_count = sum(1 for s in states.values() if s.trend == 'up')
|
||||
down_count = sum(1 for s in states.values() if s.trend == 'down')
|
||||
sideways_count = sum(1 for s in states.values() if s.trend == 'sideways')
|
||||
|
||||
# 计算平均强度
|
||||
strengths = [s.strength for s in states.values() if s.trend != 'sideways']
|
||||
avg_strength = sum(strengths) / len(strengths) if strengths else 0
|
||||
|
||||
# 判断共振
|
||||
total = len(states)
|
||||
if up_count >= total * 0.6:
|
||||
resonance = "up"
|
||||
aligned_count = up_count
|
||||
signal = f"多周期向上共振 ({up_count}/{total})"
|
||||
elif down_count >= total * 0.6:
|
||||
resonance = "down"
|
||||
aligned_count = down_count
|
||||
signal = f"多周期向下共振 ({down_count}/{total})"
|
||||
else:
|
||||
resonance = "none"
|
||||
aligned_count = max(up_count, down_count)
|
||||
signal = f"趋势分歧 (↑{up_count} ↓{down_count} →{sideways_count})"
|
||||
|
||||
result = TechResonanceResult(
|
||||
symbol=symbol,
|
||||
resonance=resonance,
|
||||
strength=int(avg_strength),
|
||||
aligned_count=aligned_count,
|
||||
up_count=up_count,
|
||||
down_count=down_count,
|
||||
sideways_count=sideways_count,
|
||||
signal=signal,
|
||||
periods={p: s.to_dict() for p, s in states.items()}
|
||||
)
|
||||
|
||||
return result.to_dict()
|
||||
|
||||
# ==================== 交易建议 ====================
|
||||
|
||||
def generate_trade_suggestion(self, symbol: str, current_price: float) -> Optional[Dict]:
|
||||
"""
|
||||
基于趋势和转折点生成交易建议
|
||||
|
||||
Args:
|
||||
symbol: 交易品种
|
||||
current_price: 当前实时价格
|
||||
|
||||
Returns:
|
||||
交易建议 或 None
|
||||
"""
|
||||
# 获取共振分析
|
||||
resonance = self.analyze_resonance(symbol)
|
||||
|
||||
if resonance['resonance'] == 'none':
|
||||
return None
|
||||
|
||||
if resonance['strength'] < 30:
|
||||
return None
|
||||
|
||||
trend = resonance['resonance']
|
||||
|
||||
# 从 pivot_store 获取转折点
|
||||
pivots = self.pivot_store.get_pivot_objects(symbol)
|
||||
if not pivots:
|
||||
return None
|
||||
|
||||
# 按时间排序
|
||||
recent_pivots = sorted(
|
||||
[p.to_dict() for p in pivots],
|
||||
key=lambda x: str(x.get('timestamp', '')),
|
||||
reverse=True
|
||||
)[:10]
|
||||
|
||||
sl = None
|
||||
tp = None
|
||||
action = None
|
||||
reason = ""
|
||||
|
||||
if trend == "up":
|
||||
action = "b"
|
||||
low_pivots = [p for p in recent_pivots if p.get('direction') == 'low']
|
||||
if low_pivots:
|
||||
sl = low_pivots[0].get('price')
|
||||
if sl and current_price > sl:
|
||||
distance = current_price - sl
|
||||
tp = current_price + distance * 1.5
|
||||
reason = f"多周期向上共振,建议买入,止损参考最近低点 {sl}"
|
||||
else:
|
||||
return None
|
||||
|
||||
elif trend == "down":
|
||||
action = "s"
|
||||
high_pivots = [p for p in recent_pivots if p.get('direction') == 'high']
|
||||
if high_pivots:
|
||||
sl = high_pivots[0].get('price')
|
||||
if sl and current_price < sl:
|
||||
distance = sl - current_price
|
||||
tp = current_price - distance * 1.5
|
||||
reason = f"多周期向下共振,建议卖出,止损参考最近高点 {sl}"
|
||||
else:
|
||||
return None
|
||||
|
||||
if not all([action, sl, tp]):
|
||||
return None
|
||||
|
||||
suggestion = TechTradeSuggestion(
|
||||
symbol=symbol,
|
||||
action=action,
|
||||
price=current_price,
|
||||
sl=round(sl, 4),
|
||||
tp=round(tp, 4),
|
||||
reason=reason,
|
||||
trend_strength=resonance['strength'],
|
||||
resonance_periods=resonance['aligned_count'],
|
||||
generated_at=datetime.now().isoformat()
|
||||
)
|
||||
|
||||
return suggestion.to_dict()
|
||||
|
||||
# ==================== 查询 ====================
|
||||
|
||||
def get_trend_state(self, symbol: str, period: str = None) -> Dict:
|
||||
"""获取趋势状态"""
|
||||
return self.tech_store.get_trend_state(symbol, period)
|
||||
|
||||
def get_trend_changes(self, symbol: str, count: int = 10) -> List[Dict]:
|
||||
"""获取趋势转换历史"""
|
||||
return self.tech_store.get_trend_changes(symbol, count)
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取状态"""
|
||||
return self.tech_store.get_status()
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
交易历史服务模块
|
||||
"""
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from ..models.trade_history import TradeDeal
|
||||
from ..store.trade_history_store import TradeHistoryStore
|
||||
|
||||
|
||||
class TradeHistoryService:
|
||||
"""
|
||||
交易历史服务
|
||||
|
||||
功能:
|
||||
1. 处理EA上报的交易历史
|
||||
2. 提供交易统计分析
|
||||
"""
|
||||
|
||||
def __init__(self, store: TradeHistoryStore = None):
|
||||
self.store = store or TradeHistoryStore()
|
||||
|
||||
def process_deals(self, deals_data: List[Dict]) -> int:
|
||||
"""
|
||||
处理EA上报的成交记录
|
||||
|
||||
Args:
|
||||
deals_data: EA上报的成交数据列表
|
||||
|
||||
Returns:
|
||||
新增记录数
|
||||
"""
|
||||
deals = [TradeDeal.from_ea_data(data) for data in deals_data]
|
||||
return self.store.add(deals)
|
||||
|
||||
def get_deals(self, symbol: str = None, hours: int = None) -> List[Dict]:
|
||||
"""获取成交记录"""
|
||||
return self.store.get_dict(symbol, hours)
|
||||
|
||||
def get_statistics(self, symbol: str = None) -> Dict:
|
||||
"""获取交易统计"""
|
||||
return self.store.get_statistics(symbol)
|
||||
|
||||
def get_recent_profit(self, symbol: str = None, hours: int = 24) -> float:
|
||||
"""获取最近N小时的盈亏"""
|
||||
return self.store.get_recent_profit(symbol, hours)
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取服务状态"""
|
||||
return self.store.get_status()
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
交易指令服务模块
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..models import TradingInstruction, PendingOrder
|
||||
from ..store import TradingInstructionStore
|
||||
|
||||
|
||||
class TradingInstructionService:
|
||||
"""交易指令服务(处理业务逻辑)"""
|
||||
|
||||
def __init__(self, instruction_store: TradingInstructionStore = None):
|
||||
self.store = instruction_store or TradingInstructionStore()
|
||||
print("[TradingInstructionService] 交易指令服务已初始化")
|
||||
|
||||
# ==================== 创建指令 ====================
|
||||
|
||||
def create_instruction(self, symbol: str, action: str, price: float,
|
||||
mount: float, sl: float = 0.0, tp: float = 0.005,
|
||||
reason: str = "", description: str = "",
|
||||
source: str = "", order_id: str = None) -> str:
|
||||
"""
|
||||
创建交易指令
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
action: 方向 (b/s)
|
||||
price: 执行价格
|
||||
mount: 手数
|
||||
sl: 止损
|
||||
tp: 止盈
|
||||
reason: 原因
|
||||
description: 描述
|
||||
source: 来源
|
||||
order_id: 来源订单ID
|
||||
|
||||
Returns:
|
||||
指令ID
|
||||
"""
|
||||
instruction = TradingInstruction(
|
||||
symbol=symbol,
|
||||
action=action,
|
||||
price=price,
|
||||
mount=mount,
|
||||
sl=sl,
|
||||
tp=tp,
|
||||
reason=reason,
|
||||
description=description,
|
||||
source=source,
|
||||
order_id=order_id,
|
||||
)
|
||||
return self.store.add_instruction(instruction)
|
||||
|
||||
def create_instruction_from_dict(self, data: Dict) -> str:
|
||||
"""从字典创建指令"""
|
||||
instruction = TradingInstruction.from_dict(data)
|
||||
return self.store.add_instruction(instruction)
|
||||
|
||||
def create_from_pending_order(self, order: PendingOrder) -> str:
|
||||
"""从待确认订单创建指令"""
|
||||
instruction = TradingInstruction.from_pending_order(order)
|
||||
return self.store.add_instruction(instruction)
|
||||
|
||||
def create_instructions_batch(self, instructions_data: List[Dict]) -> int:
|
||||
"""
|
||||
批量创建指令
|
||||
|
||||
Args:
|
||||
instructions_data: 指令字典列表
|
||||
|
||||
Returns:
|
||||
创建数量
|
||||
"""
|
||||
count = 0
|
||||
for data in instructions_data:
|
||||
# 填充默认值
|
||||
if data.get('sl') is None:
|
||||
data['sl'] = 0.0
|
||||
if data.get('tp') is None or data.get('tp', 0) <= 0:
|
||||
data['tp'] = 0.005
|
||||
|
||||
self.create_instruction_from_dict(data)
|
||||
count += 1
|
||||
|
||||
print(f"[TradingInstructionService] 批量创建指令: {count}条")
|
||||
return count
|
||||
|
||||
# ==================== 查询指令 ====================
|
||||
|
||||
def get_instruction(self, instruction_id: str) -> Optional[TradingInstruction]:
|
||||
"""获取指令"""
|
||||
return self.store.get_instruction_by_id(instruction_id)
|
||||
|
||||
def get_instructions_by_symbol(self, symbol: str) -> List[TradingInstruction]:
|
||||
"""获取指定品种的指令"""
|
||||
return self.store.get_instructions_by_symbol(symbol)
|
||||
|
||||
def get_all_instructions(self) -> List[TradingInstruction]:
|
||||
"""获取所有指令"""
|
||||
return self.store.get_all_instructions()
|
||||
|
||||
def get_all_instructions_dict(self) -> Dict[str, List[Dict]]:
|
||||
"""获取所有指令(按品种分类)"""
|
||||
return self.store.get_all_instructions_dict()
|
||||
|
||||
# ==================== EA获取指令 ====================
|
||||
|
||||
def fetch_instructions_for_ea(self, symbol: str, current_price: float = None) -> List[Dict]:
|
||||
"""
|
||||
EA获取满足条件的指令
|
||||
|
||||
Args:
|
||||
symbol: 品种
|
||||
current_price: 当前价格,用于价格过滤
|
||||
|
||||
Returns:
|
||||
满足条件的指令列表(字典格式)
|
||||
"""
|
||||
return self.store.fetch_and_remove_by_symbol(symbol, current_price)
|
||||
|
||||
# ==================== 清理指令 ====================
|
||||
|
||||
def remove_instruction(self, instruction_id: str) -> Optional[TradingInstruction]:
|
||||
"""移除指令"""
|
||||
return self.store.remove_instruction(instruction_id)
|
||||
|
||||
def clear_by_symbol(self, symbol: str) -> int:
|
||||
"""清空指定品种的指令"""
|
||||
return self.store.clear_by_symbol(symbol)
|
||||
|
||||
def clear_all(self) -> int:
|
||||
"""清空所有指令"""
|
||||
return self.store.clear_all()
|
||||
|
||||
# ==================== 统计 ====================
|
||||
|
||||
def get_count_by_symbol(self, symbol: str) -> int:
|
||||
"""获取指定品种的指令数量"""
|
||||
return self.store.get_count_by_symbol(symbol)
|
||||
|
||||
def get_total_count(self) -> int:
|
||||
"""获取总指令数量"""
|
||||
return self.store.get_total_count()
|
||||
|
||||
# ==================== 状态 ====================
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取服务状态"""
|
||||
return self.store.get_status()
|
||||
Reference in New Issue
Block a user