Skip to content

Commit 655e46c

Browse files
authored
Merge pull request shidenggui#47 from lamter/fix/重构时钟引擎和单元测试
Fix/直接使用 mock 重设单元测试中的时间戳
2 parents af099f5 + 8523b3d commit 655e46c

File tree

4 files changed

+90
-63
lines changed

4 files changed

+90
-63
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,37 @@ from easyquant import DefaultQuotationEngine
307307

308308
m = easyquant.MainEngine(broker, need_data, quotation_engine=[DefaultQuotationEngine, LFEngine, OtherEngine])
309309
```
310+
311+
#### 时间戳单元测试
312+
313+
1. 请通过 clock_engine 中的 .now 或者 .now_dt 接口,以及 time.time() 接口来获得时间戳.
314+
2. 通过上述接口获得时间戳,可以在单元测试中模拟某个时刻或者一段时间,详见单元测试 [test_set_now](https://github.com/shidenggui/easyquant/blob/master/unitest_demo.py)
315+
316+
```python
317+
from unittest import mock
318+
319+
# 使用datetime 类构建时间戳
320+
tzinfo = tz.tzlocal() # 时区
321+
now = datetime.datetime(2016, 7, 14, 8, 59, 50, tzinfo=tzinfo)
322+
323+
# 通过mock ,将 time.time() 函数的返回值重设为上面的打算模拟的值,注意要转化为浮点数时间戳
324+
time.time = mock.Mock(return_value=now.timestamp())
325+
326+
# 生成一个时钟引擎
327+
clock_engien = ClockEngine(EventEngine(), tzinfo)
328+
329+
# 此时通过 time.time 获得的时间戳,都是上面的预设值
330+
clock_engien.now == now.timestamp() # time.time 时间戳
331+
clock_engien.now_dt == now # datetime 时间戳
332+
333+
# 据此可以模拟一段时间内各个闹钟事件的触发,比如模拟开市9:00一直到休市15:00
334+
begin = datetime.datetime(2016, 7, 14, 8, 59, 50, tzinfo=tzinfo).timestamp()
335+
end = datetime.datetime(2016, 7, 14, 15, 00, 10, tzinfo=tzinfo).timestamp()
336+
337+
for pass_seconds in range(end-begin):
338+
# 时间逐秒往前
339+
now = begin + pass_seconds
340+
time.time = mock.Mock(return_value=now.timestamp())
341+
# 每秒触发一次 tick_tock
342+
clock_engien.tock()
343+
```

easyquant/main_engine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class MainEngine:
2626
"""主引擎,负责行情 / 事件驱动引擎 / 交易"""
2727

2828
def __init__(self, broker=None, need_data=None, quotation_engines=None,
29-
log_handler=DefaultLogHandler(), now=None, tzinfo=None):
29+
log_handler=DefaultLogHandler(), tzinfo=None):
3030
"""初始化事件 / 行情 引擎并启动事件引擎
3131
"""
3232
self.log = log_handler
@@ -46,7 +46,7 @@ def __init__(self, broker=None, need_data=None, quotation_engines=None,
4646
self.log.info('选择了无交易模式')
4747

4848
self.event_engine = EventEngine()
49-
self.clock_engine = ClockEngine(self.event_engine, now, tzinfo)
49+
self.clock_engine = ClockEngine(self.event_engine, tzinfo)
5050

5151
quotation_engines = quotation_engines or [DefaultQuotationEngine]
5252

easyquant/push_engine/clock_engine.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,15 @@ class ClockEngine:
101101
"""
102102
EventType = 'clock_tick'
103103

104-
def __init__(self, event_engine, now=None, tzinfo=None):
104+
def __init__(self, event_engine, tzinfo=None):
105105
"""
106106
:param event_engine:
107107
:param event_engine: tzinfo
108108
:return:
109109
"""
110110
# 默认使用当地时间的时区
111111
self.tzinfo = tzinfo or tz.tzlocal()
112-
# 引擎启动的时间,默认为当前.测试时可手动设置模拟各个时间段.
113-
self.time_delta = self._delta(now)
114-
# self.start_time = self.now_dt.replace(hour=0, minute=0, second=0, microsecond=0)
112+
115113
self.event_engine = event_engine
116114
self.is_active = True
117115
self.clock_engine_thread = Thread(target=self.clocktick)
@@ -150,23 +148,23 @@ def close():
150148
for interval in (0.5, 1, 5, 15, 30, 60):
151149
self.register_interval(interval)
152150

153-
def _delta(self, now):
154-
if now is None:
155-
return 0
156-
if now.tzinfo is None:
157-
now = arrow.get(datetime.datetime(
158-
now.year, now.month, now.day, now.hour, now.minute, now.second, now.microsecond, self.tzinfo,
159-
))
160-
161-
return (arrow.now() - now).total_seconds()
151+
# def _delta(self, now):
152+
# if now is None:
153+
# return 0
154+
# if now.tzinfo is None:
155+
# now = arrow.get(datetime.datetime(
156+
# now.year, now.month, now.day, now.hour, now.minute, now.second, now.microsecond, self.tzinfo,
157+
# ))
158+
#
159+
# return (arrow.now() - now).total_seconds()
162160

163161
@property
164162
def now(self):
165163
"""
166164
now 时间戳统一接口
167165
:return:
168166
"""
169-
return time.time() - self.time_delta
167+
return time.time()
170168

171169
@property
172170
def now_dt(self):
@@ -175,13 +173,13 @@ def now_dt(self):
175173
"""
176174
return arrow.get(self.now).to(self.tzinfo)
177175

178-
def reset_now(self, now=None):
179-
"""
180-
调试用接口,请勿在生产环境使用
181-
:param now:
182-
:return:
183-
"""
184-
self.time_delta = self._delta(now)
176+
# def reset_now(self, now=None):
177+
# """
178+
# 调试用接口,请勿在生产环境使用
179+
# :param now:
180+
# :return:
181+
# """
182+
# self.time_delta = self._delta(now)
185183

186184
def start(self):
187185
self.clock_engine_thread.start()

unitest_demo.py

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
"""
55
import time
66
import unittest
7+
from unittest import mock
78
import datetime
89
import pandas as pd
910
from easyquant.easydealutils.time import get_next_trade_date, is_trade_date
11+
import arrow
1012

1113
from dateutil import tz
1214
from easyquant.main_engine import MainEngine
@@ -57,7 +59,7 @@ def setUp(self):
5759

5860
now = datetime.datetime.combine(self.trade_date, self.time)
5961
# 此处重新定义 main_engine
60-
self._main_engine = MainEngine('ht', now=now)
62+
self._main_engine = MainEngine('ht', 'tmp/ht.json')
6163

6264
# 设置为不在交易中
6365
self.clock_engine.trading_state = False
@@ -85,50 +87,41 @@ def tearDown(self):
8587

8688
def test_set_now(self):
8789
"""
88-
重设 clock_engine 的时间
90+
1. 重设 clock_engine 的时间
91+
2. 通过 mock 来重设时间戳
92+
3. mock 只能重设 time.time 函数的时间戳,但是不能重设 datetime.datetime.now 函数的时间戳,详情见: http://stackoverflow.com/questions/4481954/python-trying-to-mock-datetime-date-today-but-not-working
93+
4. 在代码中需要使用时间戳时,请通过 clock_engine 中的 now 或者 now_dt 接口获得,也可以使用 time.time 获得.否则该段代码将不适用于需要更改时间戳的单元测试
8994
:return:
9095
"""
91-
9296
tzinfo = tz.tzlocal()
93-
now = datetime.datetime.combine(
94-
self.trade_date,
95-
datetime.time(8, 59, 00, tzinfo=tzinfo),
96-
)
97-
clock_engien = ClockEngine(EventEngine(), now, tzinfo)
97+
# 使用datetime 类构建时间戳
98+
now = datetime.datetime(2016, 7, 14, 8, 59, 50, tzinfo=tzinfo)
9899

99-
# 去掉微秒误差后比较
100-
self.assertEqual(clock_engien.now_dt.replace(microsecond=0), now)
101-
102-
def test_reset_now(self):
103-
"""
104-
重设时钟引擎当前时间为其他时间点
105-
:return:
106-
"""
107-
tzinfo = tz.tzlocal()
108-
clock_engien = ClockEngine(EventEngine())
109-
now = datetime.datetime.combine(
110-
self.trade_date,
111-
datetime.time(8, 59, 00, tzinfo=tzinfo),
112-
)
113-
clock_engien.reset_now(now)
100+
# 通过mock ,将 time.time() 函数的返回值重设为上面的打算模拟的值,注意要转化为浮点数时间戳
101+
time.time = mock.Mock(return_value=now.timestamp())
114102

115-
# 去掉微秒误差后比较
116-
self.assertEqual(clock_engien.now_dt.replace(microsecond=0), now)
103+
# 生成一个时钟引擎
104+
clock_engien = ClockEngine(EventEngine(), tzinfo)
117105

118-
# 重设为当前时间
119-
clock_engien.reset_now()
120-
now = datetime.datetime.now(tzinfo).replace(microsecond=0)
106+
# 去掉微秒误差后验证其数值
107+
self.assertEqual(clock_engien.now, now.timestamp()) # time.time 时间戳
108+
self.assertEqual(clock_engien.now_dt, now) # datetime 时间戳
121109

122-
# 去掉微秒误差后比较
123-
self.assertEqual(clock_engien.now_dt.replace(microsecond=0), now)
110+
# 据此可以模拟一段时间内各个闹钟事件的触发,比如模拟开市9:00一直到休市15:00
111+
for _ in range(60):
112+
clock_engien.tock()
113+
now += datetime.timedelta(seconds=1) # 每秒触发一次 tick_tock
114+
time.time = mock.Mock(return_value=now.timestamp())
115+
self.assertEqual(clock_engien.now, now.timestamp()) # time.time 时间戳
116+
self.assertEqual(clock_engien.now_dt, now) # datetime 时间戳
124117

125118
def test_clock_moment_is_active(self):
126119
# 设置时间
127120
now = datetime.datetime.combine(
128121
self.trade_date,
129122
datetime.time(23, 59, 58, tzinfo=tz.tzlocal()),
130123
)
131-
self.clock_engine.reset_now(now)
124+
time.time = mock.Mock(return_value=now.timestamp())
132125

133126
# 触发前, 注册时间事件
134127
moment = datetime.time(23, 59, 59, tzinfo=tz.tzlocal())
@@ -141,7 +134,8 @@ def test_clock_moment_is_active(self):
141134
self.trade_date,
142135
datetime.time(23, 59, 59, tzinfo=tz.tzlocal())
143136
)
144-
self.clock_engine.reset_now(now)
137+
time.time = mock.Mock(return_value=now.timestamp())
138+
145139
# 确认触发
146140
self.assertTrue(cmh.is_active())
147141

@@ -151,7 +145,7 @@ def test_clock_update_next_time(self):
151145
self.trade_date,
152146
datetime.time(23, 59, 58, tzinfo=tz.tzlocal())
153147
)
154-
self.clock_engine.reset_now(now)
148+
time.time = mock.Mock(return_value=now.timestamp())
155149

156150
# 触发前, 注册时间事件
157151
moment = datetime.time(23, 59, 59, tzinfo=tz.tzlocal())
@@ -164,7 +158,8 @@ def test_clock_update_next_time(self):
164158
self.trade_date,
165159
datetime.time(23, 59, 59, tzinfo=tz.tzlocal())
166160
)
167-
self.clock_engine.reset_now(now)
161+
time.time = mock.Mock(return_value=now.timestamp())
162+
168163
# 确认触发
169164
self.assertTrue(cmh.is_active())
170165

@@ -186,7 +181,7 @@ def register_clock_moent_makeup(self, makeup):
186181
self.trade_date,
187182
datetime.time(23, 59, 59, tzinfo=tz.tzlocal())
188183
)
189-
self.clock_engine.reset_now(begin)
184+
time.time = mock.Mock(return_value=begin.timestamp())
190185

191186
# 注册时刻一个超时事件
192187
moment = datetime.time(0, 0, 0, tzinfo=tz.tzlocal())
@@ -256,7 +251,7 @@ def test_register_clock_interval_not_trading_false(self):
256251
self.assertFalse(self.clock_engine.trading_state)
257252

258253
def register_clock_interval(self, begin, trading, active_times):
259-
self.clock_engine.reset_now(begin)
254+
time.time = mock.Mock(return_value=begin.timestamp())
260255
self.active_times = 0
261256

262257
def clock(event):
@@ -278,7 +273,7 @@ def clock(event):
278273
# 开启事件引擎
279274
for sec in range(int(minute_interval * 60)):
280275
now = begin + datetime.timedelta(seconds=sec)
281-
self.clock_engine.reset_now(now)
276+
time.time = mock.Mock(return_value=now.timestamp())
282277
self.clock_engine.tock()
283278
time.sleep(1)
284279
self.main_engine.event_engine.stop()
@@ -317,7 +312,7 @@ def count(event):
317312
seconds = 60 * mins
318313
for secs in range(seconds):
319314
now = begin + datetime.timedelta(seconds=secs)
320-
self.clock_engine.reset_now(now)
315+
time.time = mock.Mock(return_value=now.timestamp())
321316
self.clock_engine.tock()
322317
time.sleep(0.001)
323318

@@ -358,7 +353,7 @@ def count(event):
358353
end = (begin + datetime.timedelta(days=days)).replace(hour=23, minute=59, second=59)
359354

360355
# 重置时间到凌晨
361-
self.clock_engine.reset_now(begin)
356+
time.time = mock.Mock(return_value=begin.timestamp())
362357

363358
# 预估时间事件触发次数, 每个交易日触发一次
364359
actived_times = 0
@@ -374,7 +369,7 @@ def count(event):
374369

375370
now = begin
376371
while 1:
377-
self.clock_engine.reset_now(now)
372+
time.time = mock.Mock(return_value=now.timestamp())
378373
self.clock_engine.tock()
379374
time.sleep(0.001)
380375
now += interval

0 commit comments

Comments
 (0)