From c82fd0782b2eca300cad6ab7d07a8a8ce6bf2cf7 Mon Sep 17 00:00:00 2001 From: aaron yang Date: Sun, 24 Jul 2022 20:07:49 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20#58=20cache=20more=20frame?= =?UTF-8?q?=20type=20bras=20and=20to=20real-time=20calc=20with=20lua?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. add lua script and wrappers under scripts 2. load_lua_script during app.init 3. rebuild unclosed bars during app.init 4. realtime calc unclosed bars after 1m bars is fetched and cached --- docs/keyconcept.md | 4 + omega/master/jobs.py | 32 +++-- omega/master/tasks/rebuild_unclosed.py | 103 +++++++++++++++ omega/scripts/__init__.py | 62 +++++++++ omega/scripts/omega.lua | 112 +++++++++++++++++ poetry.lock | 20 +-- pyproject.toml | 3 +- tests/__init__.py | 26 +++- tests/data/test_rebuild_unclosed/1m.pkl | Bin 0 -> 36256 bytes tests/data/test_rebuild_unclosed/days.pkl | Bin 0 -> 11952 bytes tests/master/tasks/test_rebuild_unclosed.py | 118 ++++++++++++++++++ ..._omega_adaptors_jq-1.0.14-py3-none-any.whl | Bin 11067 -> 0 bytes ...lionare_omicron-2.0.0a36-py3-none-any.whl} | Bin 97836 -> 97806 bytes tests/scripts/__init__.py | 0 tests/scripts/test_scripts.py | 115 +++++++++++++++++ tox.ini | 1 - 16 files changed, 569 insertions(+), 27 deletions(-) create mode 100644 docs/keyconcept.md create mode 100644 omega/master/tasks/rebuild_unclosed.py create mode 100644 omega/scripts/__init__.py create mode 100644 omega/scripts/omega.lua create mode 100644 tests/data/test_rebuild_unclosed/1m.pkl create mode 100644 tests/data/test_rebuild_unclosed/days.pkl create mode 100644 tests/master/tasks/test_rebuild_unclosed.py delete mode 100644 tests/packages/zillionare_omega_adaptors_jq-1.0.14-py3-none-any.whl rename tests/packages/{zillionare_omicron-2.0.0a35-py3-none-any.whl => zillionare_omicron-2.0.0a36-py3-none-any.whl} (69%) create mode 100644 tests/scripts/__init__.py create mode 100644 tests/scripts/test_scripts.py diff --git a/docs/keyconcept.md b/docs/keyconcept.md new file mode 100644 index 0000000..24f8250 --- /dev/null +++ b/docs/keyconcept.md @@ -0,0 +1,4 @@ +# 数据存储 +Omega 2.0将数据主要存储在时间序列数据库(influxdb)中。上 +# 数据同步 +# 数据校准 diff --git a/omega/master/jobs.py b/omega/master/jobs.py index 84fe7c9..4a46215 100644 --- a/omega/master/jobs.py +++ b/omega/master/jobs.py @@ -8,9 +8,8 @@ import arrow import cfg4py from cfg4py.config import Config -from coretypes import FrameType -from omicron.dal import cache -from omicron.models.timeframe import TimeFrame +from coretypes import Frame, FrameType +from omicron import cache, tf from omega.core import constants from omega.core.events import Events @@ -27,6 +26,7 @@ sync_xrxd_reports, ) from omega.master.tasks.synctask import BarsSyncTask, master_syncbars_task +from omega.scripts import close_frame, update_unclosed_bar logger = logging.getLogger(__name__) cfg: Config = cfg4py.get_instance() @@ -35,10 +35,10 @@ async def get_after_hour_sync_job_task() -> Optional[BarsSyncTask]: """获取盘后同步的task实例""" now = arrow.now().naive - if not TimeFrame.is_trade_day(now): # pragma: no cover + if not tf.is_trade_day(now): # pragma: no cover logger.info("非交易日,不同步") return - end = TimeFrame.last_min_frame(now, FrameType.MIN1) + end = tf.last_min_frame(now, FrameType.MIN1) if now < end: # pragma: no cover logger.info("当天未收盘,禁止同步") return @@ -74,14 +74,14 @@ async def get_sync_minute_date(): end = arrow.now().naive.replace(second=0, microsecond=0) first = end.replace(hour=9, minute=30, second=0, microsecond=0) # 检查当前时间是否在交易时间内 - if not TimeFrame.is_trade_day(end): # pragma: no cover + if not tf.is_trade_day(end): # pragma: no cover logger.info("非交易日,不同步") return False if end < first: # pragma: no cover logger.info("时间过早,不能拿到k线数据") return False - end = TimeFrame.floor(end, FrameType.MIN1) + end = tf.floor(end, FrameType.MIN1) tail = await cache.sys.get(constants.BAR_SYNC_MINUTE_TAIL) # tail = "2022-02-22 13:29:00" if tail: @@ -93,8 +93,8 @@ async def get_sync_minute_date(): tail = first # 取上次同步截止时间+1 计算出n_bars - tail = TimeFrame.floor(tail + datetime.timedelta(minutes=1), FrameType.MIN1) - n_bars = TimeFrame.count_frames(tail, end, FrameType.MIN1) # 获取到一共有多少根k线 + tail = tf.floor(tail + datetime.timedelta(minutes=1), FrameType.MIN1) + n_bars = tf.count_frames(tail, end, FrameType.MIN1) # 获取到一共有多少根k线 return end, n_bars @@ -128,6 +128,20 @@ async def run_sync_minute_bars_task(task: BarsSyncTask): constants.BAR_SYNC_MINUTE_TAIL, task.end.strftime("%Y-%m-%d %H:%M:00"), ) + frame = task.end + for frame_type in ( + FrameType.MIN5, + FrameType.MIN15, + FrameType.MIN30, + FrameType.MIN60, + FrameType.DAY, + FrameType.WEEK, + FrameType.MONTH, + ): + update_unclosed_bar(frame_type, frame) + + if frame == tf.ceiling(frame, frame_type): + close_frame(frame_type, frame) return task diff --git a/omega/master/tasks/rebuild_unclosed.py b/omega/master/tasks/rebuild_unclosed.py new file mode 100644 index 0000000..fab5a3b --- /dev/null +++ b/omega/master/tasks/rebuild_unclosed.py @@ -0,0 +1,103 @@ +import logging + +import arrow +import numpy as np +from coretypes import FrameType +from omicron import cache, tf +from omicron.models.security import Security +from omicron.models.stock import Stock +from omicron.notify.dingtalk import DingTalkMessage + +logger = logging.getLogger(__name__) + + +async def _rebuild_min_level_unclosed_bars(): + """根据缓存中的分钟线,重建当日已收盘或者未收盘的分钟级别及日线级别数据""" + end = tf.floor(arrow.now().naive, FrameType.MIN1) + keys = await cache.security.keys("bars:1m:*") + + errors = 0 + for key in keys: + try: + sec = key.split(":")[2] + bars = await Stock._get_cached_bars(sec, end, 240, FrameType.MIN1) + except Exception as e: + logger.exception(e) + logger.warning("failed to get cached bars for %s", sec) + errors += 1 + continue + + try: + for frame_type in tf.minute_level_frames[1:]: + resampled = Stock.resample(bars, FrameType.MIN1, frame_type) + if tf.is_bar_closed(resampled[-1]["frame"], frame_type): + await Stock.cache_bars(sec, frame_type, resampled) + else: + await Stock.cache_bars(sec, frame_type, resampled[:-1]) + await Stock.cache_unclosed_bars(sec, frame_type, resampled[-1:]) + + # 重建日线数据 + resampled = Stock.resample(bars, FrameType.MIN1, FrameType.DAY) + await Stock.cache_unclosed_bars(sec, FrameType.DAY, resampled) + except Exception as e: + logger.exception(e) + logger.warning( + "failed to build unclosed bar for %s, frame type is %s", sec, frame_type + ) + errors += 1 + + if errors > 0: + DingTalkMessage.text(f"重建分钟级缓存数据时,出现{errors}个错误。") + + +async def _rebuild_day_level_unclosed_bars(): + """重建当期未收盘的周线、月线 + + !!!Info: + 最终我们需要实时更新年线和季线。目前数据库还没有同步这两种k线。 + """ + codes = await Security.select().eval() + end = arrow.now().date() + # just to cover one month's day bars at most + n = 30 + start = tf.day_shift(end, -n) + + errors = 0 + for code in codes: + try: + bars = await Stock._get_persisted_bars( + code, FrameType.DAY, begin=start, end=end + ) + except Exception as e: + logger.exception(e) + logger.warning("failed to get persisted bars for %s from %s to %s", code, start, end) + errors += 1 + continue + + try: + unclosed_day = await Stock._get_cached_day_bar(code) + bars = np.concatenate([bars, unclosed_day]) + + week = Stock.resample(bars, FrameType.DAY, FrameType.WEEK) + await Stock.cache_unclosed_bars(code, FrameType.WEEK, week[-1:]) + + month = Stock.resample(bars, FrameType.DAY, FrameType.MONTH) + await Stock.cache_unclosed_bars(code, FrameType.MONTH, month[-1:]) + except Exception as e: + logger.exception(e) + logger.warning( + "failed to build unclosed bar for %s, got bars %s", code, len(bars) + ) + errors += 1 + + if errors > 0: + DingTalkMessage.text(f"重建日线级别缓存数据时,出现{errors}个错误。") + + +async def rebuild_unclosed_bars(): + """在omega启动时重建未收盘数据 + + 后续未收盘数据的更新,将在每个分钟线同步完成后,调用lua脚本进行。 + """ + await _rebuild_min_level_unclosed_bars() + await _rebuild_day_level_unclosed_bars() diff --git a/omega/scripts/__init__.py b/omega/scripts/__init__.py new file mode 100644 index 0000000..4006b95 --- /dev/null +++ b/omega/scripts/__init__.py @@ -0,0 +1,62 @@ +import logging +import os + +from coretypes import Frame, FrameType +from omicron import cache, tf +from omicron.notify.dingtalk import DingTalkMessage + +logger = logging.getLogger(__name__) + + +async def load_lua_script(): + """加载lua脚本到redis中""" + dir_ = os.path.dirname(os.path.abspath(__file__)) + for file in os.listdir(dir_): + if not file.endswith(".lua"): + continue + + path = os.path.join(dir_, file) + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + r = await cache.sys.execute("FUNCTION", "LOAD", "REPLACE", content) + print(r) + + +async def update_unclosed_bar(frame_type: FrameType, source_min: Frame): + """wraps the cognominal lua script function + + Args: + frame_type: which frame type to be updated/merged + source_min: the minute bar to be merged from + """ + source = tf.time2int(source_min) + try: + await cache.security.execute( + "fcall", "update_unclosed", 0, frame_type.value, source + ) + except Exception as e: + msg = f"实时合并{frame_type}未收盘行情数据错误:{source_min}" + logger.exception(e) + logging.warning(msg) + DingTalkMessage.text(msg) + + +async def close_frame(frame_type: FrameType, frame: Frame): + """wraps the cognominal lua script function + + Args: + frame_type: which frame type to be closed + frame: the closed frame + """ + dst = ( + tf.date2int(frame) if frame_type in tf.day_level_frames else tf.time2int(frame) + ) + + try: + await cache.security.execute("fcall", "close_frame", 0, frame_type.value, dst) + except Exception as e: + msg = f"缓存收盘{frame_type}数据失败: {frame}" + logger.exception(e) + logger.warning(msg) + DingTalkMessage.text(msg) diff --git a/omega/scripts/omega.lua b/omega/scripts/omega.lua new file mode 100644 index 0000000..8174b43 --- /dev/null +++ b/omega/scripts/omega.lua @@ -0,0 +1,112 @@ +#!lua name=omega + +local function round2(num) + return math.floor(num * 100 + 0.5) / 100 +end + +local function newsplit(delimiter, str) + assert(type(delimiter) == "string") + assert(#delimiter > 0, "Must provide non empty delimiter") + + -- Add escape characters if delimiter requires it + -- delimiter = delimiter:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%0") + + local start_index = 1 + local result = {} + + while true do + local delimiter_index, _ = str:find(delimiter, start_index) + + if delimiter_index == nil then + table.insert(result, str:sub(start_index)) + break + end + + table.insert(result, str:sub(start_index, delimiter_index - 1)) + + start_index = delimiter_index + 1 + end + + return result +end + +local function close_frame(keys_, args) +--local function close_frame(frame_type, frame) + -- close the frame, write unclosed_5m hash to bars:5m:{code} hash. + local frame_type, frame = unpack(args) + local hm = redis.call('hgetall', 'bars:' .. frame_type .. ':unclosed') + + for i = 1, #hm, 2 do + local code = hm[i] + local bar = hm[i + 1] + redis.call('hset', 'bars:' .. frame_type .. ':' .. code, frame, bar) + end + + redis.call('del', 'bars:' .. frame_type .. ':unclosed') +end + +local function decode_bar(bars) + -- 将string表示的bar解码成为正确类型的OHLC,但对frame仍保持为字符串 + local frame, open, high, low, close, volume, amount, factor = unpack(newsplit(',', bars)) + + return frame, round2(tonumber(open)), round2(tonumber(high)), round2(tonumber(low)), round2(tonumber(close)), tonumber(volume), tonumber(amount), tonumber(factor) +end + +local function update_unclosed(keys_, args) +--local function update_unclosed(frame_type, min_frame) + -- merge bars:{frame_type.value}:unclosed with bars:1m:{code} hash. + -- args are: frame_type(str), min_frame(int, minute frame) + + local frame_type, min_frame = unpack(args) + local unclosed_key = 'bars:' .. frame_type .. ':unclosed' + + -- bars:1m:* should contains NO bars:1m:unclosed + local keys = redis.call('keys', 'bars:1m:*') + + for _, key_ in ipairs(keys) do + local code = key_:match('bars:1m:(.*)') + + -- get 1m bar to merge from + local mbar = redis.call('hget', key_, min_frame) + if mbar then + local t2, o2, h2, l2, c2, v2, a2, f2 = decode_bar(mbar) + + -- get unclosed bar and do the merge + local unclosed = redis.call('hget', unclosed_key, code) + + local t, opn, high, low, close, volume, amount, factor = '', o2, h2, l2, c2, v2, a2, f2 + if unclosed then + local _, o1, h1, l1, c1, v1, a1, f1 = decode_bar(unclosed) + opn = o1 + high = math.max(h1, h2) + low = math.min(l1, l2) + close = c2 + volume = v1 + v2 + amount = a1 + a2 + factor = f2 + end + + -- save unclosed bar + local bar = min_frame .. ',' .. opn .. ',' .. high .. ',' .. low .. ',' .. close .. ',' .. volume .. ',' .. amount .. ',' .. factor + redis.call('hset', unclosed_key, code, bar) + end + end +end + +-- update_unclosed('5m', 202207180935, 202207180931) +-- update_unclosed('5m', 202207180935, 202207180932) +-- update_unclosed('5m', 202207180935, 202207180933) +-- update_unclosed('5m', 202207180935, 202207180935) +-- update_unclosed('5m', 202207180935, 202207180936) + + +-- close_frame('5m', "2022020935") + +redis.register_function( + 'close_frame', + close_frame +) +redis.register_function( + 'update_unclosed', + update_unclosed +) diff --git a/poetry.lock b/poetry.lock index d3e7c3b..9eb286e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1526,7 +1526,7 @@ doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-include-markdown-plugin (>=1.0.0,<2.0. [[package]] name = "zillionare-omega-adaptors-jq" -version = "1.0.14" +version = "1.1.0" description = "Jqdatasdk adapter for zillionare omega" category = "main" optional = false @@ -1562,7 +1562,7 @@ test = ["pytest", "black", "isort", "doc8", "flake8", "pytest-cov"] [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "b62fcf916ab4c4d99853e308ec3748bcde42ee4b5dd7af1692b0cc309af1599d" +content-hash = "3d105f63afd2cbe8dddab21cb5990c6fbdca532986e39741f9e87b5438223c9c" [metadata.files] aiocache = [ @@ -2174,10 +2174,7 @@ mergedeep = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] -minio = [ - {file = "minio-7.1.8-py3-none-any.whl", hash = "sha256:0feadaf4cfd8608ccaf17b092c799bbe4b9e0692f9c15f5e03c5ee21d85e8cdb"}, - {file = "minio-7.1.8.tar.gz", hash = "sha256:c3fe5448ca281c88fe58f0486a73e0df6cdb05e8dbf72eb79e570e71125c1686"}, -] +minio = [] mkdocs = [ {file = "mkdocs-1.3.0-py3-none-any.whl", hash = "sha256:26bd2b03d739ac57a3e6eed0b7bcc86168703b719c27b99ad6ca91dc439aacde"}, {file = "mkdocs-1.3.0.tar.gz", hash = "sha256:b504405b04da38795fec9b2e5e28f6aa3a73bb0960cb6d5d27ead28952bd35ea"}, @@ -2312,7 +2309,6 @@ numpy = [ {file = "numpy-1.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8251ed96f38b47b4295b1ae51631de7ffa8260b5b087808ef09a39a9d66c97ab"}, {file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48a3aecd3b997bf452a2dedb11f4e79bc5bfd21a1d4cc760e703c31d57c84b3e"}, {file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3bae1a2ed00e90b3ba5f7bd0a7c7999b55d609e0c54ceb2b076a25e345fa9f4"}, - {file = "numpy-1.22.3-cp310-cp310-win32.whl", hash = "sha256:f950f8845b480cffe522913d35567e29dd381b0dc7e4ce6a4a9f9156417d2430"}, {file = "numpy-1.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:08d9b008d0156c70dc392bb3ab3abb6e7a711383c3247b410b39962263576cd4"}, {file = "numpy-1.22.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:201b4d0552831f7250a08d3b38de0d989d6f6e4658b709a02a73c524ccc6ffce"}, {file = "numpy-1.22.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8c1f39caad2c896bc0018f699882b345b2a63708008be29b1f355ebf6f933fe"}, @@ -2548,10 +2544,7 @@ readme-renderer = [ {file = "readme_renderer-35.0-py3-none-any.whl", hash = "sha256:73b84905d091c31f36e50b4ae05ae2acead661f6a09a9abb4df7d2ddcdb6a698"}, {file = "readme_renderer-35.0.tar.gz", hash = "sha256:a727999acfc222fc21d82a12ed48c957c4989785e5865807c65a487d21677497"}, ] -redis = [ - {file = "redis-4.3.1-py3-none-any.whl", hash = "sha256:84316970995a7adb907a56754d2b92d88fc2d252963dc5ac34c88f0f1a22c25d"}, - {file = "redis-4.3.1.tar.gz", hash = "sha256:94b617b4cd296e94991146f66fc5559756fbefe9493604f0312e4d3298ac63e9"}, -] +redis = [] requests = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, @@ -3029,10 +3022,7 @@ zillionare-core-types = [ {file = "zillionare_core_types-0.4.1-py3-none-any.whl", hash = "sha256:d38f03d196e528a523f102f19e97f0041f6c64295f885ea1484dd180e8e26316"}, {file = "zillionare_core_types-0.4.1.tar.gz", hash = "sha256:096eb62669ac5853f2b6b5753f420c07e506fe5d0aa64af8c3647fc3cf163555"}, ] -zillionare-omega-adaptors-jq = [ - {file = "zillionare-omega-adaptors-jq-1.0.14.tar.gz", hash = "sha256:1b9f19409163b9fb5ae211a1e10bc1bc2dca653d62a236a8006ed7b83bb887af"}, - {file = "zillionare_omega_adaptors_jq-1.0.14-py3-none-any.whl", hash = "sha256:0c054850a3c57babe7d8a437ffead93e4a2f86217832ad2ef1a3e7d80fa91e93"}, -] +zillionare-omega-adaptors-jq = [] zipp = [ {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, diff --git a/pyproject.toml b/pyproject.toml index 7a93225..2923d14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ livereload = {version = "^2.6.3", optional = true} minio = "^7.1.2" zillionare-core-types = "^0.4.1" msgpack = {version = "^1.0.3", optional = true} -zillionare-omega-adaptors-jq = "^1.0.14" +zillionare-omega-adaptors-jq = "^1.1.0" APScheduler = "^3.8.1" Cython = "^0.29.28" numpy = "^1.22" @@ -123,6 +123,7 @@ exclude = ''' | \.mypy_cache | \.tox | \.venv + | \.history | _build | buck-out | build diff --git a/tests/__init__.py b/tests/__init__.py index d365e45..e71871d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5751,5 +5751,29 @@ def assert_bars_equal(exp, actual): def test_dir(): + from pathlib import Path + home = os.path.dirname(__file__) - return home + return Path(home) + + +async def reset_influxdb(): + """clean up influxdb""" + import itertools + + from omicron import tf + from omicron.dal.influx.influxclient import InfluxClient + from omicron.models.stock import Stock + + # create influxdb client + url, token, bucket, org = ( + cfg.influxdb.url, + cfg.influxdb.token, + cfg.influxdb.bucket_name, + cfg.influxdb.org, + ) + client = InfluxClient(url, token, bucket, org) + + await client.delete_bucket() + await client.create_bucket() + return client diff --git a/tests/data/test_rebuild_unclosed/1m.pkl b/tests/data/test_rebuild_unclosed/1m.pkl new file mode 100644 index 0000000000000000000000000000000000000000..4fec08b76880071874dfe311724cd6e138c07a11 GIT binary patch literal 36256 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 None: + await init_test_env() + await omicron.init() + await Stock.reset_cache() + + file = test_dir() / "data" / "test_rebuild_unclosed" / "days.pkl" + with open(file, "rb") as f: + barss = pickle.load(f) + self.secs = list(barss.keys()) + await Stock.persist_bars(FrameType.DAY, barss) + + file = test_dir() / "data" / "test_rebuild_unclosed" / "1m.pkl" + with open(file, "rb") as f: + barss = pickle.load(f) + for code, bars in barss.items(): + await Stock.cache_bars(code, FrameType.MIN1, bars) + + async def asyncTearDown(self) -> None: + await omicron.close() + + @mock.patch("arrow.now", return_value=arrow.get(2022, 7, 21, 11, 14)) + async def test_rebuild_min_level_unclosed_bars(self, _): + # ensure there's no cache-related data in the database + keys = await cache.security.keys("bars:5m:*") + self.assertEqual(len(keys), 0) + + await _rebuild_min_level_unclosed_bars() + + # check the result + keys = await cache.security.keys("bars:5m:*") + self.assertSetEqual( + set(keys), + set([f"bars:5m:{code}" for code in self.secs] + [f"bars:5m:unclosed"]), + ) + + unclosed = await cache.security.hgetall("bars:5m:unclosed") + self.assertSetEqual(set(unclosed.keys()), set(self.secs)) + bar_04 = unclosed["000004.XSHE"] + self.assertEqual( + "202207211114,8.95,9.0,8.95,9.0,18100.0,162371.0,7.446", bar_04 + ) + + closed = await cache.security.hgetall("bars:30m:000004.XSHE") + self.assertSetEqual( + set(closed.keys()), set(["202207211000", "202207211030", "202207211100"]) + ) + + unclosed_day_bar = await cache.security.hgetall("bars:1d:unclosed") + self.assertSetEqual(set(unclosed_day_bar.keys()), set(self.secs)) + bar_01 = unclosed_day_bar["000001.XSHE"] + exp_bar_01 = "20220721,13.32,13.34,13.22,13.23,55886660.0,740959140.0,121.71913" + self.assertEqual(exp_bar_01, bar_01) + + # should have no bars:1d:000004.XSHE + keys = await cache.security.keys("bars:1d:*") + self.assertEqual(keys, ["bars:1d:unclosed"]) + + # test error handling + with mock.patch("omicron.models.stock.Stock._get_cached_bars", side_effect=Exception): + await _rebuild_min_level_unclosed_bars() + + with mock.patch("omicron.models.stock.Stock.resample", side_effect=Exception): + await _rebuild_min_level_unclosed_bars() + + @mock.patch("arrow.now", return_value=arrow.get(2022, 7, 21, 11, 14)) + async def test_rebuild_day_level_unclosed_bars(self, _): + await _rebuild_min_level_unclosed_bars() + + # ensure there's no cache-related data in the database + keys = await cache.security.keys("bars:1w:*") + self.assertEqual(len(keys), 0) + + keys = await cache.security.keys("bars:1M:*") + self.assertEqual(len(keys), 0) + + with mock.patch("omicron.models.security.Query.eval", return_value=self.secs): + await _rebuild_day_level_unclosed_bars() + + # check the result + unclosed_week_bar = await cache.security.hgetall("bars:1w:unclosed") + self.assertSetEqual(set(unclosed_week_bar.keys()), set(self.secs)) + bar_02 = unclosed_week_bar["300001.XSHE"] + self.assertEqual( + "20220721,18.81,21.4,18.65,19.95,146464648.0,2899841442.0,6.944613", bar_02 + ) + + unclosed_month_bar = await cache.security.hgetall("bars:1M:unclosed") + self.assertSetEqual(set(unclosed_month_bar.keys()), set(self.secs)) + bar_03 = unclosed_month_bar["000004.XSHE"] + exp_bar_03 = "20220721,8.91,10.58,8.14,9.0,58976343.0,543641584.0,7.446" + self.assertEqual(exp_bar_03, bar_03) + + # should have no bars:1w:000004.XSHE + keys = await cache.security.keys("bars:1w:*") + self.assertEqual(keys, ["bars:1w:unclosed"]) + + # test error handling: should raise no exception + with mock.patch("omicron.models.stock.Stock._get_persisted_bars", side_effect=Exception): + await _rebuild_day_level_unclosed_bars() + + with mock.patch("omicron.models.stock.Stock.resample", side_effect=Exception): + await _rebuild_day_level_unclosed_bars() diff --git a/tests/packages/zillionare_omega_adaptors_jq-1.0.14-py3-none-any.whl b/tests/packages/zillionare_omega_adaptors_jq-1.0.14-py3-none-any.whl deleted file mode 100644 index 95dd29bd7347bb470a8ef3a01e510b8d7ebd95bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11067 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 diff --git a/tests/packages/zillionare_omicron-2.0.0a35-py3-none-any.whl b/tests/packages/zillionare_omicron-2.0.0a36-py3-none-any.whl similarity index 69% rename from tests/packages/zillionare_omicron-2.0.0a35-py3-none-any.whl rename to tests/packages/zillionare_omicron-2.0.0a36-py3-none-any.whl index 4c3366acb9d555e5f867f28764e5f3cadd27fbaf..174bafcfee25b306fb4b1115ebcd64827b9917cb 100644 GIT binary patch delta 27491 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 MQo!Lj1OKJ|AHBOnQ~&?~ delta 27574 zcmV(?K-a&H`~|H11+W3g4FVB#OHma70KQxR02-6g-4l}`$Qyrov@F}4(9?|LOfr)? zZKce#p*a}vfTLjD;VuA9CM{`(Qlc+gmX-R0vSd4!EhTniOK$5bG$mXAOF|sUpZYKK zy|=r-?qYFw)JN^6y%BW>?Ay2RYu~>2c7d?l@5QO*dufn{-C!zY*PY(-au_X(%Q$Z! zpmyBrTIp&ZAS{1=|7T&6P6;3{j(abQi&lGie}7e6MT2gC)$$T6>N7O0UK{|bAQ6~; z9DCiM&b|&oL)|(Uv<7jQuHw}c>(%Rqt+*F8uy-SX;o`jikG#}-p_l$+;Q6c1V~g6u z_c{Wy<+Tn6rPf9mwL62A2L6}R1ivc**;lI*22mPwK=$8`=L+kb`JkbbEf#NgAjPf)p4I!3~leia2enouXbEwpSZIs4ew6M{Vq?TZo?yj|d#vfxv6MqKz-)8&EvP7~l0yEf&50fIeKd(q#qo6kHo zGdvr?NQG9RlcTfvywDMZ;RkA zGLVDkKX4-B4;Z1_chaO@iM2*(z^M-6sEo*Q)5dDx#dR16yhxNDreD6dO5cZj1Kt!y zgEXL@FbjK;kMe4Dyd<&yVGzWtOft4@)AxTuZtOGz9Y0LK99GF$)bO^LgnZ`lDMoW? zF~>wdP$w=-s4#V1CkZ-j;M9xJ!5|BBDn}ry7l3W9C;WtM8QcmJ0w~&nV zwekAa##(ms%GTX0*&DA8*KZH6emQ*Si{aZ}3}3(bUw1ELS3ewne%)W%+V~t`v(JAo z58wJG`})o7`ls7hep5jG{?zb;_qOhSxc%uSTn>MCgDoubkjO|wCX5lD!+G#>Wck#}7V&wX#puqqP{oK{LY(XF-b~i`_eycWz$a+Straf3o}QPh{m}r*D9MvbDP; zvVYxOJ3oBw)16b-wr_lr{mTWw0Q!_AaCa>`{T~3ed+jTYjsVvBfgiX>T1$UG#*aNv z9roD%!*8>5SBLN4s{n7UD&PRSwQ;2yJV|BfgNHP*Fk)cAN~i#Wxu98GIGtVkWA>+i z-(6eFHcsu_I+>l@aF}$UZTQ+}%!pVs7;{ZxeQV>j?A$sNz}DT%+39oJn}68-qRy%;4^w}$#J{Pz;J5Gtq96Hzng!0%Cg)?%RS$3|9*`iAU_tzN zjob%662YRCJ6#0Qm`Kvecp#*EV8KAdgK4Ci1wb|#4{X#uF!9g~XCxDa$i}jvNEaD- z7~@W2$EsykjXbg5Bmqx6bfLXTEy_5w?4QPttvNGga3gj~7)5k|zvq7_0bPJNYSBCp z#23R0=jA1=SnqxqFDLrqo20$gkvZ$w<6-0nE05Q$$LY@FCp5VBAo3H03(_`1wO?Ao zuWpyF;vh^DcH3iLUHt0w+7coWMByjoyotcY>c#LgPpzn}PQ2C-gcPBYUXm9_+X*7p zy=KkNTQh1W;{v;26YoD5s1ffxQo~OKX$Ju4?=_ zhrzNM@I{mW7Xm|8De|o3)DWzU;LX?vFUON_&K7@`C%uzA)ClIhWHo9r4|CM(u<2c( zXXS&2A1n=)A)5w>y2WDmeV7;OVQJ0WQK6P&L9!Cdi+nNZ_rYXfe|CM0%1rGF`z8R{ zWQ+DaP|Uq5(agP^Om!vY(n2t*IY~#gg0nU?%ii|Bn8KAn=E4$M&6lUTx=_}&B}5uD z|A~L50HnE;JAo;BFMA;`(KSRJ2|8v_R?}v2~MhX@7a0Ag~8>h`6zwMj;7U%{Y3X( zFm$=E7=kjEI~TJvr-2tp)FJ&Ip8qD#{I>7D$?oSIK~}`le1O?K`#Su{Uj1#u*a zb6+qd8quw!${IVd7cB>pL3*7IOof@cHQj`%AGLZ|YMr+SX?yx#jOIy|o_$K0X5j-{e zym>?AwS;n%xfdic$sNpP*7JOqV-J6W#KpRn>$C9GyefdC1U6$ehp$d#F_I-EgANtu z77BSGs|*$L%aP9MYB2;WFfQ7Dd2EHg1zyi0*f266DtE?Ds~4qVGziRuC}GE>giyI{ zu|eg?94cBk)zIzF4d>fmr+QV`HGgo5$GBRPFRGRK#D)VUB2%TFXub3)2vY(ilu?w}7 zC*1ugO02ox5=#f3e)`d{l%$hP#e#O3PQ?uEgNN+N?9NDx6sb)@(0oA<#)RXH;Xx>x zOhOd|8ASBG2NhA372daq$hd#@<}6sn6C48sddxAfifQg1%q%DsifX^4 zO^BQHR_gBou`<#R-I=Wl;$%x;h#?)h2* ziW@~vtCL7(@E8#5`H+2gEQu`I36BHFaO>7n&1N$XDj>9$x{fILVB`4mjtbAKLTbT> z$9_O{Ly}R=de&;r%ou|ks)$KNGdU&&ELu?ID8Um!Yb6C1c2X$;>Fmeea@U&!QGhjp z?W!brVG@*2Rg{+w`-Oj=EO3Z`C|<5xUGGR>C4)GyR(k{Muy-_omQE+|^OFgrMTkYi zI7u7oG9%e+KMtc5&j^I6Jdh8JNzrvlWFxFc7o2KHwr#W2fK3v%ivlxb>)y5D$y=CW ze07`U7kTagiN;^nPRSL$N0C5`NggIj9MmaWoj5$iq@kueW0rr1ak)}hmYrMQxu*JH zZJJ=#SX@;?s*XkKA(ok%p{wvaGI&WKCS=l@RkMy)gRp|CI<%@5UVK3b8fMr&Up9QS z-oURH#W1iPvadl78N?k1!;hEwOAWY{H>bTLL8JrF z%^ai`FY5qsD{o#Jw2r{gRHD|vq$b@D%O~L+AgInLsdIGmQr9|CFb{vVv5p&MpxU%1 zF|a&?;TsoM+K4a!kFIY_s#fv}h#qDlM6fD}#xS5<;WvMlIM|Mh)_Xy}=3z4;#b1m1 z=(qs`frmCAZbNnZx$6Q)zrfvFT{yAD@-*XQV$Yey()Pg^S+xhRyt3+cy|i^0qHHFx zU(rb;ZMTGO0_XJI7(jd*J6g4)Ve$oHF&C%41B0W}O0%RwIHis85N60|RVS1ZN zpDhY>RH=WPB6GgX*>VwzvwR=SR?NTu zVUCof`2O@aKA(YwKlc`F<7{3#@ z#JMky`H54o`GH%0qK}{NBh}{!#%0Aw&jNKV=fr>h>?D27`LxkU80m8s>&Vol?vmN^ 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 diff --git a/tests/scripts/__init__.py b/tests/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/test_scripts.py b/tests/scripts/test_scripts.py new file mode 100644 index 0000000..840dd0a --- /dev/null +++ b/tests/scripts/test_scripts.py @@ -0,0 +1,115 @@ +import datetime +import os +import pickle +import unittest +from unittest import mock + +import arrow +import cfg4py +import numpy as np +import omicron +import pandas as pd +from omicron import cache +from coretypes import FrameType + +from omega.scripts import load_lua_script, update_unclosed_bar, close_frame +from tests import init_test_env + +cfg = cfg4py.get_instance() + + +async def prepare_test_data(): + """准备测试数据""" + min_bars = [ + (202207180931, 7.0, 7.20, 6.90, 7.02, 1e6, 1e6 * 7.0, 1.0), + (202207180932, 7.1, 7.21, 6.89, 7.03, 1e6, 1e6 * 7.1, 1.0), + (202207180933, 7.2, 7.22, 6.88, 7.04, 1e6, 1e6 * 7.2, 1.0), + (202207180934, 7.3, 7.23, 6.87, 7.05, 1e6, 1e6 * 7.3, 1.0), + (202207180935, 7.4, 7.24, 6.86, 7.06, 1e6, 1e6 * 7.4, 1.0), + (202207180936, 7.5, 7.25, 6.85, 7.07, 1e6, 1e6 * 7.5, 1.0), + ] + + codes = ["000001.XSHE", "000002.XSHE", "600003.XSHG"] + for code in codes: + for bar in min_bars: + await cache.security.hset( + f"bars:1m:{code}", + bar[0], + f"{bar[0]},{bar[1]:.2f},{bar[2]:.2f},{bar[3]:.2f},{bar[4]:.2f},{bar[5]:.2f},{bar[6]:.2f},{bar[7]:.2f}", + ) + + # unclosed = { + # "000001.XSHE": f"202207180935,7,7.22,6.88,7.04,{1e6},{1e6*7.2},1.0", + # "000002.XSHE": f"202207180935,7,7.22,6.88,7.04,{1e6},{1e6*7.2},1.0", + # "000003.XSHG": f"202207180935,7,7.22,6.88,7.04,{1e6},{1e6*7.2},1.0", + # } + + # await cache.security.hmset_dict("bars:5m:unclosed", **unclosed) + + +class ScriptTest(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self) -> None: + await init_test_env() + await omicron.init() + + await cache.sys.execute("FUNCTION", "FLUSH") + await load_lua_script() + + async def asyncTearDown(self) -> None: + return await omicron.close() + + async def test_load_script(self): + r = await cache.sys.execute("FUNCTION", "LIST") + funcs = r[0][5][0][:2] + funcs.extend(r[0][5][1][:2]) + self.assertIn("close_frame", funcs) + self.assertIn("update_unclosed", funcs) + + async def test_close_frame(self): + await prepare_test_data() + await cache.security.delete("bars:5m:unclosed") + await cache.security.delete("bars:5m:000001.XSHE") + + for i in range(5): + await cache.security.execute( + "fcall", "update_unclosed", 0, "5m", 202207180931 + i + ) + + await close_frame(FrameType.MIN5, datetime.datetime(2022, 7, 18, 9, 35)) + unclosed = await cache.security.hgetall("bars:5m:unclosed") + closed = await cache.security.hgetall("bars:5m:000001.XSHE") + exp = f"202207180935,7,7.24,6.86,7.06,{5e6:.0f},{36e6:.0f},1" + + self.assertEqual(exp, closed["202207180935"]) + self.assertTrue(len(unclosed) == 0) + + # raise exception + with mock.patch.object(cache.security, "execute", side_effect=Exception): + await close_frame(FrameType.MIN5, datetime.datetime(2022, 7, 18, 9, 35)) + + async def test_update_unclosed(self): + await prepare_test_data() + await cache.security.delete("bars:5m:unclosed") + + await update_unclosed_bar(FrameType.MIN5, datetime.datetime(2022, 7, 18, 9, 31)) + unclosed = await cache.security.hgetall("bars:5m:unclosed") + self.assertEqual( + f"202207180931,7,7.2,6.9,7.02,{1e6:.0f},{1e6*7:.0f},1", + unclosed["000001.XSHE"], + ) + + await cache.security.execute("fcall", "update_unclosed", 0, "5m", 202207180932) + + await cache.security.execute("fcall", "update_unclosed", 0, "5m", 202207180933) + + # now unclosed bar should have + unclosed = await cache.security.hgetall("bars:5m:unclosed") + closed = await cache.security.hgetall("bars:5m:000001.XSHE") + exp = f"202207180933,7,7.22,6.88,7.04,{3e6:.0f},{1e6*21.3:.0f},1" + self.assertEqual(exp, unclosed["000001.XSHE"]) + + # error handling + with mock.patch.object(cache.security, "execute", side_effect=Exception): + await update_unclosed_bar( + FrameType.MIN5, datetime.datetime(2022, 7, 18, 9, 31) + ) diff --git a/tox.ini b/tox.ini index 82a3084..9a5b27a 100644 --- a/tox.ini +++ b/tox.ini @@ -67,7 +67,6 @@ commands = rm -rf dist poetry build /bin/sh -c "pip uninstall zillionare-omicron -y" - /bin/sh -c "pip uninstall zillionare-omega-adaptors-jq -y" /bin/sh -c "pip install $(echo tests/packages/zillionare*.whl)" /bin/sh ./stop_service.sh /bin/sh ./start_service.sh