udpate
This commit is contained in:
parent
fc5afb0e84
commit
50d6b79e43
@ -169,7 +169,12 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
# 股票智能体配置
|
# 股票智能体配置
|
||||||
stock_symbols_us: str = "" # 美股代码,逗号分隔
|
stock_symbols_us: str = "" # 美股代码,逗号分隔
|
||||||
stock_symbols_hk: str = "" # 港股代码,逗号分隔(以.HK结尾)
|
# 港股代码:科技股一线+新能源+芯片半导体+AI
|
||||||
|
# 科技:腾讯/阿里/美团/小米/京东/网易/百度/快手/知乎/B站
|
||||||
|
# 新能源:比亚迪/理想/小鹏/赣锋锂业/龙源电力/信义能源
|
||||||
|
# 芯片:中芯国际/华虹半导体/上海复旦
|
||||||
|
# AI:商汤/第四范式/创新奇智/美图/联易融/百融云
|
||||||
|
stock_symbols_hk: str = "00700.HK,09988.HK,03690.HK,01810.HK,09618.HK,00999.HK,09888.HK,01024.HK,01211.HK,02015.HK,09868.HK,01772.HK,00916.HK,03868.HK,00981.HK,01347.HK,01385.HK,00020.HK,06669.HK,02121.HK,01357.HK,02390.HK,09626.HK,02599.HK,06608.HK"
|
||||||
stock_analysis_interval: int = 300 # 分析间隔(秒,默认5分钟)
|
stock_analysis_interval: int = 300 # 分析间隔(秒,默认5分钟)
|
||||||
stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值
|
stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值
|
||||||
|
|
||||||
|
|||||||
@ -191,12 +191,17 @@ class YFinanceService:
|
|||||||
"""
|
"""
|
||||||
df = df.copy()
|
df = df.copy()
|
||||||
|
|
||||||
# 移动平均线
|
# 移动平均线(简单移动平均 MA)
|
||||||
df['ma5'] = df['close'].rolling(window=5).mean()
|
df['ma5'] = df['close'].rolling(window=5).mean()
|
||||||
df['ma10'] = df['close'].rolling(window=10).mean()
|
df['ma10'] = df['close'].rolling(window=10).mean()
|
||||||
df['ma20'] = df['close'].rolling(window=20).mean()
|
df['ma20'] = df['close'].rolling(window=20).mean()
|
||||||
df['ma50'] = df['close'].rolling(window=50).mean()
|
df['ma50'] = df['close'].rolling(window=50).mean()
|
||||||
|
|
||||||
|
# 指数移动平均线(EMA)- 用于趋势判断
|
||||||
|
df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
|
||||||
|
df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
|
||||||
|
df['ema200'] = df['close'].ewm(span=200, adjust=False).mean()
|
||||||
|
|
||||||
# RSI(使用 Wilder's Smoothing 方法)
|
# RSI(使用 Wilder's Smoothing 方法)
|
||||||
delta = df['close'].diff()
|
delta = df['close'].diff()
|
||||||
gain = delta.where(delta > 0, 0)
|
gain = delta.where(delta > 0, 0)
|
||||||
|
|||||||
@ -24,15 +24,71 @@ class StockMarketSignalAnalyzer:
|
|||||||
"""股票市场信号分析器 - 只关注市场,输出客观信号"""
|
"""股票市场信号分析器 - 只关注市场,输出客观信号"""
|
||||||
|
|
||||||
# 股票市场分析系统提示词
|
# 股票市场分析系统提示词
|
||||||
MARKET_ANALYSIS_PROMPT = """你是一位专业的股票交易员和技术分析师。你的任务是综合分析**技术面(K线、量价、技术指标)、基本面(估值、盈利、成长)、新闻舆情**,给出交易信号。
|
MARKET_ANALYSIS_PROMPT = """你是一位专业的股票交易员和技术分析师。你的任务是综合分析**趋势方向、技术面(K线、量价、技术指标)、基本面(估值、盈利、成长)、新闻舆情**,给出交易信号。
|
||||||
|
|
||||||
## 核心理念
|
## 核心理念
|
||||||
股票市场有其独特的运行规律:
|
**趋势是你的朋友,顺势交易是稳定盈利的关键。**
|
||||||
- **趋势为王**:股票更容易形成持续性趋势,顺势而为最重要
|
|
||||||
- **基本面支撑**:技术信号需要结合公司基本面来判断有效性
|
### 🚨 铁律(必须遵守)
|
||||||
- **新闻催化**:重大新闻会改变短期趋势,需要重点关注
|
1. **先判断趋势,再寻找信号** - 趋势方向错误,信号再强也不做
|
||||||
- **关注财报季**:财报前后波动加大,需要更谨慎
|
2. **顺势交易为主** - 上涨趋势只做多或观望,下跌趋势只做空或观望
|
||||||
- **板块效应**:同板块股票往往联动,关注板块整体表现
|
3. **逆势交易极其谨慎** - 必须有多重反转信号才能考虑逆势
|
||||||
|
4. **单边行情不逆势** - 强趋势中(日线连续3根以上同向K线)严禁逆势开仓
|
||||||
|
|
||||||
|
### 交易目标
|
||||||
|
- **稳健为主**:宁可错过,不做错
|
||||||
|
- **顺势而为**:在大趋势方向上寻找入场点
|
||||||
|
- **严控风险**:每次交易风险不超过本金的2%
|
||||||
|
|
||||||
|
## 零、趋势方向判断(第一步,最重要!)
|
||||||
|
**在分析任何信号之前,先判断当前趋势方向和强度。**
|
||||||
|
|
||||||
|
### 趋势判断标准(使用 EMA 和均线系统)
|
||||||
|
**上升趋势(多头市场)**:
|
||||||
|
- EMA20 > EMA50 > EMA200(短中长期均线多头排列)
|
||||||
|
- 价格站稳在 EMA20 之上
|
||||||
|
- MA5 > MA10 > MA20 > MA50
|
||||||
|
- 最近高点逐步抬高,低点也逐步抬高
|
||||||
|
|
||||||
|
**下降趋势(空头市场)**:
|
||||||
|
- EMA20 < EMA50 < EMA200(短中长期均线空头排列)
|
||||||
|
- 价格持续在 EMA20 之下
|
||||||
|
- MA5 < MA10 < MA20 < MA50
|
||||||
|
- 最近高点逐步降低,低点也逐步降低
|
||||||
|
|
||||||
|
**震荡市(无明确趋势)**:
|
||||||
|
- 均线纠缠,无明显排列
|
||||||
|
- 价格在 EMA20 上下波动
|
||||||
|
- 高点低点无规律
|
||||||
|
- 此时可双向交易,但降低仓位
|
||||||
|
|
||||||
|
### 趋势强度判断
|
||||||
|
- **强趋势**:均线完美排列 + 价格远离均线 + 成交量配合
|
||||||
|
- **中等趋势**:均线有排列 + 价格偶尔回踩均线
|
||||||
|
- **弱趋势/震荡**:均线纠缠 + 价格在均线上下反复
|
||||||
|
|
||||||
|
### 顺势交易规则(必须执行)
|
||||||
|
| 当前趋势 | 允许操作 | 条件 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| **强上升趋势** | ✅ 只做多 | 回调到支撑位、RSI超卖区、金叉 |
|
||||||
|
| **强上升趋势** | ❌ 严禁做空 | 除非出现明确的顶背离+放量反转信号 |
|
||||||
|
| **强下降趋势** | ✅ 只做空 | 反弹到阻力位、RSI超买区、死叉 |
|
||||||
|
| **强下降趋势** | ❌ 严禁做多 | 除非出现明确的底背离+放量反转信号 |
|
||||||
|
| **震荡市** | ✅ 双向交易 | 但降低仓位(轻仓),提高止损要求 |
|
||||||
|
| **趋势不明确** | ⚠️ 观望为主 | 等待趋势明确后再入场 |
|
||||||
|
|
||||||
|
### 逆势交易的条件(极其严格)
|
||||||
|
**只有在满足以下全部条件时,才允许考虑逆势交易:**
|
||||||
|
1. **多重反转信号**:
|
||||||
|
- 明确的背离(顶背离或底背离)
|
||||||
|
- 关键形态反转(头肩顶/底、双顶/底、吞没形态)
|
||||||
|
- 放量突破关键位
|
||||||
|
2. **多周期确认**:周线、日线、1h 三个周期同时出现反转信号
|
||||||
|
3. **风险收益比合理**:潜在盈利至少是风险的3倍以上
|
||||||
|
4. **基本面支持**:重大利好/利空改变趋势
|
||||||
|
5. **降低仓位**:逆势交易必须轻仓(不超过顺势仓位的50%)
|
||||||
|
|
||||||
|
**如果不符合上述条件,即使有买入/卖出信号,也必须选择 hold(观望)。**
|
||||||
|
|
||||||
## 数据说明
|
## 数据说明
|
||||||
你将获得三个维度的数据:
|
你将获得三个维度的数据:
|
||||||
@ -115,14 +171,22 @@ class StockMarketSignalAnalyzer:
|
|||||||
**均线系统是趋势判断的核心:**
|
**均线系统是趋势判断的核心:**
|
||||||
- **多头排列**(MA5 > MA10 > MA20 > MA50):强势上涨趋势,回调做多
|
- **多头排列**(MA5 > MA10 > MA20 > MA50):强势上涨趋势,回调做多
|
||||||
- **空头排列**(MA5 < MA10 < MA20 < MA50):强势下跌趋势,反弹做空
|
- **空头排列**(MA5 < MA10 < MA20 < MA50):强势下跌趋势,反弹做空
|
||||||
- **价格与 MA 的关系**:
|
- **EMA 趋势判断**(比 MA 更平滑,更适合判断长期趋势):
|
||||||
|
- **多头排列**(EMA20 > EMA50 > EMA200):长期上涨趋势确立
|
||||||
|
- **空头排列**(EMA20 < EMA50 < EMA200):长期下跌趋势确立
|
||||||
|
- 价格站稳 EMA20 上方:中期上涨趋势
|
||||||
|
- 价格跌破 EMA20:中期转为下跌趋势
|
||||||
|
- EMA50 是长期趋势的生命线
|
||||||
|
- **价格与 MA/EMA 的关系**:
|
||||||
- 价格站稳 MA5/MA10 上方:短线上涨
|
- 价格站稳 MA5/MA10 上方:短线上涨
|
||||||
- 价格突破 MA20:中线转多
|
- 价格突破 MA20/EMA20:中线转多
|
||||||
- 价格跌破 MA20:中线转空
|
- 价格跌破 MA20/EMA20:中线转空
|
||||||
- MA50 是中期趋势的分水岭
|
- MA50/EMA50 是中期趋势的分水岭
|
||||||
- **均线金叉死叉**:
|
- **均线金叉死叉**:
|
||||||
- MA5 上穿 MA10:短线买入信号
|
- MA5 上穿 MA10:短线买入信号
|
||||||
- MA5 下穿 MA10:短线卖出信号
|
- MA5 下穿 MA10:短线卖出信号
|
||||||
|
- EMA20 上穿 EMA50:中线买入信号(重要)
|
||||||
|
- EMA20 下穿 EMA50:中线卖出信号(重要)
|
||||||
|
|
||||||
## 四、多周期共振(关键分析框架)
|
## 四、多周期共振(关键分析框架)
|
||||||
**多周期共振是提高信号质量的核心方法:**
|
**多周期共振是提高信号质量的核心方法:**
|
||||||
@ -249,8 +313,12 @@ class StockMarketSignalAnalyzer:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"trend_direction": "uptrend/downtrend/neutral",
|
||||||
|
"trend_strength": "strong/medium/weak",
|
||||||
"analysis_summary": "简要描述当前市场状态(50字以内)",
|
"analysis_summary": "简要描述当前市场状态(50字以内)",
|
||||||
"volume_analysis": "量价分析结论(30字以内)",
|
"volume_analysis": "量价分析结论(30字以内)",
|
||||||
|
"news_sentiment": "positive/negative/neutral",
|
||||||
|
"news_impact": "新闻对市场的影响分析(30字以内)",
|
||||||
"signals": [
|
"signals": [
|
||||||
{
|
{
|
||||||
"type": "short_term/medium_term/long_term",
|
"type": "short_term/medium_term/long_term",
|
||||||
@ -261,7 +329,7 @@ class StockMarketSignalAnalyzer:
|
|||||||
"entry_zone": 150.50,
|
"entry_zone": 150.50,
|
||||||
"stop_loss": 148.00,
|
"stop_loss": 148.00,
|
||||||
"take_profit": 155.00,
|
"take_profit": 155.00,
|
||||||
"reasoning": "详细的入场理由(必须包含量价分析)",
|
"reasoning": "详细的入场理由(必须包含趋势判断和量价分析)",
|
||||||
"key_factors": ["关键因素1", "关键因素2"]
|
"key_factors": ["关键因素1", "关键因素2"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -395,7 +463,18 @@ class StockMarketSignalAnalyzer:
|
|||||||
context_parts.append(f"MA20: {latest.get('ma20', 'N/A')}")
|
context_parts.append(f"MA20: {latest.get('ma20', 'N/A')}")
|
||||||
context_parts.append(f"MA50: {latest.get('ma50', 'N/A')}")
|
context_parts.append(f"MA50: {latest.get('ma50', 'N/A')}")
|
||||||
|
|
||||||
# 判断均线排列
|
# EMA 均线(用于趋势判断)
|
||||||
|
ema20 = latest.get('ema20', None)
|
||||||
|
ema50 = latest.get('ema50', None)
|
||||||
|
ema200 = latest.get('ema200', None)
|
||||||
|
if ema20 is not None:
|
||||||
|
context_parts.append(f"EMA20: {ema20:.2f}")
|
||||||
|
if ema50 is not None:
|
||||||
|
context_parts.append(f"EMA50: {ema50:.2f}")
|
||||||
|
if ema200 is not None:
|
||||||
|
context_parts.append(f"EMA200: {ema200:.2f}")
|
||||||
|
|
||||||
|
# 判断 MA 排列
|
||||||
ma5 = latest.get('ma5', 0)
|
ma5 = latest.get('ma5', 0)
|
||||||
ma10 = latest.get('ma10', 0)
|
ma10 = latest.get('ma10', 0)
|
||||||
ma20 = latest.get('ma20', 0)
|
ma20 = latest.get('ma20', 0)
|
||||||
@ -403,11 +482,29 @@ class StockMarketSignalAnalyzer:
|
|||||||
|
|
||||||
if all([ma5, ma10, ma20, ma50]):
|
if all([ma5, ma10, ma20, ma50]):
|
||||||
if ma5 > ma10 > ma20 > ma50:
|
if ma5 > ma10 > ma20 > ma50:
|
||||||
context_parts.append("均线排列: 多头排列 📈")
|
context_parts.append("MA排列: 多头排列 📈")
|
||||||
elif ma5 < ma10 < ma20 < ma50:
|
elif ma5 < ma10 < ma20 < ma50:
|
||||||
context_parts.append("均线排列: 空头排列 📉")
|
context_parts.append("MA排列: 空头排列 📉")
|
||||||
else:
|
else:
|
||||||
context_parts.append("均线排列: 交织,方向不明")
|
context_parts.append("MA排列: 交织,方向不明")
|
||||||
|
|
||||||
|
# EMA 排列(更重要的趋势判断)
|
||||||
|
if all([ema20, ema50, ema200]):
|
||||||
|
if ema20 > ema50 > ema200:
|
||||||
|
context_parts.append("EMA排列: 多头排列 (长期趋势向上) 📈")
|
||||||
|
elif ema20 < ema50 < ema200:
|
||||||
|
context_parts.append("EMA排列: 空头排列 (长期趋势向下) 📉")
|
||||||
|
else:
|
||||||
|
context_parts.append("EMA排列: 交织 (震荡市) ➡️")
|
||||||
|
|
||||||
|
# 价格与 EMA20 的关系(趋势判断关键)
|
||||||
|
if ema20 is not None:
|
||||||
|
if latest['close'] > ema20:
|
||||||
|
context_parts.append("价格位置: 站稳 EMA20 之上 (多头强势)")
|
||||||
|
elif latest['close'] < ema20:
|
||||||
|
context_parts.append("价格位置: 跌破 EMA20 (空头强势)")
|
||||||
|
else:
|
||||||
|
context_parts.append("价格位置: 接近 EMA20")
|
||||||
|
|
||||||
# 量比分析(使用日线数据)
|
# 量比分析(使用日线数据)
|
||||||
df_1d = data.get('1d')
|
df_1d = data.get('1d')
|
||||||
@ -473,6 +570,16 @@ class StockMarketSignalAnalyzer:
|
|||||||
|
|
||||||
result = json.loads(json_str)
|
result = json.loads(json_str)
|
||||||
|
|
||||||
|
# 向后兼容:确保新字段存在
|
||||||
|
if 'trend_direction' not in result:
|
||||||
|
result['trend_direction'] = 'neutral'
|
||||||
|
if 'trend_strength' not in result:
|
||||||
|
result['trend_strength'] = 'weak'
|
||||||
|
if 'news_sentiment' not in result:
|
||||||
|
result['news_sentiment'] = 'neutral'
|
||||||
|
if 'news_impact' not in result:
|
||||||
|
result['news_impact'] = ''
|
||||||
|
|
||||||
# 清理价格字段 - 转换为 float
|
# 清理价格字段 - 转换为 float
|
||||||
result = self._clean_price_fields(result)
|
result = self._clean_price_fields(result)
|
||||||
|
|
||||||
@ -504,32 +611,6 @@ class StockMarketSignalAnalyzer:
|
|||||||
else:
|
else:
|
||||||
sig['grade'] = 'D'
|
sig['grade'] = 'D'
|
||||||
|
|
||||||
# 从信号中推断 market_state 和 trend
|
|
||||||
if 'signals' in result and result['signals']:
|
|
||||||
best_signal = max(result['signals'], key=lambda s: s.get('confidence', 0))
|
|
||||||
action = best_signal.get('action', 'wait')
|
|
||||||
confidence = best_signal.get('confidence', 0)
|
|
||||||
|
|
||||||
if confidence >= 70:
|
|
||||||
if action == 'buy':
|
|
||||||
result['market_state'] = '强势上涨'
|
|
||||||
elif action == 'sell':
|
|
||||||
result['market_state'] = '强势下跌'
|
|
||||||
else:
|
|
||||||
result['market_state'] = '震荡整理'
|
|
||||||
else:
|
|
||||||
result['market_state'] = '震荡整理'
|
|
||||||
|
|
||||||
if action == 'buy':
|
|
||||||
result['trend'] = 'up'
|
|
||||||
elif action == 'sell':
|
|
||||||
result['trend'] = 'down'
|
|
||||||
else:
|
|
||||||
result['trend'] = 'sideways'
|
|
||||||
else:
|
|
||||||
result['market_state'] = '无明确信号'
|
|
||||||
result['trend'] = 'sideways'
|
|
||||||
|
|
||||||
logger.info(f"✅ 市场信号分析完成: {symbol}")
|
logger.info(f"✅ 市场信号分析完成: {symbol}")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@ -727,10 +808,12 @@ class StockMarketSignalAnalyzer:
|
|||||||
"""返回空信号"""
|
"""返回空信号"""
|
||||||
return {
|
return {
|
||||||
'symbol': symbol,
|
'symbol': symbol,
|
||||||
'analysis_summary': 'unknown',
|
'trend_direction': 'neutral',
|
||||||
'volume_analysis': '分析失败',
|
'trend_strength': 'weak',
|
||||||
'market_state': '分析失败',
|
'analysis_summary': '分析失败',
|
||||||
'trend': 'sideways',
|
'volume_analysis': '',
|
||||||
|
'news_sentiment': 'neutral',
|
||||||
|
'news_impact': '',
|
||||||
'signals': [],
|
'signals': [],
|
||||||
'key_levels': {},
|
'key_levels': {},
|
||||||
'timestamp': datetime.now().isoformat(),
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
|||||||
@ -19,66 +19,6 @@ from app.stock_agent.market_signal_analyzer import StockMarketSignalAnalyzer
|
|||||||
from app.utils.system_status import get_system_monitor, AgentStatus
|
from app.utils.system_status import get_system_monitor, AgentStatus
|
||||||
|
|
||||||
|
|
||||||
# 股票名称映射表
|
|
||||||
STOCK_NAMES = {
|
|
||||||
# 美股 - 科技龙头
|
|
||||||
'AAPL': '苹果',
|
|
||||||
'MSFT': '微软',
|
|
||||||
'GOOGL': '谷歌',
|
|
||||||
'META': 'Meta',
|
|
||||||
'AMZN': '亚马逊',
|
|
||||||
'NVDA': '英伟达',
|
|
||||||
'AMD': 'AMD',
|
|
||||||
'AVGO': '博通',
|
|
||||||
'ARM': 'ARM',
|
|
||||||
'PLTR': 'Palantir',
|
|
||||||
'SNOW': 'Snowflake',
|
|
||||||
|
|
||||||
# 美股 - 生物医疗
|
|
||||||
'LLY': '礼来',
|
|
||||||
'NVO': '诺和诺德',
|
|
||||||
'VRTX': 'Vertex',
|
|
||||||
|
|
||||||
# 美股 - 新能源/汽车
|
|
||||||
'TSLA': '特斯拉',
|
|
||||||
'ENPH': 'Enphase',
|
|
||||||
|
|
||||||
# 美股 - 金融
|
|
||||||
'V': 'Visa',
|
|
||||||
'MA': 'Mastercard',
|
|
||||||
|
|
||||||
# 美股 - 消费
|
|
||||||
'HD': 'Home Depot',
|
|
||||||
'COST': 'Costco',
|
|
||||||
|
|
||||||
# 美股 - 其他
|
|
||||||
'RKLB': 'Relativity Space',
|
|
||||||
'HOOD': 'Robinhood',
|
|
||||||
'DXYZ': 'DEX',
|
|
||||||
'GLW': '康宁',
|
|
||||||
'UNTY': 'Unity',
|
|
||||||
'CRM': 'Salesforce',
|
|
||||||
'ADBE': 'Adobe',
|
|
||||||
'INTC': '英特尔',
|
|
||||||
'FSLR': 'First Solar',
|
|
||||||
'CRWD': 'CrowdStrike',
|
|
||||||
'SHOP': 'Shopify',
|
|
||||||
'NET': 'Cloudflare',
|
|
||||||
'COIN': 'Coinbase',
|
|
||||||
'MSTR': 'MicroStrategy',
|
|
||||||
|
|
||||||
# 港股
|
|
||||||
'0700.HK': '腾讯',
|
|
||||||
'9988.HK': '阿里巴巴',
|
|
||||||
'1810.HK': '小米',
|
|
||||||
'2015.HK': '理想汽车',
|
|
||||||
'9866.HK': '蔚来',
|
|
||||||
'9992.HK': '泡泡玛特',
|
|
||||||
'9626.HK': '哔哩哔哩',
|
|
||||||
'9880.HK': '优必选',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class StockAgent:
|
class StockAgent:
|
||||||
"""美股交易信号智能体(LLM 驱动,仅分析通知)"""
|
"""美股交易信号智能体(LLM 驱动,仅分析通知)"""
|
||||||
|
|
||||||
@ -130,11 +70,6 @@ class StockAgent:
|
|||||||
|
|
||||||
logger.info(f"股票智能体初始化完成 - 美股: {us_count}只, 港股: {hk_count}只, 总计: {len(self.symbols)}只")
|
logger.info(f"股票智能体初始化完成 - 美股: {us_count}只, 港股: {hk_count}只, 总计: {len(self.symbols)}只")
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_stock_name(symbol: str) -> str:
|
|
||||||
"""获取股票中文名称"""
|
|
||||||
return STOCK_NAMES.get(symbol, symbol)
|
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""启动智能体"""
|
"""启动智能体"""
|
||||||
if self.running:
|
if self.running:
|
||||||
@ -402,6 +337,7 @@ class StockAgent:
|
|||||||
|
|
||||||
result = {
|
result = {
|
||||||
'symbol': symbol,
|
'symbol': symbol,
|
||||||
|
'stock_name': '', # 从基本面数据获取的公司名称
|
||||||
'current_price': 0,
|
'current_price': 0,
|
||||||
'signals': [],
|
'signals': [],
|
||||||
'analysis_summary': '',
|
'analysis_summary': '',
|
||||||
@ -425,34 +361,34 @@ class StockAgent:
|
|||||||
current_price = ticker['lastPrice']
|
current_price = ticker['lastPrice']
|
||||||
result['current_price'] = current_price
|
result['current_price'] = current_price
|
||||||
|
|
||||||
# 获取股票中文名称
|
# 4. 获取基本面数据(包含公司名称)
|
||||||
stock_name = STOCK_NAMES.get(symbol, '')
|
|
||||||
symbol_display = f"{stock_name}({symbol})" if stock_name else symbol
|
|
||||||
|
|
||||||
logger.info(f"\n{'='*60}")
|
|
||||||
logger.info(f"📊 分析 {symbol_display} @ ${current_price:,.2f}")
|
|
||||||
logger.info(f"{'='*60}")
|
|
||||||
|
|
||||||
# 4. 获取基本面数据
|
|
||||||
logger.info(f"\n📈 【基本面分析】")
|
logger.info(f"\n📈 【基本面分析】")
|
||||||
fundamental_data = None
|
fundamental_data = None
|
||||||
fundamental_summary = ""
|
fundamental_summary = ""
|
||||||
|
stock_name = "" # 从基本面数据获取公司名称
|
||||||
try:
|
try:
|
||||||
fundamental_data = self.fundamental.get_fundamental_data(symbol)
|
fundamental_data = self.fundamental.get_fundamental_data(symbol)
|
||||||
if fundamental_data:
|
if fundamental_data:
|
||||||
# 传递已获取的数据,避免重复调用
|
# 传递已获取的数据,避免重复调用
|
||||||
fundamental_summary = self.fundamental.get_fundamental_summary(symbol, fundamental_data)
|
fundamental_summary = self.fundamental.get_fundamental_summary(symbol, fundamental_data)
|
||||||
|
# 从基本面数据获取公司名称
|
||||||
|
stock_name = fundamental_data.get('company_name', '')
|
||||||
|
result['stock_name'] = stock_name # 保存到结果中
|
||||||
# 基本面评分已经在 fundamental_service 中输出
|
# 基本面评分已经在 fundamental_service 中输出
|
||||||
else:
|
else:
|
||||||
logger.warning(f" ⚠️ 无法获取基本面数据")
|
logger.warning(f" ⚠️ 无法获取基本面数据")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f" ⚠️ 获取基本面数据失败: {e}")
|
logger.warning(f" ⚠️ 获取基本面数据失败: {e}")
|
||||||
|
|
||||||
|
symbol_display = f"{stock_name}({symbol})" if stock_name else symbol
|
||||||
|
logger.info(f"\n{'='*60}")
|
||||||
|
logger.info(f"📊 分析 {symbol_display} @ ${current_price:,.2f}")
|
||||||
|
logger.info(f"{'='*60}")
|
||||||
|
|
||||||
# 5. 获取新闻数据
|
# 5. 获取新闻数据
|
||||||
logger.info(f"\n📰 【新闻分析】")
|
logger.info(f"\n📰 【新闻分析】")
|
||||||
news_data = None
|
news_data = None
|
||||||
try:
|
try:
|
||||||
stock_name = STOCK_NAMES.get(symbol, '')
|
|
||||||
news_data = await self.news.search_stock_news(symbol, stock_name, max_results=5)
|
news_data = await self.news.search_stock_news(symbol, stock_name, max_results=5)
|
||||||
if news_data:
|
if news_data:
|
||||||
logger.info(f" 获取到 {len(news_data)} 条相关新闻")
|
logger.info(f" 获取到 {len(news_data)} 条相关新闻")
|
||||||
@ -725,7 +661,7 @@ class StockAgent:
|
|||||||
sig['symbol'] = r['symbol']
|
sig['symbol'] = r['symbol']
|
||||||
sig['current_price'] = r.get('current_price', 0)
|
sig['current_price'] = r.get('current_price', 0)
|
||||||
sig['is_hk'] = r['symbol'].endswith('.HK')
|
sig['is_hk'] = r['symbol'].endswith('.HK')
|
||||||
sig['stock_name'] = STOCK_NAMES.get(r['symbol'], '')
|
sig['stock_name'] = r.get('stock_name', '')
|
||||||
|
|
||||||
if sig.get('action') == 'buy':
|
if sig.get('action') == 'buy':
|
||||||
buy_signals.append(sig)
|
buy_signals.append(sig)
|
||||||
@ -817,7 +753,10 @@ class StockAgent:
|
|||||||
f"",
|
f"",
|
||||||
]
|
]
|
||||||
|
|
||||||
# 高等级信号
|
# 所有信号(按等级分组)
|
||||||
|
all_signals = buy_signals + sell_signals
|
||||||
|
|
||||||
|
# 高等级信号 (A/B级)
|
||||||
if high_quality_signals:
|
if high_quality_signals:
|
||||||
content_parts.append(f"⭐ **高等级信号 (A/B级)**")
|
content_parts.append(f"⭐ **高等级信号 (A/B级)**")
|
||||||
for sig in high_quality_signals[:5]:
|
for sig in high_quality_signals[:5]:
|
||||||
@ -834,6 +773,27 @@ class StockAgent:
|
|||||||
content_parts.append(f"• {market_tag} {symbol_display} {action} {grade}级 {confidence}%")
|
content_parts.append(f"• {market_tag} {symbol_display} {action} {grade}级 {confidence}%")
|
||||||
content_parts.append(f"")
|
content_parts.append(f"")
|
||||||
|
|
||||||
|
# 其他等级信号 (C/D级)
|
||||||
|
other_signals = [s for s in all_signals if s.get('grade', 'D') not in ['A', 'B']]
|
||||||
|
if other_signals:
|
||||||
|
content_parts.append(f"📋 **其他信号 (C/D级)**")
|
||||||
|
for sig in other_signals[:10]: # 最多显示10个
|
||||||
|
symbol = sig['symbol']
|
||||||
|
stock_name = sig.get('stock_name', '')
|
||||||
|
market_tag = '[港股]' if sig.get('is_hk') else '[美股]'
|
||||||
|
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
||||||
|
grade = sig.get('grade', 'D')
|
||||||
|
confidence = sig.get('confidence', 0)
|
||||||
|
|
||||||
|
# 构建带名称的股票显示
|
||||||
|
symbol_display = f"{stock_name}({symbol})" if stock_name else symbol
|
||||||
|
|
||||||
|
content_parts.append(f"• {market_tag} {symbol_display} {action} {grade}级 {confidence}%")
|
||||||
|
|
||||||
|
if len(other_signals) > 10:
|
||||||
|
content_parts.append(f" *...还有 {len(other_signals) - 10} 个信号*")
|
||||||
|
content_parts.append(f"")
|
||||||
|
|
||||||
# 信号统计
|
# 信号统计
|
||||||
content_parts.extend([
|
content_parts.extend([
|
||||||
f"📈 做多信号: {len(buy_signals)} 个",
|
f"📈 做多信号: {len(buy_signals)} 个",
|
||||||
|
|||||||
@ -13,7 +13,7 @@ class SignalFormatter:
|
|||||||
"""信号格式化工具"""
|
"""信号格式化工具"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_signal_message(signal: Dict[str, Any], symbol: str, agent_type: str = 'crypto') -> str:
|
def format_signal_message(signal: Dict[str, Any], symbol: str, agent_type: str = 'crypto', stock_name: str = '') -> str:
|
||||||
"""
|
"""
|
||||||
格式化信号消息(用于 Telegram 通知)
|
格式化信号消息(用于 Telegram 通知)
|
||||||
|
|
||||||
@ -21,14 +21,11 @@ class SignalFormatter:
|
|||||||
signal: 信号数据
|
signal: 信号数据
|
||||||
symbol: 交易对
|
symbol: 交易对
|
||||||
agent_type: 智能体类型 (crypto/stock)
|
agent_type: 智能体类型 (crypto/stock)
|
||||||
|
stock_name: 股票名称(可选,从基本面数据获取)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
格式化的消息文本
|
格式化的消息文本
|
||||||
"""
|
"""
|
||||||
# 获取股票名称
|
|
||||||
from app.stock_agent.stock_agent import STOCK_NAMES
|
|
||||||
stock_name = STOCK_NAMES.get(symbol, '')
|
|
||||||
|
|
||||||
type_map = {
|
type_map = {
|
||||||
'short_term': '短线',
|
'short_term': '短线',
|
||||||
'medium_term': '中线',
|
'medium_term': '中线',
|
||||||
@ -102,7 +99,7 @@ class SignalFormatter:
|
|||||||
return message
|
return message
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_feishu_card(signal: Dict[str, Any], symbol: str, agent_type: str = 'crypto') -> Dict[str, Any]:
|
def format_feishu_card(signal: Dict[str, Any], symbol: str, agent_type: str = 'crypto', stock_name: str = '') -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
格式化飞书卡片消息
|
格式化飞书卡片消息
|
||||||
|
|
||||||
@ -110,14 +107,11 @@ class SignalFormatter:
|
|||||||
signal: 信号数据
|
signal: 信号数据
|
||||||
symbol: 交易对
|
symbol: 交易对
|
||||||
agent_type: 智能体类型 (crypto/stock)
|
agent_type: 智能体类型 (crypto/stock)
|
||||||
|
stock_name: 股票名称(可选,从基本面数据获取)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
包含 title, content, color 的字典
|
包含 title, content, color 的字典
|
||||||
"""
|
"""
|
||||||
# 获取股票名称
|
|
||||||
from app.stock_agent.stock_agent import STOCK_NAMES
|
|
||||||
stock_name = STOCK_NAMES.get(symbol, '')
|
|
||||||
|
|
||||||
type_map = {
|
type_map = {
|
||||||
'short_term': '短线',
|
'short_term': '短线',
|
||||||
'medium_term': '中线',
|
'medium_term': '中线',
|
||||||
|
|||||||
@ -36,11 +36,17 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
Returns:
|
Returns:
|
||||||
分析结果字典
|
分析结果字典
|
||||||
"""
|
"""
|
||||||
# 导入股票名称映射
|
# 获取服务
|
||||||
from app.stock_agent.stock_agent import STOCK_NAMES
|
yf_service = get_yfinance_service()
|
||||||
|
market_analyzer = StockMarketSignalAnalyzer() # 使用新的市场信号分析器
|
||||||
|
fundamental = get_fundamental_service() # 基本面服务
|
||||||
|
news = get_news_service() # 新闻服务
|
||||||
|
feishu = get_feishu_service()
|
||||||
|
telegram = get_telegram_service()
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'symbol': symbol,
|
'symbol': symbol,
|
||||||
|
'stock_name': '', # 从基本面数据获取
|
||||||
'price': 0,
|
'price': 0,
|
||||||
'signals': [],
|
'signals': [],
|
||||||
'notified': False
|
'notified': False
|
||||||
@ -50,23 +56,11 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
threshold = settings.stock_llm_threshold * 100 # 转换为百分比
|
threshold = settings.stock_llm_threshold * 100 # 转换为百分比
|
||||||
|
|
||||||
# 获取股票中文名称
|
|
||||||
stock_name = STOCK_NAMES.get(symbol, '')
|
|
||||||
symbol_display = f"{stock_name}({symbol})" if stock_name else symbol
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print(f"📊 分析 {symbol_display}")
|
print(f"📊 分析 {symbol}")
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取服务
|
|
||||||
yf_service = get_yfinance_service()
|
|
||||||
market_analyzer = StockMarketSignalAnalyzer() # 使用新的市场信号分析器
|
|
||||||
fundamental = get_fundamental_service() # 基本面服务
|
|
||||||
news = get_news_service() # 新闻服务
|
|
||||||
feishu = get_feishu_service()
|
|
||||||
telegram = get_telegram_service()
|
|
||||||
|
|
||||||
# 获取行情
|
# 获取行情
|
||||||
print(f"获取行情...")
|
print(f"获取行情...")
|
||||||
ticker = yf_service.get_ticker(symbol)
|
ticker = yf_service.get_ticker(symbol)
|
||||||
@ -90,15 +84,19 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
|
|
||||||
print(f"时间周期: {', '.join(data.keys())}")
|
print(f"时间周期: {', '.join(data.keys())}")
|
||||||
|
|
||||||
# 获取基本面数据
|
# 获取基本面数据(包含公司名称)
|
||||||
print(f"\n📈 基本面分析中...")
|
print(f"\n📈 基本面分析中...")
|
||||||
fundamental_data = None
|
fundamental_data = None
|
||||||
fundamental_summary = ""
|
fundamental_summary = ""
|
||||||
|
stock_name = ""
|
||||||
try:
|
try:
|
||||||
fundamental_data = fundamental.get_fundamental_data(symbol)
|
fundamental_data = fundamental.get_fundamental_data(symbol)
|
||||||
if fundamental_data:
|
if fundamental_data:
|
||||||
# 传递已获取的数据,避免重复调用
|
# 传递已获取的数据,避免重复调用
|
||||||
fundamental_summary = fundamental.get_fundamental_summary(symbol, fundamental_data)
|
fundamental_summary = fundamental.get_fundamental_summary(symbol, fundamental_data)
|
||||||
|
# 获取公司名称
|
||||||
|
stock_name = fundamental_data.get('company_name', '')
|
||||||
|
result['stock_name'] = stock_name
|
||||||
# 输出基本面详细信息
|
# 输出基本面详细信息
|
||||||
score = fundamental_data.get('score', {})
|
score = fundamental_data.get('score', {})
|
||||||
print(f" ✓ 基本面数据获取成功")
|
print(f" ✓ 基本面数据获取成功")
|
||||||
@ -108,6 +106,12 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
sector = fundamental_data.get('sector', 'N/A')
|
sector = fundamental_data.get('sector', 'N/A')
|
||||||
print(f" 【公司】{company} | {sector}")
|
print(f" 【公司】{company} | {sector}")
|
||||||
|
|
||||||
|
# 更新显示
|
||||||
|
symbol_display = f"{stock_name}({symbol})" if stock_name else symbol
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"📊 分析 {symbol_display} @ ${price:,.2f}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
# 评分
|
# 评分
|
||||||
print(f" 【评分】总分: {score.get('total', 0):.0f}/100 ({score.get('rating', 'N/A')}级) | "
|
print(f" 【评分】总分: {score.get('total', 0):.0f}/100 ({score.get('rating', 'N/A')}级) | "
|
||||||
f"估值:{score.get('valuation', 0)} 盈利:{score.get('profitability', 0)} "
|
f"估值:{score.get('valuation', 0)} 盈利:{score.get('profitability', 0)} "
|
||||||
@ -168,7 +172,6 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
print(f"\n📰 新闻分析...")
|
print(f"\n📰 新闻分析...")
|
||||||
news_data = None
|
news_data = None
|
||||||
try:
|
try:
|
||||||
stock_name = STOCK_NAMES.get(symbol, '')
|
|
||||||
news_data = await news.search_stock_news(symbol, stock_name, max_results=5)
|
news_data = await news.search_stock_news(symbol, stock_name, max_results=5)
|
||||||
if news_data:
|
if news_data:
|
||||||
print(f" 获取到 {len(news_data)} 条相关新闻")
|
print(f" 获取到 {len(news_data)} 条相关新闻")
|
||||||
@ -266,7 +269,6 @@ def print_summary_report(results: list, send_notification: bool = True):
|
|||||||
send_notification: 是否发送通知(默认True)
|
send_notification: 是否发送通知(默认True)
|
||||||
"""
|
"""
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.stock_agent.stock_agent import STOCK_NAMES
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
threshold = settings.stock_llm_threshold * 100 # 获取阈值
|
threshold = settings.stock_llm_threshold * 100 # 获取阈值
|
||||||
|
|
||||||
@ -289,7 +291,7 @@ def print_summary_report(results: list, send_notification: bool = True):
|
|||||||
sig['symbol'] = r['symbol']
|
sig['symbol'] = r['symbol']
|
||||||
sig['current_price'] = r.get('price', 0)
|
sig['current_price'] = r.get('price', 0)
|
||||||
sig['is_hk'] = r['symbol'].endswith('.HK')
|
sig['is_hk'] = r['symbol'].endswith('.HK')
|
||||||
sig['stock_name'] = STOCK_NAMES.get(r['symbol'], '')
|
sig['stock_name'] = r.get('stock_name', '')
|
||||||
all_signals.append(sig)
|
all_signals.append(sig)
|
||||||
|
|
||||||
if sig.get('action') == 'buy':
|
if sig.get('action') == 'buy':
|
||||||
@ -538,7 +540,6 @@ async def main():
|
|||||||
async def send_summary_notification_async(results: list):
|
async def send_summary_notification_async(results: list):
|
||||||
"""异步发送汇总通知"""
|
"""异步发送汇总通知"""
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.stock_agent.stock_agent import STOCK_NAMES
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
threshold = settings.stock_llm_threshold * 100 # 获取阈值
|
threshold = settings.stock_llm_threshold * 100 # 获取阈值
|
||||||
|
|
||||||
@ -561,7 +562,7 @@ async def send_summary_notification_async(results: list):
|
|||||||
sig['symbol'] = r['symbol']
|
sig['symbol'] = r['symbol']
|
||||||
sig['current_price'] = r.get('price', 0)
|
sig['current_price'] = r.get('price', 0)
|
||||||
sig['is_hk'] = r['symbol'].endswith('.HK')
|
sig['is_hk'] = r['symbol'].endswith('.HK')
|
||||||
sig['stock_name'] = STOCK_NAMES.get(r['symbol'], '')
|
sig['stock_name'] = r.get('stock_name', '')
|
||||||
all_signals.append(sig)
|
all_signals.append(sig)
|
||||||
|
|
||||||
if sig.get('action') == 'buy':
|
if sig.get('action') == 'buy':
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user