Skip to content

Commit

Permalink
0.9.29 新增交易日历
Browse files Browse the repository at this point in the history
  • Loading branch information
zengbin93 committed Sep 10, 2023
1 parent cd049b9 commit 65f70d4
Show file tree
Hide file tree
Showing 16 changed files with 113 additions and 65 deletions.
1 change: 0 additions & 1 deletion czsc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
stock_holds_performance,
DummyBacktest,
SignalsParser,
get_signals_by_conf,
get_signals_config,
get_signals_freqs,
WeightBacktest,
Expand Down
4 changes: 2 additions & 2 deletions czsc/connectors/qmt_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 0 additions & 2 deletions czsc/connectors/research.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


2 changes: 1 addition & 1 deletion czsc/fsa/im.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: 图片路径
Expand Down
4 changes: 2 additions & 2 deletions czsc/fsa/spreed_sheets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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}"
Expand Down
28 changes: 14 additions & 14 deletions czsc/sensors/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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线数据,
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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()}")
Expand All @@ -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;数据样例:
=================== ========= ======= ======= ======= ======= ======== ========= ======== =============
Expand All @@ -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,每期允许变动的数量
Expand Down Expand Up @@ -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
Expand All @@ -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]
Expand All @@ -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)}个品种")

Expand All @@ -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
1 change: 0 additions & 1 deletion czsc/sensors/plates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

6 changes: 2 additions & 4 deletions czsc/sensors/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand Down
3 changes: 1 addition & 2 deletions czsc/traders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
32 changes: 4 additions & 28 deletions czsc/traders/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
class CzscSignals:
"""缠中说禅技术分析理论之多级别信号计算"""

def __init__(self, bg: BarGenerator = None, **kwargs):
def __init__(self, bg: Optional[BarGenerator] = None, **kwargs):
"""
:param bg: K线合成器
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 生成信号
Expand Down Expand Up @@ -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: 仓位名称
Expand Down
4 changes: 1 addition & 3 deletions czsc/traders/performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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


5 changes: 2 additions & 3 deletions czsc/traders/sig_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
author: zengbin93
email: [email protected]
create_dt: 2023/3/29 10:04
describe:
describe:
"""
import re
from loguru import logger
Expand Down Expand Up @@ -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'])

Expand Down Expand Up @@ -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]

5 changes: 3 additions & 2 deletions czsc/traders/weight_backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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等
Expand Down Expand Up @@ -109,6 +109,7 @@ def get_symbol_daily(self, symbol):
:param symbol: str,合约代码
:return: pd.DataFrame,品种每日收益率,
columns = ['date', 'symbol', 'edge', 'return', 'cost']
其中
date 为交易日,
Expand Down Expand Up @@ -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 = []
Expand Down
63 changes: 63 additions & 0 deletions czsc/utils/calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""
author: zengbin93
email: [email protected]
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']
Binary file added czsc/utils/china_calendar.feather
Binary file not shown.
Loading

0 comments on commit 65f70d4

Please sign in to comment.