Skip to content

Commit

Permalink
Merge pull request shidenggui#47 from lamter/fix/重构时钟引擎和单元测试
Browse files Browse the repository at this point in the history
Fix/直接使用 mock 重设单元测试中的时间戳
  • Loading branch information
shidenggui authored Jul 15, 2016
2 parents af099f5 + 8523b3d commit 655e46c
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 63 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,37 @@ from easyquant import DefaultQuotationEngine

m = easyquant.MainEngine(broker, need_data, quotation_engine=[DefaultQuotationEngine, LFEngine, OtherEngine])
```

#### 时间戳单元测试

1. 请通过 clock_engine 中的 .now 或者 .now_dt 接口,以及 time.time() 接口来获得时间戳.
2. 通过上述接口获得时间戳,可以在单元测试中模拟某个时刻或者一段时间,详见单元测试 [test_set_now](https://github.com/shidenggui/easyquant/blob/master/unitest_demo.py)

```python
from unittest import mock

# 使用datetime 类构建时间戳
tzinfo = tz.tzlocal() # 时区
now = datetime.datetime(2016, 7, 14, 8, 59, 50, tzinfo=tzinfo)

# 通过mock ,将 time.time() 函数的返回值重设为上面的打算模拟的值,注意要转化为浮点数时间戳
time.time = mock.Mock(return_value=now.timestamp())

# 生成一个时钟引擎
clock_engien = ClockEngine(EventEngine(), tzinfo)

# 此时通过 time.time 获得的时间戳,都是上面的预设值
clock_engien.now == now.timestamp() # time.time 时间戳
clock_engien.now_dt == now # datetime 时间戳

# 据此可以模拟一段时间内各个闹钟事件的触发,比如模拟开市9:00一直到休市15:00
begin = datetime.datetime(2016, 7, 14, 8, 59, 50, tzinfo=tzinfo).timestamp()
end = datetime.datetime(2016, 7, 14, 15, 00, 10, tzinfo=tzinfo).timestamp()

