daily_adjuster.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
每日自调整模块
每日收盘后自动触发,完成当日数据回测、参数微调、规律有效性验证
"""

import os
import logging
import pandas as pd
import json
from pathlib import Path
from datetime import datetime, timedelta
from dotenv import load_dotenv

# 加载环境变量
load_dotenv()

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

class DailyAdjuster:
    """每日自调整类"""
    
    def __init__(self):
        """初始化每日自调整对象"""
        self.adjustment_dir = "results/adjustments"
        Path(self.adjustment_dir).mkdir(parents=True, exist_ok=True)
        
        # 初始化数据管理模块
        from src.data_manager import DataManager
        self.data_manager = DataManager()
        
        # 初始化回测模块
        from src.backtester import Backtester
        self.backtester = Backtester()
        
        # 初始化参数优化模块
        from src.parameter_optimizer import ParameterOptimizer
        self.optimizer = ParameterOptimizer()
        
        # 初始化大模型模块
        from src.model_api import ModelAPI
        self.model_api = ModelAPI()
        
        # 模型有效性阈值
        self.accuracy_threshold = float(os.getenv("ACCURACY_THRESHOLD", 0.5))
        self.profit_loss_threshold = float(os.getenv("PROFIT_LOSS_THRESHOLD", 2.0))
    
    def run(self, trade_date=None):
        """执行每日自调整"""
        try:
            logger.info("开始每日自调整...")
            
            if not trade_date:
                trade_date = datetime.now().strftime('%Y%m%d')
            
            # 检查是否为交易日
            if not self._is_trading_day(trade_date):
                logger.warning(f"{trade_date} 不是交易日,每日自调整终止")
                return False
            
            # 完成当日数据下载
            logger.info("开始下载当日数据...")
            self.data_manager.update_history_data(trade_date, trade_date)
            logger.info("当日数据下载完成")
            
            # 完成当日数据回测
            logger.info("开始当日数据回测...")
            backtest_result = self._run_daily_backtest(trade_date)
            logger.info("当日数据回测完成")
            
            # 验证规律有效性
            logger.info("开始验证规律有效性...")
            is_valid = self._validate_trading_rules(backtest_result)
            
            if is_valid:
                logger.info("当前交易规律有效性良好,无需调整")
                return True
            else:
                logger.warning("当前交易规律有效性下降,需要重新优化")
                
                # 重新优化参数
                logger.info("开始重新优化参数...")
                best_params = self.optimizer.run()
                self.optimizer.save_best_parameters(best_params)
                
                # 重新挖掘规律
                logger.info("开始重新挖掘交易规律...")
                self._reextract_trading_rules(best_params)
                
                logger.info("每日自调整完成,已优化参数和规律")
                return True
                
        except Exception as e:
            logger.error(f"每日自调整失败:{e}")
            return False
    
    def _is_trading_day(self, date_str):
        """检查是否为交易日"""
        try:
            from chinese_calendar import is_workday
            date = datetime.strptime(date_str, '%Y%m%d').date()
            return is_workday(date)
        except Exception as e:
            logger.error(f"交易日检查失败:{e}")
            return False
    
    def _run_daily_backtest(self, trade_date):
        """执行当日数据回测"""
        try:
            logger.info(f"执行 {trade_date} 当日数据回测...")
            
            # 使用最优参数进行回测
            best_params = self.optimizer.load_best_parameters()
            
            # 回测日期范围:包含当日和前几日的数据
            start_date = (datetime.strptime(trade_date, '%Y%m%d') - timedelta(days=7)).strftime('%Y%m%d')
            end_date = trade_date
            
            report = self.backtester.run(best_params, start_date, end_date)
            
            # 保存当日回测报告
            self.backtester.save_report(report, f"{trade_date}_daily")
            
            logger.info(f"当日回测完成,胜率:{report['win_rate']:.2%},盈亏比:{report['profit_loss_ratio']:.2f}")
            return report
            
        except Exception as e:
            logger.error(f"当日回测失败:{e}")
            return self.backtester._get_empty_report()
    
    def _validate_trading_rules(self, backtest_result):
        """验证交易规律有效性"""
        logger.info("验证交易规律有效性...")
        
        # 检查核心指标是否达标
        win_rate_valid = backtest_result['win_rate'] >= self.accuracy_threshold
        profit_loss_valid = backtest_result['profit_loss_ratio'] >= self.profit_loss_threshold
        
        logger.info(f"胜率验证:{win_rate_valid} ({backtest_result['win_rate']:.2%} ≥ {self.accuracy_threshold:.2%})")
        logger.info(f"盈亏比验证:{profit_loss_valid} ({backtest_result['profit_loss_ratio']:.2f} ≥ {self.profit_loss_threshold:.2f})")
        
        return win_rate_valid and profit_loss_valid
    
    def _reextract_trading_rules(self, best_params):
        """重新挖掘交易规律"""
        try:
            logger.info("开始重新挖掘交易规律...")
            
            # 获取历史数据
            history_data = self._get_history_data_for_rule_reextraction(best_params)
            
            # 获取当日实时数据和消息
            today = datetime.now().strftime('%Y%m%d')
            realtime_data = self.data_manager.get_real_time_data(today)
            news_data = self.data_manager.get_news_data(today)
            
            # 重新提取规律
            rules = self.model_api.extract_trading_rules(
                history_data,
                realtime_data,
                news_data,
                best_params
            )
            
            logger.info(f"重新提取交易规律完成,共 {len(rules.get('trading_rules', []))} 条")
            return rules
            
        except Exception as e:
            logger.error(f"交易规律重新挖掘失败:{e}")
            return None
    
    def _get_history_data_for_rule_reextraction(self, best_params):
        """获取规律重挖掘所需的历史数据"""
        try:
            # 根据学习窗口确定历史数据范围
            window_days = 90
            window_str = best_params.get('learning_window', '90日')
            if '60' in window_str:
                window_days = 60
            elif '120' in window_str:
                window_days = 120
            
            end_date = datetime.now()
            start_date = end_date - timedelta(days=window_days)
            
            logger.info(f"规律重挖掘历史数据范围:{start_date.strftime('%Y%m%d')} 到 {end_date.strftime('%Y%m%d')}")
            
            # 获取所有有效股票的历史双数据
            history_data = self.data_manager.get_all_stock_data_for_backtest(
                start_date.strftime('%Y%m%d'),
                end_date.strftime('%Y%m%d')
            )
            
            # 合并所有股票数据
            all_history = []
            for ts_code, df_stock in history_data.items():
                df_stock['ts_code'] = ts_code
                all_history.append(df_stock)
            
            if all_history:
                return pd.concat(all_history, ignore_index=True)
            else:
                logger.warning("未获取到规律重挖掘所需的历史数据")
                return pd.DataFrame()
                
        except Exception as e:
            logger.error(f"历史数据获取失败:{e}")
            return pd.DataFrame()
    
    def save_adjustment_report(self, adjustment_result, trade_date=None):
        """保存每日自调整报告"""
        try:
            if not trade_date:
                trade_date = datetime.now().strftime('%Y%m%d')
            
            report_path = Path(self.adjustment_dir) / f"{trade_date}_adjustment_report.json"
            
            with open(report_path, 'w', encoding='utf-8') as f:
                json.dump(adjustment_result, f, ensure_ascii=False, indent=2)
            
            logger.info(f"每日自调整报告已保存到:{report_path}")
            return True
            
        except Exception as e:
            logger.error(f"每日自调整报告保存失败:{e}")
            return False
    
    def load_adjustment_report(self, trade_date=None):
        """加载每日自调整报告"""
        try:
            if not trade_date:
                trade_date = datetime.now().strftime('%Y%m%d')
            
            report_path = Path(self.adjustment_dir) / f"{trade_date}_adjustment_report.json"
            
            if report_path.exists():
                with open(report_path, 'r', encoding='utf-8') as f:
                    return json.load(f)
            else:
                logger.warning(f"每日自调整报告不存在:{report_path}")
                return None
                
        except Exception as e:
            logger.error(f"每日自调整报告加载失败:{e}")
            return None
    
    def test_adjustment(self):
        """测试每日自调整功能"""
        logger.info("开始测试每日自调整功能...")
        
        # 测试交易日检查
        test_date = datetime.now().strftime('%Y%m%d')
        is_trading = self._is_trading_day(test_date)
        logger.info(f"{test_date} 是交易日:{is_trading}")
        
        # 测试当日数据下载
        logger.info("测试当日数据下载...")
        try:
            self.data_manager.update_history_data(test_date, test_date)
            logger.info("当日数据下载成功")
        except Exception as e:
            logger.warning(f"当日数据下载测试失败:{e}")
        
        # 测试当日回测
        logger.info("测试当日回测...")
        try:
            backtest_result = self._run_daily_backtest(test_date)
            logger.info(f"当日回测结果:胜率 {backtest_result['win_rate']:.2%},盈亏比 {backtest_result['profit_loss_ratio']:.2f}")
        except Exception as e:
            logger.warning(f"当日回测测试失败:{e}")
        
        # 测试规律有效性验证
        logger.info("测试规律有效性验证...")
        try:
            is_valid = self._validate_trading_rules({
                'win_rate': 0.65,
                'profit_loss_ratio': 2.3
            })
            logger.info(f"规律有效性验证结果:{is_valid}")
        except Exception as e:
            logger.warning(f"规律有效性验证测试失败:{e}")
        
        logger.info("每日自调整功能测试完成")

if __name__ == "__main__":
    # 配置日志
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    )
    
    # 测试每日自调整模块
    adjuster = DailyAdjuster()
    adjuster.test_adjustment()
    # adjuster.run()