mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-17 01:14:35 +08:00
Add Cache to avoid frequently loading calendar (#766)
This commit is contained in:
@@ -15,6 +15,16 @@ from .log import get_module_logger
|
|||||||
|
|
||||||
# init qlib
|
# init qlib
|
||||||
def init(default_conf="client", **kwargs):
|
def init(default_conf="client", **kwargs):
|
||||||
|
"""
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
**kwargs :
|
||||||
|
clear_mem_cache: str
|
||||||
|
the default value is True;
|
||||||
|
Will the memory cache be clear.
|
||||||
|
It is often used to improve performance when init will be called for multiple times
|
||||||
|
"""
|
||||||
from .config import C
|
from .config import C
|
||||||
from .data.cache import H
|
from .data.cache import H
|
||||||
|
|
||||||
@@ -28,7 +38,9 @@ def init(default_conf="client", **kwargs):
|
|||||||
logger.warning("Skip initialization because `skip_if_reg is True`")
|
logger.warning("Skip initialization because `skip_if_reg is True`")
|
||||||
return
|
return
|
||||||
|
|
||||||
H.clear()
|
clear_mem_cache = kwargs.pop("clear_mem_cache", True)
|
||||||
|
if clear_mem_cache:
|
||||||
|
H.clear()
|
||||||
C.set(default_conf, **kwargs)
|
C.set(default_conf, **kwargs)
|
||||||
|
|
||||||
# mount nfs
|
# mount nfs
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Two modes are supported
|
|||||||
- server
|
- server
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -18,7 +19,11 @@ import logging
|
|||||||
import platform
|
import platform
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Union
|
from typing import Optional, Union
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from qlib.utils.time import Freq
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@@ -296,7 +301,9 @@ class QlibConfig(Config):
|
|||||||
else:
|
else:
|
||||||
return QlibConfig.LOCAL_URI
|
return QlibConfig.LOCAL_URI
|
||||||
|
|
||||||
def get_data_uri(self, freq: str = None) -> Path:
|
def get_data_uri(self, freq: Optional[Union[str, Freq]] = None) -> Path:
|
||||||
|
if freq is not None:
|
||||||
|
freq = str(freq) # converting Freq to string
|
||||||
if freq is None or freq not in self.provider_uri:
|
if freq is None or freq not in self.provider_uri:
|
||||||
freq = QlibConfig.DEFAULT_FREQ
|
freq = QlibConfig.DEFAULT_FREQ
|
||||||
_provider_uri = self.provider_uri[freq]
|
_provider_uri = self.provider_uri[freq]
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ from .inst_processor import InstProcessor
|
|||||||
|
|
||||||
from ..log import get_module_logger
|
from ..log import get_module_logger
|
||||||
from ..utils.time import Freq
|
from ..utils.time import Freq
|
||||||
from ..utils.resam import resam_calendar
|
|
||||||
from .cache import DiskDatasetCache, DiskExpressionCache
|
from .cache import DiskDatasetCache, DiskExpressionCache
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
Wrapper,
|
Wrapper,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import pandas as pd
|
|||||||
from qlib.utils.time import Freq
|
from qlib.utils.time import Freq
|
||||||
from qlib.utils.resam import resam_calendar
|
from qlib.utils.resam import resam_calendar
|
||||||
from qlib.config import C
|
from qlib.config import C
|
||||||
|
from qlib.data.cache import H
|
||||||
from qlib.log import get_module_logger
|
from qlib.log import get_module_logger
|
||||||
from qlib.data.storage import CalendarStorage, InstrumentStorage, FeatureStorage, CalVT, InstKT, InstVT
|
from qlib.data.storage import CalendarStorage, InstrumentStorage, FeatureStorage, CalVT, InstKT, InstVT
|
||||||
|
|
||||||
@@ -33,15 +34,15 @@ class FileStorageMixin:
|
|||||||
if hasattr(self, _v):
|
if hasattr(self, _v):
|
||||||
return getattr(self, _v)
|
return getattr(self, _v)
|
||||||
if len(self.provider_uri) == 1 and C.DEFAULT_FREQ in self.provider_uri:
|
if len(self.provider_uri) == 1 and C.DEFAULT_FREQ in self.provider_uri:
|
||||||
freq = filter(
|
freq_l = filter(
|
||||||
lambda _freq: not _freq.endswith("_future"),
|
lambda _freq: not _freq.endswith("_future"),
|
||||||
map(lambda x: x.stem, self.dpm.get_data_uri(C.DEFAULT_FREQ).joinpath("calendars").glob("*.txt")),
|
map(lambda x: x.stem, self.dpm.get_data_uri(C.DEFAULT_FREQ).joinpath("calendars").glob("*.txt")),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
freq = self.provider_uri.keys()
|
freq_l = self.provider_uri.keys()
|
||||||
freq = list(freq)
|
freq_l = [Freq(freq) for freq in freq_l]
|
||||||
setattr(self, _v, freq)
|
setattr(self, _v, freq_l)
|
||||||
return freq
|
return freq_l
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uri(self) -> Path:
|
def uri(self) -> Path:
|
||||||
@@ -65,15 +66,28 @@ class FileCalendarStorage(FileStorageMixin, CalendarStorage):
|
|||||||
super(FileCalendarStorage, self).__init__(freq, future, **kwargs)
|
super(FileCalendarStorage, self).__init__(freq, future, **kwargs)
|
||||||
self.future = future
|
self.future = future
|
||||||
self.provider_uri = C.DataPathManager.format_provider_uri(provider_uri)
|
self.provider_uri = C.DataPathManager.format_provider_uri(provider_uri)
|
||||||
self.resample_freq = None
|
self.enable_read_cache = True # TODO: make it configurable
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file_name(self) -> str:
|
def file_name(self) -> str:
|
||||||
return f"{self.use_freq}_future.txt" if self.future else f"{self.use_freq}.txt".lower()
|
return f"{self._freq_file}_future.txt" if self.future else f"{self._freq_file}.txt".lower()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def use_freq(self) -> str:
|
def _freq_file(self) -> str:
|
||||||
return self.freq if self.resample_freq is None else self.resample_freq
|
"""the freq to read from file"""
|
||||||
|
if not hasattr(self, "_freq_file_cache"):
|
||||||
|
freq = Freq(self.freq)
|
||||||
|
if freq not in self.support_freq:
|
||||||
|
# NOTE: uri
|
||||||
|
# 1. If `uri` does not exist
|
||||||
|
# - Get the `min_uri` of the closest `freq` under the same "directory" as the `uri`
|
||||||
|
# - Read data from `min_uri` and resample to `freq`
|
||||||
|
|
||||||
|
freq = Freq.get_recent_freq(freq, self.support_freq)
|
||||||
|
if freq is None:
|
||||||
|
raise ValueError(f"can't find a freq from {self.support_freq} that can resample to {self.freq}!")
|
||||||
|
self._freq_file_cache = freq
|
||||||
|
return self._freq_file_cache
|
||||||
|
|
||||||
def _read_calendar(self, skip_rows: int = 0, n_rows: int = None) -> List[CalVT]:
|
def _read_calendar(self, skip_rows: int = 0, n_rows: int = None) -> List[CalVT]:
|
||||||
if not self.uri.exists():
|
if not self.uri.exists():
|
||||||
@@ -90,25 +104,21 @@ class FileCalendarStorage(FileStorageMixin, CalendarStorage):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def uri(self) -> Path:
|
def uri(self) -> Path:
|
||||||
freq = self.freq
|
return self.dpm.get_data_uri(self._freq_file).joinpath(f"{self.storage_name}s", self.file_name)
|
||||||
if freq not in self.support_freq:
|
|
||||||
# NOTE: uri
|
|
||||||
# 1. If `uri` does not exist
|
|
||||||
# - Get the `min_uri` of the closest `freq` under the same "directory" as the `uri`
|
|
||||||
# - Read data from `min_uri` and resample to `freq`
|
|
||||||
|
|
||||||
freq = Freq.get_recent_freq(freq, self.support_freq)
|
|
||||||
if freq is None:
|
|
||||||
raise ValueError(f"can't find a freq from {self.support_freq} that can resample to {self.freq}!")
|
|
||||||
self.resample_freq = freq
|
|
||||||
return self.dpm.get_data_uri(self.use_freq).joinpath(f"{self.storage_name}s", self.file_name)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self) -> List[CalVT]:
|
def data(self) -> List[CalVT]:
|
||||||
self.check()
|
self.check()
|
||||||
_calendar = self._read_calendar()
|
# If cache is enabled, then return cache directly
|
||||||
if self.resample_freq is not None:
|
if self.enable_read_cache:
|
||||||
_calendar = resam_calendar(np.array(list(map(pd.Timestamp, _calendar))), self.resample_freq, self.freq)
|
key = "orig_file" + str(self.uri)
|
||||||
|
if not key in H["c"]:
|
||||||
|
H["c"][key] = self._read_calendar()
|
||||||
|
_calendar = H["c"][key]
|
||||||
|
else:
|
||||||
|
_calendar = self._read_calendar()
|
||||||
|
if Freq(self._freq_file) != Freq(self.freq):
|
||||||
|
_calendar = resam_calendar(np.array(list(map(pd.Timestamp, _calendar))), self._freq_file, self.freq)
|
||||||
return _calendar
|
return _calendar
|
||||||
|
|
||||||
def _get_storage_freq(self) -> List[str]:
|
def _get_storage_freq(self) -> List[str]:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from . import lazy_sort_index
|
|||||||
from .time import Freq, cal_sam_minute
|
from .time import Freq, cal_sam_minute
|
||||||
|
|
||||||
|
|
||||||
def resam_calendar(calendar_raw: np.ndarray, freq_raw: str, freq_sam: str) -> np.ndarray:
|
def resam_calendar(calendar_raw: np.ndarray, freq_raw: Union[str, Freq], freq_sam: Union[str, Freq]) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Resample the calendar with frequency freq_raw into the calendar with frequency freq_sam
|
Resample the calendar with frequency freq_raw into the calendar with frequency freq_sam
|
||||||
Assumption:
|
Assumption:
|
||||||
@@ -28,36 +28,36 @@ def resam_calendar(calendar_raw: np.ndarray, freq_raw: str, freq_sam: str) -> np
|
|||||||
np.ndarray
|
np.ndarray
|
||||||
The calendar with frequency freq_sam
|
The calendar with frequency freq_sam
|
||||||
"""
|
"""
|
||||||
raw_count, freq_raw = Freq.parse(freq_raw)
|
freq_raw = Freq(freq_raw)
|
||||||
sam_count, freq_sam = Freq.parse(freq_sam)
|
freq_sam = Freq(freq_sam)
|
||||||
if not len(calendar_raw):
|
if not len(calendar_raw):
|
||||||
return calendar_raw
|
return calendar_raw
|
||||||
|
|
||||||
# if freq_sam is xminute, divide each trading day into several bars evenly
|
# if freq_sam is xminute, divide each trading day into several bars evenly
|
||||||
if freq_sam == Freq.NORM_FREQ_MINUTE:
|
if freq_sam.base == Freq.NORM_FREQ_MINUTE:
|
||||||
if freq_raw != Freq.NORM_FREQ_MINUTE:
|
if freq_raw.base != Freq.NORM_FREQ_MINUTE:
|
||||||
raise ValueError("when sampling minute calendar, freq of raw calendar must be minute or min")
|
raise ValueError("when sampling minute calendar, freq of raw calendar must be minute or min")
|
||||||
else:
|
else:
|
||||||
if raw_count > sam_count:
|
if freq_raw.count > freq_sam.count:
|
||||||
raise ValueError("raw freq must be higher than sampling freq")
|
raise ValueError("raw freq must be higher than sampling freq")
|
||||||
_calendar_minute = np.unique(list(map(lambda x: cal_sam_minute(x, sam_count), calendar_raw)))
|
_calendar_minute = np.unique(list(map(lambda x: cal_sam_minute(x, freq_sam.count), calendar_raw)))
|
||||||
return _calendar_minute
|
return _calendar_minute
|
||||||
|
|
||||||
# else, convert the raw calendar into day calendar, and divide the whole calendar into several bars evenly
|
# else, convert the raw calendar into day calendar, and divide the whole calendar into several bars evenly
|
||||||
else:
|
else:
|
||||||
_calendar_day = np.unique(list(map(lambda x: pd.Timestamp(x.year, x.month, x.day, 0, 0, 0), calendar_raw)))
|
_calendar_day = np.unique(list(map(lambda x: pd.Timestamp(x.year, x.month, x.day, 0, 0, 0), calendar_raw)))
|
||||||
if freq_sam == Freq.NORM_FREQ_DAY:
|
if freq_sam.base == Freq.NORM_FREQ_DAY:
|
||||||
return _calendar_day[::sam_count]
|
return _calendar_day[:: freq_sam.count]
|
||||||
|
|
||||||
elif freq_sam == Freq.NORM_FREQ_WEEK:
|
elif freq_sam.base == Freq.NORM_FREQ_WEEK:
|
||||||
_day_in_week = np.array(list(map(lambda x: x.dayofweek, _calendar_day)))
|
_day_in_week = np.array(list(map(lambda x: x.dayofweek, _calendar_day)))
|
||||||
_calendar_week = _calendar_day[np.ediff1d(_day_in_week, to_begin=-1) < 0]
|
_calendar_week = _calendar_day[np.ediff1d(_day_in_week, to_begin=-1) < 0]
|
||||||
return _calendar_week[::sam_count]
|
return _calendar_week[:: freq_sam.count]
|
||||||
|
|
||||||
elif freq_sam == Freq.NORM_FREQ_MONTH:
|
elif freq_sam.base == Freq.NORM_FREQ_MONTH:
|
||||||
_day_in_month = np.array(list(map(lambda x: x.day, _calendar_day)))
|
_day_in_month = np.array(list(map(lambda x: x.day, _calendar_day)))
|
||||||
_calendar_month = _calendar_day[np.ediff1d(_day_in_month, to_begin=-1) < 0]
|
_calendar_month = _calendar_day[np.ediff1d(_day_in_month, to_begin=-1) < 0]
|
||||||
return _calendar_month[::sam_count]
|
return _calendar_month[:: freq_sam.count]
|
||||||
else:
|
else:
|
||||||
raise ValueError("sampling freq must be xmin, xd, xw, xm")
|
raise ValueError("sampling freq must be xmin, xd, xw, xm")
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Time related utils are compiled in this script
|
|||||||
"""
|
"""
|
||||||
import bisect
|
import bisect
|
||||||
from datetime import datetime, time, date
|
from datetime import datetime, time, date
|
||||||
from typing import List, Tuple, Union
|
from typing import List, Optional, Tuple, Union
|
||||||
import functools
|
import functools
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -69,13 +69,29 @@ class Freq:
|
|||||||
NORM_FREQ_MONTH = "month"
|
NORM_FREQ_MONTH = "month"
|
||||||
NORM_FREQ_WEEK = "week"
|
NORM_FREQ_WEEK = "week"
|
||||||
NORM_FREQ_DAY = "day"
|
NORM_FREQ_DAY = "day"
|
||||||
NORM_FREQ_MINUTE = "minute"
|
NORM_FREQ_MINUTE = "min" # using min instead of minute for align with Qlib's data filename
|
||||||
SUPPORT_CAL_LIST = [NORM_FREQ_MINUTE, NORM_FREQ_DAY] # FIXME: this list should from data
|
SUPPORT_CAL_LIST = [NORM_FREQ_MINUTE, NORM_FREQ_DAY] # FIXME: this list should from data
|
||||||
|
|
||||||
MIN_CAL = get_min_cal()
|
MIN_CAL = get_min_cal()
|
||||||
|
|
||||||
def __init__(self, freq: str) -> None:
|
def __init__(self, freq: Union[str, "Freq"]) -> None:
|
||||||
self.count, self.base = self.parse(freq)
|
if isinstance(freq, str):
|
||||||
|
self.count, self.base = self.parse(freq)
|
||||||
|
elif isinstance(freq, Freq):
|
||||||
|
self.count, self.base = freq.count, freq.base
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f"This type of input is not supported")
|
||||||
|
|
||||||
|
def __eq__(self, freq):
|
||||||
|
freq = Freq(freq)
|
||||||
|
return freq.count == self.count and freq.base == self.base
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# trying to align to the filename of Qlib: day, 30min, 5min, 1min...
|
||||||
|
return f"{self.count if self.count != 1 or self.base != 'day' else ''}{self.base}"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}({str(self)})"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse(freq: str) -> Tuple[int, str]:
|
def parse(freq: str) -> Tuple[int, str]:
|
||||||
@@ -159,14 +175,14 @@ class Freq:
|
|||||||
Freq.NORM_FREQ_WEEK: 7 * 60 * 24,
|
Freq.NORM_FREQ_WEEK: 7 * 60 * 24,
|
||||||
Freq.NORM_FREQ_MONTH: 30 * 7 * 60 * 24,
|
Freq.NORM_FREQ_MONTH: 30 * 7 * 60 * 24,
|
||||||
}
|
}
|
||||||
left_freq = Freq.parse(left_frq)
|
left_freq = Freq(left_frq)
|
||||||
left_minutes = left_freq[0] * minutes_map[left_freq[1]]
|
left_minutes = left_freq.count * minutes_map[left_freq.base]
|
||||||
right_freq = Freq.parse(right_freq)
|
right_freq = Freq(right_freq)
|
||||||
right_minutes = right_freq[0] * minutes_map[right_freq[1]]
|
right_minutes = right_freq.count * minutes_map[right_freq.base]
|
||||||
return left_minutes - right_minutes
|
return left_minutes - right_minutes
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_recent_freq(base_freq: str, freq_list: List[str]) -> str:
|
def get_recent_freq(base_freq: Union[str, "Freq"], freq_list: List[Union[str, "Freq"]]) -> Optional["Freq"]:
|
||||||
"""Get the closest freq to base_freq from freq_list
|
"""Get the closest freq to base_freq from freq_list
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -176,17 +192,22 @@ class Freq:
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
if the recent frequency is found
|
||||||
|
Freq
|
||||||
|
else:
|
||||||
|
None
|
||||||
"""
|
"""
|
||||||
|
base_freq = Freq(base_freq)
|
||||||
# use the nearest freq greater than 0
|
# use the nearest freq greater than 0
|
||||||
_freq_minutes = []
|
_freq_minutes = []
|
||||||
min_freq = None
|
min_freq = None
|
||||||
for _freq in freq_list:
|
for _freq in freq_list:
|
||||||
|
freq = Freq(_freq)
|
||||||
_min_delta = Freq.get_min_delta(base_freq, _freq)
|
_min_delta = Freq.get_min_delta(base_freq, _freq)
|
||||||
if _min_delta < 0:
|
if _min_delta < 0:
|
||||||
continue
|
continue
|
||||||
if min_freq is None:
|
if min_freq is None:
|
||||||
min_freq = (_min_delta, _freq)
|
min_freq = (_min_delta, str(_freq))
|
||||||
continue
|
continue
|
||||||
min_freq = min_freq if min_freq[0] <= _min_delta else (_min_delta, _freq)
|
min_freq = min_freq if min_freq[0] <= _min_delta else (_min_delta, _freq)
|
||||||
return min_freq[1] if min_freq else None
|
return min_freq[1] if min_freq else None
|
||||||
|
|||||||
Reference in New Issue
Block a user