添加钉钉
This commit is contained in:
parent
019961e629
commit
2fb2321399
@ -104,6 +104,11 @@ class Settings(BaseSettings):
|
|||||||
telegram_channel_id: str = "" # 频道 ID,如 @your_channel 或 -1001234567890
|
telegram_channel_id: str = "" # 频道 ID,如 @your_channel 或 -1001234567890
|
||||||
telegram_enabled: bool = True # 是否启用 Telegram 通知
|
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_symbols: str = "BTCUSDT,ETHUSDT" # 监控的交易对,逗号分隔
|
||||||
crypto_analysis_interval: int = 60 # 分析间隔(秒)
|
crypto_analysis_interval: int = 60 # 分析间隔(秒)
|
||||||
@ -174,7 +179,7 @@ class Settings(BaseSettings):
|
|||||||
# 新能源:比亚迪/理想/小鹏/赣锋锂业/龙源电力/信义能源
|
# 新能源:比亚迪/理想/小鹏/赣锋锂业/龙源电力/信义能源
|
||||||
# 芯片:中芯国际/华虹半导体/上海复旦
|
# 芯片:中芯国际/华虹半导体/上海复旦
|
||||||
# AI:商汤/第四范式/创新奇智/美图/联易融/百融云
|
# 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_analysis_interval: int = 300 # 分析间隔(秒,默认5分钟)
|
||||||
stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值
|
stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from app.config import get_settings
|
|||||||
from app.services.bitget_service import bitget_service
|
from app.services.bitget_service import bitget_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.dingtalk_service import get_dingtalk_service
|
||||||
from app.services.paper_trading_service import get_paper_trading_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.services.signal_database_service import get_signal_db_service
|
||||||
from app.crypto_agent.market_signal_analyzer import MarketSignalAnalyzer
|
from app.crypto_agent.market_signal_analyzer import MarketSignalAnalyzer
|
||||||
@ -41,6 +42,7 @@ class CryptoAgent:
|
|||||||
self.exchange = bitget_service # 交易所服务
|
self.exchange = bitget_service # 交易所服务
|
||||||
self.feishu = get_feishu_service()
|
self.feishu = get_feishu_service()
|
||||||
self.telegram = get_telegram_service()
|
self.telegram = get_telegram_service()
|
||||||
|
self.dingtalk = get_dingtalk_service() # 添加钉钉服务
|
||||||
|
|
||||||
# 新架构:市场信号分析器 + 交易决策器
|
# 新架构:市场信号分析器 + 交易决策器
|
||||||
self.market_analyzer = MarketSignalAnalyzer()
|
self.market_analyzer = MarketSignalAnalyzer()
|
||||||
@ -140,6 +142,8 @@ class CryptoAgent:
|
|||||||
if self.settings.telegram_enabled:
|
if self.settings.telegram_enabled:
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
await self.telegram.send_message(message)
|
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')}")
|
logger.info(f"已发送挂单成交通知: {result.get('order_id')}")
|
||||||
|
|
||||||
async def _notify_pending_cancelled(self, result: Dict[str, Any]):
|
async def _notify_pending_cancelled(self, result: Dict[str, Any]):
|
||||||
@ -164,6 +168,8 @@ class CryptoAgent:
|
|||||||
if self.settings.telegram_enabled:
|
if self.settings.telegram_enabled:
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
await self.telegram.send_message(message)
|
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')}")
|
logger.info(f"已发送挂单撤销通知: {result.get('order_id')}")
|
||||||
|
|
||||||
async def _notify_breakeven_triggered(self, result: Dict[str, Any]):
|
async def _notify_breakeven_triggered(self, result: Dict[str, Any]):
|
||||||
@ -191,6 +197,8 @@ class CryptoAgent:
|
|||||||
if self.settings.telegram_enabled:
|
if self.settings.telegram_enabled:
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
await self.telegram.send_message(message)
|
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')}")
|
logger.info(f"已发送移动止损通知: {result.get('order_id')}")
|
||||||
|
|
||||||
async def _notify_order_closed(self, result: Dict[str, Any]):
|
async def _notify_order_closed(self, result: Dict[str, Any]):
|
||||||
@ -238,6 +246,8 @@ class CryptoAgent:
|
|||||||
if self.settings.telegram_enabled:
|
if self.settings.telegram_enabled:
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
await self.telegram.send_message(message)
|
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')}")
|
logger.info(f"已发送订单平仓通知: {result.get('order_id')}")
|
||||||
|
|
||||||
def _get_seconds_until_next_5min(self) -> int:
|
def _get_seconds_until_next_5min(self) -> int:
|
||||||
@ -997,6 +1007,9 @@ class CryptoAgent:
|
|||||||
# Telegram 使用文本格式
|
# Telegram 使用文本格式
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
await self.telegram.send_message(message)
|
await self.telegram.send_message(message)
|
||||||
|
if self.settings.dingtalk_enabled:
|
||||||
|
# 钉钉使用 ActionCard 格式
|
||||||
|
await self.dingtalk.send_action_card(title, content)
|
||||||
logger.info(f" 📤 已发送市场信号通知 (阈值: {threshold}%)")
|
logger.info(f" 📤 已发送市场信号通知 (阈值: {threshold}%)")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -1124,6 +1137,8 @@ class CryptoAgent:
|
|||||||
# Telegram 使用文本格式
|
# Telegram 使用文本格式
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
await self.telegram.send_message(message)
|
await self.telegram.send_message(message)
|
||||||
|
if self.settings.dingtalk_enabled:
|
||||||
|
await self.dingtalk.send_action_card(title, content)
|
||||||
|
|
||||||
logger.info(f" 📤 已发送交易决策通知: {decision_text}")
|
logger.info(f" 📤 已发送交易决策通知: {decision_text}")
|
||||||
|
|
||||||
@ -1256,6 +1271,8 @@ class CryptoAgent:
|
|||||||
# Telegram 使用文本格式
|
# Telegram 使用文本格式
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
await self.telegram.send_message(message)
|
await self.telegram.send_message(message)
|
||||||
|
if self.settings.dingtalk_enabled:
|
||||||
|
await self.dingtalk.send_action_card(title, content)
|
||||||
logger.info(f" 📤 已发送交易执行通知: {decision_text}")
|
logger.info(f" 📤 已发送交易执行通知: {decision_text}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -1690,6 +1707,8 @@ class CryptoAgent:
|
|||||||
if self.settings.telegram_enabled:
|
if self.settings.telegram_enabled:
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
await self.telegram.send_message(message)
|
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')}")
|
logger.info(f"已发送实盘订单创建通知: {result.get('order_id')}")
|
||||||
|
|
||||||
async def _notify_position_adjustment(
|
async def _notify_position_adjustment(
|
||||||
@ -1741,6 +1760,8 @@ class CryptoAgent:
|
|||||||
if self.settings.telegram_enabled:
|
if self.settings.telegram_enabled:
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
await self.telegram.send_message(message)
|
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(
|
async def _notify_signal_not_executed(
|
||||||
self,
|
self,
|
||||||
@ -1811,6 +1832,8 @@ class CryptoAgent:
|
|||||||
if self.settings.telegram_enabled:
|
if self.settings.telegram_enabled:
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
await self.telegram.send_message(message)
|
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]}")
|
logger.info(f" 📤 已发送信号未执行通知: {decision_type} - {final_reason[:50]}")
|
||||||
|
|
||||||
|
|||||||
@ -592,12 +592,65 @@ class MarketSignalAnalyzer:
|
|||||||
|
|
||||||
# 清理 signals 中的价格字段
|
# 清理 signals 中的价格字段
|
||||||
if 'signals' in data:
|
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']
|
price_fields = ['entry_zone', 'stop_loss', 'take_profit']
|
||||||
for field in price_fields:
|
for field in price_fields:
|
||||||
if field in sig:
|
if field in sig:
|
||||||
sig[field] = clean_price(sig[field])
|
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
|
return data
|
||||||
|
|
||||||
def _calculate_price_change_24h(self, df) -> str:
|
def _calculate_price_change_24h(self, df) -> str:
|
||||||
|
|||||||
@ -573,6 +573,60 @@ class TradingDecisionMaker:
|
|||||||
if field in data:
|
if field in data:
|
||||||
data[field] = clean_price(data[field])
|
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
|
return data
|
||||||
|
|
||||||
def _clean_json_string(self, json_str: str) -> str:
|
def _clean_json_string(self, json_str: str) -> str:
|
||||||
|
|||||||
@ -365,6 +365,7 @@ async def periodic_report_loop():
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.services.paper_trading_service import get_paper_trading_service
|
from app.services.paper_trading_service import get_paper_trading_service
|
||||||
from app.services.telegram_service import get_telegram_service
|
from app.services.telegram_service import get_telegram_service
|
||||||
|
from app.config import get_settings
|
||||||
|
|
||||||
logger.info("定时报告任务已启动")
|
logger.info("定时报告任务已启动")
|
||||||
|
|
||||||
@ -394,13 +395,19 @@ async def periodic_report_loop():
|
|||||||
# 等待到下一个4小时整点
|
# 等待到下一个4小时整点
|
||||||
await asyncio.sleep(wait_seconds)
|
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()
|
paper_trading = get_paper_trading_service()
|
||||||
telegram = get_telegram_service()
|
telegram = get_telegram_service()
|
||||||
|
|
||||||
report = paper_trading.generate_report(hours=4)
|
report = paper_trading.generate_report(hours=4)
|
||||||
await telegram.send_message(report, parse_mode="HTML")
|
await telegram.send_message(report, parse_mode="HTML")
|
||||||
logger.info("已发送4小时模拟交易报告")
|
logger.info("已发送4小时模拟交易报告到 Telegram")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"定时报告循环出错: {e}")
|
logger.error(f"定时报告循环出错: {e}")
|
||||||
|
|||||||
319
backend/app/services/dingtalk_service.py
Normal file
319
backend/app/services/dingtalk_service.py
Normal 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}×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
|
||||||
@ -658,6 +658,56 @@ class StockMarketSignalAnalyzer:
|
|||||||
if field in sig:
|
if field in sig:
|
||||||
sig[field] = clean_price(sig[field])
|
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
|
return data
|
||||||
|
|
||||||
def _calculate_price_change_24h(self, df) -> str:
|
def _calculate_price_change_24h(self, df) -> str:
|
||||||
|
|||||||
102
backend/test_dingtalk.py
Normal file
102
backend/test_dingtalk.py
Normal 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
518
gold-agent-plan.md
Normal 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 周**
|
||||||
|
风险等级:**中等** (实盘交易需谨慎)
|
||||||
|
投入建议:**先模拟盘充分测试后再小资金实盘**
|
||||||
Loading…
Reference in New Issue
Block a user