Skip to content

Commit a03fc86

Browse files
authored
V0.9.64 更新一批代码 (#224)
* 0.9.64 start coding * 0.9.64 新增两个 streamlit 组件 * Add show_returns_contribution Streamlit component for strategy returns analysis * 优化 show_returns_contribution 组件,增加图表标题和说明 * 0.9.64 新增两个组件 * 优化图表布局,修复函数名拼写错误,更新示例代码以展示因子值可视化 * Refactor seasonal effect component to quarterly effect and update example for improved outsample comparison * update * Add cumulative returns visualization and new trading time function; refactor trade price calculation * Add Streamlit example for incremental line chart updates with random data * 优化季度效果展示函数,更新统计信息的显示方式,调整图表布局以提升可读性 * 更新代码注释,优化函数参数类型,移除过时函数,增加新函数以获取分钟线数据 * 标记函数subtract_fee为过时,建议使用rs_czsc.WeightBacktest替代以支持扣费 * 更新README.md,突出项目贡献部分的链接,调整Python版本要求为3.8及以上,并新增使用案例部分以提供更多参考资料。 * 移除过时的cal_trade_price函数,并在相关模块中重新导入以保持功能完整性 * 修正测试任务中的路径,更新pytest命令以指向正确的测试目录;移除过时的bar_cross_ps_V221112函数;调整测试数据构造方式以提高可读性和准确性。 * 修正测试文件中的权重计算,将权重值从整数更改为浮点数;更新交易价格计算测试,调整相关断言以匹配新计算结果。
1 parent ddf55c1 commit a03fc86

25 files changed

+926
-402
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ name: Python package
55

66
on:
77
push:
8-
branches: [ master, 'V0.9.63' ]
8+
branches: [ master, 'V0.9.64' ]
99
pull_request:
1010
branches: [ master ]
1111

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
"120"
1818
],
1919
"typescript.locale": "zh-CN",
20-
"python.analysis.typeCheckingMode": "basic",
20+
"python.analysis.typeCheckingMode": "basic"
2121
}

.vscode/tasks.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "运行所有测试",
6+
"type": "shell",
7+
"command": "python -m pytest test -v",
8+
"group": {
9+
"kind": "test",
10+
"isDefault": true
11+
},
12+
"presentation": {
13+
"reveal": "always",
14+
"panel": "new"
15+
}
16+
},
17+
{
18+
"label": "运行当前测试文件",
19+
"type": "shell",
20+
"command": "python -m pytest ${file} -v",
21+
"group": "test",
22+
"presentation": {
23+
"reveal": "always",
24+
"panel": "new"
25+
}
26+
},
27+
{
28+
"label": "运行带覆盖率报告的测试",
29+
"type": "shell",
30+
"command": "python -m pytest test -v --cov=czsc --cov-report=html",
31+
"group": "test",
32+
"presentation": {
33+
"reveal": "always",
34+
"panel": "new"
35+
}
36+
}
37+
]
38+
}

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,17 @@
3737
3838
## 项目贡献
3939

