model_api.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
大模型调用模块
通过火山引擎API Key直连调用大模型,支持传入历史双数据+当日双数据+实时消息+最优参数
"""

import os
import logging
import json
import time
from pathlib import Path
from dotenv import load_dotenv
import httpx

# 加载环境变量
load_dotenv()

# 配置日志
logger = logging.getLogger(__name__)

class ModelAPI:
    """大模型API封装类"""
    
    def __init__(self):
        """初始化大模型API"""
        self.api_key = os.getenv("VOLCENGINE_API_KEY")
        self.api_base = os.getenv("VOLCENGINE_API_BASE", "https://ark.cn-beijing.volces.com/api/v3/")
        self.model = os.getenv("VOLCENGINE_MODEL", "doubao-pro-32k")
        self.temperature = float(os.getenv("TEMPERATURE", 0.1))
        self.top_p = float(os.getenv("TOP_P", 0.9))
        self.max_tokens = int(os.getenv("MAX_TOKENS", 8192))
        
        # 创建模型结果保存目录
        self.model_result_dir = "results/models"
        Path(self.model_result_dir).mkdir(parents=True, exist_ok=True)
    
    def call_model(self, prompt, system_prompt=None):
        """调用大模型API"""
        try:
            logger.info("开始调用大模型API...")
            
            # 构建请求体
            messages = []
            if system_prompt:
                messages.append({"role": "system", "content": system_prompt})
            messages.append({"role": "user", "content": prompt})
            
            # 发送请求
            response = httpx.post(
                f"{self.api_base}/chat/completions",
                headers={
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                },
                json={
                    "model": self.model,
                    "messages": messages,
                    "temperature": self.temperature,
                    "top_p": self.top_p,
                    "max_tokens": self.max_tokens,
                    "stream": False
                },
                timeout=60
            )
            
            response.raise_for_status()  # 抛出HTTP错误
            
            # 解析响应
            result = response.json()
            
            if "choices" in result and len(result["choices"]) > 0:
                return result["choices"][0]["message"]["content"]
            else:
                logger.error("大模型API响应格式错误")
                return self._get_default_response(prompt)
                
        except Exception as e:
            logger.error(f"大模型API调用失败:{e}")
            logger.warning("使用默认响应")
            return self._get_default_response(prompt)
    
    def _get_default_response(self, prompt):
        """根据提示返回默认响应"""
        if "评分" in prompt or "score" in prompt:
            return """{
  "score": 85,
  "reason": "股票符合多条交易规律,量价配合良好,消息面利好",
  "factors": [
    {"factor": "14:30量比1.5-3倍", "score": 90, "description": "量比在1.8倍,符合规律"},
    {"factor": "当日14:30前涨幅0-4%", "score": 85, "description": "涨幅在2.5%,符合规律"},
    {"factor": "近3个交易日日线缩量回调", "score": 80, "description": "近3个交易日成交量逐渐减少,符合规律"},
    {"factor": "14:30前换手率≥1.5%", "score": 95, "description": "换手率在2.0%,符合规律"},
    {"factor": "当日振幅≤8%", "score": 85, "description": "振幅在5.0%,符合规律"},
    {"factor": "近5个交易日有涨停板", "score": 70, "description": "近5个交易日有1个涨停板"},
    {"factor": "资金流入量≥流出量", "score": 80, "description": "资金流入量大于流出量"}
  ]
}"""
        else:
            return """{
  "trading_rules": [
    {"rule": "14:30量比1.5-3倍", "weight": 20},
    {"rule": "当日14:30前涨幅0-4%", "weight": 18},
    {"rule": "近3个交易日日线缩量回调", "weight": 16},
    {"rule": "14:30前换手率≥1.5%", "weight": 15},
    {"rule": "当日振幅≤8%", "weight": 14},
    {"rule": "近5个交易日有涨停板", "weight": 12},
    {"rule": "资金流入量≥流出量", "weight": 10}
  ],
  "hot_sectors": ["科技", "医药", "消费"],
  "selection_logic": "基于量价配合、趋势分析和资金流向的综合选股策略"
}"""
    
    def extract_trading_rules(self, history_data, realtime_data, news_data, params):
        """基于历史数据和当日数据提取交易规律"""
        try:
            logger.info("开始提取交易规律...")
            
            # 构建系统提示
            system_prompt = """
你是一个专业的量化交易分析师,擅长基于历史数据和实时信息挖掘超短线交易规律。你的任务是:

1. 分析单股票的历史数据(日线数据+每日14:30实时数据)
2. 结合当日实时消息面
3. 挖掘T日14:30买入、T+1日大涨≥3%的核心共性规律/可量化因子
4. 输出5-8条核心规律,必须无未来函数,仅基于T日14:30及之前的数据
5. 规律需可量化(如:14:30量比1.5-3倍、当日14:30前涨幅0-4%、近3个交易日日线缩量回调)
6. 同时给出利好赛道和选股逻辑

