635 lines
25 KiB
Python
635 lines
25 KiB
Python
"""
|
|
智能盯盘 - 主引擎
|
|
整合DeepSeek AI决策、数据获取、交易执行、通知等功能
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
from typing import Dict, List, Optional
|
|
from datetime import datetime
|
|
import threading
|
|
|
|
from smart_monitor_deepseek import SmartMonitorDeepSeek
|
|
from smart_monitor_data import SmartMonitorDataFetcher
|
|
from smart_monitor_qmt import SmartMonitorQMT, SmartMonitorQMTSimulator
|
|
from smart_monitor_db import SmartMonitorDB
|
|
from notification_service import notification_service # 复用主程序的通知服务
|
|
from config_manager import config_manager # 复用主程序的配置管理器
|
|
|
|
|
|
class SmartMonitorEngine:
|
|
"""智能盯盘引擎"""
|
|
|
|
def __init__(self, deepseek_api_key: str = None, qmt_account_id: str = None,
|
|
use_simulator: bool = None):
|
|
"""
|
|
初始化智能盯盘引擎
|
|
|
|
Args:
|
|
deepseek_api_key: DeepSeek API密钥(可选,从配置读取)
|
|
qmt_account_id: miniQMT账户ID(可选,从配置读取)
|
|
use_simulator: 是否使用模拟交易(可选,从配置读取)
|
|
"""
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
# 从配置管理器读取配置
|
|
config = config_manager.read_env()
|
|
|
|
# DeepSeek API
|
|
if deepseek_api_key is None:
|
|
deepseek_api_key = config.get('DEEPSEEK_API_KEY', '')
|
|
|
|
# MiniQMT配置
|
|
if qmt_account_id is None:
|
|
qmt_account_id = config.get('MINIQMT_ACCOUNT_ID', '')
|
|
|
|
if use_simulator is None:
|
|
# 如果MINIQMT_ENABLED=false,则使用模拟器
|
|
miniqmt_enabled = config.get('MINIQMT_ENABLED', 'false').lower() == 'true'
|
|
use_simulator = not miniqmt_enabled
|
|
|
|
# 初始化各个模块
|
|
self.deepseek = SmartMonitorDeepSeek(deepseek_api_key)
|
|
self.data_fetcher = SmartMonitorDataFetcher()
|
|
self.db = SmartMonitorDB()
|
|
self.notification = notification_service # 使用主程序的通知服务
|
|
|
|
# 初始化交易接口
|
|
if use_simulator:
|
|
self.qmt = SmartMonitorQMTSimulator()
|
|
self.qmt.connect(qmt_account_id or "simulator")
|
|
self.logger.info("使用模拟交易模式")
|
|
else:
|
|
self.qmt = SmartMonitorQMT()
|
|
if qmt_account_id:
|
|
success = self.qmt.connect(qmt_account_id)
|
|
if success:
|
|
self.logger.info(f"已连接miniQMT账户: {qmt_account_id}")
|
|
else:
|
|
self.logger.warning(f"连接miniQMT失败,切换到模拟模式")
|
|
self.qmt = SmartMonitorQMTSimulator()
|
|
self.qmt.connect("simulator")
|
|
else:
|
|
self.logger.warning("未配置miniQMT账户,使用模拟模式")
|
|
self.qmt = SmartMonitorQMTSimulator()
|
|
self.qmt.connect("simulator")
|
|
|
|
# 监控线程控制
|
|
self.monitoring_threads = {}
|
|
self.stop_flags = {}
|
|
|
|
self.logger.info("智能盯盘引擎初始化完成")
|
|
|
|
def analyze_stock(self, stock_code: str, auto_trade: bool = False,
|
|
notify: bool = True, has_position: bool = False,
|
|
position_cost: float = 0, position_quantity: int = 0,
|
|
trading_hours_only: bool = True) -> Dict:
|
|
"""
|
|
分析单只股票并做出决策
|
|
|
|
Args:
|
|
stock_code: 股票代码
|
|
auto_trade: 是否自动交易
|
|
notify: 是否发送通知
|
|
has_position: 是否已持仓(可选)
|
|
position_cost: 持仓成本(可选)
|
|
position_quantity: 持仓数量(可选)
|
|
trading_hours_only: 是否仅在交易时段分析(可选,默认True)
|
|
|
|
Returns:
|
|
分析结果
|
|
"""
|
|
try:
|
|
self.logger.info(f"[{stock_code}] 开始分析...")
|
|
|
|
# 1. 检查交易时段
|
|
session_info = self.deepseek.get_trading_session()
|
|
self.logger.info(f"[{stock_code}] 当前时段: {session_info['session']}")
|
|
|
|
# 如果启用了仅交易时段分析,且当前不在交易时段,则跳过分析
|
|
if trading_hours_only and not session_info.get('can_trade', False):
|
|
self.logger.info(f"[{stock_code}] 非交易时段,跳过分析")
|
|
return {
|
|
'success': False,
|
|
'error': f"非交易时段({session_info['session']}),跳过分析",
|
|
'session_info': session_info,
|
|
'skipped': True
|
|
}
|
|
|
|
# 2. 获取市场数据
|
|
market_data = self.data_fetcher.get_comprehensive_data(stock_code)
|
|
if not market_data:
|
|
return {
|
|
'success': False,
|
|
'error': '获取市场数据失败'
|
|
}
|
|
|
|
# 3. 获取账户信息
|
|
account_info = self.qmt.get_account_info()
|
|
|
|
# 4. 检查是否已持有该股票
|
|
# 优先使用传入的持仓信息,否则从QMT获取
|
|
if has_position and position_cost > 0 and position_quantity > 0:
|
|
# 使用用户设置的持仓信息
|
|
self.logger.info(f"[{stock_code}] 使用监控任务设置的持仓: {position_quantity}股 @ {position_cost:.2f}元")
|
|
else:
|
|
# 从QMT获取持仓
|
|
position = self.qmt.get_position(stock_code)
|
|
has_position = position is not None
|
|
|
|
if has_position:
|
|
position_cost = position.get('cost_price', 0)
|
|
position_quantity = position.get('quantity', 0)
|
|
account_info['current_position'] = position
|
|
self.logger.info(f"[{stock_code}] 从QMT获取持仓: {position_quantity}股, "
|
|
f"成本价: {position_cost:.2f}, "
|
|
f"浮动盈亏: {position.get('profit_loss_pct', 0):+.2f}%")
|
|
|
|
# 5. 调用DeepSeek AI决策
|
|
ai_result = self.deepseek.analyze_stock_and_decide(
|
|
stock_code=stock_code,
|
|
market_data=market_data,
|
|
account_info=account_info,
|
|
has_position=has_position,
|
|
position_cost=position_cost,
|
|
position_quantity=position_quantity
|
|
)
|
|
|
|
if not ai_result['success']:
|
|
return {
|
|
'success': False,
|
|
'error': 'AI决策失败',
|
|
'details': ai_result
|
|
}
|
|
|
|
decision = ai_result['decision']
|
|
|
|
self.logger.info(f"[{stock_code}] AI决策: {decision['action']} "
|
|
f"(信心度: {decision['confidence']}%)")
|
|
self.logger.info(f"[{stock_code}] 决策理由: {decision['reasoning'][:100]}...")
|
|
|
|
# 6. 保存AI决策到数据库
|
|
decision_id = self.db.save_ai_decision({
|
|
'stock_code': stock_code,
|
|
'stock_name': market_data.get('name'),
|
|
'trading_session': session_info['session'],
|
|
'action': decision['action'],
|
|
'confidence': decision['confidence'],
|
|
'reasoning': decision['reasoning'],
|
|
'position_size_pct': decision.get('position_size_pct'),
|
|
'stop_loss_pct': decision.get('stop_loss_pct'),
|
|
'take_profit_pct': decision.get('take_profit_pct'),
|
|
'risk_level': decision.get('risk_level'),
|
|
'key_price_levels': decision.get('key_price_levels', {}),
|
|
'market_data': market_data,
|
|
'account_info': account_info
|
|
})
|
|
|
|
# 7. 执行交易(如果开启自动交易)
|
|
execution_result = None
|
|
if auto_trade and session_info['can_trade']:
|
|
execution_result = self._execute_decision(
|
|
stock_code=stock_code,
|
|
decision=decision,
|
|
market_data=market_data,
|
|
has_position=has_position
|
|
)
|
|
|
|
# 更新决策执行状态
|
|
self.db.update_decision_execution(
|
|
decision_id=decision_id,
|
|
executed=execution_result.get('success', False),
|
|
result=str(execution_result)
|
|
)
|
|
|
|
# 8. 发送通知
|
|
if notify:
|
|
self._send_notification(
|
|
stock_code=stock_code,
|
|
stock_name=market_data.get('name'),
|
|
decision=decision,
|
|
execution_result=execution_result,
|
|
market_data=market_data
|
|
)
|
|
|
|
return {
|
|
'success': True,
|
|
'stock_code': stock_code,
|
|
'stock_name': market_data.get('name'),
|
|
'session_info': session_info,
|
|
'market_data': market_data,
|
|
'decision': decision,
|
|
'decision_id': decision_id,
|
|
'execution_result': execution_result
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"[{stock_code}] 分析失败: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return {
|
|
'success': False,
|
|
'error': str(e)
|
|
}
|
|
|
|
def _execute_decision(self, stock_code: str, decision: Dict,
|
|
market_data: Dict, has_position: bool) -> Dict:
|
|
"""
|
|
执行AI决策
|
|
|
|
Args:
|
|
stock_code: 股票代码
|
|
decision: AI决策
|
|
market_data: 市场数据
|
|
has_position: 是否已持有
|
|
|
|
Returns:
|
|
执行结果
|
|
"""
|
|
action = decision['action']
|
|
|
|
try:
|
|
if action == 'BUY' and not has_position:
|
|
# 买入逻辑
|
|
return self._execute_buy(stock_code, decision, market_data)
|
|
|
|
elif action == 'SELL' and has_position:
|
|
# 卖出逻辑
|
|
return self._execute_sell(stock_code, decision, market_data)
|
|
|
|
elif action == 'HOLD':
|
|
# 持有,不操作
|
|
return {
|
|
'success': True,
|
|
'action': 'HOLD',
|
|
'message': 'AI建议持有,未执行交易'
|
|
}
|
|
|
|
else:
|
|
return {
|
|
'success': False,
|
|
'error': f'无效操作: {action}'
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"[{stock_code}] 执行交易失败: {e}")
|
|
return {
|
|
'success': False,
|
|
'error': str(e)
|
|
}
|
|
|
|
def _execute_buy(self, stock_code: str, decision: Dict, market_data: Dict) -> Dict:
|
|
"""执行买入"""
|
|
try:
|
|
# 获取账户信息
|
|
account_info = self.qmt.get_account_info()
|
|
available_cash = account_info['available_cash']
|
|
|
|
# 计算买入金额
|
|
position_size_pct = decision.get('position_size_pct', 20)
|
|
buy_amount = available_cash * (position_size_pct / 100)
|
|
|
|
# 计算买入数量(必须是100的整数倍)
|
|
current_price = market_data['current_price']
|
|
quantity = int(buy_amount / current_price / 100) * 100
|
|
|
|
if quantity < 100:
|
|
return {
|
|
'success': False,
|
|
'error': f'资金不足,最少需要买入100股(约{current_price * 100:.2f}元)'
|
|
}
|
|
|
|
# 执行买入
|
|
result = self.qmt.buy_stock(
|
|
stock_code=stock_code,
|
|
quantity=quantity,
|
|
price=current_price,
|
|
order_type='market'
|
|
)
|
|
|
|
if result['success']:
|
|
# 保存交易记录
|
|
self.db.save_trade_record({
|
|
'stock_code': stock_code,
|
|
'stock_name': market_data.get('name'),
|
|
'trade_type': 'BUY',
|
|
'quantity': quantity,
|
|
'price': current_price,
|
|
'amount': quantity * current_price,
|
|
'order_id': result.get('order_id'),
|
|
'order_status': '已提交'
|
|
})
|
|
|
|
# 保存持仓监控
|
|
self.db.save_position({
|
|
'stock_code': stock_code,
|
|
'stock_name': market_data.get('name'),
|
|
'quantity': quantity,
|
|
'cost_price': current_price,
|
|
'current_price': current_price,
|
|
'profit_loss': 0,
|
|
'profit_loss_pct': 0,
|
|
'holding_days': 0,
|
|
'buy_date': datetime.now().strftime('%Y-%m-%d'),
|
|
'stop_loss_price': current_price * (1 - decision.get('stop_loss_pct', 5) / 100),
|
|
'take_profit_price': current_price * (1 + decision.get('take_profit_pct', 10) / 100)
|
|
})
|
|
|
|
self.logger.info(f"[{stock_code}] 买入成功: {quantity}股 @ {current_price:.2f}元")
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"[{stock_code}] 买入失败: {e}")
|
|
return {
|
|
'success': False,
|
|
'error': str(e)
|
|
}
|
|
|
|
def _execute_sell(self, stock_code: str, decision: Dict, market_data: Dict) -> Dict:
|
|
"""执行卖出"""
|
|
try:
|
|
# 获取持仓
|
|
position = self.qmt.get_position(stock_code)
|
|
if not position:
|
|
return {
|
|
'success': False,
|
|
'error': '未持有该股票'
|
|
}
|
|
|
|
# 可卖数量(考虑T+1限制)
|
|
can_sell = position['can_sell']
|
|
if can_sell <= 0:
|
|
return {
|
|
'success': False,
|
|
'error': 'T+1限制,今天买入的股票明天才能卖出'
|
|
}
|
|
|
|
# 执行卖出
|
|
current_price = market_data['current_price']
|
|
result = self.qmt.sell_stock(
|
|
stock_code=stock_code,
|
|
quantity=can_sell,
|
|
price=current_price,
|
|
order_type='market'
|
|
)
|
|
|
|
if result['success']:
|
|
# 计算盈亏
|
|
profit_loss = (current_price - position['cost_price']) * can_sell
|
|
|
|
# 保存交易记录
|
|
self.db.save_trade_record({
|
|
'stock_code': stock_code,
|
|
'stock_name': market_data.get('name'),
|
|
'trade_type': 'SELL',
|
|
'quantity': can_sell,
|
|
'price': current_price,
|
|
'amount': can_sell * current_price,
|
|
'order_id': result.get('order_id'),
|
|
'order_status': '已提交',
|
|
'profit_loss': profit_loss
|
|
})
|
|
|
|
# 更新或关闭持仓记录
|
|
if can_sell >= position['quantity']:
|
|
self.db.close_position(stock_code)
|
|
|
|
self.logger.info(f"[{stock_code}] 卖出成功: {can_sell}股 @ {current_price:.2f}元, "
|
|
f"盈亏: {profit_loss:+.2f}元")
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"[{stock_code}] 卖出失败: {e}")
|
|
return {
|
|
'success': False,
|
|
'error': str(e)
|
|
}
|
|
|
|
def _send_notification(self, stock_code: str, stock_name: str,
|
|
decision: Dict, execution_result: Optional[Dict],
|
|
market_data: Dict):
|
|
"""
|
|
发送通知(使用主程序的通知服务)
|
|
优化策略:仅在买入或卖出信号时发送通知,持有信号不发送
|
|
"""
|
|
try:
|
|
action = decision['action'].upper()
|
|
|
|
# 仅在买入或卖出时发送通知,持有信号不发送
|
|
if action not in ['BUY', 'SELL']:
|
|
self.logger.info(f"[{stock_code}] 决策为{action},不发送通知")
|
|
return
|
|
|
|
# 构建通知内容
|
|
action_text = {
|
|
'BUY': '🟢 买入',
|
|
'SELL': '🔴 卖出'
|
|
}.get(action, action)
|
|
|
|
message = f"{action_text}信号 - {stock_name}({stock_code})"
|
|
|
|
# 简化的AI决策内容(提取核心信息)
|
|
reasoning_summary = decision['reasoning'][:150] + '...' if len(decision['reasoning']) > 150 else decision['reasoning']
|
|
|
|
# 提取关键价位信息
|
|
key_levels = decision.get('key_price_levels', {})
|
|
support = key_levels.get('support', 'N/A')
|
|
resistance = key_levels.get('resistance', 'N/A')
|
|
|
|
# 构建简化的详细内容
|
|
content = f"""
|
|
【{action_text}信号】{stock_name}({stock_code})
|
|
|
|
📊 市场信息
|
|
• 当前价: ¥{market_data.get('current_price', 0):.2f}
|
|
• 涨跌幅: {market_data.get('change_pct', 0):+.2f}%
|
|
• 成交量: {market_data.get('volume', 0):,.0f}手
|
|
|
|
🤖 AI决策
|
|
• 操作: {action_text}
|
|
• 信心度: {decision['confidence']}%
|
|
• 风险: {decision.get('risk_level', '中')}
|
|
|
|
💡 核心理由
|
|
{reasoning_summary}
|
|
|
|
📈 关键价位
|
|
• 支撑位: {support}
|
|
• 阻力位: {resistance}
|
|
• 止盈: {decision.get('take_profit_pct', 'N/A')}%
|
|
• 止损: {decision.get('stop_loss_pct', 'N/A')}%
|
|
|
|
📉 技术指标
|
|
• MA5: {market_data.get('ma5', 0):.2f} / MA20: {market_data.get('ma20', 0):.2f}
|
|
• RSI(6): {market_data.get('rsi6', 0):.1f}
|
|
• MACD: {market_data.get('macd', 0):.4f}
|
|
"""
|
|
|
|
if execution_result:
|
|
if execution_result.get('success'):
|
|
content += f"\n✅ 操作已自动执行"
|
|
else:
|
|
content += f"\n⚠️ 执行失败: {execution_result.get('error')}"
|
|
|
|
content += f"\n\n⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
|
|
|
# 使用主程序的通知服务格式
|
|
notification_data = {
|
|
'symbol': stock_code,
|
|
'name': stock_name,
|
|
'type': '智能盯盘',
|
|
'message': message,
|
|
'details': content,
|
|
'triggered_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
}
|
|
|
|
# 直接调用主程序的通知服务发送
|
|
success = self.notification.send_notification(notification_data)
|
|
|
|
if success:
|
|
self.logger.info(f"[{stock_code}] {action_text}通知已发送")
|
|
else:
|
|
self.logger.warning(f"[{stock_code}] 通知发送失败")
|
|
|
|
# 同时保存到智能盯盘的数据库
|
|
self.db.save_notification({
|
|
'stock_code': stock_code,
|
|
'notify_type': 'decision',
|
|
'subject': f"智能盯盘 - {message}",
|
|
'content': content,
|
|
'status': 'sent' if success else 'failed'
|
|
})
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"[{stock_code}] 发送通知失败: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
def start_monitor(self, stock_code: str, check_interval: int = 300,
|
|
auto_trade: bool = False, notify: bool = True,
|
|
has_position: bool = False, position_cost: float = 0,
|
|
position_quantity: int = 0, trading_hours_only: bool = True):
|
|
"""
|
|
启动股票监控(在独立线程中运行)
|
|
|
|
Args:
|
|
stock_code: 股票代码
|
|
check_interval: 检查间隔(秒)
|
|
auto_trade: 是否自动交易
|
|
notify: 是否发送通知
|
|
has_position: 是否已持仓
|
|
position_cost: 持仓成本
|
|
position_quantity: 持仓数量
|
|
trading_hours_only: 是否仅在交易时段监控(默认True)
|
|
"""
|
|
if stock_code in self.monitoring_threads:
|
|
self.logger.warning(f"[{stock_code}] 监控已在运行中")
|
|
return
|
|
|
|
# 创建停止标志
|
|
stop_flag = threading.Event()
|
|
self.stop_flags[stock_code] = stop_flag
|
|
|
|
# 创建监控线程
|
|
thread = threading.Thread(
|
|
target=self._monitor_loop,
|
|
args=(stock_code, check_interval, auto_trade, notify, stop_flag,
|
|
has_position, position_cost, position_quantity, trading_hours_only),
|
|
daemon=True
|
|
)
|
|
|
|
self.monitoring_threads[stock_code] = thread
|
|
thread.start()
|
|
|
|
position_info = f"(持仓: {position_quantity}股 @ {position_cost:.2f}元)" if has_position else ""
|
|
trading_info = "(仅交易时段)" if trading_hours_only else "(全时段)"
|
|
self.logger.info(f"[{stock_code}] 监控已启动{trading_info},间隔: {check_interval}秒 {position_info}")
|
|
|
|
def stop_monitor(self, stock_code: str):
|
|
"""停止股票监控"""
|
|
if stock_code not in self.monitoring_threads:
|
|
self.logger.warning(f"[{stock_code}] 监控未运行")
|
|
return
|
|
|
|
# 设置停止标志
|
|
self.stop_flags[stock_code].set()
|
|
|
|
# 等待线程结束
|
|
self.monitoring_threads[stock_code].join(timeout=5)
|
|
|
|
# 清理
|
|
del self.monitoring_threads[stock_code]
|
|
del self.stop_flags[stock_code]
|
|
|
|
self.logger.info(f"[{stock_code}] 监控已停止")
|
|
|
|
def _monitor_loop(self, stock_code: str, check_interval: int,
|
|
auto_trade: bool, notify: bool, stop_flag: threading.Event,
|
|
has_position: bool = False, position_cost: float = 0,
|
|
position_quantity: int = 0, trading_hours_only: bool = True):
|
|
"""监控循环(在独立线程中运行)"""
|
|
self.logger.info(f"[{stock_code}] 监控线程已启动")
|
|
|
|
while not stop_flag.is_set():
|
|
try:
|
|
# 执行分析
|
|
result = self.analyze_stock(
|
|
stock_code=stock_code,
|
|
auto_trade=auto_trade,
|
|
notify=notify,
|
|
has_position=has_position,
|
|
position_cost=position_cost,
|
|
position_quantity=position_quantity,
|
|
trading_hours_only=trading_hours_only
|
|
)
|
|
|
|
if result.get('skipped'):
|
|
# 非交易时段跳过,不算错误
|
|
self.logger.debug(f"[{stock_code}] {result.get('error')}")
|
|
elif result['success']:
|
|
self.logger.info(f"[{stock_code}] 分析完成: {result['decision']['action']}")
|
|
else:
|
|
self.logger.error(f"[{stock_code}] 分析失败: {result.get('error')}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"[{stock_code}] 监控循环异常: {e}")
|
|
|
|
# 等待下一次检查
|
|
stop_flag.wait(check_interval)
|
|
|
|
self.logger.info(f"[{stock_code}] 监控线程已退出")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# 测试代码
|
|
import os
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
|
|
# 使用模拟模式测试
|
|
engine = SmartMonitorEngine(
|
|
deepseek_api_key=os.getenv('DEEPSEEK_API_KEY'),
|
|
use_simulator=True
|
|
)
|
|
|
|
# 测试分析贵州茅台
|
|
print("\n测试分析贵州茅台(600519)...")
|
|
result = engine.analyze_stock('600519', auto_trade=False, notify=False)
|
|
|
|
if result['success']:
|
|
print(f"\n分析成功!")
|
|
print(f" 决策: {result['decision']['action']}")
|
|
print(f" 信心度: {result['decision']['confidence']}%")
|
|
print(f" 理由: {result['decision']['reasoning'][:100]}...")
|
|
else:
|
|
print(f"\n分析失败: {result.get('error')}")
|
|
|