app.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
网页端运行展示功能模块(插件化架构)
使用 Flask 提供网页界面,支持选股、回测、参数优化等功能
"""

import os
import logging
from pathlib import Path
from datetime import datetime, timedelta
from dotenv import load_dotenv
from flask import Flask, render_template, request, jsonify, send_file
import sys

# 加载环境变量
load_dotenv()

# 添加项目根目录到Python路径
sys.path.append(str(Path(__file__).parent.parent))

# 配置日志
logger = logging.getLogger(__name__)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)

# 创建Flask应用
app = Flask(__name__, template_folder='templates', static_folder='static')

class WebServer:
    """网页服务类"""
    
    def __init__(self, host='0.0.0.0', port=5000):
        """初始化网页服务器"""
        self.host = host
        self.port = port
        
        # 初始化选股器(核心调度器)
        from src.stock_selector import StockSelector
        self.stock_selector = StockSelector()
        
        # 初始化策略管理器
        from src.strategy_manager import strategy_manager
        self.strategy_manager = strategy_manager
        
        # 初始化回测模块
        from src.backtester import Backtester
        self.backtester = Backtester()
        
        # 初始化参数优化模块
        from src.parameter_optimizer import ParameterOptimizer
        self.optimizer = ParameterOptimizer()
        
        # 初始化数据管理模块
        from src.data_manager import DataManager
        self.data_manager = DataManager()
        
        # 初始化配置管理
        from src.config_manager import config_manager
        self.config_manager = config_manager
        
        # 配置路由
        self._configure_routes()
    
    def _configure_routes(self):
        """配置路由"""
        @app.route('/')
        def index():
            """首页"""
            return render_template('index.html')
        
        @app.route('/api/select', methods=['POST'])
        def api_select():
            """API接口:选股"""
            try:
                strategy_names = request.json.get('strategies', ['vcp', 'model'])
                trade_date = request.json.get('trade_date')
                
                logger.info(f"选股请求:策略={','.join(strategy_names)}")
                
                # 使用选股器核心调度器进行选股
                result = self.stock_selector.run(strategy_names, trade_date)
                
                if not result.empty:
                    # 保存结果
                    strategy_name = "-".join(strategy_names)
                    self.stock_selector.save_results(result, trade_date, strategy_name)
                    
                    # 转换为JSON
                    result_json = result.to_dict(orient='records')
                    
                    return jsonify({
                        "success": True,
                        "count": len(result_json),
                        "results": result_json
                    })
                else:
                    return jsonify({
                        "success": False,
                        "error": "未选出符合条件的股票"
                    }), 404
                
            except Exception as e:
                logger.error(f"选股请求失败:{e}")
                return jsonify({
                    "success": False,
                    "error": str(e)
                }), 500
        
        @app.route('/api/backtest', methods=['POST'])
        def api_backtest():
            """API接口:回测"""
            try:
                strategy_names = request.json.get('strategies', ['vcp', 'model'])
                start_date = request.json.get('start_date')
                end_date = request.json.get('end_date')
                
                logger.info(f"回测请求:策略={','.join(strategy_names)}")
                
                if start_date and end_date:
                    report = self.backtester.run(start_date=start_date, end_date=end_date)
                else:
                    report = self.backtester.run()
                
                # 保存回测报告
                self.backtester.save_report(report)
                
                return jsonify({
                    "success": True,
                    "report": report
                })
                
            except Exception as e:
                logger.error(f"回测请求失败:{e}")
                return jsonify({
                    "success": False,
                    "error": str(e)
                }), 500
        
        @app.route('/api/optimize', methods=['POST'])
        def api_optimize():
            """API接口:参数优化"""
            try:
                strategy_name = request.json.get('strategy', 'model')
                
                logger.info(f"参数优化请求:策略={strategy_name}")
                
                best_params = self.optimizer.run()
                self.optimizer.save_best_parameters(best_params)
                
                return jsonify({
                    "success": True,
                    "parameters": best_params
                })
                
            except Exception as e:
                logger.error(f"参数优化请求失败:{e}")
                return jsonify({
                    "success": False,
                    "error": str(e)
                }), 500
        
        @app.route('/api/update_data', methods=['POST'])
        def api_update_data():
            """API接口:更新数据"""
            try:
                logger.info("数据更新请求")
                
                # 更新股票基本信息
                self.data_manager.update_stock_basic()
                
                # 更新历史数据
                self.data_manager.update_history_data()
                
                return jsonify({
                    "success": True,
                    "message": "数据更新完成"
                })
                
            except Exception as e:
                logger.error(f"数据更新请求失败:{e}")
                return jsonify({
                    "success": False,
                    "error": str(e)
                }), 500
        
        @app.route('/api/results', methods=['GET'])
        def api_results():
            """API接口:获取选股结果"""
            try:
                strategy_names = request.args.get('strategies', 'vcp,model').split(",")
                trade_date = request.args.get('trade_date')
                
                logger.info(f"获取选股结果:策略={','.join(strategy_names)}")
                
                strategy_name = "-".join(strategy_names)
                results = self.stock_selector.get_results(trade_date, strategy_name)
                
                return jsonify({
                    "success": True,
                    "count": len(results),
                    "results": results.to_dict(orient='records')
                })
                
            except Exception as e:
                logger.error(f"获取选股结果失败:{e}")
                return jsonify({
                    "success": False,
                    "error": str(e)
                }), 500
        
        @app.route('/api/config', methods=['GET'])
        def api_config():
            """API接口:获取配置信息"""
            try:
                # 获取策略列表
                strategies = self.strategy_manager.list_strategies()
                
                config = {
                    "strategies": [{"name": s['name'], "description": s['description']} for s in strategies],
                    "parameters": {
                        "stock_count": self.config_manager.get('selection.count'),
                        "min_turnover_rate": self.config_manager.get('selection.min_turnover_rate'),
                        "backtest_start_date": self.config_manager.get('system.backtest_start_date'),
                        "backtest_end_date": self.config_manager.get('system.backtest_end_date')
                    }
                }
                
                # 添加策略参数配置
                for strategy in strategies:
                    config[f"{strategy['name']}_strategy"] = strategy['params']
                
                return jsonify({
                    "success": True,
                    "config": config
                })
                
            except Exception as e:
                logger.error(f"获取配置信息失败:{e}")
                return jsonify({
                    "success": False,
                    "error": str(e)
                }), 500
        
        @app.route('/api/download/<file_type>/<date>', methods=['GET'])
        def api_download(file_type, date):
            """API接口:下载文件"""
            try:
                strategy_names = request.args.get('strategies', 'vcp,model').split(",")
                strategy_name = "-".join(strategy_names)
                
                if file_type == 'csv' or file_type == 'json':
                    file_path = f"results/selections/{date}_{strategy_name}_selected_stocks.{file_type}"
                elif file_type == 'report':
                    file_path = f"results/backtests/{date}_backtest_report.csv"
                else:
                    return jsonify({"success": False, "error": "无效文件类型"}), 400
                
                if not Path(file_path).exists():
                    return jsonify({"success": False, "error": "文件不存在"}), 404
                
                return send_file(file_path, as_attachment=True)
                
            except Exception as e:
                logger.error(f"文件下载失败:{e}")
                return jsonify({
                    "success": False,
                    "error": str(e)
                }), 500
    
    def run(self, debug=True):
        """运行Flask应用"""
        logger.info(f"启动网页服务,地址:http://{self.host}:{self.port}")
        app.run(host=self.host, port=self.port, debug=debug, use_reloader=False)

if __name__ == "__main__":
    # 测试网页服务
    server = WebServer()
    server.run(debug=True)