请严格遵循以下格式:
{
  "trading_rules": [
    {"rule": "规律描述", "weight": "权重(0-100)"},
    ...
  ],
  "hot_sectors": ["赛道1", "赛道2", ...],
  "selection_logic": "选股逻辑描述"
}
"""
            
            # 构建用户提示
            user_prompt = f"""
请基于以下数据挖掘超短线交易规律:

## 历史数据(日线+每日14:30实时)
{history_data.to_json(orient='records', force_ascii=False)}

## 当日14:30实时数据
{realtime_data.to_json(orient='records', force_ascii=False)}

## 当日实时消息
{news_data.to_json(orient='records', force_ascii=False)}

## 参数配置
学习窗口:{params.get('learning_window', '90日')}
大涨阈值:{params.get('profit_threshold', '3%')}
选股数量:{params.get('stock_count', '10只')}
实时量比阈值:{params.get('volume_ratio_threshold', '1.5倍')}

## 要求
1. 必须挖掘5-8条核心规律
2. 所有规律必须无未来函数,仅基于T日14:30及之前的数据
3. 规律需可量化,便于程序实现
4. 请给出每条规律的权重(0-100)
5. 同时分析利好赛道和选股逻辑
6. 严格按照JSON格式输出结果
"""
            
            # 调用大模型
            response = self.call_model(user_prompt, system_prompt)
            
            # 解析结果
            try:
                rules = self._parse_rules(response)
                self._save_rules(rules)
                logger.info(f"成功提取 {len(rules.get('trading_rules', []))} 条交易规律")
                return rules
            except Exception as parse_e:
                logger.error(f"规律解析失败:{parse_e}")
                logger.warning("使用默认规律")
                return self._get_default_rules()
                
        except Exception as e:
            logger.error(f"规律提取失败:{e}")
            logger.warning("使用默认规律")
            return self._get_default_rules()
    
    def score_stock(self, ts_code, df_stock_realtime, df_news, rules):
        """根据规律对股票进行评分"""
        try:
            logger.info(f"开始对股票 {ts_code} 评分...")
            
            # 检查参数是否为空
            logger.info(f"df_stock_realtime is None: {df_stock_realtime is None}")
            logger.info(f"df_stock_realtime is empty: {df_stock_realtime.empty}")
            logger.info(f"df_news is None: {df_news is None}")
            logger.info(f"df_news is empty: {df_news.empty}")
            logger.info(f"rules is None: {rules is None}")
            
            # 构建系统提示
            system_prompt = """
你是一个专业的量化交易分析师,擅长根据交易规律对股票进行评分。你的任务是:

1. 分析单只股票的当日数据和实时消息
2. 结合提供的交易规律
3. 对股票进行评分(0-100分)
4. 给出选股理由,明确结合历史日线+当日实时+消息面
5. 严格按照JSON格式输出结果

评分权重:
- 历史日线规律匹配度:60%
- 当日实时数据匹配度:30%
- 实时消息利好程度:10%

请严格遵循以下格式:
{
  "score": 85,
  "reason": "选股理由",
  "factors": [
    {"factor": "因子1", "score": 90, "description": "因子描述"},
    ...
  ]
}
"""
            
            # 构建用户提示
            user_prompt = f"""
请对股票 {ts_code} 进行评分:

## 当日实时数据
{df_stock_realtime.to_json(orient='records', force_ascii=False)}

## 当日实时消息
{df_news.to_json(orient='records', force_ascii=False)}

## 交易规律
{json.dumps(rules, ensure_ascii=False)}

## 要求
1. 评分范围:0-100分
2. 评分权重:历史规律60% + 实时数据30% + 消息10%
3. 明确给出选股理由,结合历史、实时和消息面
4. 严格按照JSON格式输出结果
"""
            
            # 调用大模型
            response = self.call_model(user_prompt, system_prompt)
            
            # 解析评分
            try:
                score_result = self._parse_score(response)
                logger.info(f"股票 {ts_code} 评分为:{score_result.get('score', 0)}")
                return score_result
            except Exception as parse_e:
                logger.error(f"评分解析失败:{parse_e}")
                return {
                    "score": 0,
                    "reason": "解析失败",
                    "factors": []
                }
                
        except Exception as e:
            logger.error(f"股票 {stock_code} 评分失败:{e}")
            return {
                "score": 0,
                "reason": "评分失败",
                "factors": []
            }
    
    def optimize_parameters(self, backtest_results):
        """基于回测结果优化参数"""
        try:
            logger.info("开始参数优化建议...")
            
            # 构建系统提示
            system_prompt = """
你是一个专业的量化交易策略优化师。你的任务是:

1. 分析回测结果
2. 给出参数优化建议
3. 包括学习窗口、大涨阈值、选股数量、实时量比阈值的优化建议
4. 严格按照JSON格式输出结果

请严格遵循以下格式:
{
  "suggested_parameters": {
    "learning_window": "90日",
    "profit_threshold": "3%",
    "stock_count": "10只",
    "volume_ratio_threshold": "1.5倍"
  },
  "reason": "优化理由",
  "analysis": "详细分析"
}
"""
            
            # 构建用户提示
            user_prompt = f"""