40-
* [择时策略研究框架](https://s0cqcxuy3p.feishu.cn/wiki/wikcnhizrtIOQakwVcZLMKJNaib)
40+
* **[择时策略研究框架](https://s0cqcxuy3p.feishu.cn/wiki/wikcnhizrtIOQakwVcZLMKJNaib)**
4141
* 缠论的 `分型、笔` 的自动识别,详见 `czsc/analyze.py`
4242
* 定义并实现 `信号-因子-事件-交易` 量化交易逻辑体系,因子是信号的线性组合,事件是因子的同类合并,详见 `czsc/objects.py`
4343
* 定义并实现了若干信号函数,详见 `czsc/signals`
4444
* 缠论多级别联立决策分析交易,详见 `CzscTrader`
45-
* [Streamlit 量化研究组件库](https://s0cqcxuy3p.feishu.cn/wiki/AATuw5vN7iN9XbkVPuwcE186n9f)
45+
* **[Streamlit 量化研究组件库](https://s0cqcxuy3p.feishu.cn/wiki/AATuw5vN7iN9XbkVPuwcE186n9f)**
4646

4747

4848
## 安装使用
4949

50-
**注意:** python 版本必须大于等于 3.7
50+
**注意:** python 版本必须大于等于 3.8
5151

5252
直接从github安装:
5353
```
@@ -64,6 +64,13 @@ pip install git+https://github.com/waditu/[email protected] -U
6464
pip install czsc -U -i https://pypi.python.org/simple
6565
```
6666

67+
## 使用案例
68+
69+
1. [使用 tqsdk 进行期货交易](https://s0cqcxuy3p.feishu.cn/wiki/wikcn41lQIAJ1f8v41Dj5eAmrub)
70+
2. [CTA择时:缠论30分钟笔非多即空](https://s0cqcxuy3p.feishu.cn/wiki/YPlewoj70ikwxakPnOucTP8lnYg)
71+
3. [使用CTA研究UI页面进行策略研究](https://s0cqcxuy3p.feishu.cn/wiki/JWe3wo1VNiglO9kE999cGy8innh)
72+
73+
6774
## 使用前必看
6875

6976
* 目前的开发还在高频次的迭代中,对于已经在使用某个版本的用户,请谨慎更新,版本兼容性实在是太差,主要是因为当前还有太多考虑不完善的地方,我为此感到抱歉;

czsc/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@
8585
x_round,
8686
import_by_name,
8787
create_grid_params,
88-
cal_trade_price,
8988
update_bbars,
9089
update_tbars,
9190
update_nxb,
@@ -170,6 +169,12 @@
170169
show_df_describe,
171170
show_date_effect,
172171
show_weight_distribution,
172+
show_normality_check,
173+
show_outsample_by_dailys,
174+
show_returns_contribution,
175+
show_symbols_bench,
176+
show_quarterly_effect,
177+
show_cumulative_returns,
173178
)
174179

175180
from czsc.utils.bi_info import (
@@ -223,13 +228,14 @@
223228
dif_long_bear,
224229
tsf_type,
225230
limit_leverage,
231+
cal_trade_price,
226232
)
227233

228234

229-
__version__ = "0.9.63"
235+
__version__ = "0.9.64"
230236
__author__ = "zengbin93"
231237
__email__ = "[email protected]"
232-
__date__ = "20250101"
238+
__date__ = "20250224"
233239

234240

235241
def welcome():

czsc/connectors/cooperation.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -145,25 +145,6 @@ def get_raw_bars(symbol, freq, sdt, edt, fq="前复权", **kwargs):
145145
sdt = pd.to_datetime(sdt).strftime("%Y%m%d")
146146
edt = pd.to_datetime(edt).strftime("%Y%m%d")
147147

148-
if "SH" in symbol or "SZ" in symbol:
149-
fq_map = {"前复权": "qfq", "后复权": "hfq", "不复权": None}
150-
adj = fq_map.get(fq, None)
151-
152-
code, asset = symbol.split("#")
153-
154-
if freq.value.endswith("分钟"):
155-
df = dc.pro_bar(code=code, sdt=sdt, edt=edt, freq="min", adj=adj, asset=asset[0].lower(), v=2, ttl=ttl)
156-
df = df[~df["dt"].str.endswith("09:30:00")].reset_index(drop=True)
157-
df.rename(columns={"code": "symbol"}, inplace=True)
158-
df["dt"] = pd.to_datetime(df["dt"])
159-
return czsc.resample_bars(df, target_freq=freq, raw_bars=raw_bars, base_freq="1分钟")
160-
161-
else:
162-
df = dc.pro_bar(code=code, sdt=sdt, edt=edt, freq="day", adj=adj, asset=asset[0].lower(), v=2, ttl=ttl)
163-
df.rename(columns={"code": "symbol"}, inplace=True)
164-
df["dt"] = pd.to_datetime(df["dt"])
165-
return czsc.resample_bars(df, target_freq=freq, raw_bars=raw_bars)
166-
167148
if symbol.endswith("9001"):
168149
# https://s0cqcxuy3p.feishu.cn/wiki/WLGQwJLWQiWPCZkPV7Xc3L1engg
169150
if fq == "前复权":
@@ -203,6 +184,25 @@ def get_raw_bars(symbol, freq, sdt, edt, fq="前复权", **kwargs):
203184
df["dt"] = pd.to_datetime(df["dt"])
204185
return czsc.resample_bars(df, target_freq=freq, raw_bars=raw_bars)
205186

187+
if "SH" in symbol or "SZ" in symbol:
188+
fq_map = {"前复权": "qfq", "后复权": "hfq", "不复权": None}
189+
adj = fq_map.get(fq, None)
190+
191+
code, asset = symbol.split("#")
192+
193+
if freq.value.endswith("分钟"):
194+
df = dc.pro_bar(code=code, sdt=sdt, edt=edt, freq="min", adj=adj, asset=asset[0].lower(), v=2, ttl=ttl)
195+
df = df[~df["dt"].str.endswith("09:30:00")].reset_index(drop=True)
196+
df.rename(columns={"code": "symbol"}, inplace=True)
197+
df["dt"] = pd.to_datetime(df["dt"])
198+
return czsc.resample_bars(df, target_freq=freq, raw_bars=raw_bars, base_freq="1分钟")
199+
200+
else:
201+
df = dc.pro_bar(code=code, sdt=sdt, edt=edt, freq="day", adj=adj, asset=asset[0].lower(), v=2, ttl=ttl)
202+
df.rename(columns={"code": "symbol"}, inplace=True)
203+
df["dt"] = pd.to_datetime(df["dt"])
204+
return czsc.resample_bars(df, target_freq=freq, raw_bars=raw_bars)
205+
206206
raise ValueError(f"symbol {symbol} 无法识别,获取数据失败!")
207207

208208

czsc/connectors/qmt_connector.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,16 @@ def is_trade_time(dt: datetime = datetime.now()):
372372
return True
373373

374374

375+
def is_stock_trade_time(dt: datetime = datetime.now()):
376+
"""判断指定时间是否是股票交易时间"""
377+
hm = dt.strftime("%H:%M")
378+
if "09:30" <= hm < "11:30":
379+
return True
380+
if "13:00" <= hm < "15:00":
381+
return True
382+
return False
383+
384+
375385
def is_trade_day(dt: datetime = datetime.now()):
376386
"""判断指定日期是否是交易日"""
377387
date = dt.strftime("%Y%m%d")

czsc/connectors/ts_connector.py

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ def get_daily_basic(sdt="20100101", edt="20240101"):
7777
7878
https://s0cqcxuy3p.feishu.cn/wiki/W5UYwk0qwiHWlxk2ngDcjxQKncg
7979
"""
80-
sdt = pd.to_datetime(sdt).strftime("%Y%m%d")
81-
edt = pd.to_datetime(edt).strftime("%Y%m%d")
80+
sdt = pd.to_datetime(sdt)
81+
edt = pd.to_datetime(edt)
8282

8383
dates = czsc.get_trading_dates(sdt, edt)
8484
rows = []
@@ -129,6 +129,109 @@ def moneyflow_hsgt(start_date, end_date):
129129
return df
130130

131131

132+
def pro_bar_minutes(ts_code, sdt, edt, freq="60min", asset="E", adj=None):
133+
"""获取分钟线
134+
135+
https://tushare.pro/document/2?doc_id=109
136+
137+
:param ts_code: 标的代码
138+
:param sdt: 开始时间,精确到分钟
139+
:param edt: 结束时间,精确到分钟
140+
:param freq: 分钟周期,可选值 1min, 5min, 15min, 30min, 60min
141+
:param asset: 资产类别:E股票 I沪深指数 C数字货币 FT期货 FD基金 O期权 CB可转债(v1.2.39),默认E
142+
:param adj: 复权类型,None不复权,qfq:前复权,hfq:后复权
143+
:param raw_bar: 是否返回 RawBar 对象列表
144+
:return:
145+
"""
146+
import tushare as ts
147+
from datetime import timedelta
148+
pro = dc
149+
150+
dt_fmt = "%Y%m%d"
151+
152+
sdt = pd.to_datetime(sdt).strftime(dt_fmt)
153+
edt = pd.to_datetime(edt).strftime(dt_fmt)
154+
155+
klines = []
156+
end_dt = pd.to_datetime(edt)
157+
dt1 = pd.to_datetime(sdt)
158+
delta = timedelta(days=20 * int(freq.replace("min", "")))
159+
dt2 = dt1 + delta
160+
while dt1 < end_dt:
161+
df = ts.pro_bar(
162+
ts_code=ts_code,
163+
asset=asset,
164+
freq=freq,
165+
start_date=dt1.strftime(dt_fmt),
166+
end_date=dt2.strftime(dt_fmt),
167+
)
168+
klines.append(df)
169+
dt1 = dt2
170+
dt2 = dt1 + delta
171+
print(f"pro_bar_minutes: {ts_code} - {asset} - {freq} - {dt1} - {dt2} - {len(df)}")
172+
173+
df_klines = pd.concat(klines, ignore_index=True)
174+
kline = df_klines.drop_duplicates("trade_time").sort_values("trade_time", ascending=True, ignore_index=True)
175+
kline["trade_time"] = pd.to_datetime(kline["trade_time"], format=dt_fmt)
176+
kline["dt"] = kline["trade_time"]
177+
float_cols = ["open", "close", "high", "low", "vol", "amount"]
178+
kline[float_cols] = kline[float_cols].astype("float32")
179+
kline["avg_price"] = kline["amount"] / kline["vol"]
180+
181+
# 删除9:30的K线
182+
kline["keep"] = kline["trade_time"].apply(lambda x: 0 if x.hour == 9 and x.minute == 30 else 1)
183+
kline = kline[kline["keep"] == 1]
184+
185+
# 删除没有成交量的K线
186+
kline = kline[kline["vol"] > 0]
187+
kline.drop(["keep"], axis=1, inplace=True)
188+
189+
start_date = pd.to_datetime(sdt)
190+
end_date = pd.to_datetime(edt)
191+
kline = kline[(kline["trade_time"] >= start_date) & (kline["trade_time"] <= end_date)]
192+
kline = kline.reset_index(drop=True)
193+
kline["trade_date"] = kline.trade_time.apply(lambda x: x.strftime(dt_fmt))
194+
195+
if asset == "E":
196+
# https://tushare.pro/document/2?doc_id=28
197+
factor = pro.adj_factor(ts_code=ts_code, start_date=sdt, end_date=edt)
198+
elif asset == "FD":
199+
# https://tushare.pro/document/2?doc_id=199
200+
factor = pro.fund_adj(ts_code=ts_code, start_date=sdt, end_date=edt)
201+
else:
202+
factor = pd.DataFrame()
203+
204+
if len(factor) > 0:
205+
# 处理复权因子缺失的情况:前值填充
206+
df1 = pd.DataFrame({"trade_date": kline["trade_date"].unique().tolist()})
207+
factor = df1.merge(factor, on=["trade_date"], how="left").ffill().bfill()
208+
factor = factor.sort_values("trade_date", ignore_index=True)
209+
210+
print(f"pro_bar_minutes: {ts_code} - {asset} - 复权因子长度 = {len(factor)}")
211+
212+
# 复权行情说明:https://tushare.pro/document/2?doc_id=146
213+
if len(factor) > 0 and adj and adj == "qfq":
214+
# 前复权 = 当日收盘价 × 当日复权因子 / 最新复权因子
215+
latest_factor = factor.iloc[-1]["adj_factor"]
216+
adj_map = {row["trade_date"]: row["adj_factor"] for _, row in factor.iterrows()}
217+
for col in ["open", "close", "high", "low"]:
218+
kline[col] = kline.apply(lambda x: x[col] * adj_map[x["trade_date"]] / latest_factor, axis=1)
219+
220+
if len(factor) > 0 and adj and adj == "hfq":
221+
# 后复权 = 当日收盘价 × 当日复权因子
222+
adj_map = {row["trade_date"]: row["adj_factor"] for _, row in factor.iterrows()}
223+
for col in ["open", "close", "high", "low"]:
224+
kline[col] = kline.apply(lambda x: x[col] * adj_map[x["trade_date"]], axis=1)
225+
226+
if sdt:
227+
kline = kline[kline["trade_time"] >= pd.to_datetime(sdt)]
228+
if edt:
229+
kline = kline[kline["trade_time"] <= pd.to_datetime(edt)]
230+
231+
kline = kline.reset_index(drop=True)
232+
return kline
233+
234+
132235
def get_symbols(step="all"):
133236
"""获取标的代码"""
134237
stocks = dc.stock_basic(exchange="", list_status="L", fields="ts_code,symbol,name,area,industry,list_date")

czsc/eda.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def remove_beta_effects(df, **kwargs):
8686
def cross_sectional_strategy(df, factor, weight="weight", long=0.3, short=0.3, **kwargs):
8787
"""根据截面因子值构建多空组合
8888
89-
:param df: pd.DataFrame, 包含因子列的数据, 必须包含 dt, symbol, factor 列
89+
:param df: pd.DataFrame, 包含多个品种的因子数据, 必须包含 dt, symbol, factor 列
9090
:param factor: str, 因子列名称
9191
:param weight: str, 权重列名称,默认为 weight
9292
:param long: float, 多头持仓比例/数量,默认为 0.3, 取值范围为 [0, n_symbols], 0~1 表示比例,大于等于1表示数量
@@ -600,3 +600,68 @@ def limit_leverage(df: pd.DataFrame, leverage: float = 1.0, **kwargs):
600600

601601
return df
602602

603+
604+
def cal_trade_price(df: pd.DataFrame, digits=None, **kwargs):
605+
"""计算给定品种基础周期K线数据的交易价格表
606+
607+
:param df: 基础周期K线数据,一般是1分钟周期的K线,支持多个品种
608+
:param digits: 保留小数位数,默认值为None,用每个品种的 close 列的小数位数
609+
:param kwargs:
610+
611+
- windows: 计算TWAP和VWAP的窗口列表,默认值为(5, 10, 15, 20, 30, 60)
612+
- copy: 是否复制数据,默认值为True
613+
614+
:return: 交易价格表,包含多个品种的交易价格
615+
"""
616+
assert "symbol" in df.columns, "数据中必须包含 symbol 列"
617+
for col in ["dt", "open", "close", "vol"]:
618+
assert col in df.columns, f"数据中必须包含 {col} 列"
619+
620+
if kwargs.get("copy", True):
621+
df = df.copy()
622+
623+
# 获取所有唯一的品种
624+
symbols = df["symbol"].unique().tolist()
625+
626+
# 为每个品种分别计算交易价格
627+
dfs = []
628+
for symbol in symbols:
629+
df_symbol = df[df["symbol"] == symbol].copy()
630+
df_symbol = df_symbol.sort_values("dt").reset_index(drop=True)
631+
632+
# 如果没有指定digits,则使用该品种的close列的小数位数
633+
symbol_digits = digits
634+
if symbol_digits is None:
635+
symbol_digits = df_symbol["close"].astype(str).str.split(".").str[1].str.len().max()
636+
637+
# 下根K线开盘、收盘
638+
df_symbol["TP_CLOSE"] = df_symbol["close"]
639+
df_symbol["TP_NEXT_OPEN"] = df_symbol["open"].shift(-1)
640+
df_symbol["TP_NEXT_CLOSE"] = df_symbol["close"].shift(-1)
641+
price_cols = ["TP_CLOSE", "TP_NEXT_OPEN", "TP_NEXT_CLOSE"]
642+
643+
# TWAP / VWAP 价格
644+
df_symbol["vol_close_prod"] = df_symbol["vol"] * df_symbol["close"]
645+
for t in kwargs.get("windows", (5, 10, 15, 20, 30, 60)):
646+
647+
df_symbol[f"TP_TWAP{t}"] = df_symbol["close"].rolling(t).mean().shift(-t)
648+
649+
df_symbol[f"sum_vol_{t}"] = df_symbol["vol"].rolling(t).sum()
650+
df_symbol[f"sum_vcp_{t}"] = df_symbol["vol_close_prod"].rolling(t).sum()
651+
df_symbol[f"TP_VWAP{t}"] = (df_symbol[f"sum_vcp_{t}"] / df_symbol[f"sum_vol_{t}"]).shift(-t)
652+
653+
price_cols.extend([f"TP_TWAP{t}", f"TP_VWAP{t}"])
654+
df_symbol.drop(columns=[f"sum_vol_{t}", f"sum_vcp_{t}"], inplace=True)
655+
656+
df_symbol.drop(columns=["vol_close_prod"], inplace=True)
657+
658+
# 用当前K线的收盘价填充交易价中的 nan 值
659+
for price_col in price_cols:
660+
df_symbol[price_col] = df_symbol[price_col].fillna(df_symbol["close"])
661+
662+
df_symbol[price_cols] = df_symbol[price_cols].round(symbol_digits)
663+
dfs.append(df_symbol)
664+
665+
# 合并所有品种的交易价格数据
666+
dfk = pd.concat(dfs, ignore_index=True)
667+
return dfk

0 commit comments

Comments
 (0)