207 lines
6.2 KiB
Python
207 lines
6.2 KiB
Python
"""
|
||
A股智能体 - 主控制器
|
||
负责执行每日选股并发送通知
|
||
"""
|
||
import asyncio
|
||
from typing import Dict, Any, Optional
|
||
from datetime import datetime, time
|
||
from app.utils.logger import logger
|
||
from app.config import get_settings
|
||
from app.services.dingtalk_service import get_dingtalk_service
|
||
from app.services.telegram_service import get_telegram_service
|
||
from app.astock_agent.tushare_client import get_tushare_client
|
||
from app.astock_agent.short_term_thematic_selector import get_thematic_selector
|
||
|
||
|
||
class AStockAgent:
|
||
"""A股智能体"""
|
||
|
||
_instance = None
|
||
_initialized = False
|
||
|
||
def __new__(cls, *args, **kwargs):
|
||
"""单例模式"""
|
||
if cls._instance is None:
|
||
cls._instance = super().__new__(cls)
|
||
return cls._instance
|
||
|
||
def __init__(self):
|
||
"""初始化智能体"""
|
||
if AStockAgent._initialized:
|
||
return
|
||
|
||
AStockAgent._initialized = True
|
||
self.settings = get_settings()
|
||
|
||
# 初始化Tushare客户端
|
||
self.ts_client = get_tushare_client(self.settings.tushare_token)
|
||
if not self.ts_client:
|
||
logger.error("Tushare客户端初始化失败,请检查配置")
|
||
raise Exception("Tushare客户端初始化失败")
|
||
|
||
# 初始化选股器
|
||
self.selector = get_thematic_selector(self.ts_client)
|
||
|
||
# 初始化通知服务
|
||
self.dingtalk = get_dingtalk_service()
|
||
self.telegram = get_telegram_service()
|
||
|
||
# 运行状态
|
||
self.running = False
|
||
self._task = None
|
||
|
||
logger.info("A股智能体初始化完成")
|
||
|
||
async def run_once(self) -> Dict[str, Any]:
|
||
"""
|
||
执行一次选股
|
||
|
||
Returns:
|
||
选股结果
|
||
"""
|
||
try:
|
||
logger.info("\n" + "=" * 60)
|
||
logger.info("📊 开始执行短期题材选股")
|
||
logger.info("=" * 60)
|
||
|
||
# 执行选股
|
||
result = self.selector.select_stocks(max_stocks=10)
|
||
|
||
# 输出日志
|
||
self._log_result(result)
|
||
|
||
# 发送通知
|
||
await self._send_notifications(result)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"选股执行失败: {e}")
|
||
import traceback
|
||
logger.error(traceback.format_exc())
|
||
return {}
|
||
|
||
def _log_result(self, result: Dict[str, Any]):
|
||
"""输出选股结果到日志"""
|
||
if not result or result.get('total_stocks', 0) == 0:
|
||
logger.info("\n📊 今日未选出符合条件的股票")
|
||
return
|
||
|
||
logger.info(f"\n📊 选股完成,共选出 {result['total_stocks']} 只股票")
|
||
|
||
if result.get('summary'):
|
||
summary = result['summary']
|
||
logger.info(f" - 总仓位: {summary.get('position_percent', 0):.1f}%")
|
||
logger.info(f" - 涉及板块: {summary.get('sector_count', 0)} 个")
|
||
|
||
for stock in result.get('stocks', []):
|
||
logger.info(f" - {stock['name']}({stock['ts_code']}): {stock['close']:.2f}元, "
|
||
f"仓位:{stock['position']*100:.1f}%, 评分:{stock['score']:.1f}分")
|
||
|
||
async def _send_notifications(self, result: Dict[str, Any]):
|
||
"""发送选股通知"""
|
||
try:
|
||
# 格式化输出文本
|
||
text = self.selector.format_output_text(result)
|
||
|
||
# 发送到钉钉
|
||
if self.settings.dingtalk_enabled:
|
||
await self.dingtalk.send_markdown(
|
||
"📊 短期题材选股结果",
|
||
text
|
||
)
|
||
logger.info("✅ 钉钉通知已发送")
|
||
|
||
# 发送到Telegram
|
||
if self.settings.telegram_enabled:
|
||
await self.telegram.send_message(text)
|
||
logger.info("✅ Telegram通知已发送")
|
||
|
||
except Exception as e:
|
||
logger.error(f"发送通知失败: {e}")
|
||
|
||
async def run_daily(self, run_time: str = "15:30"):
|
||
"""
|
||
每日定时运行
|
||
|
||
Args:
|
||
run_time: 运行时间(HH:MM格式,24小时制)
|
||
"""
|
||
self.running = True
|
||
|
||
logger.info("\n" + "=" * 60)
|
||
logger.info("🚀 A股智能体已启动")
|
||
logger.info(f"⏰ 运行时间: 每天 {run_time}(盘后)")
|
||
logger.info("=" * 60)
|
||
|
||
# 解析运行时间
|
||
hour, minute = map(int, run_time.split(':'))
|
||
|
||
while self.running:
|
||
try:
|
||
# 计算下次运行时间
|
||
now = datetime.now()
|
||
next_run = now.replace(
|
||
hour=hour,
|
||
minute=minute,
|
||
second=0,
|
||
microsecond=0
|
||
)
|
||
|
||
# 如果今天的运行时间已过,设置为明天
|
||
if now >= next_run:
|
||
from datetime import timedelta
|
||
next_run = next_run + timedelta(days=1)
|
||
|
||
wait_seconds = (next_run - now).total_seconds()
|
||
|
||
logger.info(f"⏳ 等待下次运行: {next_run.strftime('%Y-%m-%d %H:%M:%S')} "
|
||
f"(等待 {wait_seconds/3600:.1f} 小时)")
|
||
|
||
# 等待到运行时间
|
||
await asyncio.sleep(wait_seconds)
|
||
|
||
# 执行选股
|
||
await self.run_once()
|
||
|
||
except Exception as e:
|
||
logger.error(f"定时运行出错: {e}")
|
||
import traceback
|
||
logger.error(traceback.format_exc())
|
||
# 等待1小时后重试
|
||
await asyncio.sleep(3600)
|
||
|
||
def stop(self):
|
||
"""停止运行"""
|
||
self.running = False
|
||
logger.info("A股智能体已停止")
|
||
|
||
|
||
# 全局单例
|
||
_astock_agent: Optional[AStockAgent] = None
|
||
|
||
|
||
def get_astock_agent() -> AStockAgent:
|
||
"""获取A股智能体单例"""
|
||
global _astock_agent
|
||
if _astock_agent is None:
|
||
_astock_agent = AStockAgent()
|
||
return _astock_agent
|
||
|
||
|
||
async def main():
|
||
"""测试入口"""
|
||
agent = get_astock_agent()
|
||
|
||
# 执行一次选股
|
||
result = await agent.run_once()
|
||
|
||
# 输出结果
|
||
print("\n" + "=" * 60)
|
||
print(agent.selector.format_output_text(result))
|
||
print("=" * 60)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|