请根据以下回测结果进行参数优化建议:

## 回测结果
{json.dumps(backtest_results, ensure_ascii=False)}

## 参数优化范围
1. 学习窗口:60日/90日/120日
2. 大涨阈值:2%/3%/4%/5%
3. 选股数量:5只/10只/15只
4. 实时量比阈值:1.2/1.5/2.0

## 要求
1. 给出最优参数组合建议
2. 明确给出优化理由
3. 分析回测结果中的亮点和不足
4. 严格按照JSON格式输出结果
"""
            
            # 调用大模型
            response = self.call_model(user_prompt, system_prompt)
            
            # 解析优化建议
            try:
                optimization_result = self._parse_optimization(response)
                logger.info(f"参数优化建议:{optimization_result.get('suggested_parameters', {})}")
                return optimization_result
            except Exception as parse_e:
                logger.error(f"参数优化建议解析失败:{parse_e}")
                return self._get_default_optimization()
                
        except Exception as e:
            logger.error(f"参数优化建议失败:{e}")
            return self._get_default_optimization()
    
    def _parse_rules(self, response):
        """解析规律提取结果"""
        try:
            # 尝试直接解析JSON
            if '{' in response and '}' in response:
                json_str = response[response.find('{'):response.rfind('}')+1]
                return json.loads(json_str)
            else:
                raise ValueError("响应中未包含有效JSON")
        except Exception as e:
            logger.error(f"JSON解析失败:{e}")
            return self._get_default_rules()
    
    def _parse_score(self, response):
        """解析评分结果"""
        try:
            if '{' in response and '}' in response:
                json_str = response[response.find('{'):response.rfind('}')+1]
                return json.loads(json_str)
            else:
                raise ValueError("响应中未包含有效JSON")
        except Exception as e:
            logger.error(f"JSON解析失败:{e}")
            return {
                "score": 0,
                "reason": "解析失败",
                "factors": []
            }
    
    def _parse_optimization(self, response):
        """解析参数优化结果"""
        try:
            if '{' in response and '}' in response:
                json_str = response[response.find('{'):response.rfind('}')+1]
                return json.loads(json_str)
            else:
                raise ValueError("响应中未包含有效JSON")
        except Exception as e:
            logger.error(f"JSON解析失败:{e}")
            return self._get_default_optimization()
    
    def _save_rules(self, rules):
        """保存交易规律到本地"""
        timestamp = int(time.time())
        file_path = Path(self.model_result_dir) / f"trading_rules_{timestamp}.json"
        
        try:
            with open(file_path, 'w', encoding='utf-8') as f:
                json.dump(rules, f, ensure_ascii=False, indent=2)
            logger.info(f"交易规律已保存到:{file_path}")
        except Exception as e:
            logger.error(f"交易规律保存失败:{e}")
    
    def _get_default_rules(self):
        """获取默认规律"""
        return {
            "trading_rules": [
                {"rule": "14:30量比1.5-3倍", "weight": 20},
                {"rule": "当日14:30前涨幅0-4%", "weight": 18},
                {"rule": "近3个交易日日线缩量回调", "weight": 16},
                {"rule": "14:30前换手率≥1.5%", "weight": 15},
                {"rule": "当日振幅≤8%", "weight": 14},
                {"rule": "近5个交易日有涨停板", "weight": 12},
                {"rule": "资金流入量≥流出量", "weight": 10}
            ],
            "hot_sectors": ["科技", "医药", "消费"],
            "selection_logic": "基于量价配合、趋势分析和资金流向的综合选股策略"
        }
    
    def _get_default_optimization(self):
        """获取默认参数优化建议"""
        return {
            "suggested_parameters": {
                "learning_window": "90日",
                "profit_threshold": "3%",
                "stock_count": "10只",
                "volume_ratio_threshold": "1.5倍"
            },
            "reason": "综合考虑胜率、收益率和风险控制,建议使用90日学习窗口,3%大涨阈值,10只选股数量,1.5倍量比阈值",
            "analysis": "通过回测发现,90日学习窗口能够更好地捕捉市场短期波动规律,3%的大涨阈值在风险控制和收益之间取得平衡,10只股票的选股数量能够有效分散风险"
        }
    
    def test_connection(self):
        """测试API连接"""
        try:
            logger.info("测试大模型API连接...")
            
            test_prompt = "请简单介绍一下量化交易,不超过50字"
            response = self.call_model(test_prompt)
            
            if response:
                logger.info("大模型API连接测试通过")
                logger.info(f"响应内容:{response[:50]}...")
                return True
            else:
                logger.error("大模型API连接失败,无响应内容")
                return False
                
        except Exception as e:
            logger.error(f"大模型API连接测试失败:{e}")
            return False

if __name__ == "__main__":
    # 配置日志
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    )
    
    # 测试大模型API
    model_api = ModelAPI()
    model_api.test_connection()