Refactor: restructure market module with services, stores, and utils

This commit is contained in:
guaiwoluo2020
2026-03-19 09:47:41 +08:00
parent ef15b92342
commit b7ea37ad09
78 changed files with 10594 additions and 7645 deletions
+36
View File
@@ -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'
]
+160
View File
@@ -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)
}
+168
View File
@@ -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()
}
+259
View File
@@ -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
+453
View File
@@ -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()
+175
View File
@@ -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,
}
+322
View File
@@ -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)
+79
View File
@@ -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()
+17
View File
@@ -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',
]
+212
View File
@@ -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)
+171
View File
@@ -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
+185
View File
@@ -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)
+194
View File
@@ -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()
+61
View File
@@ -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()
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
策略服务模块
"""
from .strategy_service import StrategyService
from .risk_manager import RiskManager
__all__ = [
'StrategyService',
'RiskManager',
]
+257
View File
@@ -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(),
}
+238
View File
@@ -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
}
+341
View File
@@ -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()
+52
View File
@@ -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()