diff --git a/backend/app/config.py b/backend/app/config.py index 1523f7c..1b1fe62 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -104,6 +104,11 @@ class Settings(BaseSettings): telegram_channel_id: str = "" # 频道 ID,如 @your_channel 或 -1001234567890 telegram_enabled: bool = True # 是否启用 Telegram 通知 + # 钉钉机器人配置 + dingtalk_webhook_url: str = "https://oapi.dingtalk.com/robot/send?access_token=a4fa1c1a6a07a5ed07d79c701f79b44efb1e726da3b47b50495ebdc9190423ec" # 钉钉群机器人 Webhook + dingtalk_secret: str = "SECdc6dffe3b6838a5d8afde3486d5415b9a17d3ebc9cbf934438883acee1189e8d" # 加签密钥 + dingtalk_enabled: bool = True # 是否启用钉钉通知 + # 加密货币交易智能体配置 crypto_symbols: str = "BTCUSDT,ETHUSDT" # 监控的交易对,逗号分隔 crypto_analysis_interval: int = 60 # 分析间隔(秒) @@ -174,7 +179,7 @@ class Settings(BaseSettings): # 新能源:比亚迪/理想/小鹏/赣锋锂业/龙源电力/信义能源 # 芯片:中芯国际/华虹半导体/上海复旦 # 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_symbols_hk: str = "" stock_analysis_interval: int = 300 # 分析间隔(秒,默认5分钟) stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值 diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 173167f..2c0ee9a 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -11,6 +11,7 @@ from app.config import get_settings from app.services.bitget_service import bitget_service from app.services.feishu_service import get_feishu_service from app.services.telegram_service import get_telegram_service +from app.services.dingtalk_service import get_dingtalk_service from app.services.paper_trading_service import get_paper_trading_service from app.services.signal_database_service import get_signal_db_service from app.crypto_agent.market_signal_analyzer import MarketSignalAnalyzer @@ -41,6 +42,7 @@ class CryptoAgent: self.exchange = bitget_service # 交易所服务 self.feishu = get_feishu_service() self.telegram = get_telegram_service() + self.dingtalk = get_dingtalk_service() # 添加钉钉服务 # 新架构:市场信号分析器 + 交易决策器 self.market_analyzer = MarketSignalAnalyzer() @@ -140,6 +142,8 @@ class CryptoAgent: if self.settings.telegram_enabled: message = f"{title}\n\n{content}" await self.telegram.send_message(message) + if self.settings.dingtalk_enabled: + await self.dingtalk.send_action_card(title, content) logger.info(f"已发送挂单成交通知: {result.get('order_id')}") async def _notify_pending_cancelled(self, result: Dict[str, Any]): @@ -164,6 +168,8 @@ class CryptoAgent: if self.settings.telegram_enabled: message = f"{title}\n\n{content}" await self.telegram.send_message(message) + if self.settings.dingtalk_enabled: + await self.dingtalk.send_action_card(title, content) logger.info(f"已发送挂单撤销通知: {result.get('order_id')}") async def _notify_breakeven_triggered(self, result: Dict[str, Any]): @@ -191,6 +197,8 @@ class CryptoAgent: if self.settings.telegram_enabled: message = f"{title}\n\n{content}" await self.telegram.send_message(message) + if self.settings.dingtalk_enabled: + await self.dingtalk.send_action_card(title, content) logger.info(f"已发送移动止损通知: {result.get('order_id')}") async def _notify_order_closed(self, result: Dict[str, Any]): @@ -238,6 +246,8 @@ class CryptoAgent: if self.settings.telegram_enabled: message = f"{title}\n\n{content}" await self.telegram.send_message(message) + if self.settings.dingtalk_enabled: + await self.dingtalk.send_action_card(title, content) logger.info(f"已发送订单平仓通知: {result.get('order_id')}") def _get_seconds_until_next_5min(self) -> int: @@ -997,6 +1007,9 @@ class CryptoAgent: # Telegram 使用文本格式 message = f"{title}\n\n{content}" await self.telegram.send_message(message) + if self.settings.dingtalk_enabled: + # 钉钉使用 ActionCard 格式 + await self.dingtalk.send_action_card(title, content) logger.info(f" 📤 已发送市场信号通知 (阈值: {threshold}%)") except Exception as e: @@ -1124,6 +1137,8 @@ class CryptoAgent: # Telegram 使用文本格式 message = f"{title}\n\n{content}" await self.telegram.send_message(message) + if self.settings.dingtalk_enabled: + await self.dingtalk.send_action_card(title, content) logger.info(f" 📤 已发送交易决策通知: {decision_text}") @@ -1256,6 +1271,8 @@ class CryptoAgent: # Telegram 使用文本格式 message = f"{title}\n\n{content}" await self.telegram.send_message(message) + if self.settings.dingtalk_enabled: + await self.dingtalk.send_action_card(title, content) logger.info(f" 📤 已发送交易执行通知: {decision_text}") except Exception as e: @@ -1690,6 +1707,8 @@ class CryptoAgent: if self.settings.telegram_enabled: message = f"{title}\n\n{content}" await self.telegram.send_message(message) + if self.settings.dingtalk_enabled: + await self.dingtalk.send_action_card(title, content) logger.info(f"已发送实盘订单创建通知: {result.get('order_id')}") async def _notify_position_adjustment( @@ -1741,6 +1760,8 @@ class CryptoAgent: if self.settings.telegram_enabled: message = f"{title}\n\n{content}" await self.telegram.send_message(message) + if self.settings.dingtalk_enabled: + await self.dingtalk.send_action_card(title, content) async def _notify_signal_not_executed( self, @@ -1811,6 +1832,8 @@ class CryptoAgent: if self.settings.telegram_enabled: message = f"{title}\n\n{content}" await self.telegram.send_message(message) + if self.settings.dingtalk_enabled: + await self.dingtalk.send_action_card(title, content) logger.info(f" 📤 已发送信号未执行通知: {decision_type} - {final_reason[:50]}") diff --git a/backend/app/crypto_agent/market_signal_analyzer.py b/backend/app/crypto_agent/market_signal_analyzer.py index c8bf6a5..183b0dd 100644 --- a/backend/app/crypto_agent/market_signal_analyzer.py +++ b/backend/app/crypto_agent/market_signal_analyzer.py @@ -592,12 +592,65 @@ class MarketSignalAnalyzer: # 清理 signals 中的价格字段 if 'signals' in data: - for sig in data['signals']: + # 标记需要移除的信号索引 + signals_to_remove = [] + + for idx, sig in enumerate(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]) + # 验证止损止盈价格的合理性 + entry_zone = sig.get('entry_zone') + stop_loss = sig.get('stop_loss') + take_profit = sig.get('take_profit') + action = sig.get('action', '') + + if entry_zone and entry_zone > 0: + MAX_REASONABLE_DEVIATION = 0.50 # 50% + has_invalid_price = False + + # 检查止损 + if stop_loss is not None: + deviation = abs(stop_loss - entry_zone) / entry_zone + if deviation > MAX_REASONABLE_DEVIATION: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 信号止损价格不合理: entry={entry_zone}, stop_loss={stop_loss}, 偏离={deviation*100:.1f}%") + has_invalid_price = True + elif action == 'buy' and stop_loss >= entry_zone: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 做多止损错误: entry={entry_zone}, stop_loss={stop_loss} 应该 < entry") + has_invalid_price = True + elif action == 'sell' and stop_loss <= entry_zone: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 做空止损错误: entry={entry_zone}, stop_loss={stop_loss} 应该 > entry") + has_invalid_price = True + + # 检查止盈 + if take_profit is not None: + deviation = abs(take_profit - entry_zone) / entry_zone + if deviation > MAX_REASONABLE_DEVIATION: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 信号止盈价格不合理: entry={entry_zone}, take_profit={take_profit}, 偏离={deviation*100:.1f}%") + has_invalid_price = True + elif action == 'buy' and take_profit <= entry_zone: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 做多止盈错误: entry={entry_zone}, take_profit={take_profit} 应该 > entry") + has_invalid_price = True + elif action == 'sell' and take_profit >= entry_zone: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 做空止盈错误: entry={entry_zone}, take_profit={take_profit} 应该 < entry") + has_invalid_price = True + + # 如果价格不合理,降低等级为 D 或移除信号 + if has_invalid_price: + original_grade = sig.get('grade', 'C') + sig['grade'] = 'D' + sig['confidence'] = 0 + # 添加错误说明 + if 'reasoning' in sig: + sig['reasoning'] = f"[价格异常] {sig['reasoning']}" + logger.error(f"❌ [{data.get('symbol', '')}] 信号价格异常,等级从 {original_grade} 降为 D,止损止盈已清空") + + # 清空不合理的价格 + sig['stop_loss'] = None + sig['take_profit'] = None + return data def _calculate_price_change_24h(self, df) -> str: diff --git a/backend/app/crypto_agent/trading_decision_maker.py b/backend/app/crypto_agent/trading_decision_maker.py index 7ea20e0..241544d 100644 --- a/backend/app/crypto_agent/trading_decision_maker.py +++ b/backend/app/crypto_agent/trading_decision_maker.py @@ -573,6 +573,60 @@ class TradingDecisionMaker: if field in data: data[field] = clean_price(data[field]) + # 验证止损止盈价格的合理性 + data = self._validate_price_fields(data) + + return data + + def _validate_price_fields(self, data: Dict[str, Any]) -> Dict[str, Any]: + """验证止损止盈价格的合理性,拒绝明显错误的值""" + entry = data.get('entry_zone') + stop_loss = data.get('stop_loss') + take_profit = data.get('take_profit') + action = data.get('decision', '') # OPEN/CLOSE/HOLD + + if not entry or entry <= 0: + return data + + # 判断是做多还是做空 + is_long = action == 'OPEN' and data.get('action') == 'buy' + is_short = action == 'OPEN' and data.get('action') == 'sell' + + # 检查止损价格是否合理(偏离入场价不超过 50%) + MAX_REASONABLE_DEVIATION = 0.50 # 50% + + if stop_loss is not None: + deviation = abs(stop_loss - entry) / entry + # 如果止损价格偏离入场价超过 50%,认为是错误的 + if deviation > MAX_REASONABLE_DEVIATION: + logger.warning(f"⚠️ 止损价格不合理: entry={entry}, stop_loss={stop_loss}, 偏离={deviation*100:.1f}%,已忽略") + data['stop_loss'] = None + else: + # 做多:止损应该低于入场价 + if is_long and stop_loss >= entry: + logger.warning(f"⚠️ 做多止损错误: entry={entry}, stop_loss={stop_loss} 应该 < entry,已忽略") + data['stop_loss'] = None + # 做空:止损应该高于入场价 + elif is_short and stop_loss <= entry: + logger.warning(f"⚠️ 做空止损错误: entry={entry}, stop_loss={stop_loss} 应该 > entry,已忽略") + data['stop_loss'] = None + + if take_profit is not None: + deviation = abs(take_profit - entry) / entry + # 如果止盈价格偏离入场价超过 50%,认为是错误的 + if deviation > MAX_REASONABLE_DEVIATION: + logger.warning(f"⚠️ 止盈价格不合理: entry={entry}, take_profit={take_profit}, 偏离={deviation*100:.1f}%,已忽略") + data['take_profit'] = None + else: + # 做多:止盈应该高于入场价 + if is_long and take_profit <= entry: + logger.warning(f"⚠️ 做多止盈错误: entry={entry}, take_profit={take_profit} 应该 > entry,已忽略") + data['take_profit'] = None + # 做空:止盈应该低于入场价 + elif is_short and take_profit >= entry: + logger.warning(f"⚠️ 做空止盈错误: entry={entry}, take_profit={take_profit} 应该 < entry,已忽略") + data['take_profit'] = None + return data def _clean_json_string(self, json_str: str) -> str: diff --git a/backend/app/main.py b/backend/app/main.py index c4a950f..4a97968 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -365,6 +365,7 @@ async def periodic_report_loop(): from datetime import datetime from app.services.paper_trading_service import get_paper_trading_service from app.services.telegram_service import get_telegram_service + from app.config import get_settings logger.info("定时报告任务已启动") @@ -394,13 +395,19 @@ async def periodic_report_loop(): # 等待到下一个4小时整点 await asyncio.sleep(wait_seconds) + # 检查是否启用 Telegram 通知 + settings = get_settings() + if not settings.telegram_enabled: + logger.info("Telegram 通知已禁用,跳过4小时报告发送") + continue + # 生成并发送报告 paper_trading = get_paper_trading_service() telegram = get_telegram_service() report = paper_trading.generate_report(hours=4) await telegram.send_message(report, parse_mode="HTML") - logger.info("已发送4小时模拟交易报告") + logger.info("已发送4小时模拟交易报告到 Telegram") except Exception as e: logger.error(f"定时报告循环出错: {e}") diff --git a/backend/app/services/dingtalk_service.py b/backend/app/services/dingtalk_service.py new file mode 100644 index 0000000..16ee2ff --- /dev/null +++ b/backend/app/services/dingtalk_service.py @@ -0,0 +1,319 @@ +""" +钉钉通知服务 - 通过群机器人发送消息 +""" +import httpx +import hmac +import hashlib +import base64 +import json +from typing import Dict, Any, Optional +from datetime import datetime +from urllib.parse import quote +from app.utils.logger import logger +from app.config import get_settings + + +class DingTalkService: + """钉钉群机器人通知服务""" + + def __init__(self, webhook_url: str = "", secret: str = ""): + """ + 初始化钉钉服务 + + Args: + webhook_url: 钉钉群机器人 Webhook URL + secret: 加签密钥 + """ + settings = get_settings() + self.webhook_url = webhook_url or getattr(settings, 'dingtalk_webhook_url', '') + self.secret = secret or getattr(settings, 'dingtalk_secret', '') + + # 检查配置开关和必要参数是否都有效 + config_enabled = getattr(settings, 'dingtalk_enabled', True) + self.enabled = config_enabled and bool(self.webhook_url) + + if not config_enabled: + logger.info("钉钉通知已通过配置禁用") + elif self.enabled: + logger.info(f"钉钉通知服务初始化完成") + else: + logger.warning("钉钉 Webhook URL 未配置,通知功能已禁用") + + def _generate_sign(self, timestamp: int) -> str: + """ + 生成钉钉签名 + + Args: + timestamp: 当前时间戳(毫秒) + + Returns: + 签名字符串 + """ + if not self.secret: + return "" + + secret_enc = self.secret.encode('utf-8') + string_to_sign = f'{timestamp}\n{self.secret}' + string_to_sign_enc = string_to_sign.encode('utf-8') + + hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest() + sign = base64.b64encode(hmac_code).decode('utf-8') + + return sign + + def _build_url(self) -> str: + """ + 构建带签名的 Webhook URL + + Returns: + 完整的 Webhook URL + """ + if not self.secret: + return self.webhook_url + + timestamp = int(datetime.now().timestamp() * 1000) + sign = self._generate_sign(timestamp) + sign_encoded = quote(sign, safe='') + + return f"{self.webhook_url}×tamp={timestamp}&sign={sign_encoded}" + + async def send_text(self, content: str, at_mobiles: list = None, at_user_ids: list = None) -> bool: + """ + 发送文本消息 + + Args: + content: 消息内容 + at_mobiles: @的手机号列表 + at_user_ids: @的用户ID列表 + + Returns: + 是否发送成功 + """ + if not self.enabled: + logger.warning("钉钉服务未启用,跳过发送") + return False + + data = { + "msgtype": "text", + "text": { + "content": content + } + } + + # 添加 @ 信息 + if at_mobiles or at_user_ids: + data["at"] = { + "atMobiles": at_mobiles or [], + "atUserIds": at_user_ids or [], + "isAtAll": False + } + + return await self._send(data) + + async def send_markdown(self, title: str, content: str) -> bool: + """ + 发送 Markdown 消息 + + Args: + title: 消息标题 + content: Markdown 格式的内容 + + Returns: + 是否发送成功 + """ + if not self.enabled: + logger.warning("钉钉服务未启用,跳过发送") + return False + + data = { + "msgtype": "markdown", + "markdown": { + "title": title, + "text": content + } + } + + return await self._send(data) + + async def send_link(self, title: str, text: str, message_url: str, pic_url: str = "") -> bool: + """ + 发送链接消息 + + Args: + title: 消息标题 + text: 消息内容 + message_url: 点击消息跳转的 URL + pic_url: 图片 URL + + Returns: + 是否发送成功 + """ + if not self.enabled: + logger.warning("钉钉服务未启用,跳过发送") + return False + + data = { + "msgtype": "link", + "link": { + "title": title, + "text": text, + "messageUrl": message_url + } + } + + if pic_url: + data["link"]["picUrl"] = pic_url + + return await self._send(data) + + async def send_action_card(self, title: str, content: str, btn_orientation: str = "0", + btn_title: str = "", btn_url: str = "") -> bool: + """ + 发送 ActionCard 消息(卡片消息) + + Args: + title: 标题 + content: Markdown 格式的内容 + btn_orientation: 按钮排列方向,0-竖直,1-横向 + btn_title: 按钮标题 + btn_url: 按钮跳转链接 + + Returns: + 是否发送成功 + """ + if not self.enabled: + logger.warning("钉钉服务未启用,跳过发送") + return False + + data = { + "msgtype": "actionCard", + "actionCard": { + "title": title, + "text": content, + "btnOrientation": btn_orientation + } + } + + if btn_title and btn_url: + data["actionCard"]["btns"] = [ + { + "title": btn_title, + "actionURL": btn_url + } + ] + + return await self._send(data) + + async def send_feed_card(self, links: list) -> bool: + """ + 发送 FeedCard 消息(多条链接) + + Args: + links: 链接列表,每个元素包含 title, messageURL, picURL + + Returns: + 是否发送成功 + """ + if not self.enabled: + logger.warning("钉钉服务未启用,跳过发送") + return False + + data = { + "msgtype": "feedCard", + "feedCard": { + "links": links + } + } + + return await self._send(data) + + async def send_trading_signal(self, signal: Dict[str, Any]) -> bool: + """ + 发送交易信号消息(Markdown 格式) + + Args: + signal: 交易信号数据 + + Returns: + 是否发送成功 + """ + if not self.enabled: + logger.warning("钉钉服务未启用,跳过发送") + return False + + action = signal.get('action', 'hold') + symbol = signal.get('symbol', 'UNKNOWN') + price = signal.get('price', 0) + trend = signal.get('trend', 'neutral') + confidence = signal.get('confidence', 0) + agent_type = signal.get('agent_type', 'crypto') + + # 操作方向映射 + action_map = { + 'buy': '🟢 做多', + 'sell': '🔴 做空', + 'hold': '⏸️ 观望', + 'close': '❌ 平仓' + } + + # 市场类型映射 + market_map = { + 'crypto': '[加密货币]', + 'stock': '[股票]' + } + + action_text = action_map.get(action, action) + market_text = market_map.get(agent_type, '') + + title = f"{market_text} {symbol} {action_text} 信号" + content = f"""### {title} + +> **操作**: {action_text} +> **价格**: ${price:,.2f} +> **趋势**: {trend} +> **信心度**: {confidence}% + +*信号来源: Stock Agent* +""" + + return await self.send_markdown(title, content) + + async def _send(self, data: Dict[str, Any]) -> bool: + """ + 发送消息到钉钉 + + Args: + data: 消息数据 + + Returns: + 是否发送成功 + """ + try: + url = self._build_url() + + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.post(url, json=data) + result = response.json() + + if result.get('errcode') == 0: + logger.info(f"钉钉消息发送成功") + return True + else: + logger.error(f"钉钉消息发送失败: {result.get('errmsg', 'Unknown error')}") + return False + + except Exception as e: + logger.error(f"钉钉消息发送异常: {e}") + return False + + +# 全局单例 +_dingtalk_service: Optional[DingTalkService] = None + + +def get_dingtalk_service() -> DingTalkService: + """获取钉钉服务单例""" + global _dingtalk_service + if _dingtalk_service is None: + _dingtalk_service = DingTalkService() + return _dingtalk_service diff --git a/backend/app/stock_agent/market_signal_analyzer.py b/backend/app/stock_agent/market_signal_analyzer.py index 621db38..18911be 100644 --- a/backend/app/stock_agent/market_signal_analyzer.py +++ b/backend/app/stock_agent/market_signal_analyzer.py @@ -658,6 +658,56 @@ class StockMarketSignalAnalyzer: if field in sig: sig[field] = clean_price(sig[field]) + # 验证止损止盈价格的合理性 + entry_zone = sig.get('entry_zone') + stop_loss = sig.get('stop_loss') + take_profit = sig.get('take_profit') + action = sig.get('action', '') + + if entry_zone and entry_zone > 0: + MAX_REASONABLE_DEVIATION = 0.50 # 50% + has_invalid_price = False + + # 检查止损 + if stop_loss is not None: + deviation = abs(stop_loss - entry_zone) / entry_zone + if deviation > MAX_REASONABLE_DEVIATION: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 信号止损价格不合理: entry={entry_zone}, stop_loss={stop_loss}, 偏离={deviation*100:.1f}%") + has_invalid_price = True + elif action == 'buy' and stop_loss >= entry_zone: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 做多止损错误: entry={entry_zone}, stop_loss={stop_loss} 应该 < entry") + has_invalid_price = True + elif action == 'sell' and stop_loss <= entry_zone: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 做空止损错误: entry={entry_zone}, stop_loss={stop_loss} 应该 > entry") + has_invalid_price = True + + # 检查止盈 + if take_profit is not None: + deviation = abs(take_profit - entry_zone) / entry_zone + if deviation > MAX_REASONABLE_DEVIATION: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 信号止盈价格不合理: entry={entry_zone}, take_profit={take_profit}, 偏离={deviation*100:.1f}%") + has_invalid_price = True + elif action == 'buy' and take_profit <= entry_zone: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 做多止盈错误: entry={entry_zone}, take_profit={take_profit} 应该 > entry") + has_invalid_price = True + elif action == 'sell' and take_profit >= entry_zone: + logger.warning(f"⚠️ [{data.get('symbol', '')}] 做空止盈错误: entry={entry_zone}, take_profit={take_profit} 应该 < entry") + has_invalid_price = True + + # 如果价格不合理,降低等级为 D + if has_invalid_price: + original_grade = sig.get('grade', 'C') + sig['grade'] = 'D' + sig['confidence'] = 0 + # 添加错误说明 + if 'reasoning' in sig: + sig['reasoning'] = f"[价格异常] {sig['reasoning']}" + logger.error(f"❌ [{data.get('symbol', '')}] 信号价格异常,等级从 {original_grade} 降为 D,止损止盈已清空") + + # 清空不合理的价格 + sig['stop_loss'] = None + sig['take_profit'] = None + return data def _calculate_price_change_24h(self, df) -> str: diff --git a/backend/test_dingtalk.py b/backend/test_dingtalk.py new file mode 100644 index 0000000..8765ff9 --- /dev/null +++ b/backend/test_dingtalk.py @@ -0,0 +1,102 @@ +""" +测试钉钉通知服务 +""" +import asyncio +import sys +import os + +# 添加项目路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from app.services.dingtalk_service import get_dingtalk_service + + +async def test_dingtalk(): + """测试钉钉消息发送""" + dingtalk = get_dingtalk_service() + + print("=" * 50) + print("测试钉钉通知服务") + print("=" * 50) + print(f"服务状态: {'启用' if dingtalk.enabled else '禁用'}") + print(f"Webhook URL: {dingtalk.webhook_url[:50]}...") + print(f"Secret: {dingtalk.secret[:20]}...") + print() + + # 测试 1: 发送文本消息 + print("测试 1: 发送文本消息...") + success = await dingtalk.send_text("📊 钉钉通知服务测试\n\n这是一条测试消息,来自 Stock Agent 系统。") + print(f"结果: {'✅ 成功' if success else '❌ 失败'}") + print() + + # 等待一下 + await asyncio.sleep(2) + + # 测试 2: 发送 Markdown 消息 + print("测试 2: 发送 Markdown 消息...") + markdown_content = """### 📈 交易信号测试 + +> **交易对**: BTCUSDT +> **方向**: 🟢 做多 +> **价格**: $95,000.00 +> **信心度**: 85% + +**分析理由**: +- 突破关键阻力位 +- 量价配合良好 +- 多周期共振向上 + +*来自 Stock Agent 系统* +""" + success = await dingtalk.send_markdown("交易信号", markdown_content) + print(f"结果: {'✅ 成功' if success else '❌ 失败'}") + print() + + await asyncio.sleep(2) + + # 测试 3: 发送 ActionCard 消息 + print("测试 3: 发送 ActionCard 消息...") + card_content = """### 📊 模拟交易报告 + +#### 统计概览 +- 总交易次数: 50 +- 胜率: 65% +- 总盈亏: +15.2% + +#### 最近交易 +| 交易对 | 方向 | 盈亏 | +|--------|------|------| +| BTCUSDT | 做多 | +5.2% | +| ETHUSDT | 做空 | +2.1% | +""" + success = await dingtalk.send_action_card( + title="📊 4小时交易报告", + content=card_content, + btn_orientation="0", + btn_title="查看详情", + btn_url="https://example.com" + ) + print(f"结果: {'✅ 成功' if success else '❌ 失败'}") + print() + + # 测试 4: 发送交易信号 + print("测试 4: 发送交易信号...") + signal = { + 'action': 'buy', + 'symbol': 'BTCUSDT', + 'price': 95000, + 'trend': 'uptrend', + 'confidence': 85, + 'agent_type': 'crypto' + } + success = await dingtalk.send_trading_signal(signal) + print(f"结果: {'✅ 成功' if success else '❌ 失败'}") + print() + + print("=" * 50) + print("测试完成!") + print("=" * 50) + + +if __name__ == "__main__": + asyncio.run(test_dingtalk()) diff --git a/gold-agent-plan.md b/gold-agent-plan.md new file mode 100644 index 0000000..4142c9b --- /dev/null +++ b/gold-agent-plan.md @@ -0,0 +1,518 @@ +# 黄金交易智能体 (Gold Agent) 技术调研与系统设计 + +## 一、项目概述 + +### 1.1 目标 +构建一个基于 LLM 驱动的黄金 (XAUUSD) 行情分析智能体,对接 MetaTrader 5 (MT5) 进行实盘交易。 + +### 1.2 核心功能 +- 实时获取黄金行情数据(通过 MT5) +- LLM 驱动的市场分析与信号生成 +- 自动化交易执行(MT5 实盘) +- 风险管理与仓位控制 +- 飞书/Telegram 通知推送 + +--- + +## 二、技术调研 + +### 2.1 MetaTrader 5 (MT5) API 调研 + +#### MT5 Python 库 +```python +import MetaTrader5 as mt5 + +# 初始化 +mt5.initialize() + +# 获取行情数据 +rates = mt5.copy_rates_from_pos("XAUUSD", mt5.TIMEFRAME_M15, 0, 100) + +# 获取当前价格 +tick = mt5.symbol_info_tick("XAUUSD") +bid = tick.bid +ask = tick.ask + +# 下单 +request = { + "action": mt5.TRADE_ACTION_DEAL, + "symbol": "XAUUSD", + "volume": 0.01, # 手数 + "type": mt5.ORDER_TYPE_BUY, + "price": ask, + "deviation": 20, + "magic": 234000, + "comment": "Gold Agent", + "type_time": mt5.ORDER_TIME_GTC, + "type_filling": mt5.ORDER_FILLING_IOC, +} +mt5.order_send(request) +``` + +#### MT5 核心概念 +| 概念 | 说明 | +|------|------| +| **手数 (Lot)** | 黄金最小 0.01 手,1 手 = 100 盎司 | +| **点值 (Point)** | 0.01 美元/点,1 点 = 0.01 USD | +| **杠杆** | 通常 1:100 - 1:500,由经纪商设定 | +| **交易时间** | 周一 00:00 - 周六 00:00 (服务器时间) | +| **点差** | 通常 20-50 点 (0.2-0.5 USD) | + +### 2.2 黄金交易特点 + +#### XAUUSD 特性 +| 特性 | 说明 | 交易策略影响 | +|------|------|-------------| +| **波动性高** | 日波动 50-200 点 | 需要宽止损 (30-50 点) | +| **流动性强** | 24 小时交易 | 可设置夜间交易 | +| **美元相关** | 与美元指数负相关 | 需关注 USD 数据 | +| **避险属性** | 市场恐慌时上涨 | 需关注 VIX 指数 | +| **交易时段** | 伦敦盘 (15:00-24:00) 和 纽约盘 (21:00-04:00) 最活跃 | 重点交易时段 | +| **周末停盘** | 周末不交易 | 周五收盘前需要平仓或宽止损 | + +#### 技术指标适用性 +- **趋势类**: MA, EMA, MACD 效果较好 +- **震荡类**: RSI, KDJ 在盘整市有效 +- **波动率类**: ATR 对止损设置重要 +- **支撑阻力**: 黄金的关键整数位 ($2000, $2050, $2100) 效果明显 + +--- + +## 三、系统架构设计 + +### 3.1 整体架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Gold Agent 主控制器 │ +│ (gold_agent/gold_agent.py) │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ MT5 服务 │ │ 市场信号分析器 │ │ 交易决策器 │ +│ (mt5_service) │ │ (market_signal_ │ │ (trading_ │ +│ │ │ analyzer) │ │ decision) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 飞书/Telegram │ │ 模拟交易 │ │ 实盘交易 │ +│ 通知 │ │ (paper_ │ │ (mt5_ │ +│ │ │ trading) │ │ trading) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +### 3.2 目录结构 + +``` +backend/app/ +├── gold_agent/ # 黄金智能体模块 +│ ├── __init__.py +│ ├── gold_agent.py # 主控制器 +│ ├── market_signal_analyzer.py # 市场信号分析器 +│ ├── trading_decision_maker.py # 交易决策器 +│ └── strategy.py # 交易策略定义 +│ +├── services/ # 服务层 +│ ├── mt5_service.py # MT5 数据服务 +│ ├── mt5_trading_service.py # MT5 实盘交易服务 +│ └── gold_paper_trading.py # 黄金模拟交易服务 +│ +├── models/ # 数据模型 +│ └── gold_order.py # 黄金订单模型 +│ +└── api/ # API 接口 + └── gold.py # 黄金智能体 API +``` + +### 3.3 数据流设计 + +``` +MT5 行情数据 + │ + ▼ +┌─────────────────────────┐ +│ 技术指标计算 │ +│ (MA, EMA, RSI, MACD...) │ +└─────────────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ 市场信号分析 (LLM) │ +│ - 趋势判断 │ +│ - 关键价位 │ +│ - 信号生成 │ +└─────────────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ 交易决策 (LLM) │ +│ - 开仓/平仓/观望 │ +│ - 仓位大小 │ +│ - 止损止盈 │ +└─────────────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ 风险检查 │ +│ - 仓位限制 │ +│ - 价格合理性验证 │ +│ - 交易时段检查 │ +└─────────────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ 执行交易 │ +│ - 模拟交易 (回测) │ +│ - 实盘交易 (MT5) │ +└─────────────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ 通知推送 │ +│ - 飞书卡片 │ +│ - Telegram 消息 │ +└─────────────────────────┘ +``` + +--- + +## 四、核心模块设计 + +### 4.1 MT5 服务 (mt5_service.py) + +```python +class MT5Service: + """MT5 数据服务 - 获取行情数据""" + + def __init__(self): + self.connected = False + self.symbol = "XAUUSD" + + def initialize(self, account: int, password: str, server: str) -> bool + def get_rates(self, timeframe: str, count: int) -> pd.DataFrame + def get_current_price(self) -> Tuple[float, float] # (bid, ask) + def get_tick(self) -> Dict + def get_positions(self) -> List[Dict] + def get_orders(self) -> List[Dict] + def get_account_info(self) -> Dict +``` + +#### 时间周期映射 +| 代码 | MT5 常量 | 用途 | +|------|----------|------| +| `M1` | `TIMEFRAME_M1` | 短线交易 | +| `M5` | `TIMEFRAME_M5` | 主要分析周期 | +| `M15` | `TIMEFRAME_M15` | 中线交易 | +| `H1` | `TIMEFRAME_H1` | 趋势确认 | +| `H4` | `TIMEFRAME_H4` | 日内趋势 | +| `D1` | `TIMEFRAME_D1` | 长期趋势 | + +### 4.2 市场信号分析器 (market_signal_analyzer.py) + +```python +class GoldMarketSignalAnalyzer: + """黄金市场信号分析器""" + + MARKET_ANALYSIS_PROMPT = """你是一位专业的黄金交易员... + + ## 黄金交易特点 + - XAUUSD 极其活跃,日波动 50-200 点 + - 关键整数位: $2000, $2050, $2100, $2150, $2200 + - 伦敦盘 (15:00-24:00) 和 纽约盘 (21:00-04:00) 最活跃 + - 周五收盘前需要谨慎持仓 + + ## 输出格式 + { + "trend_direction": "uptrend/downtrend/neutral", + "trend_strength": "strong/medium/weak", + "signals": [ + { + "action": "buy/sell", + "entry_zone": 2050.50, + "stop_loss": 2045.00, + "take_profit": 2060.00, + "confidence": 85, + "grade": "A", + "reasoning": "..." + } + ], + "key_levels": { + "support": [2045.00, 2040.00], + "resistance": [2060.00, 2065.00] + } + } + """ + + async def analyze(self, symbol: str, data: Dict) -> Dict +``` + +### 4.3 交易决策器 (trading_decision_maker.py) + +```python +class GoldTradingDecisionMaker: + """黄金交易决策器""" + + TRADING_DECISION_PROMPT = """你是黄金交易执行者... + + ## 账户信息 + - 余额: {balance} + - 持仓: {positions} + - 杠杆: 1:100 + + ## 黄金交易规则 + - 最小手数: 0.01 手 + - 1 手 = 100 盎司 + - 点值: 0.01 USD/点 + - 建议止损: 30-50 点 + - 建议止盈: 1:2 或 1:3 + + ## 输出格式 + { + "decision": "OPEN/CLOSE/HOLD", + "action": "buy/sell", + "quantity": 0.01, # 手数 + "entry_zone": 2050.50, + "stop_loss": 2045.00, + "take_profit": 2060.00, + "reasoning": "..." + } + """ + + async def make_decision(self, market_signal, positions, account) -> Dict +``` + +### 4.4 MT5 实盘交易服务 (mt5_trading_service.py) + +```python +class MT5TradingService: + """MT5 实盘交易服务""" + + def __init__(self): + self.mt5 = mt5 + self.magic_number = 234000 # 识别 Gold Agent 的订单 + + def connect(self, account: int, password: str, server: str) -> bool + def open_order(self, action: str, volume: float, price: float, + sl: float, tp: float) -> Dict + def close_order(self, order_id: int) -> Dict + def close_all_positions(self) -> Dict + def get_positions(self) -> List[Dict] + def modify_position(self, ticket: int, sl: float, tp: float) -> Dict +``` + +#### 订单类型说明 +| 类型 | MT5 常量 | 说明 | +|------|----------|------| +| 市价买单 | `ORDER_TYPE_BUY` | 以 Ask 价格成交 | +| 市价卖单 | `ORDER_TYPE_SELL` | 以 Bid 价格成交 | +| 限价买单 | `ORDER_TYPE_BUY_LIMIT` | 低于当前价挂单 | +| 限价卖单 | `ORDER_TYPE_SELL_LIMIT` | 高于当前价挂单 | + +### 4.5 风险控制规则 + +```python +# 仓位管理 +MAX_POSITIONS = 3 # 最大同时持仓数 +MAX_LOTS_PER_ORDER = 0.1 # 单笔最大手数 +MAX_TOTAL_LOTS = 0.3 # 总持仓上限 + +# 止损止盈规则 +MIN_STOP_LOSS_POINTS = 30 # 最小止损 30 点 (0.30 USD) +TAKE_PROFIT_RATIO = 2.0 # 止盈/止损比 1:2 + +# 价格合理性验证 +MAX_PRICE_DEVIATION = 0.20 # 价格偏离不超过 20% + +# 交易时段 +WEEKEND_CLOSE_HOUR = 20 # 周五 20:00 后不开新仓 +NEWS_FILTER_WINDOW = 30 # 重大新闻前后 30 分钟不开仓 +``` + +--- + +## 五、实现计划 + +### Phase 1: 基础设施 (第 1-2 周) +- [ ] MT5 服务封装 + - [ ] 连接管理 + - [ ] 行情数据获取 + - [ ] 账户信息查询 +- [ ] 数据模型定义 + - [ ] 黄金订单模型 + - [ ] 持仓记录模型 +- [ ] 配置项添加 + - [ ] MT5 账户配置 + - [ ] 飞书 webhook 配置 + +### Phase 2: 分析与决策 (第 3-4 周) +- [ ] 市场信号分析器 + - [ ] 技术指标计算 + - [ ] LLM 提示词设计 + - [ ] 信号生成逻辑 +- [ ] 交易决策器 + - [ ] 持仓状态判断 + - [ ] 开平仓决策 + - [ ] 仓位大小计算 + +### Phase 3: 交易执行 (第 5-6 周) +- [ ] 模拟交易服务 + - [ ] 订单管理 + - [ ] 止损止盈执行 + - [ ] 移动止损逻辑 +- [ ] MT5 实盘交易服务 + - [ ] 订单发送 + - [ ] 持仓查询 + - [ ] 订单修改/平仓 + +### Phase 4: 风险管理与通知 (第 7 周) +- [ ] 风险控制模块 + - [ ] 仓位限制 + - [ ] 价格验证 + - [ ] 交易时段检查 +- [ ] 通知推送 + - [ ] 飞书卡片格式 + - [ ] Telegram 消息推送 + +### Phase 5: 测试与优化 (第 8 周) +- [ ] 模拟盘测试 +- [ ] 实盘小资金测试 +- [ ] 性能优化 +- [ ] 文档完善 + +--- + +## 六、风险考虑 + +### 6.1 技术风险 + +| 风险 | 应对措施 | +|------|----------| +| MT5 连接断开 | 自动重连机制,连接状态监控 | +| 订单执行失败 | 超时重试,失败告警 | +| 数据延迟 | 多时间周期数据验证 | +| 系统崩溃 | 持久化状态,重启恢复 | + +### 6.2 交易风险 + +| 风险 | 应对措施 | +|------|----------| +| 市场剧烈波动 | 宽止损 + 小仓位 | +| 流动性枯竭 | 避开非交易时段 | +| 滑点风险 | 使用限价单,设置最大滑点 | +| 重大新闻事件 | 新闻过滤窗口,避免开仓 | + +### 6.3 LLM 相关风险 + +| 风险 | 应对措施 | +|------|----------| +| 幻觉导致错误价格 | 价格合理性验证 | +| 信号质量不稳定 | 多信号确认,降低单次权重 | +| 延迟影响执行 | 缓存机制,异步处理 | + +--- + +## 七、配置项设计 + +### 7.1 config.py 新增配置 + +```python +# ==================== 黄金交易智能体配置 ==================== +# MT5 连接配置 +mt5_account: int = 0 # MT5 账号 +mt5_password: str = "" # MT5 密码 +mt5_server: str = "" # MT5 服务器地址 + +# 黄金交易配置 +gold_symbol: str = "XAUUSD" # 黄金交易品种 +gold_analysis_interval: int = 300 # 分析间隔(秒) +gold_enabled: bool = False # 是否启用黄金智能体 + +# 仓位管理 +gold_max_positions: int = 3 # 最大持仓数 +gold_max_lots_per_order: float = 0.1 # 单笔最大手数 +gold_max_total_lots: float = 0.3 # 总持仓上限 +gold_default_lots: float = 0.01 # 默认手数 + +# 止损止盈 +gold_min_stop_loss_points: float = 30 # 最小止损(点) +gold_take_profit_ratio: float = 2.0 # 止盈/止损比 + +# 交易时段 +gold_weekend_close_hour: int = 20 # 周五收盘时间 +gold_news_filter_minutes: int = 30 # 新闻过滤窗口(分钟) + +# 通知配置 +feishu_gold_webhook_url: str = "" # 黄金智能体飞书通知 +``` + +### 7.2 环境变量 (.env) + +```bash +# MT5 配置 +MT5_ACCOUNT=12345678 +MT5_PASSWORD=your_password +MT5_SERVER=your_broker_server + +# 黄金智能体 +GOLD_ENABLED=True +GOLD_SYMBOL=XAUUSD + +# 飞书通知 +FEISHU_GOLD_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/... +``` + +--- + +## 八、监控指标 + +### 8.1 系统监控 +- MT5 连接状态 +- 分析执行频率 +- LLM 调用延迟 +- 订单执行成功率 + +### 8.2 交易监控 +- 当前持仓数 +- 总盈亏 (USD) +- 胜率 +- 最大回撤 +- 平均持仓时间 + +### 8.3 信号监控 +- 信号生成频率 +- A/B/C/D 级信号分布 +- 信号执行率 +- 信号盈亏比 + +--- + +## 九、后续优化方向 + +1. **多品种支持** - 扩展到 XAGUSD (白银)、其他贵金属 +2. **智能参数调优** - 基于历史数据自动优化参数 +3. **策略回测** - 使用 MT5 历史数据进行策略回测 +4. **风险模型升级** - VaR 计算、凯利公式仓位管理 +5. **机器学习增强** - 使用 ML 模型辅助 LLM 决策 + +--- + +## 十、总结 + +本设计方案基于现有 Crypto Agent 架构,复用了: +- LLM 驱动的市场分析框架 +- 交易决策框架 +- 风险验证逻辑 +- 通知推送机制 + +主要新增: +- MT5 数据与交易接口 +- 黄金特定的交易规则 +- 适配黄金特点的提示词 + +预计开发周期:**8 周** +风险等级:**中等** (实盘交易需谨慎) +投入建议:**先模拟盘充分测试后再小资金实盘**