utils.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
工具函数模块
提供通用的工具函数,包括日期处理、文件操作、数据验证等
"""

import os
import logging
import re
import json
from pathlib import Path
from datetime import datetime, timedelta
from typing import Any, List, Dict, Tuple, Optional

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

def ensure_dir_exists(dir_path: str) -> bool:
    """确保目录存在"""
    try:
        Path(dir_path).mkdir(parents=True, exist_ok=True)
        return True
    except Exception as e:
        logger.error(f"创建目录失败:{dir_path} - {e}")
        return False

def load_json_file(file_path: str, default: Any = None) -> Any:
    """加载JSON文件"""
    try:
        if Path(file_path).exists():
            with open(file_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        else:
            logger.warning(f"文件不存在:{file_path}")
            return default
    except Exception as e:
        logger.error(f"加载JSON文件失败:{file_path} - {e}")
        return default

def save_json_file(file_path: str, data: Any) -> bool:
    """保存JSON文件"""
    try:
        ensure_dir_exists(str(Path(file_path).parent))
        
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        
        return True
    except Exception as e:
        logger.error(f"保存JSON文件失败:{file_path} - {e}")
        return False

def load_text_file(file_path: str) -> str:
    """加载文本文件"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return f.read()
    except Exception as e:
        logger.error(f"加载文本文件失败:{file_path} - {e}")
        return ""

def save_text_file(file_path: str, content: str) -> bool:
    """保存文本文件"""
    try:
        ensure_dir_exists(str(Path(file_path).parent))
        
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(content)
        
        return True
    except Exception as e:
        logger.error(f"保存文本文件失败:{file_path} - {e}")
        return False

def format_date(date_str: str, from_format: str = "%Y%m%d", to_format: str = "%Y-%m-%d") -> str:
    """格式化日期字符串"""
    try:
        date_obj = datetime.strptime(date_str, from_format)
        return date_obj.strftime(to_format)
    except Exception as e:
        logger.error(f"日期格式化失败:{date_str} - {e}")
        return date_str

def get_trade_date(date_obj: Optional[datetime] = None) -> str:
    """获取交易日日期字符串"""
    try:
        if date_obj is None:
            date_obj = datetime.now()
        
        return date_obj.strftime("%Y%m%d")
    except Exception as e:
        logger.error(f"获取交易日日期失败:{e}")
        return datetime.now().strftime("%Y%m%d")

def get_prev_trading_date(trade_date: str, num_days: int = 1) -> str:
    """获取前N个交易日的日期"""
    try:
        date_obj = datetime.strptime(trade_date, "%Y%m%d")
        prev_date = date_obj - timedelta(days=num_days)
        return prev_date.strftime("%Y%m%d")
    except Exception as e:
        logger.error(f"获取前交易日日期失败:{trade_date} - {e}")
        return trade_date

def validate_stock_code(stock_code: str) -> bool:
    """验证股票代码格式"""
    # 股票代码格式:6位数字 + 市场后缀(.SH/.SZ)
    pattern = r"^\d{6}\.(SH|SZ)$"
    return bool(re.match(pattern, stock_code))

def validate_date_string(date_str: str, format: str = "%Y%m%d") -> bool:
    """验证日期字符串格式"""
    try:
        datetime.strptime(date_str, format)
        return True
    except:
        return False

def calculate_volatility(prices: List[float]) -> float:
    """计算价格波动率(标准差)"""
    try:
        import numpy as np
        return np.std(prices)
    except Exception as e:
        logger.error(f"计算波动率失败:{e}")
        return 0.0

def calculate_moving_average(prices: List[float], period: int) -> List[float]:
    """计算移动平均"""
    try:
        import numpy as np
        return np.convolve(prices, np.ones(period)/period, mode='valid').tolist()
    except Exception as e:
        logger.error(f"计算移动平均失败:{e}")
        return []

