跳转至

时间处理函数统一重构文档

🎯 重构目标

将所有分散在各个文件中的时间处理函数统一到 utils/time_utils.py,实现单一职责原则,确保所有时间处理逻辑有统一的维护点。

📋 重构前的问题

1. 代码重复

在至少 5 个文件中存在相同的时间转换逻辑: - services/market_service.py - services/downloaders/base_downloader.py - services/downloaders/kline_downloader.py - services/downloaders/metrics_downloader.py - models/universe.py

2. 维护困难

如果需要修改时间处理逻辑(如时区问题),需要在多个文件中同步修改。

3. 测试冗余

需要在多个地方测试相同的时间转换逻辑。

✅ 重构方案

创建统一的时间工具库

位置: src/cryptoservice/utils/time_utils.py

提供的核心函数:

def date_to_timestamp_start(date: str) -> int
def date_to_timestamp_end(date: str) -> int
def datetime_str_to_timestamp(datetime_str: str, fmt: str = "%Y-%m-%d %H:%M:%S") -> int
def timestamp_to_datetime(timestamp: int | str, unit: str = "ms") -> datetime
def timestamp_to_date_str(timestamp: int | str, unit: str = "ms") -> str
def parse_date_safe(date_str: str) -> pd.Timestamp
def now_utc() -> datetime
def now_utc_timestamp() -> int
def generate_date_range(start_date: str, end_date: str, freq: str = "D") -> pd.DatetimeIndex
def is_timezone_aware(dt: datetime) -> bool

🔄 重构详情

1. market_service.py

之前 (18 行):

def _date_to_timestamp_start(self, date: str) -> str:
    """将日期字符串转换为当天开始的时间戳(UTC)."""
    from datetime import UTC

    timestamp = int(
        datetime.strptime(f"{date} 00:00:00", "%Y-%m-%d %H:%M:%S")
        .replace(tzinfo=UTC).timestamp() * 1000
    )
    return str(timestamp)

def _date_to_timestamp_end(self, date: str) -> str:
    """将日期字符串转换为次日开始的时间戳(UTC)."""
    import pandas as pd

    timestamp = int(
        (pd.Timestamp(date, tz="UTC") + pd.Timedelta(days=1))
        .timestamp() * 1000
    )
    return str(timestamp)

之后 (10 行):

def _date_to_timestamp_start(self, date: str) -> str:
    """将日期字符串转换为当天开始的时间戳(UTC)."""
    from cryptoservice.utils import date_to_timestamp_start
    return str(date_to_timestamp_start(date))

def _date_to_timestamp_end(self, date: str) -> str:
    """将日期字符串转换为次日开始的时间戳(UTC)."""
    from cryptoservice.utils import date_to_timestamp_end
    return str(date_to_timestamp_end(date))

代码简化: 44% (18行 -> 10行)

2. base_downloader.py

之前 (24 行,包含详细注释):

def _date_to_timestamp_start(self, date: str) -> str:
    """将日期字符串转换为当天开始的时间戳(UTC)."""
    from datetime import UTC, datetime

    # 使用 UTC 时区,确保与增量检测逻辑一致
    timestamp = int(
        datetime.strptime(f"{date} 00:00:00", "%Y-%m-%d %H:%M:%S")
        .replace(tzinfo=UTC).timestamp() * 1000
    )
    return str(timestamp)

def _date_to_timestamp_end(self, date: str) -> str:
    """将日期字符串转换为次日开始的时间戳(UTC).

    使用次日 00:00:00 而不是当天 23:59:59,以确保:
    1. 包含当天最后一个完整的K线周期(例如 23:55:00 的5分钟K线)
    2. 与增量下载检测的时间范围保持一致
    """
    from datetime import UTC, datetime, timedelta

    # 解析日期并加1天,使用 UTC 时区
    date_obj = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=UTC)
    next_day = date_obj + timedelta(days=1)
    timestamp = int(next_day.timestamp() * 1000)
    return str(timestamp)

之后 (14 行):

def _date_to_timestamp_start(self, date: str) -> str:
    """将日期字符串转换为当天开始的时间戳(UTC)."""
    from cryptoservice.utils import date_to_timestamp_start
    return str(date_to_timestamp_start(date))

def _date_to_timestamp_end(self, date: str) -> str:
    """将日期字符串转换为次日开始的时间戳(UTC).

    使用次日 00:00:00 而不是当天 23:59:59,确保与增量下载逻辑一致。
    """
    from cryptoservice.utils import date_to_timestamp_end
    return str(date_to_timestamp_end(date))

代码简化: 42% (24行 -> 14行)

3. kline_downloader.py

重构模式与 base_downloader.py 相同。

4. metrics_downloader.py

重构模式与 base_downloader.py 相同。

5. models/universe.py

之前:

@staticmethod
def _calculate_timestamp(date_str: str, time_str: str = "00:00:00") -> str:
    """计算日期的时间戳(毫秒)."""
    from datetime import UTC, datetime

    return str(
        int(datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M:%S")
        .replace(tzinfo=UTC).timestamp() * 1000)
    )

