Files
etf/core/common/utils.py
aszerW 988c2335fb chore(config): 添加环境变量示例及.gitignore更新
- 新增 .env.example,包含 Tushare API、钉钉机器人和PostgreSQL数据库配置模板
- 更新.gitignore,忽略本地配置文件如 .env.local 和 config_local.py
- 添加对报表文件命名规则的支持,保留示例文件不忽略
- 删除废弃的 chart.py 及相关图表模块代码
- 新增 config/settings.py,实现从环境变量读取配置的统一接口
- 设置数据目录及缓存目录,确保目录存在,提高配置管理规范性
2026-03-18 23:33:40 +08:00

191 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
通用工具函数
"""
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import Optional
def format_date(date_str: str, output_format: str = "%Y-%m-%d") -> str:
"""
统一日期格式
Args:
date_str: 输入日期字符串(支持 YYYY-MM-DD 或 YYYYMMDD
output_format: 输出格式
Returns:
str: 格式化后的日期字符串
"""
# 尝试解析多种格式
for fmt in ["%Y-%m-%d", "%Y%m%d", "%Y/%m/%d"]:
try:
dt = datetime.strptime(date_str, fmt)
return dt.strftime(output_format)
except ValueError:
continue
raise ValueError(f"无法解析日期格式: {date_str}")
def get_date_range(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
lookback_days: int = 365,
) -> tuple[str, str]:
"""
获取日期范围
Args:
start_date: 开始日期None则根据lookback_days计算
end_date: 结束日期None则使用今天
lookback_days: 回溯天数
Returns:
tuple: (start_date, end_date) 格式为 YYYY-MM-DD
"""
if end_date is None:
end = datetime.now()
else:
end = datetime.strptime(format_date(end_date), "%Y-%m-%d")
if start_date is None:
start = end - timedelta(days=lookback_days)
else:
start = datetime.strptime(format_date(start_date), "%Y-%m-%d")
return start.strftime("%Y-%m-%d"), end.strftime("%Y-%m-%d")
def calculate_cagr(
nav_series: pd.Series,
method: str = "natural_days",
) -> float:
"""
计算年化收益率CAGR
Args:
nav_series: 净值序列index=日期)
method: 'natural_days''trading_days'
Returns:
float: CAGR值
"""
total_return = nav_series.iloc[-1] / nav_series.iloc[0]
if method == "natural_days":
days = (nav_series.index[-1] - nav_series.index[0]).days
years = days / 365.0
elif method == "trading_days":
years = len(nav_series) / 252.0
else:
raise ValueError(f"不支持的CAGR计算方式: {method}")
if years <= 0:
return 0.0
return total_return ** (1 / years) - 1
def calculate_max_drawdown(nav_series: pd.Series) -> tuple[float, datetime, datetime]:
"""
计算最大回撤
Returns:
tuple: (最大回撤比例, 回撤起始日, 回撤结束日)
"""
cummax = nav_series.cummax()
drawdown = (nav_series - cummax) / cummax
max_dd = drawdown.min()
end_idx = drawdown.idxmin()
start_idx = nav_series[:end_idx].idxmax()
return max_dd, start_idx, end_idx
def calculate_sharpe(
returns: pd.Series,
rf: float = 0.0,
periods: int = 252,
) -> float:
"""
计算年化夏普比率
Args:
returns: 日收益率序列
rf: 无风险利率(年化)
periods: 年化系数
Returns:
float: 夏普比率
"""
excess_returns = returns - rf / periods
if excess_returns.std() == 0:
return 0.0
return excess_returns.mean() / excess_returns.std() * np.sqrt(periods)
def resample_data(
df: pd.DataFrame,
timeframe: str,
time_col: str = "time",
) -> pd.DataFrame:
"""
对数据进行重采样
Args:
df: 原始数据
timeframe: 目标周期 ('1D', '1W', '1M', '1Y')
time_col: 时间列名
Returns:
DataFrame: 重采样后的数据
"""
timeframe_map = {
"1D": "D",
"1W": "W",
"1M": "M",
"3M": "3M",
"1Y": "Y",
}
if timeframe not in timeframe_map:
return df
df = df.copy()
if time_col in df.columns:
df[time_col] = pd.to_datetime(df[time_col])
df.set_index(time_col, inplace=True)
rule = timeframe_map[timeframe]
resampled = (
df.resample(rule)
.agg(
{
"open": "first",
"high": "max",
"low": "min",
"close": "last",
"volume": "sum",
}
)
.dropna()
)
return resampled.reset_index()
def safe_divide(a: float, b: float, default: float = 0.0) -> float:
"""安全除法避免除以0"""
return a / b if b != 0 else default
def truncate_string(s: str, max_length: int = 50, suffix: str = "...") -> str:
"""截断字符串"""
if len(s) <= max_length:
return s
return s[: max_length - len(suffix)] + suffix