def calculate_rsi(prices: List[float], period: int = 14) -> List[float]:
    """计算RSI指标"""
    try:
        import numpy as np
        
        deltas = np.diff(prices)
        gains = np.where(deltas > 0, deltas, 0)
        losses = np.where(deltas < 0, -deltas, 0)
        
        avg_gain = np.convolve(gains, np.ones(period)/period, mode='valid')
        avg_loss = np.convolve(losses, np.ones(period)/period, mode='valid')
        
        rs = avg_gain / avg_loss
        rsi = 100 - (100 / (1 + rs))
        
        return rsi.tolist()
    except Exception as e:
        logger.error(f"计算RSI失败:{e}")
        return []

def calculate_macd(prices: List[float], fast: int = 12, slow: int = 26, signal: int = 9) -> Tuple[List[float], List[float], List[float]]:
    """计算MACD指标"""
    try:
        import numpy as np
        
        ema_fast = calculate_exponential_moving_average(prices, fast)
        ema_slow = calculate_exponential_moving_average(prices, slow)
        
        macd = np.array(ema_fast) - np.array(ema_slow)
        signal_line = calculate_exponential_moving_average(macd, signal)
        histogram = macd - signal_line
        
        return macd.tolist(), signal_line.tolist(), histogram.tolist()
    except Exception as e:
        logger.error(f"计算MACD失败:{e}")
        return [], [], []

def calculate_exponential_moving_average(prices: List[float], period: int) -> List[float]:
    """计算指数移动平均"""
    try:
        import numpy as np
        
        alpha = 2 / (period + 1)
        ema = []
        
        for i, price in enumerate(prices):
            if i == 0:
                ema.append(price)
            else:
                ema.append(alpha * price + (1 - alpha) * ema[i-1])
        
        return ema
    except Exception as e:
        logger.error(f"计算指数移动平均失败:{e}")
        return []

def format_number(number: float, format_str: str = "{:.2f}") -> str:
    """格式化数字"""
    try:
        return format_str.format(number)
    except Exception as e:
        logger.error(f"数字格式化失败:{number} - {e}")
        return str(number)

def truncate_text(text: str, max_length: int, suffix: str = "...") -> str:
    """截断文本"""
    if len(text) <= max_length:
        return text
    return text[:max_length - len(suffix)] + suffix

def remove_duplicates(lst: List[Any]) -> List[Any]:
    """去除列表中的重复元素"""
    seen = []
    result = []
    
    for item in lst:
        if item not in seen:
            seen.append(item)
            result.append(item)
    
    return result

def get_file_size(file_path: str) -> int:
    """获取文件大小(字节)"""
    try:
        return os.path.getsize(file_path)
    except Exception as e:
        logger.error(f"获取文件大小失败:{file_path} - {e}")
        return 0

def get_file_modification_time(file_path: str) -> datetime:
    """获取文件修改时间"""
    try:
        return datetime.fromtimestamp(os.path.getmtime(file_path))
    except Exception as e:
        logger.error(f"获取文件修改时间失败:{file_path} - {e}")
        return datetime.now()

def split_list(lst: List[Any], n: int) -> List[List[Any]]:
    """将列表分成N个大小相等的子列表"""
    try:
        chunk_size = len(lst) // n
        remainder = len(lst) % n
        
        result = []
        start = 0
        
        for i in range(n):
            end = start + chunk_size + (1 if i < remainder else 0)
            result.append(lst[start:end])
            start = end
        
        return result
    except Exception as e:
        logger.error(f"列表分割失败:{e}")
        return [lst]

def merge_dicts(*dicts: Dict[Any, Any]) -> Dict[Any, Any]:
    """合并多个字典"""
    result = {}
    
    for d in dicts:
        result.update(d)
    
    return result

def safe_divide(a: float, b: float, default: float = 0.0) -> float:
    """安全除法"""
    try:
        if b == 0:
            return default
        return a / b
    except Exception as e:
        logger.error(f"除法运算失败:{a} / {b} - {e}")
        return default

def retry_on_exception(max_retries: int = 3, delay: int = 1):
    """异常重试装饰器"""
    import time
    from functools import wraps
    
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            
            for i in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    logger.warning(f"执行失败,第 {i+1} 次重试:{e}")
                    time.sleep(delay)
            
            logger.error(f"执行失败,已达到最大重试次数:{last_exception}")
            raise last_exception
        
        return wrapper
    return decorator