From f68db2a9321a09ad8ab2956baa3019a720ab2f61 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Thu, 21 Sep 2023 16:21:29 +0800 Subject: [PATCH] 0.9.29 update --- czsc/cmd.py | 1 - czsc/data/base.py | 2 +- czsc/eda.py | 7 +- czsc/enum.py | 8 + czsc/envs.py | 2 +- czsc/utils/bar_generator.py | 139 ++++++--- czsc/utils/minites_split.feather | Bin 0 -> 174474 bytes czsc/utils/st_components.py | 32 +++ ...36\345\244\232\345\215\263\347\251\272.py" | 129 ++++----- examples/develop/bar_end_time.py | 72 +++++ setup.py | 2 +- test/test_bar_generator.py | 264 +++++++++++++++++- 12 files changed, 543 insertions(+), 115 deletions(-) create mode 100644 czsc/utils/minites_split.feather create mode 100644 czsc/utils/st_components.py create mode 100644 examples/develop/bar_end_time.py diff --git a/czsc/cmd.py b/czsc/cmd.py index 83c8f5b61..076531dac 100644 --- a/czsc/cmd.py +++ b/czsc/cmd.py @@ -8,7 +8,6 @@ https://click.palletsprojects.com/en/8.0.x/quickstart/ """ import click -from loguru import logger @click.group() diff --git a/czsc/data/base.py b/czsc/data/base.py index 5a3ff8d49..80a42153b 100644 --- a/czsc/data/base.py +++ b/czsc/data/base.py @@ -17,6 +17,7 @@ "3600s": "60分钟", "1d": "日线"} freq_cn2gm = {v: k for k, v in freq_gm2cn.items()} + def jq_symbol_to_gm(symbol: str) -> str: """聚宽代码转掘金代码""" code, exchange = symbol.split(".") @@ -154,4 +155,3 @@ def save_symbols_to_ebk(symbols, file_ebk, source='ts'): tdx_symbols = [symbol_to_tdx(ts_code) for ts_code in symbols] with open(file_ebk, encoding='utf-8', mode='w') as f: f.write("\n".join(tdx_symbols)) - diff --git a/czsc/eda.py b/czsc/eda.py index 0aa4269ba..c1488b4b3 100644 --- a/czsc/eda.py +++ b/czsc/eda.py @@ -6,12 +6,11 @@ describe: 用于探索性分析的函数 """ import numpy as np -import pandas as pd def vwap(price: np.array, volume: np.array, **kwargs) -> float: """计算成交量加权平均价 - + :param price: 价格序列 :param volume: 成交量序列 :return: 平均价 @@ -26,7 +25,3 @@ def twap(price: np.array, **kwargs) -> float: :return: 平均价 """ return np.average(price) - - - - diff --git a/czsc/enum.py b/czsc/enum.py index 29c8daaf8..071f9f81a 100644 --- a/czsc/enum.py +++ b/czsc/enum.py @@ -39,10 +39,18 @@ def __str__(self): class Freq(Enum): Tick = "Tick" F1 = "1分钟" + F2 = "2分钟" + F3 = "3分钟" + F4 = "4分钟" F5 = "5分钟" + F6 = "6分钟" + F10 = "10分钟" + F12 = "12分钟" F15 = "15分钟" + F20 = "20分钟" F30 = "30分钟" F60 = "60分钟" + F120 = "120分钟" D = "日线" W = "周线" M = "月线" diff --git a/czsc/envs.py b/czsc/envs.py index 569d197a4..d79b07ad7 100644 --- a/czsc/envs.py +++ b/czsc/envs.py @@ -50,5 +50,5 @@ def get_bi_change_th(v: float = None) -> float: """ bi_change_th = v if v else os.environ.get('czsc_bi_change_th', '1') bi_change_th = float(bi_change_th) - assert 2 >= bi_change_th >= 0.5 or bi_change_th == -1, f"czsc_bi_change_th not in [0.5, 2]" + assert 2 >= bi_change_th >= 0.5 or bi_change_th == -1, "czsc_bi_change_th not in [0.5, 2]" return bi_change_th diff --git a/czsc/utils/bar_generator.py b/czsc/utils/bar_generator.py index 2e6862b4b..2d6c71dd3 100644 --- a/czsc/utils/bar_generator.py +++ b/czsc/utils/bar_generator.py @@ -6,9 +6,109 @@ describe: 从任意周期K线开始合成更高周期K线的工具类 """ import pandas as pd -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date from typing import List, Union, AnyStr from czsc.objects import RawBar, Freq +from pathlib import Path +from loguru import logger +from czsc.utils.calendar import next_trading_date + + +mss = pd.read_feather(Path(__file__).parent / "minites_split.feather") +freq_market_times, freq_edt_map = {}, {} +for _m, dfg in mss.groupby('market'): + for _f in [x for x in mss.columns if x.endswith("分钟")]: + freq_market_times[f"{_f}_{_m}"] = list(dfg[_f].unique()) + freq_edt_map[f"{_f}_{_m}"] = {k: v for k, v in dfg[["time", _f]].values} + + +def check_freq_and_market(time_seq: List[AnyStr]): + """检查时间序列是否为同一周期,是否为同一市场 + + :param time_seq: 时间序列,如 ['11:00', '15:00', '23:00', '01:00', '02:30'] + :return: + - freq K线周期 + - market 交易市场 + """ + assert len(time_seq) >= 2, "time_seq长度必须大于等于2" + res = {} + for key, tts in freq_market_times.items(): + if set(tts) == set(time_seq): + freq, market = key.split("_") + return freq, market + + if len(time_seq) < len(tts) * 0.9 or len(time_seq) > len(tts) * 1.1: + continue + + res[key] = len(set(time_seq).intersection(set(tts))) / len(tts) + freq, market = sorted(res.items(), key=lambda x: x[1], reverse=True)[0][0].split("_") + return freq, market + + +def freq_end_date(dt, freq: Union[Freq, AnyStr]): + """交易日结束时间计算""" + if not isinstance(dt, date): + dt = pd.to_datetime(dt).date() + if not isinstance(freq, Freq): + freq = Freq(freq) + + dt = pd.to_datetime(dt) + if freq == Freq.D: + return dt + + if freq == Freq.W: + return dt + timedelta(days=5 - dt.isoweekday()) + + if freq == Freq.Y: + return datetime(year=dt.year, month=12, day=31) + + if freq == Freq.M: + if dt.month == 12: + edt = datetime(year=dt.year, month=12, day=31) + else: + edt = datetime(year=dt.year, month=dt.month + 1, day=1) - timedelta(days=1) + return edt + + if freq == Freq.S: + dt_m = dt.month + if dt_m in [1, 2, 3]: + edt = datetime(year=dt.year, month=4, day=1) - timedelta(days=1) + elif dt_m in [4, 5, 6]: + edt = datetime(year=dt.year, month=7, day=1) - timedelta(days=1) + elif dt_m in [7, 8, 9]: + edt = datetime(year=dt.year, month=10, day=1) - timedelta(days=1) + else: + edt = datetime(year=dt.year, month=12, day=31) + return edt + + logger.warning(f'error: {dt} - {freq}') + return dt + + +def freq_end_time_V230921(dt: datetime, freq: Union[Freq, AnyStr], market="A股") -> datetime: + """A股与期货市场精确的获取 dt 对应的K线周期结束时间 + + :param dt: datetime + :param freq: Freq + :return: datetime + """ + assert market in ['A股', '期货', '默认'], "market 参数必须为 A股 或 期货 或 默认" + if not isinstance(freq, Freq): + freq = Freq(freq) + if dt.second > 0 or dt.microsecond > 0: + dt = dt.replace(second=0, microsecond=0) + timedelta(minutes=1) + + hm = dt.strftime("%H:%M") + key = f"{freq.value}_{market}" + if freq.value.endswith("分钟"): + h, m = freq_edt_map[key][hm].split(":") + edt = dt.replace(hour=int(h), minute=int(m)) + return edt + + if not ("15:00" > hm > "09:00") and market == "期货": + dt = next_trading_date(dt.strftime("%Y-%m-%d"), 1) + + return freq_end_date(dt.date(), freq) def freq_end_time(dt: datetime, freq: Union[Freq, AnyStr]) -> datetime: @@ -44,39 +144,7 @@ def freq_end_time(dt: datetime, freq: Union[Freq, AnyStr]) -> datetime: return edt # 处理 日、周、月、季、年 的结束时间 - dt = dt.replace(hour=0, minute=0) - - if freq == Freq.D: - return dt - - if freq == Freq.W: - sdt = dt + timedelta(days=5 - dt.isoweekday()) - return sdt - - if freq == Freq.M: - if dt.month == 12: - sdt = datetime(year=dt.year + 1, month=1, day=1) - timedelta(days=1) - else: - sdt = datetime(year=dt.year, month=dt.month + 1, day=1) - timedelta(days=1) - return sdt - - if freq == Freq.S: - dt_m = dt.month - if dt_m in [1, 2, 3]: - sdt = datetime(year=dt.year, month=4, day=1) - timedelta(days=1) - elif dt_m in [4, 5, 6]: - sdt = datetime(year=dt.year, month=7, day=1) - timedelta(days=1) - elif dt_m in [7, 8, 9]: - sdt = datetime(year=dt.year, month=10, day=1) - timedelta(days=1) - else: - sdt = datetime(year=dt.year + 1, month=1, day=1) - timedelta(days=1) - return sdt - - if freq == Freq.Y: - return datetime(year=dt.year, month=12, day=31) - - print(f'freq_end_time error: {dt} - {freq}') - return dt + return freq_end_date(dt.date(), freq) def resample_bars(df: pd.DataFrame, target_freq: Union[Freq, AnyStr], raw_bars=True, **kwargs): @@ -102,7 +170,8 @@ def resample_bars(df: pd.DataFrame, target_freq: Union[Freq, AnyStr], raw_bars=T if not isinstance(target_freq, Freq): target_freq = Freq(target_freq) - df['freq_edt'] = df['dt'].apply(lambda x: freq_end_time(x, target_freq)) + market = kwargs.get("market", "默认") + df['freq_edt'] = df['dt'].apply(lambda x: freq_end_time_V230921(x, target_freq, market)) dfk1 = df.groupby('freq_edt').agg( {'symbol': 'first', 'dt': 'last', 'open': 'first', 'close': 'last', 'high': 'max', 'low': 'min', 'vol': 'sum', 'amount': 'sum', 'freq_edt': 'last'}) diff --git a/czsc/utils/minites_split.feather b/czsc/utils/minites_split.feather new file mode 100644 index 0000000000000000000000000000000000000000..607f3cb536e9c0acf6d92699afbacf76072a20dd GIT binary patch literal 174474 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 z