新架构切换
This commit is contained in:
parent
efc0abf5cb
commit
65f039235e
@ -2,7 +2,6 @@
|
|||||||
加密货币交易智能体模块
|
加密货币交易智能体模块
|
||||||
"""
|
"""
|
||||||
from app.crypto_agent.crypto_agent import CryptoAgent
|
from app.crypto_agent.crypto_agent import CryptoAgent
|
||||||
from app.crypto_agent.signal_analyzer import SignalAnalyzer
|
|
||||||
from app.crypto_agent.strategy import TrendFollowingStrategy
|
from app.crypto_agent.strategy import TrendFollowingStrategy
|
||||||
|
|
||||||
__all__ = ['CryptoAgent', 'SignalAnalyzer', 'TrendFollowingStrategy']
|
__all__ = ['CryptoAgent', 'TrendFollowingStrategy']
|
||||||
|
|||||||
@ -38,7 +38,7 @@ class CryptoAgent:
|
|||||||
|
|
||||||
CryptoAgent._initialized = True
|
CryptoAgent._initialized = True
|
||||||
self.settings = get_settings()
|
self.settings = get_settings()
|
||||||
self.binance = bitget_service # 使用 Bitget 服务
|
self.exchange = bitget_service # 交易所服务
|
||||||
self.feishu = get_feishu_service()
|
self.feishu = get_feishu_service()
|
||||||
self.telegram = get_telegram_service()
|
self.telegram = get_telegram_service()
|
||||||
|
|
||||||
@ -46,9 +46,6 @@ class CryptoAgent:
|
|||||||
self.market_analyzer = MarketSignalAnalyzer()
|
self.market_analyzer = MarketSignalAnalyzer()
|
||||||
self.decision_maker = TradingDecisionMaker()
|
self.decision_maker = TradingDecisionMaker()
|
||||||
|
|
||||||
# 保留旧的 LLM 分析器用于兼容(可选)
|
|
||||||
# self.llm_analyzer = LLMSignalAnalyzer()
|
|
||||||
|
|
||||||
self.signal_db = get_signal_db_service() # 信号数据库服务
|
self.signal_db = get_signal_db_service() # 信号数据库服务
|
||||||
|
|
||||||
# 模拟交易服务(始终启用)
|
# 模拟交易服务(始终启用)
|
||||||
@ -406,7 +403,7 @@ class CryptoAgent:
|
|||||||
logger.info(f"{'─' * 50}")
|
logger.info(f"{'─' * 50}")
|
||||||
|
|
||||||
# 1. 获取多周期数据
|
# 1. 获取多周期数据
|
||||||
data = self.binance.get_multi_timeframe_data(symbol)
|
data = self.exchange.get_multi_timeframe_data(symbol)
|
||||||
|
|
||||||
if not self._validate_data(data):
|
if not self._validate_data(data):
|
||||||
logger.warning(f"⚠️ {symbol} 数据不完整,跳过分析")
|
logger.warning(f"⚠️ {symbol} 数据不完整,跳过分析")
|
||||||
@ -1483,7 +1480,7 @@ class CryptoAgent:
|
|||||||
|
|
||||||
async def analyze_once(self, symbol: str) -> Dict[str, Any]:
|
async def analyze_once(self, symbol: str) -> Dict[str, Any]:
|
||||||
"""单次分析(用于测试或手动触发)"""
|
"""单次分析(用于测试或手动触发)"""
|
||||||
data = self.binance.get_multi_timeframe_data(symbol)
|
data = self.exchange.get_multi_timeframe_data(symbol)
|
||||||
|
|
||||||
if not self._validate_data(data):
|
if not self._validate_data(data):
|
||||||
return {'error': '数据不完整'}
|
return {'error': '数据不完整'}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -83,9 +83,10 @@ class YFinanceService:
|
|||||||
多时间周期数据字典 {'1d': df, '1h': df, ...}
|
多时间周期数据字典 {'1d': df, '1h': df, ...}
|
||||||
"""
|
"""
|
||||||
if timeframes is None:
|
if timeframes is None:
|
||||||
# 默认时间周期配置
|
# 默认时间周期配置 - 股票不需要太实时的数据
|
||||||
timeframes = {
|
timeframes = {
|
||||||
'1d': ('1d', '3mo'), # 日级别,3个月
|
'1w': ('1wk', '2y'), # 周级别,2年
|
||||||
|
'1d': ('1d', '6mo'), # 日级别,6个月
|
||||||
'1h': ('1h', '1mo'), # 小时级别,1个月
|
'1h': ('1h', '1mo'), # 小时级别,1个月
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
738
backend/app/stock_agent/market_signal_analyzer.py
Normal file
738
backend/app/stock_agent/market_signal_analyzer.py
Normal file
@ -0,0 +1,738 @@
|
|||||||
|
"""
|
||||||
|
股票市场信号分析器 - 纯市场分析,不包含任何仓位信息
|
||||||
|
|
||||||
|
职责:
|
||||||
|
1. 分析K线、量价、技术指标
|
||||||
|
2. 分析新闻舆情
|
||||||
|
3. 输出纯市场信号(buy/sell/hold + confidence + reasoning)
|
||||||
|
|
||||||
|
不负责:
|
||||||
|
- 仓位管理
|
||||||
|
- 风险控制
|
||||||
|
- 具体下单决策
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import pandas as pd
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
from app.utils.logger import logger
|
||||||
|
from app.services.llm_service import llm_service
|
||||||
|
|
||||||
|
|
||||||
|
class StockMarketSignalAnalyzer:
|
||||||
|
"""股票市场信号分析器 - 只关注市场,输出客观信号"""
|
||||||
|
|
||||||
|
# 股票市场分析系统提示词
|
||||||
|
MARKET_ANALYSIS_PROMPT = """你是一位专业的股票交易员和技术分析师。你的任务是综合分析**技术面(K线、量价、技术指标)、基本面(估值、盈利、成长)、新闻舆情**,给出交易信号。
|
||||||
|
|
||||||
|
## 核心理念
|
||||||
|
股票市场有其独特的运行规律:
|
||||||
|
- **趋势为王**:股票更容易形成持续性趋势,顺势而为最重要
|
||||||
|
- **基本面支撑**:技术信号需要结合公司基本面来判断有效性
|
||||||
|
- **新闻催化**:重大新闻会改变短期趋势,需要重点关注
|
||||||
|
- **关注财报季**:财报前后波动加大,需要更谨慎
|
||||||
|
- **板块效应**:同板块股票往往联动,关注板块整体表现
|
||||||
|
|
||||||
|
## 数据说明
|
||||||
|
你将获得三个维度的数据:
|
||||||
|
1. **技术面数据**:K线、量价、技术指标(RSI、MACD、布林带、均线)
|
||||||
|
2. **基本面数据**:估值指标(PE、PB)、盈利能力(ROE、净利率)、成长性(营收增长、盈利增长)、财务健康度
|
||||||
|
3. **新闻舆情**:最新相关新闻
|
||||||
|
|
||||||
|
## 分析框架(重要!)
|
||||||
|
### 优先级排序:
|
||||||
|
1. **技术面** = 40%:K线、量价、技术指标决定入场时机
|
||||||
|
2. **基本面** = 35%:估值和盈利能力决定信号的长期有效性
|
||||||
|
3. **新闻** = 25%:重大新闻可能改变短期趋势
|
||||||
|
|
||||||
|
### 综合判断规则:
|
||||||
|
- **技术面强 + 基本面好 + 无负面新闻** → A级信号,高置信度
|
||||||
|
- **技术面强 + 基本面一般** → B级信号,中等置信度
|
||||||
|
- **技术面一般 + 基本面好** → C级信号,低置信度,观望为主
|
||||||
|
- **技术面强 + 基本面差 + 有负面新闻** → D级信号,不推荐交易
|
||||||
|
- **技术面弱** → 无论基本面如何,不推荐交易(观望)
|
||||||
|
|
||||||
|
## 一、量价分析(最重要)
|
||||||
|
量价关系是判断趋势真假的核心:
|
||||||
|
|
||||||
|
### 1. 健康上涨信号
|
||||||
|
- **放量上涨**:价格上涨 + 成交量放大(量比>1.5)= 上涨有效,可追多
|
||||||
|
- **缩量回调**:上涨后回调 + 成交量萎缩(量比<0.7)= 回调健康,可低吸
|
||||||
|
- **温和放量**:量比在1.2-1.5之间,价格稳步上涨 = 最健康的上涨
|
||||||
|
|
||||||
|
### 2. 健康下跌信号
|
||||||
|
- **放量下跌**:价格下跌 + 成交量放大 = 下跌有效,暂不抄底
|
||||||
|
- **缩量阴跌**:下跌 + 成交量萎缩 = 抛压逐渐枯竭,关注反弹
|
||||||
|
- **地量企稳**:极端缩量后价格横盘 = 可能见底
|
||||||
|
|
||||||
|
### 3. 量价背离(重要反转信号)
|
||||||
|
- **顶背离**:价格创新高,但成交量未创新高 → 上涨动能衰竭
|
||||||
|
- **底背离**:价格创新低,但成交量未创新低 → 下跌动能衰竭
|
||||||
|
- **天量见顶**:单日成交量突然放大2-3倍后价格滞涨 → 主力出货
|
||||||
|
- **地量见底**:成交量创阶段新低后价格企稳 → 抛压枯竭
|
||||||
|
|
||||||
|
### 4. 突破确认
|
||||||
|
- **有效突破**:突破关键位 + 放量确认(量比>1.3)= 真突破
|
||||||
|
- **假突破**:突破关键位 + 缩量 = 假突破,可能回落
|
||||||
|
|
||||||
|
## 二、K线形态分析
|
||||||
|
### 反转形态
|
||||||
|
- **锤子线/倒锤子**:下跌趋势中出现,下影线长 = 底部信号
|
||||||
|
- **吞没形态**:大阳吞没前一根阴线 = 看涨;大阴吞没前一根阳线 = 看跌
|
||||||
|
- **十字星**:在高位/低位出现 = 变盘信号
|
||||||
|
- **早晨之星/黄昏之星**:三根K线组合的反转信号
|
||||||
|
|
||||||
|
### 持续形态
|
||||||
|
- **三连阳/三连阴**:趋势延续信号
|
||||||
|
- **旗形整理**:趋势中的健康回调
|
||||||
|
|
||||||
|
## 三、技术指标分析
|
||||||
|
### RSI(相对强弱指标)
|
||||||
|
**RSI 是最重要的超买超卖指标:**
|
||||||
|
- **RSI < 30**:超卖区,关注反弹机会
|
||||||
|
- RSI 从 30 以下回升,交叉上穿 30:买入信号
|
||||||
|
- RSI 底背离(价格新低但 RSI 未创新低):强买入信号
|
||||||
|
- **RSI > 70**:超买区,关注回落风险
|
||||||
|
- RSI 从 70 以上回落,交叉下穿 70:卖出信号
|
||||||
|
- RSI 顶背离(价格新高但 RSI 未创新高):强卖出信号
|
||||||
|
- **RSI 40-60**:震荡区,观望为主
|
||||||
|
|
||||||
|
### MACD
|
||||||
|
- 金叉(DIF 上穿 DEA):做多信号
|
||||||
|
- 死叉(DIF 下穿 DEA):做空信号
|
||||||
|
- 零轴上方金叉:强势做多
|
||||||
|
- 零轴下方死叉:强势做空
|
||||||
|
- MACD 柱状图背离:重要反转信号
|
||||||
|
|
||||||
|
### 布林带
|
||||||
|
- 触及下轨 + 企稳:反弹做多
|
||||||
|
- 触及上轨 + 受阻:回落做空
|
||||||
|
- 布林带收口:即将变盘
|
||||||
|
- 布林带开口:趋势启动
|
||||||
|
|
||||||
|
### 均线系统(重要)
|
||||||
|
**均线系统是趋势判断的核心:**
|
||||||
|
- **多头排列**(MA5 > MA10 > MA20 > MA50):强势上涨趋势,回调做多
|
||||||
|
- **空头排列**(MA5 < MA10 < MA20 < MA50):强势下跌趋势,反弹做空
|
||||||
|
- **价格与 MA 的关系**:
|
||||||
|
- 价格站稳 MA5/MA10 上方:短线上涨
|
||||||
|
- 价格突破 MA20:中线转多
|
||||||
|
- 价格跌破 MA20:中线转空
|
||||||
|
- MA50 是中期趋势的分水岭
|
||||||
|
- **均线金叉死叉**:
|
||||||
|
- MA5 上穿 MA10:短线买入信号
|
||||||
|
- MA5 下穿 MA10:短线卖出信号
|
||||||
|
|
||||||
|
## 四、多周期共振(关键分析框架)
|
||||||
|
**多周期共振是提高信号质量的核心方法:**
|
||||||
|
|
||||||
|
### 周期层级关系
|
||||||
|
- **周线(趋势层)**:决定长期大方向
|
||||||
|
- **日线(主周期)**:主要交易周期
|
||||||
|
- **1h(入场层)**:寻找入场时机
|
||||||
|
|
||||||
|
### 共振判断标准
|
||||||
|
**强共振(A级信号)**:
|
||||||
|
- 所有周期趋势同向(如周线多 + 日线多 + 1h多)
|
||||||
|
- 多周期 RSI 同时超买/超卖后出现背离
|
||||||
|
- 多周期 MA 同时金叉/死叉
|
||||||
|
|
||||||
|
**中等共振(B级信号)**:
|
||||||
|
- 大周期(周线+日线)同向
|
||||||
|
- 主周期(日线)技术指标明确
|
||||||
|
|
||||||
|
**弱共振(C级信号)**:
|
||||||
|
- 只有单一周期信号
|
||||||
|
- 多周期方向不一致
|
||||||
|
|
||||||
|
### 实战策略
|
||||||
|
- **顺势交易**:周线和日线同向时,在 1h 寻找入场点
|
||||||
|
- **逆势谨慎**:只有日线信号但周线反向时,降低置信度
|
||||||
|
- **突破交易**:多周期同时突破关键位,信号最强
|
||||||
|
|
||||||
|
## 五、基本面分析(重要)
|
||||||
|
**基本面是判断信号长期有效性的关键:**
|
||||||
|
|
||||||
|
### 估值指标
|
||||||
|
- **PE(市盈率)**:
|
||||||
|
- PE < 15:低估,安全边际高
|
||||||
|
- PE 15-25:合理估值
|
||||||
|
- PE > 40:高估,风险较大
|
||||||
|
- **PB(市净率)**:
|
||||||
|
- PB < 1:低于净资产,价值投资机会
|
||||||
|
- PB 1-3:合理区间
|
||||||
|
- PB > 5:高估
|
||||||
|
- **PEG(市盈率增长率)**:
|
||||||
|
- PEG < 1:低估,成长性好
|
||||||
|
- PEG 1-2:合理
|
||||||
|
- PEG > 2:高估
|
||||||
|
|
||||||
|
### 盈利能力
|
||||||
|
- **ROE(净资产收益率)**:
|
||||||
|
- ROE > 20%:优秀
|
||||||
|
- ROE 15-20%:良好
|
||||||
|
- ROE < 10%:较差
|
||||||
|
- **净利率**:
|
||||||
|
- 净利率 > 20%:优秀(通常是科技、消费品牌)
|
||||||
|
- 净利率 10-20%:良好
|
||||||
|
- 净利率 < 5%:较低(通常是零售、制造业)
|
||||||
|
|
||||||
|
### 成长性
|
||||||
|
- **营收增长**:
|
||||||
|
- > 30%:高成长
|
||||||
|
- 20-30%:稳健成长
|
||||||
|
- < 10%:低成长
|
||||||
|
- **盈利增长**:
|
||||||
|
- > 30%:高成长
|
||||||
|
- 10-30%:稳健成长
|
||||||
|
- < 0%:负增长,警惕
|
||||||
|
|
||||||
|
### 财务健康
|
||||||
|
- **债务股本比**:
|
||||||
|
- < 1:健康
|
||||||
|
- 1-2:可控
|
||||||
|
- > 3:高风险
|
||||||
|
- **流动比率**:
|
||||||
|
- > 2:健康
|
||||||
|
- 1.5-2:良好
|
||||||
|
- < 1:流动性风险
|
||||||
|
|
||||||
|
### 基本面综合判断
|
||||||
|
- **基本面优秀**(ROE>15%, 营收增长>20%, 财务健康)+ 技术面信号 = 提高置信度
|
||||||
|
- **基本面一般**(ROE 10-15%, 营收增长 10-20%)+ 技术面信号 = 正常置信度
|
||||||
|
- **基本面较差**(ROE<10%, 营收增长<10% 或负增长, 高负债)+ 技术面信号 = 降低置信度
|
||||||
|
- **基本面差**(连续亏损, 高负债, 负增长)= 不建议交易,无论技术面如何
|
||||||
|
|
||||||
|
## 六、新闻舆情分析
|
||||||
|
**新闻会改变短期趋势,需要重点关注:**
|
||||||
|
|
||||||
|
### 正面新闻(提高做多置信度)
|
||||||
|
- 财报超预期
|
||||||
|
- 重大产品发布
|
||||||
|
- 业务扩张/并购
|
||||||
|
- 分析师上调评级
|
||||||
|
- 行业利好政策
|
||||||
|
|
||||||
|
### 负面新闻(提高做空置信度或降低做多置信度)
|
||||||
|
- 财报不及预期
|
||||||
|
- 监管调查/处罚
|
||||||
|
- 管理层变动
|
||||||
|
- 分析师下调评级
|
||||||
|
- 行业监管收紧
|
||||||
|
- 重大安全事故/质量问题
|
||||||
|
|
||||||
|
### 新闻综合判断
|
||||||
|
- **重大正面新闻** + 技术面做多信号 = 提高置信度 10-20%
|
||||||
|
- **重大负面新闻** + 技术面做多信号 = 降低置信度或转为观望
|
||||||
|
- **无重大新闻** = 技术面 + 基本面分析为主
|
||||||
|
|
||||||
|
## 七、入场方式
|
||||||
|
根据市场分析综合判断入场方式:
|
||||||
|
- **market**:现价立即入场
|
||||||
|
- 信号强烈且明确(A级或高置信度B级)
|
||||||
|
- 放量突破关键位,趋势明确
|
||||||
|
- 多周期共振,等待可能错过机会
|
||||||
|
- 市场波动大,等待可能价格变化太快
|
||||||
|
- **limit**:挂单等待入场
|
||||||
|
- 信号强度中等(B级或C级)
|
||||||
|
- 当前价格距离理想入场位有一定距离
|
||||||
|
- 判断市场可能回调到更好位置
|
||||||
|
- 希望获得更优成交价格,愿意承担可能无法成交的风险
|
||||||
|
|
||||||
|
**重要**:
|
||||||
|
- 必须同时输出 `entry_zone`(建议入场价)和 `entry_type`(入场方式)
|
||||||
|
- 入场方式由你的市场分析判断,不是简单的价格距离计算
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
请严格按照以下 JSON 格式输出:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"analysis_summary": "简要描述当前市场状态(50字以内)",
|
||||||
|
"volume_analysis": "量价分析结论(30字以内)",
|
||||||
|
"signals": [
|
||||||
|
{
|
||||||
|
"type": "short_term/medium_term/long_term",
|
||||||
|
"action": "buy/sell",
|
||||||
|
"entry_type": "market/limit",
|
||||||
|
"confidence": 0-100,
|
||||||
|
"grade": "A/B/C/D",
|
||||||
|
"entry_zone": 150.50,
|
||||||
|
"stop_loss": 148.00,
|
||||||
|
"take_profit": 155.00,
|
||||||
|
"reasoning": "详细的入场理由(必须包含量价分析)",
|
||||||
|
"key_factors": ["关键因素1", "关键因素2"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"key_levels": {
|
||||||
|
"support": [148, 145],
|
||||||
|
"resistance": [152, 155]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 重要说明
|
||||||
|
- **所有价格必须是纯数字**,不要加 $ 符号、逗号或其他格式
|
||||||
|
- `entry_zone`、`stop_loss`、`take_profit` 必须是数字类型,不要是字符串
|
||||||
|
- `key_levels` 中的支撑位和阻力位也必须是数字数组
|
||||||
|
|
||||||
|
## 信号等级与置信度(综合技术面 + 基本面 + 新闻)
|
||||||
|
- **A级**(80-100):量价配合 + 多指标共振 + 多周期确认 + 基本面优秀 + 无负面新闻
|
||||||
|
- **B级**(60-79):量价配合 + 主要指标确认 + 基本面良好/一般
|
||||||
|
- **C级**(40-59):技术面有机会但基本面一般,或基本面好但技术面不够明确
|
||||||
|
- **D级**(<40):量价背离或信号矛盾,或基本面差,或有重大负面新闻
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
1. **只在有明确的做多或做空机会时才输出信号**(action 为 buy 或 sell)
|
||||||
|
2. 如果市场不明朗,没有明确交易机会,**不要输出任何信号**(signals 为空数组 [])
|
||||||
|
3. 信号强度(confidence)要合理,不要随意给高分
|
||||||
|
4. 60-70分:一般信号,可轻仓试探
|
||||||
|
5. 75-85分:较强信号,可正常仓位
|
||||||
|
6. 90+分:强信号,但也要控制风险
|
||||||
|
7. 不要输出 action 为 "wait" 的信号,如果没有交易机会就不输出
|
||||||
|
8. **必须综合考虑技术面、基本面、新闻三个维度**,不能只看技术面
|
||||||
|
|
||||||
|
记住:你只负责分析市场,输出客观的交易信号,不需要考虑仓位管理和风险控制!
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def analyze(self, symbol: str, data: Dict[str, Any],
|
||||||
|
symbols: List[str] = None,
|
||||||
|
fundamental_data: Dict[str, Any] = None,
|
||||||
|
news_data: List[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
分析市场并生成信号
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: 股票代码
|
||||||
|
data: 多周期K线数据
|
||||||
|
symbols: 所有监控的股票(用于市场对比)
|
||||||
|
fundamental_data: 基本面数据
|
||||||
|
news_data: 新闻数据列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
市场信号字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1. 准备市场数据(技术面 + 基本面 + 新闻)
|
||||||
|
market_context = self._prepare_market_context(
|
||||||
|
symbol, data, symbols,
|
||||||
|
fundamental_data, news_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 构建 LLM 提示词
|
||||||
|
prompt = self._build_analysis_prompt(symbol, market_context)
|
||||||
|
|
||||||
|
# 3. 调用 LLM 分析
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": self.MARKET_ANALYSIS_PROMPT},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
]
|
||||||
|
response = await llm_service.achat(messages)
|
||||||
|
|
||||||
|
# 4. 解析结果
|
||||||
|
result = self._parse_llm_response(response, symbol)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"市场信号分析失败: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.debug(traceback.format_exc())
|
||||||
|
return self._get_empty_signal(symbol)
|
||||||
|
|
||||||
|
def _prepare_market_context(self, symbol: str, data: Dict,
|
||||||
|
symbols: List[str] = None,
|
||||||
|
fundamental_data: Dict[str, Any] = None,
|
||||||
|
news_data: List[Dict[str, Any]] = None) -> str:
|
||||||
|
"""准备市场上下文信息(技术面 + 基本面 + 新闻)"""
|
||||||
|
context_parts = []
|
||||||
|
|
||||||
|
# 当前价格和24h变化(使用日线数据)
|
||||||
|
df_1d = data.get('1d')
|
||||||
|
if df_1d is None or len(df_1d) == 0:
|
||||||
|
df_1h = data.get('1h') # 备用:使用1h数据
|
||||||
|
if df_1d is None or len(df_1d) == 0:
|
||||||
|
return "" # 没有数据就返回空
|
||||||
|
|
||||||
|
current_price = float(df_1d.iloc[-1]['close'])
|
||||||
|
price_change_24h = self._calculate_price_change_24h(df_1d)
|
||||||
|
context_parts.append(f"当前价格: ${current_price:,.2f} ({price_change_24h})")
|
||||||
|
|
||||||
|
# 多周期数据
|
||||||
|
for tf_name, df in data.items():
|
||||||
|
if df is None or len(df) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
latest = df.iloc[-1]
|
||||||
|
context_parts.append(f"\n## {tf_name} 数据")
|
||||||
|
context_parts.append(f"开: {latest['open']}, 高: {latest['high']}, 低: {latest['low']}, 收: {latest['close']}")
|
||||||
|
context_parts.append(f"成交量: {latest.get('volume', 'N/A')}")
|
||||||
|
|
||||||
|
# 技术指标
|
||||||
|
if 'rsi' in df.columns:
|
||||||
|
rsi = df['rsi'].iloc[-1]
|
||||||
|
context_parts.append(f"RSI: {rsi:.2f}")
|
||||||
|
if 'macd' in df.columns:
|
||||||
|
macd = df['macd'].iloc[-1]
|
||||||
|
signal = df['macd_signal'].iloc[-1]
|
||||||
|
context_parts.append(f"MACD: {macd:.4f}, 信号线: {signal:.4f}")
|
||||||
|
if 'bb_upper' in df.columns:
|
||||||
|
bb_upper = df['bb_upper'].iloc[-1]
|
||||||
|
bb_lower = df['bb_lower'].iloc[-1]
|
||||||
|
context_parts.append(f"布林带: 上轨 {bb_upper:.2f}, 下轨 {bb_lower:.2f}")
|
||||||
|
|
||||||
|
# 均线系统(使用日线数据)
|
||||||
|
context_parts.append(f"\n## 均线系统")
|
||||||
|
df_1d = data.get('1d')
|
||||||
|
if df_1d is not None and len(df_1d) > 0:
|
||||||
|
latest = df_1d.iloc[-1]
|
||||||
|
context_parts.append(f"MA5: {latest.get('ma5', 'N/A')}")
|
||||||
|
context_parts.append(f"MA10: {latest.get('ma10', 'N/A')}")
|
||||||
|
context_parts.append(f"MA20: {latest.get('ma20', 'N/A')}")
|
||||||
|
context_parts.append(f"MA50: {latest.get('ma50', 'N/A')}")
|
||||||
|
|
||||||
|
# 判断均线排列
|
||||||
|
ma5 = latest.get('ma5', 0)
|
||||||
|
ma10 = latest.get('ma10', 0)
|
||||||
|
ma20 = latest.get('ma20', 0)
|
||||||
|
ma50 = latest.get('ma50', 0)
|
||||||
|
|
||||||
|
if all([ma5, ma10, ma20, ma50]):
|
||||||
|
if ma5 > ma10 > ma20 > ma50:
|
||||||
|
context_parts.append("均线排列: 多头排列 📈")
|
||||||
|
elif ma5 < ma10 < ma20 < ma50:
|
||||||
|
context_parts.append("均线排列: 空头排列 📉")
|
||||||
|
else:
|
||||||
|
context_parts.append("均线排列: 交织,方向不明")
|
||||||
|
|
||||||
|
# 量比分析(使用日线数据)
|
||||||
|
df_1d = data.get('1d')
|
||||||
|
if df_1d is not None and len(df_1d) >= 20:
|
||||||
|
vol_latest = df_1d['volume'].iloc[-1]
|
||||||
|
vol_ma20 = df_1d['volume'].iloc[-20:-1].mean()
|
||||||
|
volume_ratio = vol_latest / vol_ma20 if vol_ma20 > 0 else 1
|
||||||
|
context_parts.append(f"\n## 量价分析")
|
||||||
|
context_parts.append(f"最新成交量: {vol_latest:.0f}")
|
||||||
|
context_parts.append(f"20周期均量: {vol_ma20:.0f}")
|
||||||
|
context_parts.append(f"量比: {volume_ratio:.2f}")
|
||||||
|
|
||||||
|
if volume_ratio > 1.5:
|
||||||
|
context_parts.append("量价状态: 放量 📊")
|
||||||
|
elif volume_ratio < 0.7:
|
||||||
|
context_parts.append("量价状态: 缩量 📉")
|
||||||
|
else:
|
||||||
|
context_parts.append("量价状态: 平量 ➖")
|
||||||
|
|
||||||
|
# 波动率分析
|
||||||
|
volatility_analysis = self._analyze_volatility(data)
|
||||||
|
if volatility_analysis:
|
||||||
|
context_parts.append(f"\n## 波动率分析")
|
||||||
|
context_parts.append(volatility_analysis)
|
||||||
|
|
||||||
|
# 基本面分析
|
||||||
|
if fundamental_data:
|
||||||
|
context_parts.append(f"\n## 基本面分析")
|
||||||
|
context_parts.append(self._format_fundamental_data(fundamental_data))
|
||||||
|
|
||||||
|
# 新闻舆情
|
||||||
|
if news_data:
|
||||||
|
context_parts.append(f"\n## 最新新闻")
|
||||||
|
context_parts.append(self._format_news_data(news_data))
|
||||||
|
|
||||||
|
return "\n".join(context_parts)
|
||||||
|
|
||||||
|
def _build_analysis_prompt(self, symbol: str, market_context: str) -> str:
|
||||||
|
"""构建分析提示词"""
|
||||||
|
return f"""请分析 {symbol} 的市场情况:
|
||||||
|
|
||||||
|
{market_context}
|
||||||
|
|
||||||
|
请根据以上数据,给出你的市场判断和交易信号。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _parse_llm_response(self, response: str, symbol: str) -> Dict[str, Any]:
|
||||||
|
"""解析 LLM 响应"""
|
||||||
|
try:
|
||||||
|
# 尝试提取 JSON
|
||||||
|
json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response)
|
||||||
|
if json_match:
|
||||||
|
json_str = json_match.group(1)
|
||||||
|
else:
|
||||||
|
json_match = re.search(r'\{[\s\S]*\}', response)
|
||||||
|
if json_match:
|
||||||
|
json_str = json_match.group(0)
|
||||||
|
else:
|
||||||
|
raise ValueError("无法找到 JSON 响应")
|
||||||
|
|
||||||
|
# 清理 JSON 字符串
|
||||||
|
json_str = self._clean_json_string(json_str)
|
||||||
|
|
||||||
|
result = json.loads(json_str)
|
||||||
|
|
||||||
|
# 清理价格字段 - 转换为 float
|
||||||
|
result = self._clean_price_fields(result)
|
||||||
|
|
||||||
|
# 添加元数据
|
||||||
|
result['symbol'] = symbol
|
||||||
|
result['timestamp'] = datetime.now().isoformat()
|
||||||
|
result['raw_response'] = response
|
||||||
|
|
||||||
|
# 兼容处理:确保 signals 中的字段与旧格式一致
|
||||||
|
if 'signals' in result:
|
||||||
|
for sig in result['signals']:
|
||||||
|
if 'type' in sig:
|
||||||
|
if sig['type'] in ['short_term', 'medium_term', 'long_term']:
|
||||||
|
sig['timeframe'] = sig.pop('type')
|
||||||
|
elif sig['type'] in ['buy', 'sell', 'wait']:
|
||||||
|
sig['action'] = sig.pop('type')
|
||||||
|
|
||||||
|
if 'action' not in sig and 'timeframe' in sig:
|
||||||
|
sig['action'] = 'wait'
|
||||||
|
|
||||||
|
if 'grade' not in sig:
|
||||||
|
confidence = sig.get('confidence', 0)
|
||||||
|
if confidence >= 80:
|
||||||
|
sig['grade'] = 'A'
|
||||||
|
elif confidence >= 60:
|
||||||
|
sig['grade'] = 'B'
|
||||||
|
elif confidence >= 40:
|
||||||
|
sig['grade'] = 'C'
|
||||||
|
else:
|
||||||
|
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}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"解析 LLM 响应失败: {e}")
|
||||||
|
logger.warning(f"原始响应: {response[:1000]}...")
|
||||||
|
return self._get_empty_signal(symbol)
|
||||||
|
|
||||||
|
def _clean_json_string(self, json_str: str) -> str:
|
||||||
|
"""清理 JSON 字符串,移除可能导致解析错误的内容"""
|
||||||
|
import re
|
||||||
|
json_str = re.sub(r'//.*?(?=\n|$)', '', json_str)
|
||||||
|
json_str = re.sub(r'/\*[\s\S]*?\*/', '', json_str)
|
||||||
|
json_str = re.sub(r',\s*([}\]])', r'\1', json_str)
|
||||||
|
return json_str
|
||||||
|
|
||||||
|
def _clean_price_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""清理价格字段,转换为 float"""
|
||||||
|
def clean_price(price_value):
|
||||||
|
if price_value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(price_value, (int, float)):
|
||||||
|
return float(price_value)
|
||||||
|
if isinstance(price_value, str):
|
||||||
|
cleaned = price_value.replace('$', '').replace(',', '').strip()
|
||||||
|
if cleaned:
|
||||||
|
try:
|
||||||
|
return float(cleaned)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
if 'key_levels' in data and data['key_levels']:
|
||||||
|
key_levels = data['key_levels']
|
||||||
|
if 'support' in key_levels:
|
||||||
|
data['key_levels']['support'] = [clean_price(s) for s in key_levels['support']]
|
||||||
|
if 'resistance' in key_levels:
|
||||||
|
data['key_levels']['resistance'] = [clean_price(r) for r in key_levels['resistance']]
|
||||||
|
|
||||||
|
if 'signals' in data:
|
||||||
|
for sig in data['signals']:
|
||||||
|
price_fields = ['entry_zone', 'stop_loss', 'take_profit']
|
||||||
|
for field in price_fields:
|
||||||
|
if field in sig:
|
||||||
|
sig[field] = clean_price(sig[field])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _calculate_price_change_24h(self, df) -> str:
|
||||||
|
"""计算24小时涨跌幅"""
|
||||||
|
try:
|
||||||
|
if df is None or len(df) < 24:
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
current_price = float(df['close'].iloc[-1])
|
||||||
|
price_24h_ago = float(df['close'].iloc[-24])
|
||||||
|
change = ((current_price - price_24h_ago) / price_24h_ago) * 100
|
||||||
|
|
||||||
|
sign = "+" if change >= 0 else ""
|
||||||
|
return f"{sign}{change:.2f}%"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"计算24h涨跌失败: {e}")
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
def _analyze_volatility(self, data: Dict[str, pd.DataFrame]) -> str:
|
||||||
|
"""分析波动率变化(使用日线数据)"""
|
||||||
|
df = data.get('1d')
|
||||||
|
if df is None or len(df) < 24 or 'atr' not in df.columns:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
recent_atr = df['atr'].iloc[-6:].mean()
|
||||||
|
older_atr = df['atr'].iloc[-12:-6].mean()
|
||||||
|
|
||||||
|
if pd.isna(recent_atr) or pd.isna(older_atr) or older_atr == 0:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
atr_change = (recent_atr - older_atr) / older_atr * 100
|
||||||
|
|
||||||
|
current_atr = float(df['atr'].iloc[-1])
|
||||||
|
current_price = float(df['close'].iloc[-1])
|
||||||
|
atr_percent = current_atr / current_price * 100
|
||||||
|
|
||||||
|
lines.append(f"当前 ATR: ${current_atr:.2f} ({atr_percent:.2f}%)")
|
||||||
|
|
||||||
|
if atr_change > 20:
|
||||||
|
lines.append(f"**波动率扩张**: ATR 上升 {atr_change:.0f}%,趋势可能启动")
|
||||||
|
elif atr_change < -20:
|
||||||
|
lines.append(f"**波动率收缩**: ATR 下降 {abs(atr_change):.0f}%,可能即将突破")
|
||||||
|
else:
|
||||||
|
lines.append(f"波动率稳定: ATR 变化 {atr_change:+.0f}%")
|
||||||
|
|
||||||
|
if 'bb_upper' in df.columns and 'bb_lower' in df.columns:
|
||||||
|
bb_width = (float(df['bb_upper'].iloc[-1]) - float(df['bb_lower'].iloc[-1])) / current_price * 100
|
||||||
|
bb_width_prev = (float(df['bb_upper'].iloc[-6]) - float(df['bb_lower'].iloc[-6])) / float(df['close'].iloc[-6]) * 100
|
||||||
|
|
||||||
|
if bb_width < bb_width_prev * 0.8:
|
||||||
|
lines.append(f"**布林带收口**: 宽度 {bb_width:.1f}%,变盘信号")
|
||||||
|
elif bb_width > bb_width_prev * 1.2:
|
||||||
|
lines.append(f"**布林带开口**: 宽度 {bb_width:.1f}%,趋势延续")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _format_fundamental_data(self, data: Dict[str, Any]) -> str:
|
||||||
|
"""格式化基本面数据"""
|
||||||
|
if not data:
|
||||||
|
return "暂无基本面数据"
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
# 基本信息
|
||||||
|
company_name = data.get('company_name', 'N/A')
|
||||||
|
sector = data.get('sector', 'N/A')
|
||||||
|
lines.append(f"公司: {company_name}")
|
||||||
|
lines.append(f"行业: {sector}")
|
||||||
|
|
||||||
|
# 估值指标
|
||||||
|
val = data.get('valuation', {})
|
||||||
|
if val.get('pe_ratio'):
|
||||||
|
pe = val['pe_ratio']
|
||||||
|
pb = val.get('pb_ratio')
|
||||||
|
ps = val.get('ps_ratio')
|
||||||
|
peg = val.get('peg_ratio')
|
||||||
|
pb_str = f"{pb:.2f}" if pb is not None else "N/A"
|
||||||
|
ps_str = f"{ps:.2f}" if ps is not None else "N/A"
|
||||||
|
peg_str = f"{peg:.2f}" if peg is not None else "N/A"
|
||||||
|
lines.append(f"估值: PE={pe:.2f} | PB={pb_str} | PS={ps_str} | PEG={peg_str}")
|
||||||
|
|
||||||
|
# 盈利能力
|
||||||
|
prof = data.get('profitability', {})
|
||||||
|
if prof.get('return_on_equity'):
|
||||||
|
roe = prof['return_on_equity']
|
||||||
|
pm = prof.get('profit_margin')
|
||||||
|
gm = prof.get('gross_margin')
|
||||||
|
pm_str = f"{pm:.1f}" if pm is not None else "N/A"
|
||||||
|
gm_str = f"{gm:.1f}" if gm is not None else "N/A"
|
||||||
|
lines.append(f"盈利: ROE={roe:.2f}% | 净利率={pm_str}% | 毛利率={gm_str}%")
|
||||||
|
|
||||||
|
# 成长性
|
||||||
|
growth = data.get('growth', {})
|
||||||
|
rg = growth.get('revenue_growth')
|
||||||
|
eg = growth.get('earnings_growth')
|
||||||
|
if rg is not None or eg is not None:
|
||||||
|
rg_str = f"{rg:.1f}" if rg is not None else "N/A"
|
||||||
|
eg_str = f"{eg:.1f}" if eg is not None else "N/A"
|
||||||
|
lines.append(f"成长: 营收增长={rg_str}% | 盈利增长={eg_str}%")
|
||||||
|
|
||||||
|
# 财务健康
|
||||||
|
fin = data.get('financial_health', {})
|
||||||
|
if fin.get('debt_to_equity'):
|
||||||
|
de = fin['debt_to_equity']
|
||||||
|
cr = fin.get('current_ratio')
|
||||||
|
cr_str = f"{cr:.2f}" if cr is not None else "N/A"
|
||||||
|
lines.append(f"财务: 债务股本比={de:.2f} | 流动比率={cr_str}")
|
||||||
|
|
||||||
|
# 分析师建议
|
||||||
|
analyst = data.get('analyst', {})
|
||||||
|
if analyst.get('target_price'):
|
||||||
|
tp = analyst['target_price']
|
||||||
|
rec = analyst.get('recommendation', 'N/A')
|
||||||
|
lines.append(f"分析师: 目标价=${tp:.2f} | 评级={rec}")
|
||||||
|
|
||||||
|
# 基本面评分
|
||||||
|
score = data.get('score', {})
|
||||||
|
if score.get('total'):
|
||||||
|
lines.append(f"基本面评分: {score['total']:.0f}/100 ({score.get('rating', 'N/A')}级)")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _format_news_data(self, news_list: List[Dict[str, Any]]) -> str:
|
||||||
|
"""格式化新闻数据"""
|
||||||
|
if not news_list:
|
||||||
|
return "暂无相关新闻"
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for i, news in enumerate(news_list[:5], 1): # 最多5条
|
||||||
|
title = news.get('title', '')
|
||||||
|
desc = news.get('description', '')[:150] # 限制描述长度
|
||||||
|
source = news.get('source', '')
|
||||||
|
time_str = news.get('time_str', '')
|
||||||
|
|
||||||
|
lines.append(f"{i}. [{time_str}] {title}")
|
||||||
|
if desc:
|
||||||
|
lines.append(f" {desc}")
|
||||||
|
if source:
|
||||||
|
lines.append(f" 来源: {source}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _get_empty_signal(self, symbol: str) -> Dict[str, Any]:
|
||||||
|
"""返回空信号"""
|
||||||
|
return {
|
||||||
|
'symbol': symbol,
|
||||||
|
'analysis_summary': 'unknown',
|
||||||
|
'volume_analysis': '分析失败',
|
||||||
|
'market_state': '分析失败',
|
||||||
|
'trend': 'sideways',
|
||||||
|
'signals': [],
|
||||||
|
'key_levels': {},
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': '信号分析失败'
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
美股交易智能体 - 主控制器(LLM 驱动版)
|
美股交易智能体 - 主控制器(新架构版)
|
||||||
只进行市场分析和通知,不执行模拟交易
|
只进行市场分析和通知,不执行模拟交易
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
@ -14,7 +14,8 @@ from app.services.feishu_service import get_feishu_service
|
|||||||
from app.services.telegram_service import get_telegram_service
|
from app.services.telegram_service import get_telegram_service
|
||||||
from app.services.signal_database_service import get_signal_db_service
|
from app.services.signal_database_service import get_signal_db_service
|
||||||
from app.services.fundamental_service import get_fundamental_service
|
from app.services.fundamental_service import get_fundamental_service
|
||||||
from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer
|
from app.services.news_service import get_news_service
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@ -87,9 +88,10 @@ class StockAgent:
|
|||||||
self.yfinance = get_yfinance_service()
|
self.yfinance = get_yfinance_service()
|
||||||
self.feishu = get_feishu_service()
|
self.feishu = get_feishu_service()
|
||||||
self.telegram = get_telegram_service()
|
self.telegram = get_telegram_service()
|
||||||
self.llm_analyzer = LLMSignalAnalyzer(agent_type="stock") # 指定使用 stock 模型配置
|
self.market_analyzer = StockMarketSignalAnalyzer() # 使用新的市场信号分析器
|
||||||
self.signal_db = get_signal_db_service() # 信号数据库服务
|
self.signal_db = get_signal_db_service() # 信号数据库服务
|
||||||
self.fundamental = get_fundamental_service() # 基本面数据服务
|
self.fundamental = get_fundamental_service() # 基本面数据服务
|
||||||
|
self.news = get_news_service() # 新闻服务
|
||||||
|
|
||||||
# 状态管理
|
# 状态管理
|
||||||
self.last_signals: Dict[str, Dict[str, Any]] = {}
|
self.last_signals: Dict[str, Dict[str, Any]] = {}
|
||||||
@ -421,32 +423,38 @@ class StockAgent:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f" ⚠️ 获取基本面数据失败: {e}")
|
logger.warning(f" ⚠️ 获取基本面数据失败: {e}")
|
||||||
|
|
||||||
# 5. LLM 分析
|
# 5. 获取新闻数据
|
||||||
logger.info(f"\n🤖 【LLM 分析中...】")
|
logger.info(f"\n📰 【新闻分析】")
|
||||||
analysis = await self.llm_analyzer.analyze(
|
news_data = None
|
||||||
|
try:
|
||||||
|
stock_name = STOCK_NAMES.get(symbol, '')
|
||||||
|
news_data = await self.news.search_stock_news(symbol, stock_name, max_results=5)
|
||||||
|
if news_data:
|
||||||
|
logger.info(f" 获取到 {len(news_data)} 条相关新闻")
|
||||||
|
else:
|
||||||
|
logger.info(f" 暂无相关新闻")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f" ⚠️ 获取新闻数据失败: {e}")
|
||||||
|
|
||||||
|
# 6. 市场信号分析(使用新架构 - 技术面 + 基本面 + 新闻)
|
||||||
|
logger.info(f"\n🤖 【市场信号分析中...】")
|
||||||
|
market_signal = await self.market_analyzer.analyze(
|
||||||
symbol, data,
|
symbol, data,
|
||||||
symbols=self.symbols,
|
symbols=self.symbols,
|
||||||
position_info=None, # 美股不跟踪持仓
|
fundamental_data=fundamental_data,
|
||||||
fundamental_data=fundamental_data, # 传递基本面数据
|
news_data=news_data
|
||||||
fundamental_summary=fundamental_summary # 传递基本面摘要
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 输出分析摘要
|
# 输出分析摘要
|
||||||
summary = analysis.get('analysis_summary', '无')
|
summary = market_signal.get('analysis_summary', '无')
|
||||||
result['analysis_summary'] = summary
|
result['analysis_summary'] = summary
|
||||||
logger.info(f" 市场状态: {summary}")
|
logger.info(f" 市场状态: {summary}")
|
||||||
|
|
||||||
# 输出新闻情绪
|
# 输出新闻情绪(如果有)
|
||||||
news_sentiment = analysis.get('news_sentiment', '')
|
# 注:新的分析器不包含新闻分析,可以跳过或从其他地方获取
|
||||||
news_impact = analysis.get('news_impact', '')
|
|
||||||
if news_sentiment:
|
|
||||||
sentiment_icon = {'positive': '📈', 'negative': '📉', 'neutral': '➖'}.get(news_sentiment, '')
|
|
||||||
logger.info(f" 新闻情绪: {sentiment_icon} {news_sentiment}")
|
|
||||||
if news_impact:
|
|
||||||
logger.info(f" 消息影响: {news_impact}")
|
|
||||||
|
|
||||||
# 输出关键价位
|
# 输出关键价位
|
||||||
levels = analysis.get('key_levels', {})
|
levels = market_signal.get('key_levels', {})
|
||||||
if levels.get('support') or levels.get('resistance'):
|
if levels.get('support') or levels.get('resistance'):
|
||||||
support_str = ', '.join([f"${s:,.2f}" for s in levels.get('support', [])[:2]])
|
support_str = ', '.join([f"${s:,.2f}" for s in levels.get('support', [])[:2]])
|
||||||
resistance_str = ', '.join([f"${r:,.2f}" for r in levels.get('resistance', [])[:2]])
|
resistance_str = ', '.join([f"${r:,.2f}" for r in levels.get('resistance', [])[:2]])
|
||||||
@ -454,7 +462,7 @@ class StockAgent:
|
|||||||
logger.info(f" 阻力位: {resistance_str or '-'}")
|
logger.info(f" 阻力位: {resistance_str or '-'}")
|
||||||
|
|
||||||
# 5. 处理信号
|
# 5. 处理信号
|
||||||
signals = analysis.get('signals', [])
|
signals = market_signal.get('signals', [])
|
||||||
result['signals'] = signals
|
result['signals'] = signals
|
||||||
|
|
||||||
if not signals:
|
if not signals:
|
||||||
@ -556,8 +564,11 @@ class StockAgent:
|
|||||||
):
|
):
|
||||||
"""发送信号通知"""
|
"""发送信号通知"""
|
||||||
try:
|
try:
|
||||||
# 使用正确的方法格式化信号
|
from app.utils.signal_formatter import get_signal_formatter
|
||||||
card = self.llm_analyzer.format_feishu_card(signal, symbol)
|
formatter = get_signal_formatter()
|
||||||
|
|
||||||
|
# 使用格式化工具格式化信号
|
||||||
|
card = formatter.format_feishu_card(signal, symbol, agent_type='stock')
|
||||||
title = card['title']
|
title = card['title']
|
||||||
content = card['content']
|
content = card['content']
|
||||||
|
|
||||||
@ -568,7 +579,7 @@ class StockAgent:
|
|||||||
await self.feishu.send_card(title, content, color)
|
await self.feishu.send_card(title, content, color)
|
||||||
|
|
||||||
# 发送到 Telegram
|
# 发送到 Telegram
|
||||||
await self.telegram.send_message(self.llm_analyzer.format_signal_message(signal, symbol))
|
await self.telegram.send_message(formatter.format_signal_message(signal, symbol, agent_type='stock'))
|
||||||
|
|
||||||
logger.info(f"✅ 信号通知已发送: {title}")
|
logger.info(f"✅ 信号通知已发送: {title}")
|
||||||
|
|
||||||
@ -610,12 +621,9 @@ class StockAgent:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"获取基本面数据失败: {e}")
|
logger.warning(f"获取基本面数据失败: {e}")
|
||||||
|
|
||||||
result = await self.llm_analyzer.analyze(
|
result = await self.market_analyzer.analyze(
|
||||||
symbol, data,
|
symbol, data,
|
||||||
symbols=self.symbols,
|
symbols=self.symbols
|
||||||
position_info=None,
|
|
||||||
fundamental_data=fundamental_data,
|
|
||||||
fundamental_summary=fundamental_summary
|
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
226
backend/app/utils/signal_formatter.py
Normal file
226
backend/app/utils/signal_formatter.py
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
"""
|
||||||
|
信号格式化工具
|
||||||
|
|
||||||
|
用于格式化交易信号通知,支持:
|
||||||
|
- 飞书卡片格式
|
||||||
|
- Telegram 文本格式
|
||||||
|
- 支持加密货币、美股、港股
|
||||||
|
"""
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class SignalFormatter:
|
||||||
|
"""信号格式化工具"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_signal_message(signal: Dict[str, Any], symbol: str, agent_type: str = 'crypto') -> str:
|
||||||
|
"""
|
||||||
|
格式化信号消息(用于 Telegram 通知)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
signal: 信号数据
|
||||||
|
symbol: 交易对
|
||||||
|
agent_type: 智能体类型 (crypto/stock)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化的消息文本
|
||||||
|
"""
|
||||||
|
# 获取股票名称
|
||||||
|
from app.stock_agent.stock_agent import STOCK_NAMES
|
||||||
|
stock_name = STOCK_NAMES.get(symbol, '')
|
||||||
|
|
||||||
|
type_map = {
|
||||||
|
'short_term': '短线',
|
||||||
|
'medium_term': '中线',
|
||||||
|
'long_term': '长线'
|
||||||
|
}
|
||||||
|
action_map = {
|
||||||
|
'buy': '做多',
|
||||||
|
'sell': '做空'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 兼容 timeframe 和 type 字段
|
||||||
|
signal_type_key = 'timeframe' if 'timeframe' in signal else 'type'
|
||||||
|
signal_type = type_map.get(signal.get(signal_type_key), signal.get(signal_type_key))
|
||||||
|
action = action_map.get(signal['action'], signal['action'])
|
||||||
|
grade = signal.get('grade', 'C')
|
||||||
|
confidence = signal.get('confidence', 0)
|
||||||
|
entry_type = signal.get('entry_type', 'market')
|
||||||
|
|
||||||
|
# 等级图标
|
||||||
|
grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(grade, '')
|
||||||
|
|
||||||
|
# 方向图标
|
||||||
|
action_icon = '🟢' if signal['action'] == 'buy' else '🔴'
|
||||||
|
|
||||||
|
# 入场类型
|
||||||
|
entry_type_text = '现价入场' if entry_type == 'market' else '挂单等待'
|
||||||
|
entry_type_icon = '⚡' if entry_type == 'market' else '⏳'
|
||||||
|
|
||||||
|
# 仓位大小
|
||||||
|
position_size = signal.get('position_size', 'light')
|
||||||
|
position_map = {'heavy': '重仓', 'medium': '中仓', 'light': '轻仓'}
|
||||||
|
position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱')
|
||||||
|
position_text = position_map.get(position_size, '轻仓')
|
||||||
|
|
||||||
|
# 计算风险收益比
|
||||||
|
entry = signal.get('entry_price') or signal.get('entry_zone', 0)
|
||||||
|
sl = signal.get('stop_loss', 0)
|
||||||
|
tp = signal.get('take_profit', 0)
|
||||||
|
sl_percent = ((sl - entry) / entry * 100) if entry else 0
|
||||||
|
tp_percent = ((tp - entry) / entry * 100) if entry else 0
|
||||||
|
|
||||||
|
# 识别市场类型
|
||||||
|
if agent_type == 'crypto':
|
||||||
|
market_tag = '[加密货币] '
|
||||||
|
elif symbol.endswith('.HK'):
|
||||||
|
market_tag = '[港股] '
|
||||||
|
else:
|
||||||
|
market_tag = '[美股] '
|
||||||
|
|
||||||
|
# 构建标题(带股票名称和市场类型)
|
||||||
|
symbol_display = f"{stock_name}({symbol})" if stock_name else symbol
|
||||||
|
|
||||||
|
message = f"""📊 {market_tag}{symbol_display} {signal_type}信号
|
||||||
|
|
||||||
|
{action_icon} **方向**: {action}
|
||||||
|
{entry_type_icon} **入场**: {entry_type_text}
|
||||||
|
{position_icon} **仓位**: {position_text}
|
||||||
|
⭐ **等级**: {grade} {grade_icon}
|
||||||
|
📈 **置信度**: {confidence}%
|
||||||
|
|
||||||
|
💰 **入场价**: ${entry:,.2f}
|
||||||
|
🛑 **止损价**: ${sl:,.2f} ({sl_percent:+.1f}%)
|
||||||
|
🎯 **止盈价**: ${tp:,.2f} ({tp_percent:+.1f}%)
|
||||||
|
|
||||||
|
📝 **分析理由**:
|
||||||
|
{signal.get('reason', '无')}
|
||||||
|
|
||||||
|
⚠️ **风险提示**:
|
||||||
|
{signal.get('risk_warning', '请注意风险控制')}"""
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_feishu_card(signal: Dict[str, Any], symbol: str, agent_type: str = 'crypto') -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
格式化飞书卡片消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
signal: 信号数据
|
||||||
|
symbol: 交易对
|
||||||
|
agent_type: 智能体类型 (crypto/stock)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含 title, content, color 的字典
|
||||||
|
"""
|
||||||
|
# 获取股票名称
|
||||||
|
from app.stock_agent.stock_agent import STOCK_NAMES
|
||||||
|
stock_name = STOCK_NAMES.get(symbol, '')
|
||||||
|
|
||||||
|
type_map = {
|
||||||
|
'short_term': '短线',
|
||||||
|
'medium_term': '中线',
|
||||||
|
'long_term': '长线'
|
||||||
|
}
|
||||||
|
action_map = {
|
||||||
|
'buy': '做多',
|
||||||
|
'sell': '做空'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 兼容 timeframe 和 type 字段
|
||||||
|
signal_type_key = 'timeframe' if 'timeframe' in signal else 'type'
|
||||||
|
signal_type = type_map.get(signal.get(signal_type_key), signal.get(signal_type_key))
|
||||||
|
action = action_map.get(signal['action'], signal['action'])
|
||||||
|
grade = signal.get('grade', 'C')
|
||||||
|
confidence = signal.get('confidence', 0)
|
||||||
|
entry_type = signal.get('entry_type', 'market')
|
||||||
|
|
||||||
|
# 等级图标
|
||||||
|
grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(grade, '')
|
||||||
|
|
||||||
|
# 入场类型
|
||||||
|
entry_type_text = '现价入场' if entry_type == 'market' else '挂单等待'
|
||||||
|
entry_type_icon = '⚡' if entry_type == 'market' else '⏳'
|
||||||
|
|
||||||
|
# 仓位大小
|
||||||
|
position_size = signal.get('position_size', 'light')
|
||||||
|
position_map = {'heavy': '重仓', 'medium': '中仓', 'light': '轻仓'}
|
||||||
|
position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱')
|
||||||
|
position_text = position_map.get(position_size, '轻仓')
|
||||||
|
|
||||||
|
# 标题和颜色 - 区分加密货币/美股/港股
|
||||||
|
is_market_order = entry_type == 'market'
|
||||||
|
market_badge = '【现价】' if is_market_order else ''
|
||||||
|
|
||||||
|
# 识别市场类型
|
||||||
|
if agent_type == 'crypto':
|
||||||
|
market_tag = '[加密货币] '
|
||||||
|
elif symbol.endswith('.HK'):
|
||||||
|
market_tag = '[港股] '
|
||||||
|
else:
|
||||||
|
market_tag = '[美股] '
|
||||||
|
|
||||||
|
# 构建带名称的股票显示
|
||||||
|
symbol_display = f"{stock_name}({symbol})" if stock_name else symbol
|
||||||
|
|
||||||
|
if signal['action'] == 'buy':
|
||||||
|
title = f"🟢 {market_tag}{symbol_display} {signal_type}做多信号 {market_badge}"
|
||||||
|
color = "green"
|
||||||
|
else:
|
||||||
|
title = f"🔴 {market_tag}{symbol_display} {signal_type}做空信号 {market_badge}"
|
||||||
|
color = "red"
|
||||||
|
|
||||||
|
# 计算风险收益比
|
||||||
|
entry = signal.get('entry_price') or signal.get('entry_zone', 0)
|
||||||
|
sl = signal.get('stop_loss', 0)
|
||||||
|
tp = signal.get('take_profit', 0)
|
||||||
|
sl_percent = ((sl - entry) / entry * 100) if entry else 0
|
||||||
|
tp_percent = ((tp - entry) / entry * 100) if entry else 0
|
||||||
|
|
||||||
|
# 构建内容
|
||||||
|
content_lines = [
|
||||||
|
f"{action_icon} **操作**: {action}",
|
||||||
|
f"{entry_type_icon} **入场方式**: {entry_type_text}",
|
||||||
|
f"{position_icon} **仓位**: {position_text} | 📈 信心度: **{confidence}%**",
|
||||||
|
f"⭐ **等级**: {grade} {grade_icon}",
|
||||||
|
f"",
|
||||||
|
f"💰 **入场价**: ${entry:,.2f}",
|
||||||
|
f"🛑 **止损价**: ${sl:,.2f} ({sl_percent:+.1f}%)",
|
||||||
|
f"🎯 **止盈价**: ${tp:,.2f} ({tp_percent:+.1f}%)",
|
||||||
|
f"",
|
||||||
|
f"📝 **分析理由**:",
|
||||||
|
f"{signal.get('reason', '无')}",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 添加关键因素(如果有)
|
||||||
|
key_factors = signal.get('key_factors')
|
||||||
|
if key_factors and isinstance(key_factors, list):
|
||||||
|
content_lines.append("")
|
||||||
|
content_lines.append("**关键因素**:")
|
||||||
|
for factor in key_factors[:5]:
|
||||||
|
content_lines.append(f"- {factor}")
|
||||||
|
|
||||||
|
# 添加风险提示(如果有)
|
||||||
|
risk_warning = signal.get('risk_warning')
|
||||||
|
if risk_warning:
|
||||||
|
content_lines.append("")
|
||||||
|
content_lines.append(f"⚠️ **风险提示**:")
|
||||||
|
content_lines.append(risk_warning)
|
||||||
|
|
||||||
|
content = "\n".join(content_lines)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'content': content,
|
||||||
|
'color': color
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 全局单例
|
||||||
|
_signal_formatter = SignalFormatter()
|
||||||
|
|
||||||
|
|
||||||
|
def get_signal_formatter() -> SignalFormatter:
|
||||||
|
"""获取信号格式化工具单例"""
|
||||||
|
return _signal_formatter
|
||||||
@ -50,30 +50,26 @@ async def test_binance_service():
|
|||||||
|
|
||||||
|
|
||||||
async def test_signal_analyzer():
|
async def test_signal_analyzer():
|
||||||
"""测试信号分析器"""
|
"""测试信号分析器 - 使用新架构"""
|
||||||
print("\n" + "=" * 50)
|
print("\n" + "=" * 50)
|
||||||
print("测试信号分析器")
|
print("测试市场信号分析器(新架构)")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
from app.services.binance_service import binance_service
|
from app.services.binance_service import binance_service
|
||||||
from app.crypto_agent.signal_analyzer import SignalAnalyzer
|
from app.crypto_agent.market_signal_analyzer import MarketSignalAnalyzer
|
||||||
|
|
||||||
analyzer = SignalAnalyzer()
|
analyzer = MarketSignalAnalyzer()
|
||||||
|
|
||||||
# 获取数据
|
# 获取数据
|
||||||
data = binance_service.get_multi_timeframe_data('BTCUSDT')
|
data = binance_service.get_multi_timeframe_data('BTCUSDT')
|
||||||
|
|
||||||
# 测试趋势分析
|
# 测试 LLM 分析
|
||||||
print("\n1. 分析趋势...")
|
print("\n1. LLM 市场分析...")
|
||||||
trend = analyzer.analyze_trend(data['1h'], data['4h'])
|
signal = await analyzer.analyze('BTCUSDT', data, symbols=['BTCUSDT', 'ETHUSDT'])
|
||||||
print(f" 趋势: {trend}")
|
|
||||||
|
|
||||||
# 测试进场信号
|
print(f" 市场状态: {signal.get('market_state')}")
|
||||||
print("\n2. 分析进场信号...")
|
print(f" 趋势: {signal.get('trend')}")
|
||||||
signal = analyzer.analyze_entry_signal(data['5m'], data['15m'], trend)
|
print(f" 信号数量: {len(signal.get('signals', []))}")
|
||||||
print(f" 动作: {signal['action']}")
|
|
||||||
print(f" 置信度: {signal['confidence']}%")
|
|
||||||
print(f" 原因: {', '.join(signal['reasons'])}")
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
美股分析脚本(修复版)
|
美股分析脚本(新架构版)
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
python3 scripts/test_stock.py AAPL
|
python3 scripts/test_stock.py AAPL
|
||||||
@ -20,7 +20,8 @@ from app.services.yfinance_service import get_yfinance_service
|
|||||||
from app.services.feishu_service import get_feishu_service
|
from app.services.feishu_service import get_feishu_service
|
||||||
from app.services.telegram_service import get_telegram_service
|
from app.services.telegram_service import get_telegram_service
|
||||||
from app.services.fundamental_service import get_fundamental_service
|
from app.services.fundamental_service import get_fundamental_service
|
||||||
from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer
|
from app.services.news_service import get_news_service
|
||||||
|
from app.stock_agent.market_signal_analyzer import StockMarketSignalAnalyzer
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
|
|
||||||
@ -60,8 +61,9 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
try:
|
try:
|
||||||
# 获取服务
|
# 获取服务
|
||||||
yf_service = get_yfinance_service()
|
yf_service = get_yfinance_service()
|
||||||
llm = LLMSignalAnalyzer(agent_type="stock") # 指定使用 stock 模型配置
|
market_analyzer = StockMarketSignalAnalyzer() # 使用新的市场信号分析器
|
||||||
fundamental = get_fundamental_service() # 基本面服务
|
fundamental = get_fundamental_service() # 基本面服务
|
||||||
|
news = get_news_service() # 新闻服务
|
||||||
feishu = get_feishu_service()
|
feishu = get_feishu_service()
|
||||||
telegram = get_telegram_service()
|
telegram = get_telegram_service()
|
||||||
|
|
||||||
@ -162,19 +164,31 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ⚠️ 获取基本面数据失败: {e}")
|
print(f" ⚠️ 获取基本面数据失败: {e}")
|
||||||
|
|
||||||
# LLM分析
|
# 获取新闻数据
|
||||||
print(f"\n🤖 LLM分析中...\n")
|
print(f"\n📰 新闻分析...")
|
||||||
analysis = await llm.analyze(
|
news_data = None
|
||||||
|
try:
|
||||||
|
stock_name = STOCK_NAMES.get(symbol, '')
|
||||||
|
news_data = await news.search_stock_news(symbol, stock_name, max_results=5)
|
||||||
|
if news_data:
|
||||||
|
print(f" 获取到 {len(news_data)} 条相关新闻")
|
||||||
|
else:
|
||||||
|
print(f" 暂无相关新闻")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ 获取新闻数据失败: {e}")
|
||||||
|
|
||||||
|
# 市场信号分析(使用新架构 - 技术面 + 基本面 + 新闻)
|
||||||
|
print(f"\n🤖 市场信号分析中...\n")
|
||||||
|
market_signal = await market_analyzer.analyze(
|
||||||
symbol, data,
|
symbol, data,
|
||||||
symbols=[symbol],
|
symbols=[symbol],
|
||||||
position_info=None,
|
|
||||||
fundamental_data=fundamental_data,
|
fundamental_data=fundamental_data,
|
||||||
fundamental_summary=fundamental_summary
|
news_data=news_data
|
||||||
)
|
)
|
||||||
|
|
||||||
# 输出结果
|
# 输出结果
|
||||||
summary = analysis.get('analysis_summary', '')
|
summary = market_signal.get('analysis_summary', '')
|
||||||
signals = analysis.get('signals', [])
|
signals = market_signal.get('signals', [])
|
||||||
result['signals'] = signals
|
result['signals'] = signals
|
||||||
|
|
||||||
print(f"市场状态: {summary}")
|
print(f"市场状态: {summary}")
|
||||||
@ -215,8 +229,11 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if best_signal:
|
if best_signal:
|
||||||
# 使用正确的方法格式化通知
|
# 使用格式化工具格式化通知
|
||||||
card = llm.format_feishu_card(best_signal, symbol)
|
from app.utils.signal_formatter import get_signal_formatter
|
||||||
|
formatter = get_signal_formatter()
|
||||||
|
|
||||||
|
card = formatter.format_feishu_card(best_signal, symbol, agent_type='stock')
|
||||||
title = card['title']
|
title = card['title']
|
||||||
content = card['content']
|
content = card['content']
|
||||||
|
|
||||||
@ -224,7 +241,7 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
# 根据信号方向选择颜色
|
# 根据信号方向选择颜色
|
||||||
color = "green" if best_signal.get('action') == 'buy' else "red"
|
color = "green" if best_signal.get('action') == 'buy' else "red"
|
||||||
await feishu.send_card(title, content, color)
|
await feishu.send_card(title, content, color)
|
||||||
await telegram.send_message(llm.format_signal_message(best_signal, symbol))
|
await telegram.send_message(formatter.format_signal_message(best_signal, symbol, agent_type='stock'))
|
||||||
print(f"\n📬 通知已发送:{title}")
|
print(f"\n📬 通知已发送:{title}")
|
||||||
result['notified'] = True
|
result['notified'] = True
|
||||||
else:
|
else:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user