1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-07 13:00:58 +08:00
This commit is contained in:
wangwenxi.handsome
2021-08-27 10:48:10 +00:00
committed by you-n-g
parent 7ee4a207bc
commit 43a8f502ed
6 changed files with 109 additions and 137 deletions

View File

@@ -18,7 +18,7 @@ from ..config import C, REG_CN
from ..utils.resam import resam_ts_data, ts_data_last
from ..log import get_module_logger
from .order import Order, OrderDir, OrderHelper
from .high_performance_ds import PandasQuote, CN1min_NumpyQuote
from .high_performance_ds import PandasQuote, CN1minNumpyQuote
class Exchange:
@@ -36,7 +36,7 @@ class Exchange:
close_cost=0.0025,
min_cost=5,
extra_quote=None,
quote_cls=CN1min_NumpyQuote,
quote_cls=CN1minNumpyQuote,
**kwargs,
):
"""__init__
@@ -327,20 +327,20 @@ class Exchange:
"""
if direction is None:
buy_limit = self.quote.get_data(stock_id, start_time, end_time, fields="limit_buy", method="all")
sell_limit = self.quote.get_data(stock_id, start_time, end_time, fields="limit_sell", method="all")
buy_limit = self.quote.get_data(stock_id, start_time, end_time, field="limit_buy", method="all")
sell_limit = self.quote.get_data(stock_id, start_time, end_time, field="limit_sell", method="all")
return buy_limit or sell_limit
elif direction == Order.BUY:
return self.quote.get_data(stock_id, start_time, end_time, fields="limit_buy", method="all")
return self.quote.get_data(stock_id, start_time, end_time, field="limit_buy", method="all")
elif direction == Order.SELL:
return self.quote.get_data(stock_id, start_time, end_time, fields="limit_sell", method="all")
return self.quote.get_data(stock_id, start_time, end_time, field="limit_sell", method="all")
else:
raise ValueError(f"direction {direction} is not supported!")
def check_stock_suspended(self, stock_id, start_time, end_time):
# is suspended
if stock_id in self.quote.get_all_stock():
return self.quote.get_data(stock_id, start_time, end_time) is None
return self.quote.get_data(stock_id, start_time, end_time, "$close") is None
else:
return True
@@ -411,10 +411,10 @@ class Exchange:
return self.quote.get_data(stock_id, start_time, end_time, method=method)
def get_close(self, stock_id, start_time, end_time, method=ts_data_last):
return self.quote.get_data(stock_id, start_time, end_time, fields="$close", method=method)
return self.quote.get_data(stock_id, start_time, end_time, field="$close", method=method)
def get_volume(self, stock_id, start_time, end_time, method="sum"):
return self.quote.get_data(stock_id, start_time, end_time, fields="$volume", method=method)
return self.quote.get_data(stock_id, start_time, end_time, field="$volume", method=method)
def get_deal_price(self, stock_id, start_time, end_time, direction: OrderDir, method=ts_data_last):
if direction == OrderDir.SELL:
@@ -423,7 +423,7 @@ class Exchange:
pstr = self.buy_price
else:
raise NotImplementedError(f"This type of input is not supported")
deal_price = self.quote.get_data(stock_id, start_time, end_time, fields=pstr, method=method)
deal_price = self.quote.get_data(stock_id, start_time, end_time, field=pstr, method=method)
if method is not None and (np.isclose(deal_price, 0.0) or np.isnan(deal_price)):
self.logger.warning(f"(stock_id:{stock_id}, trade_time:{(start_time, end_time)}, {pstr}): {deal_price}!!!")
self.logger.warning(f"setting deal_price to close price")
@@ -441,7 +441,7 @@ class Exchange:
assert start_time is not None and end_time is not None, "the time range must be given"
if stock_id not in self.quote.get_all_stock():
return None
return self.quote.get_data(stock_id, start_time, end_time, fields="$factor", method=ts_data_last)
return self.quote.get_data(stock_id, start_time, end_time, field="$factor", method=ts_data_last)
def generate_amount_position_from_weight_position(
self, weight_position, cash, start_time, end_time, direction=OrderDir.BUY
@@ -684,7 +684,7 @@ class Exchange:
order.stock_id,
order.start_time,
order.end_time,
fields=limit[1],
field=limit[1],
method="sum",
)
vol_limit_num.append(limit_value)
@@ -693,7 +693,7 @@ class Exchange:
order.stock_id,
order.start_time,
order.end_time,
fields=limit[1],
field=limit[1],
method=ts_data_last,
)
vol_limit_num.append(limit_value - dealt_order_amount[order.stock_id])

