第三章:时间度量基础
做量化交易,时间就是金钱。这句话一点都不夸张。
我见过太多策略,逻辑完美,回测漂亮,一上实盘就崩。为什么?因为时间度量出了问题。你想想看,如果连 tick 的到达时间都量不准,那你的策略执行、滑点计算、延迟分析,全都会变成空中楼阁。
这一章,我们就来彻底搞懂 Python 里的时间度量工具。我会把我在实盘开发中踩过的坑、积累的经验,全都摊开来讲。
3.1 time 模块:最基础的时间工具
time 模块是 Python 标准库中最古老的时间模块。它直接调用操作系统底层的 C 库函数,性能很高,但精度和功能都比较原始。
3.1.1 核心函数
| 函数 | 返回值 | 精度 | 用途 |
|---|---|---|---|
time.time() |
Unix 时间戳(秒) | 毫秒级(Windows 上约 1ms) | 获取当前时间、计算耗时 |
time.sleep(secs) |
无 | 取决于操作系统调度 | 暂停执行指定秒数 |
time.perf_counter() |
性能计数器(秒) | 纳秒级 | 高精度计时 |
time.process_time() |
进程 CPU 时间(秒) | 微秒级 | 测量 CPU 执行时间 |
time.time() 返回的是墙上时间(wall-clock time)。如果系统时间被 NTP 同步调整了,它可能会往回跳!我在实盘交易中遇到过这种情况,导致策略误判了行情时间,差点造成损失。
3.1.2 代码示例:基础计时
import time
# 获取当前时间戳
now = time.time()
print(f"当前 Unix 时间戳:{now}")
# 格式化时间
local_time = time.localtime(now)
formatted = time.strftime("%Y-%m-%d %H:%M:%S", local_time)
print(f"格式化时间:{formatted}")
# 简单计时
start = time.time()
# 模拟一些计算
for _ in range(1000000):
_ = 1 + 1
end = time.time()
print(f"耗时:{end - start:.6f} 秒")
3.2 timeit 模块:精准测量代码性能
用 time.time() 手动计时,有个大问题:单次测量受系统负载影响太大。你跑一次可能 0.1 秒,再跑一次可能 0.3 秒。这怎么判断性能?
timeit 模块就是来解决这个问题的。它会自动多次运行你的代码,取最小值或平均值,排除系统干扰。
3.2.1 基本用法
import timeit
# 测量单条语句
t = timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
print(f"执行 10000 次耗时:{t:.6f} 秒")
# 测量函数
def test_func():
return sum(range(1000))
t = timeit.timeit(test_func, number=10000)
print(f"函数执行 10000 次耗时:{t:.6f} 秒")
timeit.repeat() 跑 3-5 轮,每轮 10000 次,然后取最小值。这样能最大程度排除偶然因素。
3.2.2 避坑指南
我曾经犯过一个低级错误:在 timeit 的代码字符串里引用了外部变量,结果报错。后来才知道,timeit 默认是在独立的命名空间中执行的。
# 错误写法
x = 42
t = timeit.timeit('x + 1', number=1000) # 会报错:NameError
# 正确写法
t = timeit.timeit('x + 1', globals=globals(), number=1000)
3.3 datetime 与 timedelta:时间运算的利器
做量化交易,经常要处理时间加减。比如「获取过去 5 分钟的 K 线数据」、「计算距离收盘还有多少秒」。这时候 datetime 和 timedelta 就派上用场了。
3.3.1 精度对比
| 模块 | 最小精度 | 适用场景 |
|---|---|---|
time.time() |
毫秒级 | 简单计时、时间戳获取 |
datetime.now() |
微秒级 | 时间运算、格式化输出 |
time.perf_counter() |
纳秒级 | 高精度性能测量 |
time.process_time() |
微秒级 | CPU 时间测量 |
3.3.2 实战:计算 K 线时间窗口
from datetime import datetime, timedelta
# 获取当前时间
now = datetime.now()
print(f"当前时间:{now}")
# 计算 5 分钟前
five_min_ago = now - timedelta(minutes=5)
print(f"5 分钟前:{five_min_ago}")
# 计算距离下一个整点还有多少秒
next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
seconds_until_next_hour = (next_hour - now).total_seconds()
print(f"距离下一个整点还有:{seconds_until_next_hour:.2f} 秒")
datetime 的精度是微秒级,但 timedelta 内部使用 days、seconds、microseconds 三个字段存储。如果你需要纳秒级精度,必须用 perf_counter。
3.4 高精度计时器:perf_counter 与 process_time
在量化交易中,我们经常要测量「从收到行情到发出信号」的延迟。这个延迟可能只有几十微秒。用 time.time() 根本量不准。
3.4.1 perf_counter:墙上时间的高精度版本
perf_counter 返回的是性能计数器,精度通常能达到纳秒级。它不受系统时间调整的影响,只用于测量时间间隔。
import time
# 测量极短代码段的执行时间
start = time.perf_counter()
# 模拟一个微秒级的操作
_ = [x**2 for x in range(1000)]
end = time.perf_counter()
print(f"耗时:{(end - start) * 1_000_000:.2f} 微秒")
perf_counter 的起始值是不确定的。你不能用它来获取「当前时间」,只能用来计算时间差。
3.4.2 process_time:只算 CPU 时间
有时候我们想知道「代码本身花了多少 CPU 时间」,而不是墙上时间。比如你的代码被操作系统挂起了 1 秒,墙上时间多了 1 秒,但 CPU 时间没变。这时候用 process_time。
import time
# 测量 CPU 执行时间
start = time.process_time()
# 模拟 CPU 密集型计算
for _ in range(10_000_000):
_ = 1 + 1
end = time.process_time()
print(f"CPU 时间:{end - start:.6f} 秒")
3.4.3 实战对比:perf_counter vs process_time
import time
def io_simulation():
# 模拟 I/O 等待
time.sleep(0.1)
# 模拟计算
for _ in range(1_000_000):
_ = 1 + 1
# 用 perf_counter 测量
start_wall = time.perf_counter()
io_simulation()
end_wall = time.perf_counter()
print(f"墙上时间:{(end_wall - start_wall) * 1000:.2f} 毫秒")
# 用 process_time 测量
start_cpu = time.process_time()
io_simulation()
end_cpu = time.process_time()
print(f"CPU 时间:{(end_cpu - start_cpu) * 1000:.2f} 毫秒")
你会发现,墙上时间比 CPU 时间多了大约 100 毫秒。这 100 毫秒就是 time.sleep() 造成的 I/O 等待。在量化交易中,如果你用 process_time 去测量网络延迟,那结果会严重失真。
3.5 知识体系总览
下面这张图,是我自己总结的时间度量工具选择逻辑。每次写新代码前,我都会看一眼。
3.6 我的选择原则
说了这么多,到底该用哪个?我总结了一个简单的判断逻辑:
- 获取当前时间:用
datetime.now()。它支持时区、格式化、运算,功能最全。 - 测量代码耗时:用
timeit模块。它自动处理了多次测量和统计。 - 测量微秒级延迟:用
perf_counter。精度最高,不受系统时间影响。 - 测量 CPU 使用率:用
process_time。它能告诉你代码到底吃了多少 CPU。 - 简单计时:用
time.time()。够用就行,别过度设计。
perf_counter 和 datetime。前者用于性能监控,后者用于日志记录。两者配合,既能定位延迟问题,又能追溯历史事件。
嗯,时间度量这块就讲到这里。记住一句话:选对工具,比用对工具更重要。下一章,我们会把这些工具用在真正的性能基准测试中。