@staticmethod
def _calculate_end_timestamp(date_str: str) -> str:
    """计算日期结束时间戳(次日00:00:00的毫秒时间戳)."""
    import pandas as pd

    # 使用 UTC 时区,次日00:00:00
    return str(
        int((pd.Timestamp(date_str, tz="UTC") + pd.Timedelta(days=1))
        .timestamp() * 1000)
    )

之后:

@staticmethod
def _calculate_timestamp(date_str: str, time_str: str = "00:00:00") -> str:
    """计算日期时间的时间戳(毫秒)."""
    from cryptoservice.utils import date_to_timestamp_start, datetime_str_to_timestamp

    if time_str == "00:00:00":
        return str(date_to_timestamp_start(date_str))
    return str(datetime_str_to_timestamp(f"{date_str} {time_str}"))

@staticmethod
def _calculate_end_timestamp(date_str: str) -> str:
    """计算日期结束时间戳(次日00:00:00的毫秒时间戳)."""
    from cryptoservice.utils import date_to_timestamp_end
    return str(date_to_timestamp_end(date_str))

📊 重构统计

指标 数值
重构文件数 5 个
删除重复代码 ~60 行
平均代码简化 40-50%
新增工具函数 10 个
统一调用点 1 个 (time_utils.py)

🎁 重构带来的好处

1. 单一职责原则

  • ✅ 所有时间处理逻辑集中在 time_utils.py
  • ✅ 每个类/模块只负责自己的业务逻辑
  • ✅ 时间处理成为独立的工具模块

2. 易于维护

  • ✅ 只需修改一处,所有地方生效
  • ✅ 减少了 bug 的可能性
  • ✅ 降低了维护成本

3. 代码简洁

  • ✅ 每个方法从 10-20 行减少到 3-5 行
  • ✅ 消除了重复代码
  • ✅ 提高了代码可读性

4. 类型安全

  • ✅ 统一的类型转换(int -> str)
  • ✅ 清晰的函数签名
  • ✅ 完整的类型提示

5. 测试集中

  • ✅ 只需测试 time_utils 中的函数
  • ✅ 减少了测试冗余
  • ✅ 提高了测试覆盖率

6. 文档完善

  • ✅ 每个函数都有详细的文档
  • ✅ 包含使用示例
  • ✅ 说明了设计决策(如使用次日 00:00:00)

🔍 验证重构结果

检查没有遗留的重复实现

# 在 services/ 和 models/ 中应该只有简单的调用
grep -r 'datetime.strptime.*timestamp' --include='*.py' src/cryptoservice/services/
grep -r 'datetime.strptime.*timestamp' --include='*.py' src/cryptoservice/models/

# 应该只在 time_utils.py 中找到实现

运行测试确保功能正确

pytest tests/ -v

检查所有时间处理都使用 UTC

# 查找可能的时区问题
grep -r "pd\.to_datetime" src/cryptoservice/ | grep -v "utc=True"
grep -r "pd\.Timestamp" src/cryptoservice/ | grep -v 'tz='

📝 使用示例

在新代码中使用时间工具

from cryptoservice.utils import (
    date_to_timestamp_start,
    date_to_timestamp_end,
    timestamp_to_datetime,
)

# 将日期转换为时间戳
start_ts = date_to_timestamp_start("2024-10-31")  # 返回 int
end_ts = date_to_timestamp_end("2024-10-31")      # 返回 int

# 将时间戳转换为 datetime
dt = timestamp_to_datetime(start_ts)
print(dt)  # 2024-10-31 00:00:00+00:00

旧方法仍然可用(向后兼容)

# 各个类中的方法仍然存在,但内部调用 time_utils
service = MarketDataService(...)
start_ts_str = service._date_to_timestamp_start("2024-10-31")  # 返回 str

🎓 最佳实践建议

1. 优先使用 time_utils

在新代码中,直接使用 time_utils 中的函数:

# ✅ 推荐
from cryptoservice.utils import date_to_timestamp_start
timestamp = date_to_timestamp_start(date)

# ❌ 不推荐(除非在类方法内部需要保持接口一致性)
timestamp = self._date_to_timestamp_start(date)

2. 保持简洁

如果类方法只是简单的包装,考虑直接使用工具函数:

# 可以考虑在未来移除这些包装方法
# 直接使用 time_utils.date_to_timestamp_start()

3. 文档引用

在文档中引用 time_utils 作为标准时间处理方式。

🚀 未来改进

短期

  • [ ] 为 time_utils 编写完整的单元测试
  • [ ] 在文档中添加时间处理最佳实践

中期

  • [ ] 考虑移除各个类中的包装方法,直接使用 time_utils
  • [ ] 添加更多时间处理工具函数(如时区转换)

长期

  • [ ] 考虑使用 Python 的 zoneinfo 替代 UTC 常量
  • [ ] 支持更多时间格式和时区

📚 相关文档


重构完成日期: 2024-10-08 重构原则: 单一职责、避免重复、易于维护