From 2945e8dbc758e14f0e1d136c9bb2e44fc3b3880c Mon Sep 17 00:00:00 2001 From: Oak Ken Date: Fri, 15 Apr 2016 04:06:40 +0800 Subject: [PATCH 01/35] Adding yh.json --- yh.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 yh.json diff --git a/yh.json b/yh.json new file mode 100644 index 0000000..a25f279 --- /dev/null +++ b/yh.json @@ -0,0 +1,4 @@ +{ + "inputaccount":"客户号", + "trdpwd":"加密后的密码" +} From 17861343b25a3fa73811c899cba10d3bb4dccf0d Mon Sep 17 00:00:00 2001 From: lamter Date: Sat, 14 May 2016 15:06:57 +0800 Subject: [PATCH 02/35] =?UTF-8?q?=20-=20change=20:=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=88=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easyquant/easydealutils/time.py | 46 ++++++++++++------------ easyquant/main_engine.py | 4 +-- easyquant/push_engine/clock_engine.py | 49 +++++++++++++++++++++++--- easyquant/strategy/strategyTemplate.py | 1 - requirements.txt | 1 + test.py | 2 ++ unitest_demo.py | 16 +++++++++ 7 files changed, 88 insertions(+), 31 deletions(-) diff --git a/easyquant/easydealutils/time.py b/easyquant/easydealutils/time.py index d1dabc7..8ecbba9 100644 --- a/easyquant/easydealutils/time.py +++ b/easyquant/easydealutils/time.py @@ -80,26 +80,26 @@ def is_closing(now_time, start=datetime.time(14, 54, 30)): return False -def calc_next_trade_time_delta_seconds(): - now_time = datetime.datetime.now() - now = (now_time.hour, now_time.minute, now_time.second) - if now < (9, 15, 0): - next_trade_start = now_time.replace(hour=9, minute=15, second=0, microsecond=0) - elif (12, 0, 0) < now < (13, 0, 0): - next_trade_start = now_time.replace(hour=13, minute=0, second=0, microsecond=0) - elif now > (15, 0, 0): - distance_next_work_day = 1 - while True: - target_day = now_time + timedelta(days=distance_next_work_day) - if is_holiday(target_day.strftime('%Y%m%d')): - distance_next_work_day += 1 - else: - break - - day_delta = timedelta(days=distance_next_work_day) - next_trade_start = (now_time + day_delta).replace(hour=9, minute=15, - second=0, microsecond=0) - else: - return 0 - time_delta = next_trade_start - now_time - return time_delta.total_seconds() +# def calc_next_trade_time_delta_seconds(): +# now_time = datetime.datetime.now() +# now = (now_time.hour, now_time.minute, now_time.second) +# if now < (9, 15, 0): +# next_trade_start = now_time.replace(hour=9, minute=15, second=0, microsecond=0) +# elif (12, 0, 0) < now < (13, 0, 0): +# next_trade_start = now_time.replace(hour=13, minute=0, second=0, microsecond=0) +# elif now > (15, 0, 0): +# distance_next_work_day = 1 +# while True: +# target_day = now_time + timedelta(days=distance_next_work_day) +# if is_holiday(target_day.strftime('%Y%m%d')): +# distance_next_work_day += 1 +# else: +# break +# +# day_delta = timedelta(days=distance_next_work_day) +# next_trade_start = (now_time + day_delta).replace(hour=9, minute=15, +# second=0, microsecond=0) +# else: +# return 0 +# time_delta = next_trade_start - now_time +# return time_delta.total_seconds() diff --git a/easyquant/main_engine.py b/easyquant/main_engine.py index 07d8114..926fef8 100644 --- a/easyquant/main_engine.py +++ b/easyquant/main_engine.py @@ -24,7 +24,7 @@ class MainEngine: """主引擎,负责行情 / 事件驱动引擎 / 交易""" def __init__(self, broker, need_data='me.json', quotation_engines=None, - log_handler=DefaultLogHandler()): + log_handler=DefaultLogHandler(), now=None, tzinfo=None): """初始化事件 / 行情 引擎并启动事件引擎 """ # 登录账户 @@ -36,7 +36,7 @@ def __init__(self, broker, need_data='me.json', quotation_engines=None, log_handler.warn("券商账号信息文件 %s 不存在, easytrader 将不可用" % need_data) self.event_engine = EventEngine() - self.clock_engine = ClockEngine(self.event_engine) + self.clock_engine = ClockEngine(self.event_engine, now, tzinfo) quotation_engines = quotation_engines or [DefaultQuotationEngine] diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index bc6d3bf..b76885f 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -1,8 +1,10 @@ # coding: utf-8 import datetime from threading import Thread - import time +import arrow +from dateutil import tz + from ..easydealutils import time as etime from ..event_engine import Event @@ -14,17 +16,55 @@ def __init__(self, trading_time, clock_event): class ClockEngine: - """时间推送引擎""" + """ + 时间推送引擎 + 1. 提供统一的 now 时间戳. + """ EventType = 'clock_tick' - def __init__(self, event_engine): - self.start_time = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + def __init__(self, event_engine, now=None, tzinfo=None): + """ + :param event_engine: + :param start_time: issubclass(datetime.datetime) or arrow.Arrow 指定开始时间, 测试时可用. + :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) self.sleep_time = 1 self.trading_state = True if etime.is_tradetime(datetime.datetime.now()) else False + 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 + + @property + def now_dt(self): + """ + :return: datetime 类型, 带时区的时间戳.建议使用 arrow 库 + """ + return arrow.get(self.now).to(self.tzinfo) + def start(self): self.clock_engine_thread.start() @@ -50,7 +90,6 @@ def tock(self, now_time): if etime.is_tradetime(now_time): # 交易时间段 if self.trading_state is True: - if etime.is_closing(now_time): self.push_event_type('closing') diff --git a/easyquant/strategy/strategyTemplate.py b/easyquant/strategy/strategyTemplate.py index 082fc45..26607f4 100644 --- a/easyquant/strategy/strategyTemplate.py +++ b/easyquant/strategy/strategyTemplate.py @@ -52,7 +52,6 @@ def strategy(self, event): 'turnover': '420004912', 'volume': '206390073.351'}} """ - pass def run(self, event): try: diff --git a/requirements.txt b/requirements.txt index d21ec5f..3d48021 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ anyjson aiohttp easytrader easyquotation +arrow diff --git a/test.py b/test.py index 11658c3..137f0fd 100644 --- a/test.py +++ b/test.py @@ -1,4 +1,5 @@ import easyquotation +from easyquant.push_engine.clock_engine import ClockEngine import easyquant from easyquant import DefaultQuotationEngine, DefaultLogHandler, PushBaseEngine @@ -49,6 +50,7 @@ def fetch_quotation(self): log_handler = DefaultLogHandler(name='测试', log_type=log_type, filepath=log_filepath) + m = easyquant.MainEngine(broker, need_data, quotation_engines=[quotation_engine], log_handler=log_handler) m.load_strategy() m.start() diff --git a/unitest_demo.py b/unitest_demo.py index c0cfde8..237e04e 100644 --- a/unitest_demo.py +++ b/unitest_demo.py @@ -3,10 +3,13 @@ 演示如何进行单元测试 """ import time +import arrow import unittest import datetime +from dateutil import tz from easyquant.main_engine import MainEngine from easyquant.push_engine.clock_engine import ClockEngine +from easyquant.event_engine import EventEngine __author__ = 'Shawn' @@ -56,6 +59,19 @@ def tearDown(self): :return: """ + def test_set_now(self): + """ + 重设 clock_engine 的时间 + :return: + """ + + tzinfo = tz.tzlocal() + now = datetime.datetime(2016, 5, 5, 8, 59, 00, tzinfo) + clock_engien = ClockEngine(EventEngine(), now, tzinfo) + + # 去掉微秒误差后比较 + self.assertEqual(clock_engien.now_dt.replace(microsecond=0), now) + def test_tick(self): """ 测试时钟接口 From 37f58af2ad61ccffb2a9e86b10d0f0284cbd8e83 Mon Sep 17 00:00:00 2001 From: Oak Ken Date: Sun, 15 May 2016 11:30:52 +0800 Subject: [PATCH 03/35] Adding select without user model --- easyquant/main_engine.py | 18 +++++++++++------- easyquant/strategy/strategyTemplate.py | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/easyquant/main_engine.py b/easyquant/main_engine.py index 07d8114..9eaf23e 100644 --- a/easyquant/main_engine.py +++ b/easyquant/main_engine.py @@ -23,17 +23,22 @@ class MainEngine: """主引擎,负责行情 / 事件驱动引擎 / 交易""" - def __init__(self, broker, need_data='me.json', quotation_engines=None, + def __init__(self, broker=None, need_data=None, quotation_engines=None, log_handler=DefaultLogHandler()): """初始化事件 / 行情 引擎并启动事件引擎 """ + self.log = log_handler + # 登录账户 - self.user = easytrader.use(broker) - need_data_file = pathlib.Path(need_data) - if need_data_file.exists(): - self.user.prepare(need_data) + if (broker is not None) and (need_data is not None): + self.user = easytrader.use(broker) + need_data_file = pathlib.Path(need_data) + if need_data_file.exists(): + self.user.prepare(need_data) + else: + log_handler.warn("券商账号信息文件 %s 不存在, easytrader 将不可用" % need_data) else: - log_handler.warn("券商账号信息文件 %s 不存在, easytrader 将不可用" % need_data) + self.log.info('选择了无交易模式') self.event_engine = EventEngine() self.clock_engine = ClockEngine(self.event_engine) @@ -49,7 +54,6 @@ def __init__(self, broker, need_data='me.json', quotation_engines=None, # 保存读取的策略类 self.strategies = OrderedDict() self.strategy_list = list() - self.log = log_handler self.log.info('启动主引擎') diff --git a/easyquant/strategy/strategyTemplate.py b/easyquant/strategy/strategyTemplate.py index 082fc45..5650b64 100644 --- a/easyquant/strategy/strategyTemplate.py +++ b/easyquant/strategy/strategyTemplate.py @@ -6,7 +6,7 @@ class StrategyTemplate: name = 'DefaultStrategyTemplate' - def __init__(self, user, log_handler, main_engine): + def __init__(self, user=None, log_handler, main_engine): self.user = user self.main_engine = main_engine # 优先使用自定义 log 句柄, 否则使用主引擎日志句柄 From 0bb32e8d7f640794beda2f2452584671f7c1ecf2 Mon Sep 17 00:00:00 2001 From: Oak Ken Date: Sun, 15 May 2016 11:39:10 +0800 Subject: [PATCH 04/35] Adding without brocker model --- easyquant/main_engine.py | 1 + easyquant/strategy/strategyTemplate.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/easyquant/main_engine.py b/easyquant/main_engine.py index 9eaf23e..85a6786 100644 --- a/easyquant/main_engine.py +++ b/easyquant/main_engine.py @@ -38,6 +38,7 @@ def __init__(self, broker=None, need_data=None, quotation_engines=None, else: log_handler.warn("券商账号信息文件 %s 不存在, easytrader 将不可用" % need_data) else: + self.user = None self.log.info('选择了无交易模式') self.event_engine = EventEngine() diff --git a/easyquant/strategy/strategyTemplate.py b/easyquant/strategy/strategyTemplate.py index 5650b64..b53e29d 100644 --- a/easyquant/strategy/strategyTemplate.py +++ b/easyquant/strategy/strategyTemplate.py @@ -6,12 +6,11 @@ class StrategyTemplate: name = 'DefaultStrategyTemplate' - def __init__(self, user=None, log_handler, main_engine): + def __init__(self, user, log_handler, main_engine): self.user = user self.main_engine = main_engine # 优先使用自定义 log 句柄, 否则使用主引擎日志句柄 self.log = self.log_handler() or log_handler - self.init() def init(self): From 081c1ba84f61b1b723b29c9094f33dbc0a622683 Mon Sep 17 00:00:00 2001 From: lamter Date: Sun, 15 May 2016 17:27:44 +0800 Subject: [PATCH 05/35] =?UTF-8?q?=20-=20add=20:=20=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom/fixeddataengine.py | 8 +- custom/fixedmainengine.py | 3 +- easyquant/main_engine.py | 2 +- easyquant/push_engine/base_engine.py | 3 +- easyquant/push_engine/clock_engine.py | 215 ++++++++++++--- easyquant/strategy/strategyTemplate.py | 1 + .../\347\255\226\347\225\2451_Demo.py" | 14 + unitest_demo.py | 261 +++++++++++++++--- 8 files changed, 427 insertions(+), 80 deletions(-) diff --git a/custom/fixeddataengine.py b/custom/fixeddataengine.py index dc0f1a9..4b69af6 100644 --- a/custom/fixeddataengine.py +++ b/custom/fixeddataengine.py @@ -6,7 +6,7 @@ import easyquotation from easyquant import PushBaseEngine import aiohttp -from easyquant.easydealutils import time as work_time +# from easyquant.easydealutils import time as work_time import time from easyquant.event_engine import Event import multiprocessing as mp @@ -17,16 +17,16 @@ class FixedDataEngine(PushBaseEngine): EventType = 'custom' PushInterval = 15 - def __init__(self, event_engine, watch_stocks=None, s='sina'): + def __init__(self, event_engine, clock_engine, watch_stocks=None, s='sina'): self.watch_stocks = watch_stocks self.s = s self.source = None self.__queue = mp.Queue(1000) - self.is_pause = False if work_time.is_tradetime_now() else True + self.is_pause = not clock_engine.is_tradetime_now() self._control_thread = Thread(target=self._process_control) self._control_thread.start() - super(FixedDataEngine, self).__init__(event_engine) + super(FixedDataEngine, self).__init__(event_engine, clock_engine) def _process_control(self): diff --git a/custom/fixedmainengine.py b/custom/fixedmainengine.py index 091caf7..05ef0d4 100644 --- a/custom/fixedmainengine.py +++ b/custom/fixedmainengine.py @@ -14,7 +14,6 @@ from easyquant.multiprocess.strategy_wrapper import ProcessWrapper - class FixedMainEngine(MainEngine): def __init__(self, broker, need_data='ht.json', quotation_engines=[FixedDataEngine], @@ -37,7 +36,7 @@ def __init__(self, broker, need_data='ht.json', quotation_engines=[FixedDataEngi positions = [p['stock_code'] for p in self.user.position] positions.extend(ext_stocks) for quotation_engine in quotation_engines: - self.quotation_engines.append(quotation_engine(self.event_engine, positions)) + self.quotation_engines.append(quotation_engine(self.event_engine, self.clock_engine, positions)) def load(self, names, strategy_file): with self.lock: diff --git a/easyquant/main_engine.py b/easyquant/main_engine.py index 926fef8..d2f221d 100644 --- a/easyquant/main_engine.py +++ b/easyquant/main_engine.py @@ -44,7 +44,7 @@ def __init__(self, broker, need_data='me.json', quotation_engines=None, quotation_engines = [quotation_engines] self.quotation_engines = [] for quotation_engine in quotation_engines: - self.quotation_engines.append(quotation_engine(self.event_engine)) + self.quotation_engines.append(quotation_engine(self.event_engine, self.clock_engine)) # 保存读取的策略类 self.strategies = OrderedDict() diff --git a/easyquant/push_engine/base_engine.py b/easyquant/push_engine/base_engine.py index 6cb16aa..5bd2084 100644 --- a/easyquant/push_engine/base_engine.py +++ b/easyquant/push_engine/base_engine.py @@ -12,8 +12,9 @@ class BaseEngine: EventType = 'base' PushInterval = 1 - def __init__(self, event_engine): + def __init__(self, event_engine, clock_engine): self.event_engine = event_engine + self.clock_engine = clock_engine self.is_active = True self.quotation_thread = Thread(target=self.push_quotation) self.quotation_thread.setDaemon(False) diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index b76885f..a5563e5 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -4,6 +4,7 @@ import time import arrow from dateutil import tz +from collections import deque from ..easydealutils import time as etime from ..event_engine import Event @@ -15,6 +16,72 @@ def __init__(self, trading_time, clock_event): self.clock_event = clock_event +class ClockIntervalHandler: + def __init__(self, clock_engine, interval, trading=True, call=None): + """ + :param interval: float(minute) + :param trading: 在交易阶段才触发 + :return: + """ + self.clock_engine = clock_engine + self.clock_type = interval + self.interval = interval + self.second = int(interval * 60) + self.trading = trading + self.call = call or (lambda: None) + + def is_active(self): + if self.trading: + if not self.clock_engine.trading_state: + return False + return int(self.clock_engine.now) % self.second == 0 + + def __eq__(self, other): + if isinstance(other, ClockIntervalHandler): + return self.interval == other.interval + else: + return False + + def __hash__(self): + return self.second + + +class ClockMomentHandler: + def __init__(self, clock_engine, clock_type, moment=None, makeup=False, call=None): + """ + :param clock_type: + :param moment: datetime.time + :param makeup: 注册时,如果已经过了触发时机,是否立即触发 + :return: + """ + self.clock_engine = clock_engine + self.clock_type = clock_type + self.moment = moment + self.makeup = makeup + self.call = call or (lambda: None) + self.next_time = datetime.datetime.combine( + self.clock_engine.now_dt.date(), + self.moment, + ) + + if not self.makeup and self.is_active(): + self.update_next_time() + + def update_next_time(self): + """ + 下次激活时间 + :return: + """ + if self.is_active(): + self.next_time = datetime.datetime.combine( + self.next_time.date() + datetime.timedelta(days=1), + self.moment + ) + + def is_active(self): + return self.next_time <= self.clock_engine.now_dt + + class ClockEngine: """ 时间推送引擎 @@ -25,7 +92,6 @@ class ClockEngine: def __init__(self, event_engine, now=None, tzinfo=None): """ :param event_engine: - :param start_time: issubclass(datetime.datetime) or arrow.Arrow 指定开始时间, 测试时可用. :param event_engine: tzinfo :return: """ @@ -33,12 +99,44 @@ def __init__(self, event_engine, now=None, tzinfo=None): 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.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) self.sleep_time = 1 self.trading_state = True if etime.is_tradetime(datetime.datetime.now()) else False + self.clock_moment_handlers = deque() + self.clock_interval_handlers = set() + + self._init_clock_handler() + + def _init_clock_handler(self): + """ + 注册默认的时钟事件 + :return: + """ + + # 开盘事件 + def open_(): + self.trading_state = True + + self._register_moment('open', datetime.time(9, tzinfo=self.tzinfo), True, open_) + + # 中午休市 + self._register_moment('pause', datetime.time(11, 30, tzinfo=self.tzinfo), True) + + # 下午开盘 + self._register_moment('continue', datetime.time(13, tzinfo=self.tzinfo), True) + + # 收盘事件 + def close(): + self.trading_state = False + + self._register_moment('close', datetime.time(15, tzinfo=self.tzinfo), True, close) + + # 间隔事件 + for interval in (0.5, 1, 5, 15, 30, 60): + self.register_interval(interval) def _delta(self, now): if now is None: @@ -65,55 +163,96 @@ def now_dt(self): """ return arrow.get(self.now).to(self.tzinfo) + def reset_now(self, now=None): + self.time_delta = self._delta(now) + def start(self): self.clock_engine_thread.start() def clocktick(self): while self.is_active: - now_time = datetime.datetime.now() - self.tock(now_time) + self.tock() time.sleep(self.sleep_time) - def tock(self, now_time): - """ - :param now_time: datetime.datetime() - :return: - """ - min_seconds = 60 - time_delta = now_time - self.start_time - seconds_delta = int(time_delta.total_seconds()) - - if etime.is_holiday(now_time): + def tock(self): + if etime.is_holiday(self.now_dt): pass # 假日暂停时钟引擎 else: - # 工作日,干活了 - if etime.is_tradetime(now_time): - # 交易时间段 - if self.trading_state is True: - if etime.is_closing(now_time): - self.push_event_type('closing') + self._tock() - for delta in [0.5, 1, 5, 15, 30, 60]: - if seconds_delta % (min_seconds * delta) == 0: - self.push_event_type(delta) + def _tock(self): + # 间隔事件 + for handler in self.clock_interval_handlers: + if handler.is_active(): + handler.call() + self.push_event_type(handler) + # 时刻事件 + while self.clock_moment_handlers: + clock_handler = self.clock_moment_handlers.pop() + if clock_handler.is_active(): + clock_handler.call() + self.push_event_type(clock_handler) + clock_handler.update_next_time() + self.clock_moment_handlers.appendleft(clock_handler) + else: + self.clock_moment_handlers.append(clock_handler) + break - else: - self.trading_state = True - self.push_event_type('open') + # # 工作日,干活了 + # if etime.is_tradetime(now_time): + # # 交易时间段 + # if self.trading_state is True: + # if etime.is_closing(now_time): + # self.push_event_type('closing') + # + # for delta in [0.5, 1, 5, 15, 30, 60]: + # if seconds_delta % (min_seconds * delta) == 0: + # self.push_event_type(delta) + # + # else: + # self.trading_state = True + # self.push_event_type('open') + # + # elif etime.is_pause(now_time): + # self.push_event_type('pause') + # + # elif etime.is_continue(now_time): + # self.push_event_type('continue') + # + # elif self.trading_state is True: + # self.trading_state = False + # self.push_event_type('close') - elif etime.is_pause(now_time): - self.push_event_type('pause') - - elif etime.is_continue(now_time): - self.push_event_type('continue') - - elif self.trading_state is True: - self.trading_state = False - self.push_event_type('close') - - def push_event_type(self, etype): - event = Event(event_type=self.EventType, data=Clock(self.trading_state, etype)) + def push_event_type(self, clock_handler): + event = Event(event_type=self.EventType, data=Clock(self.trading_state, clock_handler.clock_type)) self.event_engine.put(event) def stop(self): self.is_active = False + + def is_tradetime_now(self): + """ + :return: + """ + return etime.is_tradetime(self.now_dt) + + def register_moment(self, clock_type, moment, makeup=False): + return self._register_moment(clock_type, moment, makeup) + + def _register_moment(self, clock_type, moment, makeup=False, call=None): + handlers = list(self.clock_moment_handlers) + handler = ClockMomentHandler(self, clock_type, moment, makeup, call) + handlers.append(handler) + + # 触发事件重新排序 + handlers.sort(key=lambda h: h.next_time, reverse=True) + self.clock_moment_handlers = deque(handlers) + return handler + + def register_interval(self, interval_minute, trading=True): + return self._register_interval(interval_minute, trading) + + def _register_interval(self, interval_minute, trading=True, call=None): + handler = ClockIntervalHandler(self, interval_minute, trading, call) + self.clock_interval_handlers.add(handler) + return handler diff --git a/easyquant/strategy/strategyTemplate.py b/easyquant/strategy/strategyTemplate.py index 26607f4..8e63260 100644 --- a/easyquant/strategy/strategyTemplate.py +++ b/easyquant/strategy/strategyTemplate.py @@ -9,6 +9,7 @@ class StrategyTemplate: def __init__(self, user, log_handler, main_engine): self.user = user self.main_engine = main_engine + self.clock_engine = main_engine.clock_engine # 优先使用自定义 log 句柄, 否则使用主引擎日志句柄 self.log = self.log_handler() or log_handler diff --git "a/strategies/\347\255\226\347\225\2451_Demo.py" "b/strategies/\347\255\226\347\225\2451_Demo.py" index 10f2511..7daac74 100644 --- "a/strategies/\347\255\226\347\225\2451_Demo.py" +++ "b/strategies/\347\255\226\347\225\2451_Demo.py" @@ -1,3 +1,5 @@ +import datetime as dt +from dateutil import tz from easyquant import DefaultLogHandler from easyquant import StrategyTemplate @@ -5,6 +7,18 @@ class Strategy(StrategyTemplate): name = '测试策略1' + def init(self): + now = self.clock_engine.now_dt + + # 注册时钟事件 + clock_type = "盘尾" + moment = dt.time(14, 56, 30, tzinfo=tz.tzlocal()) + self.clock_engine.register_moment(clock_type, moment) + + # 注册时钟间隔事件, 不在交易阶段也会触发, clock_type == minute_interval + minute_interval = 1.5 + self.clock_engine.register_interval(minute_interval, trading=False) + def strategy(self, event): """:param event event.data 为所有股票的信息,结构如下 {'162411': diff --git a/unitest_demo.py b/unitest_demo.py index 237e04e..f37ff6c 100644 --- a/unitest_demo.py +++ b/unitest_demo.py @@ -3,12 +3,12 @@ 演示如何进行单元测试 """ import time -import arrow import unittest import datetime +import threading from dateutil import tz from easyquant.main_engine import MainEngine -from easyquant.push_engine.clock_engine import ClockEngine +from easyquant.push_engine.clock_engine import ClockEngine, ClockMomentHandler from easyquant.event_engine import EventEngine __author__ = 'Shawn' @@ -47,8 +47,12 @@ def setUp(self): 执行每个单元测试 前 都要执行的逻辑 :return: """ + self.trading_date = datetime.date(2016, 5, 5) + self.time = datetime.time(0, 0, 0, tzinfo=tz.tzlocal()) + + now = datetime.datetime.combine(self.trading_date, self.time) # 此处重新定义 main_engine - self._main_engine = MainEngine('ht') + self._main_engine = MainEngine('ht', now=now) # 设置为不在交易中 self.clock_engine.trading_state = False @@ -66,12 +70,200 @@ def test_set_now(self): """ tzinfo = tz.tzlocal() - now = datetime.datetime(2016, 5, 5, 8, 59, 00, tzinfo) + now = datetime.datetime.combine( + self.trading_date, + datetime.time(8, 59, 00, tzinfo=tzinfo), + ) clock_engien = ClockEngine(EventEngine(), now, 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.trading_date, + datetime.time(8, 59, 00, tzinfo=tzinfo), + ) + clock_engien.reset_now(now) + + # 去掉微秒误差后比较 + self.assertEqual(clock_engien.now_dt.replace(microsecond=0), now) + + # 重设为当前时间 + clock_engien.reset_now() + now = datetime.datetime.now(tzinfo).replace(microsecond=0) + + # 去掉微秒误差后比较 + self.assertEqual(clock_engien.now_dt.replace(microsecond=0), now) + + def test_clock_moment_is_active(self): + # 设置时间 + now = datetime.datetime.combine( + self.trading_date, + datetime.time(23, 59, 58, tzinfo=tz.tzlocal()), + ) + self.clock_engine.reset_now(now) + + # 触发前, 注册时间事件 + moment = datetime.time(23, 59, 59, tzinfo=tz.tzlocal()) + cmh = ClockMomentHandler(self.clock_engine, 'test', moment) + # 确认未触发 + self.assertFalse(cmh.is_active()) + + # 将系统时间设置为触发时间 + now = datetime.datetime.combine( + self.trading_date, + datetime.time(23, 59, 59, tzinfo=tz.tzlocal()) + ) + self.clock_engine.reset_now(now) + # 确认触发 + self.assertTrue(cmh.is_active()) + + def test_clock_update_next_time(self): + # 设置时间 + now = datetime.datetime.combine( + self.trading_date, + datetime.time(23, 59, 58, tzinfo=tz.tzlocal()) + ) + self.clock_engine.reset_now(now) + + # 触发前, 注册时间事件 + moment = datetime.time(23, 59, 59, tzinfo=tz.tzlocal()) + cmh = ClockMomentHandler(self.clock_engine, 'test', moment) + # 确认未触发 + self.assertFalse(cmh.is_active()) + + # 将系统时间设置为触发时间 + now = datetime.datetime.combine( + self.trading_date, + datetime.time(23, 59, 59, tzinfo=tz.tzlocal()) + ) + self.clock_engine.reset_now(now) + # 确认触发 + self.assertTrue(cmh.is_active()) + + # 更新下次触发时间 + cmh.update_next_time() + # 确认未触发 + self.assertFalse(cmh.is_active()) + + def test_register_clock_moment_makeup(self): + # 测试补发 + self.register_clock_moent_makeup(True) + + def test_register_clock_moment_not_makeup(self): + # 测试不补发 + self.register_clock_moent_makeup(False) + + def register_clock_moent_makeup(self, makeup): + begin = datetime.datetime.combine( + self.trading_date, + datetime.time(23, 59, 59, tzinfo=tz.tzlocal()) + ) + self.clock_engine.reset_now(begin) + + # 注册时刻一个超时事件 + moment = datetime.time(0, 0, 0, tzinfo=tz.tzlocal()) + self.clock_engine.register_moment('test', moment, makeup=makeup) + + self.test_active = False + + def clock(event): + # 记录补发 + if event.data.clock_event == 'test': + self.test_active = True + + self.main_engine.event_engine.register(ClockEngine.EventType, clock) + + # 开启事件引擎 + self.main_engine.event_engine.start() + self.clock_engine.tock() + + time.sleep(0.1) + self.main_engine.event_engine.stop() + + # 确认补发 + self.assertEqual(self.test_active, makeup) + + def test_register_clock_interval_trading_true(self): + # 交易触发, 交易阶段 + trading = True + begin = datetime.datetime.combine( + self.trading_date, + datetime.time(9, 15, 0, tzinfo=tz.tzlocal()) + ) + # 确认在交易中 + self.register_clock_interval(begin, trading, 1) + self.assertTrue(self.clock_engine.trading_state) + + def test_register_clock_interval_not_trading_true(self): + # 交易触发, 非交易阶段 + trading = True + begin = datetime.datetime.combine( + self.trading_date, + datetime.time(15, 15, 0, tzinfo=tz.tzlocal()) + ) + # 确认在交易中 + self.register_clock_interval(begin, trading, 0) + self.assertFalse(self.clock_engine.trading_state) + + def test_register_clock_interval_trading_false(self): + # 非交易触发, 交易阶段 + trading = False + begin = datetime.datetime.combine( + self.trading_date, + datetime.time(9, 15, 0, tzinfo=tz.tzlocal()) + ) + # 确认在交易中 + self.register_clock_interval(begin, trading, 1) + self.assertTrue(self.clock_engine.trading_state) + + def test_register_clock_interval_not_trading_false(self): + # 非交易触发, 非交易阶段 + trading = False + begin = datetime.datetime.combine( + self.trading_date, + datetime.time(15, 15, 0, tzinfo=tz.tzlocal()) + ) + # 确认在交易中 + self.register_clock_interval(begin, trading, 1) + self.assertFalse(self.clock_engine.trading_state) + + def register_clock_interval(self, begin, trading, active_times): + self.clock_engine.reset_now(begin) + self.active_times = 0 + + def clock(event): + # 记录补发 + if event.data.clock_event == clock_type: + self.active_times += 1 + self.main_engine.event_engine.register(ClockEngine.EventType, clock) + + self.main_engine.event_engine.start() + self.clock_engine.tock() + + clock_type = minute_interval = 2.5 + # 注册事件 + handler = self.clock_engine.register_interval(minute_interval, trading) + # 确定已经添加到列表 + self.assertIn(handler, self.clock_engine.clock_interval_handlers) + + # 开启事件引擎 + for sec in range(int(minute_interval * 60)): + now = begin + datetime.timedelta(seconds=sec) + self.clock_engine.reset_now(now) + self.clock_engine.tock() + time.sleep(1) + self.main_engine.event_engine.stop() + + self.assertEqual(self.active_times, active_times) + def test_tick(self): """ 测试时钟接口 @@ -80,24 +272,23 @@ def test_tick(self): """ # 各个时间间隔的触发次数计数 counts = { - 0.5: 0, - 1: 0, - 5: 0, - 15: 0, - 30: 0, - 60: 0, - "open": 0, - "pause": 0, - "continue": 0, - "closing": 0, - "close": 0, + 0.5: [], + 1: [], + 5: [], + 15: [], + 30: [], + 60: [], + "open": [], + "pause": [], + "continue": [], + "close": [], } def count(event): # 时钟引擎必定在上述的类型中 self.assertIn(event.data.clock_event, counts) # 计数 - counts[event.data.clock_event] += 1 + counts[event.data.clock_event].append(self.clock_engine.now_dt) # 注册一个响应时钟事件的函数 self.main_engine.event_engine.register(ClockEngine.EventType, count) @@ -106,30 +297,32 @@ def count(event): self.main_engine.event_engine.start() # 模拟从开市前1分钟, 即8:59分, 到休市后1分钟的每秒传入时钟接口 - begin = datetime.datetime(2016, 5, 5, 8, 59) + begin = datetime.datetime.combine( + self.trading_date, + datetime.time(8, 59, tzinfo=self.clock_engine.tzinfo) + ) hours = 15 - 9 mins = hours * 60 + 2 seconds = 60 * mins for secs in range(seconds): - now_time = begin + datetime.timedelta(seconds=secs) - self.clock_engine.tock(now_time) + now = begin + datetime.timedelta(seconds=secs) + self.clock_engine.reset_now(now) + self.clock_engine.tock() + time.sleep(0.001) # 等待事件引擎处理 - time.sleep(10) - print(counts) + for k, v in counts.items(): + print(k, [d.strftime("%H:%M:%S") for d in v]) self.main_engine.event_engine.stop() + # 开盘收盘, 中午开盘休盘, 必定会触发1次 + self.assertEqual(len(counts['open']), 1) + self.assertEqual(len(counts['pause']), 1) + self.assertEqual(len(counts['continue']), 1) + self.assertEqual(len(counts['close']), 1) + # 核对次数, 休市的时候不会统计 - self.assertEqual(counts[60], 15 - 9 + 1 - len(["9:00", "12:00", "15:00"])) - self.assertEqual(counts[30], (15 - 9) * 2 + 1 - len(["9:00", "11:30", "12:00", "12:30", "15:00"])) - self.assertEqual(counts[15], - (15 - 9) * 4 + 1 - len(["9:00", "9:15", "11:30", "11:45", "12:00", "12:15", "12:30", - "12:45", "15:00"])) - - # 开盘收盘, 中午开盘休盘, 必定会触发1次, 如果报错,说明是因为当前时间处于非交易日 - self.assertEqual(counts['open'], 1) - self.assertEqual(counts['closing'], 330) - self.assertEqual(counts['close'], 1) - # 目前的时钟引擎会一直推送 pause 和 continue - self.assertEqual(counts['pause'], 5370) - self.assertEqual(counts['continue'], 30) + self.assertEqual(len(counts[60]), 15 - 9 + 1 - len(["9:00"])) + self.assertEqual(len(counts[30]), (15 - 9) * 2 + 1 - len(["9:00", "11:30", "12:00", "12:30", "15:00"])) + self.assertEqual(len(counts[15]), (15 - 9) * 4 + 1 - + len(["9:00", "9:15", "11:30", "11:45", "12:00", "12:15", "12:30", "12:45", "15:00"])) \ No newline at end of file From 712bd36cd0f8c426fe238df591d5eb1de33e6b07 Mon Sep 17 00:00:00 2001 From: lamter Date: Sun, 15 May 2016 17:35:46 +0800 Subject: [PATCH 06/35] =?UTF-8?q?=20-=20change=20:=20=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E6=8E=89=E6=97=A0=E7=94=A8=E7=9A=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unitest_demo.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/unitest_demo.py b/unitest_demo.py index f37ff6c..366e129 100644 --- a/unitest_demo.py +++ b/unitest_demo.py @@ -243,6 +243,7 @@ def clock(event): # 记录补发 if event.data.clock_event == clock_type: self.active_times += 1 + self.main_engine.event_engine.register(ClockEngine.EventType, clock) self.main_engine.event_engine.start() @@ -311,8 +312,6 @@ def count(event): time.sleep(0.001) # 等待事件引擎处理 - for k, v in counts.items(): - print(k, [d.strftime("%H:%M:%S") for d in v]) self.main_engine.event_engine.stop() # 开盘收盘, 中午开盘休盘, 必定会触发1次 @@ -323,6 +322,6 @@ def count(event): # 核对次数, 休市的时候不会统计 self.assertEqual(len(counts[60]), 15 - 9 + 1 - len(["9:00"])) - self.assertEqual(len(counts[30]), (15 - 9) * 2 + 1 - len(["9:00", "11:30", "12:00", "12:30", "15:00"])) + self.assertEqual(len(counts[30]), (15 - 9) * 2 + 1 - len(["9:00"])) self.assertEqual(len(counts[15]), (15 - 9) * 4 + 1 - - len(["9:00", "9:15", "11:30", "11:45", "12:00", "12:15", "12:30", "12:45", "15:00"])) \ No newline at end of file + len(["9:00"])) From 7257ffdffed5f22f62b0c808f50c3bb0418b1e70 Mon Sep 17 00:00:00 2001 From: shidenggui Date: Sun, 15 May 2016 23:46:27 +0800 Subject: [PATCH 07/35] refactor --- custom/fixeddataengine.py | 11 ++++---- custom/fixedmainengine.py | 38 ++++++++++++------------- easyquant/main_engine.py | 2 +- easyquant/push_engine/clock_engine.py | 40 ++++++--------------------- requirements.txt | 1 + 5 files changed, 35 insertions(+), 57 deletions(-) diff --git a/custom/fixeddataengine.py b/custom/fixeddataengine.py index 4b69af6..36ed927 100644 --- a/custom/fixeddataengine.py +++ b/custom/fixeddataengine.py @@ -3,14 +3,15 @@ # __author__ = 'keping.chu' -import easyquotation -from easyquant import PushBaseEngine +import multiprocessing as mp +from threading import Thread + import aiohttp -# from easyquant.easydealutils import time as work_time +import easyquotation + import time +from easyquant import PushBaseEngine from easyquant.event_engine import Event -import multiprocessing as mp -from threading import Thread class FixedDataEngine(PushBaseEngine): diff --git a/custom/fixedmainengine.py b/custom/fixedmainengine.py index 05ef0d4..ca0560a 100644 --- a/custom/fixedmainengine.py +++ b/custom/fixedmainengine.py @@ -3,35 +3,35 @@ # __author__ = 'keping.chu' -from easyquant.main_engine import MainEngine -from easyquant.log_handler.default_handler import DefaultLogHandler -from .fixeddataengine import FixedDataEngine -from easyquant.push_engine.clock_engine import ClockEngine -import os -import time import importlib +import os from threading import Thread, Lock +import time +from easyquant.log_handler.default_handler import DefaultLogHandler +from easyquant.main_engine import MainEngine from easyquant.multiprocess.strategy_wrapper import ProcessWrapper +from easyquant.push_engine.clock_engine import ClockEngine +from .fixeddataengine import FixedDataEngine -class FixedMainEngine(MainEngine): +class FixedMainEngine(MainEngine): def __init__(self, broker, need_data='ht.json', quotation_engines=[FixedDataEngine], log_handler=DefaultLogHandler(), ext_stocks=[]): super(FixedMainEngine, self).__init__(broker, need_data, [], log_handler) if type(quotation_engines) != list: quotation_engines = [quotation_engines] self.quotation_engines = [] - #修改时间缓存 + # 修改时间缓存 self._cache = {} - #文件进程映射 + # 文件进程映射 self._process_map = {} - #文件模块映射 + # 文件模块映射 self._modules = {} self._names = None - #加载锁 + # 加载锁 self.lock = Lock() - #加载线程 + # 加载线程 self._watch_thread = Thread(target=self._load_strategy) positions = [p['stock_code'] for p in self.user.position] positions.extend(ext_stocks) @@ -42,20 +42,20 @@ def load(self, names, strategy_file): with self.lock: mtime = os.path.getmtime(os.path.join('strategies', strategy_file)) - #是否需要重新加载 + # 是否需要重新加载 reload = False strategy_module_name = os.path.basename(strategy_file)[:-3] if self._cache.get(strategy_file, None) == mtime: return elif self._cache.get(strategy_file, None) is not None: - #原有进程退出 + # 原有进程退出 _process = self._process_map.get(strategy_file) self.unbind_event(_process) _process.stop() self.log.info(u'卸载策略: %s' % strategy_module_name) time.sleep(2) reload = True - #重新加载 + # 重新加载 if reload: strategy_module = importlib.reload(self._modules[strategy_file]) else: @@ -65,9 +65,9 @@ def load(self, names, strategy_file): strategy_class = getattr(strategy_module, 'Strategy') if names is None or strategy_class.name in names: self.strategies[strategy_module_name] = strategy_class - #进程包装 + # 进程包装 _process = ProcessWrapper(strategy_class(self.user, log_handler=self.log, main_engine=self)) - #缓存加载信息 + # 缓存加载信息 self._process_map[strategy_file] = _process self.strategy_list.append(_process) self._cache[strategy_file] = mtime @@ -100,7 +100,7 @@ def load_strategy(self, names=None): importlib.import_module(s_folder) for strategy_file in strategies: self.load(self._names, strategy_file) - #如果线程没有启动,就启动策略监视线程 + # 如果线程没有启动,就启动策略监视线程 if not self._watch_thread.is_alive(): self._watch_thread.start() @@ -110,4 +110,4 @@ def _load_strategy(self): self.load_strategy(self._names) time.sleep(2) except Exception as e: - print(e) \ No newline at end of file + print(e) diff --git a/easyquant/main_engine.py b/easyquant/main_engine.py index 091d93a..0a9e164 100644 --- a/easyquant/main_engine.py +++ b/easyquant/main_engine.py @@ -1,8 +1,8 @@ import importlib import os +import pathlib import sys from collections import OrderedDict -import pathlib import easytrader from logbook import Logger, StreamHandler diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index a5563e5..d9fede9 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -1,11 +1,12 @@ # coding: utf-8 import datetime +from collections import deque from threading import Thread -import time + import arrow from dateutil import tz -from collections import deque +import time from ..easydealutils import time as etime from ..event_engine import Event @@ -60,8 +61,8 @@ def __init__(self, clock_engine, clock_type, moment=None, makeup=False, call=Non self.makeup = makeup self.call = call or (lambda: None) self.next_time = datetime.datetime.combine( - self.clock_engine.now_dt.date(), - self.moment, + self.clock_engine.now_dt.date(), + self.moment, ) if not self.makeup and self.is_active(): @@ -74,8 +75,8 @@ def update_next_time(self): """ if self.is_active(): self.next_time = datetime.datetime.combine( - self.next_time.date() + datetime.timedelta(days=1), - self.moment + self.next_time.date() + datetime.timedelta(days=1), + self.moment ) def is_active(self): @@ -143,7 +144,7 @@ def _delta(self, now): 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, + now.year, now.month, now.day, now.hour, now.minute, now.second, now.microsecond, self.tzinfo, )) return (arrow.now() - now).total_seconds() @@ -198,31 +199,6 @@ def _tock(self): self.clock_moment_handlers.append(clock_handler) break - # # 工作日,干活了 - # if etime.is_tradetime(now_time): - # # 交易时间段 - # if self.trading_state is True: - # if etime.is_closing(now_time): - # self.push_event_type('closing') - # - # for delta in [0.5, 1, 5, 15, 30, 60]: - # if seconds_delta % (min_seconds * delta) == 0: - # self.push_event_type(delta) - # - # else: - # self.trading_state = True - # self.push_event_type('open') - # - # elif etime.is_pause(now_time): - # self.push_event_type('pause') - # - # elif etime.is_continue(now_time): - # self.push_event_type('continue') - # - # elif self.trading_state is True: - # self.trading_state = False - # self.push_event_type('close') - def push_event_type(self, clock_handler): event = Event(event_type=self.EventType, data=Clock(self.trading_state, clock_handler.clock_type)) self.event_engine.put(event) diff --git a/requirements.txt b/requirements.txt index 3d48021..be6b188 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ aiohttp easytrader easyquotation arrow +dateutil \ No newline at end of file From e9248b5e6b6d2a885db97cbb4a4184791a9cc84d Mon Sep 17 00:00:00 2001 From: Joey Jiao Date: Fri, 20 May 2016 16:55:21 +0800 Subject: [PATCH 08/35] Fix requirements --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index be6b188..3d48021 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,3 @@ aiohttp easytrader easyquotation arrow -dateutil \ No newline at end of file From 5ef74afb363153172027bf1bc4a9b850a7e93268 Mon Sep 17 00:00:00 2001 From: Joey Jiao Date: Fri, 20 May 2016 19:55:21 +0800 Subject: [PATCH 09/35] Add GF choose --- test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test.py b/test.py index 137f0fd..9738c99 100644 --- a/test.py +++ b/test.py @@ -6,7 +6,7 @@ print('easyquant 测试 DEMO') print('请输入你使用的券商:') -choose = input('1: 华泰 2: 佣金宝 3: 银河 4: 雪球模拟组合\n:') +choose = input('1: 华泰 2: 佣金宝 3: 银河 4: 雪球模拟组合 5: 广发\n:') broker = 'ht' if choose == '2': @@ -15,6 +15,8 @@ broker = 'yh' elif choose == '4': broker = 'xq' +elif choose == '5': + broker = 'gf' def get_broker_need_data(choose_broker): From 6909c66021a0409290843c2158f8eccaf33b34f2 Mon Sep 17 00:00:00 2001 From: lamter Date: Sun, 22 May 2016 14:45:48 +0800 Subject: [PATCH 10/35] =?UTF-8?q?=20-=20fix=20:=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=97=B6=E9=92=9F=E4=BA=8B=E4=BB=B6=E8=A7=A6=E5=8F=91=E7=9A=84?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=EF=BC=9A=E4=BA=A4=E6=98=93=E6=97=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easyquant/easydealutils/time.py | 1 + easyquant/push_engine/clock_engine.py | 32 +++++++++++++++------ unitest_demo.py | 40 +++++++++++++++++---------- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/easyquant/easydealutils/time.py b/easyquant/easydealutils/time.py index 8ecbba9..0a8147f 100644 --- a/easyquant/easydealutils/time.py +++ b/easyquant/easydealutils/time.py @@ -7,6 +7,7 @@ @lru_cache() def _is_holiday(day): + # 该接口可能将于 2016.7.1 过期, 请关注该主页 api = 'http://www.easybots.cn/api/holiday.php' params = {'d': day} rep = requests.get(api, params) diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index d9fede9..27b63f8 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -32,6 +32,11 @@ def __init__(self, clock_engine, interval, trading=True, call=None): self.call = call or (lambda: None) def is_active(self): + # TODO 测试代码 + if self.clock_type == 60: + if int(self.clock_engine.now) % self.second == 0: + print(self.trading, self.clock_engine.now_dt, self.clock_engine.trading_state) + if self.trading: if not self.clock_engine.trading_state: return False @@ -48,16 +53,18 @@ def __hash__(self): class ClockMomentHandler: - def __init__(self, clock_engine, clock_type, moment=None, makeup=False, call=None): + def __init__(self, clock_engine, clock_type, moment=None, is_trading_date=True, makeup=False, call=None): """ :param clock_type: :param moment: datetime.time + :param is_trading_date: bool(是否只有在交易日触发) :param makeup: 注册时,如果已经过了触发时机,是否立即触发 :return: """ self.clock_engine = clock_engine self.clock_type = clock_type self.moment = moment + self.is_trading_date = is_trading_date self.makeup = makeup self.call = call or (lambda: None) self.next_time = datetime.datetime.combine( @@ -80,6 +87,10 @@ def update_next_time(self): ) def is_active(self): + if self.is_trading_date and etime.is_holiday(self.clock_engine.now_dt): + # 仅在交易日触发时的判断 + return False + return self.next_time <= self.clock_engine.now_dt @@ -118,22 +129,22 @@ def _init_clock_handler(self): """ # 开盘事件 - def open_(): + def _open(): self.trading_state = True - self._register_moment('open', datetime.time(9, tzinfo=self.tzinfo), True, open_) + self._register_moment('open', datetime.time(9, tzinfo=self.tzinfo), makeup=True, call=_open) # 中午休市 - self._register_moment('pause', datetime.time(11, 30, tzinfo=self.tzinfo), True) + self._register_moment('pause', datetime.time(11, 30, tzinfo=self.tzinfo), makeup=True) # 下午开盘 - self._register_moment('continue', datetime.time(13, tzinfo=self.tzinfo), True) + self._register_moment('continue', datetime.time(13, tzinfo=self.tzinfo), makeup=True) # 收盘事件 def close(): self.trading_state = False - self._register_moment('close', datetime.time(15, tzinfo=self.tzinfo), True, close) + self._register_moment('close', datetime.time(15, tzinfo=self.tzinfo), makeup=True, call=close) # 间隔事件 for interval in (0.5, 1, 5, 15, 30, 60): @@ -165,6 +176,11 @@ 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 start(self): @@ -215,9 +231,9 @@ def is_tradetime_now(self): def register_moment(self, clock_type, moment, makeup=False): return self._register_moment(clock_type, moment, makeup) - def _register_moment(self, clock_type, moment, makeup=False, call=None): + def _register_moment(self, clock_type, moment, is_trading_date=True, makeup=False, call=None): handlers = list(self.clock_moment_handlers) - handler = ClockMomentHandler(self, clock_type, moment, makeup, call) + handler = ClockMomentHandler(self, clock_type, moment, is_trading_date, makeup, call) handlers.append(handler) # 触发事件重新排序 diff --git a/unitest_demo.py b/unitest_demo.py index 366e129..8681b71 100644 --- a/unitest_demo.py +++ b/unitest_demo.py @@ -5,7 +5,9 @@ import time import unittest import datetime -import threading +import pandas as pd +from easyquant.easydealutils.time import is_holiday + from dateutil import tz from easyquant.main_engine import MainEngine from easyquant.push_engine.clock_engine import ClockEngine, ClockMomentHandler @@ -47,10 +49,18 @@ def setUp(self): 执行每个单元测试 前 都要执行的逻辑 :return: """ - self.trading_date = datetime.date(2016, 5, 5) + # 设定下一个交易日 + self.trade_date = None + for date in pd.date_range(datetime.date.today(), periods=10): + if not is_holiday(date): + self.trade_date = date + break + else: + raise ValueError("无法获得下一个交易日") + print(1212, self.trade_date) self.time = datetime.time(0, 0, 0, tzinfo=tz.tzlocal()) - now = datetime.datetime.combine(self.trading_date, self.time) + now = datetime.datetime.combine(self.trade_date, self.time) # 此处重新定义 main_engine self._main_engine = MainEngine('ht', now=now) @@ -71,7 +81,7 @@ def test_set_now(self): tzinfo = tz.tzlocal() now = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(8, 59, 00, tzinfo=tzinfo), ) clock_engien = ClockEngine(EventEngine(), now, tzinfo) @@ -87,7 +97,7 @@ def test_reset_now(self): tzinfo = tz.tzlocal() clock_engien = ClockEngine(EventEngine()) now = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(8, 59, 00, tzinfo=tzinfo), ) clock_engien.reset_now(now) @@ -105,7 +115,7 @@ def test_reset_now(self): def test_clock_moment_is_active(self): # 设置时间 now = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(23, 59, 58, tzinfo=tz.tzlocal()), ) self.clock_engine.reset_now(now) @@ -118,7 +128,7 @@ def test_clock_moment_is_active(self): # 将系统时间设置为触发时间 now = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(23, 59, 59, tzinfo=tz.tzlocal()) ) self.clock_engine.reset_now(now) @@ -128,7 +138,7 @@ def test_clock_moment_is_active(self): def test_clock_update_next_time(self): # 设置时间 now = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(23, 59, 58, tzinfo=tz.tzlocal()) ) self.clock_engine.reset_now(now) @@ -141,7 +151,7 @@ def test_clock_update_next_time(self): # 将系统时间设置为触发时间 now = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(23, 59, 59, tzinfo=tz.tzlocal()) ) self.clock_engine.reset_now(now) @@ -163,7 +173,7 @@ def test_register_clock_moment_not_makeup(self): def register_clock_moent_makeup(self, makeup): begin = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(23, 59, 59, tzinfo=tz.tzlocal()) ) self.clock_engine.reset_now(begin) @@ -195,7 +205,7 @@ def test_register_clock_interval_trading_true(self): # 交易触发, 交易阶段 trading = True begin = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(9, 15, 0, tzinfo=tz.tzlocal()) ) # 确认在交易中 @@ -206,7 +216,7 @@ def test_register_clock_interval_not_trading_true(self): # 交易触发, 非交易阶段 trading = True begin = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(15, 15, 0, tzinfo=tz.tzlocal()) ) # 确认在交易中 @@ -217,7 +227,7 @@ def test_register_clock_interval_trading_false(self): # 非交易触发, 交易阶段 trading = False begin = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(9, 15, 0, tzinfo=tz.tzlocal()) ) # 确认在交易中 @@ -228,7 +238,7 @@ def test_register_clock_interval_not_trading_false(self): # 非交易触发, 非交易阶段 trading = False begin = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(15, 15, 0, tzinfo=tz.tzlocal()) ) # 确认在交易中 @@ -299,7 +309,7 @@ def count(event): # 模拟从开市前1分钟, 即8:59分, 到休市后1分钟的每秒传入时钟接口 begin = datetime.datetime.combine( - self.trading_date, + self.trade_date, datetime.time(8, 59, tzinfo=self.clock_engine.tzinfo) ) hours = 15 - 9 From 4bfaa10eaa4e3f97d48cf060a5b3e81c83123f0f Mon Sep 17 00:00:00 2001 From: lamter Date: Sun, 22 May 2016 14:52:16 +0800 Subject: [PATCH 11/35] =?UTF-8?q?=20-=20change=20:=20=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easyquant/push_engine/clock_engine.py | 5 ----- unitest_demo.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index 27b63f8..df1d26e 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -32,11 +32,6 @@ def __init__(self, clock_engine, interval, trading=True, call=None): self.call = call or (lambda: None) def is_active(self): - # TODO 测试代码 - if self.clock_type == 60: - if int(self.clock_engine.now) % self.second == 0: - print(self.trading, self.clock_engine.now_dt, self.clock_engine.trading_state) - if self.trading: if not self.clock_engine.trading_state: return False diff --git a/unitest_demo.py b/unitest_demo.py index 8681b71..d22c68b 100644 --- a/unitest_demo.py +++ b/unitest_demo.py @@ -57,7 +57,7 @@ def setUp(self): break else: raise ValueError("无法获得下一个交易日") - print(1212, self.trade_date) + self.time = datetime.time(0, 0, 0, tzinfo=tz.tzlocal()) now = datetime.datetime.combine(self.trade_date, self.time) From 938753a1ebba862b14b2baa2a9f917932930bca1 Mon Sep 17 00:00:00 2001 From: lamter Date: Sun, 22 May 2016 16:13:31 +0800 Subject: [PATCH 12/35] =?UTF-8?q?=20-=20fix=20:=20=E5=AF=B9=20interval=20c?= =?UTF-8?q?lock=20event=20=E5=92=8C=20moment=20clock=20event=20=E5=88=86?= =?UTF-8?q?=E5=BC=80=E5=81=9A=20tick=20=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easyquant/easydealutils/time.py | 55 +++++++------ easyquant/push_engine/clock_engine.py | 9 ++- unitest_demo.py | 110 +++++++++++++++++++------- 3 files changed, 119 insertions(+), 55 deletions(-) diff --git a/easyquant/easydealutils/time.py b/easyquant/easydealutils/time.py index 0a8147f..046f90d 100644 --- a/easyquant/easydealutils/time.py +++ b/easyquant/easydealutils/time.py @@ -1,5 +1,5 @@ import datetime -from datetime import timedelta +import doctest from functools import lru_cache import requests @@ -20,6 +20,33 @@ def is_holiday(now_time): return _is_holiday(today) +def is_trade_date(now_time): + return not is_holiday(now_time) + + +def get_next_trade_date(now_time): + """ + :param now_time: datetime.datetime + :return: + >>> import datetime + >>> get_next_trade_date(datetime.date(2016, 5, 5)) + datetime.date(2016, 5, 6) + """ + now = now_time + max_days = 365 + days = 0 + while 1: + days += 1 + now += datetime.timedelta(days=1) + if is_trade_date(now): + if isinstance(now, datetime.date): + return now + else: + return now.date() + if days > max_days: + raise ValueError('无法确定 %s 下一个交易日' % now_time) + + OPEN_TIME = ( (datetime.time(9, 15, 0), datetime.time(11, 30, 0)), (datetime.time(13, 0, 0), datetime.time(15, 0, 0)), @@ -80,27 +107,5 @@ def is_closing(now_time, start=datetime.time(14, 54, 30)): return True return False - -# def calc_next_trade_time_delta_seconds(): -# now_time = datetime.datetime.now() -# now = (now_time.hour, now_time.minute, now_time.second) -# if now < (9, 15, 0): -# next_trade_start = now_time.replace(hour=9, minute=15, second=0, microsecond=0) -# elif (12, 0, 0) < now < (13, 0, 0): -# next_trade_start = now_time.replace(hour=13, minute=0, second=0, microsecond=0) -# elif now > (15, 0, 0): -# distance_next_work_day = 1 -# while True: -# target_day = now_time + timedelta(days=distance_next_work_day) -# if is_holiday(target_day.strftime('%Y%m%d')): -# distance_next_work_day += 1 -# else: -# break -# -# day_delta = timedelta(days=distance_next_work_day) -# next_trade_start = (now_time + day_delta).replace(hour=9, minute=15, -# second=0, microsecond=0) -# else: -# return 0 -# time_delta = next_trade_start - now_time -# return time_delta.total_seconds() +if __name__ == "__main__": + doctest.testmod() \ No newline at end of file diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index df1d26e..43daf83 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -3,6 +3,7 @@ from collections import deque from threading import Thread +import pandas as pd import arrow from dateutil import tz @@ -76,8 +77,13 @@ def update_next_time(self): :return: """ if self.is_active(): + if self.is_trading_date: + next_date = etime.get_next_trade_date(self.clock_engine.now_dt) + else: + next_date = self.next_time.date() + datetime.timedelta(days=1) + self.next_time = datetime.datetime.combine( - self.next_time.date() + datetime.timedelta(days=1), + next_date, self.moment ) @@ -85,7 +91,6 @@ def is_active(self): if self.is_trading_date and etime.is_holiday(self.clock_engine.now_dt): # 仅在交易日触发时的判断 return False - return self.next_time <= self.clock_engine.now_dt diff --git a/unitest_demo.py b/unitest_demo.py index d22c68b..9f6ed9a 100644 --- a/unitest_demo.py +++ b/unitest_demo.py @@ -6,7 +6,7 @@ import unittest import datetime import pandas as pd -from easyquant.easydealutils.time import is_holiday +from easyquant.easydealutils.time import get_next_trade_date, is_trade_date from dateutil import tz from easyquant.main_engine import MainEngine @@ -50,13 +50,7 @@ def setUp(self): :return: """ # 设定下一个交易日 - self.trade_date = None - for date in pd.date_range(datetime.date.today(), periods=10): - if not is_holiday(date): - self.trade_date = date - break - else: - raise ValueError("无法获得下一个交易日") + self.trade_date = get_next_trade_date(datetime.date.today()) self.time = datetime.time(0, 0, 0, tzinfo=tz.tzlocal()) @@ -67,6 +61,21 @@ def setUp(self): # 设置为不在交易中 self.clock_engine.trading_state = False + # 时钟事件计数 + self.counts = { + 0.5: [], + 1: [], + 5: [], + 15: [], + 30: [], + 60: [], + "open": [], + "pause": [], + "continue": [], + "close": [], + + } + def tearDown(self): """ 执行每个单元测试 后 都要执行的逻辑 @@ -275,25 +284,15 @@ def clock(event): self.assertEqual(self.active_times, active_times) - def test_tick(self): + def test_tick_interval_event(self): """ - 测试时钟接口 + 测试 tick 中的时间间隔事件 + 时间间隔事件 从开始前1分钟一直到收市后1分钟, 触发所有的已定义时钟事件 :return: """ # 各个时间间隔的触发次数计数 - counts = { - 0.5: [], - 1: [], - 5: [], - 15: [], - 30: [], - 60: [], - "open": [], - "pause": [], - "continue": [], - "close": [], - } + counts = self.counts def count(event): # 时钟引擎必定在上述的类型中 @@ -324,14 +323,69 @@ def count(event): # 等待事件引擎处理 self.main_engine.event_engine.stop() - # 开盘收盘, 中午开盘休盘, 必定会触发1次 - self.assertEqual(len(counts['open']), 1) - self.assertEqual(len(counts['pause']), 1) - self.assertEqual(len(counts['continue']), 1) - self.assertEqual(len(counts['close']), 1) - # 核对次数, 休市的时候不会统计 self.assertEqual(len(counts[60]), 15 - 9 + 1 - len(["9:00"])) self.assertEqual(len(counts[30]), (15 - 9) * 2 + 1 - len(["9:00"])) self.assertEqual(len(counts[15]), (15 - 9) * 4 + 1 - len(["9:00"])) + + + def test_tick_moment_event(self): + """ + 测试 tick 中的时刻时钟事件 + 时间间隔事件 + 每隔25分钟触发一次,连续进行8天 + :return: + """ + # 各个时间间隔的触发次数计数 + counts = self.counts + days = 8 + interval = datetime.timedelta(minutes=25) + + def count(event): + # 时钟引擎必定在上述的类型中 + self.assertIn(event.data.clock_event, counts) + # 计数 + counts[event.data.clock_event].append(self.clock_engine.now_dt) + + # 从 self.trade_date 的零点开始 + begin = datetime.datetime.combine( + self.trade_date, + datetime.time(0, 0, tzinfo=self.clock_engine.tzinfo) + ) + # 结束时间为8天后的23:59:59 + end = (begin + datetime.timedelta(days=days)).replace(hour=23, minute=59, second=59) + + # 重置时间到凌晨 + self.clock_engine.reset_now(begin) + + # 预估时间事件触发次数, 每个交易日触发一次 + actived_times = 0 + for date in pd.date_range(begin.date(), periods=days+1): + if is_trade_date(date): + actived_times += 1 + + # 注册一个响应时钟事件的函数 + self.main_engine.event_engine.register(ClockEngine.EventType, count) + + # 开启事件引擎 + self.main_engine.event_engine.start() + + now = begin + while 1: + self.clock_engine.reset_now(now) + self.clock_engine.tock() + time.sleep(0.001) + now += interval + if now >= end: + break + + # 等待事件引擎处理 + self.main_engine.event_engine.stop() + print({k: len(v) for k, v in counts.items() if isinstance(k, str)}) + # 开盘收盘, 中午开盘休盘, 必定会触发1次 + self.assertEqual(len(counts['open']), actived_times) + self.assertEqual(len(counts['pause']), actived_times) + self.assertEqual(len(counts['continue']), actived_times) + self.assertEqual(len(counts['close']), actived_times) + From c52cc8e098980a20809e6d05200c37b0249eafce Mon Sep 17 00:00:00 2001 From: lamter Date: Sun, 22 May 2016 16:24:19 +0800 Subject: [PATCH 13/35] =?UTF-8?q?=20-=20change=20:=20=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=E6=97=B6=E4=BD=BF=E7=94=A8=E7=9A=84=E6=97=A5=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unitest_demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unitest_demo.py b/unitest_demo.py index 9f6ed9a..4670a58 100644 --- a/unitest_demo.py +++ b/unitest_demo.py @@ -50,7 +50,7 @@ def setUp(self): :return: """ # 设定下一个交易日 - self.trade_date = get_next_trade_date(datetime.date.today()) + self.trade_date = get_next_trade_date(datetime.date.today() - datetime.timedelta(days=1)) self.time = datetime.time(0, 0, 0, tzinfo=tz.tzlocal()) From 6d415c70aef75d3927b9b57406ffdc711ab2e63c Mon Sep 17 00:00:00 2001 From: shidenggui Date: Sun, 22 May 2016 21:49:04 +0800 Subject: [PATCH 14/35] fix(requiremtens): dateutil -> python-dateutil fix #34 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index be6b188..34e41fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ aiohttp easytrader easyquotation arrow -dateutil \ No newline at end of file +python-dateutil From df0d3b858c387bcd3fdb01cd2cf1ea067fd58e46 Mon Sep 17 00:00:00 2001 From: Joey Jiao Date: Fri, 3 Jun 2016 20:08:05 +0800 Subject: [PATCH 15/35] Allow multiple login from different thread --- .gitignore | 3 ++- easyquant/main_engine.py | 6 +++++- easyquant/push_engine/base_engine.py | 5 +++++ easyquant/strategy/strategyTemplate.py | 8 ++++++-- requirements.txt | 6 ++++++ 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index f4d5877..1c107e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +account.session yjb_account.json account.json .idea @@ -61,4 +62,4 @@ docs/_build/ # PyBuilder target/ -vcode \ No newline at end of file +vcode diff --git a/easyquant/main_engine.py b/easyquant/main_engine.py index 0a9e164..e56b670 100644 --- a/easyquant/main_engine.py +++ b/easyquant/main_engine.py @@ -3,6 +3,7 @@ import pathlib import sys from collections import OrderedDict +import dill import easytrader from logbook import Logger, StreamHandler @@ -19,6 +20,7 @@ if (PY_MAJOR_VERSION, PY_MINOR_VERSION) < (3, 5): raise Exception('Python 版本需要 3.5 或以上, 当前版本为 %s.%s 请升级 Python' % (PY_MAJOR_VERSION, PY_MINOR_VERSION)) +ACCOUNT_OBJECT_FILE = 'account.session' class MainEngine: """主引擎,负责行情 / 事件驱动引擎 / 交易""" @@ -35,6 +37,8 @@ def __init__(self, broker=None, need_data=None, quotation_engines=None, need_data_file = pathlib.Path(need_data) if need_data_file.exists(): self.user.prepare(need_data) + with open(ACCOUNT_OBJECT_FILE, 'wb') as f: + dill.dump(self.user, f) else: log_handler.warn("券商账号信息文件 %s 不存在, easytrader 将不可用" % need_data) else: @@ -79,7 +83,7 @@ def load_strategy(self, names=None): if names is None or strategy_class.name in names: self.strategies[strategy_module_name] = strategy_class - self.strategy_list.append(strategy_class(self.user, log_handler=self.log, main_engine=self)) + self.strategy_list.append(strategy_class(log_handler=self.log, main_engine=self)) self.log.info('加载策略: %s' % strategy_module_name) for strategy in self.strategy_list: for quotation_engine in self.quotation_engines: diff --git a/easyquant/push_engine/base_engine.py b/easyquant/push_engine/base_engine.py index 5bd2084..3bbc7f5 100644 --- a/easyquant/push_engine/base_engine.py +++ b/easyquant/push_engine/base_engine.py @@ -1,4 +1,5 @@ # coding: utf-8 +import dill from threading import Thread import aiohttp @@ -6,6 +7,8 @@ import time from easyquant.event_engine import Event +ACCOUNT_OBJECT_FILE = 'account.session' + class BaseEngine: """行情推送引擎基类""" @@ -13,6 +16,8 @@ class BaseEngine: PushInterval = 1 def __init__(self, event_engine, clock_engine): + with open(ACCOUNT_OBJECT_FILE, 'rb') as f: + self.user = dill.load(f) self.event_engine = event_engine self.clock_engine = clock_engine self.is_active = True diff --git a/easyquant/strategy/strategyTemplate.py b/easyquant/strategy/strategyTemplate.py index c5aa696..5215bbe 100644 --- a/easyquant/strategy/strategyTemplate.py +++ b/easyquant/strategy/strategyTemplate.py @@ -1,13 +1,17 @@ # coding:utf-8 import sys import traceback +import dill + +ACCOUNT_OBJECT_FILE = 'account.session' class StrategyTemplate: name = 'DefaultStrategyTemplate' - def __init__(self, user, log_handler, main_engine): - self.user = user + def __init__(self, log_handler, main_engine): + with open(ACCOUNT_OBJECT_FILE, 'rb') as f: + self.user = dill.load(f) self.main_engine = main_engine self.clock_engine = main_engine.clock_engine # 优先使用自定义 log 句柄, 否则使用主引擎日志句柄 diff --git a/requirements.txt b/requirements.txt index 3d48021..8344f49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ +numpy +python_dateutil +pytz +pandas requests logbook anyjson @@ -5,3 +9,5 @@ aiohttp easytrader easyquotation arrow +tushare +xlrd From af437782b1bb7e132b0a8c16e8bde62a35a0c61f Mon Sep 17 00:00:00 2001 From: Joey Jiao Date: Fri, 3 Jun 2016 21:40:14 +0800 Subject: [PATCH 16/35] Priority clock engine (cherry picked from commit de597eb6b789199aa66e05cdce6ffec46c44e3a6) --- easyquant/main_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easyquant/main_engine.py b/easyquant/main_engine.py index e56b670..59a0bc0 100644 --- a/easyquant/main_engine.py +++ b/easyquant/main_engine.py @@ -86,7 +86,7 @@ def load_strategy(self, names=None): self.strategy_list.append(strategy_class(log_handler=self.log, main_engine=self)) self.log.info('加载策略: %s' % strategy_module_name) for strategy in self.strategy_list: + self.event_engine.register(ClockEngine.EventType, strategy.clock) for quotation_engine in self.quotation_engines: self.event_engine.register(quotation_engine.EventType, strategy.run) - self.event_engine.register(ClockEngine.EventType, strategy.clock) self.log.info('加载策略完毕') From f86e2cac75f2116cab8d5c25fd2abc35c14efa21 Mon Sep 17 00:00:00 2001 From: Weichao Guo Date: Wed, 15 Jun 2016 11:58:44 +0800 Subject: [PATCH 17/35] fix is_trade_date & its usage. --- easyquant/easydealutils/time.py | 6 +++++- easyquant/push_engine/clock_engine.py | 9 +++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/easyquant/easydealutils/time.py b/easyquant/easydealutils/time.py index 046f90d..7c3c558 100644 --- a/easyquant/easydealutils/time.py +++ b/easyquant/easydealutils/time.py @@ -20,8 +20,12 @@ def is_holiday(now_time): return _is_holiday(today) +def is_weekend(now_time): + return now_time.weekday() >= 5 + + def is_trade_date(now_time): - return not is_holiday(now_time) + return not (is_holiday(now_time) or is_weekend(now_time)) def get_next_trade_date(now_time): diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index 43daf83..9116f54 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -88,7 +88,7 @@ def update_next_time(self): ) def is_active(self): - if self.is_trading_date and etime.is_holiday(self.clock_engine.now_dt): + if self.is_trading_date and not etime.is_trade_date(self.clock_engine.now_dt): # 仅在交易日触发时的判断 return False return self.next_time <= self.clock_engine.now_dt @@ -116,11 +116,12 @@ def __init__(self, event_engine, now=None, tzinfo=None): self.is_active = True self.clock_engine_thread = Thread(target=self.clocktick) self.sleep_time = 1 - self.trading_state = True if etime.is_tradetime(datetime.datetime.now()) else False + self.trading_state = True if (etime.is_tradetime(datetime.datetime.now()) and etime.is_trade_date(datetime.datetime.now())) else False self.clock_moment_handlers = deque() self.clock_interval_handlers = set() - self._init_clock_handler() + if self.trading_state: + self._init_clock_handler() def _init_clock_handler(self): """ @@ -192,7 +193,7 @@ def clocktick(self): time.sleep(self.sleep_time) def tock(self): - if etime.is_holiday(self.now_dt): + if not etime.is_trade_date(self.now_dt): pass # 假日暂停时钟引擎 else: self._tock() From 8f176ec163148d34e395fb2e9ffbd932b24f69a1 Mon Sep 17 00:00:00 2001 From: Weichao Guo Date: Wed, 15 Jun 2016 17:10:15 +0800 Subject: [PATCH 18/35] add xueqiu zuhe tracing strategy. --- strategies/xq.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ xqzh.py | 17 ++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 strategies/xq.py create mode 100644 xqzh.py diff --git a/strategies/xq.py b/strategies/xq.py new file mode 100644 index 0000000..752387a --- /dev/null +++ b/strategies/xq.py @@ -0,0 +1,53 @@ +from easyquant import StrategyTemplate + + +class Strategy(StrategyTemplate): + name = 'xq' + + def strategy(self, event): + self.log.info('\n\nxq strategy') + #self.log.info('检查持仓') + #self.log.info(self.user.balance) + #self.log.info('\n') + if self.clock_engine.trading_state: + self.log.info('trading') + else: + self.log.info('not trading') + + def __tracing(self): + xqzh = {"ZH010389": 0.2, "ZH572114": 0.4, "ZH062130": 0.4} + stocks = {} + for pos in self.user.get_position(): + stocks[pos['stock_code']] = pos['market_value'] + + for zh in xqzh: + for pos in self.user.get_position(zh): + stocks[pos['stock_code']] = stocks.get(pos['stock_code'], 0) - xqzh[zh] * pos['market_value'] + for code in stocks: + if stocks[code] > 10000: + try: + self.user.sell(code, 0, 0, stocks[code]) + except: + self.log.info('sell error') + for code in stocks: + if stocks[code] < -10000: + try: + self.user.buy(code, 0, 0, -stocks[code]) + except: + self.log.info('buy error') + + def clock(self, event): + """在交易时间会定时推送 clock 事件 + :param event: event.data.clock_event 为 [0.5, 1, 3, 5, 15, 30, 60] 单位为分钟, ['open', 'close'] 为开市、收市 + event.data.trading_state bool 是否处于交易时间 + """ + if event.data.clock_event == 'open': + # 开市了 + self.log.info('open') + elif event.data.clock_event == 'close': + # 收市了 + self.log.info('close') + elif event.data.clock_event == 5: + # 5 分钟的 clock + self.log.info("5分钟") + self.__tracing() diff --git a/xqzh.py b/xqzh.py new file mode 100644 index 0000000..b4b6219 --- /dev/null +++ b/xqzh.py @@ -0,0 +1,17 @@ +import sys +from datetime import timedelta, timezone + +import easyquotation +from easyquant.push_engine.clock_engine import ClockEngine + +import easyquant +from easyquant import DefaultQuotationEngine, DefaultLogHandler, PushBaseEngine + + +quotation_engine = DefaultQuotationEngine + +quotation_engine.PushInterval = 30 + +m = easyquant.MainEngine(broker='xq', need_data=sys.argv[1], quotation_engines=[quotation_engine], tzinfo=timezone(timedelta(hours=8))) +m.load_strategy() +m.start() From a2fa53fe41905877f9c0b4901fc11cb062491880 Mon Sep 17 00:00:00 2001 From: Weichao Guo Date: Thu, 16 Jun 2016 17:07:27 +0800 Subject: [PATCH 19/35] fix is_trade_date & its usage. --- easyquant/easydealutils/time.py | 6 +++++- easyquant/push_engine/clock_engine.py | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/easyquant/easydealutils/time.py b/easyquant/easydealutils/time.py index 046f90d..7c3c558 100644 --- a/easyquant/easydealutils/time.py +++ b/easyquant/easydealutils/time.py @@ -20,8 +20,12 @@ def is_holiday(now_time): return _is_holiday(today) +def is_weekend(now_time): + return now_time.weekday() >= 5 + + def is_trade_date(now_time): - return not is_holiday(now_time) + return not (is_holiday(now_time) or is_weekend(now_time)) def get_next_trade_date(now_time): diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index 43daf83..67811cc 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -88,7 +88,7 @@ def update_next_time(self): ) def is_active(self): - if self.is_trading_date and etime.is_holiday(self.clock_engine.now_dt): + if self.is_trading_date and not etime.is_trade_date(self.clock_engine.now_dt): # 仅在交易日触发时的判断 return False return self.next_time <= self.clock_engine.now_dt @@ -116,7 +116,7 @@ def __init__(self, event_engine, now=None, tzinfo=None): self.is_active = True self.clock_engine_thread = Thread(target=self.clocktick) self.sleep_time = 1 - self.trading_state = True if etime.is_tradetime(datetime.datetime.now()) else False + self.trading_state = True if (etime.is_tradetime(datetime.datetime.now()) and etime.is_trade_date(datetime.datetime.now())) else False self.clock_moment_handlers = deque() self.clock_interval_handlers = set() @@ -192,7 +192,7 @@ def clocktick(self): time.sleep(self.sleep_time) def tock(self): - if etime.is_holiday(self.now_dt): + if not etime.is_trade_date(self.now_dt): pass # 假日暂停时钟引擎 else: self._tock() From d16acdced5b858bcb651d16dc6ec9bc0ab630ce0 Mon Sep 17 00:00:00 2001 From: shidenggui Date: Thu, 16 Jun 2016 23:03:13 +0800 Subject: [PATCH 20/35] remove no use files --- strategies/xq.py | 53 ------------------------------------------------ xqzh.py | 17 ---------------- 2 files changed, 70 deletions(-) delete mode 100644 strategies/xq.py delete mode 100644 xqzh.py diff --git a/strategies/xq.py b/strategies/xq.py deleted file mode 100644 index 752387a..0000000 --- a/strategies/xq.py +++ /dev/null @@ -1,53 +0,0 @@ -from easyquant import StrategyTemplate - - -class Strategy(StrategyTemplate): - name = 'xq' - - def strategy(self, event): - self.log.info('\n\nxq strategy') - #self.log.info('检查持仓') - #self.log.info(self.user.balance) - #self.log.info('\n') - if self.clock_engine.trading_state: - self.log.info('trading') - else: - self.log.info('not trading') - - def __tracing(self): - xqzh = {"ZH010389": 0.2, "ZH572114": 0.4, "ZH062130": 0.4} - stocks = {} - for pos in self.user.get_position(): - stocks[pos['stock_code']] = pos['market_value'] - - for zh in xqzh: - for pos in self.user.get_position(zh): - stocks[pos['stock_code']] = stocks.get(pos['stock_code'], 0) - xqzh[zh] * pos['market_value'] - for code in stocks: - if stocks[code] > 10000: - try: - self.user.sell(code, 0, 0, stocks[code]) - except: - self.log.info('sell error') - for code in stocks: - if stocks[code] < -10000: - try: - self.user.buy(code, 0, 0, -stocks[code]) - except: - self.log.info('buy error') - - def clock(self, event): - """在交易时间会定时推送 clock 事件 - :param event: event.data.clock_event 为 [0.5, 1, 3, 5, 15, 30, 60] 单位为分钟, ['open', 'close'] 为开市、收市 - event.data.trading_state bool 是否处于交易时间 - """ - if event.data.clock_event == 'open': - # 开市了 - self.log.info('open') - elif event.data.clock_event == 'close': - # 收市了 - self.log.info('close') - elif event.data.clock_event == 5: - # 5 分钟的 clock - self.log.info("5分钟") - self.__tracing() diff --git a/xqzh.py b/xqzh.py deleted file mode 100644 index b4b6219..0000000 --- a/xqzh.py +++ /dev/null @@ -1,17 +0,0 @@ -import sys -from datetime import timedelta, timezone - -import easyquotation -from easyquant.push_engine.clock_engine import ClockEngine - -import easyquant -from easyquant import DefaultQuotationEngine, DefaultLogHandler, PushBaseEngine - - -quotation_engine = DefaultQuotationEngine - -quotation_engine.PushInterval = 30 - -m = easyquant.MainEngine(broker='xq', need_data=sys.argv[1], quotation_engines=[quotation_engine], tzinfo=timezone(timedelta(hours=8))) -m.load_strategy() -m.start() From f192260c4947168e786e4c5b67481d6a70c6479e Mon Sep 17 00:00:00 2001 From: hahahay <52usd@163.com> Date: Fri, 17 Jun 2016 20:11:39 +0800 Subject: [PATCH 21/35] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E5=AD=97=E5=8F=82=E6=95=B0=E4=BC=A0=E9=80=92markup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit register_moment调用_register_moment时,需要用关键字参数传递markup,不能用位置参数 --- easyquant/push_engine/clock_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index 9116f54..7a86ea4 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -230,7 +230,7 @@ def is_tradetime_now(self): return etime.is_tradetime(self.now_dt) def register_moment(self, clock_type, moment, makeup=False): - return self._register_moment(clock_type, moment, makeup) + return self._register_moment(clock_type, moment, makeup=markup) def _register_moment(self, clock_type, moment, is_trading_date=True, makeup=False, call=None): handlers = list(self.clock_moment_handlers) From a4bd7e87ee77b93fd45aa52e41680f770b7208bb Mon Sep 17 00:00:00 2001 From: hahahay <52usd@163.com> Date: Sun, 19 Jun 2016 10:24:37 +0800 Subject: [PATCH 22/35] fixed typo in register_moment change typo "markup" in register_moment to "makeup" --- easyquant/push_engine/clock_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index 7a86ea4..e2ccd29 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -230,7 +230,7 @@ def is_tradetime_now(self): return etime.is_tradetime(self.now_dt) def register_moment(self, clock_type, moment, makeup=False): - return self._register_moment(clock_type, moment, makeup=markup) + return self._register_moment(clock_type, moment, makeup=makeup) def _register_moment(self, clock_type, moment, is_trading_date=True, makeup=False, call=None): handlers = list(self.clock_moment_handlers) From 12c4f4184458eb21d601f9d983c24c5a7055c240 Mon Sep 17 00:00:00 2001 From: lamter Date: Wed, 13 Jul 2016 19:11:17 +0800 Subject: [PATCH 23/35] =?UTF-8?q?=20-=20add=20:=20=E4=BF=AE=E6=AD=A3=20clo?= =?UTF-8?q?ck=5Fengine=20=E5=88=9D=E5=A7=8B=E5=8C=96=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=EF=BC=8C=5Finit=5Fclock=5Fhandler=20=E5=BF=85?= =?UTF-8?q?=E9=A1=BB=E5=9C=A8=E4=BB=BB=E4=BD=95=E6=83=85=E5=86=B5=E4=B8=8B?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=97=B6=E9=83=BD=E8=A6=81=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ easyquant/push_engine/clock_engine.py | 3 +-- unitest_demo.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 1c107e9..570ebca 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ __pycache__/ account.json # C extensions *.so +tmp/ +log/ # Distribution / packaging .Python diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index e2ccd29..5b8a96c 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -120,8 +120,7 @@ def __init__(self, event_engine, now=None, tzinfo=None): self.clock_moment_handlers = deque() self.clock_interval_handlers = set() - if self.trading_state: - self._init_clock_handler() + self._init_clock_handler() def _init_clock_handler(self): """ diff --git a/unitest_demo.py b/unitest_demo.py index 4670a58..51eb426 100644 --- a/unitest_demo.py +++ b/unitest_demo.py @@ -15,7 +15,8 @@ __author__ = 'Shawn' -main_engine = MainEngine('ht') +# 需要制定一个有效的证券账户信息 +main_engine = MainEngine('ht', "tmp/ht.json") class BaseTest(unittest.TestCase): From d0b9a93d535f75f7849874168b31d2bf8b642a6d Mon Sep 17 00:00:00 2001 From: lamter Date: Thu, 14 Jul 2016 12:48:32 +0800 Subject: [PATCH 24/35] =?UTF-8?q?=20-=20add=20:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E4=BD=BF=E7=94=A8=20mock=20=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=88=B3=E7=9A=84=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- easyquant/main_engine.py | 4 +-- easyquant/push_engine/clock_engine.py | 40 +++++++++++------------- unitest_demo.py | 45 +++++++++------------------ 3 files changed, 36 insertions(+), 53 deletions(-) diff --git a/easyquant/main_engine.py b/easyquant/main_engine.py index 59a0bc0..e983a54 100644 --- a/easyquant/main_engine.py +++ b/easyquant/main_engine.py @@ -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 @@ -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] diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index 5b8a96c..d64b89b 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -101,7 +101,7 @@ 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 @@ -109,9 +109,7 @@ def __init__(self, event_engine, now=None, tzinfo=None): """ # 默认使用当地时间的时区 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) @@ -150,15 +148,15 @@ 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): @@ -166,7 +164,7 @@ def now(self): now 时间戳统一接口 :return: """ - return time.time() - self.time_delta + return time.time() @property def now_dt(self): @@ -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() diff --git a/unitest_demo.py b/unitest_demo.py index 51eb426..3e3363b 100644 --- a/unitest_demo.py +++ b/unitest_demo.py @@ -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 @@ -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 @@ -86,41 +88,24 @@ def tearDown(self): def test_set_now(self): """ 重设 clock_engine 的时间 + 通过 mock 来重设时间戳 + mock 只能重设 time.time 函数的时间戳,但是不能重设 datetime.datetime.now 函数的时间戳,详情见: + http://stackoverflow.com/questions/4481954/python-trying-to-mock-datetime-date-today-but-not-working :return: """ - tzinfo = tz.tzlocal() - now = datetime.datetime.combine( - self.trade_date, - datetime.time(8, 59, 00, tzinfo=tzinfo), - ) - clock_engien = ClockEngine(EventEngine(), now, 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) + # 使用datetime 类构建时间戳,转化为浮点数时间戳 + now = datetime.datetime(1990, 10, 16, 19, 27, 16) - # 去掉微秒误差后比较 - self.assertEqual(clock_engien.now_dt.replace(microsecond=0), now) + # 通过mock ,将 time.time() 函数的返回值重设为上面的打算模拟的值 + time.time = mock.Mock(return_value=now) - # 重设为当前时间 - clock_engien.reset_now() - now = datetime.datetime.now(tzinfo).replace(microsecond=0) + # 生成一个时钟引擎 + clock_engien = ClockEngine(EventEngine(), tzinfo) - # 去掉微秒误差后比较 - self.assertEqual(clock_engien.now_dt.replace(microsecond=0), now) + # 去掉微秒误差后验证其数值 + self.assertEqual(clock_engien.now, now) # time.time 时间戳 + self.assertEqual(clock_engien.now_dt, arrow.get(now)) # datetime 时间戳 def test_clock_moment_is_active(self): # 设置时间 From 0fcb2ca346a74da096d2efdc9c7ef268e190e5af Mon Sep 17 00:00:00 2001 From: lamter Date: Thu, 14 Jul 2016 13:10:00 +0800 Subject: [PATCH 25/35] =?UTF-8?q?=20-=20change=20:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E4=B8=AD=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=88=B3=E7=9A=84=E4=BE=8B=E5=AD=90=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +++++++++++ unitest_demo.py | 50 +++++++++++++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b4a6d2b..dceee6d 100644 --- a/README.md +++ b/README.md @@ -307,3 +307,14 @@ from easyquant import DefaultQuotationEngine m = easyquant.MainEngine(broker, need_data, quotation_engine=[DefaultQuotationEngine, LFEngine, OtherEngine]) ``` + +#### 时间戳单元测试 + +1. 请通过 clock_engine 中的 .now 或者 .now_dt 接口,以及 time.time() 接口来获得时间戳. +2. 通过上述接口获得时间戳,可以在单元测试中模拟某个时刻或者一段时间,详见 + +```python +from easyquant import DefaultQuotationEngine + +m = easyquant.MainEngine(broker, need_data, quotation_engine=[DefaultQuotationEngine, LFEngine, OtherEngine]) +``` diff --git a/unitest_demo.py b/unitest_demo.py index 3e3363b..c3b73c2 100644 --- a/unitest_demo.py +++ b/unitest_demo.py @@ -87,25 +87,33 @@ def tearDown(self): def test_set_now(self): """ - 重设 clock_engine 的时间 - 通过 mock 来重设时间戳 - mock 只能重设 time.time 函数的时间戳,但是不能重设 datetime.datetime.now 函数的时间戳,详情见: - http://stackoverflow.com/questions/4481954/python-trying-to-mock-datetime-date-today-but-not-working + 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() - # 使用datetime 类构建时间戳,转化为浮点数时间戳 - now = datetime.datetime(1990, 10, 16, 19, 27, 16) + # 使用datetime 类构建时间戳 + now = datetime.datetime(2016, 7, 14, 8, 59, 50, tzinfo=tzinfo) - # 通过mock ,将 time.time() 函数的返回值重设为上面的打算模拟的值 - time.time = mock.Mock(return_value=now) + # 通过mock ,将 time.time() 函数的返回值重设为上面的打算模拟的值,注意要转化为浮点数时间戳 + time.time = mock.Mock(return_value=now.timestamp()) # 生成一个时钟引擎 clock_engien = ClockEngine(EventEngine(), tzinfo) # 去掉微秒误差后验证其数值 - self.assertEqual(clock_engien.now, now) # time.time 时间戳 - self.assertEqual(clock_engien.now_dt, arrow.get(now)) # datetime 时间戳 + self.assertEqual(clock_engien.now, now.timestamp()) # time.time 时间戳 + self.assertEqual(clock_engien.now_dt, now) # datetime 时间戳 + + # 据此可以模拟一段时间内各个闹钟事件的触发,比如模拟开市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): # 设置时间 @@ -113,7 +121,7 @@ def test_clock_moment_is_active(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()) @@ -126,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()) @@ -136,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()) @@ -149,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()) @@ -171,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()) @@ -241,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): @@ -263,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() @@ -302,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) @@ -343,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 @@ -359,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 From 13e4bc4aa55a39d9724d4a786e14f6fc6047c152 Mon Sep 17 00:00:00 2001 From: lamter Date: Thu, 14 Jul 2016 13:19:45 +0800 Subject: [PATCH 26/35] =?UTF-8?q?=20-=20add=20:=20=E5=AE=8C=E6=88=90=20rea?= =?UTF-8?q?dme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dceee6d..dfadf2b 100644 --- a/README.md +++ b/README.md @@ -311,10 +311,33 @@ m = easyquant.MainEngine(broker, need_data, quotation_engine=[DefaultQuotationEn #### 时间戳单元测试 1. 请通过 clock_engine 中的 .now 或者 .now_dt 接口,以及 time.time() 接口来获得时间戳. -2. 通过上述接口获得时间戳,可以在单元测试中模拟某个时刻或者一段时间,详见 +2. 通过上述接口获得时间戳,可以在单元测试中模拟某个时刻或者一段时间,详见单元测试[test_set_now](*https://github.com/lamter/easyquant/blob/master/unitest_demo.py) ```python -from easyquant import DefaultQuotationEngine +from unittest import mock -m = easyquant.MainEngine(broker, need_data, quotation_engine=[DefaultQuotationEngine, LFEngine, OtherEngine]) +# 使用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() ``` From 37326ca0f4df6ec8eb3e7475cc1f8dbcc915e3f0 Mon Sep 17 00:00:00 2001 From: lamter Date: Thu, 14 Jul 2016 13:22:00 +0800 Subject: [PATCH 27/35] =?UTF-8?q?=20-=20change=20:=20=E4=BF=AE=E6=AD=A3=20?= =?UTF-8?q?=E8=B6=85=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dfadf2b..1cf77de 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ m = easyquant.MainEngine(broker, need_data, quotation_engine=[DefaultQuotationEn #### 时间戳单元测试 1. 请通过 clock_engine 中的 .now 或者 .now_dt 接口,以及 time.time() 接口来获得时间戳. -2. 通过上述接口获得时间戳,可以在单元测试中模拟某个时刻或者一段时间,详见单元测试[test_set_now](*https://github.com/lamter/easyquant/blob/master/unitest_demo.py) +2. 通过上述接口获得时间戳,可以在单元测试中模拟某个时刻或者一段时间,详见单元测试 [test_set_now](https://github.com/lamter/easyquant/blob/master/unitest_demo.py) ```python from unittest import mock From 8523b3dcbfebe94a695863d176c2636c17922d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BD=87Shawn=E7=BA=B9?= Date: Thu, 14 Jul 2016 13:25:30 +0800 Subject: [PATCH 28/35] =?UTF-8?q?=20-=20change=20:=20=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=E8=B6=85=E9=93=BE=E6=8E=A5=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1cf77de..9e15fd6 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ m = easyquant.MainEngine(broker, need_data, quotation_engine=[DefaultQuotationEn #### 时间戳单元测试 1. 请通过 clock_engine 中的 .now 或者 .now_dt 接口,以及 time.time() 接口来获得时间戳. -2. 通过上述接口获得时间戳,可以在单元测试中模拟某个时刻或者一段时间,详见单元测试 [test_set_now](https://github.com/lamter/easyquant/blob/master/unitest_demo.py) +2. 通过上述接口获得时间戳,可以在单元测试中模拟某个时刻或者一段时间,详见单元测试 [test_set_now](https://github.com/shidenggui/easyquant/blob/master/unitest_demo.py) ```python from unittest import mock From 8e6a24eb1377d883c57244835bc9d0d738bff4ad Mon Sep 17 00:00:00 2001 From: shidenggui Date: Fri, 15 Jul 2016 11:16:27 +0800 Subject: [PATCH 29/35] Update clock_engine.py --- easyquant/push_engine/clock_engine.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/easyquant/push_engine/clock_engine.py b/easyquant/push_engine/clock_engine.py index d64b89b..a1155d2 100644 --- a/easyquant/push_engine/clock_engine.py +++ b/easyquant/push_engine/clock_engine.py @@ -148,16 +148,6 @@ 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() - @property def now(self): """ @@ -173,14 +163,6 @@ 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 start(self): self.clock_engine_thread.start() From 368885c8b8d39b14357775ed518b230f032aa973 Mon Sep 17 00:00:00 2001 From: Oak Ken Date: Sun, 17 Jul 2016 09:18:37 +0800 Subject: [PATCH 30/35] Adding redis IO --- easyquant/easydealutils/__init__.py | 1 + easyquant/easydealutils/easyredis.py | 65 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 easyquant/easydealutils/easyredis.py diff --git a/easyquant/easydealutils/__init__.py b/easyquant/easydealutils/__init__.py index e69de29..c84ab36 100644 --- a/easyquant/easydealutils/__init__.py +++ b/easyquant/easydealutils/__init__.py @@ -0,0 +1 @@ +from .easyredis import RedisIo diff --git a/easyquant/easydealutils/easyredis.py b/easyquant/easydealutils/easyredis.py new file mode 100644 index 0000000..83ea541 --- /dev/null +++ b/easyquant/easydealutils/easyredis.py @@ -0,0 +1,65 @@ +# coding: utf-8 +import os +import sys +import redis +import json + +from ..log_handler import DefaultLogHandler + +class RedisIo(object): + """Redis操作类""" + + def __init__(self, conf): + self.config = self.file2dict(conf) + if self.config['passwd'] is None: + self.r = redis.Redis(host=self.config['redisip'], port=self.config['redisport'], db=self.config['db']) + else: + self.r = redis.Redis(host=self.config['redisip'], port=self.config['redisport'], db=self.config['db'], password = self.config['passwd']) + self.log = self.log_handler() + + def file2dict(self, path): + with open(path) as f: + return json.load(f) + + def cleanup(self): + self.r.flushdb() + + def lookup_redist_info(self): + info = self.r.info() + for key in info: + self.log.info('%s:%s' % (key, info[key])) + + def set_key_value(self, key, value): + self.r.set(key, value) + + def get_key_value(self, key): + return self.r.get(key) + + def save(self): + return self.r.save() + + def get_keys(self): + return self.r.keys() + + def delete_key(self, key): + return self.r.delete(key) + + def push_list_value(self, listname, value): + return self.r.lpush(listname, value) + + def pull_list_range(self, listname, starpos, endpos): + return self.r.lrange(listname, starpos, endpos) + + def get_list_len(self, listname): + return self.r.llen(listname) + + def log_handler(self): + """重定向日志""" + return DefaultLogHandler() + +def main(): + ri = RedisIo('redis.conf') + ri.lookup_redist_info() + +if __name__ == '__main__': + main() From dfad0f3085c3b535a3dcc4084d5c73b316bc86ec Mon Sep 17 00:00:00 2001 From: Oak Ken Date: Sun, 17 Jul 2016 09:41:56 +0800 Subject: [PATCH 31/35] Create easyredis class --- easyquant/easydealutils/easyredis.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/easyquant/easydealutils/easyredis.py b/easyquant/easydealutils/easyredis.py index 83ea541..ad48364 100644 --- a/easyquant/easydealutils/easyredis.py +++ b/easyquant/easydealutils/easyredis.py @@ -6,6 +6,12 @@ from ..log_handler import DefaultLogHandler +"""Debug""" +""" +sys.path.append('..') +from log_handler import DefaultLogHandler +""" + class RedisIo(object): """Redis操作类""" @@ -18,48 +24,64 @@ def __init__(self, conf): self.log = self.log_handler() def file2dict(self, path): + #读取配置文件 with open(path) as f: return json.load(f) def cleanup(self): + #清理Redis当前数据库 self.r.flushdb() def lookup_redist_info(self): + #查询Redis配置 info = self.r.info() for key in info: self.log.info('%s:%s' % (key, info[key])) def set_key_value(self, key, value): + #设置键值对key<-->value self.r.set(key, value) def get_key_value(self, key): + #查询键值对 return self.r.get(key) def save(self): + #强行保存数据到硬盘 return self.r.save() def get_keys(self): + #获取当前数据库里面所有键值 return self.r.keys() def delete_key(self, key): + #删除某个键 return self.r.delete(key) def push_list_value(self, listname, value): + #推入到队列 return self.r.lpush(listname, value) - def pull_list_range(self, listname, starpos, endpos): + def pull_list_range(self, listname, starpos=0, endpos=-1): + #获取队列某个连续片段 return self.r.lrange(listname, starpos, endpos) def get_list_len(self, listname): + #获取队列长度 return self.r.llen(listname) def log_handler(self): - """重定向日志""" + #重定向日志 return DefaultLogHandler() def main(): ri = RedisIo('redis.conf') ri.lookup_redist_info() + ri.set_key_value('test1', 1) + ri.log.info(ri.get_key_value('test1')) + ri.push_list_value('test2', 1) + ri.push_list_value('test2', 2) + ri.log.info(ri.pull_list_range('test2',0,-1)) if __name__ == '__main__': main() From 50a867f987b307eb55a371f23171c0b5e4c86067 Mon Sep 17 00:00:00 2001 From: Oak Ken Date: Sun, 17 Jul 2016 09:42:41 +0800 Subject: [PATCH 32/35] Adding redis.conf --- redis.conf | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 redis.conf diff --git a/redis.conf b/redis.conf new file mode 100644 index 0000000..e89a23f --- /dev/null +++ b/redis.conf @@ -0,0 +1,7 @@ +{ + "redisip": "", + "redisport": "", + "passwd": "", + "db": 0 +} + From 931268559a1e47f7e0105a7eaed36b35567baec1 Mon Sep 17 00:00:00 2001 From: Oak Ken Date: Sun, 17 Jul 2016 09:54:17 +0800 Subject: [PATCH 33/35] Fix gf trade delay problem --- easyquant/main_engine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/easyquant/main_engine.py b/easyquant/main_engine.py index e983a54..77c8991 100644 --- a/easyquant/main_engine.py +++ b/easyquant/main_engine.py @@ -2,6 +2,7 @@ import os import pathlib import sys +import time from collections import OrderedDict import dill @@ -65,6 +66,7 @@ def __init__(self, broker=None, need_data=None, quotation_engines=None, def start(self): """启动主引擎""" self.event_engine.start() + time.sleep(10) for quotation_engine in self.quotation_engines: quotation_engine.start() self.clock_engine.start() From b75c94a0892c9453883c16ed194294cc8604fc5d Mon Sep 17 00:00:00 2001 From: shidenggui Date: Sun, 17 Jul 2016 10:31:56 +0800 Subject: [PATCH 34/35] feat(gf): sleep 10s for gf trader load delay --- easyquant/main_engine.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/easyquant/main_engine.py b/easyquant/main_engine.py index 77c8991..4a04386 100644 --- a/easyquant/main_engine.py +++ b/easyquant/main_engine.py @@ -31,6 +31,7 @@ def __init__(self, broker=None, need_data=None, quotation_engines=None, """初始化事件 / 行情 引擎并启动事件引擎 """ self.log = log_handler + self.broker = broker # 登录账户 if (broker is not None) and (need_data is not None): @@ -66,7 +67,9 @@ def __init__(self, broker=None, need_data=None, quotation_engines=None, def start(self): """启动主引擎""" self.event_engine.start() - time.sleep(10) + if self.broker == 'gf': + self.log.warn("sleep 10s 等待 gf 账户加载") + time.sleep(10) for quotation_engine in self.quotation_engines: quotation_engine.start() self.clock_engine.start() From b79e287eb49854e9c866958480f6a504c73a147f Mon Sep 17 00:00:00 2001 From: shidenggui Date: Sun, 17 Jul 2016 10:32:37 +0800 Subject: [PATCH 35/35] fix(redisio): remove invalid log handler import --- easyquant/easydealutils/easyredis.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/easyquant/easydealutils/easyredis.py b/easyquant/easydealutils/easyredis.py index ad48364..23c59be 100644 --- a/easyquant/easydealutils/easyredis.py +++ b/easyquant/easydealutils/easyredis.py @@ -4,14 +4,6 @@ import redis import json -from ..log_handler import DefaultLogHandler - -"""Debug""" -""" -sys.path.append('..') -from log_handler import DefaultLogHandler -""" - class RedisIo(object): """Redis操作类""" @@ -21,7 +13,6 @@ def __init__(self, conf): self.r = redis.Redis(host=self.config['redisip'], port=self.config['redisport'], db=self.config['db']) else: self.r = redis.Redis(host=self.config['redisip'], port=self.config['redisport'], db=self.config['db'], password = self.config['passwd']) - self.log = self.log_handler() def file2dict(self, path): #读取配置文件 @@ -35,8 +26,6 @@ def cleanup(self): def lookup_redist_info(self): #查询Redis配置 info = self.r.info() - for key in info: - self.log.info('%s:%s' % (key, info[key])) def set_key_value(self, key, value): #设置键值对key<-->value @@ -70,18 +59,13 @@ def get_list_len(self, listname): #获取队列长度 return self.r.llen(listname) - def log_handler(self): - #重定向日志 - return DefaultLogHandler() def main(): ri = RedisIo('redis.conf') ri.lookup_redist_info() ri.set_key_value('test1', 1) - ri.log.info(ri.get_key_value('test1')) ri.push_list_value('test2', 1) ri.push_list_value('test2', 2) - ri.log.info(ri.pull_list_range('test2',0,-1)) if __name__ == '__main__': main()