for pass_seconds in range(end-begin):
# 时间逐秒往前
now = begin + pass_seconds
time.time = mock.Mock(return_value=now.timestamp())
# 每秒触发一次 tick_tock
clock_engien.tock()
```
4 changes: 2 additions & 2 deletions easyquant/main_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class MainEngine:
"""主引擎,负责行情 / 事件驱动引擎 / 交易"""

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

self.event_engine = EventEngine()
self.clock_engine = ClockEngine(self.event_engine, now, tzinfo)
self.clock_engine = ClockEngine(self.event_engine, tzinfo)

quotation_engines = quotation_engines or [DefaultQuotationEngine]

Expand Down
40 changes: 19 additions & 21 deletions easyquant/push_engine/clock_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,15 @@ class ClockEngine:
"""
EventType = 'clock_tick'

def __init__(self, event_engine, now=None, tzinfo=None):
def __init__(self, event_engine, tzinfo=None):
"""
:param event_engine:
:param event_engine: tzinfo
:return:
"""
# 默认使用当地时间的时区
self.tzinfo = tzinfo or tz.tzlocal()
# 引擎启动的时间,默认为当前.测试时可手动设置模拟各个时间段.
self.time_delta = self._delta(now)
# self.start_time = self.now_dt.replace(hour=0, minute=0, second=0, microsecond=0)

self.event_engine = event_engine
self.is_active = True
self.clock_engine_thread = Thread(target=self.clocktick)
Expand Down Expand Up @@ -150,23 +148,23 @@ def close():
for interval in (0.5, 1, 5, 15, 30, 60):
self.register_interval(interval)

def _delta(self, now):
if now is None:
return 0
if now.tzinfo is None:
now = arrow.get(datetime.datetime(
now.year, now.month, now.day, now.hour, now.minute, now.second, now.microsecond, self.tzinfo,
))

return (arrow.now() - now).total_seconds()
# def _delta(self, now):
# if now is None:
# return 0
# if now.tzinfo is None:
# now = arrow.get(datetime.datetime(
# now.year, now.month, now.day, now.hour, now.minute, now.second, now.microsecond, self.tzinfo,
# ))
#
# return (arrow.now() - now).total_seconds()

@property
def now(self):
"""
now 时间戳统一接口
:return:
"""
return time.time() - self.time_delta
return time.time()

@property
def now_dt(self):
Expand All @@ -175,13 +173,13 @@ def now_dt(self):
"""
return arrow.get(self.now).to(self.tzinfo)

def reset_now(self, now=None):
"""
调试用接口,请勿在生产环境使用
:param now:
:return:
"""
self.time_delta = self._delta(now)
# def reset_now(self, now=None):
# """
# 调试用接口,请勿在生产环境使用
# :param now:
# :return:
# """
# self.time_delta = self._delta(now)

def start(self):
self.clock_engine_thread.start()
Expand Down
75 changes: 35 additions & 40 deletions unitest_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
"""
import time
import unittest
from unittest import mock
import datetime
import pandas as pd
from easyquant.easydealutils.time import get_next_trade_date, is_trade_date
import arrow

from dateutil import tz
from easyquant.main_engine import MainEngine
Expand Down Expand Up @@ -57,7 +59,7 @@ def setUp(self):

now = datetime.datetime.combine(self.trade_date, self.time)
# 此处重新定义 main_engine
self._main_engine = MainEngine('ht', now=now)
self._main_engine = MainEngine('ht', 'tmp/ht.json')

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

def test_set_now(self):
"""
重设 clock_engine 的时间
1. 重设 clock_engine 的时间
2. 通过 mock 来重设时间戳
3. mock 只能重设 time.time 函数的时间戳,但是不能重设 datetime.datetime.now 函数的时间戳,详情见: http://stackoverflow.com/questions/4481954/python-trying-to-mock-datetime-date-today-but-not-working
4. 在代码中需要使用时间戳时,请通过 clock_engine 中的 now 或者 now_dt 接口获得,也可以使用 time.time 获得.否则该段代码将不适用于需要更改时间戳的单元测试
:return:
"""

tzinfo = tz.tzlocal()
now = datetime.datetime.combine(
self.trade_date,
datetime.time(8, 59, 00, tzinfo=tzinfo),
)
clock_engien = ClockEngine(EventEngine(), now, tzinfo)
# 使用datetime 类构建时间戳
now = datetime.datetime(2016, 7, 14, 8, 59, 50, tzinfo=tzinfo)

# 去掉微秒误差后比较
self.assertEqual(clock_engien.now_dt.replace(microsecond=0), now)

def test_reset_now(self):
"""
重设时钟引擎当前时间为其他时间点
:return:
"""
tzinfo = tz.tzlocal()
clock_engien = ClockEngine(EventEngine())
now = datetime.datetime.combine(
self.trade_date,
datetime.time(8, 59, 00, tzinfo=tzinfo),
)
clock_engien.reset_now(now)
# 通过mock ,将 time.time() 函数的返回值重设为上面的打算模拟的值,注意要转化为浮点数时间戳
time.time = mock.Mock(return_value=now.timestamp())

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

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

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

def test_clock_moment_is_active(self):
# 设置时间
now = datetime.datetime.combine(
self.trade_date,
datetime.time(23, 59, 58, tzinfo=tz.tzlocal()),
)
self.clock_engine.reset_now(now)
time.time = mock.Mock(return_value=now.timestamp())

# 触发前, 注册时间事件
moment = datetime.time(23, 59, 59, tzinfo=tz.tzlocal())
Expand All @@ -141,7 +134,8 @@ def test_clock_moment_is_active(self):
self.trade_date,
datetime.time(23, 59, 59, tzinfo=tz.tzlocal())
)
self.clock_engine.reset_now(now)
time.time = mock.Mock(return_value=now.timestamp())

# 确认触发
self.assertTrue(cmh.is_active())

Expand All @@ -151,7 +145,7 @@ def test_clock_update_next_time(self):
self.trade_date,
datetime.time(23, 59, 58, tzinfo=tz.tzlocal())
)
self.clock_engine.reset_now(now)
time.time = mock.Mock(return_value=now.timestamp())

# 触发前, 注册时间事件
moment = datetime.time(23, 59, 59, tzinfo=tz.tzlocal())
Expand All @@ -164,7 +158,8 @@ def test_clock_update_next_time(self):
self.trade_date,
datetime.time(23, 59, 59, tzinfo=tz.tzlocal())
)
self.clock_engine.reset_now(now)
time.time = mock.Mock(return_value=now.timestamp())

# 确认触发
self.assertTrue(cmh.is_active())

Expand All @@ -186,7 +181,7 @@ def register_clock_moent_makeup(self, makeup):
self.trade_date,
datetime.time(23, 59, 59, tzinfo=tz.tzlocal())
)
self.clock_engine.reset_now(begin)
time.time = mock.Mock(return_value=begin.timestamp())

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

def register_clock_interval(self, begin, trading, active_times):
self.clock_engine.reset_now(begin)
time.time = mock.Mock(return_value=begin.timestamp())
self.active_times = 0

def clock(event):
Expand All @@ -278,7 +273,7 @@ def clock(event):
# 开启事件引擎
for sec in range(int(minute_interval * 60)):
now = begin + datetime.timedelta(seconds=sec)
self.clock_engine.reset_now(now)
time.time = mock.Mock(return_value=now.timestamp())
self.clock_engine.tock()
time.sleep(1)
self.main_engine.event_engine.stop()
Expand Down Expand Up @@ -317,7 +312,7 @@ def count(event):
seconds = 60 * mins
for secs in range(seconds):
now = begin + datetime.timedelta(seconds=secs)
self.clock_engine.reset_now(now)
time.time = mock.Mock(return_value=now.timestamp())
self.clock_engine.tock()
time.sleep(0.001)

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

# 重置时间到凌晨
self.clock_engine.reset_now(begin)
time.time = mock.Mock(return_value=begin.timestamp())

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

now = begin
while 1:
self.clock_engine.reset_now(now)
time.time = mock.Mock(return_value=now.timestamp())
self.clock_engine.tock()
time.sleep(0.001)
now += interval
Expand Down

0 comments on commit 655e46c

Please sign in to comment.