feat: complete trading system with FastAPI backend, web frontend, and auto-analysis
This commit is contained in:
184
backend/services/strategy_service.py
Normal file
184
backend/services/strategy_service.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
策略服务
|
||||
实现v7策略:三维度评分 + 大盘过滤 + 盈利保护
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
class StrategyService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.weights = {'tech': 0.6, 'fund': 0.3, 'sent': 0.1}
|
||||
|
||||
def calculate_signal(self, ticker: str, stock_data: pd.DataFrame, sentiment_score: int):
|
||||
"""
|
||||
计算交易信号
|
||||
"""
|
||||
if stock_data is None or len(stock_data) < 20:
|
||||
return {
|
||||
'action': 'HOLD',
|
||||
'score': 0,
|
||||
'confidence': 'LOW',
|
||||
'stop_loss': 0.08,
|
||||
'position_ratio': 0,
|
||||
'reasons': ['数据不足']
|
||||
}
|
||||
|
||||
latest = stock_data.iloc[-1]
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
# 1. 技术面评分
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
tech_score = 0
|
||||
reasons = []
|
||||
|
||||
# RSI
|
||||
rsi = latest.get('rsi', 50)
|
||||
if rsi < 30:
|
||||
tech_score += 3
|
||||
reasons.append(f'RSI超卖({rsi:.1f})')
|
||||
elif rsi < 45:
|
||||
tech_score += 1
|
||||
elif rsi > 70:
|
||||
tech_score -= 3
|
||||
reasons.append(f'RSI超买({rsi:.1f})')
|
||||
elif rsi > 55:
|
||||
tech_score -= 1
|
||||
|
||||
# 均线
|
||||
close = latest.get('close', 0)
|
||||
ma5 = latest.get('ma5', 0)
|
||||
ma20 = latest.get('ma20', 0)
|
||||
ma60 = latest.get('ma60', 0)
|
||||
|
||||
if close > ma5 > ma20:
|
||||
tech_score += 2
|
||||
reasons.append('均线多头排列')
|
||||
elif close < ma5 < ma20:
|
||||
tech_score -= 2
|
||||
reasons.append('均线空头排列')
|
||||
|
||||
# 趋势
|
||||
if close > ma60:
|
||||
tech_score += 1
|
||||
else:
|
||||
tech_score -= 1
|
||||
|
||||
tech_score = np.clip(tech_score, -10, 10)
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
# 2. 基本面(简化,实际应从数据库读取)
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
# 这里简化处理,实际应该根据股票获取对应的基本面评分
|
||||
fund_score = 0 # 默认为中性
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
# 3. 综合评分
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
total_score = (
|
||||
self.weights['tech'] * tech_score +
|
||||
self.weights['fund'] * fund_score +
|
||||
self.weights['sent'] * sentiment_score * 2 # sentiment是-5~5,放大
|
||||
)
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
# 4. 生成信号
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
atr = latest.get('atr', close * 0.05)
|
||||
atr_percent = atr / close if close > 0 else 0.05
|
||||
|
||||
# 止损设置
|
||||
if atr_percent < 0.05:
|
||||
stop_loss = 0.08 # 低波动,固定8%
|
||||
stop_type = '固定8%'
|
||||
elif atr_percent < 0.15:
|
||||
stop_loss = min(0.35, max(0.08, atr_percent * 2.5)) # 中波动
|
||||
stop_type = f'ATR×2.5 ({stop_loss*100:.1f}%)'
|
||||
else:
|
||||
stop_loss = min(0.40, max(0.08, atr_percent * 2.0)) # 高波动
|
||||
stop_type = f'ATR×2.0 ({stop_loss*100:.1f}%)'
|
||||
|
||||
# 仓位建议
|
||||
if total_score >= 5:
|
||||
position_ratio = 1.0
|
||||
confidence = 'HIGH'
|
||||
elif total_score >= 3:
|
||||
position_ratio = 0.6
|
||||
confidence = 'MEDIUM'
|
||||
elif total_score >= 1.5:
|
||||
position_ratio = 0.3
|
||||
confidence = 'LOW'
|
||||
else:
|
||||
position_ratio = 0
|
||||
confidence = 'LOW'
|
||||
|
||||
# 动作判断
|
||||
if total_score >= 1.5:
|
||||
action = 'BUY'
|
||||
elif total_score <= -1.5:
|
||||
action = 'SELL'
|
||||
else:
|
||||
action = 'HOLD'
|
||||
|
||||
reasons.append(f'舆情{sentiment_score:+d}分')
|
||||
reasons.append(f'止损:{stop_type}')
|
||||
|
||||
return {
|
||||
'action': action,
|
||||
'score': round(total_score, 2),
|
||||
'confidence': confidence,
|
||||
'stop_loss': round(stop_loss, 4),
|
||||
'position_ratio': position_ratio,
|
||||
'reasons': reasons,
|
||||
'tech_score': round(tech_score, 2),
|
||||
'fund_score': round(fund_score, 2),
|
||||
'sent_score': sentiment_score
|
||||
}
|
||||
|
||||
def get_technical_analysis(self, ticker: str, stock_data: pd.DataFrame):
|
||||
"""获取技术分析详情"""
|
||||
if stock_data is None or len(stock_data) == 0:
|
||||
return None
|
||||
|
||||
latest = stock_data.iloc[-1]
|
||||
|
||||
close = latest.get('close', 0)
|
||||
ma5 = latest.get('ma5', 0)
|
||||
ma20 = latest.get('ma20', 0)
|
||||
ma60 = latest.get('ma60', 0)
|
||||
rsi = latest.get('rsi', 50)
|
||||
atr = latest.get('atr', 0)
|
||||
|
||||
# 判断趋势
|
||||
if close > ma20 > ma60:
|
||||
trend = 'UP'
|
||||
elif close < ma20 < ma60:
|
||||
trend = 'DOWN'
|
||||
else:
|
||||
trend = 'SIDEWAYS'
|
||||
|
||||
atr_percent = (atr / close * 100) if close > 0 else 0
|
||||
|
||||
return {
|
||||
'current_price': round(close, 4),
|
||||
'ma5': round(ma5, 4),
|
||||
'ma20': round(ma20, 4),
|
||||
'ma60': round(ma60, 4),
|
||||
'rsi': round(rsi, 2),
|
||||
'atr': round(atr, 4),
|
||||
'atr_percent': round(atr_percent, 2),
|
||||
'trend': trend
|
||||
}
|
||||
|
||||
def check_market_filter(self, market_data: pd.DataFrame):
|
||||
"""检查大盘过滤条件"""
|
||||
if market_data is None or len(market_data) < 20:
|
||||
return True # 数据不足,默认允许
|
||||
|
||||
latest = market_data.iloc[-1]
|
||||
close = latest.get('close', 0)
|
||||
ma20 = latest.get('ma20', 0)
|
||||
|
||||
return close >= ma20
|
||||
Reference in New Issue
Block a user