添加钉钉

This commit is contained in:
aaron 2026-02-26 20:46:56 +08:00
parent 019961e629
commit 2fb2321399
9 changed files with 1134 additions and 3 deletions

View File

@ -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 分析的置信度阈值

View File

@ -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]}")

View File

@ -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:

View File

@ -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:

View File

@ -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}")

View File

@ -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}&timestamp={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

View File

@ -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:

102
backend/test_dingtalk.py Normal file
View File

@ -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())

518
gold-agent-plan.md Normal file
View File

@ -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 周**
风险等级:**中等** (实盘交易需谨慎)
投入建议:**先模拟盘充分测试后再小资金实盘**