Skip to content

Commit

Permalink
V0.9.40 更新一批代码 (waditu#184)
Browse files Browse the repository at this point in the history
* 0.9.40 first commit

* 0.9.40 update

* 0.9.40 update

* 0.9.40 update

* 0.9.40 新增 rolling_compare

* 解决虚拟货币市场非一分钟周期k线生成的bug (waditu#183)

* 0.9.40 update

* 0.9.40 update

* 0.9.40 fix test

* 0.9.40 update

* 0.9.40 update calendar

* 0.9.40 update

* update

---------

Co-authored-by: feng zhu <[email protected]>
  • Loading branch information
zengbin93 and zhufeng7 authored Jan 14, 2024
1 parent c55a00b commit 912aeb7
Show file tree
Hide file tree
Showing 14 changed files with 150 additions and 66 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Python package

on:
push:
branches: [ master, V0.9.39 ]
branches: [ master, V0.9.40 ]
pull_request:
branches: [ master ]

Expand All @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, '3.10', '3.11']
python-version: [3.7, 3.8, 3.9, '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v2
Expand Down
18 changes: 4 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
[![PyPI](https://img.shields.io/pypi/v/czsc.svg)](https://pypi.org/project/czsc/)
[![Documentation Status](https://readthedocs.org/projects/czsc/badge/?version=latest)](https://czsc.readthedocs.io/en/latest/?badge=latest)

**[API文档](https://czsc.readthedocs.io/en/latest/modules.html)** |
**[项目文档](https://s0cqcxuy3p.feishu.cn/wiki/wikcn3gB1MKl3ClpLnboHM1QgKf)** |
**[投研数据共享](https://s0cqcxuy3p.feishu.cn/wiki/wikcnzuPawXtBB7Cj7mqlYZxpDh)** |
**[API文档](https://czsc.readthedocs.io/en/latest/modules.html)** |
**[项目文档](https://s0cqcxuy3p.feishu.cn/wiki/wikcn3gB1MKl3ClpLnboHM1QgKf)** |
**[投研数据共享](https://s0cqcxuy3p.feishu.cn/wiki/wikcnzuPawXtBB7Cj7mqlYZxpDh)** |
**[信号函数编写规范](https://s0cqcxuy3p.feishu.cn/wiki/wikcnCFLLTNGbr2THqo7KtWfBkd)**

>源于[缠中说缠博客](http://blog.sina.com.cn/chzhshch),原始博客中的内容不太完整,且没有评论,以下是网友整理的原文备份
Expand All @@ -36,7 +36,7 @@

## 安装使用

**注意:** python 版本必须大于等于 3.7
**注意:** python 版本必须大于等于 3.7

直接从github安装:
```
Expand Down Expand Up @@ -67,16 +67,6 @@ pip install czsc -U -i https://pypi.python.org/simple
* 如果你发现了项目中的 Bug,可以先读一下《[如何有效地报告 Bug](https://www.chiark.greenend.org.uk/~sgtatham/bugs-cn.html)》,然后在 [issues](https://github.com/waditu/czsc/issues) 中报告 Bug


## 使用案例

>案例中主要使用了 Tushare 的数据,开通相应的数据权限可以[点击联系](https://tushare.pro/document/2?doc_id=244),备注:**CZSC用户**,1500元可以开通CZSC项目目前用到的全部数据权限。
>掘金终端主要用于交易策略的实盘跟踪,[点击了解](https://www.myquant.cn/)
* `examples/ts_plates_sensor.py` 同花顺概念板块轮动策略回测
* `examples/ts_check_signal_acc.py` 验证信号计算的准确性,信号是否符合定义
* `examples/ts_stocks_sensors.py` 日线选股策略回测


## 原文整理

* [缠中说禅重新编排版《论语》(整理版)](https://blog.csdn.net/baidu_25764509/article/details/109517775)
Expand Down
6 changes: 4 additions & 2 deletions czsc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
# streamlit 量化分析组件
from czsc.utils.st_components import (
show_daily_return,
show_monthly_return,
show_correlation,
show_sectional_ic,
show_factor_returns,
Expand All @@ -114,13 +115,14 @@
rolling_rank,
rolling_norm,
rolling_qcut,
rolling_compare,
find_most_similarity,
)

__version__ = "0.9.39"
__version__ = "0.9.40"
__author__ = "zengbin93"
__email__ = "[email protected]"
__date__ = "20231212"
__date__ = "20231218"


def welcome():
Expand Down
2 changes: 1 addition & 1 deletion czsc/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ def update(self, bar: RawBar):
# 当前 bar 是上一根 bar 的时间延伸
self.bars_raw[-1] = bar
last_bars = self.bars_ubi.pop(-1).raw_bars
assert bar.dt == last_bars[-1].dt
assert bar.dt == last_bars[-1].dt, f"{bar.dt} != {last_bars[-1].dt},时间错位"
last_bars[-1] = bar

# 去除包含关系
Expand Down
30 changes: 7 additions & 23 deletions czsc/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,31 +649,15 @@ def is_match(self, s: dict):
4. 最后判断因子是否满足,顺序遍历因子列表,找到第一个满足的因子就退出,并返回 True 和该因子的名称,表示事件满足。
5. 如果遍历完所有因子都没有找到满足的因子,则返回 False,表示事件不满足。
"""
# 首先判断 event 层面的信号是否得到满足
if self.signals_not:
# 满足任意一个,直接返回 False
for signal in self.signals_not:
if signal.is_match(s):
return False, None
if self.signals_not and any(signal.is_match(s) for signal in self.signals_not):
return False, None

if self.signals_all:
# 任意一个不满足,直接返回 False
for signal in self.signals_all:
if not signal.is_match(s):
return False, None
if self.signals_all and not all(signal.is_match(s) for signal in self.signals_all):
return False, None

if self.signals_any and not any(signal.is_match(s) for signal in self.signals_any):
return False, None

if self.signals_any:
one_match = False
for signal in self.signals_any:
if signal.is_match(s):
one_match = True
break
# 一个都不满足,直接返回 False
if not one_match:
return False, None

# 判断因子是否满足,顺序遍历,找到第一个满足的因子就退出
# 因子放入事件中时,建议因子列表按关注度从高到低排序
for factor in self.factors:
if factor.is_match(s):
return True, factor.name
Expand Down
6 changes: 2 additions & 4 deletions czsc/traders/rwc.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,12 +344,11 @@ def get_hist_weights(self, symbol, sdt, edt) -> pd.DataFrame:
dfw = dfw.sort_values('dt').reset_index(drop=True)
return dfw

def get_all_weights(self, sdt=None, edt=None, ignore_zero=True) -> pd.DataFrame:
def get_all_weights(self, sdt=None, edt=None, **kwargs) -> pd.DataFrame:
"""获取所有权重数据
:param sdt: str, 开始时间, eg: 20210924 10:19:00
:param edt: str, 结束时间, eg: 20220924 10:19:00
:param ignore_zero: boolean, 是否忽略权重为0的品种
:return: pd.DataFrame
"""
lua_script = """
Expand All @@ -374,8 +373,7 @@ def get_all_weights(self, sdt=None, edt=None, ignore_zero=True) -> pd.DataFrame:

df1 = pd.pivot_table(df, index='dt', columns='symbol', values='weight').sort_index().ffill().fillna(0)
df1 = pd.melt(df1.reset_index(), id_vars='dt', value_vars=df1.columns, value_name='weight') # type: ignore
if ignore_zero:
df1 = df1[df1['weight'] != 0].reset_index(drop=True)

if sdt:
df1 = df1[df1['dt'] >= pd.to_datetime(sdt)].reset_index(drop=True)
if edt:
Expand Down
8 changes: 6 additions & 2 deletions czsc/utils/bar_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,12 @@ def freq_end_time(dt: datetime, freq: Union[Freq, AnyStr], market="A股") -> dat
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))
h, m = map(int, freq_edt_map[key][hm].split(":"))
edt = dt.replace(hour=h, minute=m)

if h == m == 0 and freq != Freq.F1 and hm != "00:00":
edt += timedelta(days=1)

return edt

# if not ("15:00" > hm > "09:00") and market == "期货":
Expand Down
Binary file modified czsc/utils/china_calendar.feather
Binary file not shown.
2 changes: 2 additions & 0 deletions czsc/utils/data_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ def get_url_token(url):
hash_key = hashlib.md5(str(url).encode('utf-8')).hexdigest()
file_token = Path("~").expanduser() / f"{hash_key}.txt"
if file_token.exists():
logger.info(f"从 {file_token} 读取 {url} 的访问凭证码")
return open(file_token, 'r', encoding='utf-8').read()

logger.warning(f"请设置 {url} 的访问凭证码,如果没有请联系管理员申请")
token = input(f"请输入 {url} 的访问凭证码(token):")
if token:
Expand Down
52 changes: 52 additions & 0 deletions czsc/utils/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pandas as pd
from loguru import logger
from sklearn.preprocessing import scale
from sklearn.linear_model import LinearRegression


def normalize_feature(df, x_col, **kwargs):
Expand Down Expand Up @@ -240,6 +241,57 @@ def __qcut_func(x):
df[new_col] = df[new_col].fillna(-1)


def rolling_compare(df, col1, col2, new_col=None, n=None, **kwargs):
"""计算序列的滚动归一化值
:param df: pd.DataFrame
待计算的数据
:param col1: str
第一个列名
:param col2: str
第二个列名
:param n: int
滚动窗口大小, 默认为None, 表示计算 expanding ,否则计算 rolling
:param new_col: str
新列名,默认为 None, 表示使用 f'{col}_norm' 作为新列名
:param kwargs:
min_periods: int
最小计算周期
"""
min_periods = kwargs.get('min_periods', 2)
new_col = new_col if new_col else f'compare_{col1}_{col2}'
method = kwargs.get('method', 'sub')
assert method in ['sub', 'divide', 'lr_intercept', 'lr_coef'], "method 必须为 sub, divide, lr_intercept, lr_coef 中的一种"

for i in range(len(df)):
dfi = df.loc[:i, [col1, col2]] if n is None else df.loc[i - n + 1:i, [col1, col2]]
dfi = dfi.copy()
if i < min_periods:
df.loc[i, new_col] = 0
continue

if method == 'sub':
df.loc[i, new_col] = dfi[col1].sub(dfi[col2]).mean()

elif method == 'divide':
df.loc[i, new_col] = dfi[col1].divide(dfi[col2]).mean()

elif method == 'lr_intercept':
x = dfi[col2].values.reshape(-1, 1)
y = dfi[col1].values.reshape(-1, 1)
reg = LinearRegression().fit(x, y)
df.loc[i, new_col] = reg.intercept_[0]

elif method == 'lr_coef':
x = dfi[col2].values.reshape(-1, 1)
y = dfi[col1].values.reshape(-1, 1)
reg = LinearRegression().fit(x, y)
df.loc[i, new_col] = reg.coef_[0][0]

else:
raise ValueError(f"method {method} not support")


def find_most_similarity(vector: pd.Series, matrix: pd.DataFrame, n=10, metric='cosine', **kwargs):
"""寻找向量在矩阵中最相似的n个向量
Expand Down
10 changes: 7 additions & 3 deletions czsc/utils/oss.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def upload(self, filepath: str, oss_key: str, replace: bool = False) -> bool:
logger.error(f"Upload {filepath} to {oss_key} failed: {result.status}, {result.request_id}")
return False

def download(self, oss_key: str, filepath: str) -> bool:
def download(self, oss_key: str, filepath: str, replace: bool = False) -> bool:
"""
从OSS下载文件。
Expand All @@ -70,6 +70,10 @@ def download(self, oss_key: str, filepath: str) -> bool:
if not os.path.exists(path):
os.makedirs(path)

if not replace and os.path.exists(filepath):
logger.info(f"{filepath} exists in the local. Set replace=True to overwrite.")
return False

result = self.bucket.get_object_to_file(oss_key, filepath)
if result.status == 200:
logger.info(f"Download {oss_key} to {filepath} successfully.")
Expand Down Expand Up @@ -150,7 +154,7 @@ def batch_upload(self, filepaths: List[str], oss_keys: List[str], replace: bool
for filepath, oss_key in tqdm(zip(filepaths, oss_keys), total=len(filepaths), desc="Uploading"):
executor.submit(self.upload, filepath, oss_key, replace)

def batch_download(self, oss_keys: List[str], local_paths: List[str], threads: int = 5):
def batch_download(self, oss_keys: List[str], local_paths: List[str], replace: bool = False, threads: int = 5):
"""
批量从OSS下载文件。
Expand All @@ -161,7 +165,7 @@ def batch_download(self, oss_keys: List[str], local_paths: List[str], threads: i
assert len(oss_keys) == len(local_paths), "The length of oss_keys and local_paths must be the same."
with ThreadPoolExecutor(max_workers=threads) as executor:
for oss_key, local_path in zip(oss_keys, local_paths):
executor.submit(self.download, oss_key, local_path)
executor.submit(self.download, oss_key, local_path, replace)

def multipart_upload(self, filepath: str, oss_key: str):
"""
Expand Down
40 changes: 30 additions & 10 deletions czsc/utils/st_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@


def show_daily_return(df, **kwargs):
"""用 streamlit 展示日收益"""
"""用 streamlit 展示日收益
:param df: pd.DataFrame,数据源
:param kwargs:
- title: str,标题
- stat_hold_days: bool,是否展示持有日绩效指标,默认为 True
- legend_only_cols: list,仅在图例中展示的列名
"""
assert df.index.dtype == 'datetime64[ns]', "index必须是datetime64[ns]类型, 请先使用 pd.to_datetime 进行转换"
df = df.copy().fillna(0)

Expand All @@ -33,11 +42,15 @@ def _stats(df_, type_='持有日'):
st.subheader(title)
st.divider()

col1, col2 = st.columns([1, 1])
col1.write("交易日绩效指标")
col1.dataframe(_stats(df, type_='交易日'), use_container_width=True)
col2.write("持有日绩效指标")
col2.dataframe(_stats(df, type_='持有日'), use_container_width=True)
if kwargs.get("stat_hold_days", True):
col1, col2 = st.columns([1, 1])
col1.write("交易日绩效指标")
col1.dataframe(_stats(df, type_='交易日'), use_container_width=True)
col2.write("持有日绩效指标")
col2.dataframe(_stats(df, type_='持有日'), use_container_width=True)
else:
st.write("绩效指标")
st.dataframe(_stats(df, type_='交易日'), use_container_width=True)

df = df.cumsum()
fig = px.line(df, y=df.columns.to_list(), title="日收益累计曲线")
Expand All @@ -62,7 +75,7 @@ def show_monthly_return(df, ret_col='total', title="月度累计收益", **kwarg
monthly['year'] = monthly.index.year
monthly['month'] = monthly.index.month
monthly = monthly.pivot_table(index='year', columns='month', values=ret_col)
month_cols = [f"{x}月" for x in range(1, 13)]
month_cols = [f"{x}月" for x in monthly.columns]
monthly.columns = month_cols
monthly['年收益'] = monthly.sum(axis=1)
monthly = monthly.style.background_gradient(cmap='RdYlGn_r', axis=None, subset=month_cols).format('{:.2%}', na_rep='-')
Expand Down Expand Up @@ -267,6 +280,9 @@ def show_weight_backtest(dfw, **kwargs):
:param kwargs:
- fee: 单边手续费,单位为BP,默认为2BP
- digits: 权重小数位数,默认为2
- show_daily_detail: bool,是否展示每日收益详情,默认为 False
"""
fee = kwargs.get("fee", 2)
digits = kwargs.get("digits", 2)
Expand All @@ -275,9 +291,7 @@ def show_weight_backtest(dfw, **kwargs):
st.dataframe(dfw[dfw.isnull().sum(axis=1) > 0], use_container_width=True)
st.stop()

from czsc.traders.weight_backtest import WeightBacktest

wb = WeightBacktest(dfw, fee_rate=fee / 10000, digits=digits)
wb = czsc.WeightBacktest(dfw, fee_rate=fee / 10000, digits=digits)
stat = wb.results['绩效评价']

st.divider()
Expand All @@ -295,4 +309,10 @@ def show_weight_backtest(dfw, **kwargs):
dret = wb.results['品种等权日收益']
dret.index = pd.to_datetime(dret.index)
show_daily_return(dret, legend_only_cols=dfw['symbol'].unique().tolist())

if kwargs.get("show_daily_detail", False):
with st.expander("查看品种等权日收益详情", expanded=False):
df_ = wb.results['品种等权日收益'].copy()
st.dataframe(df_.style.background_gradient(cmap='RdYlGn_r').format("{:.2%}"), use_container_width=True)

return wb
6 changes: 1 addition & 5 deletions czsc/utils/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ def evaluate_pairs(pairs: pd.DataFrame, trade_dir: str = "多空") -> dict:
:return: 交易表现
"""
from czsc.objects import cal_break_even_point
assert trade_dir in ["多头", "空头", "多空"], "trade_dir 参数错误,可选值 ['多头', '空头', '多空']"

pairs = pairs.copy()

Expand All @@ -236,13 +237,8 @@ def evaluate_pairs(pairs: pd.DataFrame, trade_dir: str = "多空") -> dict:
"持仓K线数": 0,
}

if len(pairs) == 0:
return p

if trade_dir in ["多头", "空头"]:
pairs = pairs[pairs["交易方向"] == trade_dir]
else:
assert trade_dir == "多空", "trade_dir 参数错误,可选值 ['多头', '空头', '多空']"

if len(pairs) == 0:
return p
Expand Down
Loading

0 comments on commit 912aeb7

Please sign in to comment.