View File

@@ -2,21 +2,19 @@
# Licensed under the MIT License.
from builtins import ValueError, isinstance
from functools import lru_cache
import logging
from typing import List, Text, Union, Callable, Iterable, Dict
from collections import OrderedDict
import inspect
import bisect
import pandas as pd
import numpy as np
from ..utils.index_data import IndexData
from ..utils.index_data import IndexData, SingleData
from ..utils.resam import resam_ts_data, ts_data_last
from ..log import get_module_logger
from ..utils.time import if_single_data
from ..utils.time import is_single_value
class BaseQuote:
@@ -39,10 +37,10 @@ class BaseQuote:
stock_id: str,
start_time: Union[pd.Timestamp, str],
end_time: Union[pd.Timestamp, str],
fields: Union[str, None] = None,
field: Union[str],
method: Union[str, Callable, None] = None,
) -> Union[None, Union[int, float, bool], "IndexData"]:
"""get the specific fields of stock data during start time and end_time,
) -> Union[None, int, float, bool, "IndexData"]:
"""get the specific field of stock data during start time and end_time,
and apply method to the data.
Example:
@@ -63,22 +61,13 @@ class BaseQuote:
this function is used for three case:
1. Both fields and method are not None. It returns int/float/bool.
print(get_data(stock_id="SH600000", start_time="2010-01-04", end_time="2010-01-06", fields="$close", method="last"))
1. method is not None. It returns int/float/bool.
print(get_data(stock_id="SH600000", start_time="2010-01-04", end_time="2010-01-06", field="$close", method="last"))
85.713585
2. Both fields and method are None. It returns np.ndarray.
print(get_data(stock_id="SH600000", start_time="2010-01-04", end_time="2010-01-06", fields=None, method=None))
[
[86.778313, 16162960.0],
[87.433578, 28117442.0],
[85.713585, 23632884.0],
]
3. fields is not None, and method is None. It returns IndexData.
print(get_data(stock_id="SH600000", start_time="2010-01-04", end_time="2010-01-06", fields="$close", method=None))
2. method is None. It returns IndexData.
print(get_data(stock_id="SH600000", start_time="2010-01-04", end_time="2010-01-06", field="$close", method=None))
IndexData([86.778313, 87.433578, 85.713585], [2010-01-04, 2010-01-05, 2010-01-06])
@@ -89,7 +78,7 @@ class BaseQuote:
closed start time for backtest
end_time : Union[pd.Timestamp, str]
closed end time for backtest
fields : Union[str, None]
field : str
the columns of data to fetch
method : Union[str, Callable, None]
the method apply to data.
@@ -97,7 +86,8 @@ class BaseQuote:
Return
----------
Union[None, Union[int, float, bool], IndexData]
Union[None, int, float, bool, IndexData]
None means there is no stock data from data source.
please refer to Example as following.
"""
@@ -115,32 +105,21 @@ class PandasQuote(BaseQuote):
def get_all_stock(self):
return self.data.keys()
def get_data(self, stock_id, start_time, end_time, fields=None, method=None):
if fields is None and method is not None:
raise ValueError(f"method must be None when fields is None")
if fields is None:
stock_data = resam_ts_data(self.data[stock_id], start_time, end_time, method=method)
elif isinstance(fields, str):
stock_data = resam_ts_data(self.data[stock_id][fields], start_time, end_time, method=method)
else:
raise ValueError(f"fields must be None, str")
def get_data(self, stock_id, start_time, end_time, field, method=None):
stock_data = resam_ts_data(self.data[stock_id][field], start_time, end_time, method=method)
if stock_data is None:
return None
elif isinstance(stock_data, (bool, np.bool_, int, float, np.signedinteger, np.floating)):
elif isinstance(stock_data, (bool, np.bool_, int, float, np.number)):
return stock_data
elif isinstance(stock_data, pd.Series):
return IndexData.Series(stock_data)
elif isinstance(stock_data, pd.DataFrame):
return stock_data.values
else:
raise ValueError(f"stock data from resam_ts_data must be a number, pd.Series or pd.DataFrame")
class CN1min_NumpyQuote(BaseQuote):
class CN1minNumpyQuote(BaseQuote):
def __init__(self, quote_df: pd.DataFrame):
"""CN1min_NumpyQuote
"""CN1minNumpyQuote
Parameters
----------
@@ -153,48 +132,37 @@ class CN1min_NumpyQuote(BaseQuote):
for stock_id, stock_val in quote_df.groupby(level="instrument"):
quote_dict[stock_id] = IndexData.DataFrame(stock_val.droplevel(level="instrument"))
self.data = quote_dict
self.freq = np.timedelta64(1, "m")
self.freq = pd.Timedelta(minutes=1)
def get_all_stock(self):
return self.data.keys()
@lru_cache(maxsize=512)
def get_data(self, stock_id, start_time, end_time, fields=None, method=None):
if fields is None and method is not None:
raise ValueError(f"method must be None when fields is None")
def get_data(self, stock_id, start_time, end_time, field, method=None):
# check stock id
if stock_id not in self.get_all_stock():
return None
# single data
# If it don't consider the classification of single data, it will consume a lot of time.
if if_single_data(start_time, end_time, self.freq):
if is_single_value(start_time, end_time, self.freq):
now_index_map = self.data[stock_id].index_map
now_columns_map = self.data[stock_id].columns_map
if start_time not in now_index_map:
return None
if fields is None:
return self.data[stock_id].values[now_index_map[start_time]]
else:
return self.data[stock_id].values[now_index_map[start_time], now_columns_map[fields]]
return self.data[stock_id].values[now_index_map[start_time], now_columns_map[field]]
# multi data
else:
if fields is None and method is None:
stock_data = self.data[stock_id].loc(start_time, end_time)
if stock_data.empty:
return None
else:
return stock_data.values
elif fields is not None and method is None:
stock_data = self.data[stock_id].loc(start_time, end_time, fields)
if method is None:
stock_data = self.data[stock_id].loc(start_time, end_time, field)
if stock_data.empty:
return None
else:
return stock_data
elif fields is not None and method is not None:
stock_data = self.data[stock_id].loc(start_time, end_time, fields)
else:
stock_data = self.data[stock_id].loc(start_time, end_time, field)
if stock_data.empty:
return None
elif len(stock_data) == 1:
@@ -231,6 +199,20 @@ class BaseSingleMetric:
"""
def __init__(self, metric: Union[dict, pd.Series]):
"""Single data structure for each metric.
Parameters
----------
metric : Union[dict, pd.Series]
keys/index is stock_id, value is the metric value.
for example:
SH600068 NaN
SH600079 1.0
SH600266 NaN
...
SZ300692 NaN
SZ300719 NaN,
"""
raise NotImplementedError(f"Please implement the `__init__` method")
def __add__(self, other: Union["BaseSingleMetric", int, float]) -> "BaseSingleMetric":
@@ -277,7 +259,7 @@ class BaseSingleMetric:
def abs(self) -> "BaseSingleMetric":
raise NotImplementedError(f"Please implement the `abs` method")
def astype(self, type: type) -> "BaseSingleMetric":
def astype(self, dtype: type) -> "BaseSingleMetric":
raise NotImplementedError(f"Please implement the `astype` method")
@property
@@ -316,7 +298,8 @@ class BaseOrderIndicator:
to inherit the BaseSingleMetric.
"""
def __init__(self):
def __init__(self, data):
self.data = data
self.logger = get_module_logger("online operator")
def assign(self, col: str, metric: Union[dict, pd.Series]):
@@ -358,8 +341,13 @@ class BaseOrderIndicator:
BaseSingleMetric
new metric.
"""
raise NotImplementedError(f"Please implement the 'transfer' method")
func_sig = inspect.signature(func).parameters.keys()
func_kwargs = {sig: self.data[sig] for sig in func_sig}
tmp_metric = func(**func_kwargs)
if new_col is not None:
self.data[new_col] = tmp_metric
else:
return tmp_metric
def get_metric_series(self, metric: str) -> pd.Series:
"""return the single metric with pd.Series format.
@@ -378,8 +366,8 @@ class BaseOrderIndicator:
raise NotImplementedError(f"Please implement the 'get_metric_series' method")
def get_index_data(self, metric) -> IndexData.Series:
"""get one metric with the format of IndexData.Series
def get_index_data(self, metric) -> SingleData:
"""get one metric with the format of SingleData
Parameters
----------
@@ -389,7 +377,7 @@ class BaseOrderIndicator:
Return
------
IndexData.Series
one metric with the format of IndexData.Series
one metric with the format of SingleData
"""
raise NotImplementedError(f"Please implement the 'get_index_data' method")
@@ -431,6 +419,9 @@ class BaseOrderIndicator:
class SingleMetric(BaseSingleMetric):
def __init__(self, metric):
self.metric = metric
def __add__(self, other):
if isinstance(other, (int, float)):
return self.__class__(self.metric + other)
@@ -502,7 +493,7 @@ class SingleMetric(BaseSingleMetric):
class PandasSingleMetric(SingleMetric):
"""Each SingleMetric is based on pd.Series."""
def __init__(self, metric: Union[dict, pd.Series]):
def __init__(self, metric: Union[dict, pd.Series] = {}):
if isinstance(metric, dict):
self.metric = pd.Series(metric)
elif isinstance(metric, pd.Series):
@@ -522,13 +513,17 @@ class PandasSingleMetric(SingleMetric):
def abs(self):
return self.__class__(self.metric.abs())
def astype(self, type):
return self.__class__(self.metric.astype(type))
def astype(self, dtype):
return self.__class__(self.metric.astype(dtype))
@property
def empty(self):
return self.metric.empty
@property
def index(self):
return list(self.metric.index)
def add(self, other, fill_value=None):
return self.__class__(self.metric.add(other.metric, fill_value=fill_value))
@@ -538,6 +533,9 @@ class PandasSingleMetric(SingleMetric):
def apply(self, func: Callable):
return self.__class__(self.metric.apply(func))
def reindex(self, index, fill_value):
return self.__class__(self.metric.reindex(index, fill_value=fill_value))
class PandasOrderIndicator(BaseOrderIndicator):
"""
@@ -552,15 +550,6 @@ class PandasOrderIndicator(BaseOrderIndicator):
def assign(self, col: str, metric: Union[dict, pd.Series]):
self.data[col] = PandasSingleMetric(metric)
def transfer(self, func: Callable, new_col: str = None) -> Union[None, PandasSingleMetric]:
func_sig = inspect.signature(func).parameters.keys()
func_kwargs = {sig: self.data[sig] for sig in func_sig}
tmp_metric = func(**func_kwargs)
if new_col is not None:
self.data[new_col] = tmp_metric
else:
return tmp_metric
def get_index_data(self, metric):
if metric in self.data:
return IndexData.Series(self.data[metric].metric)
@@ -577,7 +566,7 @@ class PandasOrderIndicator(BaseOrderIndicator):
return {k: v.metric for k, v in self.data.items()}
@staticmethod
def sum_all_indicators(order_indicator, indicators: list, metrics: Union[str, List[str]], fill_value=None):
def sum_all_indicators(order_indicator, indicators: list, metrics: Union[str, List[str]], fill_value=0):
if isinstance(metrics, str):
metrics = [metrics]
for metric in metrics:
@@ -589,26 +578,17 @@ class PandasOrderIndicator(BaseOrderIndicator):
class NumpyOrderIndicator(BaseOrderIndicator):
"""
The data structure is OrderedDict(str: IndexData.Series).
The data structure is OrderedDict(str: SingleData).
Each IndexData.Series is one metric.
Str is the name of metric.
"""
def __init__(self):
self.data: Dict[str, IndexData.Series] = OrderedDict()
self.data: Dict[str, SingleData] = OrderedDict()
def assign(self, col: str, metric: dict):
self.data[col] = IndexData.Series(metric)
def transfer(self, func: Callable, new_col: str = None) -> Union[None, IndexData.Series]:
func_sig = inspect.signature(func).parameters.keys()
func_kwargs = {sig: self.data[sig] for sig in func_sig}
tmp_metric = func(**func_kwargs)
if new_col is not None:
self.data[new_col] = tmp_metric
else:
return tmp_metric
def get_index_data(self, metric):
if metric in self.data:
return self.data[metric]
@@ -616,7 +596,7 @@ class NumpyOrderIndicator(BaseOrderIndicator):
return IndexData.Series()
def get_metric_series(self, metric: str) -> Union[pd.Series]:
return self.data[metric].to_pd_series()
return self.data[metric].to_series()
def to_series(self) -> Dict[str, pd.Series]:
tmp_metric_dict = {}

View File

@@ -109,7 +109,7 @@ class Order:
return self.direction * 2 - 1
@staticmethod
def parse_dir(direction: Union[str, int, np.integer, OrderDir, np.ndarray]) -> OrderDir:
def parse_dir(direction: Union[str, int, np.integer, OrderDir, np.ndarray]) -> Union[OrderDir, np.ndarray]:
if isinstance(direction, OrderDir):
return direction
elif isinstance(direction, (int, float, np.integer, np.floating)):

View File

@@ -4,15 +4,14 @@
from collections import OrderedDict
import pathlib
from typing import Dict, List, Tuple
from typing import Dict, List, Tuple, Union
import numpy as np
import pandas as pd
from qlib.backtest.exchange import Exchange
from qlib.backtest.order import BaseTradeDecision, Order, OrderDir
from .high_performance_ds import PandasOrderIndicator, NumpyOrderIndicator
from .high_performance_ds import PandasOrderIndicator, NumpyOrderIndicator, SingleMetric
from ..utils.index_data import IndexData, SingleData
from ..tests.config import CSI300_BENCH
from ..utils.resam import get_higher_eq_freq_feature, resam_ts_data
@@ -305,8 +304,9 @@ class Indicator:
def _update_order_fulfill_rate(self):
def func(deal_amount, amount):
# deal_amount is np.NaN when there is no inner decision. So full fill rate is 0.
tmp_deal_amount = deal_amount.replace({np.NaN: 0})
# deal_amount is np.NaN or None when there is no inner decision. So full fill rate is 0.
tmp_deal_amount = deal_amount.reindex(amount.index, 0)
tmp_deal_amount = tmp_deal_amount.replace({np.NaN: 0})
return tmp_deal_amount / amount
self.order_indicator.transfer(func, "ffr")
@@ -385,7 +385,7 @@ class Indicator:
if price_s is None:
return None, None
if isinstance(price_s, (int, float, np.signedinteger, np.floating)):
if isinstance(price_s, (int, float, np.number)):
price_s = IndexData.Series(price_s, [trade_start_time])
elif isinstance(price_s, SingleData):
pass
@@ -400,7 +400,7 @@ class Indicator:
if agg == "vwap":
volume_s = trade_exchange.get_volume(inst, trade_start_time, trade_end_time, method=None)
if isinstance(volume_s, (int, float, np.floating)):
if isinstance(volume_s, (int, float, np.number)):
volume_s = IndexData.Series(volume_s, [trade_start_time])
volume_s = volume_s.reindex(price_s.index)
elif agg == "twap":
@@ -414,7 +414,7 @@ class Indicator:
def _agg_base_price(
self,
inner_order_indicators: List[Dict[str, pd.Series]],
inner_order_indicators: List[Dict[str, Union[SingleMetric, SingleData]]],
decision_list: List[Tuple[BaseTradeDecision, pd.Timestamp, pd.Timestamp]],
trade_exchange: Exchange,
pa_config: dict = {},

View File

@@ -35,7 +35,7 @@ class IndexData:
return MultiData(data, index, columns)
@staticmethod
def concat(data_list, axis=0):
def concat(data_list: Union["SingleData"], axis=0) -> "MultiData":
"""concat all SingleData by index.
TODO: now just for SingleData.
@@ -50,7 +50,7 @@ class IndexData:
the MultiData with ndim == 2
"""
if axis == 0:
raise NotImplementedError(f"please implement this fuc when axis == 0")
raise NotImplementedError(f"please implement this func when axis == 0")
elif axis == 1:
# get all index and row
all_index = set()
@@ -90,7 +90,7 @@ class BaseData:
raise NotImplementedError(f"please implement _align_index func")
def __add__(self, other):
if isinstance(other, (int, float, np.floating)):
if isinstance(other, (int, float, np.number)):
return self.__class__(self.data + other, *self.index_columns)
elif isinstance(other, self.__class__):
tmp_data1, tmp_data2 = self._align_index(other)
@@ -99,7 +99,7 @@ class BaseData:
return NotImplemented
def __sub__(self, other):
if isinstance(other, (int, float, np.floating)):
if isinstance(other, (int, float, np.number)):
return self.__class__(self.data - other, *self.index_columns)
elif isinstance(other, self.__class__):
tmp_data1, tmp_data2 = self._align_index(other)
@@ -108,7 +108,7 @@ class BaseData:
return NotImplemented
def __rsub__(self, other):
if isinstance(other, (int, float, np.floating)):
if isinstance(other, (int, float, np.number)):
return self.__class__(other - self.data, *self.index_columns)
elif isinstance(other, self.__class__):
tmp_data1, tmp_data2 = self._align_index(other)
@@ -117,7 +117,7 @@ class BaseData:
return NotImplemented
def __mul__(self, other):
if isinstance(other, (int, float, np.floating)):
if isinstance(other, (int, float, np.number)):
return self.__class__(self.data * other, *self.index_columns)
elif isinstance(other, self.__class__):
tmp_data1, tmp_data2 = self._align_index(other)
@@ -126,7 +126,7 @@ class BaseData:
return NotImplemented
def __truediv__(self, other):
if isinstance(other, (int, float, np.floating)):
if isinstance(other, (int, float, np.number)):
return self.__class__(self.data / other, *self.index_columns)
elif isinstance(other, self.__class__):
tmp_data1, tmp_data2 = self._align_index(other)
@@ -135,7 +135,7 @@ class BaseData:
return NotImplemented
def __eq__(self, other):
if isinstance(other, (int, float, np.floating)):
if isinstance(other, (int, float, np.number)):
return self.__class__(self.data == other, *self.index_columns)
elif isinstance(other, self.__class__):
tmp_data1, tmp_data2 = self._align_index(other)
@@ -144,7 +144,7 @@ class BaseData:
return NotImplemented
def __gt__(self, other):
if isinstance(other, (int, float, np.floating)):
if isinstance(other, (int, float, np.number)):
return self.__class__(self.data > other, *self.index_columns)
elif isinstance(other, self.__class__):
tmp_data1, tmp_data2 = self._align_index(other)
@@ -153,7 +153,7 @@ class BaseData:
return NotImplemented
def __lt__(self, other):
if isinstance(other, (int, float, np.floating)):
if isinstance(other, (int, float, np.number)):
return self.__class__(self.data < other, *self.index_columns)
elif isinstance(other, self.__class__):
tmp_data1, tmp_data2 = self._align_index(other)
@@ -169,9 +169,9 @@ class BaseData:
tmp_data = np.absolute(self.data)
return self.__class__(tmp_data, *self.index_columns)
def astype(self, type):
def astype(self, dtype):
"""change the type of data."""
tmp_data = self.data.astype(type)
tmp_data = self.data.astype(dtype)
return self.__class__(tmp_data, *self.index_columns)
def replace(self, to_replace: dict):
@@ -234,7 +234,7 @@ class BaseData:
class SingleData(BaseData):
def __init__(self, data: Union[int, float, np.floating, list, np.ndarray] = [], index: Union[list, pd.Index] = []):
def __init__(self, data: Union[int, float, np.number, list] = [], index: Union[list, pd.Index] = []):
"""A data structure of index and numpy data.
It's used to replace pd.Series due to high-speed.
@@ -301,6 +301,8 @@ class SingleData(BaseData):
SingleData
reindex data
"""
if self.index == index:
return self
tmp_data = np.full(len(index), fill_value, dtype=np.float64)
for index_id, index_item in enumerate(index):
if index_item in self.index:
@@ -323,17 +325,7 @@ class SingleData(BaseData):
"""
return dict(zip(self.index, self.data.tolist()))
def to_frame(self):
"""convert SingleData to MultiData.
Returns
-------
MultiData
data with the MultiData format.
"""
return MultiData(self.data[:, np.newaxis], self.index)
def to_pd_series(self):
def to_series(self):
return pd.Series(self.data, index=self.index)
def __getitem__(self, index: Union["SingleData", int, str]):

View File

@@ -38,8 +38,8 @@ def get_min_cal(shift: int = 0) -> List[time]:
return cal
def if_single_data(start_time, end_time, freq):
"""Is there only one piece of data to obtain.
def is_single_value(start_time, end_time, freq):
"""Is there only one piece of data for cn stock market.
Parameters
----------