Files

341 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()