From cd049b9801c1ba731a42648ca752ae2c5f03b075 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Mon, 4 Sep 2023 17:33:41 +0800 Subject: [PATCH 01/22] 0.9.29 start coding --- czsc/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/czsc/__init__.py b/czsc/__init__.py index 138172b09..68c43f208 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -66,10 +66,10 @@ ) -__version__ = "0.9.28" +__version__ = "0.9.29" __author__ = "zengbin93" __email__ = "zeng_bin8888@163.com" -__date__ = "20230820" +__date__ = "20230904" def welcome(): From 65f70d45cedd023d6b61a0f7efe67720c0b9d4fe Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Sun, 10 Sep 2023 11:40:34 +0800 Subject: [PATCH 02/22] =?UTF-8?q?0.9.29=20=E6=96=B0=E5=A2=9E=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E6=97=A5=E5=8E=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- czsc/__init__.py | 1 - czsc/connectors/qmt_connector.py | 4 +- czsc/connectors/research.py | 2 - czsc/fsa/im.py | 2 +- czsc/fsa/spreed_sheets.py | 4 +- czsc/sensors/feature.py | 28 ++++++------- czsc/sensors/plates.py | 1 - czsc/sensors/utils.py | 6 +-- czsc/traders/__init__.py | 3 +- czsc/traders/base.py | 32 ++------------- czsc/traders/performance.py | 4 +- czsc/traders/sig_parse.py | 5 +-- czsc/traders/weight_backtest.py | 5 ++- czsc/utils/calendar.py | 63 ++++++++++++++++++++++++++++++ czsc/utils/china_calendar.feather | Bin 0 -> 46186 bytes test/test_calendar.py | 18 +++++++++ 16 files changed, 113 insertions(+), 65 deletions(-) create mode 100644 czsc/utils/calendar.py create mode 100644 czsc/utils/china_calendar.feather create mode 100644 test/test_calendar.py diff --git a/czsc/__init__.py b/czsc/__init__.py index 68c43f208..076fd8c2b 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -26,7 +26,6 @@ stock_holds_performance, DummyBacktest, SignalsParser, - get_signals_by_conf, get_signals_config, get_signals_freqs, WeightBacktest, diff --git a/czsc/connectors/qmt_connector.py b/czsc/connectors/qmt_connector.py index 3495943cd..ce26309e2 100644 --- a/czsc/connectors/qmt_connector.py +++ b/czsc/connectors/qmt_connector.py @@ -88,10 +88,10 @@ def get_kline(symbol, period, start_time, end_time, count=-1, dividend_type='fro """ start_time = pd.to_datetime(start_time).strftime('%Y%m%d%H%M%S') if '1d' == period: - end_time = pd.to_datetime(end_time).replace(hour=15,minute=0).strftime('%Y%m%d%H%M%S') + end_time = pd.to_datetime(end_time).replace(hour=15, minute=0).strftime('%Y%m%d%H%M%S') else: end_time = pd.to_datetime(end_time).strftime('%Y%m%d%H%M%S') - + if kwargs.get("download_hist", True): xtdata.download_history_data(symbol, period=period, start_time=start_time, end_time=end_time) diff --git a/czsc/connectors/research.py b/czsc/connectors/research.py index 9c0d2c6c1..8c9ad92e5 100644 --- a/czsc/connectors/research.py +++ b/czsc/connectors/research.py @@ -56,5 +56,3 @@ def get_raw_bars(symbol, freq, sdt, edt, fq='前复权', **kwargs): return [] _bars = czsc.resample_bars(kline, freq, raw_bars=True) return _bars - - diff --git a/czsc/fsa/im.py b/czsc/fsa/im.py index 4832c25c3..cc59d0e05 100644 --- a/czsc/fsa/im.py +++ b/czsc/fsa/im.py @@ -95,7 +95,7 @@ def send_text(self, text, receive_id, receive_id_type='open_id'): payload = {"receive_id": receive_id, "content": {"text": text}, "msg_type": "text"} return self.send(payload, receive_id_type) - def send_image(self, image_path, receive_id, receive_id_type='open_id'): + def send_image(self, image_path, receive_id, receive_id_type='open_id'): """发送图片 :param image_path: 图片路径 diff --git a/czsc/fsa/spreed_sheets.py b/czsc/fsa/spreed_sheets.py index c050ab71b..d67a01038 100644 --- a/czsc/fsa/spreed_sheets.py +++ b/czsc/fsa/spreed_sheets.py @@ -281,7 +281,7 @@ def append(self, token, sheet_id, df: pd.DataFrame, batch_size=2000, overwrite=T if df.empty: logger.warning("待写入的数据为空,不执行写入操作") return - + if overwrite: self.delete_values(token, sheet_id) cols = df.columns.tolist() @@ -298,7 +298,7 @@ def append(self, token, sheet_id, df: pd.DataFrame, batch_size=2000, overwrite=T assert df.shape[1] == col_count, f"df 列数 {df.shape[1]} 与表格列数 {col_count} 不一致" for i in range(0, len(df), batch_size): - dfi = df.iloc[i : i + batch_size] + dfi = df.iloc[i: i + batch_size] si = i + start_index + 1 ei = si + batch_size vol_range = f"{sheet_id}!A{si}:{string.ascii_uppercase[col_count - 1]}{ei}" diff --git a/czsc/sensors/feature.py b/czsc/sensors/feature.py index ef36b21ed..9b540db5d 100644 --- a/czsc/sensors/feature.py +++ b/czsc/sensors/feature.py @@ -73,7 +73,7 @@ def _one_symbol_features(self, symbol): except Exception as e: logger.error(f"{symbol} error: {e}") return pd.DataFrame() - + def get_features(self) -> pd.DataFrame: """ 此方法检索self.symbols中每个符号的K线数据, @@ -96,7 +96,7 @@ def get_features(self) -> pd.DataFrame: res.append(df) return pd.concat(res, ignore_index=True) - + def layering(self, feature, min_q, max_q): """分层回测""" df = self.dfs.copy() @@ -106,7 +106,7 @@ def layering(self, feature, min_q, max_q): dfm = df1.groupby('dt').agg({'n1b': 'mean'}).fillna(0) logger.info(f"分层累计收益 {feature} {min_q}-{max_q}:{dfm['n1b'].sum():.4f}; {daily_performance((dfm['n1b'] / 10000).to_list())}") return dfm - + def report(self): """打印特征分析报告""" results_path = self.kwargs.get('results_path', None) @@ -130,7 +130,7 @@ def report(self): _ = self.layering(feature, 0.9, 1) _ = self.layering(feature, 0, 0.1) _ = self.layering(feature, 0, 0.05) - + df1['年月'] = df1['dt'].apply(lambda x: x.strftime("%Y年%m月")) dfm = df1.groupby('年月').agg({'ic': 'mean'}) logger.info(f"特征 {feature} 与未来1日收益的相关系数月度描述:{dfm.describe().round(4).to_dict()}") @@ -139,15 +139,15 @@ def report(self): class FixedNumberSelector: """选择固定数量(等权)的交易品种 - + 可优化项: 1. 传入 res_path, 将分析过程和分析结果保存下来 2. 支持传入大盘择时信号,例如:大盘择时信号为空头时,多头只平不开 """ - + def __init__(self, dfs, k, d, **kwargs): """ - + :param dfs: pd.DataFrame,所有交易品种的特征打分数据,必须包含以下列:dt, open, close, high, low, vol, amount, score;数据样例: =================== ========= ======= ======= ======= ======= ======== ========= ======== ============= @@ -163,7 +163,7 @@ def __init__(self, dfs, k, d, **kwargs): 2017-01-12 00:00:00 000001.SZ 956.441 958.536 960.631 956.441 42800677 391869402 10.9411 2.5563e-11 2017-01-13 00:00:00 000001.SZ 957.488 959.583 962.726 955.393 43430137 397601906 -32.7865 2.51649e-11 2017-01-16 00:00:00 000001.SZ 958.536 957.488 959.583 950.155 68316586 623025820 21.9292 -3.19607e-11 - =================== ========= ======= ======= ======= ======= ======== ========= ======== ============= + =================== ========= ======= ======= ======= ======= ======== ========= ======== ============= :param k: int,每期固定选择的数量 :param d: int,每期允许变动的数量 @@ -193,7 +193,7 @@ def __preprocess(self): last_dt_map = {dt: dts[i-1] for i, dt in enumerate(dts)} self.dts, self.last_dt_map = dts, last_dt_map self.score_map = {dt: dfg[['symbol', 'dt', 'open', 'close', 'high', 'low', 'score', 'n1b']].copy() for dt, dfg in self.dfs.groupby('dt')} - + def __deal_one_time(self, dt): """单次调整记录""" k, d, is_stocks = self.k, self.d, self.is_stocks @@ -217,8 +217,8 @@ def __deal_one_time(self, dt): _df_operates = [{'symbol': row['symbol'], 'dt': dt, 'action': 'buy', 'price': row['close']} for _, row in _df.iterrows()] self.operates[dt] = pd.DataFrame(_df_operates) - return - + return + # 有持仓的情况 score = self.score_map[dt] last_dt = self.last_dt_map[dt] @@ -239,7 +239,7 @@ def __deal_one_time(self, dt): assert len(buy_symbols) == len(sell_symbols), "买入品种数量必须等于卖出品种数量" assert len(keep_symbols + buy_symbols) == k, "保持品种数量+买入品种数量必须等于k" _df = score[score.symbol.isin(keep_symbols + buy_symbols)].sort_values(by='score', ascending=False) - + if len(_df) != k: logger.warning(f"选择的品种数量不等于{k},当前只有{len(_df)}个品种") @@ -250,9 +250,9 @@ def __deal_one_time(self, dt): last_holds['edge'] = last_holds.apply(lambda row: row['edge'] - self.operate_fee if row['symbol'] in sell_symbols else row['edge'], axis=1) self.holds[last_dt] = last_holds - _sell_operates = [{'symbol': row['symbol'], 'dt': dt, 'action': 'sell', 'price': row['close']} + _sell_operates = [{'symbol': row['symbol'], 'dt': dt, 'action': 'sell', 'price': row['close']} for _, row in score[score.symbol.isin(sell_symbols)].iterrows()] _buy_operates = [{'symbol': row['symbol'], 'dt': dt, 'action': 'buy', 'price': row['close']} - for _, row in score[score.symbol.isin(buy_symbols)].iterrows()] + for _, row in score[score.symbol.isin(buy_symbols)].iterrows()] _df_operates = pd.DataFrame(_sell_operates + _buy_operates) self.operates[dt] = _df_operates diff --git a/czsc/sensors/plates.py b/czsc/sensors/plates.py index 9a5c0aa4a..49f8ae193 100644 --- a/czsc/sensors/plates.py +++ b/czsc/sensors/plates.py @@ -251,4 +251,3 @@ def create_holds(self, mean_col, mean_col_bins, sort_col, sort_col_bins, max_num from czsc.traders.performance import stock_holds_performance stock_holds_performance(self.dc, dfh, res_path=res_path) return dfh - diff --git a/czsc/sensors/utils.py b/czsc/sensors/utils.py index b501a5f6f..76384b59d 100644 --- a/czsc/sensors/utils.py +++ b/czsc/sensors/utils.py @@ -8,11 +8,9 @@ import numpy as np from tqdm import tqdm from collections import Counter -from typing import Callable, List, AnyStr +from typing import List from sklearn.preprocessing import KBinsDiscretizer -from deprecated import deprecated - -from ..data import TsDataCache, freq_cn2ts +from ..data import TsDataCache def discretizer(df: pd.DataFrame, col: str, n_bins=20, encode='ordinal', strategy='quantile'): diff --git a/czsc/traders/__init__.py b/czsc/traders/__init__.py index df764413f..50142e6de 100644 --- a/czsc/traders/__init__.py +++ b/czsc/traders/__init__.py @@ -6,8 +6,7 @@ describe: 交易员(traders):使用 CZSC 分析工具进行择时策略的开发,交易等 """ from czsc.traders.base import ( - CzscSignals, CzscTrader, generate_czsc_signals, check_signals_acc, get_unique_signals, - get_signals_by_conf + CzscSignals, CzscTrader, generate_czsc_signals, check_signals_acc, get_unique_signals ) from czsc.traders.performance import ( diff --git a/czsc/traders/base.py b/czsc/traders/base.py index 362103859..a29f8c78d 100644 --- a/czsc/traders/base.py +++ b/czsc/traders/base.py @@ -29,7 +29,7 @@ class CzscSignals: """缠中说禅技术分析理论之多级别信号计算""" - def __init__(self, bg: BarGenerator = None, **kwargs): + def __init__(self, bg: Optional[BarGenerator] = None, **kwargs): """ :param bg: K线合成器 @@ -69,12 +69,14 @@ def get_signals_by_conf(self): """通过信号参数配置获取信号 信号参数配置,格式如下: + signals_config = [ {'name': 'czsc.signals.tas_ma_base_V221101', 'freq': '日线', 'di': 1, 'ma_type': 'SMA', 'timeperiod': 5}, {'name': 'czsc.signals.tas_ma_base_V221101', 'freq': '日线', 'di': 5, 'ma_type': 'SMA', 'timeperiod': 5}, {'name': 'czsc.signals.tas_double_ma_V221203', 'freq': '日线', 'di': 1, 'ma_seq': (5, 20), 'th': 100}, {'name': 'czsc.signals.tas_double_ma_V221203', 'freq': '日线', 'di': 5, 'ma_seq': (5, 20), 'th': 100}, ] + :return: 信号字典 """ s = OrderedDict() @@ -152,32 +154,6 @@ def update_signals(self, bar: RawBar): self.s.update(last_bar.__dict__) -@deprecated(version="0.9.16", reason="请使用 CzscSignals 类") -def get_signals_by_conf(cat: CzscSignals, conf): - """通过信号参数配置获取信号 - - :param cat: - :param conf: 信号参数配置,格式如下: - conf = [ - {'name': 'czsc.signals.tas_ma_base_V221101', 'freq': '日线', 'di': 1, 'ma_type': 'SMA', 'timeperiod': 5}, - {'name': 'czsc.signals.tas_ma_base_V221101', 'freq': '日线', 'di': 5, 'ma_type': 'SMA', 'timeperiod': 5}, - {'name': 'czsc.signals.tas_double_ma_V221203', 'freq': '日线', 'di': 1, 'ma_seq': (5, 20), 'th': 100}, - {'name': 'czsc.signals.tas_double_ma_V221203', 'freq': '日线', 'di': 5, 'ma_seq': (5, 20), 'th': 100}, - ] - :return: 信号字典 - """ - s = OrderedDict({"symbol": cat.symbol, "dt": cat.end_dt, "close": cat.latest_price}) - for param in conf: - param = dict(param) - sig_func = import_by_name(param.pop('name')) - freq = param.pop('freq', None) - if freq in cat.kas: # 如果指定了 freq,那么就使用 CZSC 对象作为输入 - s.update(sig_func(cat.kas[freq], **param)) # type: ignore - else: # 否则使用 CAT 作为输入 - s.update(sig_func(cat, **param)) - return s - - def generate_czsc_signals(bars: List[RawBar], signals_config: List[dict], sdt: Union[AnyStr, datetime] = "20170101", init_n: int = 500, df=False, **kwargs): """使用 CzscSignals 生成信号 @@ -409,7 +385,7 @@ def get_ensemble_pos(self, method: Union[AnyStr, Callable] = None) -> float: return pos - def get_position(self, name: str) -> Position: + def get_position(self, name: str) -> Optional[Position]: """获取指定名称的仓位策略对象 :param name: 仓位名称 diff --git a/czsc/traders/performance.py b/czsc/traders/performance.py index 76c96c936..98f707fbf 100644 --- a/czsc/traders/performance.py +++ b/czsc/traders/performance.py @@ -69,7 +69,7 @@ def stock_holds_performance(dc: TsDataCache, dfh, res_path): # 绘制收益曲线 plt.close() - fig = plt.figure(figsize=(13, 4*len(index_list))) + fig = plt.figure(figsize=(13, 4 * len(index_list))) axes = fig.subplots(len(index_list), 1, sharex=True) for i, _index in enumerate(index_list): ax = axes[i] @@ -325,5 +325,3 @@ def combine_dates_and_pairs(dates: list, pairs: pd.DataFrame, results_path): tp_old.agg_to_excel(os.path.join(results_path, "原始交易评价.xlsx")) tp_new.agg_to_excel(os.path.join(results_path, "组合过滤评价.xlsx")) return tp_old, tp_new - - diff --git a/czsc/traders/sig_parse.py b/czsc/traders/sig_parse.py index b1cfc77d9..20393a4c3 100644 --- a/czsc/traders/sig_parse.py +++ b/czsc/traders/sig_parse.py @@ -3,7 +3,7 @@ author: zengbin93 email: zeng_bin8888@163.com create_dt: 2023/3/29 10:04 -describe: +describe: """ import re from loguru import logger @@ -61,7 +61,7 @@ def parse_params(self, name, signal): return None try: - params = parse(pats, key).named # type: ignore + params = parse(pats, key).named # type: ignore if 'di' in params: params['di'] = int(params['di']) @@ -140,4 +140,3 @@ def get_signals_freqs(signals_seq: List) -> List[str]: if _freqs: freqs.extend(_freqs) return [x for x in sorted_freqs if x in freqs] - diff --git a/czsc/traders/weight_backtest.py b/czsc/traders/weight_backtest.py index cfcbca974..6a970d375 100644 --- a/czsc/traders/weight_backtest.py +++ b/czsc/traders/weight_backtest.py @@ -70,7 +70,7 @@ def __init__(self, dfw, digits=2, **kwargs) -> None: :param dfw: pd.DataFrame, columns = ['dt', 'symbol', 'weight', 'price'], 持仓权重数据,其中 - dt 为K线结束时间, + dt 为K线结束时间,必须是连续的交易时间序列,不允许有时间断层 symbol 为合约代码, weight 为K线结束时间对应的持仓权重, price 为结束时间对应的交易价格,可以是当前K线的收盘价,或者下一根K线的开盘价,或者未来N根K线的TWAP、VWAP等 @@ -109,6 +109,7 @@ def get_symbol_daily(self, symbol): :param symbol: str,合约代码 :return: pd.DataFrame,品种每日收益率, + columns = ['date', 'symbol', 'edge', 'return', 'cost'] 其中 date 为交易日, @@ -143,7 +144,7 @@ def get_symbol_pairs(self, symbol): """获取某个合约的开平交易记录""" dfs = self.dfw[self.dfw['symbol'] == symbol].copy() dfs['volume'] = (dfs['weight'] * pow(10, self.digits)).astype(int) - dfs['bar_id'] = list(range(1, len(dfs)+1)) + dfs['bar_id'] = list(range(1, len(dfs) + 1)) # 根据权重变化生成开平仓记录 operates = [] diff --git a/czsc/utils/calendar.py b/czsc/utils/calendar.py new file mode 100644 index 000000000..16825b23b --- /dev/null +++ b/czsc/utils/calendar.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" +author: zengbin93 +email: zeng_bin8888@163.com +create_dt: 2023/9/10 17:53 +describe: A股+期货的交易日历 +""" +import pandas as pd +from pathlib import Path + + +def __prepare_cal(): + """使用tushare获取交易日历,保存到本地""" + import pandas as pd + import tushare as ts + + pro = ts.pro_api() + exchanges = ['SSE', 'SZSE', 'CFFEX', 'SHFE', 'CZCE', 'DCE', 'INE'] + res = [] + for exchange in exchanges: + df = pro.trade_cal(exchange=exchange, start_date='20100101', end_date='20251231') + res.append(df) + df = pd.concat(res, ignore_index=True) + df['is_open'] = df['is_open'].astype(int) + dfc = pd.pivot_table(df, index='cal_date', columns='exchange', values='is_open').fillna(0).reset_index() + dfc['cal_date'] = pd.to_datetime(dfc['cal_date']) + dfc.to_feather('calendar.feather') + + # 验证:是不是所有交易所的交易日都一样 + dfc = dfc[dfc['cal_date'] >= '2021-01-01'] + dfc['sum'] = dfc[exchanges].sum(axis=1) + assert dfc['sum'].value_counts() + + # 所有国内交易所的交易日历都是一样的 + df = pro.trade_cal(exchange="SSE", start_date='20100101', end_date='20251231') + df['cal_date'] = pd.to_datetime(df['cal_date']) + df.sort_values('cal_date', inplace=True, ascending=True) + df = df.reset_index(drop=True) + df[['cal_date', 'is_open']].to_feather('china_calendar.feather') + + +calendar = pd.read_feather(Path(__file__).parent / "china_calendar.feather") + + +def is_trading_date(date): + """判断是否是交易日""" + date = pd.to_datetime(date) + is_open = calendar[calendar['cal_date'] == date].iloc[0]['is_open'] + return is_open == 1 + + +def next_trading_date(date, n=1): + """获取未来第N个交易日""" + date = pd.to_datetime(date) + df = calendar[calendar['cal_date'] >= date] + return df[df['is_open'] == 1].iloc[n - 1]['cal_date'] + + +def prev_trading_date(date, n=1): + """获取过去第N个交易日""" + date = pd.to_datetime(date) + df = calendar[calendar['cal_date'] <= date] + return df[df['is_open'] == 1].iloc[-n]['cal_date'] diff --git a/czsc/utils/china_calendar.feather b/czsc/utils/china_calendar.feather new file mode 100644 index 0000000000000000000000000000000000000000..6a343c186e88e899947520c13e675e6fca6487f2 GIT binary patch literal 46186 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 XX&S>ajL&5A_~H8S=- Date: Sun, 10 Sep 2023 11:41:50 +0800 Subject: [PATCH 03/22] update --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8721ed6f8..1a4d7c2f2 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ packages=find_packages(exclude=['test', 'images', 'docs', 'examples', 'hist']), include_package_data=True, install_requires=install_requires, - package_data={'': ['data/*.csv']}, + package_data={'': ['utils/china_calendar.feather']}, classifiers=[ 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 3.7', From eb09521a9198be592aba031d47dc3961c5824d6f Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Sun, 10 Sep 2023 14:55:18 +0800 Subject: [PATCH 04/22] 0.9.29 update --- czsc/__init__.py | 7 +++++++ czsc/utils/calendar.py | 42 ++++++++++-------------------------------- test/test_calendar.py | 8 ++++++++ 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/czsc/__init__.py b/czsc/__init__.py index 076fd8c2b..fd234eeda 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -64,6 +64,13 @@ empty_cache_path, ) +from czsc.utils.calendar import ( + is_trading_date, + next_trading_date, + prev_trading_date, + get_trading_dates, +) + __version__ = "0.9.29" __author__ = "zengbin93" diff --git a/czsc/utils/calendar.py b/czsc/utils/calendar.py index 16825b23b..a846c335c 100644 --- a/czsc/utils/calendar.py +++ b/czsc/utils/calendar.py @@ -9,36 +9,6 @@ from pathlib import Path -def __prepare_cal(): - """使用tushare获取交易日历,保存到本地""" - import pandas as pd - import tushare as ts - - pro = ts.pro_api() - exchanges = ['SSE', 'SZSE', 'CFFEX', 'SHFE', 'CZCE', 'DCE', 'INE'] - res = [] - for exchange in exchanges: - df = pro.trade_cal(exchange=exchange, start_date='20100101', end_date='20251231') - res.append(df) - df = pd.concat(res, ignore_index=True) - df['is_open'] = df['is_open'].astype(int) - dfc = pd.pivot_table(df, index='cal_date', columns='exchange', values='is_open').fillna(0).reset_index() - dfc['cal_date'] = pd.to_datetime(dfc['cal_date']) - dfc.to_feather('calendar.feather') - - # 验证:是不是所有交易所的交易日都一样 - dfc = dfc[dfc['cal_date'] >= '2021-01-01'] - dfc['sum'] = dfc[exchanges].sum(axis=1) - assert dfc['sum'].value_counts() - - # 所有国内交易所的交易日历都是一样的 - df = pro.trade_cal(exchange="SSE", start_date='20100101', end_date='20251231') - df['cal_date'] = pd.to_datetime(df['cal_date']) - df.sort_values('cal_date', inplace=True, ascending=True) - df = df.reset_index(drop=True) - df[['cal_date', 'is_open']].to_feather('china_calendar.feather') - - calendar = pd.read_feather(Path(__file__).parent / "china_calendar.feather") @@ -52,12 +22,20 @@ def is_trading_date(date): def next_trading_date(date, n=1): """获取未来第N个交易日""" date = pd.to_datetime(date) - df = calendar[calendar['cal_date'] >= date] + df = calendar[calendar['cal_date'] > date] return df[df['is_open'] == 1].iloc[n - 1]['cal_date'] def prev_trading_date(date, n=1): """获取过去第N个交易日""" date = pd.to_datetime(date) - df = calendar[calendar['cal_date'] <= date] + df = calendar[calendar['cal_date'] < date] return df[df['is_open'] == 1].iloc[-n]['cal_date'] + + +def get_trading_dates(sdt, edt): + """获取两个日期之间的所有交易日""" + sdt = pd.to_datetime(sdt) + edt = pd.to_datetime(edt) + df = calendar[(calendar['cal_date'] >= sdt) & (calendar['cal_date'] <= edt)] + return df[df['is_open'] == 1]['cal_date'].tolist() diff --git a/test/test_calendar.py b/test/test_calendar.py index 684e65d26..329100e42 100644 --- a/test/test_calendar.py +++ b/test/test_calendar.py @@ -11,8 +11,16 @@ def test_is_trading_date(): def test_next_trading_date(): assert next_trading_date('2023-09-09') == pd.Timestamp('2023-09-11') assert next_trading_date('2023-09-09', n=2) == pd.Timestamp('2023-09-12') + assert next_trading_date('2023-09-12', n=1) == pd.Timestamp('2023-09-13') def test_prev_trading_date(): assert prev_trading_date('2023-09-09') == pd.Timestamp('2023-09-08') assert prev_trading_date('2023-09-09', n=2) == pd.Timestamp('2023-09-07') + assert prev_trading_date('2023-09-07', n=1) == pd.Timestamp('2023-09-06') + + +def test_get_trading_dates(): + from czsc.utils.calendar import get_trading_dates + dates = get_trading_dates('2023-09-08', '2023-09-12') + assert dates == pd.to_datetime(['2023-09-08', '2023-09-11', '2023-09-12']).tolist() From 2c1f0298ecadc322d0ff0f8604bd650d651e61d5 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Sun, 10 Sep 2023 15:07:54 +0800 Subject: [PATCH 05/22] update --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 867fd745a..2231ac947 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -5,7 +5,7 @@ name: Python package on: push: - branches: [ master, V0.9.28 ] + branches: [ master, V0.9.29 ] pull_request: branches: [ master ] From da862417ee176ca37aa6b7a22368484350894ab9 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Sun, 10 Sep 2023 22:49:46 +0800 Subject: [PATCH 06/22] 0.9.29 delete signals merged --- ...36\345\244\232\345\215\263\347\251\272.py" | 124 +++++++++++++ examples/cal_ensemble_weight.py | 22 --- examples/evaluate_holds_by_csp.py | 20 -- .../merged/adtm_up_dw_line_V230603.py | 74 -------- .../merged/amv_up_dw_line_V230603.py | 65 ------- .../merged/asi_up_dw_line_V230603.py | 86 --------- .../signals_dev/merged/bar_eight_V230702.py | 88 --------- .../signals_dev/merged/bar_end_V221211.py | 64 ------- .../signals_dev/merged/bar_tnr_V230629.py | 91 --------- .../signals_dev/merged/bar_tnr_V230630.py | 84 --------- .../merged/bar_window_ps_V230731.py | 83 --------- .../merged/bar_window_ps_V230801.py | 60 ------ .../merged/bar_window_std_V230731.py | 86 --------- .../merged/bias_up_dw_line_V230604.py | 83 --------- .../signals_dev/merged/byi_fx_num_V230628.py | 65 ------- .../signals_dev/merged/check_atr_cache.py | 24 --- .../signals_dev/merged/check_boll_cache.py | 28 --- .../signals_dev/merged/check_cci_cache.py | 24 --- .../signals_dev/merged/check_macd_cache.py | 28 --- .../signals_dev/merged/check_sar_cache.py | 24 --- .../merged/clv_up_dw_line_V230605.py | 62 ------- .../merged/cmo_up_dw_line_V230605.py | 66 ------- .../merged/cvolp_up_dw_line_V230612.py | 76 -------- .../signals_dev/merged/cxt_bi_end_V230618.py | 125 ------------- .../signals_dev/merged/cxt_bi_end_V230815.py | 53 ------ .../signals_dev/merged/cxt_bi_stop_V230815.py | 61 ------- .../merged/cxt_bi_trend_V230824.py | 77 -------- .../merged/cxt_eleven_bi_V230622.py | 130 ------------- .../signals_dev/merged/cxt_five_bi_V230619.py | 107 ----------- .../merged/cxt_intraday_V230701.py | 98 ---------- .../signals_dev/merged/cxt_nine_bi_V230621.py | 172 ------------------ .../merged/cxt_range_oscillation_V230620.py | 88 --------- .../merged/cxt_seven_bi_V230620.py | 131 ------------- .../merged/cxt_three_bi_V230618.py | 92 ---------- .../signals_dev/merged/cxt_ubi_end_V230816.py | 85 --------- .../merged/dema_up_dw_line_V230605.py | 63 ------- .../merged/demakder_up_dw_line_V230605.py | 101 ---------- .../merged/emv_up_dw_line_V230605.py | 62 ------- .../merged/holds_concepts_effect.py | 87 --------- .../merged/kcatr_up_dw_line_V230823.py | 74 -------- .../merged/lverage_up_dw_line_V230824.py | 83 --------- examples/signals_dev/merged/ntmdk_V230824.py | 56 ------ .../merged/obv_up_dw_line_V230719.py | 95 ---------- .../signals_dev/merged/obvm_line_V230610.py | 86 --------- .../merged/pos_fix_exit_V230624.py | 144 --------------- .../signals_dev/merged/pos_fx_stop_V230414.py | 83 --------- .../signals_dev/merged/pos_holds_V230414.py | 83 --------- .../signals_dev/merged/pos_holds_V230807.py | 85 --------- .../merged/pos_profit_loss_V230624.py | 91 --------- .../signals_dev/merged/pos_status_V230808.py | 85 --------- .../signals_dev/merged/tas_angle_V230802.py | 77 -------- .../signals_dev/merged/tas_atr_V230630.py | 76 -------- .../merged/tas_atr_stop_V230424.py | 113 ------------ .../merged/tas_cross_status_V230619.py | 103 ----------- .../merged/tas_cross_status_V230624.py | 101 ---------- .../merged/tas_cross_status_V230625.py | 98 ---------- .../merged/tas_double_ma_V230512.py | 96 ---------- .../merged/tas_low_trend_V230627.py | 90 --------- .../signals_dev/merged/tas_rumi_V230704.py | 92 ---------- .../merged/tas_sar_base_V230424.py | 101 ---------- .../signals_dev/merged/vol_window_V230731.py | 76 -------- .../signals_dev/merged/vol_window_V230801.py | 55 ------ examples/use_cross_sectional.py | 26 --- 63 files changed, 124 insertions(+), 4904 deletions(-) create mode 100644 "examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" delete mode 100644 examples/cal_ensemble_weight.py delete mode 100644 examples/evaluate_holds_by_csp.py delete mode 100644 examples/signals_dev/merged/adtm_up_dw_line_V230603.py delete mode 100644 examples/signals_dev/merged/amv_up_dw_line_V230603.py delete mode 100644 examples/signals_dev/merged/asi_up_dw_line_V230603.py delete mode 100644 examples/signals_dev/merged/bar_eight_V230702.py delete mode 100644 examples/signals_dev/merged/bar_end_V221211.py delete mode 100644 examples/signals_dev/merged/bar_tnr_V230629.py delete mode 100644 examples/signals_dev/merged/bar_tnr_V230630.py delete mode 100644 examples/signals_dev/merged/bar_window_ps_V230731.py delete mode 100644 examples/signals_dev/merged/bar_window_ps_V230801.py delete mode 100644 examples/signals_dev/merged/bar_window_std_V230731.py delete mode 100644 examples/signals_dev/merged/bias_up_dw_line_V230604.py delete mode 100644 examples/signals_dev/merged/byi_fx_num_V230628.py delete mode 100644 examples/signals_dev/merged/check_atr_cache.py delete mode 100644 examples/signals_dev/merged/check_boll_cache.py delete mode 100644 examples/signals_dev/merged/check_cci_cache.py delete mode 100644 examples/signals_dev/merged/check_macd_cache.py delete mode 100644 examples/signals_dev/merged/check_sar_cache.py delete mode 100644 examples/signals_dev/merged/clv_up_dw_line_V230605.py delete mode 100644 examples/signals_dev/merged/cmo_up_dw_line_V230605.py delete mode 100644 examples/signals_dev/merged/cvolp_up_dw_line_V230612.py delete mode 100644 examples/signals_dev/merged/cxt_bi_end_V230618.py delete mode 100644 examples/signals_dev/merged/cxt_bi_end_V230815.py delete mode 100644 examples/signals_dev/merged/cxt_bi_stop_V230815.py delete mode 100644 examples/signals_dev/merged/cxt_bi_trend_V230824.py delete mode 100644 examples/signals_dev/merged/cxt_eleven_bi_V230622.py delete mode 100644 examples/signals_dev/merged/cxt_five_bi_V230619.py delete mode 100644 examples/signals_dev/merged/cxt_intraday_V230701.py delete mode 100644 examples/signals_dev/merged/cxt_nine_bi_V230621.py delete mode 100644 examples/signals_dev/merged/cxt_range_oscillation_V230620.py delete mode 100644 examples/signals_dev/merged/cxt_seven_bi_V230620.py delete mode 100644 examples/signals_dev/merged/cxt_three_bi_V230618.py delete mode 100644 examples/signals_dev/merged/cxt_ubi_end_V230816.py delete mode 100644 examples/signals_dev/merged/dema_up_dw_line_V230605.py delete mode 100644 examples/signals_dev/merged/demakder_up_dw_line_V230605.py delete mode 100644 examples/signals_dev/merged/emv_up_dw_line_V230605.py delete mode 100644 examples/signals_dev/merged/holds_concepts_effect.py delete mode 100644 examples/signals_dev/merged/kcatr_up_dw_line_V230823.py delete mode 100644 examples/signals_dev/merged/lverage_up_dw_line_V230824.py delete mode 100644 examples/signals_dev/merged/ntmdk_V230824.py delete mode 100644 examples/signals_dev/merged/obv_up_dw_line_V230719.py delete mode 100644 examples/signals_dev/merged/obvm_line_V230610.py delete mode 100644 examples/signals_dev/merged/pos_fix_exit_V230624.py delete mode 100644 examples/signals_dev/merged/pos_fx_stop_V230414.py delete mode 100644 examples/signals_dev/merged/pos_holds_V230414.py delete mode 100644 examples/signals_dev/merged/pos_holds_V230807.py delete mode 100644 examples/signals_dev/merged/pos_profit_loss_V230624.py delete mode 100644 examples/signals_dev/merged/pos_status_V230808.py delete mode 100644 examples/signals_dev/merged/tas_angle_V230802.py delete mode 100644 examples/signals_dev/merged/tas_atr_V230630.py delete mode 100644 examples/signals_dev/merged/tas_atr_stop_V230424.py delete mode 100644 examples/signals_dev/merged/tas_cross_status_V230619.py delete mode 100644 examples/signals_dev/merged/tas_cross_status_V230624.py delete mode 100644 examples/signals_dev/merged/tas_cross_status_V230625.py delete mode 100644 examples/signals_dev/merged/tas_double_ma_V230512.py delete mode 100644 examples/signals_dev/merged/tas_low_trend_V230627.py delete mode 100644 examples/signals_dev/merged/tas_rumi_V230704.py delete mode 100644 examples/signals_dev/merged/tas_sar_base_V230424.py delete mode 100644 examples/signals_dev/merged/vol_window_V230731.py delete mode 100644 examples/signals_dev/merged/vol_window_V230801.py delete mode 100644 examples/use_cross_sectional.py diff --git "a/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" "b/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" new file mode 100644 index 000000000..4f4ac5ccf --- /dev/null +++ "b/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +""" +author: zengbin93 +email: zeng_bin8888@163.com +create_dt: 2023/9/10 19:45 +describe: 30分钟笔非多即空策略 +""" +import czsc +from pathlib import Path +from loguru import logger +from czsc.connectors import research +from czsc import Event, Position + + +def create_long_short_V230908(symbol, **kwargs): + """30分钟笔非多即空策略 + + 使用的信号函数: + + https://czsc.readthedocs.io/en/latest/api/czsc.signals.cxt_bi_status_V230101.htm + """ + opens = [ + { + "operate": "开多", + "signals_all": [], + "signals_any": [], + "signals_not": [], + "factors": [ + { + "signals_all": [ + "30分钟_D1_表里关系V230101_向上_任意_任意_0" + ], + "signals_any": [], + "signals_not": [ + "30分钟_D1_涨跌停V230331_涨停_任意_任意_0" + ] + } + ] + }, + + { + "operate": "开空", + "signals_all": [], + "signals_any": [], + "signals_not": [], + "factors": [ + { + "signals_all": [ + "30分钟_D1_表里关系V230101_向下_任意_任意_0" + ], + "signals_any": [], + "signals_not": [ + "30分钟_D1_涨跌停V230331_跌停_任意_任意_0" + ] + } + ] + } + ] + + exits = [] + + pos = Position(name="30分钟笔非多即空", symbol=symbol, + opens=[Event.load(x) for x in opens], + exits=[Event.load(x) for x in exits], + interval=3600 * 4, timeout=16 * 30, stop_loss=500) + return pos + + + +class Strategy(czsc.CzscStrategyBase): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.is_stocks = kwargs.get('is_stocks', True) + + @property + def positions(self): + pos_list = [ + create_long_short_V230908(self.symbol), + ] + return pos_list + + +if __name__ == '__main__': + results_path = Path(r'D:\策略研究\30分钟笔非多即空') + logger.add(results_path / "czsc.log", rotation="1 week", encoding="utf-8") + results_path.mkdir(exist_ok=True, parents=True) + + symbols = research.get_symbols('中证500成分股')[:30] + symbol = symbols[0] + tactic = Strategy(symbol=symbol, is_stocks=True) + + # 使用 logger 记录策略的基本信息 + logger.info(f"K线周期列表:{tactic.freqs}") + logger.info(f"信号函数配置列表:{tactic.signals_config}") + + # replay 查看策略的编写是否正确,执行过程是否符合预期 + bars = research.get_raw_bars(symbol, freq=tactic.base_freq, sdt='20150101', edt='20220101') + trader = tactic.replay(bars, sdt='20200101', res_path=results_path / "replay", refresh=True) + + # 当策略执行过程符合预期后,将持仓策略保存到本地 json 文件中 + tactic.save_positions(results_path / "positions") + + + # for _pos in trader.positions: + # print(_pos.name, symbol, _pos.evaluate('多头')) + + # # dummy backtest, 只能通过命令行执行,不能在PyCharm中的Python终端执行 + # from czsc import DummyBacktest + # db = DummyBacktest(strategy=CzscStocksBeta, signals_path=r'D:\策略研究\signals', + # results_path=r'D:\策略研究\CzscStocksBetaV2', read_bars=research.get_raw_bars) + # # db.execute(symbols=symbols, n_jobs=10) + + # # on bar 回测 + # stats = [] + # for symbol in symbols[:3]: + # try: + # tactic = CzscStocksBeta(symbol=symbol, is_stocks=True) + # bars = research.get_raw_bars(symbol, freq=tactic.base_freq, sdt='20150101', edt='20220101') + # trader = tactic.backtest(bars, sdt='20200101') + # for _pos in trader.positions: + # stats.append(_pos.evaluate('多头')) + # print(_pos.name, symbol, _pos.evaluate('多头')) + # except Exception as e: + # print(symbol, '回测失败', e) diff --git a/examples/cal_ensemble_weight.py b/examples/cal_ensemble_weight.py deleted file mode 100644 index 9fabd25d0..000000000 --- a/examples/cal_ensemble_weight.py +++ /dev/null @@ -1,22 +0,0 @@ -import czsc -import pandas as pd - - -def run_by_weights(): - """从持仓权重样例数据中回测""" - dfw = pd.read_feather(r"C:\Users\zengb\Desktop\230814\weight_example.feather") - wb = czsc.WeightBacktest(dfw, digits=1, fee_rate=0.0002, res_path=r"C:\Users\zengb\Desktop\230814\weight_example") - res = wb.backtest() - - -def run_by_ensemble(): - """从单个 trader 中获取持仓权重,然后回测""" - trader = czsc.dill_load(r"C:\Users\zengb\Desktop\230814\DLi9001.trader") - - def __ensemble_method(x): - return x['A股日线SMA#5多头'] + 0.5 * x['A股日线SMA#5空头'] - - dfw = czsc.get_ensemble_weight(trader, method=__ensemble_method) - wb = czsc.WeightBacktest(dfw, digits=1, fee_rate=0.0002, res_path=r"C:\Users\zengb\Desktop\230814\DLi9001_ensemble") - res = wb.backtest() - diff --git a/examples/evaluate_holds_by_csp.py b/examples/evaluate_holds_by_csp.py deleted file mode 100644 index bc3b83761..000000000 --- a/examples/evaluate_holds_by_csp.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/5/11 18:11 -describe: 使用 CrossSectionalPerformance 进行截面持仓表现分析 -""" -import pandas as pd -from czsc import CrossSectionalPerformance - -dfh = pd.read_feather(r"C:\截面分析样例数据.feather") - -# 不使用杠杆进行截面分析 -csp = CrossSectionalPerformance(dfh, max_total_weight=1) -csp.report('不使用杠杆的表现.docx') - -# 使用一倍杠杆进行截面分析 -csp = CrossSectionalPerformance(dfh, max_total_weight=2) -csp.report('使用1倍杠杆的表现.docx') - diff --git a/examples/signals_dev/merged/adtm_up_dw_line_V230603.py b/examples/signals_dev/merged/adtm_up_dw_line_V230603.py deleted file mode 100644 index 07c5c70f1..000000000 --- a/examples/signals_dev/merged/adtm_up_dw_line_V230603.py +++ /dev/null @@ -1,74 +0,0 @@ -from collections import OrderedDict -import numpy as np -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def adtm_up_dw_line_V230603(c: CZSC, **kwargs) -> OrderedDict: - """ADTM能量异动,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}M{m}TH{th}_ADTMV230603" - - **信号逻辑:** - - 1. 如果今天的开盘价大于昨天的开盘价,取最高价 - 开盘价、开盘价 - 昨天的开盘价这二者中最大值, - 再将取出的最大值求和;反之取0,形成up_sum - 2. 如果今天的开盘价小于昨天的开盘价,取开盘价 - 最低价、昨天的开盘价 -开盘价这二者中最大值, - 再将取出的最大值求和;么之取0,形成dw_sum - 3. 当 up_sum > dw_sum 或 最大值的差值之商小于TH 看多,反之看空 - - - **信号列表:** - - - Signal('日线_D1N30M20TH5_ADTMV230603_看空_任意_任意_0') - - Signal('日线_D1N30M20TH5_ADTMV230603_看多_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数,默认为30 - - :param m: 获取K线的根数,默认为20 - - :param th: adtm阈值,默认为5,代表 5 / 10 = 0.5 - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 30)) - m = int(kwargs.get("m", 20)) - th = int(kwargs.get("th", 5)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}M{m}TH{th}_ADTMV230603".split('_') - - v1 = "其他" - if len(c.bars_raw) < di + 30: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - n_bars = get_sub_elements(c.bars_raw, di=di, n=n) - m_bars = get_sub_elements(c.bars_raw, di=di, n=m) - - up_sum = np.sum([max(n_bars[i].high - n_bars[i].open, n_bars[i].open - n_bars[i - 1].open) - for i in range(1, len(n_bars)) if n_bars[i].open > n_bars[i - 1].open]) - dw_sum = np.sum([max(m_bars[i].open - m_bars[i].low, m_bars[i - 1].open - m_bars[i].open) - for i in range(1, len(m_bars)) if m_bars[i].open < m_bars[i - 1].open]) - - adtm = (up_sum - dw_sum) / max(up_sum, dw_sum) - if up_sum > dw_sum or adtm > th / 10: - v1 = "看多" - if up_sum < dw_sum or adtm < th / 10: - v1 = "看空" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [ - {'name': adtm_up_dw_line_V230603, 'freq': '日线', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() diff --git a/examples/signals_dev/merged/amv_up_dw_line_V230603.py b/examples/signals_dev/merged/amv_up_dw_line_V230603.py deleted file mode 100644 index e789e15c5..000000000 --- a/examples/signals_dev/merged/amv_up_dw_line_V230603.py +++ /dev/null @@ -1,65 +0,0 @@ -from collections import OrderedDict -import numpy as np -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def amv_up_dw_line_V230603(c: CZSC, **kwargs) -> OrderedDict: - """AMV能量异动,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}M{m}_AMV能量V230603" - - **信号逻辑:** - - 用成交量作为权重对开盘价和收盘价的均值进行加权移动平均。成交量越大的价格对移动平均结果的影响越大, - AMV 指标减小了成交量小的价格波动的影响。当短期 AMV 线上穿/下穿长期 AMV线时,产生买入/卖出信号。 - - - **信号列表:** - - - Signal('日线_D1N30M120_AMV能量V230603_看多_任意_任意_0') - - Signal('日线_D1N30M120_AMV能量V230603_看空_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数,默认为30 - - :param m: 获取K线的根数,默认为20 - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 30)) - m = int(kwargs.get("m", 120)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}M{m}_AMV能量V230603".split('_') - v1 = "其他" - if len(c.bars_raw) < di + 120: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - n_bars = get_sub_elements(c.bars_raw, di=di, n=n) - m_bars = get_sub_elements(c.bars_raw, di=di, n=m) - - amov1 = np.sum([(n_bars[i].amount * (n_bars[i].open + n_bars[i].close) / 2) for i in range(len(n_bars))]) - amov2 = np.sum([(m_bars[i].amount * (m_bars[i].open + m_bars[i].close) / 2) for i in range(len(m_bars))]) - vol_sum1 = np.sum([n_bars[i].amount for i in range(len(n_bars))]) - vol_sum2 = np.sum([m_bars[i].amount for i in range(len(m_bars))]) - amv1 = amov1 / vol_sum1 - amv2 = amov2 / vol_sum2 - - v1 = "看多" if amv1 > amv2 else "看空" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [ - {'name': amv_up_dw_line_V230603, 'freq': '日线', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() diff --git a/examples/signals_dev/merged/asi_up_dw_line_V230603.py b/examples/signals_dev/merged/asi_up_dw_line_V230603.py deleted file mode 100644 index 96c49d119..000000000 --- a/examples/signals_dev/merged/asi_up_dw_line_V230603.py +++ /dev/null @@ -1,86 +0,0 @@ -from collections import OrderedDict -import numpy as np -import pandas as pd -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - - -def asi_up_dw_line_V230603(c: CZSC, **kwargs) -> OrderedDict: - """ASI多空分类,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}P{p}_ASI多空V230603" - - **信号逻辑:** - - 由于 SI 的波动性比较大,所以我们一般对 SI 累计求和得到 ASI 并捕 - 捉 ASI 的变化趋势。一般我们不会直接看 ASI 的数值(对 SI 累计求 - 和的求和起点不同会导致求出 ASI 的值不同),而是会观察 ASI 的变 - 化方向。我们利用 ASI 与其均线的交叉来产生交易信号,上穿/下穿均 - 线时买入/卖出 - - **信号列表:** - - - Signal('日线_D1N30P120_ASI多空V230603_看多_任意_任意_0') - - Signal('日线_D1N30P120_ASI多空V230603_看空_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数,默认为30 - - :param p: 获取K线的根数,默认为20 - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 30)) - p = int(kwargs.get("p", 120)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}P{p}_ASI多空V230603".split('_') - v1 = "其他" - if len(c.bars_raw) < di + p + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - _bars = get_sub_elements(c.bars_raw, di=di, n=p) - close_prices = np.array([bar.close for bar in _bars]) - open_prices = np.array([bar.open for bar in _bars]) - high_prices = np.array([bar.high for bar in _bars]) - low_prices = np.array([bar.low for bar in _bars]) - - o = np.concatenate([[close_prices[0]], close_prices[:-1]]) - a = np.abs(high_prices - o) - b = np.abs(low_prices - o) - c = np.abs(high_prices - np.concatenate([[low_prices[0]], low_prices[:-1]])) # type: ignore - d = np.abs(o - np.concatenate([[open_prices[0]], open_prices[:-1]])) - - k = np.maximum(a, b) - m = np.maximum(high_prices - low_prices, n) - r1 = a + 0.5 * b + 0.25 * d - r2 = b + 0.5 * a + 0.25 * d - r3 = c + 0.25 * d - r4 = np.where((a >= b) & (a >= c), r1, r2) - r = np.where((c >= a) & (c >= b), r3, r4) - - if (r * k / m != 0).all(): - si = 50 * (close_prices - c + (c - open_prices) + 0.5 * (close_prices - open_prices)) / (r * k / m) - else: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - asi = np.cumsum(si) - - v1 = "看多" if asi[-1] > np.mean(asi[-p:]) else "看空" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [ - {'name': asi_up_dw_line_V230603, 'freq': '日线', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() diff --git a/examples/signals_dev/merged/bar_eight_V230702.py b/examples/signals_dev/merged/bar_eight_V230702.py deleted file mode 100644 index c5f0e49b4..000000000 --- a/examples/signals_dev/merged/bar_eight_V230702.py +++ /dev/null @@ -1,88 +0,0 @@ -from loguru import logger -from czsc import CZSC -from collections import OrderedDict -from czsc.utils import create_single_signal, get_sub_elements - - -def bar_eight_V230702(c: CZSC, **kwargs) -> OrderedDict: - """8K走势分类 - - 参数模板:"{freq}_D{di}#8K_走势分类V230702" - - **信号逻辑:** - - 参见博客:https://blog.sina.com.cn/s/blog_486e105c010009uy.html - 这篇博客给出了8K走势分类的逻辑。 - - **信号列表:** - - - Signal('30分钟_D1#8K_走势分类V230702_弱平衡市_任意_任意_0') - - Signal('30分钟_D1#8K_走势分类V230702_双中枢下跌_任意_任意_0') - - Signal('30分钟_D1#8K_走势分类V230702_转折平衡市_任意_任意_0') - - Signal('30分钟_D1#8K_走势分类V230702_强平衡市_任意_任意_0') - - Signal('30分钟_D1#8K_走势分类V230702_双中枢上涨_任意_任意_0') - - Signal('30分钟_D1#8K_走势分类V230702_无中枢上涨_任意_任意_0') - - Signal('30分钟_D1#8K_走势分类V230702_无中枢下跌_任意_任意_0') - - :param c: CZSC对象 - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}#8K_走势分类V230702".split("_") - v1 = "其他" - if len(c.bars_raw) < di + 12: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=8) - zs_list = [] - for b1, b2, b3 in zip(bars[:-2], bars[1:-1], bars[2:]): - if min(b1.high, b2.high, b3.high) > max(b1.low, b2.low, b3.low): - zs_list.append([b1, b2, b3]) - - _dir = "上涨" if bars[-1].close > bars[0].open else "下跌" - - if not zs_list: - v1 = f"无中枢{_dir}" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - # 双中枢的情况,有一根K线的 high low 与前后两个中枢没有重叠 - if len(zs_list) >= 2: - zs1, zs2 = zs_list[0], zs_list[-1] - zs1_high, zs1_low = max([x.high for x in zs1]), min([x.low for x in zs1]) - zs2_high, zs2_low = max([x.high for x in zs2]), min([x.low for x in zs2]) - if _dir == "上涨" and zs1_high < zs2_low: - v1 = f"双中枢{_dir}" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - if _dir == "下跌" and zs1_low > zs2_high: - v1 = f"双中枢{_dir}" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - # 单中枢的情况,前三根K线出现高点:弱平衡市,前三根K线出现低点:强平衡市,否则:转折平衡市 - high_first = max(bars[0].high, bars[1].high, bars[2].high) == max([x.high for x in bars]) - low_first = min(bars[0].low, bars[1].low, bars[2].low) == min([x.low for x in bars]) - if high_first and not low_first: - v1 = "弱平衡市" - elif low_first and not high_first: - v1 = "强平衡市" - else: - v1 = "转折平衡市" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': bar_eight_V230702, 'di': 1, 'freq': '30分钟'}] - check_signals_acc(bars, signals_config=signals_config, height='780px', delta_days=0) # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/bar_end_V221211.py b/examples/signals_dev/merged/bar_end_V221211.py deleted file mode 100644 index 615bf2b8f..000000000 --- a/examples/signals_dev/merged/bar_end_V221211.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -# @Time : 2023/6/18 16:11 -# @Author : 琅盎 -# @FileName: BIAS_V1.py -# @Software: PyCharm -from collections import OrderedDict -import numpy as np -from datetime import datetime -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal, freq_end_time - - -def bar_end_V221211(c: CZSC, freq1='60分钟', **kwargs) -> OrderedDict: - """判断分钟 K 线是否结束 - - 参数模板:"{freq}_{freq1}结束_BS辅助221211" - - **信号逻辑:** - - 以 freq 为基础周期,freq1 为大周期,判断 freq1 K线是否结束。 - 如果结束,返回信号值为 "闭合",否则返回 "未闭x",x 为未闭合的次数。 - - **信号列表:** - - - Signal('15分钟_60分钟结束_BS辅助221211_未闭1_任意_任意_0') - - Signal('15分钟_60分钟结束_BS辅助221211_未闭2_任意_任意_0') - - Signal('15分钟_60分钟结束_BS辅助221211_未闭3_任意_任意_0') - - Signal('15分钟_60分钟结束_BS辅助221211_闭合_任意_任意_0') - - :param c: 基础周期的 CZSC 对象 - :param freq1: 分钟周期名称 - :return: s - """ - freq = c.freq.value - k1, k2, k3 = f"{freq}_{freq1}结束_BS辅助221211".split('_') - assert "分钟" in freq1 - - c1_dt = freq_end_time(c.bars_raw[-1].dt, freq1) - i = 0 - for bar in c.bars_raw[::-1]: - _edt = freq_end_time(bar.dt, freq1) - if _edt != c1_dt: - break - i += 1 - - if c1_dt == c.bars_raw[-1].dt: - v = "闭合" - else: - v = "未闭{}".format(i) - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [ - {'name': bar_end_V221211, 'freq': '15分钟', 'freq1': '60分钟', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config, delta_days=0) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/signals_dev/merged/bar_tnr_V230629.py b/examples/signals_dev/merged/bar_tnr_V230629.py deleted file mode 100644 index 207322d5d..000000000 --- a/examples/signals_dev/merged/bar_tnr_V230629.py +++ /dev/null @@ -1,91 +0,0 @@ -import sys - -sys.path.insert(0, '.') -sys.path.insert(0, '..') -sys.path.insert(0, '../..') -sys.path.insert(0, '../../..') -import math -import numpy as np -import pandas as pd -from collections import OrderedDict -from czsc import CZSC -from loguru import logger -from czsc.signals.tas import update_atr_cache -from czsc.utils import create_single_signal, get_sub_elements - - -# 定义信号函数 -# ---------------------------------------------------------------------------------------------------------------------- -def bar_tnr_V230629(c: CZSC, **kwargs) -> OrderedDict: - """趋势噪音指标(TNR,Trend to Noise Rate)分层 - - 参数模板:"{freq}_D{di}TNR{timeperiod}_趋势V230629" - - **信号逻辑:** - - TNR计算公式:取N根K线,首尾两个close的绝对差值 除以 相邻两个close的绝对差值累计。 - - 取最近100个bar的TNR进行分层。 - - **信号列表:** - - - Signal('15分钟_D1TNR14_趋势V230629_第7层_任意_任意_0') - - Signal('15分钟_D1TNR14_趋势V230629_第6层_任意_任意_0') - - Signal('15分钟_D1TNR14_趋势V230629_第8层_任意_任意_0') - - Signal('15分钟_D1TNR14_趋势V230629_第9层_任意_任意_0') - - Signal('15分钟_D1TNR14_趋势V230629_第10层_任意_任意_0') - - Signal('15分钟_D1TNR14_趋势V230629_第5层_任意_任意_0') - - Signal('15分钟_D1TNR14_趋势V230629_第2层_任意_任意_0') - - Signal('15分钟_D1TNR14_趋势V230629_第1层_任意_任意_0') - - Signal('15分钟_D1TNR14_趋势V230629_第3层_任意_任意_0') - - Signal('15分钟_D1TNR14_趋势V230629_第4层_任意_任意_0') - - :param c: czsc对象 - :param kwargs: - - - di: 倒数第i根K线 - - timeperiod: TNR指标的参数 - - :return: 信号字典 - """ - di = int(kwargs.get('di', 1)) - timeperiod = int(kwargs.get('timeperiod', 14)) - freq = c.freq.value - - # 更新缓存 - cache_key = f"TNR{timeperiod}" - for i, bar in enumerate(c.bars_raw, 0): - if cache_key in bar.cache: - continue - if i < timeperiod: - bar.cache[cache_key] = 0 - else: - _bars = c.bars_raw[max(0, i - timeperiod):i + 1] - sum_abs = sum([abs(_bars[j].close - _bars[j - 1].close) for j in range(1, len(_bars))]) - bar.cache[cache_key] = 0 if sum_abs == 0 else abs(_bars[-1].close - _bars[0].close) / sum_abs - - k1, k2, k3 = f"{freq}_D{di}TNR{timeperiod}_趋势V230629".split('_') - v1 = "其他" - if len(c.bars_raw) < di + timeperiod + 8: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=100) - tnr = [bar.cache[cache_key] for bar in bars] - lev = pd.qcut(tnr, 10, labels=False, duplicates='drop')[-1] - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=f"第{int(lev+1)}层") - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[10] - bars = research.get_raw_bars(symbol, '15分钟', '20181101', '20210101', fq='前复权') - signals_config = [{'name': bar_tnr_V230629, 'freq': '15分钟', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/bar_tnr_V230630.py b/examples/signals_dev/merged/bar_tnr_V230630.py deleted file mode 100644 index df82f3bdb..000000000 --- a/examples/signals_dev/merged/bar_tnr_V230630.py +++ /dev/null @@ -1,84 +0,0 @@ -import sys - -sys.path.insert(0, '.') -sys.path.insert(0, '..') -sys.path.insert(0, '../..') -sys.path.insert(0, '../../..') -import math -import numpy as np -from collections import OrderedDict -from czsc import CZSC -from loguru import logger -from czsc.signals.tas import update_atr_cache -from czsc.utils import create_single_signal, get_sub_elements - - -# 定义信号函数 -# ---------------------------------------------------------------------------------------------------------------------- -def bar_tnr_V230630(c: CZSC, **kwargs) -> OrderedDict: - """趋势噪音指标(TNR,Trend to Noise Rate) - - 参数模板:"{freq}_D{di}TNR{timeperiod}K{k}_趋势V230630" - - **信号逻辑:** - - TNR计算公式:取N根K线,首尾两个close的绝对差值 除以 相邻两个close的绝对差值累计。 - - 噪音变化判断,如果 t 时刻的 TNR > 过去k个TNR的均值,则说明噪音在减少,此时趋势较强;反之,噪音在增加,此时趋势较弱。 - - **信号列表:** - - - Signal('15分钟_D1TNR14K3_趋势V230630_噪音减少_任意_任意_0') - - Signal('15分钟_D1TNR14K3_趋势V230630_噪音增加_任意_任意_0') - - :param c: czsc对象 - :param kwargs: - - - di: 倒数第i根K线 - - timeperiod: TNR指标的参数 - - k: 过去k个TNR的均值 - - :return: 信号字典 - """ - di = int(kwargs.get('di', 1)) - timeperiod = int(kwargs.get('timeperiod', 14)) - k = int(kwargs.get('k', 3)) - freq = c.freq.value - - # 更新缓存 - cache_key = f"TNR{timeperiod}" - for i, bar in enumerate(c.bars_raw, 0): - if cache_key in bar.cache: - continue - if i < timeperiod: - bar.cache[cache_key] = 0 - else: - _bars = c.bars_raw[max(0, i - timeperiod):i + 1] - sum_abs = sum([abs(_bars[j].close - _bars[j - 1].close) for j in range(1, len(_bars))]) - bar.cache[cache_key] = 0 if sum_abs == 0 else abs(_bars[-1].close - _bars[0].close) / sum_abs - - k1, k2, k3 = f"{freq}_D{di}TNR{timeperiod}K{k}_趋势V230630".split('_') - v1 = "其他" - if len(c.bars_raw) < di + timeperiod + 8: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=k) - delta_tnr = bars[-1].cache[cache_key] - np.mean([bar.cache[cache_key] for bar in bars]) - v1 = "噪音减少" if delta_tnr > 0 else "噪音增加" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[10] - bars = research.get_raw_bars(symbol, '15分钟', '20181101', '20210101', fq='前复权') - signals_config = [{'name': bar_tnr_V230630, 'freq': '15分钟', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/bar_window_ps_V230731.py b/examples/signals_dev/merged/bar_window_ps_V230731.py deleted file mode 100644 index 2514e212e..000000000 --- a/examples/signals_dev/merged/bar_window_ps_V230731.py +++ /dev/null @@ -1,83 +0,0 @@ -import sys -sys.path.insert(0, '.') -sys.path.insert(0, '..') - -from collections import OrderedDict -import pandas as pd -import numpy as np -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def bar_window_ps_V230731(c: CZSC, **kwargs) -> OrderedDict: - """指定窗口内支撑压力位分位数计算,贡献者:chenlei - - 参数模板:"{freq}_W{w}M{m}N{n}L{l}_支撑压力位V230731" - - **信号逻辑:** - - 1. 计算最近 N 笔的最高价 NH 和最低价 NL,这个可以近似理解成价格的支撑和压力位 - 2. 计算并缓存最新K线的收盘价格 C 处于 NH、NL 之间的位置,计算方法为 P = (C - NL)/ (NH - NL) - 3. 取最近 M 个 P 值序列,按分位数分层,分层数量为 L,分层的最大值为最近的压力,最小值为最近的支撑,当前值为最近的价格位置 - - **信号列表:** - - - Signal('15分钟_W5M40N8L5_支撑压力位V230731_压力N5_支撑N5_当前N5_0') - - Signal('15分钟_W5M40N8L5_支撑压力位V230731_压力N5_支撑N4_当前N5_0') - - Signal('15分钟_W5M40N8L5_支撑压力位V230731_压力N5_支撑N4_当前N4_0') - - Signal('15分钟_W5M40N8L5_支撑压力位V230731_压力N5_支撑N3_当前N5_0') - - Signal('15分钟_W5M40N8L5_支撑压力位V230731_压力N5_支撑N2_当前N2_0') - - Signal('15分钟_W5M40N8L5_支撑压力位V230731_压力N5_支撑N1_当前N2_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param w: 评价分位数分布用的窗口大小 - - :param m: 计算分位数所需取K线的数量。 - - :param n: 最近N笔 - - :param l: 分层的数量。 - - :return: 信号识别结果 - """ - w = int(kwargs.get("w", 5)) - m = int(kwargs.get("m", 40)) - n = int(kwargs.get("n", 8)) - l = int(kwargs.get("l", 5)) - - assert m > l * 2 > 2, "参数 m 必须大于 l * 2,且 l 必须大于 2" - assert w < m, "参数 w 必须小于 m" - freq = c.freq.value - k1, k2, k3 = f"{freq}_W{w}M{m}N{n}L{l}_支撑压力位V230731".split('_') - - if len(c.bi_list) < n+2: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1="其他") - - # 更新支撑压力位位置 - cache_key_pct = "pct" - H_line, L_line = max([x.high for x in c.bi_list[-n:]]), min([x.low for x in c.bi_list[-n:]]) - for i, bar in enumerate(c.bars_raw): - if cache_key_pct in bar.cache: - continue - bar.cache[cache_key_pct] = (bar.close - L_line) / (H_line - L_line) - - fenweis = [x.cache[cache_key_pct] for x in get_sub_elements(c.bars_raw, n=m)] - layer = pd.qcut(fenweis, l, labels=False, duplicates='drop') - max_layer = max(layer[-w:]) + 1 - min_layer = min(layer[-w:]) + 1 - - v1, v2, v3 = f"压力N{max_layer}", f"支撑N{min_layer}", f"当前N{layer[-1]+1}" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2, v3=v3) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20191101', '20210101', fq='前复权') - - signals_config = [ - {'name': bar_window_ps_V230731, 'freq': '15分钟'}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - -if __name__ == '__main__': - main() diff --git a/examples/signals_dev/merged/bar_window_ps_V230801.py b/examples/signals_dev/merged/bar_window_ps_V230801.py deleted file mode 100644 index 6dc6bb92c..000000000 --- a/examples/signals_dev/merged/bar_window_ps_V230801.py +++ /dev/null @@ -1,60 +0,0 @@ -import sys -sys.path.insert(0, '.') -sys.path.insert(0, '..') - -from collections import OrderedDict -import pandas as pd -import numpy as np -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def bar_window_ps_V230801(c: CZSC, **kwargs) -> OrderedDict: - """指定窗口内支撑压力位分位数计算 - - 参数模板:"{freq}_N{n}W{w}_支撑压力位V230801" - - **信号逻辑:** - - 1. 计算最近 N 笔的最高价 NH 和最低价 NL,这个可以近似理解成价格的支撑和压力位 - 2. 计算并缓存最新K线的收盘价格 C 处于 NH、NL 之间的位置,计算方法为 P = (C - NL)/ (NH - NL) - 3. 取最近 M 个 P 值序列,四舍五入精确到小数点后1位,作为当前K线的分位数 - - **信号列表:** - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param w: 评价分位数分布用的窗口大小 - - :param n: 最近N笔 - - :return: 信号识别结果 - """ - w = int(kwargs.get("w", 5)) - n = int(kwargs.get("n", 8)) - - if len(c.bi_list) < n+2: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1="其他") - - ubi = c.ubi - H_line, L_line = max([x.high for x in c.bi_list[-n:]] + [ubi['high']]), min([x.low for x in c.bi_list[-n:]] + [ubi['low']]) - freq = c.freq.value - k1, k2, k3 = f"{freq}_N{n}W{w}_支撑压力位V230801".split('_') - bars = c.bars_raw[-w:] - pcts = [int(max((x.close - L_line) / (H_line - L_line), 0) * 10) for x in bars] - v1, v2, v3 = f"最大N{max(pcts)}", f"最小N{min(pcts)}", f"当前N{pcts[-1]}" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2, v3=v3) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20191101', '20210101', fq='前复权') - - signals_config = [ - {'name': bar_window_ps_V230801, 'freq': '15分钟'}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - -if __name__ == '__main__': - main() diff --git a/examples/signals_dev/merged/bar_window_std_V230731.py b/examples/signals_dev/merged/bar_window_std_V230731.py deleted file mode 100644 index 4a0092bdc..000000000 --- a/examples/signals_dev/merged/bar_window_std_V230731.py +++ /dev/null @@ -1,86 +0,0 @@ -from collections import OrderedDict -import pandas as pd -import numpy as np -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def bar_window_std_V230731(c: CZSC, **kwargs) -> OrderedDict: - """指定窗口内波动率的特征 - - 参数模板:"{freq}_D{di}W{window}M{m}N{n}_窗口波动V230731" - - **信号逻辑:** - - 滚动计算最近m根K线的波动率,分成n层,最大值为n,最小值为1; - 最近window根K线的最大值为max_layer,最小值为min_layer。 - 以这两个值作为窗口内的波动率特征。 - - **信号列表:** - - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N7_低波N6_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N6_低波N6_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N8_低波N6_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N9_低波N6_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N9_低波N9_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N9_低波N8_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N8_低波N8_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N8_低波N7_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N7_低波N7_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N7_低波N5_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N6_低波N5_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N5_低波N4_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N5_低波N3_任意_0') - - Signal('60分钟_D2W3M100N10_窗口波动V230731_高波N4_低波N3_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param di: 信号计算截止倒数第i根K线 - - :param w: 观察的窗口大小。 - - :param m: 计算分位数所需取K线的数量。 - - :param n: 分层的数量。 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - w = int(kwargs.get("w", 5)) - m = int(kwargs.get("m", 100)) - n = int(kwargs.get("n", 10)) - - # 更新STD20波动率缓存 - cache_key = "STD20" - for i, bar in enumerate(c.bars_raw): - if cache_key in bar.cache: - continue - bar.cache[cache_key] = 0 if i < 5 else np.std([x.close for x in c.bars_raw[max(i-20, 0):i]]) - - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}W{w}M{m}N{n}_窗口波动V230731".split('_') - v1 = "其他" - - if len(c.bars_raw) < di + m + w: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - stds = [x.cache[cache_key] for x in get_sub_elements(c.bars_raw, di=di, n=m)] - layer = pd.qcut(stds, n, labels=False, duplicates='drop') - max_layer = max(layer[-w:]) + 1 - min_layer = min(layer[-w:]) + 1 - - v1, v2 = f"高波N{max_layer}", f"低波N{min_layer}" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20191101', '20210101', fq='前复权') - - signals_config = [ - {'name': bar_window_std_V230731, 'freq': '60分钟', 'di': 2, 'm': 200, 'window': 10}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() diff --git a/examples/signals_dev/merged/bias_up_dw_line_V230604.py b/examples/signals_dev/merged/bias_up_dw_line_V230604.py deleted file mode 100644 index c15d618a7..000000000 --- a/examples/signals_dev/merged/bias_up_dw_line_V230604.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -# @Time : 2023/6/18 16:11 -# @Author : 琅盎 -# @FileName: BIAS_V1.py -# @Software: PyCharm -from collections import OrderedDict -import numpy as np -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def bias_up_dw_line_V230618(c: CZSC, **kwargs) -> OrderedDict: - """BIAS乖离率指标,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}M{m}P{p}TH1{th1}TH2{th2}TH3{th3}_BIAS乖离率V230618" - - **信号逻辑:** - - 乖离率 BIAS 用来衡量收盘价与移动平均线之间的差距。 - 当 BIAS6 大于 3 且 BIAS12 大于 5 且 BIAS24 大于 8, - 三个乖离率均进入股价强势上涨区间,产生买入信号; - 当 BIAS6 小于-3 且 BIAS12 小于-5 且BIAS24 小于-8 时, - 三种乖离率均进入股价强势下跌区间,产生卖出信号 - - **信号列表:** - - - Signal('日线_D1N6M12P24TH11TH23TH35_BIAS乖离率V230618_看空_任意_任意_0') - - Signal('日线_D1N6M12P24TH11TH23TH35_BIAS乖离率V230618_看多_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数,默认为30 - - :param m: 获取K线的根数,默认为20 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 6)) - m = int(kwargs.get("m", 12)) - p = int(kwargs.get("p", 24)) - th1 = int(kwargs.get("th1", 1)) - th2 = int(kwargs.get("th2", 3)) - th3 = int(kwargs.get("th3", 5)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}M{m}P{p}TH1{th1}TH2{th2}TH3{th3}_BIAS乖离率V230618".split('_') - v1 = "其他" - if len(c.bars_raw) < di + max(n, m, p): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars1 = get_sub_elements(c.bars_raw, di=di, n=n) - bars2 = get_sub_elements(c.bars_raw, di=di, n=m) - bars3 = get_sub_elements(c.bars_raw, di=di, n=p) - - bias_ma1 = np.mean([bars1[i].close for i in range(len(bars1))]) - bias_ma2 = np.mean([bars2[i].close for i in range(len(bars2))]) - bias_ma3 = np.mean([bars3[i].close for i in range(len(bars3))]) - - bias1 = (bars1[-1].close - bias_ma1) / bias_ma1 * 100 - bias2 = (bars2[-1].close - bias_ma2) / bias_ma2 * 100 - bias3 = (bars3[-1].close - bias_ma3) / bias_ma3 * 100 - - if bias1 > th1 and bias2 > th2 and bias3 > th3: - v1 = "看多" - if bias1 < -th1 and bias2 < -th2 and bias3 < -th3: - v1 = "看空" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [ - {'name': bias_up_dw_line_V230618, 'freq': '日线', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/signals_dev/merged/byi_fx_num_V230628.py b/examples/signals_dev/merged/byi_fx_num_V230628.py deleted file mode 100644 index ba8daaa01..000000000 --- a/examples/signals_dev/merged/byi_fx_num_V230628.py +++ /dev/null @@ -1,65 +0,0 @@ -import sys - -sys.path.insert(0, '.') -sys.path.insert(0, '..') -sys.path.insert(0, '../..') -sys.path.insert(0, '../../..') -import math -import numpy as np -import pandas as pd -from collections import OrderedDict -from czsc import CZSC -from loguru import logger -from czsc.signals.tas import update_atr_cache -from czsc.utils import create_single_signal, get_sub_elements - - -# 定义信号函数 -# ---------------------------------------------------------------------------------------------------------------------- -def byi_fx_num_V230628(c: CZSC, **kwargs) -> OrderedDict: - """白仪前面下跌或上涨一笔次级别笔结构数量满足条件;贡献者:谌意勇 - - 参数模板:"{freq}_D{di}笔分型数大于{num}_BE辅助V230628" - - **信号逻辑:** - - 对于采用分型停顿或者分型验证开开仓,前一笔内部次级别笔结构尽量带结构, - 此信号函数为当分型笔数量判断大于 num 为满足条件 - - **信号列表:** - - - Signal('15分钟_D1笔分型数大于4_BE辅助V230628_向下_满足_任意_0') - - Signal('15分钟_D1笔分型数大于4_BE辅助V230628_向上_满足_任意_0') - - :param c: CZSC对象 - :param di: 从倒数第几笔开始检查 - :param num: 前笔内部次级别笔数量 - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - num = int(kwargs.get('num', 4)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}笔分型数大于{num}_BE辅助V230628".split('_') - v1 = "其他" - if len(c.bi_list) < di + 1 or len(c.bars_ubi) > 7: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bi = c.bi_list[-di] - v1 = bi.direction.value - v2 = "满足" if len(bi.fxs) >= num else "其他" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[10] - bars = research.get_raw_bars(symbol, '15分钟', '20181101', '20210101', fq='前复权') - signals_config = [{'name': byi_fx_num_V230628, 'freq': '15分钟', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/check_atr_cache.py b/examples/signals_dev/merged/check_atr_cache.py deleted file mode 100644 index ba24f0151..000000000 --- a/examples/signals_dev/merged/check_atr_cache.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/15 12:54 -describe: 检查ATR增量更新带来的影响 -""" -import sys -sys.path.insert(0, '../..') -import czsc -czsc.welcome() -import talib as ta -from test.test_analyze import read_1min - -bars = read_1min() -signals_config = [{'name': "czsc.signals.tas_atr_break_V230424", 'freq': '1分钟', 'di': 1, 'timeperiod': 20, 'th': 30}] -df = czsc.generate_czsc_signals(bars, signals_config=signals_config, signals_module_name='czsc.signals', df=True) -df['atr'] = ta.ATR(df.high, df.low, df.close, timeperiod=20) -# parse cache -df['cache_atr'] = df['cache'].apply(lambda x: x['ATR20']) - -df = df.tail(10000) -print('ATR 差异', (df['atr'] - df['cache_atr']).abs().sum()) - diff --git a/examples/signals_dev/merged/check_boll_cache.py b/examples/signals_dev/merged/check_boll_cache.py deleted file mode 100644 index a9279b2ab..000000000 --- a/examples/signals_dev/merged/check_boll_cache.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/15 12:54 -describe: 检查ATR增量更新带来的影响 -""" -import sys -sys.path.insert(0, '../..') -import czsc -czsc.welcome() -import talib as ta -from test.test_analyze import read_1min - -bars = read_1min() -signals_config = [{'name': "czsc.signals.tas_boll_cc_V230312", 'freq': '1分钟', 'di': 1}] -df = czsc.generate_czsc_signals(bars, signals_config=signals_config, signals_module_name='czsc.signals', df=True) -df['u1'], df['m'], df['l1'] = ta.BBANDS(df.close, timeperiod=20, nbdevup=2, nbdevdn=2, matype=0) - -# parse cache -df['cache_u1'] = df['cache'].apply(lambda x: x['BOLL20S20']['上轨']) -df['cache_m'] = df['cache'].apply(lambda x: x['BOLL20S20']['中线']) -df['cache_l1'] = df['cache'].apply(lambda x: x['BOLL20S20']['下轨']) - -df = df.tail(10000) -print('u1 差异', (df['u1'] - df['cache_u1']).abs().sum()) -print('m 差异', (df['m'] - df['cache_m']).abs().sum()) -print('l1 差异', (df['l1'] - df['cache_l1']).abs().sum()) diff --git a/examples/signals_dev/merged/check_cci_cache.py b/examples/signals_dev/merged/check_cci_cache.py deleted file mode 100644 index f05ec599b..000000000 --- a/examples/signals_dev/merged/check_cci_cache.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/15 12:54 -describe: 检查CCI增量更新带来的影响 -""" -import sys -sys.path.insert(0, '../..') -import czsc -czsc.welcome() -import talib as ta -from test.test_analyze import read_1min - -bars = read_1min() -signals_config = [{'name': "czsc.signals.tas_cci_base_V230402", 'freq': '1分钟', 'di': 1, 'timeperiod': 20}] -df = czsc.generate_czsc_signals(bars, signals_config=signals_config, signals_module_name='czsc.signals', df=True) -df['cci'] = ta.CCI(df.high, df.low, df.close, timeperiod=20) -# parse cache -df['cache_cci'] = df['cache'].apply(lambda x: x['CCI20']) - -df = df.tail(10000) -print('CCI 差异', (df['cci'] - df['cache_cci']).abs().sum()) - diff --git a/examples/signals_dev/merged/check_macd_cache.py b/examples/signals_dev/merged/check_macd_cache.py deleted file mode 100644 index b522f852b..000000000 --- a/examples/signals_dev/merged/check_macd_cache.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/15 12:54 -describe: 检查MACD增量更新带来的影响 -""" -import sys -sys.path.insert(0, '../..') -import czsc -czsc.welcome() -import talib as ta -from test.test_analyze import read_1min - -bars = read_1min() -signals_config = [{'name': "czsc.signals.tas_macd_base_V230320", 'freq': '1分钟', 'di': 1}] -df = czsc.generate_czsc_signals(bars, signals_config=signals_config, signals_module_name='czsc.signals', df=True) -df['dif'], df['dea'], df['macd'] = ta.MACD(df['close'], fastperiod=12, slowperiod=26, signalperiod=9) -# parse cache -df['cache_macd'] = df['cache'].apply(lambda x: x['MACD12#26#9']['macd']) -df['cache_dif'] = df['cache'].apply(lambda x: x['MACD12#26#9']['dif']) -df['cache_dea'] = df['cache'].apply(lambda x: x['MACD12#26#9']['dea']) - -df = df.tail(10000) -print('macd 差异', (df['macd'] - df['cache_macd']).abs().sum()) -print('dif 差异', (df['dif'] - df['cache_dif']).abs().sum()) -print('dea 差异', (df['dea'] - df['cache_dea']).abs().sum()) - diff --git a/examples/signals_dev/merged/check_sar_cache.py b/examples/signals_dev/merged/check_sar_cache.py deleted file mode 100644 index d01e73593..000000000 --- a/examples/signals_dev/merged/check_sar_cache.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/15 12:54 -describe: 检查ATR增量更新带来的影响 -""" -import sys -sys.path.insert(0, '../..') -import czsc -czsc.welcome() -import talib as ta -from test.test_analyze import read_1min - -bars = read_1min() -signals_config = [{'name': "czsc.signals.tas_sar_base_V230425", 'freq': '1分钟', 'di': 1, 'max_overlap': 5}] -df = czsc.generate_czsc_signals(bars, signals_config=signals_config, signals_module_name='czsc.signals', df=True) -df['sar'] = ta.SAR(df.high, df.low) -# parse cache -df['cache_sar'] = df['cache'].apply(lambda x: x['SAR']) - -df = df.tail(10000) -print('SAR 差异', (df['sar'] - df['cache_sar']).abs().sum()) - diff --git a/examples/signals_dev/merged/clv_up_dw_line_V230605.py b/examples/signals_dev/merged/clv_up_dw_line_V230605.py deleted file mode 100644 index c672af4a8..000000000 --- a/examples/signals_dev/merged/clv_up_dw_line_V230605.py +++ /dev/null @@ -1,62 +0,0 @@ -from collections import OrderedDict -import numpy as np -import pandas as pd -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def clv_up_dw_line_V230605(c: CZSC, **kwargs) -> OrderedDict: - """CLV多空分类,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}_CLV多空V230605" - - **信号逻辑:** - - CLV 用来衡量收盘价在最低价和最高价之间的位置。 - 当CLOSE=HIGH 时,CLV=1; 当 CLOSE=LOW 时,CLV=-1;当 CLOSE位于 HIGH 和 LOW 的中点时, - CLV=0。CLV>0(<0),说明收盘价离最高(低)价更近。我们用 CLVMA 上穿/下穿 0 来产生买入/卖出信号 - - **信号列表:** - - - Signal('日线_D1N70_CLV多空V230605_看多_任意_任意_0') - - Signal('日线_D1N70_CLV多空V230605_看空_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数,默认为60 - - :param m: 收盘价倍数,默认为2 - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 70)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}_CLV多空V230605".split('_') - - if len(c.bars_raw) < di + 100: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1="其他") - - _bars = get_sub_elements(c.bars_raw, di=di, n=n) - - close = np.array([bar.close for bar in _bars]) - low = np.array([bar.low for bar in _bars]) - high = np.array([bar.high for bar in _bars]) - clv_ma = np.mean((2 * close - low - high) / (high - low)) - - v1 = "看多" if clv_ma > 0 else "看空" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [ - {'name': clv_up_dw_line_V230605, 'freq': '日线', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() diff --git a/examples/signals_dev/merged/cmo_up_dw_line_V230605.py b/examples/signals_dev/merged/cmo_up_dw_line_V230605.py deleted file mode 100644 index 7eb9ac383..000000000 --- a/examples/signals_dev/merged/cmo_up_dw_line_V230605.py +++ /dev/null @@ -1,66 +0,0 @@ -from collections import OrderedDict -import numpy as np -import pandas as pd -from loguru import logger -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def cmo_up_dw_line_V230605(c: CZSC, **kwargs) -> OrderedDict: - """CMO能量异动,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}M{m}_CMO能量V230605" - - 信号逻辑:** - - CMO指标用过去N天的价格上涨量和价格下跌量得到,CMO>(<)0 表示当前处于上涨(下跌)趋势,CMO 越 - 大(小)则当前上涨(下跌)趋势越强。我们用 CMO 上穿 30/下穿-30来产生买入/卖出信号。 - - 信号列表: - - - Signal('30分钟_D1N70M30_CMO能量V230605_看空_任意_任意_0') - - Signal('30分钟_D1N70M30_CMO能量V230605_看多_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数,默认为60 - - :param m: 信号预警轴,默认为30 - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 70)) - m = int(kwargs.get("m", 30)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}M{m}_CMO能量V230605".split('_') - - v1 = "其他" - if len(c.bars_raw) < di + n + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - _bars = get_sub_elements(c.bars_raw, di=di, n=n) - up_sum = np.sum([_bars[i].close - _bars[i - 1].close for i in range(1, len(_bars)) - if (_bars[i].close - _bars[i - 1].close) > 0]) - dw_sum = np.sum([_bars[i - 1].close - _bars[i].close for i in range(1, len(_bars)) - if (_bars[i - 1].close - _bars[i].close) > 0]) - - cmo = (up_sum - dw_sum) / (up_sum + dw_sum) * 100 - if cmo > m: - v1 = "看多" - if cmo < -m: - v1 = "看空" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [ - {'name': cmo_up_dw_line_V230605, 'freq': '30分钟', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() diff --git a/examples/signals_dev/merged/cvolp_up_dw_line_V230612.py b/examples/signals_dev/merged/cvolp_up_dw_line_V230612.py deleted file mode 100644 index 01339c3b8..000000000 --- a/examples/signals_dev/merged/cvolp_up_dw_line_V230612.py +++ /dev/null @@ -1,76 +0,0 @@ -from collections import OrderedDict -import numpy as np -import pandas as pd - -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def cvolp_up_dw_line_V230612(c: CZSC, **kwargs) -> OrderedDict: - """CVOLP动量变化率指标,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}M{m}UP{up}DW{dw}_CVOLP动量变化率V230612" - - **信号逻辑:** - - 成交量移动平均平滑变化率。 - 先计算了成交量的N周期指数移动平均线,然后计算了EMAP的M周期前的值,最后计算了CVOLP的值。 - CVOLP 上穿 up 买入,下穿 dw 卖出。 - - **信号列表:** - - - Signal('日线_D1N13M21UP5DW5_CVOLP动量变化率V230612_看多_任意_任意_0') - - Signal('日线_D1N13M21UP5DW5_CVOLP动量变化率V230612_看空_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - :param di: 信号计算截止倒数第i根K线 - - :param n: 取K线数量 - - :param m: 信号预警值 - - :param up: 看多信号预警值 - - :param dw: 看空信号预警值 - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 34)) - m = int(kwargs.get("m", 55)) - up = int(kwargs.get("up", 5)) - dw = int(kwargs.get("dw", 5)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}M{m}UP{up}DW{dw}_CVOLP动量变化率V230612".split('_') - - # 增加一个约束,如果K线数量不足时直接返回 - v1 = "其他" - if len(c.bars_raw) < di + n + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=n + m) - - volume = np.array([bar.vol for bar in bars]) - n_weights = np.exp(np.linspace(-1., 0., n)) - n_weights /= n_weights.sum() - emap = np.convolve(volume, n_weights, mode='full')[:len(volume)] - emap[:n] = emap[n] - sroc = (emap - np.roll(emap, m))[-1] / np.roll(emap, m)[-1] - - if sroc > up / 100: - v1 = "看多" - if sroc < -dw / 100: - v1 = "看空" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20171101', '20210101', fq='前复权') - - signals_config = [ - {'name': cvolp_up_dw_line_V230612, 'freq': '日线', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/signals_dev/merged/cxt_bi_end_V230618.py b/examples/signals_dev/merged/cxt_bi_end_V230618.py deleted file mode 100644 index f35e0937f..000000000 --- a/examples/signals_dev/merged/cxt_bi_end_V230618.py +++ /dev/null @@ -1,125 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_bi_end_V230618(c: CZSC, **kwargs) -> OrderedDict: - """笔结束辅助判断 - - 参数模板:"{freq}_D{di}MO{max_overlap}_BE辅助V230618" - - **信号逻辑:** - - 以向下笔为例,判断笔内是否有小级别中枢,如果有则看多: - - 1. 笔内任意两根k线的重叠使该价格位的计数加1,计算从笔.high到笔.low之间各价格位的重叠次数 - 2. 通过各价格位的重叠可以得到横轴价格,纵轴重叠次数的图,通过计算途中波峰的个数来得到近似的小中枢个数 - 例子:横轴从小到大对应的重叠次数为 1112233211112133334445553321,则可以通过计算从n变为1的次数来得到波峰个数 - 这里2-1,2-1,2-1,得到波峰数为3 - - **信号列表:** - - - Signal('日线_D1MO1_BE辅助V230618_看多_1小中枢_任意_0') - - Signal('日线_D1MO1_BE辅助V230618_看空_3小中枢_任意_0') - - Signal('日线_D1MO1_BE辅助V230618_看空_2小中枢_任意_0') - - Signal('日线_D1MO1_BE辅助V230618_看空_1小中枢_任意_0') - - Signal('日线_D1MO1_BE辅助V230618_看多_2小中枢_任意_0') - - Signal('日线_D1MO1_BE辅助V230618_看空_5小中枢_任意_0') - - Signal('日线_D1MO1_BE辅助V230618_看空_4小中枢_任意_0') - - Signal('日线_D1MO1_BE辅助V230618_看多_3小中枢_任意_0') - - **信号说明:** - - 类似 cxt_third_bs_V230318 信号,但增加了笔内有无小级别中枢的判断。用k线重叠来近似小级别中枢的判断 - - :param c: CZSC对象 - :param kwargs: - - - di: int, 默认1,表示取倒数第几笔 - - max_overlap: int, 默认3,表示笔内最多允许有几个信号重叠 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - max_overlap = int(kwargs.get("max_overlap", 3)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}MO{max_overlap}_BE辅助V230618".split('_') - v1 = "其他" - if len(c.bi_list) < di + 6 or len(c.bars_ubi) > 3 + max_overlap - 1: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - def __cal_zs_number(raw_bars): - """计算笔内的小中枢数量 - - **信号逻辑:** - - 1. 笔内任意两根k线的重叠使该价格位的计数加1,计算从笔.high到笔.low之间各价格位的重叠次数 - 2. 通过各价格位的重叠可以得到横轴价格,纵轴重叠次数的图,通过计算途中波峰的个数来得到近似的小中枢个数 - 例子:横轴从小到大对应的重叠次数为 1112233211112133334445553321,则可以通过计算从n变为1的次数来得到波峰个数 - 这里2-1,2-1,2-1,得到波峰数为3 - - :param raw_bars: 构成笔的bar - :return: 小中枢数量 - """ - # 用笔内价格极值取得笔内价格范围 - max_price = max(bar.high for bar in raw_bars[:-1]) - min_price = min(bar.low for bar in raw_bars[:-1]) - price_range = max_price - min_price - - # 计算当前k线所覆盖的笔内价格范围,并用百分比表示 - for bar in raw_bars[:-1]: - bar_high_pct = int((100 * (bar.high - min_price) / price_range)) - bar_low_pct = int((100 * (bar.low - min_price) / price_range)) - bar.dt_high_pct = bar_high_pct - bar.dt_low_pct = bar_low_pct - - # 用这个list保存每个价格的重叠次数,把每个价格映射到100以内的区间内 - df_chengjiaoqu = [[i, 0] for i in range(101)] - - # 对每个k线进行映射,把该k线的价格范围映射到df_chengjiaoqu - for bar in raw_bars[:-1]: - range_max = bar.dt_high_pct - range_min = bar.dt_low_pct - - if range_max == range_min: - df_chengjiaoqu[range_max][1] += 1 - else: - for i in range(range_min, range_max + 1): - df_chengjiaoqu[i][1] += 1 - - # 计算波峰个数,相当于有多少个小中枢 - # 每个波峰结束后价格重叠区域必然会回到1 - peak_count = 0 - for i in range(1, len(df_chengjiaoqu) - 1): - if df_chengjiaoqu[i][1] == 1 and df_chengjiaoqu[i][1] < df_chengjiaoqu[i - 1][1]: - peak_count += 1 - return peak_count - - bi = c.bi_list[-di] - zs_count = __cal_zs_number(bi.raw_bars) - v1 = '看多' if bi.direction == Direction.Down else '看空' - # 为了增加稳定性,要确保笔内有小中枢,并且要确保笔内有至少2个分型存在,保证从上往下的分型12的长度比分型34的长度大,来确认背驰 - if len(bi.fxs) >= 4 and zs_count >= 1 and (bi.fxs[-4].fx - bi.fxs[-3].fx) - (bi.fxs[-2].fx - bi.fxs[-1].fx) > 0: - v2 = f"{zs_count}小中枢" - else: - v2 = "其他" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': cxt_bi_end_V230618, 'freq': '日线', 'di': 1, 'max_overlap': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_bi_end_V230815.py b/examples/signals_dev/merged/cxt_bi_end_V230815.py deleted file mode 100644 index 65de92f44..000000000 --- a/examples/signals_dev/merged/cxt_bi_end_V230815.py +++ /dev/null @@ -1,53 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.objects import Mark -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_bi_end_V230815(c: CZSC, **kwargs) -> OrderedDict: - """一两根K线快速突破反向笔 - - 参数模板:"{freq}_快速突破_BE辅助V230815" - - **信号逻辑:** - - 以向上笔为例:右侧分型完成后第一根K线的最低价低于该笔的最低价,认为向上笔结束,反向向下笔开始。 - - **信号列表:** - - - Signal('15分钟_快速突破_BE辅助V230815_向下_任意_任意_0') - - Signal('15分钟_快速突破_BE辅助V230815_向上_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: - :return: 信号识别结果 - """ - freq = c.freq.value - k1, k2, k3 = f"{freq}_快速突破_BE辅助V230815".split('_') - v1 = '其他' - if len(c.bi_list) < 5 or len(c.bars_ubi) >= 5: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bi, last_bar = c.bi_list[-1], c.bars_ubi[-1] - if bi.direction == Direction.Up and last_bar.low < bi.low: - v1 = '向下' - if bi.direction == Direction.Down and last_bar.high > bi.high: - v1 = '向上' - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': cxt_bi_end_V230815, 'freq': '15分钟'}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_bi_stop_V230815.py b/examples/signals_dev/merged/cxt_bi_stop_V230815.py deleted file mode 100644 index 0be562ac2..000000000 --- a/examples/signals_dev/merged/cxt_bi_stop_V230815.py +++ /dev/null @@ -1,61 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.objects import Mark -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_bi_stop_V230815(c: CZSC, **kwargs) -> OrderedDict: - """定位笔的止损距离大小 - - 参数模板:"{freq}_距离{th}BP_止损V230815" - - **信号逻辑:** - - 以向上笔为例:如果当前K线的收盘价高于该笔的最高价的1 - 0.5%,则认为在止损阈值内,否则认为在止损阈值外。 - - **信号列表:** - - - Signal('15分钟_距离50BP_止损V230815_向下_阈值外_任意_0') - - Signal('15分钟_距离50BP_止损V230815_向上_阈值内_任意_0') - - Signal('15分钟_距离50BP_止损V230815_向下_阈值内_任意_0') - - Signal('15分钟_距离50BP_止损V230815_向上_阈值外_任意_0') - - :param c: CZSC对象 - :param kwargs: - - - th: 止损距离阈值,单位为BP, 默认为50BP, 即0.5% - - :return: 信号识别结果 - """ - th = int(kwargs.get('th', 50)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_距离{th}BP_止损V230815".split('_') - v1, v2 = '其他', '其他' - if len(c.bi_list) < 5: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bi, last_bar = c.bi_list[-1], c.bars_ubi[-1] - if bi.direction == Direction.Up: - v1 = '向下' - v2 = "阈值内" if last_bar.close > bi.high * (1 - th / 10000) else "阈值外" - if bi.direction == Direction.Down: - v1 = '向上' - v2 = "阈值内" if last_bar.close < bi.low * (1 + th / 10000) else "阈值外" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': cxt_bi_stop_V230815, 'freq': '15分钟'}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_bi_trend_V230824.py b/examples/signals_dev/merged/cxt_bi_trend_V230824.py deleted file mode 100644 index 4ac38179e..000000000 --- a/examples/signals_dev/merged/cxt_bi_trend_V230824.py +++ /dev/null @@ -1,77 +0,0 @@ -import sys - -sys.path.insert(0, '.') -sys.path.insert(0, '..') - -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_bi_trend_V230824(c: CZSC, **kwargs) -> OrderedDict: - """判断N笔形态 - - 参数模板:"{freq}_D{di}N{n}TH{th}_形态V230824" - - **信号逻辑:** - - 1. 通过对最近N笔的中心点的均值和-n笔的中心点的位置关系来判断当前N比是上涨形态还是下跌,横盘震荡形态 - 2. 给定阈值 th,判断上涨下跌横盘按照 所有笔中心点/第-n笔中心点 与 正负th区间的相对位置来判断。 - 3. 当在区间上时为上涨,区间内为横盘,区间下为下跌 - - **信号列表:** - - - Signal('日线_D1N4TH5_形态V230824_横盘_任意_任意_0') - - Signal('日线_D1N4TH5_形态V230824_向上_任意_任意_0') - - Signal('日线_D1N4TH5_形态V230824_向下_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: - - - di: 倒数第几笔 - - n :检查范围 - - th: 振幅阈值,2 表示 2%,即 2% 以内的振幅都认为是震荡 - - :return: 信号识别结果 - """ - di = int(kwargs.get('di', 1)) - n = int(kwargs.get('n', 4)) - th = int(kwargs.get('th', 2)) # 振幅阈值,2 表示 2%,即 2% 以内的振幅都认为是震荡 - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}TH{th}_形态V230824".split('_') - v1 = '其他' - if len(c.bi_list) < di + n + 2: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - _bis = get_sub_elements(c.bi_list, di=di, n=n) - assert len(_bis) == n, f"获取第 {di} 笔到第 {di+n} 笔失败" - - all_means = [(bi.low + bi.high) / 2 for bi in _bis] - average_of_means = sum(all_means) / n - ratio = all_means[0] / average_of_means - - if ratio * 100 > 100 + th: - v1 = "向下" - elif ratio * 100 < 100 - th: - v1 = "向上" - else: - v1 = "横盘" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[0] - # for symbol in symbols[:10]: - bars = research.get_raw_bars(symbol, '15分钟', '20181101', '20210101', fq='前复权') - signals_config = [{'name': cxt_bi_trend_V230824, 'freq': '日线', 'di': 1, 'n': 6, 'th': 5}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_eleven_bi_V230622.py b/examples/signals_dev/merged/cxt_eleven_bi_V230622.py deleted file mode 100644 index 88c3e8f60..000000000 --- a/examples/signals_dev/merged/cxt_eleven_bi_V230622.py +++ /dev/null @@ -1,130 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_eleven_bi_V230622(c: CZSC, **kwargs) -> OrderedDict: - """十一笔形态分类 - - 参数模板:"{freq}_D{di}十一笔_形态V230622" - - **信号逻辑:** - - 十一笔的形态分类 - - **信号列表:** - - - Signal('60分钟_D1九笔_形态V230621_类三买_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_A3B3C5式类一卖_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_类二买_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_A5B3C3式类一卖_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: - - - di: 倒数第几笔 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}九笔_形态V230621".split('_') - v1 = "其他" - if len(c.bi_list) < di + 16 or len(c.bars_ubi) > 7: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bis = get_sub_elements(c.bi_list, di=di, n=11) - assert len(bis) == 11 and bis[0].direction == bis[2].direction == bis[4].direction, "笔的方向错误" - bi1, bi2, bi3, bi4, bi5, bi6, bi7, bi8, bi9, bi10, bi11 = bis - max_high = max([x.high for x in bis]) - min_low = min([x.low for x in bis]) - direction = bi11.direction - - if direction == Direction.Down: - if min_low == bi11.low and max_high == bi1.high: - # ABC式类一买,A5B3C3 - if bi5.low == min([x.low for x in [bi1, bi3, bi5]]) \ - and bi9.low > bi11.low and bi9.high > bi11.high \ - and bi8.high > bi6.low and bi1.high - bi5.low > bi9.high - bi11.low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='A5B3C3式类一买') - - # ABC式类一买,A3B3C5 - if bi1.high > bi3.high and bi1.low > bi3.low \ - and bi7.high == max([x.high for x in [bi7, bi9, bi11]]) \ - and bi6.high > bi4.low and bi1.high - bi3.low > bi7.high - bi11.low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='A3B3C5式类一买') - - # ABC式类一买,A3B5C3 - if bi1.low > bi3.low and min(bi4.high, bi6.high, bi8.high) > max(bi4.low, bi6.low, bi8.low) \ - and bi9.high > bi11.high and bi1.high - bi3.low > bi9.high - bi11.low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='A3B5C3式类一买') - - # a1Ab式类一买,a1(1~7构成的类趋势) - if bi2.low > bi4.high > bi4.low > bi6.high > bi5.low > bi7.low and bi10.high > bi8.low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='a1Ab式类一买') - - # 类二买(1~7构成盘整背驰,246构成下跌中枢,9/11构成上涨中枢,且上涨中枢GG大于下跌中枢ZG) - if bi7.power < bi1.power and min_low == bi7.low < max([x.low for x in [bi2, bi4, bi6]]) \ - < min([x.high for x in [bi2, bi4, bi6]]) < max([x.high for x in [bi9, bi11]]) < bi1.high == max_high \ - and bi11.low > min([x.low for x in [bi2, bi4, bi6]]) \ - and min([x.high for x in [bi9, bi11]]) > max([x.low for x in [bi9, bi11]]): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类二买') - - # 类二买(1~7为区间极值,9~11构成上涨中枢,上涨中枢GG大于4~6的最大值,上涨中枢DD大于4~6的最小值) - if max_high == bi1.high and min_low == bi7.low \ - and min(bi9.high, bi11.high) > max(bi9.low, bi11.low) \ - and max(bi11.high, bi9.high) > max(bi4.high, bi6.high) \ - and min(bi9.low, bi11.low) > min(bi4.low, bi6.low): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类二买') - - # 类三买(1~9构成大级别中枢,10离开,11回调不跌破GG) - gg = max([x.high for x in [bi1, bi2, bi3]]) - zg = min([x.high for x in [bi1, bi2, bi3]]) - zd = max([x.low for x in [bi1, bi2, bi3]]) - dd = min([x.low for x in [bi1, bi2, bi3]]) - if max_high == bi11.high and bi11.low > zg > zd \ - and gg > bi5.low and gg > bi7.low and gg > bi9.low \ - and dd < bi5.high and dd < bi7.high and dd < bi9.high: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类三买') - - if direction == Direction.Up: - if max_high == bi11.high and min_low == bi1.low: - # ABC式类一卖,A5B3C3 - if bi5.high == max([bi1.high, bi3.high, bi5.high]) and bi9.low < bi11.low and bi9.high < bi11.high \ - and bi8.low < bi6.high and bi11.high - bi9.low < bi5.high - bi1.low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='A5B3C3式类一卖') - - # ABC式类一卖,A3B3C5 - if bi7.low == min([bi11.low, bi9.low, bi7.low]) and bi1.high < bi3.high and bi1.low < bi3.low \ - and bi6.low < bi4.high and bi11.high - bi7.low < bi3.high - bi1.low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='A3B3C5式类一卖') - - # ABC式类一卖,A3B5C3 - if bi1.high < bi3.high and min(bi4.high, bi6.high, bi8.high) > max(bi4.low, bi6.low, bi8.low) \ - and bi9.low < bi11.low and bi3.high - bi1.low > bi11.high - bi9.low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='A3B5C3式类一卖') - - # 类二卖:1~9构成类趋势,11不创新高 - if max_high == bi9.high > bi8.low > bi6.high > bi6.low > bi4.high > bi4.low > bi2.high > bi1.low == min_low \ - and bi11.high < bi9.high: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类二卖') - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': cxt_eleven_bi_V230622, 'freq': '60分钟', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_five_bi_V230619.py b/examples/signals_dev/merged/cxt_five_bi_V230619.py deleted file mode 100644 index fbca011fb..000000000 --- a/examples/signals_dev/merged/cxt_five_bi_V230619.py +++ /dev/null @@ -1,107 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_five_bi_V230619(c: CZSC, **kwargs) -> OrderedDict: - """五笔形态分类 - - 参数模板:"{freq}_D{di}五笔_形态V230619" - - **信号逻辑:** - - 五笔的形态分类 - - **信号列表:** - - - Signal('60分钟_D1五笔_形态V230619_上颈线突破_任意_任意_0') - - Signal('60分钟_D1五笔_形态V230619_类三卖_任意_任意_0') - - Signal('60分钟_D1五笔_形态V230619_类趋势底背驰_任意_任意_0') - - Signal('60分钟_D1五笔_形态V230619_类趋势顶背驰_任意_任意_0') - - Signal('60分钟_D1五笔_形态V230619_下颈线突破_任意_任意_0') - - Signal('60分钟_D1五笔_形态V230619_类三买_任意_任意_0') - - Signal('60分钟_D1五笔_形态V230619_aAb式顶背驰_任意_任意_0') - - Signal('60分钟_D1五笔_形态V230619_aAb式底背驰_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: - - - di: 倒数第几笔 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}五笔_形态V230619".split('_') - v1 = "其他" - if len(c.bi_list) < di + 6 or len(c.bars_ubi) > 7: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bis = get_sub_elements(c.bi_list, di=di, n=5) - assert len(bis) == 5 and bis[0].direction == bis[2].direction == bis[4].direction, "笔的方向错误" - bi1, bi2, bi3, bi4, bi5 = bis - - direction = bi1.direction - max_high = max([x.high for x in bis]) - min_low = min([x.low for x in bis]) - assert direction in [Direction.Down, Direction.Up], "direction 的取值错误" - - if direction == Direction.Down: - # aAb式底背驰 - if min(bi2.high, bi4.high) > max(bi2.low, bi4.low) and max_high == bi1.high and bi5.power < bi1.power: - if (min_low == bi3.low and bi5.low < bi1.low) or (min_low == bi5.low): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAb式底背驰') - - # 类趋势底背驰 - if max_high == bi1.high and min_low == bi5.low and bi4.high < bi2.low and bi5.power < max(bi3.power, bi1.power): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类趋势底背驰') - - # 上颈线突破 - if (min_low == bi1.low and bi5.high > min(bi1.high, bi2.high) > bi5.low > bi1.low) \ - or (min_low == bi3.low and bi5.high > bi3.high > bi5.low > bi3.low): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='上颈线突破') - - # 五笔三买,要求bi5.high是最高点 - if max_high == bi5.high > bi5.low > max(bi1.high, bi3.high) \ - > min(bi1.high, bi3.high) > max(bi1.low, bi3.low) > min_low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类三买') - - if direction == Direction.Up: - # aAb式顶背驰 - if min(bi2.high, bi4.high) > max(bi2.low, bi4.low) and min_low == bi1.low and bi5.power < bi1.power: - if (max_high == bi3.high and bi5.high > bi1.high) or (max_high == bi5.high): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAb式顶背驰') - - # 类趋势顶背驰 - if min_low == bi1.low and max_high == bi5.high and bi5.power < max(bi1.power, bi3.power) and bi4.low > bi2.high: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类趋势顶背驰') - - # 下颈线突破 - if (max_high == bi1.high and bi5.low < max(bi1.low, bi2.low) < bi5.high < max_high) \ - or (max_high == bi3.high and bi5.low < bi3.low < bi5.high < max_high): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='下颈线突破') - - # 五笔三卖,要求bi5.low是最低点 - if min_low == bi5.low < bi5.high < min(bi1.low, bi3.low) \ - < max(bi1.low, bi3.low) < min(bi1.high, bi3.high) < max_high: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类三卖') - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': cxt_five_bi_V230619, 'freq': '60分钟', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_intraday_V230701.py b/examples/signals_dev/merged/cxt_intraday_V230701.py deleted file mode 100644 index bd16b55e1..000000000 --- a/examples/signals_dev/merged/cxt_intraday_V230701.py +++ /dev/null @@ -1,98 +0,0 @@ -from loguru import logger -from czsc import CzscSignals -from collections import OrderedDict -from czsc.utils import create_single_signal - - -def cxt_intraday_V230701(cat: CzscSignals, **kwargs) -> OrderedDict: - """每日走势分类 - - 参数模板:"{freq1}#{freq2}_D{di}日_走势分类V230701" - - **信号逻辑:** - - 参见博客:https://blog.sina.com.cn/s/blog_486e105c010009uy.html - - **信号列表:** - - - Signal('30分钟#日线_D2日_走势分类V230701_强平衡市_任意_任意_0') - - Signal('30分钟#日线_D2日_走势分类V230701_弱平衡市_任意_任意_0') - - Signal('30分钟#日线_D2日_走势分类V230701_双中枢下跌_任意_任意_0') - - Signal('30分钟#日线_D2日_走势分类V230701_转折平衡市_任意_任意_0') - - Signal('30分钟#日线_D2日_走势分类V230701_双中枢上涨_任意_任意_0') - - :param c: CZSC对象 - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 2)) - freq1 = kwargs.get("freq1", "30分钟") - freq2 = kwargs.get("freq2", "日线") - assert freq1 == '30分钟', 'freq1必须为30分钟' - assert freq2 == '日线', 'freq2必须为日线' - - assert 21 > di > 0, "di必须为大于0小于21的整数,暂不支持当日走势分类" - k1, k2, k3 = f"{freq1}#{freq2}_D{di}日_走势分类V230701".split('_') - v1 = "其他" - if '30分钟' not in cat.kas.keys() or '日线' not in cat.kas.keys(): - logger.warning(f"缺少30分钟或日线K线数据,无法计算当日走势分类, {cat.kas.keys()}") - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - c1 = cat.kas[freq1] - c2 = cat.kas[freq2] - day = c2.bars_raw[-di].dt.date() - bars = [x for x in c1.bars_raw if x.dt.date() == day] - assert len(bars) <= 8, f"仅适用于A股市场,日内有8根30分钟K线的情况, {len(bars)}, {day}, {bars}" - if len(bars) <= 4: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - zs_list = [] - for b1, b2, b3 in zip(bars[:-2], bars[1:-1], bars[2:]): - if min(b1.high, b2.high, b3.high) > max(b1.low, b2.low, b3.low): - zs_list.append([b1, b2, b3]) - - _dir = "上涨" if bars[-1].close > bars[0].open else "下跌" - - if not zs_list: - v1 = f"无中枢{_dir}" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - # 双中枢的情况,有一根K线的 high low 与前后两个中枢没有重叠 - if len(zs_list) >= 2: - zs1, zs2 = zs_list[0], zs_list[-1] - zs1_high, zs1_low = max([x.high for x in zs1]), min([x.low for x in zs1]) - zs2_high, zs2_low = max([x.high for x in zs2]), min([x.low for x in zs2]) - if _dir == "上涨" and zs1_high < zs2_low: - v1 = f"双中枢{_dir}" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - if _dir == "下跌" and zs1_low > zs2_high: - v1 = f"双中枢{_dir}" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - # 单中枢的情况,前三根K线出现高点:弱平衡市,前三根K线出现低点:强平衡市,否则:转折平衡市 - high_first = max(bars[0].high, bars[1].high, bars[2].high) == max([x.high for x in bars]) - low_first = min(bars[0].low, bars[1].low, bars[2].low) == min([x.low for x in bars]) - if high_first and not low_first: - v1 = "弱平衡市" - elif low_first and not high_first: - v1 = "强平衡市" - else: - v1 = "转折平衡市" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': cxt_intraday_V230701, 'di': 1, 'freq1': '30分钟', 'freq2': '日线'}] - check_signals_acc(bars, signals_config=signals_config, height='780px', delta_days=0) # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_nine_bi_V230621.py b/examples/signals_dev/merged/cxt_nine_bi_V230621.py deleted file mode 100644 index ae4845951..000000000 --- a/examples/signals_dev/merged/cxt_nine_bi_V230621.py +++ /dev/null @@ -1,172 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_nine_bi_V230621(c: CZSC, **kwargs) -> OrderedDict: - """九笔形态分类 - - 参数模板:"{freq}_D{di}九笔_形态V230621" - - **信号逻辑:** - - 九笔的形态分类 - - **信号列表:** - - - Signal('60分钟_D1九笔_形态V230621_类三买A_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_aAb式类一卖_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_类三卖A_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_aAbcd式类一买_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_ABC式类一卖_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_aAbBc式类一买_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_aAbcd式类一卖_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_ZD三卖_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_aAbBc式类一卖_任意_任意_0') - - Signal('60分钟_D1九笔_形态V230621_ABC式类一买_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: - - - di: 倒数第几笔 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}九笔_形态V230621".split('_') - v1 = "其他" - if len(c.bi_list) < di + 13 or len(c.bars_ubi) > 7: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bis = get_sub_elements(c.bi_list, di=di, n=9) - assert len(bis) == 9 and bis[0].direction == bis[2].direction == bis[4].direction, "笔的方向错误" - bi1, bi2, bi3, bi4, bi5, bi6, bi7, bi8, bi9 = bis - max_high = max([x.high for x in bis]) - min_low = min([x.low for x in bis]) - direction = bi9.direction - - if direction == Direction.Down: - if min_low == bi9.low and max_high == bi1.high: - # aAb式类一买 - if min(bi2.high, bi4.high, bi6.high, bi8.high) > max(bi2.low, bi4.low, bi6.low, bi8.low) \ - and bi9.power < bi1.power and bi3.low >= bi1.low and bi7.high <= bi9.high: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAb式类一买') - - # aAbcd式类一买 - if min(bi2.high, bi4.high, bi6.high) > max(bi2.low, bi4.low, bi6.low) > bi8.high \ - and bi9.power < bi7.power: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAbcd式类一买') - - # ABC式类一买 - if bi3.low < bi1.low and bi7.high > bi9.high \ - and min(bi4.high, bi6.high) > max(bi4.low, bi6.low) \ - and (bi1.high - bi3.low) > (bi7.high - bi9.low): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='ABC式类一买') - - # 类趋势一买 - if bi8.high < bi6.low < bi6.high < bi4.low < bi4.high < bi2.low \ - and bi9.power < max([bi1.power, bi3.power, bi5.power, bi7.power]): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类趋势一买') - - # aAbBc式类一买(2~4构成中枢A,6~8构成中枢B,9背驰) - if max_high == max(bi1.high, bi3.high) and min_low == bi9.low \ - and min(bi2.high, bi4.high) > max(bi2.low, bi4.low) \ - and min(bi2.low, bi4.low) > max(bi6.high, bi8.high) \ - and min(bi6.high, bi8.high) > max(bi6.low, bi8.low) \ - and bi9.power < bi5.power: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAbBc式类一买') - - # 类三买(1357构成中枢,最低点在3或5) - if max_high == bi9.high > bi9.low \ - > max([x.high for x in [bi1, bi3, bi5, bi7]]) \ - > min([x.high for x in [bi1, bi3, bi5, bi7]]) \ - > max([x.low for x in [bi1, bi3, bi5, bi7]]) \ - > min([x.low for x in [bi3, bi5]]) == min_low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类三买A') - - # 类三买(357构成中枢,8的力度小于2,9回调不跌破GG构成三买) - if bi8.power < bi2.power and max_high == bi9.high > bi9.low \ - > max([x.high for x in [bi3, bi5, bi7]]) \ - > min([x.high for x in [bi3, bi5, bi7]]) \ - > max([x.low for x in [bi3, bi5, bi7]]) > bi1.low == min_low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类三买B') - - if min_low == bi5.low and max_high == bi1.high and bi4.high < bi2.low: # 前五笔构成向下类趋势 - zd = max([x.low for x in [bi5, bi7]]) - zg = min([x.high for x in [bi5, bi7]]) - gg = max([x.high for x in [bi5, bi7]]) - if zg > zd and bi8.high > gg: # 567构成中枢,且8的高点大于gg - if bi9.low > zg: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='ZG三买') - - # 类二买 - if bi9.high > gg > zg > bi9.low > zd: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类二买') - - if direction == Direction.Up: - if max_high == bi9.high and min_low == bi1.low: - # aAbBc式类一卖 - if bi6.low > min(bi2.high, bi4.high) > max(bi2.low, bi4.low) \ - and min(bi6.high, bi8.high) > max(bi6.low, bi8.low) \ - and max(bi2.high, bi4.high) < min(bi6.low, bi8.low) \ - and bi9.power < bi5.power: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAbBc式类一卖') - - # aAb式类一卖 - if min(bi2.high, bi4.high, bi6.high, bi8.high) > max(bi2.low, bi4.low, bi6.low, bi8.low) \ - and bi9.power < bi1.power and bi3.high <= bi1.high and bi7.low >= bi9.low: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAb式类一卖') - - # aAbcd式类一卖 - if bi8.low > min(bi2.high, bi4.high, bi6.high) > max(bi2.low, bi4.low, bi6.low) \ - and bi9.power < bi7.power: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAbcd式类一卖') - - # ABC式类一卖 - if bi3.high > bi1.high and bi7.low < bi9.low \ - and min(bi4.high, bi6.high) > max(bi4.low, bi6.low) \ - and (bi3.high - bi1.low) > (bi9.high - bi7.low): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='ABC式类一卖') - - # 类趋势一卖 - if bi8.low > bi6.high > bi6.low > bi4.high > bi4.low > bi2.high \ - and bi9.power < max([bi1.power, bi3.power, bi5.power, bi7.power]): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类趋势一卖') - - # 九笔三卖 - if max_high == bi1.high and min_low == bi9.low \ - and bi9.high < max([x.low for x in [bi3, bi5, bi7]]) < min([x.high for x in [bi3, bi5, bi7]]): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类三卖A') - - if min_low == bi1.low and max_high == bi5.high and bi2.high < bi4.low: # 前五笔构成向上类趋势 - zd = max([x.low for x in [bi5, bi7]]) - zg = min([x.high for x in [bi5, bi7]]) - dd = min([x.low for x in [bi5, bi7]]) - if zg > zd and bi8.low < dd: # 567构成中枢,且8的低点小于dd - if bi9.high < zd: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='ZD三卖') - - # 类二卖 - if dd < zd <= bi9.high < zg: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类二卖') - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': cxt_nine_bi_V230621, 'freq': '60分钟', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_range_oscillation_V230620.py b/examples/signals_dev/merged/cxt_range_oscillation_V230620.py deleted file mode 100644 index 3ac2e4ec0..000000000 --- a/examples/signals_dev/merged/cxt_range_oscillation_V230620.py +++ /dev/null @@ -1,88 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_range_oscillation_V230620(c: CZSC, **kwargs) -> OrderedDict: - """判断区间震荡 - - 参数模板:"{freq}_D{di}TH{th}_区间震荡V230620" - - **信号逻辑:** - - 1. 在区间震荡中,无论振幅大小,各笔的中心应改在相近的价格区间内平移,当各笔的中心的振幅大于一定数值时就认为这个窗口内没有固定区间的中枢震荡 - 2. 给定阈值 th,当各笔的中心的振幅大于 th 时,认为这个窗口内没有固定区间的中枢震荡 - - **信号列表:** - - - Signal('日线_D1TH5_区间震荡V230620_2笔震荡_向下_任意_0') - - Signal('日线_D1TH5_区间震荡V230620_3笔震荡_向上_任意_0') - - Signal('日线_D1TH5_区间震荡V230620_4笔震荡_向下_任意_0') - - Signal('日线_D1TH5_区间震荡V230620_5笔震荡_向上_任意_0') - - Signal('日线_D1TH5_区间震荡V230620_6笔震荡_向下_任意_0') - - Signal('日线_D1TH5_区间震荡V230620_5笔震荡_向下_任意_0') - - Signal('日线_D1TH5_区间震荡V230620_2笔震荡_向上_任意_0') - - Signal('日线_D1TH5_区间震荡V230620_3笔震荡_向下_任意_0') - - Signal('日线_D1TH5_区间震荡V230620_4笔震荡_向上_任意_0') - - :param c: CZSC对象 - :param kwargs: - - - di: 倒数第几笔 - - th: 振幅阈值,2 表示 2%,即 2% 以内的振幅都认为是震荡 - - :return: 信号识别结果 - """ - di = int(kwargs.get('di', 1)) - th = int(kwargs.get('th', 2)) # 振幅阈值,2 表示 2%,即 2% 以内的振幅都认为是震荡 - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}TH{th}_区间震荡V230620".split('_') - v1, v2 = '其他', '其他' - if len(c.bi_list) < di + 11: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - def __calculate_max_amplitude_percentage(prices): - """计算给定价位列表的最大振幅的百分比""" - if not prices: - return 100 - max_price, min_price = max(prices), min(prices) - return ((max_price - min_price) / min_price) * 100 if min_price != 0 else 100 - - _bis = get_sub_elements(c.bi_list, di=di, n=12) - - if len(_bis) != 12: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - price_list = [] - count = 1 - for bi in _bis[::-1]: - price_list.append((bi.high + bi.low) / 2) - if len(price_list) > 1: - if __calculate_max_amplitude_percentage(price_list) < th: - count += 1 - else: - break - - if count != 1: - v1 = f"{count}笔震荡" - v2 = "向上" if _bis[-1].direction == Direction.Up else "向下" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[0] - # for symbol in symbols[:10]: - bars = research.get_raw_bars(symbol, '15分钟', '20181101', '20210101', fq='前复权') - signals_config = [{'name': cxt_range_oscillation_V230620, 'freq': '日线', 'di': 1, 'th': 5}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_seven_bi_V230620.py b/examples/signals_dev/merged/cxt_seven_bi_V230620.py deleted file mode 100644 index c2175e948..000000000 --- a/examples/signals_dev/merged/cxt_seven_bi_V230620.py +++ /dev/null @@ -1,131 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_seven_bi_V230620(c: CZSC, **kwargs) -> OrderedDict: - """七笔形态分类 - - 参数模板:"{freq}_D{di}七笔_形态V230620" - - **信号逻辑:** - - 七笔的形态分类 - - **信号列表:** - - - Signal('60分钟_D1七笔_形态V230620_类三卖_任意_任意_0') - - Signal('60分钟_D1七笔_形态V230620_向上中枢完成_任意_任意_0') - - Signal('60分钟_D1七笔_形态V230620_aAbcd式顶背驰_任意_任意_0') - - Signal('60分钟_D1七笔_形态V230620_类三买_任意_任意_0') - - Signal('60分钟_D1七笔_形态V230620_向下中枢完成_任意_任意_0') - - Signal('60分钟_D1七笔_形态V230620_aAb式底背驰_任意_任意_0') - - Signal('60分钟_D1七笔_形态V230620_abcAd式顶背驰_任意_任意_0') - - Signal('60分钟_D1七笔_形态V230620_abcAd式底背驰_任意_任意_0') - - Signal('60分钟_D1七笔_形态V230620_aAb式顶背驰_任意_任意_0') - - Signal('60分钟_D1七笔_形态V230620_类趋势顶背驰_任意_任意_0') - - Signal('60分钟_D1七笔_形态V230620_aAbcd式底背驰_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: - - - di: 倒数第几笔 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}七笔_形态V230620".split('_') - v1 = "其他" - if len(c.bi_list) < di + 10 or len(c.bars_ubi) > 7: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bis = get_sub_elements(c.bi_list, di=di, n=7) - assert len(bis) == 7 and bis[0].direction == bis[2].direction == bis[4].direction, "笔的方向错误" - bi1, bi2, bi3, bi4, bi5, bi6, bi7 = bis - max_high = max([x.high for x in bis]) - min_low = min([x.low for x in bis]) - direction = bi7.direction - - if direction == Direction.Down: - if bi1.high == max_high and bi7.low == min_low: - # aAbcd式底背驰 - if min(bi2.high, bi4.high) > max(bi2.low, bi4.low) > bi6.high and bi7.power < bi5.power: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAbcd式底背驰') - - # abcAd式底背驰 - if bi2.low > min(bi4.high, bi6.high) > max(bi4.low, bi6.low) and bi7.power < (bi1.high - bi3.low): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='abcAd式底背驰') - - # aAb式底背驰 - if min(bi2.high, bi4.high, bi6.high) > max(bi2.low, bi4.low, bi6.low) and bi7.power < bi1.power: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAb式底背驰') - - # 类趋势底背驰 - if bi2.low > bi4.high and bi4.low > bi6.high and bi7.power < max(bi5.power, bi3.power, bi1.power): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类趋势底背驰') - - # 向上中枢完成 - if bi4.low == min_low and min(bi1.high, bi3.high) > max(bi1.low, bi3.low) \ - and min(bi5.high, bi7.high) > max(bi5.low, bi7.low) \ - and max(bi4.high, bi6.high) > min(bi3.high, bi4.high): - if max(bi1.low, bi3.low) < max(bi5.high, bi7.high): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='向上中枢完成') - - # 七笔三买:1~3构成中枢,最低点在1~3,最高点在5~7,5~7的最低点大于1~3的最高点 - if min(bi1.low, bi3.low) == min_low and max(bi5.high, bi7.high) == max_high \ - and min(bi5.low, bi7.low) > max(bi1.high, bi3.high) \ - and min(bi1.high, bi3.high) > max(bi1.low, bi3.low): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类三买') - - if direction == Direction.Up: - # 顶背驰 - if bi1.low == min_low and bi7.high == max_high: - # aAbcd式顶背驰 - if bi6.low > min(bi2.high, bi4.high) > max(bi2.low, bi4.low) and bi7.power < bi5.power: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAbcd式顶背驰') - - # abcAd式顶背驰 - if min(bi4.high, bi6.high) > max(bi4.low, bi6.low) > bi2.high and bi7.power < (bi3.high - bi1.low): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='abcAd式顶背驰') - - # aAb式顶背驰 - if min(bi2.high, bi4.high, bi6.high) > max(bi2.low, bi4.low, bi6.low) and bi7.power < bi1.power: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAb式顶背驰') - - # 类趋势顶背驰 - if bi2.high < bi4.low and bi4.high < bi6.low and bi7.power < max(bi5.power, bi3.power, bi1.power): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类趋势顶背驰') - - # 向下中枢完成 - if bi4.high == max_high and min(bi1.high, bi3.high) > max(bi1.low, bi3.low) \ - and min(bi5.high, bi7.high) > max(bi5.low, bi7.low) \ - and min(bi4.low, bi6.low) < max(bi3.low, bi4.low): - if min(bi1.high, bi3.high) > min(bi5.low, bi7.low): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='向下中枢完成') - - # 七笔三卖:1~3构成中枢,最高点在1~3,最低点在5~7,5~7的最高点小于1~3的最低点 - if min(bi5.low, bi7.low) == min_low and max(bi1.high, bi3.high) == max_high \ - and max(bi7.high, bi5.high) < min(bi1.low, bi3.low) \ - and min(bi1.high, bi3.high) > max(bi1.low, bi3.low): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类三卖') - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': cxt_seven_bi_V230620, 'freq': '60分钟', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_three_bi_V230618.py b/examples/signals_dev/merged/cxt_three_bi_V230618.py deleted file mode 100644 index 5d4bc49b6..000000000 --- a/examples/signals_dev/merged/cxt_three_bi_V230618.py +++ /dev/null @@ -1,92 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_three_bi_V230618(c: CZSC, **kwargs) -> OrderedDict: - """三笔形态分类 - - 参数模板:"{freq}_D{di}三笔_形态V230618" - - **信号逻辑:** - - 三笔的形态分类 - - **信号列表:** - - - Signal('日线_D1三笔_形态V230618_向下盘背_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向上奔走型_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向上扩张_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向下奔走型_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向上收敛_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向下无背_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向上不重合_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向下收敛_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向下扩张_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向下不重合_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向上盘背_任意_任意_0') - - Signal('日线_D1三笔_形态V230618_向上无背_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: - - - di: 倒数第几笔 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}三笔_形态V230618".split('_') - v1 = "其他" - if len(c.bi_list) < di + 6 or len(c.bars_ubi) > 7: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bis = get_sub_elements(c.bi_list, di=di, n=3) - assert len(bis) == 3 and bis[0].direction == bis[2].direction - bi1, bi2, bi3 = bis - - # 识别向下形态 - if bi3.direction == Direction.Down: - if bi3.low > bi1.high: - v1 = '向下不重合' - elif bi2.low < bi3.low < bi1.high < bi2.high: - v1 = '向下奔走型' - elif bi1.high > bi3.high and bi1.low < bi3.low: - v1 = '向下收敛' - elif bi1.high < bi3.high and bi1.low > bi3.low: - v1 = '向下扩张' - elif bi3.low < bi1.low and bi3.high < bi1.high: - v1 = '向下盘背' if bi3.power < bi1.power else '向下无背' - - # 识别向上形态 - elif bi3.direction == Direction.Up: - if bi3.high < bi1.low: - v1 = '向上不重合' - elif bi2.low < bi1.low < bi3.high < bi2.high: - v1 = '向上奔走型' - elif bi1.high > bi3.high and bi1.low < bi3.low: - v1 = '向上收敛' - elif bi1.high < bi3.high and bi1.low > bi3.low: - v1 = '向上扩张' - elif bi3.low > bi1.low and bi3.high > bi1.high: - v1 = '向上盘背' if bi3.power < bi1.power else '向上无背' - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': cxt_three_bi_V230618, 'freq': '日线', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/cxt_ubi_end_V230816.py b/examples/signals_dev/merged/cxt_ubi_end_V230816.py deleted file mode 100644 index 3039d3ccb..000000000 --- a/examples/signals_dev/merged/cxt_ubi_end_V230816.py +++ /dev/null @@ -1,85 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC, Direction -from collections import OrderedDict -from czsc.objects import Mark -from czsc.utils import create_single_signal, get_sub_elements - - -def cxt_ubi_end_V230816(c: CZSC, **kwargs) -> OrderedDict: - """当前是未完成笔的第几次新低或新高,用于笔结束辅助 - - 参数模板:"{freq}_UBI_BE辅助V230816" - - **信号逻辑:** - - 以向上未完成笔为例:取所有顶分型,计算创新高的底分型数量N,如果当前K线创新高,则新高次数为N+1 - - **信号列表:** - - - Signal('日线_UBI_BE辅助V230816_新低_第4次_任意_0') - - Signal('日线_UBI_BE辅助V230816_新低_第5次_任意_0') - - Signal('日线_UBI_BE辅助V230816_新低_第6次_任意_0') - - Signal('日线_UBI_BE辅助V230816_新高_第2次_任意_0') - - Signal('日线_UBI_BE辅助V230816_新高_第3次_任意_0') - - Signal('日线_UBI_BE辅助V230816_新高_第4次_任意_0') - - Signal('日线_UBI_BE辅助V230816_新高_第5次_任意_0') - - Signal('日线_UBI_BE辅助V230816_新高_第6次_任意_0') - - Signal('日线_UBI_BE辅助V230816_新高_第7次_任意_0') - - Signal('日线_UBI_BE辅助V230816_新低_第2次_任意_0') - - Signal('日线_UBI_BE辅助V230816_新低_第3次_任意_0') - - :param c: CZSC对象 - :param kwargs: - :return: 信号识别结果 - """ - freq = c.freq.value - k1, k2, k3 = f"{freq}_UBI_BE辅助V230816".split('_') - v1, v2 = '其他','其他' - ubi = c.ubi - if not ubi or len(ubi['fxs']) <= 2 or len(c.bars_ubi) <= 5: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - fxs = ubi['fxs'] - if ubi['direction'] == Direction.Up: - fxs = [x for x in fxs if x.mark == Mark.G] - cnt = 1 - cur_hfx = fxs[0] - for fx in fxs[1:]: - if fx.high > cur_hfx.high: - cnt += 1 - cur_hfx = fx - - if ubi['raw_bars'][-1].high > cur_hfx.high: - v1 = '新高' - v2 = f"第{cnt + 1}次" - - if ubi['direction'] == Direction.Down: - fxs = [x for x in fxs if x.mark == Mark.D] - cnt = 1 - cur_lfx = fxs[0] - for fx in fxs[1:]: - if fx.low < cur_lfx.low: - cnt += 1 - cur_lfx = fx - - if ubi['raw_bars'][-1].low < cur_lfx.low: - v1 = '新低' - v2 = f"第{cnt + 1}次" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': cxt_ubi_end_V230816, 'freq': '日线', 'di': 1, 'max_overlap': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/dema_up_dw_line_V230605.py b/examples/signals_dev/merged/dema_up_dw_line_V230605.py deleted file mode 100644 index b1c926e6c..000000000 --- a/examples/signals_dev/merged/dema_up_dw_line_V230605.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -# @Time : 2023/6/18 15:51 -# @Author : 琅盎 -# @FileName: DEMA.py -# @Software: PyCharm -from collections import OrderedDict -import numpy as np -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def dema_up_dw_line_V230605(c: CZSC, **kwargs) -> OrderedDict: - """DEMA短线趋势指标,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}_DEMA短线趋势V230605" - - **信号逻辑:** - - DEMA指标是一种趋势指标,用于衡量价格趋势的方向和强度。 - 与其他移动平均线指标相比,DEMA指标更加灵敏,能够更快地反应价格趋势的变化,因此在短期交易中具有一定的优势。 - 当收盘价大于DEMA看多, 当收盘价小于DEMA看空 - - **信号列表:** - - - Signal('日线_D1N5_DEMA短线趋势V230605_看多_任意_任意_0') - - Signal('日线_D1N5_DEMA短线趋势V230605_看空_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数,默认为5 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 5)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}_DEMA短线趋势V230605".split('_') - v1 = "其他" - if len(c.bars_raw) < di + 2*n + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - short_bars = get_sub_elements(c.bars_raw, di=di, n=n) - long_bars = get_sub_elements(c.bars_raw, di=di, n=n * 2) - dema = np.mean([x.close for x in short_bars]) * 2 - np.mean([x.close for x in long_bars]) - - v1 = "看多" if short_bars[-1].close > dema else "看空" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [ - {'name': dema_up_dw_line_V230605, 'freq': '日线', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/signals_dev/merged/demakder_up_dw_line_V230605.py b/examples/signals_dev/merged/demakder_up_dw_line_V230605.py deleted file mode 100644 index 520d2bdc4..000000000 --- a/examples/signals_dev/merged/demakder_up_dw_line_V230605.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# @Time : 2023/6/10 13:56 -# @Author : 琅盎 -# @FileName: ER.py -# @Software: PyCharm -from collections import OrderedDict - -import numpy as np -import pandas as pd -from loguru import logger -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def er_up_dw_line_V230604(c: CZSC, **kwargs) -> OrderedDict: - """ER价格动量指标,贡献者:琅盎 - - 参数模板:"{freq}_D{di}W{w}N{n}_ER价格动量V230604" - - **信号逻辑:** - - er 为动量指标。用来衡量市场的多空力量对比。在多头市场, - 人们会更贪婪地在接近高价的地方买入,BullPower 越高则当前 - 多头力量越强;而在空头市场,人们可能因为恐惧而在接近低价 - 的地方卖出。BearPower 越低则当前空头力量越强。当两者都大 - 于 0 时,反映当前多头力量占据主导地位;两者都小于 0 则反映 - 空头力量占据主导地位。 - 如果 BearPower 上穿 0,则产生买入信号; - 如果 BullPower 下穿 0,则产生卖出信号。 - - **信号列表:** - - - Signal('日线_D1W21N10_ER价格动量V230604_均线下方_第10层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线下方_第9层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线下方_第8层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线上方_第5层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线上方_第1层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线上方_第10层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线上方_第2层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线上方_第6层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线上方_第7层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线上方_第8层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线上方_第9层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线上方_第4层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线下方_第5层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线下方_第7层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线下方_第3层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线下方_第2层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线下方_第6层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线下方_第1层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线下方_第4层_任意_0') - - Signal('日线_D1W21N10_ER价格动量V230604_均线上方_第3层_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数,默认为105 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - w = int(kwargs.get("w", 60)) - n = int(kwargs.get("n", 10)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}W{w}N{n}_ER价格动量V230604".split('_') - - cache_key = f"ER{w}" - for i, bar in enumerate(c.bars_raw, 1): - if cache_key in bar.cache: - continue - _bars = c.bars_raw[i-w:i] - ma = np.mean([x.close for x in _bars]) - bull_power = bar.high - ma if bar.high > ma else bar.low - ma - bar.cache.update({cache_key: bull_power}) - - v1 = "其他" - if len(c.bars_raw) < di + w + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - _bars = get_sub_elements(c.bars_raw, di=di, n=w*10) - factors = [x.cache[cache_key] for x in _bars] - factors = [x for x in factors if x * factors[-1] > 0] - - v1 = "均线上方" if factors[-1] > 0 else "均线下方" - q = pd.cut(factors, n, labels=list(range(1, n+1)), precision=5, duplicates='drop')[-1] - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=f"第{q}层") - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20171101', '20210101', fq='前复权') - - signals_config = [ - {'name': er_up_dw_line_V230604, 'freq': '日线', 'di': 1, 'w': 21, 'n': 10}, - ] - check_signals_acc(bars, signals_config=signals_config) - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/signals_dev/merged/emv_up_dw_line_V230605.py b/examples/signals_dev/merged/emv_up_dw_line_V230605.py deleted file mode 100644 index d29068a71..000000000 --- a/examples/signals_dev/merged/emv_up_dw_line_V230605.py +++ /dev/null @@ -1,62 +0,0 @@ -from collections import OrderedDict -import numpy as np -import pandas as pd -from loguru import logger -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def emv_up_dw_line_V230605(c: CZSC, **kwargs) -> OrderedDict: - """能量异动,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}_V230605emv" - - **信号逻辑:** - - emv 综合考虑了成交量和价格(中间价)的变化。 - emv>0 则多头处于优势,emv 上升说明买方力量在增大; - emv<0 则空头处于优势,emv 下降说明卖方力量在增大。 - 如果 emv 上穿 0,则产生买入信号; - 如果 emv 下穿 0,则产生卖出信号。 - - **信号列表:** - - - Signal('30分钟_D1_EMV多空V230605_看空_任意_任意_0') - - Signal('30分钟_D1_EMV多空V230605_看多_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数,默认为105 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}_EMV多空V230605".split('_') - - if len(c.bars_raw) < di + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1="其他") - - _bars = get_sub_elements(c.bars_raw, di=di, n=2) - mid_pt_move = (_bars[-1].high + _bars[-1].low) / 2 - (_bars[-2].high + _bars[-2].low) / 2 - # box_ratio = _bars[-1].vol / 1000000 / (_bars[-1].high - _bars[-1].low) - # emv = mid_pt_move / box_ratio - - v1 = '看多' if mid_pt_move > 0 else '看空' - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [ - {'name': emv_up_dw_line_V230605, 'freq': '30分钟', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() diff --git a/examples/signals_dev/merged/holds_concepts_effect.py b/examples/signals_dev/merged/holds_concepts_effect.py deleted file mode 100644 index d4be8ba22..000000000 --- a/examples/signals_dev/merged/holds_concepts_effect.py +++ /dev/null @@ -1,87 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/3/19 22:03 -describe: -""" -import pandas as pd -from tqdm import tqdm -from collections import Counter -from czsc.data import TsDataCache - - -def get_symbol_concepts(): - dc = TsDataCache(data_path=r"D:\ts_data", sdt='2014-01-01') - ths_members = dc.get_all_ths_members(exchange="A", type_="N") - ths_members = ths_members[ths_members['code'].str.contains('[SH|SZ]$')] - - # 过滤掉一些成分股数量特别多的概念 - not_concepts = ths_members.groupby('概念名称')['code'].count().sort_values(ascending=False).head(20).to_dict() - - ths_members = ths_members[~ths_members['概念名称'].isin(not_concepts)] - ths_members = ths_members.groupby('code')['概念名称'].apply(list).reset_index() - sc = ths_members.set_index('code')['概念名称'].to_dict() - return sc - - -def holds_concepts_effect(holds: pd.DataFrame, concepts: dict, top_n=20, min_n=3, **kwargs): - """股票持仓列表的板块效应 - - 原理概述:在选股时,如果股票的概念板块与组合中的其他股票的概念板块有重合,那么这个股票的表现会更好。 - - :param holds: 组合股票池数据,样例: - 成分日期 证券代码 n1b 持仓权重 - 0 2020-01-02 000001.SZ 183.758194 0.001232 - 1 2020-01-02 000002.SZ -156.633896 0.001232 - 2 2020-01-02 000063.SZ 310.296204 0.001232 - 3 2020-01-02 000066.SZ -131.824997 0.001232 - 4 2020-01-02 000069.SZ -38.561699 0.001232 - :param concepts: 股票的概念板块,样例: - { - '002507.SZ': ['电子商务', '超级品牌', '国企改革'], - '002508.SZ': ['家用电器', '杭州亚运会', '恒大概念'] - } - :param top_n: 选取前 n 个密集概念 - :param min_n: 单股票至少要有 n 个概念在 top_n 中 - :return: 过滤后的选股结果,每个时间点的 top_n 概念 - """ - if kwargs.get('copy', True): - holds = holds.copy() - - holds['概念板块'] = holds['证券代码'].map(concepts).fillna('') - holds['概念数量'] = holds['概念板块'].apply(len) - holds = holds[holds['概念数量'] > 0] - - new_holds = [] - dt_key_concepts = {} - for dt, dfg in tqdm(holds.groupby('成分日期'), desc='计算板块效应'): - # 计算密集出现的概念 - key_concepts = [k for k, v in Counter([x for y in dfg['概念板块'] for x in y]).most_common(top_n)] - dt_key_concepts[dt] = key_concepts - - # 计算在密集概念中出现次数超过min_n的股票 - dfg['强势概念'] = dfg['概念板块'].apply(lambda x: ','.join(set(x) & set(key_concepts))) - sel = dfg[dfg['强势概念'].apply(lambda x: len(x.split(',')) >= min_n)] - new_holds.append(sel) - - dfh = pd.concat(new_holds, ignore_index=True) - dfk = pd.DataFrame([{"成分日期": k, '强势概念': v} for k, v in dt_key_concepts.items()]) - return dfh, dfk - - -def test_get_symbol_concepts(): - import matplotlib.pyplot as plt - concepts = get_symbol_concepts() - holds = pd.read_feather(r"D:\ts_data\holds_20180103_20230213.feather") - dfh, dfk = holds_concepts_effect(holds, concepts, top_n=30, min_n=3) - - old = holds.groupby('成分日期')['n1b'].mean().cumsum().to_frame().dropna() - new = dfh.groupby('成分日期')['n1b'].mean().cumsum().to_frame().dropna() - print(f"旧的组合收益:{old.iloc[-1, 0]:.2f},新的组合收益:{new.iloc[-1, 0]:.2f},增长:{new.iloc[-1, 0] / old.iloc[-1, 0] - 1:.2%}") - dfh.groupby('成分日期')['n1b'].mean().cumsum().plot() - plt.show() - holds.groupby('成分日期')['n1b'].mean().cumsum().plot() - plt.show() - - diff --git a/examples/signals_dev/merged/kcatr_up_dw_line_V230823.py b/examples/signals_dev/merged/kcatr_up_dw_line_V230823.py deleted file mode 100644 index 87f6b82d4..000000000 --- a/examples/signals_dev/merged/kcatr_up_dw_line_V230823.py +++ /dev/null @@ -1,74 +0,0 @@ -from collections import OrderedDict -import numpy as np -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal -from czsc.signals.tas import update_atr_cache - - -def kcatr_up_dw_line_V230823(c: CZSC, **kwargs) -> OrderedDict: - """用atr波幅构造上下轨,收盘价突破判断多空 贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}M{m}T{th}_KCATR多空V230823" - - **信号逻辑:** - - 与布林带类似,都是用价格的移动平均构造中轨,不同的是表示波幅 - 的方法,这里用 atr 来作为波幅构造上下轨。价格突破上轨, - 可看成新的上升趋势,买入;价格突破下轨, - - **信号列表:** - - - Signal('日线_D1N30M16T2_KCATR多空V230823_看多_任意_任意_0') - - Signal('日线_D1N30M16T2_KCATR多空V230823_看空_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数进行ATR计算,默认为30 - - :param m: 获取K线的根数进行均价计算,默认为16 - - :param th: 突破ATR的倍数,默认为2 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 30)) - m = int(kwargs.get("m", 16)) - th = int(kwargs.get("th", 2)) # 突破ATR的倍数 - - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}M{m}T{th}_KCATR多空V230823".split('_') - v1 = "其他" - if len(c.bars_raw) < di + max(m, n) + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - n_bars = get_sub_elements(c.bars_raw, di=di, n=n) - m_bars = get_sub_elements(c.bars_raw, di=di, n=m) - atr = np.mean([ - max( - abs(n_bars[i].high - n_bars[i].low), - abs(n_bars[i].high - n_bars[i - 1].close), - abs(n_bars[i - 1].close - n_bars[i - 1].low), - ) - for i in range(1, len(n_bars)) - ]) - middle = np.mean([x.close for x in m_bars]) - - if m_bars[-1].close > middle + atr * th: - v1 = "看多" - elif m_bars[-1].close < middle - atr * th: - v1 = "看空" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20171101', '20210101', fq='前复权') - - signals_config = [{'name': kcatr_up_dw_line_V230823, 'freq': '日线', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config) - - -if __name__ == '__main__': - main() diff --git a/examples/signals_dev/merged/lverage_up_dw_line_V230824.py b/examples/signals_dev/merged/lverage_up_dw_line_V230824.py deleted file mode 100644 index fda686745..000000000 --- a/examples/signals_dev/merged/lverage_up_dw_line_V230824.py +++ /dev/null @@ -1,83 +0,0 @@ -from collections import OrderedDict -import pandas as pd -import tushare as ts -import numpy as np -from tqdm import tqdm -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def lverage_up_dw_line_V230824(c: CZSC, **kwargs) -> OrderedDict: - """.....,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}_V230604dc" - - 信号逻辑:** - - - - 信号列表: - - - Signal('日线_D1N10_V230604dc_看空_任意_任意_0') - - Signal('日线_D1N10_V230604dc_看多_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - :param di: 信号计算截止倒数第i根K线 - - :param n: 获取K线的根数,默认为105 - - :param start_date: 获取tushare备用行情的开始日期--> 调用函数时需要和主数据开始日期保持一致 - - :param end_date: 获取tushare备用行情的结束日期--> 调用函数时需要和主数据结束日期保持一致 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 10)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}_V230604dc".split('_') - assert freq == "日线", "该信号只能在日线上使用" - - # 从外部获取数据进行缓存 - cache_key = "lverage_up_dw_line_V230824_volume_ratio" - if cache_key not in c.cache.keys(): - pro = ts.pro_api() - _df = pro.daily_basic(ts_code=c.symbol, start_date='20100101', end_date='20240101', fields='trade_date,volume_ratio') - c.cache[cache_key] = _df.set_index('trade_date')['volume_ratio'].to_dict() - map_volume_ratio = c.cache[cache_key] - - v1 = "其他" - if len(c.bars_raw) < di + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - _bars = get_sub_elements(c.bars_raw, di=di, n=n) # 取n根K线 - max_high = max([x.high for x in _bars]) - min_low = min([x.low for x in _bars]) - ratio = [map_volume_ratio.get(x.dt.strftime("%Y%m%d"), 1) for x in _bars] - max_ratio = max(ratio) - min_ratio = min(ratio) - - dc = (max_high + min_low) / 2 - ra = (max_ratio + min_ratio) / 2 # type: ignore - - if _bars[-1].close > dc and ratio[-1] < ra: - v1 = "看多" - if _bars[-1].close < dc and ratio[-1] > ra: - v1 = "看空" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[0] - # for symbol in symbols[:10]: - bars = research.get_raw_bars(symbol, '日线', '20101101', '20230101', fq='前复权') - signals_config = [{'name': lverage_up_dw_line_V230824, 'freq': '日线', 'di': 1, 'n': 10}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/ntmdk_V230824.py b/examples/signals_dev/merged/ntmdk_V230824.py deleted file mode 100644 index 09efd9d21..000000000 --- a/examples/signals_dev/merged/ntmdk_V230824.py +++ /dev/null @@ -1,56 +0,0 @@ -from collections import OrderedDict -import numpy as np -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def ntmdk_V230824(c: CZSC, **kwargs) -> OrderedDict: - """NTMDK多空指标,贡献者:琅盎 - - 参数模板:"freq}_D{di}M{m}_NTMDK多空V230824" - - **信号逻辑:** - - 此信号函数的逻辑非常简单,流传于股市中有一句话:日日新高日日持股, - 那么此信号函数利用的是收盘价和M日前的收盘价进行比较,如果差值为正 - 即多头成立,反之空头成立。 - - **信号列表:** - - - Signal('日线_D1M10_NTMDK多空V230824_看空_任意_任意_0') - - Signal('日线_D1M10_NTMDK多空V230824_看多_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param di: 信号计算截止倒数第i根K线 - - :param m: m天前的价格 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - m = int(kwargs.get("m", 10)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}M{m}_NTMDK多空V230824".split('_') - v1 = "其他" - if len(c.bars_raw) < di + m + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=m) - v1 = "看多" if bars[-1].close > bars[0].close else "看空" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20171101', '20210101', fq='前复权') - - signals_config = [ - {'name': ntmdk_V230824, 'freq': '日线', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/signals_dev/merged/obv_up_dw_line_V230719.py b/examples/signals_dev/merged/obv_up_dw_line_V230719.py deleted file mode 100644 index 2e4a7597a..000000000 --- a/examples/signals_dev/merged/obv_up_dw_line_V230719.py +++ /dev/null @@ -1,95 +0,0 @@ -from collections import OrderedDict -import numpy as np -import pandas as pd -import talib as ta -from loguru import logger -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def obv_up_dw_line_V230719(c: CZSC, **kwargs) -> OrderedDict: - """OBV能量指标,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}M{m}MO{max_overlap}_OBV能量V230719" - - **信号逻辑:** - - OBV 指标把成交量分为正的成交量(价格上升时的成交量)和负的成交量(价格下降时)的成交量。 - OBV 就是分了正负之后的成交量的累计和。 - - 1. 先定义OBVM,OBVM是 OBV 7天的指数平均。 - 2. 再定义一条信号线Signal line,这条线是OBVM 10天的指数平均。 - - 其中的「7天」和「10天」都是参数,根据你的交易时间级别设置,设置的越小,OBVM对成交量的变化越敏感, - 产生的交易信号也就越多。 - - 开多仓的规则:当OBVM上穿Signal line,开多仓;当OBVM下穿Signal line,平仓。这个规则只捕捉上涨趋势。 - - **信号列表:** - - - Signal('日线_D1N7M10MO3_OBV能量V230719_看多_任意_任意_0') - - Signal('日线_D1N7M10MO3_OBV能量V230719_看空_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param di: 信号计算截止倒数第i根K线 - - :param n: short窗口大小。 - - :param m: long窗口大小。 - - :param max_overlap: 信号计算时,最大重叠K线数量。 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 7)) - m = int(kwargs.get("m", 10)) - max_overlap = int(kwargs.get("max_overlap", 3)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}M{m}MO{max_overlap}_OBV能量V230719".split('_') - v1 = "其他" - - # 计算OBV,缓存到bar.cache中 - cache_key = "OBV" - for i in range(1, len(c.bars_raw)): - bar1, bar2 = c.bars_raw[i - 1], c.bars_raw[i] - if cache_key not in bar1.cache: - last_obv = bar1.vol if bar1.close > bar1.open else -bar1.vol - bar1.cache[cache_key] = last_obv - else: - last_obv = bar1.cache[cache_key] - - if cache_key not in bar2.cache: - cur_obv = bar2.vol if bar2.close > bar2.open else -bar2.vol - bar2.cache[cache_key] = last_obv + cur_obv - - min_k_num = di + max(n, m) + max_overlap + 10 - if len(c.bars_raw) < min_k_num: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=min_k_num) - obv_seq = np.array([x.cache[cache_key] for x in bars], dtype=np.float64) - obvm = ta.EMA(obv_seq, n) - sig_ = ta.EMA(obvm, m) - - if obvm[-1] > sig_[-1] and obvm[-max_overlap] < sig_[-max_overlap]: - v1 = "看多" - elif obvm[-1] < sig_[-1] and obvm[-max_overlap] > sig_[-max_overlap]: - v1 = "看空" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20191101', '20210101', fq='前复权') - - signals_config = [ - {'name': obv_up_dw_line_V230719, 'freq': '日线', 'di': 1}, - # {'name': 'czsc.signals.tas_ma_base_V221101', 'freq': '日线', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/signals_dev/merged/obvm_line_V230610.py b/examples/signals_dev/merged/obvm_line_V230610.py deleted file mode 100644 index 695635c6d..000000000 --- a/examples/signals_dev/merged/obvm_line_V230610.py +++ /dev/null @@ -1,86 +0,0 @@ -from collections import OrderedDict -import numpy as np -import pandas as pd -import talib as ta -from loguru import logger -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def obvm_line_V230610(c: CZSC, **kwargs) -> OrderedDict: - """OBV能量指标,贡献者:琅盎 - - 参数模板:"{freq}_D{di}N{n}M{m}_OBV能量V230610" - - **信号逻辑:** - - OBV 指标把成交量分为正的成交量(价格上升时的成交量)和负的 - 成交量(价格下降时)的成交量。OBV 就是分了正负之后的成交量 - 的累计和。 - - 首先,根据传入的参数 di、n 和 m,从 CZSC 对象中获取对应的 K 线数据,然后计算 OBV 序列。 - 接着,使用 talib 库中的 EMA 函数计算 OBV 序列的短期和长期指数移动平均线, - 最后根据两条移动平均线的大小关系判断看多或看空信号。 - - **信号列表:** - - - Signal('日线_D1N10M30_OBV能量V230610_看空_任意_任意_0') - - Signal('日线_D1N10M30_OBV能量V230610_看多_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param di: 信号计算截止倒数第i根K线 - - :param n: short窗口大小。 - - :param m: long窗口大小。 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - n = int(kwargs.get("n", 10)) - m = int(kwargs.get("m", 30)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}M{m}_OBV能量V230610".split('_') - v1 = "其他" - - # 计算OBV,缓存到bar.cache中 - cache_key = "OBV" - for i in range(1, len(c.bars_raw)): - bar1, bar2 = c.bars_raw[i - 1], c.bars_raw[i] - if cache_key not in bar1.cache: - last_obv = bar1.vol if bar1.close > bar1.open else -bar1.vol - bar1.cache[cache_key] = last_obv - else: - last_obv = bar1.cache[cache_key] - - if cache_key not in bar2.cache: - cur_obv = bar2.vol if bar2.close > bar2.open else -bar2.vol - bar2.cache[cache_key] = last_obv + cur_obv - - if len(c.bars_raw) < di + max(n, m) + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=max(n, m) + 10) - obv_seq = np.array([x.cache[cache_key] for x in bars], dtype=np.float64) - - ema_n1 = ta.EMA(obv_seq, n)[-1] - ema_n2 =ta.EMA(obv_seq, m)[-1] - - v1 = "看多" if ema_n1 > ema_n2 else "看空" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20191101', '20210101', fq='前复权') - - signals_config = [ - {'name': obvm_line_V230610, 'freq': '日线', 'di': 1}, - # {'name': 'czsc.signals.tas_ma_base_V221101', 'freq': '日线', 'di': 1}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/signals_dev/merged/pos_fix_exit_V230624.py b/examples/signals_dev/merged/pos_fix_exit_V230624.py deleted file mode 100644 index a3159560e..000000000 --- a/examples/signals_dev/merged/pos_fix_exit_V230624.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/14 17:48 -describe: -""" -import os -import sys - -sys.path.insert(0, r'D:\ZB\git_repo\waditu\czsc\examples\signals_dev') -os.environ['czsc_verbose'] = '1' -import pandas as pd -from typing import List -from loguru import logger -from czsc import CzscStrategyBase, Position -from czsc.connectors import research -from czsc.analyze import CZSC -from collections import OrderedDict -from czsc.traders.base import CzscTrader -from czsc.utils import create_single_signal -from czsc.objects import Operate, Direction, Mark -from czsc.signals.tas import update_ma_cache - -logger.enable('czsc.analyze') - -pd.set_option('expand_frame_repr', False) -pd.set_option('display.max_rows', 1000) -pd.set_option('display.max_columns', 1000) -pd.set_option('display.width', 1000) - - -def pos_fix_exit_V230624(cat: CzscTrader, **kwargs) -> OrderedDict: - """固定比例止损,止盈 - - 参数模板:"{pos_name}_固定{th}BP止盈止损_出场V230624" - - **信号逻辑:** - - 以多头为例,如果持有收益超过 th 个BP,则止盈;如果亏损超过 th 个BP,则止损。 - - **信号列表:** - - - Signal('日线三买多头_固定100BP止盈止损_出场V230624_多头止损_任意_任意_0') - - Signal('日线三买多头_固定100BP止盈止损_出场V230624_空头止损_任意_任意_0') - - :param cat: CzscTrader对象 - :param kwargs: 参数字典 - - pos_name: str,开仓信号的名称 - - freq1: str,给定的K线周期 - - n: int,向前找的K线个数,默认为 3 - :return: - """ - pos_name = kwargs["pos_name"] - th = int(kwargs.get('th', 300)) - k1, k2, k3 = f"{pos_name}_固定{th}BP止盈止损_出场V230624".split("_") - v1 = '其他' - if not hasattr(cat, "positions"): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - pos_ = [x for x in cat.positions if x.name == pos_name][0] - if len(pos_.operates) == 0 or pos_.operates[-1]['op'] in [Operate.SE, Operate.LE]: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - op = pos_.operates[-1] - op_price = op['price'] - - if op['op'] == Operate.LO: - if cat.latest_price < op_price * (1 - th / 10000): - v1 = '多头止损' - if cat.latest_price > op_price * (1 + th / 10000): - v1 = '多头止盈' - - if op['op'] == Operate.SO: - if cat.latest_price > op_price * (1 + th / 10000): - v1 = '空头止损' - if cat.latest_price < op_price * (1 - th / 10000): - v1 = '空头止盈' - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -class MyStrategy(CzscStrategyBase): - def create_pos(self, freq='60分钟', freq1='15分钟'): - _pos_name = f'{freq}通道突破' - _pos = { - 'symbol': self.symbol, - 'name': _pos_name, - 'opens': [ - { - 'operate': '开多', - 'factors': [ - {'name': f'{freq}看多', 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看多_任意_任意_0']} - ], - }, - { - 'operate': '开空', - 'factors': [ - {'name': f'{freq}看空', 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看空_任意_任意_0']} - ], - }, - ], - 'exits': [ - { - 'operate': '平多', - 'factors': [ - {'name': '止损出场V230624', 'signals_all': [f'{_pos_name}_固定{200}BP止盈止损_出场V230624_空头止损_任意_任意_0']}, - {'name': '止损出场V230624', 'signals_all': [f'{_pos_name}_固定{200}BP止盈止损_出场V230624_空头止盈_任意_任意_0']}, - ], - }, - { - 'operate': '平空', - 'factors': [ - {'name': '止损出场V230624', 'signals_all': [f'{_pos_name}_固定{200}BP止盈止损_出场V230624_空头止损_任意_任意_0']}, - {'name': '止损出场V230624', 'signals_all': [f'{_pos_name}_固定{200}BP止盈止损_出场V230624_空头止盈_任意_任意_0']}, - ], - }, - ], - 'interval': 7200, - 'timeout': 100, - 'stop_loss': 500, - 'T0': True, - } - - return Position.load(_pos) - - @property - def positions(self) -> List[Position]: - _pos_list = [self.create_pos(freq='日线', freq1='60分钟')] - return _pos_list - - -def check(): - from czsc.connectors import research - - symbols = research.get_symbols('A股主要指数') - tactic = MyStrategy(symbol=symbols[0], signals_module_name='czsc.signals') - bars = research.get_raw_bars(symbols[0], tactic.base_freq, '20151101', '20210101', fq='前复权') - - tactic.check(bars, res_path=r'C:\Users\zengb\.czsc\策略信号验证', refresh=True) - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/pos_fx_stop_V230414.py b/examples/signals_dev/merged/pos_fx_stop_V230414.py deleted file mode 100644 index 8bac38f82..000000000 --- a/examples/signals_dev/merged/pos_fx_stop_V230414.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/14 17:48 -describe: -""" -import os -import sys -sys.path.insert(0, r'D:\ZB\git_repo\waditu\czsc\examples\signals_dev') -os.environ['czsc_verbose'] = '1' -import pandas as pd -from typing import List -from loguru import logger -from czsc import CzscStrategyBase, Position -from czsc.connectors import research -logger.enable('czsc.analyze') - -pd.set_option('expand_frame_repr', False) -pd.set_option('display.max_rows', 1000) -pd.set_option('display.max_columns', 1000) -pd.set_option('display.width', 1000) - - -class MyStrategy(CzscStrategyBase): - - def create_pos(self, freq='60分钟', freq1='15分钟'): - _pos_name = f'{freq}通道突破' - _pos = {'symbol': self.symbol, - 'name': _pos_name, - 'opens': [{'operate': '开多', - 'factors': [ - {'name': f'{freq}看多', - 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看多_任意_任意_0']}, - ]}, - - {'operate': '开空', - 'factors': [ - {'name': f'{freq}看空', - 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看空_任意_任意_0']}, - ]} - ], - 'exits': [ - {'operate': '平多', - 'factors': [ - {'name': f'{freq1}_{_pos_name}_止损V230414', - 'signals_all': [f'{freq1}_{_pos_name}N1_止损V230414_多头止损_任意_任意_0']}, - ]}, - - {'operate': '平空', - 'factors': [ - {'name': f'{freq1}_{_pos_name}_止损V230414', - 'signals_all': [f'{freq1}_{_pos_name}N1_止损V230414_空头止损_任意_任意_0']}, - ]}, - ], - 'interval': 7200, - 'timeout': 100, - 'stop_loss': 500, - 'T0': True} - - return Position.load(_pos) - - @property - def positions(self) -> List[Position]: - _pos_list = [self.create_pos(freq='日线', freq1='60分钟')] - return _pos_list - - -def check(): - from czsc.connectors import research - - symbols = research.get_symbols('A股主要指数') - tactic = MyStrategy(symbol=symbols[0], signals_module_name='pos_signals') - bars = research.get_raw_bars(symbols[0], tactic.base_freq, '20151101', '20210101', fq='前复权') - - tactic.check(bars, res_path=r'C:\Users\zengb\.czsc\策略信号验证', refresh=True) - - -if __name__ == '__main__': - check() - - - diff --git a/examples/signals_dev/merged/pos_holds_V230414.py b/examples/signals_dev/merged/pos_holds_V230414.py deleted file mode 100644 index 8e52793f1..000000000 --- a/examples/signals_dev/merged/pos_holds_V230414.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/14 17:48 -describe: -""" -import os -import sys -sys.path.insert(0, r'D:\ZB\git_repo\waditu\czsc\examples\signals_dev') -os.environ['czsc_verbose'] = '1' -import pandas as pd -from typing import List -from loguru import logger -from czsc import CzscStrategyBase, Position -from czsc.connectors import research -logger.enable('czsc.analyze') - -pd.set_option('expand_frame_repr', False) -pd.set_option('display.max_rows', 1000) -pd.set_option('display.max_columns', 1000) -pd.set_option('display.width', 1000) - - -class MyStrategy(CzscStrategyBase): - - def create_pos(self, freq='60分钟', freq1='15分钟'): - _pos_name = f'{freq}通道突破' - _pos = {'symbol': self.symbol, - 'name': _pos_name, - 'opens': [{'operate': '开多', - 'factors': [ - {'name': f'{freq}看多', - 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看多_任意_任意_0']}, - ]}, - - {'operate': '开空', - 'factors': [ - {'name': f'{freq}看空', - 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看空_任意_任意_0']}, - ]} - ], - 'exits': [ - {'operate': '平多', - 'factors': [ - {'name': f'{freq1}_{_pos_name}_多头存疑', - 'signals_all': [f'{_pos_name}_{freq1}N5M100_趋势判断V230414_多头存疑_任意_任意_0']}, - ]}, - - {'operate': '平空', - 'factors': [ - {'name': f'{freq1}_{_pos_name}_空头存疑', - 'signals_all': [f'{_pos_name}_{freq1}N5M100_趋势判断V230414_空头存疑_任意_任意_0']}, - ]}, - ], - 'interval': 7200, - 'timeout': 100, - 'stop_loss': 500, - 'T0': True} - - return Position.load(_pos) - - @property - def positions(self) -> List[Position]: - _pos_list = [self.create_pos(freq='日线', freq1='60分钟')] - return _pos_list - - -def check(): - from czsc.connectors import research - - symbols = research.get_symbols('A股主要指数') - tactic = MyStrategy(symbol=symbols[0], signals_module_name='czsc.signals') - bars = research.get_raw_bars(symbols[0], tactic.base_freq, '20151101', '20210101', fq='前复权') - - tactic.check(bars, res_path=r'C:\Users\zengb\.czsc\策略信号验证', refresh=True) - - -if __name__ == '__main__': - check() - - - diff --git a/examples/signals_dev/merged/pos_holds_V230807.py b/examples/signals_dev/merged/pos_holds_V230807.py deleted file mode 100644 index 655df81a8..000000000 --- a/examples/signals_dev/merged/pos_holds_V230807.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/14 17:48 -describe: -""" -import os -import sys -sys.path.insert(0, r'D:\ZB\git_repo\waditu\czsc') -os.environ['czsc_verbose'] = '1' -import pandas as pd -from typing import List -from loguru import logger -from czsc import CzscStrategyBase, Position -from czsc.connectors import research -logger.enable('czsc.analyze') - -pd.set_option('expand_frame_repr', False) -pd.set_option('display.max_rows', 1000) -pd.set_option('display.max_columns', 1000) -pd.set_option('display.width', 1000) - - -class MyStrategy(CzscStrategyBase): - - def create_pos(self, freq='60分钟', freq1='15分钟'): - _pos_name = f'{freq}通道突破' - _pos = {'symbol': self.symbol, - 'name': _pos_name, - 'opens': [{'operate': '开多', - 'factors': [ - {'name': f'{freq}看多', - 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看多_任意_任意_0', - f'{_pos_name}_持仓状态_BS辅助V230808_持币_任意_任意_0']}, - ]}, - - {'operate': '开空', - 'factors': [ - {'name': f'{freq}看空', - 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看空_任意_任意_0', - f'{_pos_name}_持仓状态_BS辅助V230808_持币_任意_任意_0']}, - ]} - ], - 'exits': [ - {'operate': '平多', - 'factors': [ - {'name': f'{freq1}_{_pos_name}_止损V230414', - 'signals_all': [f'{_pos_name}_60分钟N5M50T10_BS辅助V230807_多头保本_任意_任意_0']}, - ]}, - - {'operate': '平空', - 'factors': [ - {'name': f'{freq1}_{_pos_name}_止损V230414', - 'signals_all': [f'{_pos_name}_60分钟N5M50T10_BS辅助V230807_空头保本_任意_任意_0']}, - ]}, - ], - 'interval': 7200, - 'timeout': 100, - 'stop_loss': 500, - 'T0': True} - - return Position.load(_pos) - - @property - def positions(self) -> List[Position]: - _pos_list = [self.create_pos(freq='日线', freq1='60分钟')] - return _pos_list - - -def check(): - from czsc.connectors import research - - symbols = research.get_symbols('A股主要指数') - tactic = MyStrategy(symbol=symbols[0], signals_module_name='czsc.signals') - bars = research.get_raw_bars(symbols[0], tactic.base_freq, '20151101', '20210101', fq='前复权') - - tactic.check(bars, res_path=r'C:\Users\zengb\.czsc\策略信号验证', refresh=True, sdt='20170101') - - -if __name__ == '__main__': - check() - - - diff --git a/examples/signals_dev/merged/pos_profit_loss_V230624.py b/examples/signals_dev/merged/pos_profit_loss_V230624.py deleted file mode 100644 index fc02ca788..000000000 --- a/examples/signals_dev/merged/pos_profit_loss_V230624.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/14 17:48 -describe: -""" -import os -import sys - -sys.path.insert(0, '.') -sys.path.insert(0, '..') -sys.path.insert(0, '../..') -sys.path.insert(0, '../../..') -os.environ['czsc_verbose'] = '1' -import pandas as pd -from typing import List -from loguru import logger -from czsc import CzscStrategyBase, Position -from czsc.connectors import research - -logger.enable('czsc.analyze') - -pd.set_option('expand_frame_repr', False) -pd.set_option('display.max_rows', 1000) -pd.set_option('display.max_columns', 1000) -pd.set_option('display.width', 1000) - - -class MyStrategy(CzscStrategyBase): - def create_pos(self, freq='60分钟', freq1='15分钟'): - _pos_name = f'{freq}通道突破' - _pos = { - 'symbol': self.symbol, - 'name': _pos_name, - 'opens': [ - { - 'operate': '开多', - 'factors': [ - {'name': f'{freq}看多', 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看多_任意_任意_0']} - ], - }, - { - 'operate': '开空', - 'factors': [ - {'name': f'{freq}看空', 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看空_任意_任意_0']} - ], - }, - ], - 'exits': [ - { - 'operate': '平多', - 'factors': [ - {'name': '止盈', 'signals_all': [f'{_pos_name}_{freq1}YKB20N3_盈亏比判断V230624_多头达标_任意_任意_0']}, - {'name': '止损', 'signals_all': [f'{_pos_name}_{freq1}YKB20N3_盈亏比判断V230624_多头止损_任意_任意_0']}, - ], - }, - { - 'operate': '平空', - 'factors': [ - {'name': '止盈', 'signals_all': [f'{_pos_name}_{freq1}YKB20N3_盈亏比判断V230624_空头达标_任意_任意_0']}, - {'name': '止损', 'signals_all': [f'{_pos_name}_{freq1}YKB20N3_盈亏比判断V230624_空头止损_任意_任意_0']}, - ], - }, - ], - 'interval': 7200, - 'timeout': 100, - 'stop_loss': 500, - 'T0': True, - } - - return Position.load(_pos) - - @property - def positions(self) -> List[Position]: - _pos_list = [self.create_pos(freq='日线', freq1='60分钟')] - return _pos_list - - -def check(): - from czsc.connectors import research - - symbols = research.get_symbols('A股主要指数') - tactic = MyStrategy(symbol=symbols[0], signals_module_name='czsc.signals') - bars = research.get_raw_bars(symbols[0], tactic.base_freq, '20151101', '20210101', fq='前复权') - - tactic.check(bars, res_path=r'C:\Users\zengb\.czsc\策略信号验证', refresh=True) - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/pos_status_V230808.py b/examples/signals_dev/merged/pos_status_V230808.py deleted file mode 100644 index 743c0dc2e..000000000 --- a/examples/signals_dev/merged/pos_status_V230808.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/4/14 17:48 -describe: -""" -import os -import sys -sys.path.insert(0, r'D:\ZB\git_repo\waditu\czsc') -os.environ['czsc_verbose'] = '1' -import pandas as pd -from typing import List -from loguru import logger -from czsc import CzscStrategyBase, Position -from czsc.connectors import research -logger.enable('czsc.analyze') - -pd.set_option('expand_frame_repr', False) -pd.set_option('display.max_rows', 1000) -pd.set_option('display.max_columns', 1000) -pd.set_option('display.width', 1000) - - -class MyStrategy(CzscStrategyBase): - - def create_pos(self, freq='60分钟', freq1='15分钟'): - _pos_name = f'{freq}通道突破' - _pos = {'symbol': self.symbol, - 'name': _pos_name, - 'opens': [{'operate': '开多', - 'factors': [ - {'name': f'{freq}看多', - 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看多_任意_任意_0', - f'{_pos_name}_持仓状态_BS辅助V230808_持币_任意_任意_0']}, - ]}, - - {'operate': '开空', - 'factors': [ - {'name': f'{freq}看空', - 'signals_all': [f'{freq}_D1通道突破#5#30#30_BS辅助V230403_看空_任意_任意_0', - f'{_pos_name}_持仓状态_BS辅助V230808_持币_任意_任意_0']}, - ]} - ], - 'exits': [ - {'operate': '平多', - 'factors': [ - {'name': f'{freq1}_{_pos_name}_止损V230414', - 'signals_all': [f'{freq1}_{_pos_name}N1_止损V230414_多头止损_任意_任意_0']}, - ]}, - - {'operate': '平空', - 'factors': [ - {'name': f'{freq1}_{_pos_name}_止损V230414', - 'signals_all': [f'{freq1}_{_pos_name}N1_止损V230414_空头止损_任意_任意_0']}, - ]}, - ], - 'interval': 7200, - 'timeout': 100, - 'stop_loss': 500, - 'T0': True} - - return Position.load(_pos) - - @property - def positions(self) -> List[Position]: - _pos_list = [self.create_pos(freq='日线', freq1='60分钟')] - return _pos_list - - -def check(): - from czsc.connectors import research - - symbols = research.get_symbols('A股主要指数') - tactic = MyStrategy(symbol=symbols[0], signals_module_name='czsc.signals') - bars = research.get_raw_bars(symbols[0], tactic.base_freq, '20151101', '20210101', fq='前复权') - - tactic.check(bars, res_path=r'C:\Users\zengb\.czsc\策略信号验证', refresh=True) - - -if __name__ == '__main__': - check() - - - diff --git a/examples/signals_dev/merged/tas_angle_V230802.py b/examples/signals_dev/merged/tas_angle_V230802.py deleted file mode 100644 index 942a95884..000000000 --- a/examples/signals_dev/merged/tas_angle_V230802.py +++ /dev/null @@ -1,77 +0,0 @@ -from loguru import logger - -try: - import talib as ta -except: - logger.warning( - f"ta-lib 没有正确安装,相关信号函数无法正常执行。" f"请参考安装教程 https://blog.csdn.net/qaz2134560/article/details/98484091") -import math -import pandas as pd -import numpy as np -from collections import OrderedDict -from deprecated import deprecated -from czsc.analyze import CZSC -from czsc.objects import Signal, Direction, BI, RawBar, FX -from czsc.signals.tas import update_ma_cache -from czsc.utils import get_sub_elements, fast_slow_cross, count_last_same, create_single_signal -from czsc.utils.sig import cross_zero_axis, cal_cross_num, down_cross_count -from typing import Union, List - - -def tas_angle_V230802(c: CZSC, **kwargs) -> OrderedDict: - """笔的角度比较 贡献者:谌意勇 - - 参数模板:"{freq}_D{di}N{n}T{th}_笔角度V230802" - - **信号逻辑:** - - 笔的角度,走过的笔的空间最高价和最低价的空间与走过的时间(原始K的数量)形成比值。 - 如果当前笔的角度小于前面9笔的平均角度的50%,当前笔向上认为是空头笔,否则是多头笔。 - - **信号列表:** - - - Signal('60分钟_D1N9T50_笔角度V230802_空头_任意_任意_0') - - Signal('60分钟_D1N9T50_笔角度V230802_多头_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: - - -n:统计笔的数量 - -di:取第几笔 - - :return: 信号识别结果 - """ - di = int(kwargs.get('di', 1)) - n = int(kwargs.get('n', 9)) - th = int(kwargs.get('th', 50)) - assert 300 > th > 30, "th 取值范围为 30 ~ 300" - - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}T{th}_笔角度V230802".split('_') - v1 = '其他' - if len(c.bi_list) < di + 2 * n + 2 or len(c.bars_ubi) >= 7: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bis = get_sub_elements(c.bi_list, di=di, n=n*2+1) - b1 = bis[-1] - b1_angle = b1.power_price / b1.length - same_dir_ang = [bi.power_price / bi.length for bi in bis[:-1] if bi.direction == b1.direction][-n:] - - if b1_angle < np.mean(same_dir_ang) * th / 100: - v1 = '空头' if b1.direction == Direction.Up else '多头' - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': tas_angle_V230802, 'freq': "60分钟", 'di': 1,'n': 9}] - check_signals_acc(bars, signals_config=signals_config, height='780px', delta_days=0) # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/tas_atr_V230630.py b/examples/signals_dev/merged/tas_atr_V230630.py deleted file mode 100644 index 407b52cc3..000000000 --- a/examples/signals_dev/merged/tas_atr_V230630.py +++ /dev/null @@ -1,76 +0,0 @@ -import sys - -sys.path.insert(0, '.') -sys.path.insert(0, '..') -sys.path.insert(0, '../..') -sys.path.insert(0, '../../..') -import math -import pandas as pd -from collections import OrderedDict -from czsc import CZSC -from loguru import logger -from czsc.signals.tas import update_atr_cache -from czsc.utils import create_single_signal, get_sub_elements - - -# 定义信号函数 -# ---------------------------------------------------------------------------------------------------------------------- - -def tas_atr_V230630(c: CZSC, **kwargs) -> OrderedDict: - """ATR波动强弱 - - 参数模板:"{freq}_D{di}ATR{timeperiod}_波动V230630" - - **信号逻辑:** - - ATR与收盘价的比值衡量了价格振幅比率的大小,对这个值进行分层。 - - **信号列表:** - - - Signal('日线_D1ATR14_波动V230630_第7层_任意_任意_0') - - Signal('日线_D1ATR14_波动V230630_第6层_任意_任意_0') - - Signal('日线_D1ATR14_波动V230630_第8层_任意_任意_0') - - Signal('日线_D1ATR14_波动V230630_第9层_任意_任意_0') - - Signal('日线_D1ATR14_波动V230630_第10层_任意_任意_0') - - Signal('日线_D1ATR14_波动V230630_第5层_任意_任意_0') - - Signal('日线_D1ATR14_波动V230630_第4层_任意_任意_0') - - Signal('日线_D1ATR14_波动V230630_第3层_任意_任意_0') - - Signal('日线_D1ATR14_波动V230630_第2层_任意_任意_0') - - Signal('日线_D1ATR14_波动V230630_第1层_任意_任意_0') - - :param c: czsc对象 - :param kwargs: - - - di: 倒数第i根K线 - - timeperiod: ATR指标的参数 - - :return: 信号字典 - """ - di = int(kwargs.get('di', 1)) - timeperiod = int(kwargs.get('timeperiod', 14)) - freq = c.freq.value - cache_key = update_atr_cache(c, timeperiod=timeperiod) - k1, k2, k3 = f"{freq}_D{di}ATR{timeperiod}_波动V230630".split('_') - v1 = "其他" - if len(c.bars_raw) < di + timeperiod + 8: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=100) - lev = [bar.cache[cache_key] / bar.close for bar in bars] - lev = pd.qcut(lev, 10, labels=False, duplicates='drop')[-1] - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=f"第{int(lev+1)}层") - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[10] - bars = research.get_raw_bars(symbol, '15分钟', '20151101', '20210101', fq='前复权') - signals_config = [{'name': tas_atr_V230630, 'freq': '日线', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px', sdt='20200101') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/tas_atr_stop_V230424.py b/examples/signals_dev/merged/tas_atr_stop_V230424.py deleted file mode 100644 index c5e4fb1d0..000000000 --- a/examples/signals_dev/merged/tas_atr_stop_V230424.py +++ /dev/null @@ -1,113 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC -from czsc.utils import create_single_signal, get_sub_elements - - -def update_atr_cache(c: CZSC, **kwargs): - """更新ATR缓存 - - 平均真实波幅(ATR)的计算方法: - - 1、当前交易日的最高价与最低价间的波幅 - 2、前一交易日收盘价与当个交易日最高价间的波幅 - 3、前一交易日收盘价与当个交易日最低价间的波幅 - - 今日振幅、今日最高与昨收差价,今日最低与昨收差价中的最大值,为真实波幅,在有了真实波幅后,就可以利用一段时间的平均值计算ATR了。 - - :param c: CZSC对象 - :return: - """ - timeperiod = int(kwargs.get('timeperiod', 14)) - cache_key = f"ATR{timeperiod}" - if c.bars_raw[-1].cache and c.bars_raw[-1].cache.get(cache_key, None): - # 如果最后一根K线已经有对应的缓存,不执行更新 - return cache_key - - last_cache = dict(c.bars_raw[-2].cache) if c.bars_raw[-2].cache else dict() - if cache_key not in last_cache.keys() or len(c.bars_raw) < timeperiod + 15: - # 初始化缓存 - bars = c.bars_raw - else: - # 增量更新最近5个K线缓存 - bars = c.bars_raw[-timeperiod - 10:] - - high = np.array([x.high for x in bars]) - low = np.array([x.low for x in bars]) - close = np.array([x.close for x in bars]) - atr = ta.ATR(high, low, close, timeperiod=timeperiod) - - for i in range(len(bars)): - _c = dict(bars[i].cache) if bars[i].cache else dict() - if cache_key not in _c.keys(): - _c.update({cache_key: atr[i] if atr[i] else 0}) - bars[i].cache = _c - - return cache_key - - -def bar_atr_break_V230424(c: CZSC, **kwargs): - """ATR突破 - - 参数模板:"{freq}_D{di}通道突破#{N}#{K1}#{K2}_BS辅助V230403" - - **信号逻辑:** - - 1. 以ATR为基础的通道突破; - 2. close 向上突破 LL + th * ATR, 看多; - 3. close 向下突破 HH - th * ATR,看空 - - **信号列表:** - - - Signal('日线_D1ATR5T30突破_BS辅助V230424_看空_任意_任意_0') - - Signal('日线_D1ATR5T30突破_BS辅助V230424_看多_任意_任意_0') - - :param c: 基础周期的 CZSC 对象 - :param kwargs: 其他参数 - - di: 倒数第 di 根 K 线 - - timeperiod: ATR的计算周期 - - th: ATR突破的倍数,根据经验优化 - :return: 信号字典 - """ - di = int(kwargs.get('di', 1)) - th = int(kwargs.get('th', 30)) - timeperiod = int(kwargs.get('timeperiod', 5)) - cache_key = update_atr_cache(c, timeperiod=timeperiod) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}ATR{timeperiod}T{th}突破_BS辅助V230424".split('_') - if len(c.bars_raw) < 3: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='其他') - - bars = get_sub_elements(c.bars_raw, di=di, n=timeperiod) - HH = max([i.high for i in bars]) - LL = min([i.low for i in bars]) - bar = c.bars_raw[-di] - atr = c.bars_raw[-di].cache[cache_key] - - th = th / 10 - if HH - th * atr > bar.close > LL + th * atr: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='其他') - - if bar.close > LL + th * atr: - v1 = '看多' - elif bar.close < HH - th * atr: - v1 = '看空' - else: - v1 = '其他' - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': bar_atr_break_V230424, 'freq': '日线', 'di': 1, 'timeperiod': 5, 'th': 30}] - check_signals_acc(bars, signals_config=signals_config, height='780px') - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/tas_cross_status_V230619.py b/examples/signals_dev/merged/tas_cross_status_V230619.py deleted file mode 100644 index ce446fd26..000000000 --- a/examples/signals_dev/merged/tas_cross_status_V230619.py +++ /dev/null @@ -1,103 +0,0 @@ -import sys -sys.path.insert(0, '.') -sys.path.insert(0, '..') -sys.path.insert(0, '../..') -sys.path.insert(0, '../../..') -import pandas as pd -import numpy as np -from collections import OrderedDict -from czsc import CZSC -from czsc.signals.tas import update_macd_cache, update_ma_cache -from czsc.utils import get_sub_elements, create_single_signal, fast_slow_cross -from czsc.utils.sig import cross_zero_axis, cal_cross_num, down_cross_count -from czsc.objects import Direction -from typing import List, Union - - -# 定义信号函数 -# ---------------------------------------------------------------------------------------------------------------------- - -def tas_cross_status_V230619(c: CZSC, **kwargs) -> OrderedDict: - """0轴上下金死叉次数计算信号函数 贡献者:谌意勇 - - 参数模板:"{freq}_D{di}MACD{fastperiod}#{slowperiod}#{signalperiod}_金死叉V230619" - - **信号逻辑:** - - 精确确立MACD指标中0轴以上或以下位置第几次金叉和死叉,作为开仓的辅助买点: - - **信号列表:** - - - Signal('日线_D1MACD12#26#9_金死叉V230619_0轴上死叉第2次_任意_任意_0') - - Signal('日线_D1MACD12#26#9_金死叉V230619_0轴下金叉第1次_任意_任意_0') - - Signal('日线_D1MACD12#26#9_金死叉V230619_0轴下死叉第1次_任意_任意_0') - - Signal('日线_D1MACD12#26#9_金死叉V230619_0轴下金叉第2次_任意_任意_0') - - Signal('日线_D1MACD12#26#9_金死叉V230619_0轴下死叉第2次_任意_任意_0') - - Signal('日线_D1MACD12#26#9_金死叉V230619_0轴下金叉第3次_任意_任意_0') - - Signal('日线_D1MACD12#26#9_金死叉V230619_0轴上死叉第1次_任意_任意_0') - - Signal('日线_D1MACD12#26#9_金死叉V230619_0轴上金叉第1次_任意_任意_0') - - Signal('日线_D1MACD12#26#9_金死叉V230619_0轴下死叉第3次_任意_任意_0') - - Signal('日线_D1MACD12#26#9_金死叉V230619_0轴下金叉第4次_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param di: 信号计算截止倒数第i根K线 - - :param fastperiod: MACD快线周期 - - :param slowperiod: MACD慢线周期 - - :param signalperiod: MACD信号线周期 - - :return: 信号识别结果 - """ - di = int(kwargs.get('di', 1)) - freq = c.freq.value - fastperiod = int(kwargs.get('fastperiod', 12)) - slowperiod = int(kwargs.get('slowperiod', 26)) - signalperiod = int(kwargs.get('signalperiod', 9)) - cache_key = update_macd_cache(c, **kwargs) - s = OrderedDict() - bars = get_sub_elements(c.bars_raw, di=di, n=100) - k1, k2, k3 = f"{freq}_D{di}MACD{fastperiod}#{slowperiod}#{signalperiod}_金死叉V230619".split('_') - v1 = "其他" - if len(bars)>=100: - dif = [x.cache[cache_key]['dif'] for x in bars] - dea = [x.cache[cache_key]['dea'] for x in bars] - - num_k = cross_zero_axis(dif, dea) # type: ignore - dif_temp = get_sub_elements(dif, di=di, n=num_k) - dea_temp = get_sub_elements(dea, di=di, n=num_k) - - if dif[-1] < 0 and dea[-1] < 0: - down_num_sc = down_cross_count(dif_temp, dea_temp) - down_num_jc = down_cross_count(dea_temp, dif_temp) - if dif[-1] > dea[-1] and dif[-2] < dea[-2]: - v1 = f'0轴下金叉第{down_num_jc}次' - elif dif[-1] < dea[-1] and dif[-2] > dea[-2]: - v1 = f'0轴下死叉第{down_num_sc}次' - - - elif dif[-1] > 0 and dea[-1] > 0: - up_num_sc = down_cross_count(dif_temp, dea_temp) - up_num_jc = down_cross_count(dea_temp, dif_temp) - if dif[-1] > dea[-1] and dif[-2] < dea[-2]: - v1 = f'0轴上金叉第{up_num_jc}次' - elif dif[-1] < dea[-1] and dif[-2] > dea[-2]: - v1 = f'0轴上死叉第{up_num_sc}次' - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[0] - # for symbol in symbols[:10]: - bars = research.get_raw_bars(symbol, '15分钟', '20181101', '20210101', fq='前复权') - signals_config = [{'name': tas_cross_status_V230619, 'freq': '日线', 'di': 1, 'th': 5}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/tas_cross_status_V230624.py b/examples/signals_dev/merged/tas_cross_status_V230624.py deleted file mode 100644 index 73dcb4ca8..000000000 --- a/examples/signals_dev/merged/tas_cross_status_V230624.py +++ /dev/null @@ -1,101 +0,0 @@ -import sys -sys.path.insert(0, '.') -sys.path.insert(0, '..') -sys.path.insert(0, '../..') -sys.path.insert(0, '../../..') -import pandas as pd -import numpy as np -from collections import OrderedDict -from czsc import CZSC -from czsc.signals.tas import update_macd_cache, update_ma_cache -from czsc.utils import get_sub_elements, create_single_signal, fast_slow_cross -from czsc.utils.sig import cross_zero_axis, cal_cross_num -from czsc.objects import Direction -from typing import List, Union - - - -def tas_cross_status_V230624(c: CZSC, **kwargs) -> OrderedDict: - """指定金死叉数值信号函数,以此来确定MACD交易区间 贡献者:谌意勇 - - 参数模板:"{freq}_D{di}N{n}MD{md}_MACD交叉数量V230624" - - **信号逻辑:** - - 1、通过指定0轴上下金死叉数量,来选择自己想要的指标形态,通过配合其他信号函数出信号 - 2、金叉数量和死叉数量要注意连续对应。0轴上一定是第一次先死叉,再金叉,死叉的数值同 - 金叉数值相比永远是相等或者大1,不能出现>=2的情况,0轴下则反之。 - - **信号列表:** - - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴上金叉第1次_0轴上死叉第1次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴上金叉第1次_0轴上死叉第2次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴下金叉第0次_0轴下死叉第0次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴下金叉第1次_0轴下死叉第0次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴下金叉第1次_0轴下死叉第1次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴下金叉第2次_0轴下死叉第1次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴下金叉第2次_0轴下死叉第2次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴下金叉第3次_0轴下死叉第2次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴上金叉第0次_0轴上死叉第0次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴上金叉第0次_0轴上死叉第1次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴下金叉第3次_0轴下死叉第3次_任意_0') - - Signal('日线_D1N100MD1_MACD交叉数量V230624_0轴下金叉第4次_0轴下死叉第3次_任意_0') - - :param c: czsc对象 - :param kwargs: - - - di: 倒数第i根K线 - - n: 从dik往前数n根k线(此数值不需要精确,函数会自动截取最后上下0轴以后的数据) - - md: 抖动过滤参数,金死叉之间格距离小于此数值,将被忽略(去除一些杂波扰动因素,最小值不小于1) - 0轴上下金死叉状态信息,与其他信号加以辅助操作。 - - :return: 信号字典 - """ - di = int(kwargs.get('di', 1)) - n = int(kwargs.get('n', 100)) - md = int(kwargs.get('md', 1)) # md 是 min distance 的缩写,表示金死叉之间格距离小于此数值,将被忽略(去除一些杂波扰动因素,最小值不小于1) - assert md >= 1, "md必须大于等于1" - freq = c.freq.value - cache_key = update_macd_cache(c, **kwargs) - - k1, k2, k3 = f"{freq}_D{di}N{n}MD{md}_MACD交叉数量V230624".split('_') - v1 = "其他" - v2 = "其他" - if len(c.bars_raw) < n + 1: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=n) - dif = [x.cache[cache_key]['dif'] for x in bars] - dea = [x.cache[cache_key]['dea'] for x in bars] - num_k = cross_zero_axis(dif, dea) - dif_temp = get_sub_elements(dif, di=1, n=num_k) - dea_temp = get_sub_elements(dea, di=1, n=num_k) - cross = fast_slow_cross(dif_temp, dea_temp) - - jc, sc = cal_cross_num(cross, md) - - if dif[-1] < 0 and dea[-1] < 0: - v1 = f'0轴下金叉第{jc}次' - v2 = f'0轴下死叉第{sc}次' - - elif dif[-1] > 0 and dea[-1] > 0: - v1 = f'0轴上金叉第{jc}次' - v2 = f'0轴上死叉第{sc}次' - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[0] - # for symbol in symbols[:10]: - bars = research.get_raw_bars(symbol, '15分钟', '20181101', '20210101', fq='前复权') - signals_config = [{'name': tas_cross_status_V230624, 'freq': '日线', 'di': 1, 'th': 5}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/tas_cross_status_V230625.py b/examples/signals_dev/merged/tas_cross_status_V230625.py deleted file mode 100644 index 5111d40e6..000000000 --- a/examples/signals_dev/merged/tas_cross_status_V230625.py +++ /dev/null @@ -1,98 +0,0 @@ -import sys - -sys.path.insert(0, '.') -sys.path.insert(0, '..') -sys.path.insert(0, '../..') -sys.path.insert(0, '../../..') -import pandas as pd -import numpy as np -from collections import OrderedDict -from czsc import CZSC -from czsc.signals.tas import update_macd_cache, update_ma_cache -from czsc.utils import get_sub_elements, create_single_signal, fast_slow_cross -from czsc.utils.sig import cross_zero_axis, cal_cross_num -from czsc.objects import Direction -from typing import List, Union - - -# 定义信号函数 -# ---------------------------------------------------------------------------------------------------------------------- -def tas_cross_status_V230625(c: CZSC, **kwargs) -> OrderedDict: - """指定金死叉数值信号函数, 以此来确定MACD交易区间 贡献者:谌意勇 - - 参数模板:"{freq}_D{di}N{n}MD{md}J{j}S{s}_MACD交叉数量V230625" - - **信号逻辑:** - - 1、通过指定jc或者sc数值来确定为哪第几次金叉或死叉之后的信号。两者最少要指定一个,并且指定其中一个时,另外一个需为0. - - **信号列表:** - - - Signal('15分钟_D1N100MD1J3S0_MACD交叉数量V230625_0轴下第3次金叉以后_任意_任意_0') - - Signal('15分钟_D1N100MD1J3S0_MACD交叉数量V230625_0轴上第3次金叉以后_任意_任意_0') - - :param c: czsc对象 - :param kwargs: - - - di: 倒数第i根K线 - - j: 金叉数值 - - s: 死叉数值 - - n: 从dik往前数n根k线(此数值不需要精确,函数会自动截取最后上下0轴以后的数据) - - md: 抖动过滤参数,金死叉之间格距离小于此数值,将被忽略(去除一些杂波扰动因素,最小值不小于1 - 0轴上下金死叉状态信息,与其他信号加以辅助操作。 - - :return: 信号字典 - """ - di = int(kwargs.get('di', 1)) - j = int(kwargs.get('j', 0)) - s = int(kwargs.get('s', 0)) - n = int(kwargs.get('n', 100)) - md = int(kwargs.get('md', 1)) - freq = c.freq.value - cache_key = update_macd_cache(c, **kwargs) - assert j * s == 0, "金叉死叉参数错误, j和s必须有一个为0" - - k1, k2, k3 = f"{freq}_D{di}N{n}MD{md}J{j}S{s}_MACD交叉数量V230625".split('_') - v1 = "其他" - if len(c.bars_raw) < di + n + 1: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=n) - dif = [x.cache[cache_key]['dif'] for x in bars] - dea = [x.cache[cache_key]['dea'] for x in bars] - num_k = cross_zero_axis(dif, dea) - dif_temp = get_sub_elements(dif, di=1, n=num_k) - dea_temp = get_sub_elements(dea, di=1, n=num_k) - cross = fast_slow_cross(dif_temp, dea_temp) - - jc, sc = cal_cross_num(cross, md) - - if dif[-1] < 0 and dea[-1] < 0: - if jc >= j and s == 0: - v1 = f'0轴下第{j}次金叉以后' - elif j == 0 and sc >= s: - v1 = f'0轴下第{s}次死叉以后' - - elif dif[-1] > 0 and dea[-1] > 0: - if jc >= j and s == 0: - v1 = f'0轴上第{j}次金叉以后' - elif j == 0 and sc >= s: - v1 = f'0轴上第{s}次死叉以后' - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[0] - # for symbol in symbols[:10]: - bars = research.get_raw_bars(symbol, '15分钟', '20181101', '20210101', fq='前复权') - signals_config = [{'name': tas_cross_status_V230625, 'freq': '15分钟', 'di': 1, 'j': 3}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/tas_double_ma_V230512.py b/examples/signals_dev/merged/tas_double_ma_V230512.py deleted file mode 100644 index 3ef4e2817..000000000 --- a/examples/signals_dev/merged/tas_double_ma_V230512.py +++ /dev/null @@ -1,96 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC -from czsc.utils import create_single_signal, get_sub_elements -from czsc.signals.tas import update_ma_cache - - -def tas_double_ma_V230511(c: CZSC, **kwargs): - """双均线金叉死叉后的反向信号 - - 参数模板:"{freq}_D{di}#{ma_type}#{t1}#{t2}_BS辅助V230511" - - **信号逻辑:** - - 1. t1周期均线上穿t2周期均线,且当前K线为大实体阴线,看多信号; - 2. t1周期均线下穿t2周期均线,且当前K线为大实体阳线,看空信号; - - **信号列表:** - - - Signal('日线_D2#SMA#5#20_BS辅助V230511_看空_任意_任意_0') - - Signal('日线_D2#SMA#5#20_BS辅助V230511_看多_第一个_任意_0') - - Signal('日线_D2#SMA#5#20_BS辅助V230511_看多_任意_任意_0') - - Signal('日线_D2#SMA#5#20_BS辅助V230511_看空_第一个_任意_0') - - :param c: 基础周期的 CZSC 对象 - :param kwargs: 其他参数 - - di: 倒数第 di 根 K 线 - - t1: 均线1周期 - - t2: 均线2周期 - - ma_type: 均线类型,支持:MA, EMA, WMA, DEMA, TEMA, TRIMA, KAMA, MAMA, T3 - :return: 信号字典 - """ - di = int(kwargs.get('di', 1)) - t1 = int(kwargs.get('t1', 5)) - t2 = int(kwargs.get('t2', 20)) - assert t1 < t2, "t1 必须小于 t2,否则无法判断金叉死叉" - ma_type = kwargs.get('ma_type', 'SMA').upper() - cache_key1 = update_ma_cache(c, ma_type=ma_type, timeperiod=t1) - cache_key2 = update_ma_cache(c, ma_type=ma_type, timeperiod=t2) - - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}#{ma_type}#{t1}#{t2}_BS辅助V230511".split('_') - v1, v2 = '其他', '任意' - if len(c.bars_raw) < t2 + 10: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=t2+1) - mean_solid = np.mean([x.solid for x in bars]) - bar = c.bars_raw[-di] - solid_th = max(bar.upper, bar.lower, mean_solid) - - if bar.solid < solid_th: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - if bar.cache[cache_key1] > bar.cache[cache_key2] and bar.close < bar.open: - v1 = '看多' - - right_bars = [] - for x in bars[::-1]: - if x.cache[cache_key1] > x.cache[cache_key2]: - right_bars.append({'bar': x, '大实体阴线': x.solid > solid_th and x.close < x.open}) - else: - break - - if len(right_bars) < t2 / 2 and sum([x['大实体阴线'] for x in right_bars]) == 1: - v2 = '第一个' - - if bar.cache[cache_key1] < bar.cache[cache_key2] and bar.close > bar.open: - v1 = '看空' - - right_bars = [] - for x in bars[::-1]: - if x.cache[cache_key1] < x.cache[cache_key2]: - right_bars.append({'bar': x, '大实体阳线': x.solid > solid_th and x.close > x.open}) - else: - break - - if len(right_bars) < t2 / 2 and sum([x['大实体阳线'] for x in right_bars]) == 1: - v2 = '第一个' - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': tas_double_ma_V230511, 'freq': '日线', 'di': 2}] - check_signals_acc(bars, signals_config=signals_config, height='780px') - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/tas_low_trend_V230627.py b/examples/signals_dev/merged/tas_low_trend_V230627.py deleted file mode 100644 index 3f675fe8a..000000000 --- a/examples/signals_dev/merged/tas_low_trend_V230627.py +++ /dev/null @@ -1,90 +0,0 @@ -import sys - -sys.path.insert(0, '.') -sys.path.insert(0, '..') -sys.path.insert(0, '../..') -sys.path.insert(0, '../../..') -import pandas as pd -import numpy as np -from collections import OrderedDict -from czsc import CZSC -from loguru import logger -from czsc.signals.tas import update_macd_cache, update_ma_cache -from czsc.utils import get_sub_elements, create_single_signal, fast_slow_cross -from czsc.utils.sig import cross_zero_axis, cal_cross_num -from czsc.objects import Direction -from typing import List, Union - - -# 定义信号函数 -# ---------------------------------------------------------------------------------------------------------------------- -def tas_low_trend_V230627(c: CZSC, **kwargs) -> OrderedDict: - """阴跌趋势、小阳趋势 - - 参数模板:"{freq}_D{di}N{n}TH{th}_趋势230627" - - **信号逻辑:** - - 1、阴跌趋势:在连续N根K线上rolling计数,如果当前最低价小于rolling min close,min_count + 1 - ,当 min_count > 0.8 * n 且 N根K线中振幅超过TH的K线数量小于0.2 * N,则为阴跌趋势; - 2. 小阳趋势:在连续N根K线上rolling计数,如果当前最高价大于rolling max close,max_count + 1 - ,当 max_count > 0.8 * n 且 N根K线中振幅超过TH的K线数量小于0.2 * N,则为小阳趋势; - - **信号列表:** - - - Signal('15分钟_D1N13TH500_趋势230627_阴跌趋势_任意_任意_0') - - Signal('15分钟_D1N13TH500_趋势230627_小阳趋势_任意_任意_0') - - :param c: czsc对象 - :param kwargs: - - - di: 倒数第i根K线 - - n: 从dik往前数n根k线(此数值不需要精确,函数会自动截取最后上下0轴以后的数据) - - th: 实体振幅阈值,单位为 BP - - :return: 信号字典 - """ - di = int(kwargs.get('di', 1)) - n = int(kwargs.get('n', 13)) - th = int(kwargs.get('th', 300)) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}N{n}TH{th}_趋势230627".split('_') - v1 = "其他" - if len(c.bars_raw) < di + n + 8: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - bars = get_sub_elements(c.bars_raw, di=di, n=n+5) - solid_zf = [abs(x.close / x.open - 1) * 10000 for x in bars[5:]] - if len([x for x in solid_zf if x > th]) > max(0.2 * n, 3): - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - min_count = 0 - max_count = 0 - for i in range(5, len(bars)): - bar, w5 = bars[i], bars[:i] - if bar.low <= min([x.close for x in w5]): - min_count += 1 - if bar.high >= max([x.close for x in w5]): - max_count += 1 - - if min_count >= 0.8 * n: - v1 = "阴跌趋势" - if max_count >= 0.8 * n: - v1 = "小阳趋势" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('中证500成分股') - symbol = symbols[0] - bars = research.get_raw_bars(symbol, '15分钟', '20181101', '20210101', fq='前复权') - signals_config = [{'name': tas_low_trend_V230627, 'freq': '日线', 'di': 1, 'th': 500, 'n': 21}] - check_signals_acc(bars, signals_config=signals_config, height='780px') # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/tas_rumi_V230704.py b/examples/signals_dev/merged/tas_rumi_V230704.py deleted file mode 100644 index 5a9bd92f5..000000000 --- a/examples/signals_dev/merged/tas_rumi_V230704.py +++ /dev/null @@ -1,92 +0,0 @@ -from loguru import logger - -try: - import talib as ta -except: - logger.warning(f"ta-lib 没有正确安装,相关信号函数无法正常执行。" f"请参考安装教程 https://blog.csdn.net/qaz2134560/article/details/98484091") -import math -import pandas as pd -import numpy as np -from collections import OrderedDict -from deprecated import deprecated -from czsc.analyze import CZSC -from czsc.objects import Signal, Direction, BI, RawBar, FX -from czsc.signals.tas import update_ma_cache -from czsc.utils import get_sub_elements, fast_slow_cross, count_last_same, create_single_signal -from czsc.utils.sig import cross_zero_axis, cal_cross_num, down_cross_count -from typing import Union,List - - - -def tas_rumi_V230704(c: CZSC, **kwargs) -> OrderedDict: - """对均线偏离度平滑处理,通过平滑处理的方式降低DIFF的敏感度来解决均线缠绕的问题 贡献者:谌意勇 - - 参数模板:"{freq}_D{di}F{timeperiod1}S{timeperiod2}R{rumi_window}_BS辅助V230704" - - **信号逻辑:** - - RUMI 计算参考: - 1. https://zhuanlan.zhihu.com/p/610377004 - 2. https://zhuanlan.zhihu.com/p/618394552 - - 多空规则: - 1. RUMI上穿0轴,买入做多 - 2. RUMI下穿0轴,卖出做空。 - - **信号列表:** - - - Signal('60分钟_D1F3S50R30_BS辅助V230704_空头_任意_任意_0') - - Signal('60分钟_D1F3S50R30_BS辅助V230704_多头_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: - - - di: 信号计算截止倒数第i根K线 - - timeperiod1: 均线1的周期 - - timeperiod2: 均线2的周期 - - rumi_window: rumi的周期 - - :return: 信号识别结果 - """ - di = int(kwargs.get('di', 1)) - rumi_window = int(kwargs.get('rumi_window', 30)) - timeperiod1 = int(kwargs.get('timeperiod1', 3)) - timeperiod2 = int(kwargs.get('timeperiod2', 50)) - - assert rumi_window < timeperiod2, "rumi_window 必须小于 timeperiod2" - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}F{timeperiod1}S{timeperiod2}R{rumi_window}_BS辅助V230704".split('_') - v1 = '其他' - - if len(c.bars_raw) < di + timeperiod2: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - key1 = update_ma_cache(c, ma_type='SMA', timeperiod=timeperiod1) - key2 = update_ma_cache(c, ma_type='WMA', timeperiod=timeperiod2) - bars = get_sub_elements(c.bars_raw, di=di, n=timeperiod2) - fast_array = np.array([x.cache[key1] for x in bars]) - slow_array = np.array([x.cache[key2] for x in bars]) - diff_array = fast_array - slow_array - rumi_array = ta.MA(diff_array, timeperiod=rumi_window, matype=ta.MA_Type.SMA) - - if rumi_array[-1] > 0 and rumi_array[-2] < 0: - v1='多头' - elif rumi_array[-1] < 0 and rumi_array[-2] > 0: - v1='空头' - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': tas_rumi_V230704, 'freq': "60分钟", 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px', delta_days=0) # type: ignore - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/tas_sar_base_V230424.py b/examples/signals_dev/merged/tas_sar_base_V230424.py deleted file mode 100644 index 402725986..000000000 --- a/examples/signals_dev/merged/tas_sar_base_V230424.py +++ /dev/null @@ -1,101 +0,0 @@ -import talib as ta -import numpy as np -from czsc import CZSC -from czsc.utils import create_single_signal, get_sub_elements - - -def update_sar_cache(c: CZSC, **kwargs): - """更新SAR缓存 - - SAR是止损转向操作点指标的简称,英文名称为“Stop and ReVere",缩写为SAR,一般称为抛物线指标。 - 该指标是由美国技术分析大师威尔斯·威尔德所创造出来的。 - - 详细介绍: - - - https://zhuanlan.zhihu.com/p/210169446 - - https://www.investopedia.com/terms/p/parabolicindicator.asp - - :param c: CZSC对象 - :return: - """ - cache_key = "SAR" - if c.bars_raw[-1].cache and c.bars_raw[-1].cache.get(cache_key, None): - # 如果最后一根K线已经有对应的缓存,不执行更新 - return cache_key - - last_cache = dict(c.bars_raw[-2].cache) if c.bars_raw[-2].cache else dict() - if cache_key not in last_cache.keys() or len(c.bars_raw) < 50: - # 初始化缓存 - bars = c.bars_raw - else: - # 增量更新最近5个K线缓存 - bars = c.bars_raw[-60:] - - high = np.array([x.high for x in bars]) - low = np.array([x.low for x in bars]) - sar = ta.SAR(high, low) - - for i in range(len(bars)): - _c = dict(bars[i].cache) if bars[i].cache else dict() - if cache_key not in _c.keys(): - _c.update({cache_key: sar[i] if sar[i] else 0}) - bars[i].cache = _c - - return cache_key - - -def tas_sar_base_V230425(c: CZSC, **kwargs): - """SAR基础信号 - - 参数模板:"{freq}_D{di}MO{max_overlap}SAR_BS辅助V230425" - - **信号逻辑:** - - 1. 收盘价升破SAR,且前面MO根K中有任意一根K线的收盘价都低于SAR,看多信号 - 2. 收盘价跌破SAR,且前面MO根K中有任意一根K线的收盘价都高于SAR,看空信号 - - **信号列表:** - - - Signal('日线_D1MO5SAR_BS辅助V230425_看空_任意_任意_0') - - Signal('日线_D1MO5SAR_BS辅助V230425_看多_任意_任意_0') - - :param c: 基础周期的 CZSC 对象 - :param kwargs: 其他参数 - - di: 倒数第 di 根 K 线 - - max_overlap: 信号最大重叠K线数 - :return: 信号字典 - """ - di = int(kwargs.get('di', 1)) - max_overlap = int(kwargs.get('max_overlap', 5)) - cache_key = update_sar_cache(c) - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}MO{max_overlap}SAR_BS辅助V230425".split('_') - if len(c.bars_raw) < 3: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1='其他') - - bars = get_sub_elements(c.bars_raw, di=di, n=max_overlap) - bar = c.bars_raw[-di] - sar = c.bars_raw[-di].cache[cache_key] - if bar.close > sar and any([x.close < x.cache[cache_key] for x in bars]): - v1 = '看多' - elif bar.close < sar and any([x.close > x.cache[cache_key] for x in bars]): - v1 = '看空' - else: - v1 = '其他' - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def check(): - from czsc.connectors import research - from czsc.traders.base import check_signals_acc - - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20181101', '20210101', fq='前复权') - - signals_config = [{'name': bar_sar_base_V230425, 'freq': '日线', 'di': 1}] - check_signals_acc(bars, signals_config=signals_config, height='780px') - - -if __name__ == '__main__': - check() diff --git a/examples/signals_dev/merged/vol_window_V230731.py b/examples/signals_dev/merged/vol_window_V230731.py deleted file mode 100644 index 8569d043f..000000000 --- a/examples/signals_dev/merged/vol_window_V230731.py +++ /dev/null @@ -1,76 +0,0 @@ -from collections import OrderedDict -import pandas as pd -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def vol_window_V230731(c: CZSC, **kwargs) -> OrderedDict: - """指定窗口内成交量的特征 - - 参数模板:"{freq}_D{di}W{window}M{m}N{n}_窗口能量V230731" - - **信号逻辑:** - - 取最近 m 根K线,计算每根K线的成交量,分成n层,最大值为n,最小值为1; - 最近 w 根K线的成交量分层最大值为max_vol_layer,最小值为min_vol_layer, - 以这两个值作为窗口内的成交量特征。 - - **信号列表:** - - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N9_低量N4_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N9_低量N5_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N9_低量N2_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N9_低量N3_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N10_低量N4_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N8_低量N3_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N10_低量N3_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N10_低量N6_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N10_低量N7_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N10_低量N5_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N9_低量N6_任意_0') - - Signal('60分钟_D2W5M100N10_窗口能量V230731_高量N8_低量N2_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param di: 信号计算截止倒数第i根K线 - - :param w: 观察的窗口大小。 - - :param n: 分层的数量。 - - :param m: 计算分位数所需取K线的数量。 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - w = int(kwargs.get("w", 5)) - m = int(kwargs.get("m", 30)) - n = int(kwargs.get("n", 10)) - - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}W{w}M{m}N{n}_窗口能量V230731".split('_') - v1 = "其他" - - if len(c.bars_raw) < di + m: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - vols = [x.vol for x in get_sub_elements(c.bars_raw, di=di, n=m)] - vols_layer = pd.qcut(vols, n, labels=False, duplicates='drop') - max_vol_layer = max(vols_layer[-w:]) + 1 - min_vol_layer = min(vols_layer[-w:]) + 1 - - v1, v2 = f"高量N{max_vol_layer}", f"低量N{min_vol_layer}" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20191101', '20210101', fq='前复权') - - signals_config = [ - {'name': vol_window_V230731, 'freq': '60分钟', 'di': 2, 'm': 100, 'window': 3}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/signals_dev/merged/vol_window_V230801.py b/examples/signals_dev/merged/vol_window_V230801.py deleted file mode 100644 index 4a96f03b8..000000000 --- a/examples/signals_dev/merged/vol_window_V230801.py +++ /dev/null @@ -1,55 +0,0 @@ -from collections import OrderedDict -import pandas as pd -from czsc.connectors import research -from czsc import CZSC, check_signals_acc, get_sub_elements -from czsc.utils import create_single_signal - - -def vol_window_V230801(c: CZSC, **kwargs) -> OrderedDict: - """指定窗口内成交量的特征 - - 参数模板:"{freq}_D{di}W{w}_窗口能量V230801" - - **信号逻辑:** - - 观察一个固定窗口内的成交量特征,本信号以窗口内的最大成交量与最小成交量的先后顺序作为窗口成交量的特征。 - - **信号列表:** - - - Signal('60分钟_D1W5_窗口能量V230801_先缩后放_任意_任意_0') - - Signal('60分钟_D1W5_窗口能量V230801_先放后缩_任意_任意_0') - - :param c: CZSC对象 - :param kwargs: 参数字典 - - - :param di: 信号计算截止倒数第i根K线 - - :param w: 观察的窗口大小。 - - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - w = int(kwargs.get("w", 5)) - - freq = c.freq.value - k1, k2, k3 = f"{freq}_D{di}W{w}_窗口能量V230801".split('_') - if len(c.bars_raw) < di + w: - return create_single_signal(k1=k1, k2=k2, k3=k3, v1="其他") - - vols = [x.vol for x in get_sub_elements(c.bars_raw, di=di, n=w)] - min_i, max_i = vols.index(min(vols)), vols.index(max(vols)) - v1 = "先放后缩" if min_i > max_i else "先缩后放" - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -def main(): - symbols = research.get_symbols('A股主要指数') - bars = research.get_raw_bars(symbols[0], '15分钟', '20191101', '20210101', fq='前复权') - - signals_config = [ - {'name': vol_window_V230801, 'freq': '60分钟', 'window': 10}, - ] - check_signals_acc(bars, signals_config=signals_config) # type: ignore - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/use_cross_sectional.py b/examples/use_cross_sectional.py deleted file mode 100644 index 387c236c6..000000000 --- a/examples/use_cross_sectional.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -""" -author: zengbin93 -email: zeng_bin8888@163.com -create_dt: 2023/5/29 15:06 -describe: 根据截面持仓信息,计算截面绩效 -""" -import sys -sys.path.insert(0, '.') -sys.path.insert(0, '..') -import pandas as pd - -from czsc.utils.cross import CrossSectionalPerformance - -dfh = pd.read_feather(r"C:\Users\zengb\Downloads\截面分析样例数据.feather") -# 不使用杠杆进行截面分析 -csp = CrossSectionalPerformance(dfh, max_total_weight=1) -# csp.report('不使用杠杆的表现.docx') -# -# # 使用一倍杠杆进行截面分析 -# csp = CrossSectionalPerformance(dfh, max_total_weight=2) -# csp.report('使用1倍杠杆的表现.docx') -# - - - From e63007e2ebe475f4e3a1f54a52fddf9fc192ac6f Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Mon, 11 Sep 2023 14:35:03 +0800 Subject: [PATCH 07/22] 0.9.29 fix bug (#166) --- czsc/traders/dummy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/czsc/traders/dummy.py b/czsc/traders/dummy.py index fb9e27244..145f3db55 100644 --- a/czsc/traders/dummy.py +++ b/czsc/traders/dummy.py @@ -90,7 +90,6 @@ def one_symbol_dummy(self, symbol): dfh = pd.DataFrame(pos.holds) dfh['n1b'] = (dfh['price'].shift(-1) / dfh['price'] - 1) * 10000 - dfh.drop(columns=['bid'], inplace=True) dfh.fillna(0, inplace=True) dfh['symbol'] = pos.symbol dfh.to_parquet(file_holds) From 0646b806f04f36afcaab3bc0ebe3f7589d5d746b Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Mon, 11 Sep 2023 19:24:32 +0800 Subject: [PATCH 08/22] =?UTF-8?q?0.9.29=20=E6=9B=B4=E6=96=B0=20streamlit?= =?UTF-8?q?=20=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...36\345\244\232\345\215\263\347\251\272.py" | 23 -- ...225\245\345\233\236\346\265\213V230911.py" | 290 ++++++++++++++++++ ...225\245\345\233\236\346\224\276V230911.py" | 244 +++++++++++++++ ...26\347\225\245\345\233\236\346\224\276.py" | 0 ...26\347\225\245\345\233\236\346\224\276.py" | 0 5 files changed, 534 insertions(+), 23 deletions(-) create mode 100644 "examples/streamlit_pages/CTA\347\255\226\347\225\245\345\233\236\346\265\213V230911.py" create mode 100644 "examples/streamlit_pages/JSON\347\255\226\347\225\245\345\233\236\346\224\276V230911.py" rename "examples/streamlit_pages/JSON\347\255\226\347\225\245\345\233\236\346\224\276.py" => "examples/streamlit_pages/history/JSON\347\255\226\347\225\245\345\233\236\346\224\276.py" (100%) rename "examples/streamlit_pages/\347\255\226\347\225\245\345\233\236\346\224\276.py" => "examples/streamlit_pages/history/\347\255\226\347\225\245\345\233\236\346\224\276.py" (100%) diff --git "a/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" "b/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" index 4f4ac5ccf..d029ceb99 100644 --- "a/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" +++ "b/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" @@ -99,26 +99,3 @@ def positions(self): # 当策略执行过程符合预期后,将持仓策略保存到本地 json 文件中 tactic.save_positions(results_path / "positions") - - - # for _pos in trader.positions: - # print(_pos.name, symbol, _pos.evaluate('多头')) - - # # dummy backtest, 只能通过命令行执行,不能在PyCharm中的Python终端执行 - # from czsc import DummyBacktest - # db = DummyBacktest(strategy=CzscStocksBeta, signals_path=r'D:\策略研究\signals', - # results_path=r'D:\策略研究\CzscStocksBetaV2', read_bars=research.get_raw_bars) - # # db.execute(symbols=symbols, n_jobs=10) - - # # on bar 回测 - # stats = [] - # for symbol in symbols[:3]: - # try: - # tactic = CzscStocksBeta(symbol=symbol, is_stocks=True) - # bars = research.get_raw_bars(symbol, freq=tactic.base_freq, sdt='20150101', edt='20220101') - # trader = tactic.backtest(bars, sdt='20200101') - # for _pos in trader.positions: - # stats.append(_pos.evaluate('多头')) - # print(_pos.name, symbol, _pos.evaluate('多头')) - # except Exception as e: - # print(symbol, '回测失败', e) diff --git "a/examples/streamlit_pages/CTA\347\255\226\347\225\245\345\233\236\346\265\213V230911.py" "b/examples/streamlit_pages/CTA\347\255\226\347\225\245\345\233\236\346\265\213V230911.py" new file mode 100644 index 000000000..ddcf08138 --- /dev/null +++ "b/examples/streamlit_pages/CTA\347\255\226\347\225\245\345\233\236\346\265\213V230911.py" @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +""" +author: zengbin93 +email: zeng_bin8888@163.com +create_dt: 2023/9/11 11:24 +describe: 期货CTA投研 +""" +import os +import sys +sys.path.insert(0, '.') +sys.path.insert(0, '..') +os.environ['base_path'] = r"D:\CTA研究" # 回测结果保存路径 +# os.environ['czsc_min_bi_len'] = '7' # 最小笔长度,内部无包含关系K线数量 +# os.environ['czsc_bi_change_th'] = '-1' # 笔划分时,是否用涨跌幅优化笔划分 +os.environ['signals_module_name'] = 'czsc.signals' # 信号函数所在模块 +os.environ['czsc_research_cache'] = r"D:\CZSC投研数据" # 本地数据缓存目录 +import czsc +import json +import glob +import hashlib +import pandas as pd +import streamlit as st +import plotly.express as px +from pathlib import Path +from loguru import logger +from typing import List +from stqdm import stqdm as tqdm +from streamlit_extras.mandatory_date_range import date_range_picker +from multiprocessing import cpu_count +from czsc.connectors.research import get_symbols, get_raw_bars +from concurrent.futures import ProcessPoolExecutor, as_completed + + +st.set_page_config(layout="wide", page_title="CTA策略回测", page_icon="🧭") + + +class JsonStreamStrategy(czsc.CzscStrategyBase): + """读取 streamlit 传入的 json 策略,进行回测""" + @property + def positions(self) -> List[czsc.Position]: + """返回当前的持仓策略""" + json_strategies = self.kwargs.get("json_strategies") + assert json_strategies, "请在初始化策略时,传入参数 json_strategies" + positions = [] + for _, pos in json_strategies.items(): + pos["symbol"] = self.symbol + positions.append(czsc.Position.load(pos)) + return positions + + +@st.cache_data() +def read_holds_and_pairs(files_traders, pos_name, fee=1): + holds, pairs = [], [] + for file in tqdm(files_traders): + try: + trader = czsc.dill_load(file) + pos = trader.get_position(pos_name) + if not pos.holds: + logger.info(f"{trader.symbol} {pos_name} 无持仓,跳过") + continue + + hd = pd.DataFrame(pos.holds) + hd['symbol'] = trader.symbol + hd = czsc.subtract_fee(hd, fee=fee) + holds.append(hd) + + pr = pd.DataFrame(pos.pairs) + pairs.append(pr) + except Exception as e: + logger.warning(f"{file} {pos_name} 读取失败: {e}") + + dfh = pd.concat(holds, ignore_index=True) + dfp = pd.concat(pairs, ignore_index=True) + return dfh, dfp + + +@st.cache_data() +def get_daily_nv(df): + """获取每日净值""" + res = [] + for symbol, hd in tqdm(df.groupby('symbol')): + hd = hd.sort_values('dt', ascending=True) + try: + daily = hd.groupby('date').agg({'edge_pre_fee': 'sum', 'edge_post_fee': 'sum'}).reset_index() + daily['symbol'] = symbol + res.append(daily) + except Exception as e: + logger.exception(f"{symbol} 日收益获取失败: {e}") + + dfr = pd.concat(res, ignore_index=True) + return dfr + + +def show_pos_detail(file_trader, pos_name): + """显示持仓策略详情""" + trader = czsc.dill_load(file_trader) + pos = trader.get_position(pos_name) + with st.expander(f"{pos_name} 持仓策略详情", expanded=False): + _pos = pos.dump() + _pos.pop('symbol') + st.json(_pos) + + +def show_backtest_results(file_traders, pos_name, fee=1): + dfh, dfp = read_holds_and_pairs(file_traders, pos_name, fee=fee) + dfr = get_daily_nv(dfh) + show_pos_detail(file_traders[0], pos_name) + + st.subheader("一、单笔收益评价") + + pp = czsc.PairsPerformance(dfp) + # st.write(pp.basic_info) + df1 = pp.agg_statistics('标的代码') + _res = pp.basic_info + _res['标的代码'] = "全部品种" + df1 = pd.concat([pd.DataFrame([_res]), df1], ignore_index=True) + _cols = [ + '标的代码', + '开始时间', + '结束时间', + '交易标的数量', + '总体交易次数', + '平均持仓K线数', + '平均单笔收益', + '单笔收益标准差', + '交易胜率', + '单笔盈亏比', + '累计盈亏比', + '盈亏平衡点', + '每根K线收益', + ] + df1 = df1[_cols].set_index('标的代码') + color_cols = ['交易标的数量', '总体交易次数', '平均持仓K线数', '平均单笔收益', '单笔收益标准差', + '交易胜率', '单笔盈亏比', '累计盈亏比', '盈亏平衡点', '每根K线收益'] + df1 = df1.style.format('{0:,.2f}', subset=color_cols, na_rep="-").background_gradient(cmap='RdYlGn_r', subset=color_cols) + + st.dataframe(df1, use_container_width=True) + + st.divider() + + st.subheader("二、品种等权收益曲线") + dfg = dfr.groupby('date').agg({'edge_pre_fee': 'mean', 'edge_post_fee': 'mean'}).cumsum() + dfg.rename({'edge_pre_fee': '等权费前收益', 'edge_post_fee': f'双边扣费{2*fee}BP'}, axis=1, inplace=True) + + fig = px.line(dfg, x=dfg.index, y=['等权费前收益', f'双边扣费{2*fee}BP'], labels=[], title="全部品种日收益等权") + st.plotly_chart(fig, use_container_width=True, height=600) + + dfg['dt'] = dfg.index.to_list() + stats = [] + for col in ['等权费前收益', f'双边扣费{2*fee}BP']: + dfg_ = dfg[['dt', col]].copy().rename(columns={col: 'edge'}).reset_index(drop=True) + dfg_['edge'] = dfg_['edge'].diff() + stats_ = czsc.net_value_stats(dfg_, sub_cost=False) + stats_['name'] = col + stats.append(stats_) + st.dataframe(pd.DataFrame(stats).set_index('name'), use_container_width=True) + + +def symbol_backtest(strategies, symbol, bar_sdt, sdt, edt, results_path): + """回测单个标的 + + :param strategies: 策略配置 + :param symbol: 标的代码 + :param bar_sdt: 行情开始日期 + :param sdt: 回测开始日期 + :param edt: 回测结束日期 + :param results_path: 回测结果保存路径 + """ + file_trader = results_path / f"{symbol}.trader" + if file_trader.exists(): + logger.info(f"{symbol} 已回测,跳过") + return + + try: + tactic = JsonStreamStrategy(json_strategies=strategies, symbol=symbol) + bars = get_raw_bars(symbol, tactic.base_freq, sdt=bar_sdt, edt=edt) + trader = tactic.backtest(bars, sdt=sdt) + czsc.dill_dump(trader, file_trader) + except: + logger.exception(f"{symbol} 回测失败") + + +@st.cache_data(ttl=60 * 60 * 24) +def backtest_all(strategies, results_path): + """回测全部标的 + + :param strategies: 策略配置 + :param results_path: 回测结果保存路径 + """ + bar_sdt = st.session_state.bar_sdt + gruop = st.session_state.gruop + sdt = st.session_state.sdt + edt = st.session_state.edt + max_workers = st.session_state.max_workers + symbols = get_symbols(gruop) + + if max_workers <= 1: + for symbol in tqdm(symbols, desc="On Bar 回测进度"): + symbol_backtest(strategies, symbol, bar_sdt, sdt, edt, results_path) + else: + with ProcessPoolExecutor(max_workers=max_workers) as executor: + tasks = [executor.submit(symbol_backtest, strategies, symbol, bar_sdt, sdt, edt, results_path) + for symbol in symbols] + for future in tqdm(as_completed(tasks), desc="On Bar 回测进度", total=len(tasks)): + future.result() + + +def main(): + with st.sidebar: + st.title("CTA策略回测") + st.divider() + with st.form(key='my_form_czsc'): + files = st.file_uploader(label='上传策略文件', type='json', accept_multiple_files=True) + col1, col2 = st.columns([1, 1]) + bar_sdt = col2.date_input(label='行情开始日期', value=pd.to_datetime('2018-01-01')) + gruop = col1.selectbox(label="回测品类", options=['A股主要指数', 'A股场内基金', '中证500成分股', '期货主力'], index=3) + sdt, edt = date_range_picker("回测起止日期", default_start=pd.to_datetime('2019-01-01'), default_end=pd.to_datetime('2022-01-01')) + col1, col2 = st.columns([1, 1]) + max_workers = int(col1.number_input(label='指定进程数量', value=cpu_count() // 4, min_value=1, max_value=cpu_count() // 2)) + fee = int(col2.number_input(label='单边手续费(单位:BP)', value=2, min_value=0, max_value=100)) + submit_button = st.form_submit_button(label='开始回测') + + if submit_button: + st.session_state.files = files + st.session_state.bar_sdt = bar_sdt + st.session_state.gruop = gruop + st.session_state.sdt = sdt + st.session_state.edt = edt + st.session_state.max_workers = max_workers + st.session_state.fee = fee + + + if not hasattr(st.session_state, 'files'): + st.warning("请先设置策略回测参数") + st.stop() + + files = st.session_state.files + bar_sdt = st.session_state.bar_sdt + gruop = st.session_state.gruop + sdt = st.session_state.sdt + edt = st.session_state.edt + max_workers = st.session_state.max_workers + fee = st.session_state.fee + + strategies = {file.name: json.loads(file.getvalue().decode("utf-8")) for file in files} + hash_code = hashlib.sha256(f"{str(strategies)}".encode('utf-8')).hexdigest()[:8].upper() + results_path = Path(os.getenv("base_path")) / "CTA策略回测" / f"{sdt}_{edt}_{hash_code}" / gruop + results_path.mkdir(exist_ok=True, parents=True) + + with st.sidebar.expander("策略详情", expanded=False): + tactic = JsonStreamStrategy(json_strategies=strategies, symbol='symbol') + st.caption(f"K线周期列表:{tactic.freqs}") + st.caption("独立信号列表:") + st.json(tactic.unique_signals) + st.caption("信号函数配置:") + st.json(tactic.signals_config) + + backtest_all(strategies, results_path) + + file_traders = glob.glob(fr"{results_path}\*.trader") + if not file_traders: + st.warning("当前回测参数下,没有任何标的回测结果;请调整回测参数后重试") + st.stop() + + all_pos_names = [x.name for x in czsc.dill_load(file_traders[0]).positions] + tabs = st.tabs(['全部品种', '选择特定品种组合']) + + with tabs[0]: + pos_name = st.selectbox("选择持仓", all_pos_names, index=0, key="pos_name") + show_backtest_results(file_traders, pos_name, fee=fee) + + with tabs[1]: + candidates = [Path(x).stem for x in file_traders] + sel_symbols = [] + with st.form(key='my_form_czsc_2'): + col1, col2 = st.columns([1, 3]) + pos_name_a = col1.selectbox("选择持仓", all_pos_names, index=0, key="pos_name_a") + sel_symbols = col2.multiselect("选择品种", candidates, default=candidates[:3]) + submit_button = st.form_submit_button(label='分析特定品种组合') + + if not sel_symbols: + st.warning("请先选择品种组合") + st.stop() + + sel_files= [x for x in file_traders if Path(x).stem in sel_symbols] + show_backtest_results(sel_files, pos_name_a, fee=fee) + + +if __name__ == "__main__": + main() diff --git "a/examples/streamlit_pages/JSON\347\255\226\347\225\245\345\233\236\346\224\276V230911.py" "b/examples/streamlit_pages/JSON\347\255\226\347\225\245\345\233\236\346\224\276V230911.py" new file mode 100644 index 000000000..e648d1558 --- /dev/null +++ "b/examples/streamlit_pages/JSON\347\255\226\347\225\245\345\233\236\346\224\276V230911.py" @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +""" +author: zengbin93 +email: zeng_bin8888@163.com +create_dt: 2023/9/11 11:24 +describe: CZSC策略单品种回放工具 +""" +import os +import sys +sys.path.insert(0, '.') +sys.path.insert(0, '..') +# os.environ['czsc_min_bi_len'] = '7' +# os.environ['czsc_bi_change_th'] = '-1' +os.environ['czsc_max_bi_num'] = '20' +os.environ['signals_module_name'] = 'czsc.signals' +os.environ['czsc_research_cache'] = r"D:\CZSC投研数据" # 本地数据缓存目录 +import json +import streamlit as st +import pandas as pd +from copy import deepcopy +from typing import List +from czsc.utils.bar_generator import freq_end_time +from czsc.connectors.research import get_symbols, get_raw_bars +from czsc import CzscStrategyBase, CzscTrader, KlineChart, Freq, Operate, Position +from streamlit_extras.mandatory_date_range import date_range_picker + +st.set_page_config(layout="wide", page_title="CZSC策略回放", page_icon="🏖️") + + +class JsonStreamStrategy(CzscStrategyBase): + """读取 streamlit 传入的 json 策略,进行回测""" + + @property + def positions(self) -> List[Position]: + """返回当前的持仓策略""" + json_strategies = self.kwargs.get("json_strategies") + assert json_strategies, "请在初始化策略时,传入参数 json_strategies" + positions = [] + for _, pos in json_strategies.items(): + pos["symbol"] = self.symbol + positions.append(Position.load(pos)) + return positions + + +def show_trader(trader: CzscTrader, files): + if not trader.freqs or not trader.kas or not trader.positions: + st.error("当前 trader 没有回测数据") + return + + freqs = trader.freqs + tabs = st.tabs(freqs + ['回测记录', '策略详情']) + + i = 0 + for freq in freqs: + c = trader.kas[freq] + df = pd.DataFrame(c.bars_raw) + kline = KlineChart(n_rows=3, row_heights=(0.5, 0.3, 0.2), title='', width="100%", height=600) + kline.add_kline(df, name="") + + if len(c.bi_list) > 0: + bi = pd.DataFrame( + [{'dt': x.fx_a.dt, "bi": x.fx_a.fx} for x in c.bi_list] + + [{'dt': c.bi_list[-1].fx_b.dt, "bi": c.bi_list[-1].fx_b.fx}] + ) + fx = pd.DataFrame([{'dt': x.dt, "fx": x.fx} for x in c.fx_list]) + kline.add_scatter_indicator(fx['dt'], fx['fx'], name="分型", row=1, line_width=1.2, visible=True) + kline.add_scatter_indicator(bi['dt'], bi['bi'], name="笔", row=1, line_width=1.5) + + kline.add_sma(df, ma_seq=(5, 20, 120, 240), row=1, visible=False, line_width=1) + kline.add_vol(df, row=2, line_width=1) + kline.add_macd(df, row=3, line_width=1) + + for pos in trader.positions: + bs_df = pd.DataFrame([x for x in pos.operates if x['dt'] >= c.bars_raw[0].dt]) + if not bs_df.empty: + bs_df['dt'] = bs_df['dt'].apply(lambda x: freq_end_time(x, Freq(freq))) + bs_df['tag'] = bs_df['op'].apply(lambda x: 'triangle-up' if x == Operate.LO else 'triangle-down') + bs_df['color'] = bs_df['op'].apply(lambda x: 'red' if x == Operate.LO else 'silver') + kline.add_scatter_indicator( + bs_df['dt'], + bs_df['price'], + name=pos.name, + text=bs_df['op_desc'], + row=1, + mode='text+markers', + marker_size=15, + marker_symbol=bs_df['tag'], + marker_color=bs_df['color'], + ) + + with tabs[i]: + config = { + "scrollZoom": True, + "displayModeBar": True, + "displaylogo": False, + 'modeBarButtonsToRemove': [ + 'toggleSpikelines', + 'select2d', + 'zoomIn2d', + 'zoomOut2d', + 'lasso2d', + 'autoScale2d', + 'hoverClosestCartesian', + 'hoverCompareCartesian', + ], + } + st.plotly_chart(kline.fig, use_container_width=True, config=config) + i += 1 + + with tabs[i]: + with st.expander("查看所有开平交易记录", expanded=False): + show_cols = ['策略标记', '交易方向', '盈亏比例', '开仓时间', '平仓时间', '持仓K线数', '事件序列'] + st.dataframe(st.session_state.pos_pairs[show_cols], use_container_width=True, hide_index=True) + + df = pd.DataFrame([x.evaluate() for x in trader.positions]) + st.dataframe(df, use_container_width=True) + + with st.expander("分别查看多头和空头的表现", expanded=False): + df1 = pd.DataFrame([x.evaluate('多头') for x in trader.positions]) + st.dataframe(df1, use_container_width=True) + + df2 = pd.DataFrame([x.evaluate('空头') for x in trader.positions]) + st.dataframe(df2, use_container_width=True) + + i += 1 + with tabs[i]: + with st.expander("查看最新信号", expanded=False): + if len(trader.s): + s = {k: v for k, v in trader.s.items() if len(k.split('_')) == 3} + st.write(s) + else: + st.warning("当前没有信号配置信息") + + for file in files: + with st.expander(f"持仓策略配置:{file.name}", expanded=False): + st.json(json.loads(file.getvalue().decode("utf-8")), expanded=True) + + +def init_trader(files, symbol, bar_sdt, sdt, edt): + """初始化回放参数 + + :param files: 策略文件 + :param symbol: 交易标的 + :param bar_sdt: 行情开始日期 + :param sdt: 回放开始日期 + :param edt: 回放结束日期 + """ + assert pd.to_datetime(bar_sdt) < pd.to_datetime(sdt) < pd.to_datetime(edt), "回放起止日期设置错误" + + json_strategies = {file.name: json.loads(file.getvalue().decode("utf-8")) for file in files} + tactic: CzscStrategyBase = JsonStreamStrategy( + symbol=symbol, signals_module_name=os.environ['signals_module_name'], json_strategies=json_strategies + ) + bars = get_raw_bars(symbol, tactic.base_freq, sdt=bar_sdt, edt=edt) + bg, bars_right = tactic.init_bar_generator(bars, sdt=sdt) + trader = CzscTrader(bg=bg, positions=deepcopy(tactic.positions), signals_config=deepcopy(tactic.signals_config)) + + st.session_state.trader = deepcopy(trader) + st.session_state.bars_right = deepcopy(bars_right) + st.session_state.bars_index = 0 + st.session_state.run = False + + # 跑一遍回测,生成持仓记录,用于回放时给人工检查策略一个参考 + for bar in bars_right: + trader.on_bar(bar) + + assert trader.positions, "当前策略没有持仓记录" + pairs = [pd.DataFrame(pos.pairs) for pos in trader.positions if pos.pairs] + st.session_state.pos_pairs = pd.concat(pairs, ignore_index=True) + + +def main(): + with st.sidebar: + with st.form(key='my_form_replay'): + files = st.file_uploader(label='上传策略文件:', type='json', accept_multiple_files=True) + col1, col2 = st.columns([1, 1]) + symbol = col1.selectbox("选择交易标的:", get_symbols('ALL'), index=0) + bar_sdt = col2.date_input(label='行情开始日期:', value=pd.to_datetime('2018-01-01')) + sdt, edt = date_range_picker("回放起止日期", default_start=pd.to_datetime('2019-01-01'), default_end=pd.to_datetime('2022-01-01')) + submitted = st.form_submit_button(label='设置回放参数') + + if submitted: + init_trader(files, symbol, bar_sdt, sdt, edt) + + if files and hasattr(st.session_state, 'trader'): + trader = deepcopy(st.session_state.trader) + bars_right = deepcopy(st.session_state.bars_right) + bars_num = len(bars_right) + + c1, c2, c3, c4, c5 = st.columns([5, 5, 5, 5, 25]) + + bar_edt = bars_right[st.session_state.bars_index].dt + target_bar_edt = c5.text_input('行情定位到指定时间:', placeholder=bar_edt.strftime('%Y-%m-%d %H:%M'), key="bar_edt") + if target_bar_edt: + target_bar_edt = pd.to_datetime(target_bar_edt) + for i, bar in enumerate(bars_right): + if bar.dt >= target_bar_edt: + st.session_state.bars_index = i + break + + if c1.button('行情播放'): + st.session_state.run = True + if c2.button('行情暂停'): + st.session_state.run = False + if c3.button('左移一根K线'): + st.session_state.bars_index -= 1 + if c4.button('右移一根K线'): + st.session_state.bars_index += 1 + + # 约束 bars_index 的范围在 [0, bars_num] + st.session_state.bars_index = max(0, st.session_state.bars_index) + st.session_state.bars_index = min(st.session_state.bars_index, bars_num) + + suffix = f"共{bars_num}根K线" if bars_num < 1000 else f"共{bars_num}根K线,回放数据量较大(超过1000根K线),建议缩小回放时间范围" + st.caption(f"行情播放时间范围:{bars_right[0].dt} - {bars_right[-1].dt}; 当前K线:{bar_edt};{suffix}") + + if st.session_state.run: + idx = st.session_state.bars_index + bars1 = bars_right[0: idx].copy() + while bars1: + bar_ = bars1.pop(0) + trader.on_bar(bar_) + + bars2 = bars_right[idx:].copy() + with st.empty(): + while bars2: + bar_ = bars2.pop(0) + trader.on_bar(bar_) + show_trader(trader, files) + st.session_state.bars_index += 1 + + else: + bars2 = bars_right[: st.session_state.bars_index + 1].copy() + with st.empty(): + while bars2: + bar_ = bars2.pop(0) + trader.on_bar(bar_) + show_trader(trader, files) + else: + st.warning("请上传策略文件, 文件格式为 json,配置回放参数") + + +if __name__ == '__main__': + main() diff --git "a/examples/streamlit_pages/JSON\347\255\226\347\225\245\345\233\236\346\224\276.py" "b/examples/streamlit_pages/history/JSON\347\255\226\347\225\245\345\233\236\346\224\276.py" similarity index 100% rename from "examples/streamlit_pages/JSON\347\255\226\347\225\245\345\233\236\346\224\276.py" rename to "examples/streamlit_pages/history/JSON\347\255\226\347\225\245\345\233\236\346\224\276.py" diff --git "a/examples/streamlit_pages/\347\255\226\347\225\245\345\233\236\346\224\276.py" "b/examples/streamlit_pages/history/\347\255\226\347\225\245\345\233\236\346\224\276.py" similarity index 100% rename from "examples/streamlit_pages/\347\255\226\347\225\245\345\233\236\346\224\276.py" rename to "examples/streamlit_pages/history/\347\255\226\347\225\245\345\233\236\346\224\276.py" From 204377dd01a8915505614e87cf1c700d8747d4b0 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Mon, 11 Sep 2023 22:25:55 +0800 Subject: [PATCH 09/22] 0.9.29 update --- ...36\345\244\232\345\215\263\347\251\272.py" | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git "a/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" "b/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" index d029ceb99..d83c5e9a1 100644 --- "a/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" +++ "b/examples/30\345\210\206\351\222\237\347\254\224\351\235\236\345\244\232\345\215\263\347\251\272.py" @@ -17,7 +17,7 @@ def create_long_short_V230908(symbol, **kwargs): 使用的信号函数: - https://czsc.readthedocs.io/en/latest/api/czsc.signals.cxt_bi_status_V230101.htm + https://czsc.readthedocs.io/en/latest/api/czsc.signals.cxt_bi_status_V230101.html """ opens = [ { @@ -66,6 +66,62 @@ def create_long_short_V230908(symbol, **kwargs): return pos +def create_long_short_V230909(symbol, **kwargs): + """笔非多即空策略 + + 使用的信号函数: + + https://czsc.readthedocs.io/en/latest/api/czsc.signals.cxt_bi_status_V230101.html + """ + base_freq = kwargs.get('base_freq', '30分钟') + + opens = [ + { + "operate": "开多", + "signals_all": [], + "signals_any": [], + "signals_not": [], + "factors": [ + { + "signals_all": [ + f"{base_freq}_D1_表里关系V230101_向上_任意_任意_0" + ], + "signals_any": [], + "signals_not": [ + f"{base_freq}_D1_涨跌停V230331_涨停_任意_任意_0" + ] + } + ] + }, + + { + "operate": "开空", + "signals_all": [], + "signals_any": [], + "signals_not": [], + "factors": [ + { + "signals_all": [ + f"{base_freq}_D1_表里关系V230101_向下_任意_任意_0" + ], + "signals_any": [], + "signals_not": [ + f"{base_freq}_D1_涨跌停V230331_跌停_任意_任意_0" + ] + } + ] + } + ] + + exits = [] + + pos = Position(name=f"{base_freq}笔非多即空", symbol=symbol, + opens=[Event.load(x) for x in opens], + exits=[Event.load(x) for x in exits], + interval=3600 * 4, timeout=16 * 30, stop_loss=500) + return pos + + class Strategy(czsc.CzscStrategyBase): def __init__(self, **kwargs): @@ -75,13 +131,16 @@ def __init__(self, **kwargs): @property def positions(self): pos_list = [ - create_long_short_V230908(self.symbol), + # create_long_short_V230908(self.symbol), + create_long_short_V230909(self.symbol, base_freq='30分钟'), + create_long_short_V230909(self.symbol, base_freq='60分钟'), + create_long_short_V230909(self.symbol, base_freq='日线'), ] return pos_list if __name__ == '__main__': - results_path = Path(r'D:\策略研究\30分钟笔非多即空') + results_path = Path(r'D:\策略研究\笔非多即空') logger.add(results_path / "czsc.log", rotation="1 week", encoding="utf-8") results_path.mkdir(exist_ok=True, parents=True) @@ -95,7 +154,7 @@ def positions(self): # replay 查看策略的编写是否正确,执行过程是否符合预期 bars = research.get_raw_bars(symbol, freq=tactic.base_freq, sdt='20150101', edt='20220101') - trader = tactic.replay(bars, sdt='20200101', res_path=results_path / "replay", refresh=True) + trader = tactic.replay(bars, sdt='20210101', res_path=results_path / "replay", refresh=True) # 当策略执行过程符合预期后,将持仓策略保存到本地 json 文件中 tactic.save_positions(results_path / "positions") From d9c520c26e526ec1fa97e3c64ab2121302bd28dc Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Mon, 11 Sep 2023 22:27:19 +0800 Subject: [PATCH 10/22] 0.9.29 update --- examples/{ => dropit}/strategy_quick_start.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{ => dropit}/strategy_quick_start.py (100%) diff --git a/examples/strategy_quick_start.py b/examples/dropit/strategy_quick_start.py similarity index 100% rename from examples/strategy_quick_start.py rename to examples/dropit/strategy_quick_start.py From 7f0871a9fd8b0f4f255a1f15beb7c1261df7d043 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Wed, 13 Sep 2023 19:48:17 +0800 Subject: [PATCH 11/22] 0.9.29 update --- czsc/__init__.py | 1 + czsc/utils/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/czsc/__init__.py b/czsc/__init__.py index fd234eeda..9ea7ebceb 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -62,6 +62,7 @@ home_path, get_dir_size, empty_cache_path, + print_df_sample, ) from czsc.utils.calendar import ( diff --git a/czsc/utils/__init__.py b/czsc/utils/__init__.py index 2c4e1e7ef..a7778c42a 100644 --- a/czsc/utils/__init__.py +++ b/czsc/utils/__init__.py @@ -125,7 +125,7 @@ def create_grid_params(prefix: str, detail=False, **kwargs) -> dict: key = "#".join([f"{k}={v}" for k, v in row.items()]) # params[f"{prefix}@{key}"] = row else: - key = f"{'0' * (3-len(str(i)))}{i}" + key = str(i).zfill(3) row['version'] = f"{prefix}@{key}" params[f"{prefix}@{key}"] = row From 57726d28151abd5090bfa46c2d30368d40591d6b Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Thu, 14 Sep 2023 13:41:39 +0800 Subject: [PATCH 12/22] =?UTF-8?q?0.9.29=20=E6=96=B0=E5=A2=9E=E5=A4=9A?= =?UTF-8?q?=E7=A9=BA=E5=AF=B9=E5=86=B2=E7=BB=84=E5=90=88=E5=88=86=E6=9E=90?= =?UTF-8?q?=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- czsc/__init__.py | 2 + czsc/traders/__init__.py | 2 +- czsc/traders/weight_backtest.py | 69 +++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/czsc/__init__.py b/czsc/__init__.py index 9ea7ebceb..34b244b9c 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -14,6 +14,7 @@ from czsc.objects import Freq, Operate, Direction, Signal, Factor, Event, RawBar, NewBar, Position from czsc.strategies import CzscStrategyBase, CzscJsonStrategy from czsc.sensors import holds_concepts_effect, CTAResearch, EventMatchSensor +from czsc.sensors.feature import FixedNumberSelector, FeatureAnalyzeBase from czsc.traders import ( CzscTrader, CzscSignals, @@ -30,6 +31,7 @@ get_signals_freqs, WeightBacktest, get_ensemble_weight, + long_short_equity, ) from czsc.utils import ( KlineChart, diff --git a/czsc/traders/__init__.py b/czsc/traders/__init__.py index 50142e6de..5e89eb206 100644 --- a/czsc/traders/__init__.py +++ b/czsc/traders/__init__.py @@ -14,4 +14,4 @@ ) from czsc.traders.dummy import DummyBacktest from czsc.traders.sig_parse import SignalsParser, get_signals_config, get_signals_freqs -from czsc.traders.weight_backtest import WeightBacktest, get_ensemble_weight +from czsc.traders.weight_backtest import WeightBacktest, get_ensemble_weight, long_short_equity diff --git a/czsc/traders/weight_backtest.py b/czsc/traders/weight_backtest.py index 6a970d375..b59fcc412 100644 --- a/czsc/traders/weight_backtest.py +++ b/czsc/traders/weight_backtest.py @@ -16,6 +16,75 @@ from czsc.utils.stats import daily_performance, evaluate_pairs +def long_short_equity(factors, returns, hold_period=2, rank=5, **kwargs): + """根据截面因子值与收益率,回测分析多空对冲组合的收益率 + + :param factors: 截面因子,因子值越大,越偏向于做多,因子值越小,越偏向于做空;数据格式如下: + SFIH9001 SFIF9001 SFIC9001 + dt + 2022-08-31 1.403915 1.252826 0.968868 + 2022-09-01 1.376690 1.253377 0.972276 + 2022-09-02 1.380867 1.253929 0.974999 + 2022-09-05 1.370359 1.254482 0.977737 + 2022-09-06 0.685180 0.633634 0.493986 + + :param returns: 品种收益率矩阵,数据格式如下: + SFIH9001 SFIF9001 SFIC9001 + dt + 2021-01-04 0.007803 0.017228 0.004843 + 2021-01-05 0.014068 0.008300 0.000598 + 2021-01-06 0.024520 0.022766 0.004974 + 2021-01-07 -0.006193 -0.003698 0.005951 + 2021-01-08 -0.005651 -0.012263 -0.016441 + + :param hold_period: 持仓周期,dt 时刻的数量,如果是 2,则表示每两个交易时刻调仓一次 + :param rank: 排序因子值前几名,或者排名因子值的前百分之几; + 如果是整数,则表示排名因子值前几名;如果是浮点数,则表示排名因子值的前百分之几。 + 排名靠前,越偏向于做多;排名靠后,越偏向于做空。 + :param kwargs: + :return: + """ + # 单边费率 + fee = kwargs.get('fee', 2) / 10000 + factors, returns = factors.copy(), returns.copy() + factors.index = pd.to_datetime(factors.index) + returns.index = pd.to_datetime(returns.index) + + # index 对齐 + factors, returns = factors.align(returns, join='inner') + assert len(factors) == len(returns), 'factors and cross_ret must have the same length' + assert factors.index.equals(returns.index), 'factors and cross_ret must have the same index' + assert factors.columns.sort_values().tolist() == returns.columns.sort_values().tolist(), 'factors and cross_ret must have the same columns' + + if isinstance(rank, float): + assert 0 < rank < 1, 'rank must be between 0 and 1' + rank = int(len(factors.columns) * rank) + + # 1. 计算截面品种的多空权重 + long = (factors.rank(1, ascending=True, method='first') <= rank).iloc[::hold_period].reindex(factors.index).ffill() + short = (factors.rank(1, ascending=False, method='first') <= rank).iloc[::hold_period].reindex(factors.index).ffill() + weight = long + 0 - short + assert weight.sum(axis=1).unique().tolist() == [0], '每个时间截面的多空权重之和必须为0' + + # 2. 计算多空组合的收益率 + lrt = returns[long].mean(1) / 2 + srt = (-returns[short]).mean(1) / 2 + lst = (returns * weight).sum(axis=1) / (rank * 2) + cost = weight.diff().abs().sum(axis=1) / (rank * 4) * fee * 2 + ret = pd.DataFrame({'多头': lrt, '空头': srt, '多空': lst, '多空费后': lst - cost}) + df_nav = ret.cumsum().resample('1D').last().dropna(axis=0, thresh=3) + df_nav = ret.dropna(axis=0, thresh=3).diff() + + # 2. 分品种收益统计 + ret_symbol = pd.concat([returns[long].sum(), -returns[short].sum()], axis=1) + ret_symbol.columns = ['多头', '空头'] + ret_symbol['多空'] = ret_symbol['多头'] + ret_symbol['空头'] + ret_symbol = ret_symbol.sort_values(by='多空') + + results = {'日收益率': df_nav, '品种收益': ret_symbol, '持仓权重': weight} + return results + + def get_ensemble_weight(trader: CzscTrader, method: Union[AnyStr, Callable] = 'mean'): """获取 CzscTrader 中所有 positions 按照 method 方法集成之后的权重 From 68480416a44c9090dd953ece38b9ea1c8f98f01f Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Fri, 15 Sep 2023 10:14:31 +0800 Subject: [PATCH 13/22] 0.9.29 update --- czsc/traders/weight_backtest.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/czsc/traders/weight_backtest.py b/czsc/traders/weight_backtest.py index b59fcc412..383f6093e 100644 --- a/czsc/traders/weight_backtest.py +++ b/czsc/traders/weight_backtest.py @@ -67,13 +67,14 @@ def long_short_equity(factors, returns, hold_period=2, rank=5, **kwargs): assert weight.sum(axis=1).unique().tolist() == [0], '每个时间截面的多空权重之和必须为0' # 2. 计算多空组合的收益率 - lrt = returns[long].mean(1) / 2 - srt = (-returns[short]).mean(1) / 2 - lst = (returns * weight).sum(axis=1) / (rank * 2) - cost = weight.diff().abs().sum(axis=1) / (rank * 4) * fee * 2 - ret = pd.DataFrame({'多头': lrt, '空头': srt, '多空': lst, '多空费后': lst - cost}) - df_nav = ret.cumsum().resample('1D').last().dropna(axis=0, thresh=3) - df_nav = ret.dropna(axis=0, thresh=3).diff() + long_ret = returns[long].mean(1).cumsum() + short_ret = (-returns[short]).mean(1).cumsum() + ls_ret = ((returns * weight).sum(axis=1) / (rank * 2)).cumsum() + ls_post_fee_ret = ((returns * weight).sum(axis=1) / (rank * 2) - weight.diff().abs().sum(axis=1) / (rank * 4) * fee * 2).cumsum() + + ret = pd.DataFrame({'多头': long_ret / 2, '空头': short_ret / 2, '多空': ls_ret, '多空费后': ls_post_fee_ret}) + df_nav = ret.resample('1D').last().dropna(axis=0, thresh=3) + df_nav = df_nav.diff() # 2. 分品种收益统计 ret_symbol = pd.concat([returns[long].sum(), -returns[short].sum()], axis=1) From f68db2a9321a09ad8ab2956baa3019a720ab2f61 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Thu, 21 Sep 2023 16:21:29 +0800 Subject: [PATCH 14/22] 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 Date: Fri, 22 Sep 2023 10:55:39 +0800 Subject: [PATCH 15/22] 0.9.29 update --- czsc/sensors/feature.py | 4 ++-- czsc/utils/bar_generator.py | 1 - examples/close_sma5_dist.py | 7 +++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/czsc/sensors/feature.py b/czsc/sensors/feature.py index 9b540db5d..07873adf2 100644 --- a/czsc/sensors/feature.py +++ b/czsc/sensors/feature.py @@ -16,7 +16,7 @@ class FeatureAnalyzeBase: - """【基类】特征计算与分析""" + """【基类】特征计算与分析,适用于时序量价类因子""" def __init__(self, symbols, read_bars, **kwargs) -> None: """初始化函数 @@ -124,7 +124,7 @@ def report(self): for feature in self.new_features: logger.info(f"特征 {feature} 的取值范围:{self.dfs[feature].describe().round(4).to_dict()}") - df1, res1 = cross_sectional_ic(self.dfs, x_col=feature, y_col='n1b', method=corr_method, dt_col='dt') + df1, res1 = cross_sectional_ic(self.dfs, x_col=feature, y_col='n1b', method=corr_method, dt_col='dt') logger.info(f"特征 {feature} 与未来1日收益的相关系数:{res1}") _ = self.layering(feature, 0.95, 1) _ = self.layering(feature, 0.9, 1) diff --git a/czsc/utils/bar_generator.py b/czsc/utils/bar_generator.py index 2d6c71dd3..70974cd49 100644 --- a/czsc/utils/bar_generator.py +++ b/czsc/utils/bar_generator.py @@ -195,7 +195,6 @@ def resample_bars(df: pd.DataFrame, target_freq: Union[Freq, AnyStr], raw_bars=T class BarGenerator: - """使用日线合成周线、月线、季线""" def __init__(self, base_freq: str, freqs: List[str], max_count: int = 5000): self.symbol = None diff --git a/examples/close_sma5_dist.py b/examples/close_sma5_dist.py index 82a57cd9e..c6df7868c 100644 --- a/examples/close_sma5_dist.py +++ b/examples/close_sma5_dist.py @@ -1,7 +1,7 @@ -from czsc.sensors.feature import FeatureAnalyzeBase +import czsc -class SMA5Dist(FeatureAnalyzeBase): +class SMA5Dist(czsc.FeatureAnalyzeBase): @property def new_features(self): @@ -31,5 +31,4 @@ def add_features(self, df): from czsc.connectors.research import get_raw_bars, get_symbols sd = SMA5Dist(symbols=get_symbols('中证500成分股'), read_bars=get_raw_bars, freq='日线', sdt='20210101', edt='20230101', - max_workers=10, - results_path=r"C:\sma5dist") + max_workers=10, results_path=r"C:\sma5dist") From c09f1285b1092758e5f1c556a6137db618dfe4c4 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Fri, 22 Sep 2023 15:48:10 +0800 Subject: [PATCH 16/22] 0.9.29 fix bar_window_ps_V230801 --- czsc/signals/bar.py | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/czsc/signals/bar.py b/czsc/signals/bar.py index f0e15da47..d291903ee 100644 --- a/czsc/signals/bar.py +++ b/czsc/signals/bar.py @@ -234,8 +234,10 @@ def bar_vol_grow_V221112(c: CZSC, **kwargs) -> OrderedDict: :param c: CZSC对象 :param kwargs: 参数字典 + - di: 倒数第i根K线 - n: 过去N根K线 + :return: 信号识别结果 """ di = int(kwargs.get("di", 2)) @@ -358,6 +360,7 @@ def bar_mean_amount_V221112(c: CZSC, **kwargs) -> OrderedDict: return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) +@deprecated(version='1.0.0', reason="计算耗时,逻辑不严谨") def bar_cross_ps_V221112(c: CZSC, **kwargs) -> OrderedDict: """倒数第 di 根 K 线穿越支撑、压力位的数量【慎用,非常耗时】 @@ -1278,7 +1281,7 @@ def bar_tnr_V230629(c: CZSC, **kwargs) -> OrderedDict: **信号逻辑:** TNR计算公式:取N根K线,首尾两个close的绝对差值 除以 相邻两个close的绝对差值累计。 - + 取最近100个bar的TNR进行分层。 **信号列表:** @@ -1437,13 +1440,13 @@ def bar_eight_V230702(c: CZSC, **kwargs) -> OrderedDict: for b1, b2, b3 in zip(bars[:-2], bars[1:-1], bars[2:]): if min(b1.high, b2.high, b3.high) >= max(b1.low, b2.low, b3.low): zs_list.append([b1, b2, b3]) - + _dir = "上涨" if bars[-1].close > bars[0].open else "下跌" if not zs_list: v1 = f"无中枢{_dir}" return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - + # 双中枢的情况,有一根K线的 high low 与前后两个中枢没有重叠 if len(zs_list) >= 2: zs1, zs2 = zs_list[0], zs_list[-1] @@ -1452,11 +1455,11 @@ def bar_eight_V230702(c: CZSC, **kwargs) -> OrderedDict: if _dir == "上涨" and zs1_high < zs2_low: v1 = f"双中枢{_dir}" return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - + if _dir == "下跌" and zs1_low > zs2_high: v1 = f"双中枢{_dir}" return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - + # 单中枢的情况,前三根K线出现高点:弱平衡市,前三根K线出现低点:强平衡市,否则:转折平衡市 high_first = max(bars[0].high, bars[1].high, bars[2].high) == max([x.high for x in bars]) low_first = min(bars[0].low, bars[1].low, bars[2].low) == min([x.low for x in bars]) @@ -1466,7 +1469,7 @@ def bar_eight_V230702(c: CZSC, **kwargs) -> OrderedDict: v1 = "强平衡市" else: v1 = "转折平衡市" - + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) @@ -1500,7 +1503,7 @@ def bar_window_std_V230731(c: CZSC, **kwargs) -> OrderedDict: :param c: CZSC对象 :param kwargs: 参数字典 - + - :param di: 信号计算截止倒数第i根K线 - :param w: 观察的窗口大小。 - :param m: 计算分位数所需取K线的数量。 @@ -1577,7 +1580,7 @@ def bar_window_ps_V230731(c: CZSC, **kwargs) -> OrderedDict: freq = c.freq.value k1, k2, k3 = f"{freq}_W{w}M{m}N{n}L{l}_支撑压力位V230731".split('_') - if len(c.bi_list) < n+2: + if len(c.bi_list) < n + 2: return create_single_signal(k1=k1, k2=k2, k3=k3, v1="其他") # 更新支撑压力位位置 @@ -1585,7 +1588,7 @@ def bar_window_ps_V230731(c: CZSC, **kwargs) -> OrderedDict: H_line, L_line = max([x.high for x in c.bi_list[-n:]]), min([x.low for x in c.bi_list[-n:]]) for i, bar in enumerate(c.bars_raw): if cache_key_pct in bar.cache: - continue + continue bar.cache[cache_key_pct] = (bar.close - L_line) / (H_line - L_line) fenweis = [x.cache[cache_key_pct] for x in get_sub_elements(c.bars_raw, n=m)] @@ -1610,6 +1613,18 @@ def bar_window_ps_V230801(c: CZSC, **kwargs) -> OrderedDict: **信号列表:** + - Signal('60分钟_N8W5_支撑压力位V230801_最大N7_最小N4_当前N5_0') + - Signal('60分钟_N8W5_支撑压力位V230801_最大N8_最小N4_当前N4_0') + - Signal('60分钟_N8W5_支撑压力位V230801_最大N6_最小N2_当前N6_0') + - Signal('60分钟_N8W5_支撑压力位V230801_最大N6_最小N2_当前N5_0') + - Signal('60分钟_N8W5_支撑压力位V230801_最大N6_最小N2_当前N3_0') + - Signal('60分钟_N8W5_支撑压力位V230801_最大N4_最小N0_当前N3_0') + - Signal('60分钟_N8W5_支撑压力位V230801_最大N4_最小N0_当前N2_0') + - Signal('60分钟_N8W5_支撑压力位V230801_最大N4_最小N0_当前N1_0') + - Signal('60分钟_N8W5_支撑压力位V230801_最大N7_最小N3_当前N6_0') + - Signal('60分钟_N8W5_支撑压力位V230801_最大N9_最小N4_当前N9_0') + - Signal('60分钟_N8W5_支撑压力位V230801_最大N4_最小N0_当前N4_0') + :param c: CZSC对象 :param kwargs: 参数字典 @@ -1623,11 +1638,12 @@ def bar_window_ps_V230801(c: CZSC, **kwargs) -> OrderedDict: freq = c.freq.value k1, k2, k3 = f"{freq}_N{n}W{w}_支撑压力位V230801".split('_') - if len(c.bi_list) < n+2: + ubi = c.ubi + if len(c.bi_list) < n + 2 or not ubi: return create_single_signal(k1=k1, k2=k2, k3=k3, v1="其他") - ubi = c.ubi - H_line, L_line = max([x.high for x in c.bi_list[-n:]] + [ubi['high']]), min([x.low for x in c.bi_list[-n:]] + [ubi['low']]) + H_line = max([x.high for x in c.bi_list[-n:]] + [ubi['high']]) + L_line = min([x.low for x in c.bi_list[-n:]] + [ubi['low']]) pcts = [int(max((x.close - L_line) / (H_line - L_line), 0) * 10) for x in c.bars_raw[-w:]] v1, v2, v3 = f"最大N{max(pcts)}", f"最小N{min(pcts)}", f"当前N{pcts[-1]}" From ac49a439c61eb873a1e3ef3a13600a8cb2c3d398 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Fri, 22 Sep 2023 15:49:02 +0800 Subject: [PATCH 17/22] 0.9.29 update --- czsc/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/czsc/__init__.py b/czsc/__init__.py index 34b244b9c..f8b750c91 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -67,6 +67,7 @@ print_df_sample, ) +# 交易日历工具 from czsc.utils.calendar import ( is_trading_date, next_trading_date, @@ -74,6 +75,11 @@ get_trading_dates, ) +# streamlit 量化分析组件 +from czsc.utils.st_components import ( + show_daily_return, +) + __version__ = "0.9.29" __author__ = "zengbin93" From 3273646449b7e1f696cac5f05aade594d6c97813 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Sun, 24 Sep 2023 09:55:13 +0800 Subject: [PATCH 18/22] =?UTF-8?q?0.9.29=20=E6=96=B0=E5=A2=9E=20RWC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- czsc/__init__.py | 1 + czsc/traders/__init__.py | 1 + czsc/traders/rwc.py | 243 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 4 +- 4 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 czsc/traders/rwc.py diff --git a/czsc/__init__.py b/czsc/__init__.py index f8b750c91..13967ab25 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -32,6 +32,7 @@ WeightBacktest, get_ensemble_weight, long_short_equity, + RedisWeightsClient, ) from czsc.utils import ( KlineChart, diff --git a/czsc/traders/__init__.py b/czsc/traders/__init__.py index 5e89eb206..a8f925ae5 100644 --- a/czsc/traders/__init__.py +++ b/czsc/traders/__init__.py @@ -15,3 +15,4 @@ from czsc.traders.dummy import DummyBacktest from czsc.traders.sig_parse import SignalsParser, get_signals_config, get_signals_freqs from czsc.traders.weight_backtest import WeightBacktest, get_ensemble_weight, long_short_equity +from czsc.traders.rwc import RedisWeightsClient diff --git a/czsc/traders/rwc.py b/czsc/traders/rwc.py new file mode 100644 index 000000000..197214188 --- /dev/null +++ b/czsc/traders/rwc.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +""" +author: zengbin93 +email: zeng_bin8888@163.com +create_dt: 2023/9/24 15:19 +describe: 策略持仓权重管理 +""" +import time +import json +import redis +import threading +import pandas as pd +from loguru import logger +from datetime import datetime + + +class RedisWeightsClient: + """策略持仓权重收发客户端""" + + def __init__(self, strategy_name, redis_url, **kwargs): + """ + :param name: str, 策略名 + :param redis_url: str, redis连接字符串 + + For example:: + + redis://[[username]:[password]]@localhost:6379/0 + rediss://[[username]:[password]]@localhost:6379/0 + unix://[username@]/path/to/socket.sock?db=0[&password=password] + + Three URL schemes are supported: + + - `redis://` creates a TCP socket connection. See more at: + + - `rediss://` creates a SSL wrapped TCP socket connection. See more at: + + - ``unix://``: creates a Unix Domain Socket connection. + + """ + self.strategy_name = strategy_name + self.redis_url = redis_url + + self.heartbeat_client = redis.from_url(redis_url, decode_responses=True) + self.heartbeat_prefix = kwargs.get("heartbeat_prefix", "heartbeat") + + thread_safe_pool = redis.BlockingConnectionPool.from_url(redis_url, decode_responses=True) + self.r = redis.Redis(connection_pool=thread_safe_pool) + self.lua_publish = RedisWeightsClient.register_lua_publish(self.r) + + self.heartbeat_thread = threading.Thread(target=self.__heartbeat, daemon=True) + self.heartbeat_thread.start() + + def set_metadata(self, base_freq, description, author, outsample_sdt, **kwargs): + """设置策略元数据""" + outsample_sdt = pd.to_datetime(outsample_sdt).strftime('%Y%m%d') + meta = {'name': self.strategy_name, 'base_freq': base_freq, + 'description': description, 'author': author, 'outsample_sdt': outsample_sdt, + 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'kwargs': json.dumps(kwargs)} + self.r.hset(f'{self.strategy_name}:meta', mapping=meta) + + @property + def metadata(self): + """获取策略元数据""" + return self.r.hgetall(f'{self.strategy_name}:meta') + + def publish(self, symbol, dt, weight, price=0, ref=None, overwrite=False): + """发布单个策略持仓权重 + + :param symbol: str, eg; SFIF9001 + :param dt: py_datetime or pandas Timestamp + :param weight: float, 信号值 + :param price: float, 产生信号时的价格 + :param ref: dict, 自定义数据 + :param overwrite: boolean, 是否覆盖已有记录 + :return: 成功发布信号的条数 + """ + if not isinstance(dt, datetime): + dt = pd.to_datetime(dt) + + udt = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + key = f'Weights:{self.strategy_name}:{symbol}:{dt.strftime("%Y%m%d%H%M%S")}' + ref = ref if ref else '{}' + ref_str = json.dumps(ref) if isinstance(ref, dict) else ref + return self.lua_publish(keys=[key], args=[1 if overwrite else 0, udt, weight, price, ref_str]) + + def publish_dataframe(self, df, overwrite=False, batch_size=10000): + """批量发布多个策略信号 + + :param df: pandas.DataFrame, 必需包含['symbol', 'dt', 'weight']列, + 可选['price', 'ref']列, 如没有price则写0, dtype同publish方法 + :param overwrite: boolean, 是否覆盖已有记录 + :return: 成功发布信号的条数 + """ + df = df.copy() + df['dt'] = pd.to_datetime(df['dt']) + df = df.sort_values('dt') + + if 'price' not in df.columns: + df['price'] = 0 + if 'ref' not in df.columns: + df['ref'] = '{}' + + keys, args = [], [] + for row in df[['symbol', 'dt', 'weight', 'price', 'ref']].to_numpy(): + key = f'Weights:{self.strategy_name}:{row[0]}:{row[1].strftime("%Y%m%d%H%M%S")}' + keys.append(key) + + args.append(row[2]) + args.append(row[3]) + ref = row[4] + args.append(json.dumps(ref) if isinstance(ref, dict) else ref) + + udt = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + overwrite = 1 if overwrite else 0 + + pub_cnt = 0 + len_keys = len(keys) + for i in range(0, len_keys, batch_size): + if i + batch_size < len_keys: + tmp_keys = keys[i: i + batch_size] + tmp_args = [overwrite, udt] + args[3 * i: 3 * (i + batch_size)] + else: + tmp_keys = keys[i: len_keys] + tmp_args = [overwrite, udt] + args[3 * i: 3 * len_keys] + logger.info(f"索引 {i},即将发布 {len(tmp_keys)} 条权重信号") + pub_cnt += self.lua_publish(keys=tmp_keys, args=tmp_args) + logger.info(f"已完成 {pub_cnt} 次发布") + return pub_cnt + + def __heartbeat(self): + while True: + key = f'{self.heartbeat_prefix}:{self.strategy_name}' + try: + self.heartbeat_client.set(key, datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + except: + continue + time.sleep(15) + + def get_keys(self, pattern): + """使用 lua 获取 redis 中指定 pattern 的 keys""" + return self.r.eval('''local pattern = ARGV[1]\nreturn redis.call('KEYS', pattern)''', 0, pattern) + + def clear_all(self): + """删除该策略所有记录""" + self.r.delete(f"{self.strategy_name}:meta") + keys = self.get_keys(f'Weights:{self.strategy_name}*') + if keys is not None and len(keys) > 0: + self.r.delete(*keys) + + @staticmethod + def register_lua_publish(client): + lua_body = ''' +local overwrite = ARGV[1] +local update_time = ARGV[2] +local cnt = 0 +local ret +for i = 1, #KEYS do + local key = KEYS[i] + local sig = ARGV[3 + 3 * (i - 1)] + local price = ARGV[4 + 3 * (i - 1)] + local ref_str = ARGV[5 + 3 * (i - 1)] + local split_str = {} + key:gsub('[^:]+', function(s) table.insert(split_str, s) end) + local model_key = split_str[1] .. ':' .. split_str[2] .. ':' .. split_str[3] + + local if_pass = true + if overwrite ~= '0' then + if_pass = false + else + local pos = redis.call('HGET', model_key .. ':LAST', 'weight') + if not pos or math.abs(tonumber(sig) - tonumber(pos)) > 0.00001 then + if_pass = false + end + end + + if not if_pass then + local strategy_name, symbol, action_time = split_str[2], split_str[3], split_str[4] + local at_str = string.sub(action_time, 1, 4) .. '-' .. string.sub(action_time, 5, 6) .. '-' .. + string.sub(action_time, 7, 8) .. ' ' .. string.sub(action_time, 9, 10) .. ':' .. + string.sub(action_time, 11, 12) .. ':' .. string.sub(action_time, 13, 14) + redis.call('ZADD', model_key, tonumber(action_time), key) + local ret1 = redis.call('HMSET', key, 'symbol', symbol, 'weight', sig, 'dt', at_str, 'update_time', update_time, 'price', price, 'ref', ref_str) + local ret2 = redis.call('HMSET', key:gsub(action_time, 'LAST'), 'symbol', symbol, 'weight', sig, 'dt', at_str, 'update_time', update_time, 'price', price, 'ref', ref_str) + if ret1.ok and ret2.ok then + cnt = cnt + 1 + local pubKey = 'PUBSUB:' .. split_str[1] .. ':' .. strategy_name .. ':' .. symbol + redis.call('PUBLISH', pubKey, key .. ':' .. sig .. ':' .. price .. ':' .. ref_str) + end + end +end +return cnt +''' + return client.register_script(lua_body) + + def get_symbols(self): + """获取策略交易的品种列表""" + keys = self.get_keys(f'Weights:{self.strategy_name}*') + symbols = {x.split(":")[2] for x in keys} + return list(symbols) + + def get_last_weights(self, symbols=None): + """获取最近的持仓权重""" + symbols = symbols if symbols else self.get_symbols() + with self.r.pipeline() as pipe: + for symbol in symbols: + pipe.hgetall(f"Weights:{self.strategy_name}:{symbol}:LAST") + rows = pipe.execute() + + dfw = pd.DataFrame(rows) + dfw['weight'] = dfw['weight'].astype(float) + dfw['dt'] = pd.to_datetime(dfw['dt']) + return dfw + + def get_hist_weights(self, symbol, sdt, edt) -> pd.DataFrame: + """获取单个品种的持仓权重历史数据""" + start_score = pd.to_datetime(sdt).strftime('%Y%m%d%H%M%S') + end_score = pd.to_datetime(edt).strftime('%Y%m%d%H%M%S') + model_key = f'Weights:{self.strategy_name}:{symbol}' + key_list = self.r.zrangebyscore(model_key, start_score, end_score) + + if len(key_list) == 0: + logger.warning(f'no history weights: {symbol} - {sdt} - {edt}') + return pd.DataFrame() + + with self.r.pipeline() as pipe: + for key in key_list: + pipe.hmget(key, 'weight', 'price', 'ref') + rows = pipe.execute() + + weights = [] + for i in range(len(key_list)): + dt = pd.to_datetime(key_list[i].split(":")[-1]) # type: ignore + weight, price, ref = rows[i] + weight = weight if weight is None else float(weight) + price = price if price is None else float(price) + try: + ref = json.loads(ref) + except Exception: + ref = ref + weights.append((self.strategy_name, symbol, dt, weight, price, ref)) + dfw = pd.DataFrame(weights, columns=['strategy_name', 'symbol', 'dt', 'weight', 'price', 'ref']) + return dfw diff --git a/requirements.txt b/requirements.txt index da8b4e2dc..a0dda89ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,4 +19,6 @@ tenacity>=8.1.0 requests-toolbelt>=0.10.1 plotly>=5.11.0 parse>=1.19.0 -lightgbm>=4.0.0 \ No newline at end of file +lightgbm>=4.0.0 +streamlit +redis \ No newline at end of file From f7c15117c8999b2f5c09004dfed2c132e2d670a7 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Sun, 24 Sep 2023 21:39:34 +0800 Subject: [PATCH 19/22] =?UTF-8?q?0.9.29=20=E6=96=B0=E5=A2=9E=E5=BC=80?= =?UTF-8?q?=E5=B9=B3=E4=BB=93=E4=BC=98=E5=8C=96=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- czsc/__init__.py | 2 + czsc/traders/__init__.py | 1 + czsc/traders/optimize.py | 416 +++++++++++++++++++++++++++++ examples/create_json_strategies.py | 6 +- examples/use_optimize.py | 112 ++++++++ 5 files changed, 535 insertions(+), 2 deletions(-) create mode 100644 czsc/traders/optimize.py create mode 100644 examples/use_optimize.py diff --git a/czsc/__init__.py b/czsc/__init__.py index 13967ab25..dd62d6768 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -33,6 +33,8 @@ get_ensemble_weight, long_short_equity, RedisWeightsClient, + OpensOptimize, + ExitsOptimize, ) from czsc.utils import ( KlineChart, diff --git a/czsc/traders/__init__.py b/czsc/traders/__init__.py index a8f925ae5..f4dd351e9 100644 --- a/czsc/traders/__init__.py +++ b/czsc/traders/__init__.py @@ -16,3 +16,4 @@ from czsc.traders.sig_parse import SignalsParser, get_signals_config, get_signals_freqs from czsc.traders.weight_backtest import WeightBacktest, get_ensemble_weight, long_short_equity from czsc.traders.rwc import RedisWeightsClient +from czsc.traders.optimize import OpensOptimize, ExitsOptimize diff --git a/czsc/traders/optimize.py b/czsc/traders/optimize.py new file mode 100644 index 000000000..489093d7a --- /dev/null +++ b/czsc/traders/optimize.py @@ -0,0 +1,416 @@ +# -*- coding: utf-8 -*- +""" +author: zengbin93 +email: zeng_bin8888@163.com +create_dt: 2023/9/24 13:33 +describe: 择时策略开平仓优化 +""" +import os +import czsc +import time +import hashlib +import pandas as pd +from tqdm import tqdm +from loguru import logger +from copy import deepcopy +from pathlib import Path +from typing import Callable, Union, List, AnyStr +from czsc.strategies import CzscStrategyBase +from czsc.objects import Position, Event +from concurrent.futures import ProcessPoolExecutor, as_completed + + +class CzscOpenOptimStrategy(CzscStrategyBase): + + @staticmethod + def update_beta_opens(beta: Position, open_signals_all: Union[List[AnyStr], AnyStr]): + """更新 beta 出场信号""" + pos = deepcopy(beta) + assert len(pos.opens) == 1, "基础策略入场信号必须为单个Event" + if isinstance(open_signals_all, str): + open_signals_all = [open_signals_all] + + pos_dict = pos.dump() + sig_hash = hashlib.md5(f"{open_signals_all}".encode('utf-8')).hexdigest()[:8].upper() + pos_dict['name'] = f"{pos.name}#{sig_hash}" + pos_dict['opens'][0]['signals_all'].extend(open_signals_all) + return Position.load(pos_dict) + + @property + def positions(self): + betas = self.load_positions(self.kwargs['files_position']) + candidate_signals = deepcopy(self.kwargs['candidate_signals']) + pos_list = deepcopy(betas) + for beta in betas: + for sigs_ in candidate_signals: + pos = self.update_beta_opens(beta, sigs_) + pos_list.append(pos) + return pos_list + + +class CzscExitOptimStrategy(CzscStrategyBase): + + @staticmethod + def update_beta_exits(beta: Position, event_dict: dict, mode='replace'): + """更新 beta 出场信号 + + 出场优化,有三种路径: + + 1. 开仓后识别走势加速,找个合适的机会提前平仓 + 2. 根据开仓后一段时间内的走势,如果发现异样,提前平仓 + 3. 用新的出场信号完全替换旧的出场信号 + + """ + pos = deepcopy(beta) + event: Event = Event.load(deepcopy(event_dict)) + event_hash = hashlib.md5(f"{event.dump()}".encode('utf-8')).hexdigest()[:8].upper() + open_ops = [x.operate.value for x in pos.opens] + + if all(x == '开多' for x in open_ops) and event.operate.value != '平多': + return None + + if all(x == '开空' for x in open_ops) and event.operate.value != '平空': + return None + + assert mode in ['replace', 'append'], "mode must be replace or append" + if mode == 'replace': + pos.exits = [deepcopy(event)] + pos.name = f"{beta.name}#替换{event_hash}" + else: + pos.exits.append(deepcopy(event)) + pos.name = f"{beta.name}#追加{event_hash}" + + # 踩坑记录:对于 dataclass 对象,如果直接修改属性,会导致原对象也被修改,因此这里需要重新创建一个对象 + return Position.load(pos.dump()) + + @property + def positions(self): + betas = self.load_positions(self.kwargs['files_position']) + candidate_events = deepcopy(self.kwargs['candidate_events']) # 优化出场信号 + pos_list = deepcopy(betas) + + for beta in betas: + for event in candidate_events: + + pos1 = self.update_beta_exits(beta, event, mode='append') + pos2 = self.update_beta_exits(beta, event, mode='replace') + + if pos1 is not None: + pos_list.append(pos1) + if pos2 is not None: + pos_list.append(pos2) + + return pos_list + + +def one_symbol_optim(symbol, read_bars: Callable, path: str, **kwargs): + """单个标的优化 + + :param symbol: 标的代码 + :param read_bars: K线数据读取函数 + :param path: 优化结果保存路径 + :param kwargs: 其他参数 + + - bar_sdt: K线数据开始日期 + - bar_edt: K线数据结束日期 + - sdt: 优化开始日期 + - optim_type: 优化类型,open 或 exit + + """ + symbol_path = Path(path) / symbol + if symbol_path.exists(): + logger.info(f"{symbol} dummy 结果已经存在") + return + symbol_path.mkdir(parents=True, exist_ok=True) + + bar_sdt = kwargs.get('bar_sdt', '20150101') + bar_edt = kwargs.get('bar_edt', '20220101') + sdt = kwargs.get('sdt', '20170101') + assert bar_sdt < sdt < bar_edt, "sdt 必须在 bar_sdt 和 bar_edt 之间" + + start_time = time.time() + optim_type = kwargs.get('optim_type', 'open') + if optim_type == 'open': + tactic = CzscOpenOptimStrategy(symbol=symbol, **kwargs) + else: + assert optim_type == 'exit', "optim_type must be open or exit" + tactic = CzscExitOptimStrategy(symbol=symbol, **kwargs) + + try: + bars = read_bars(symbol, tactic.base_freq, bar_sdt, bar_edt, fq='后复权', raw_bar=True) + if len(bars) < 100: + logger.warning(f"{symbol} K线数量不足,无法优化") + return None + + trader = tactic.backtest(bars, sdt=sdt) + except Exception as e: + logger.exception(f"{symbol} 优化失败,原因:{e}") + return None + + for pos in trader.positions: # type: ignore + try: + file_pairs = os.path.join(symbol_path, f"{pos.name}.pairs") + file_holds = os.path.join(symbol_path, f"{pos.name}.holds") + + pairs = pd.DataFrame(pos.pairs) + pairs.to_parquet(file_pairs) + + dfh = pd.DataFrame(pos.holds) + dfh['n1b'] = (dfh['price'].shift(-1) / dfh['price'] - 1) * 10000 + dfh.fillna(0, inplace=True) + dfh['symbol'] = pos.symbol + dfh.to_parquet(file_holds) + except Exception as e: + logger.debug(f"{symbol} {pos.name} 保存失败,原因:{e}") + + logger.info(f"{symbol} - {optim_type} 优化完成,耗时 {time.time() - start_time:.2f} 秒") + + +def one_position_stats(path, pos_name): + """分析单个 pos 的表现""" + path = Path(path) + files_p = path.glob(f"**/{pos_name}.pairs") + files_h = path.glob(f"**/{pos_name}.holds") + + pos_pairs = [] + for file_p in files_p: + try: + dfp = pd.read_parquet(file_p) + pos_pairs.append(dfp) + except Exception as e: + logger.debug(f"{file_p} 读取失败,原因:{e}") + + pos_holds = [] + for file_h in files_h: + try: + dfh = pd.read_parquet(file_h) + pos_holds.append(dfh) + except Exception as e: + logger.debug(f"{file_h} 读取失败,原因:{e}") + + pairs = pd.concat(pos_pairs, ignore_index=True) + holds = pd.concat(pos_holds, ignore_index=True) + + if len(pairs) == 0 or len(holds) == 0: + return None + + try: + pp = czsc.PairsPerformance(pairs) + stats = dict(pp.basic_info) + # 加入截面等权评价 + cross = holds.groupby('dt').apply(lambda x: (x['n1b'] * x['pos']).sum() / (sum(x['pos'] != 0) + 1)).sum() + stats['截面等权收益'] = cross + cross1 = holds.groupby('dt').apply(lambda x: (x['n1b'] * x['pos']).mean()).sum() + stats['截面品种等权'] = cross1 + stats['pos_name'] = pos_name + return stats + except Exception as e: + logger.exception(f"{pos_name} 分析失败,原因:{e}") + return None + + +class OpensOptimize: + """基础策略入场优化流程""" + + def __init__(self, read_bars: Callable, **kwargs): + """ + + :param read_bars: K线数据读取函数 + :param kwargs: 其他参数 + + - symbols: 优化标的列表 + - candidate_events: 优化出场信号列表 + - results_path: 优化结果保存路径 + - files_position: 优化入场信号文件路径列表 + - signals_module_name: 信号模块名 + - bar_sdt: K线数据开始日期 + - bar_edt: K线数据结束日期 + + """ + self.version = 'OpensOptimizeV230924' + self.symbols = sorted(kwargs['symbols']) + self.read_bars = read_bars + self.kwargs = kwargs + self.task_name = kwargs.get('task_name', '出场优化') + self.candidate_signals = sorted(kwargs.pop('candidate_signals')) + self.task_hash = hashlib.md5(f"{self.candidate_signals}_{self.symbols}".encode('utf-8')).hexdigest()[:8].upper() + + results_path = os.path.join(kwargs['results_path'], f"{self.task_name}_{self.task_hash}") + os.makedirs(results_path, exist_ok=True) + self.poss_path = os.path.join(results_path, 'poss') + os.makedirs(self.poss_path, exist_ok=True) + + self.results_path = results_path + logger.add(f"{self.results_path}\\信号优化.log", encoding='utf-8', enqueue=True) + logger.info(f"{self.task_name} | {self.candidate_signals} | 其他参数:{kwargs}") + + def _one_symbol_optim(self, symbol): + one_symbol_optim(symbol, self.read_bars, self.poss_path, optim_type='open', + candidate_signals=self.candidate_signals, **self.kwargs) + + def _one_pos_stats(self, pos_name): + return one_position_stats(self.poss_path, pos_name) + + def __symbols_optim(self, n_jobs=1): + symbols = self.symbols + if n_jobs <= 1: + for symbol in tqdm(sorted(symbols), desc="优化进度"): + self._one_symbol_optim(symbol) + return + + with ProcessPoolExecutor(n_jobs) as pool: + pool.map(self._one_symbol_optim, sorted(symbols)) + + def _positions_stats(self, dumps_map, n_jobs=1): + """统计所有 pos 的表现""" + if n_jobs <= 1: + all_stats = [self._one_pos_stats(pos_name) for pos_name in dumps_map.keys()] + for s, pos_name in zip(all_stats, dumps_map.keys()): + if not s: + continue + s['pos_dump'] = dumps_map[pos_name] + return all_stats + + all_stats = [] + with ProcessPoolExecutor(n_jobs) as pool: + futures = [pool.submit(self._one_pos_stats, pos_name) for pos_name in dumps_map.keys()] + for future in as_completed(futures): + s = future.result() + if s: + s['pos_dump'] = dumps_map[s['pos_name']] + all_stats.append(s) + return all_stats + + def execute(self, n_jobs=1): + """批量优化策略 + + :param n_jobs: 进程数量 + :return: + """ + symbols, results_path = self.symbols, self.results_path + tactic = CzscOpenOptimStrategy(symbol='symbol', candidate_signals=self.candidate_signals, **self.kwargs) + tactic.save_positions(os.path.join(results_path, 'positions')) + + dumps_map = {pos.name: pos.dump() for pos in tactic.positions} + logger.info(f"{self.version} 开始优化策略,策略数量:{len(tactic.positions)},共 {len(symbols)} 只标的,进程数量:{n_jobs};" + f"结果保存在 {results_path},请耐心等待...") + + self.__symbols_optim(n_jobs=n_jobs) + all_stats = self._positions_stats(dumps_map, n_jobs=n_jobs) + + file_report = os.path.join(results_path, f"入场优化_{self.task_name}_{self.task_hash}.xlsx") + if all_stats: + logger.info(f"优化完成,共 {len(all_stats)} 个策略,结果保存在 {file_report}") + report_df = pd.DataFrame(all_stats).sort_values(['截面等权收益'], ascending=False, ignore_index=True) + report_df.to_excel(file_report, index=False) + else: + logger.warning("优化结果为空!") + + if self.kwargs.get('feishu_app_id') and self.kwargs.get('feishu_app_secret'): + if os.path.exists(file_report): + czsc.fsa.push_message(file_report, msg_type='file', **self.kwargs) + else: + czsc.fsa.push_message(f"开仓策略优化任务【{self.task_name} 优化结果为空!", **self.kwargs) + + +class ExitsOptimize: + """基础策略出场优化流程""" + + def __init__(self, read_bars: Callable, **kwargs): + """ + + :param read_bars: K线数据读取函数 + :param kwargs: 其他参数 + + - symbols: 优化标的列表 + - candidate_events: 优化出场信号列表 + - results_path: 优化结果保存路径 + - files_position: 优化入场信号文件路径列表 + - signals_module_name: 信号模块名 + + """ + self.version = 'ExitsOptimizeV230924' + self.symbols = kwargs['symbols'] + self.read_bars = read_bars + self.kwargs = kwargs + self.task_name = kwargs.get('task_name', '出场优化') + self.candidate_events = kwargs.pop('candidate_events') + self.task_hash = hashlib.md5(f"{self.candidate_events}_{self.symbols}".encode('utf-8')).hexdigest()[:8].upper() + + results_path = os.path.join(kwargs['results_path'], f"{self.task_name}_{self.task_hash}") + os.makedirs(results_path, exist_ok=True) + self.poss_path = os.path.join(results_path, 'poss') + os.makedirs(self.poss_path, exist_ok=True) + + self.results_path = results_path + logger.add(f"{self.results_path}\\信号优化.log", encoding='utf-8', enqueue=True) + logger.info(f"{self.task_name} | {self.candidate_events} | 其他参数:{kwargs}") + + def _one_symbol_optim(self, symbol): + one_symbol_optim(symbol, self.read_bars, self.poss_path, optim_type='exit', + candidate_events=self.candidate_events, **self.kwargs) + + def _one_pos_stats(self, pos_name): + return one_position_stats(self.poss_path, pos_name) + + def __symbols_optim(self, n_jobs=1): + symbols = self.symbols + if n_jobs <= 1: + for symbol in tqdm(sorted(symbols), desc="优化进度"): + self._one_symbol_optim(symbol) + return + + with ProcessPoolExecutor(n_jobs) as pool: + pool.map(self._one_symbol_optim, sorted(symbols)) + + def _positions_stats(self, dumps_map, n_jobs=1): + """统计所有 pos 的表现""" + if n_jobs <= 1: + all_stats = [self._one_pos_stats(pos_name) for pos_name in dumps_map.keys()] + for s, pos_name in zip(all_stats, dumps_map.keys()): + if not s: + continue + s['pos_dump'] = dumps_map[pos_name] + return all_stats + + all_stats = [] + with ProcessPoolExecutor(n_jobs) as pool: + futures = [pool.submit(self._one_pos_stats, pos_name) for pos_name in dumps_map.keys()] + for future in as_completed(futures): + s = future.result() + if s: + s['pos_dump'] = dumps_map[s['pos_name']] + all_stats.append(s) + return all_stats + + def execute(self, n_jobs=1): + """批量优化策略 + + :param n_jobs: 进程数量 + :return: + """ + symbols = self.symbols + results_path = self.results_path + tactic = CzscExitOptimStrategy(symbol='symbol', candidate_events=self.candidate_events, **self.kwargs) + tactic.save_positions(os.path.join(results_path, 'positions')) + + dumps_map = {pos.name: pos.dump() for pos in tactic.positions} + logger.info(f"{self.version} 开始优化策略,策略数量:{len(tactic.positions)},共 {len(symbols)} 只标的,进程数量:{n_jobs};" + f"结果保存在 {results_path},请耐心等待...") + + self.__symbols_optim(n_jobs=n_jobs) + all_stats = self._positions_stats(dumps_map, n_jobs=n_jobs) + + file_report = os.path.join(results_path, f"出场优化_{self.task_name}_{self.task_hash}.xlsx") + if all_stats: + logger.info(f"策略出场优化完成,共 {len(all_stats)} 个策略,结果保存在 {file_report}") + report_df = pd.DataFrame(all_stats).sort_values(['截面等权收益'], ascending=False, ignore_index=True) + report_df.to_excel(file_report, index=False) + else: + logger.warning("策略出场优化结果为空!请检查执行日志!") + + if self.kwargs.get('feishu_app_id') and self.kwargs.get('feishu_app_secret'): + if os.path.exists(file_report): + czsc.fsa.push_message(file_report, msg_type='file', **self.kwargs) + else: + czsc.fsa.push_message("优化结果为空!", **self.kwargs) diff --git a/examples/create_json_strategies.py b/examples/create_json_strategies.py index 3be06770a..577b4bade 100644 --- a/examples/create_json_strategies.py +++ b/examples/create_json_strategies.py @@ -9,6 +9,7 @@ ) from czsc import CzscStrategyBase, Position + class BetaStrategy(CzscStrategyBase): @property @@ -101,6 +102,7 @@ def positions(self) -> List[Position]: ] return _pos + if __name__ == "__main__": - tactic = BetaStrategy(symbol="000001.XSHG") - tactic.save_positions(r"D:\QMT投研\基础策略V230707") \ No newline at end of file + tactic = BetaStrategy(symbol="000001.XSHG", signals_module_name='czsc.signals') + tactic.save_positions(r"D:\QMT投研\基础策略V230707") diff --git a/examples/use_optimize.py b/examples/use_optimize.py new file mode 100644 index 000000000..34a603d0a --- /dev/null +++ b/examples/use_optimize.py @@ -0,0 +1,112 @@ +from czsc.connectors.research import get_symbols, get_raw_bars +from czsc.traders.optimize import OpensOptimize, ExitsOptimize + + +def run_opens_optim(): + symbols = get_symbols('期货主力')[:10] + results_path = r"D:\QMT投研\CCI入场优化结果B" + files_position = [ + r"D:\QMT投研\基础策略V230707\A股日线CCI空头基准.json", + r"D:\QMT投研\基础策略V230707\A股日线CCI多头基准.json", + ] + task_name = 'CCI入场优化' + + candidate_signals = """ +日线_D2单K趋势N5_BS辅助V230506_第1层_任意_任意_0 +日线_D2单K趋势N5_BS辅助V230506_第2层_任意_任意_0 +日线_D2单K趋势N5_BS辅助V230506_第3层_任意_任意_0 +日线_D2单K趋势N5_BS辅助V230506_第4层_任意_任意_0 +日线_D2单K趋势N10_BS辅助V230506_第1层_任意_任意_0 +日线_D2单K趋势N10_BS辅助V230506_第2层_任意_任意_0 +日线_D2单K趋势N10_BS辅助V230506_第4层_任意_任意_0 +日线_D2单K趋势N10_BS辅助V230506_第6层_任意_任意_0 +日线_D2单K趋势N10_BS辅助V230506_第7层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第11层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第12层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第13层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第14层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第15层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第16层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第18层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第1层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第2层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第3层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第4层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第5层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第6层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第7层_任意_任意_0 +日线_D2单K趋势N20_BS辅助V230506_第8层_任意_任意_0 + """.strip().replace(' ', '').split('\n') + candidate_signals = list(set(candidate_signals)) + oop = OpensOptimize(symbols=symbols, files_position=files_position, task_name=task_name, + candidate_signals=candidate_signals, read_bars=get_raw_bars, + results_path=results_path, signals_module_name='czsc.signals', + bar_sdt='20160101', bar_edt='20230101', sdt='20170101') + oop.execute(n_jobs=10) + + +def run_exits_optim(): + symbols = get_symbols('期货主力')[:10] + results_path = r"D:\QMT投研\CCI出场优化结果B" + files_position = [ + r"D:\QMT投研\基础策略V230707\A股日线CCI空头基准.json", + r"D:\QMT投研\基础策略V230707\A股日线CCI多头基准.json", + ] + task_name = '加速上涨优化多头' + + candidate_events = [ + {'operate': '平多', + 'factors': [ + {'name': '加速上涨', + 'signals_all': [ + "日线_D2N5T500_绝对动量V230227_超强_任意_任意_0", + ]} + ]}, + {'operate': '平多', + 'factors': [ + {'name': '加速上涨', + 'signals_all': [ + "日线_D2N8T600_绝对动量V230227_超强_任意_任意_0", + ]} + ]}, + {'operate': '平多', + 'factors': [ + {'name': '加速上涨', + 'signals_all': [ + "日线_D2N10T800_绝对动量V230227_超强_任意_任意_0", + ]} + ]}, + + {'operate': '平空', + 'factors': [ + {'name': '加速下跌', + 'signals_all': [ + "日线_D2N5T300_绝对动量V230227_超弱_任意_任意_0", + ]} + ]}, + {'operate': '平空', + 'factors': [ + {'name': '加速下跌', + 'signals_all': [ + "日线_D2N8T500_绝对动量V230227_超弱_任意_任意_0", + ]} + ]}, + {'operate': '平空', + 'factors': [ + {'name': '加速下跌', + 'signals_all': [ + "日线_D2N10T800_绝对动量V230227_超弱_任意_任意_0", + ]} + ]}, + ] + + eop = ExitsOptimize(symbols=symbols, files_position=files_position, task_name=task_name, + candidate_events=candidate_events, read_bars=get_raw_bars, + results_path=results_path, signals_module_name='czsc.signals', + bar_sdt='20160101', bar_edt='20230101', sdt='20170101') + eop.execute(n_jobs=10) + + +if __name__ == '__main__': + run_opens_optim() + run_exits_optim() From ee8134d93bc8d450dbb01099e05aa5fd0afa6cdf Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Sun, 24 Sep 2023 21:50:48 +0800 Subject: [PATCH 20/22] =?UTF-8?q?0.9.29=20=E6=96=B0=E5=A2=9E=E7=AC=94?= =?UTF-8?q?=E7=89=B9=E5=BE=81=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- czsc/__init__.py | 5 ++++ czsc/utils/bi_info.py | 62 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 czsc/utils/bi_info.py diff --git a/czsc/__init__.py b/czsc/__init__.py index dd62d6768..19b3a844c 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -83,6 +83,11 @@ show_daily_return, ) +from czsc.utils.bi_info import ( + calculate_bi_info, + symbols_bi_infos, +) + __version__ = "0.9.29" __author__ = "zengbin93" diff --git a/czsc/utils/bi_info.py b/czsc/utils/bi_info.py new file mode 100644 index 000000000..a9976dd0f --- /dev/null +++ b/czsc/utils/bi_info.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +""" +author: zengbin93 +email: zeng_bin8888@163.com +create_dt: 2023/9/24 12:39 +describe: K线的笔特征计算 +""" +import pandas as pd +from tqdm import tqdm +from typing import List +from czsc.objects import RawBar +from czsc.analyze import CZSC + + +def calculate_bi_info(bars: List[RawBar], **kwargs) -> pd.DataFrame: + """计算笔的特征 + + :param bars: 原始K线数据 + :return: 笔的特征 + """ + c = CZSC(bars, max_bi_num=kwargs.get("max_bi_num", 10000)) + + res = [ + { + "symbol": c.symbol, + "sdt": bi.fx_a.dt, + "edt": bi.fx_b.dt, + "方向": bi.direction.value, + "长度": bi.length, + "分型数": len(bi.fxs), + "斜边长度": bi.hypotenuse, + "斜边角度": bi.angle, + "涨跌幅": (bi.fx_b.fx / bi.fx_a.fx - 1) * 10000, + "R2": bi.rsq, + } for bi in c.bi_list + ] + _df = pd.DataFrame(res) + _df['未来第一笔涨跌幅'] = _df['涨跌幅'].shift(-1) + _df['未来第二笔涨跌幅'] = _df['涨跌幅'].shift(-2) + return _df + + +def symbols_bi_infos(symbols, read_bars, freq='5分钟', sdt='20130101', edt='20190101', **kwargs) -> pd.DataFrame: + """计算多个标的的笔特征 + + :param symbols: 品种代码列表 + :param read_bars: 读取K线数据的函数,要求返回 RawBar 对象列表 + :param freq: K线周期, defaults to '5分钟' + :param sdt: 开始时间, defaults to '20130101' + :param edt: 结束时间, defaults to '20190101' + :return: 笔的特征 + """ + bis = [] + for symbol in tqdm(symbols, desc="计算笔的特征"): + try: + bars = read_bars(symbol=symbol, freq=freq, sdt=sdt, edt=edt, fq='后复权') + dfr = calculate_bi_info(bars) + bis.append(dfr) + except Exception as e: + print(f"{symbol} 计算失败: {e}") + dfb = pd.concat(bis, ignore_index=True) + return dfb From e2f305d50d23e3a42fa6b837bc5ecdf194c70de1 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Sun, 24 Sep 2023 21:59:54 +0800 Subject: [PATCH 21/22] =?UTF-8?q?0.9.29=20=E6=96=B0=E5=A2=9E=20cxt=5Fbi=5F?= =?UTF-8?q?trend=5FV230913=20=E4=BF=A1=E5=8F=B7=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- czsc/signals/__init__.py | 1 + czsc/signals/cxt.py | 130 ++++++++++++++++++++++----- examples/signals_dev/signal_match.py | 6 +- 3 files changed, 110 insertions(+), 27 deletions(-) diff --git a/czsc/signals/__init__.py b/czsc/signals/__init__.py index a99e74cf7..28417dbde 100644 --- a/czsc/signals/__init__.py +++ b/czsc/signals/__init__.py @@ -45,6 +45,7 @@ cxt_bi_end_V230815, cxt_bi_stop_V230815, cxt_bi_trend_V230824, + cxt_bi_trend_V230913, ) diff --git a/czsc/signals/cxt.py b/czsc/signals/cxt.py index 5ee8a56fd..32e138bb1 100644 --- a/czsc/signals/cxt.py +++ b/czsc/signals/cxt.py @@ -16,7 +16,7 @@ from czsc.signals.tas import update_ma_cache, update_macd_cache from collections import OrderedDict from deprecated import deprecated - +from sklearn.linear_model import LinearRegression def cxt_bi_base_V230228(c: CZSC, **kwargs) -> OrderedDict: @@ -258,7 +258,7 @@ def cxt_zhong_shu_gong_zhen_V221221(cat: CzscSignals, freq1='日线', freq2='60 if not cat.kas or freq1 not in cat.kas or freq2 not in cat.kas: return create_single_signal(k1=k1, k2=k2, k3=k3, v1="其他") - + max_freq: CZSC = cat.kas[freq1] min_freq: CZSC = cat.kas[freq2] symbol = cat.symbol @@ -1166,7 +1166,7 @@ def cxt_bi_end_V230618(c: CZSC, **kwargs) -> OrderedDict: 类似 cxt_third_bs_V230318 信号,但增加了笔内有无小级别中枢的判断。用k线重叠来近似小级别中枢的判断 :param c: CZSC对象 - :param kwargs: + :param kwargs: - di: int, 默认1,表示取倒数第几笔 - max_overlap: int, 默认3,表示笔内最多允许有几个信号重叠 @@ -1268,7 +1268,7 @@ def cxt_three_bi_V230618(c: CZSC, **kwargs) -> OrderedDict: :param kwargs: - di: 倒数第几笔 - + :return: 信号识别结果 """ di = int(kwargs.get("di", 1)) @@ -1335,7 +1335,7 @@ def cxt_five_bi_V230619(c: CZSC, **kwargs) -> OrderedDict: :param kwargs: - di: 倒数第几笔 - + :return: 信号识别结果 """ di = int(kwargs.get("di", 1)) @@ -1424,7 +1424,7 @@ def cxt_seven_bi_V230620(c: CZSC, **kwargs) -> OrderedDict: :param kwargs: - di: 倒数第几笔 - + :return: 信号识别结果 """ di = int(kwargs.get("di", 1)) @@ -1533,7 +1533,7 @@ def cxt_nine_bi_V230621(c: CZSC, **kwargs) -> OrderedDict: :param kwargs: - di: 倒数第几笔 - + :return: 信号识别结果 """ di = int(kwargs.get("di", 1)) @@ -1595,7 +1595,7 @@ def cxt_nine_bi_V230621(c: CZSC, **kwargs) -> OrderedDict: > min([x.high for x in [bi3, bi5, bi7]]) \ > max([x.low for x in [bi3, bi5, bi7]]) > bi1.low == min_low: return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类三买B') - + if min_low == bi5.low and max_high == bi1.high and bi4.high < bi2.low: # 前五笔构成向下类趋势 zd = max([x.low for x in [bi5, bi7]]) zg = min([x.high for x in [bi5, bi7]]) @@ -1626,18 +1626,18 @@ def cxt_nine_bi_V230621(c: CZSC, **kwargs) -> OrderedDict: if bi8.low > min(bi2.high, bi4.high, bi6.high) > max(bi2.low, bi4.low, bi6.low) \ and bi9.power < bi7.power: return create_single_signal(k1=k1, k2=k2, k3=k3, v1='aAbcd式类一卖') - + # ABC式类一卖 if bi3.high > bi1.high and bi7.low < bi9.low \ and min(bi4.high, bi6.high) > max(bi4.low, bi6.low) \ and (bi3.high - bi1.low) > (bi9.high - bi7.low): return create_single_signal(k1=k1, k2=k2, k3=k3, v1='ABC式类一卖') - + # 类趋势一卖 if bi8.low > bi6.high > bi6.low > bi4.high > bi4.low > bi2.high \ and bi9.power < max([bi1.power, bi3.power, bi5.power, bi7.power]): return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类趋势一卖') - + # 九笔三卖 if max_high == bi1.high and min_low == bi9.low \ and bi9.high < max([x.low for x in [bi3, bi5, bi7]]) < min([x.high for x in [bi3, bi5, bi7]]): @@ -1678,7 +1678,7 @@ def cxt_eleven_bi_V230622(c: CZSC, **kwargs) -> OrderedDict: :param kwargs: - di: 倒数第几笔 - + :return: 信号识别结果 """ di = int(kwargs.get("di", 1)) @@ -1758,12 +1758,12 @@ def cxt_eleven_bi_V230622(c: CZSC, **kwargs) -> OrderedDict: if bi1.high < bi3.high and min(bi4.high, bi6.high, bi8.high) > max(bi4.low, bi6.low, bi8.low) \ and bi9.low < bi11.low and bi3.high - bi1.low > bi11.high - bi9.low: return create_single_signal(k1=k1, k2=k2, k3=k3, v1='A3B5C3式类一卖') - + # 类二卖:1~9构成类趋势,11不创新高 if max_high == bi9.high > bi8.low > bi6.high > bi6.low > bi4.high > bi4.low > bi2.high > bi1.low == min_low \ and bi11.high < bi9.high: return create_single_signal(k1=k1, k2=k2, k3=k3, v1='类二卖') - + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) @@ -1865,7 +1865,7 @@ def cxt_intraday_V230701(cat: CzscSignals, **kwargs) -> OrderedDict: v1 = "其他" if not cat.kas or freq1 not in cat.kas.keys() or freq2 not in cat.kas.keys(): return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - + c1, c2 = cat.kas[freq1], cat.kas[freq2] day = c2.bars_raw[-di].dt.date() bars = [x for x in c1.bars_raw if x.dt.date() == day] @@ -1877,13 +1877,13 @@ def cxt_intraday_V230701(cat: CzscSignals, **kwargs) -> OrderedDict: for b1, b2, b3 in zip(bars[:-2], bars[1:-1], bars[2:]): if min(b1.high, b2.high, b3.high) >= max(b1.low, b2.low, b3.low): zs_list.append([b1, b2, b3]) - + _dir = "上涨" if bars[-1].close > bars[0].open else "下跌" if not zs_list: v1 = f"无中枢{_dir}" return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - + # 双中枢的情况,有一根K线的 high low 与前后两个中枢没有重叠 if len(zs_list) >= 2: zs1, zs2 = zs_list[0], zs_list[-1] @@ -1892,11 +1892,11 @@ def cxt_intraday_V230701(cat: CzscSignals, **kwargs) -> OrderedDict: if _dir == "上涨" and zs1_high < zs2_low: # type: ignore v1 = f"双中枢{_dir}" return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - + if _dir == "下跌" and zs1_low > zs2_high: # type: ignore v1 = f"双中枢{_dir}" return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - + # 单中枢的情况,前三根K线出现高点:弱平衡市,前三根K线出现低点:强平衡市,否则:转折平衡市 high_first = max(bars[0].high, bars[1].high, bars[2].high) == max([x.high for x in bars]) low_first = min(bars[0].low, bars[1].low, bars[2].low) == min([x.low for x in bars]) @@ -1906,7 +1906,7 @@ def cxt_intraday_V230701(cat: CzscSignals, **kwargs) -> OrderedDict: v1 = "强平衡市" else: v1 = "转折平衡市" - + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) @@ -1957,7 +1957,7 @@ def cxt_ubi_end_V230816(c: CZSC, **kwargs) -> OrderedDict: if ubi['raw_bars'][-1].high > cur_hfx.high: v1 = '新高' v2 = f"第{cnt + 1}次" - + if ubi['direction'] == Direction.Down: fxs = [x for x in fxs if x.mark == Mark.D] cnt = 1 @@ -1997,7 +1997,7 @@ def cxt_bi_end_V230815(c: CZSC, **kwargs) -> OrderedDict: v1 = '其他' if len(c.bi_list) < 5 or len(c.bars_ubi) >= 5: return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - + bi, last_bar = c.bi_list[-1], c.bars_ubi[-1] if bi.direction == Direction.Up and last_bar.low < bi.low: v1 = '向下' @@ -2023,7 +2023,7 @@ def cxt_bi_stop_V230815(c: CZSC, **kwargs) -> OrderedDict: - Signal('15分钟_距离50BP_止损V230815_向上_阈值外_任意_0') :param c: CZSC对象 - :param kwargs: + :param kwargs: - th: 止损距离阈值,单位为BP, 默认为50BP, 即0.5% @@ -2035,7 +2035,7 @@ def cxt_bi_stop_V230815(c: CZSC, **kwargs) -> OrderedDict: v1, v2 = '其他', '其他' if len(c.bi_list) < 5: return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - + bi, last_bar = c.bi_list[-1], c.bars_ubi[-1] if bi.direction == Direction.Up: v1 = '向下' @@ -2096,3 +2096,85 @@ def cxt_bi_trend_V230824(c: CZSC, **kwargs) -> OrderedDict: v1 = "横盘" return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) + +def cxt_bi_trend_V230913(c: CZSC, **kwargs) -> OrderedDict: + """辅助判断股票通道信号,贡献者:马鸣 + + 参数模板:"{freq}_D{di}N{n}笔趋势_高低点辅助判断V230913" + + **信号逻辑:** + + 1. 倒数di笔之间的高低点形成趋势线,根据股价的当前位置,推断趋势强弱 + + **信号列表:** + + - Signal('日线_D3N1笔趋势_高低点辅助判断V230913_下降趋势_超强_任意_0') + - Signal('日线_D3N1笔趋势_高低点辅助判断V230913_观望_末笔延伸_任意_0') + - Signal('日线_D3N1笔趋势_高低点辅助判断V230913_上升趋势_强_任意_0') + - Signal('日线_D3N1笔趋势_高低点辅助判断V230913_观望_趋势线交叉_任意_0') + - Signal('日线_D3N1笔趋势_高低点辅助判断V230913_下降趋势_强_任意_0') + - Signal('日线_D3N1笔趋势_高低点辅助判断V230913_上升趋势_超强_任意_0') + + :param c: CZSC对象 + :param kwargs: 参数字典 + + -:param di: 倒数di笔 + -:param n: 倒数第n根K线 + + :return: 信号识别结果 + """ + di = int(kwargs.get("di", 4)) + n = int(kwargs.get("n", 1)) + freq = c.freq.value + k1, k2, k3 = f"{freq}_D{di}N{n}笔趋势_高低点辅助判断V230913".split('_') + v1 = "其他" + if len(c.bi_list) <= di + 2 or len(c.bars_ubi) <= n + 1: + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) + + up_trend_price = np.array([x.high for x in c.bi_list if x.direction == Direction.Up][-di:]) + up_trend_time = np.array([x.edt.timestamp() for x in c.bi_list if x.direction == Direction.Up][-di:]).reshape(-1, 1) + + down_trend_price = np.array([x.low for x in c.bi_list if x.direction == Direction.Down][-di:]) + down_trend_time = np.array([x.edt.timestamp() for x in c.bi_list if x.direction == Direction.Down][-di:]).reshape(-1, 1) + + model_up = LinearRegression() + model_down = LinearRegression() + model_up.fit(up_trend_time, up_trend_price) + model_down.fit(down_trend_time, down_trend_price) + + new_bar_data = np.array([c.bars_ubi[-n].dt.timestamp()]).reshape(-1, 1) + pre_up_price = model_up.predict(new_bar_data) + pre_down_price = model_down.predict(new_bar_data) + pre_mid_price = (pre_up_price + pre_down_price) / 2 + + if pre_up_price <= pre_down_price: + v1 = "观望" + v2 = '趋势线交叉' + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) + + if len(c.bars_ubi) >= 5: + v1 = "观望" + v2 = '末笔延伸' + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) + + if c.bars_raw[-n].close >= pre_up_price: + v1 = '上升趋势' + v2 = '超强' + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) + + elif pre_mid_price < c.bars_raw[-n].close < pre_up_price: + v1 = '上升趋势' + v2 = '强' + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) + + elif pre_down_price < c.bars_raw[-n].close < pre_mid_price: + v1 = '下降趋势' + v2 = '强' + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) + + elif c.bars_raw[-n].close <= pre_down_price: + v1 = '下降趋势' + v2 = '超强' + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) + + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) diff --git a/examples/signals_dev/signal_match.py b/examples/signals_dev/signal_match.py index 0d595f4db..9132efa6d 100644 --- a/examples/signals_dev/signal_match.py +++ b/examples/signals_dev/signal_match.py @@ -3,7 +3,7 @@ author: zengbin93 email: zeng_bin8888@163.com create_dt: 2023/3/30 12:08 -describe: +describe: """ import os import sys @@ -44,8 +44,8 @@ conf = sp.parse(signals_seq) parsed_name = {x['name'] for x in conf} print(f"total signal functions: {len(sp.sig_name_map)}; parsed: {len(parsed_name)}") - # total signal functions: 182; parsed: 182 - + # total signal functions: 197; parsed: 197 + # 测试信号配置生成信号 from czsc import generate_czsc_signals, get_signals_freqs, get_signals_config from test.test_analyze import read_1min From 0c3295b62714932914bc98ef4b55cd2a6c76b9c1 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Sun, 24 Sep 2023 22:02:46 +0800 Subject: [PATCH 22/22] 0.9.29 update --- czsc/traders/rwc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/czsc/traders/rwc.py b/czsc/traders/rwc.py index 197214188..d6e884d82 100644 --- a/czsc/traders/rwc.py +++ b/czsc/traders/rwc.py @@ -19,7 +19,7 @@ class RedisWeightsClient: def __init__(self, strategy_name, redis_url, **kwargs): """ - :param name: str, 策略名 + :param strategy_name: str, 策略名 :param redis_url: str, redis连接字符串 For example:: @@ -133,7 +133,7 @@ def __heartbeat(self): key = f'{self.heartbeat_prefix}:{self.strategy_name}' try: self.heartbeat_client.set(key, datetime.now().strftime('%Y-%m-%d %H:%M:%S')) - except: + except Exception: continue time.sleep(15)