Skip to content

Commit 8a51373

Browse files
committed
feat: 新增换手率显示功能,优化换手率计算逻辑并更新相关测试
1 parent fc7fec4 commit 8a51373

File tree

4 files changed

+67
-11
lines changed

4 files changed

+67
-11
lines changed

czsc/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
show_cta_periods_classify,
176176
show_volatility_classify,
177177
show_portfolio,
178+
show_turnover_rate,
178179
)
179180

180181
from czsc.utils.bi_info import (

czsc/eda.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -868,20 +868,20 @@ def turnover_rate(df: pd.DataFrame, **kwargs):
868868

869869
# 由于是 diff 计算,第一个时刻的仓位变化被忽视了,修改一下
870870
sdt = df["dt"].min()
871-
df_turns.loc[(df_turns["dt"] == sdt), "change"] = df[df["dt"] == sdt]["weight"].sum()
871+
df_turns.loc[(df_turns["dt"] == sdt), "change"] = df[df["dt"] == sdt]["weight"].abs().sum()
872872

873873
# 按日期 resample
874-
df_turns = df_turns.set_index("dt").resample("D").sum().reset_index()
874+
df_daily = df_turns.set_index("dt").resample("D").sum().reset_index()
875875

876876
if verbose:
877877
logger.info(f"组合换手率:{round(df_turns.change.sum() / 2, 4)}")
878878

879879
res = {
880-
"单边换手率": round(df_turns.change.sum(), 4),
881-
"每日换手率": df_turns.change.mean(),
882-
"最大单日换手率": df_turns.change.max(),
883-
"最小单日换手率": df_turns.change.min(),
884-
"换手详情": df_turns
880+
"单边换手率": round(df_daily.change.sum(), 4),
881+
"日均换手率": df_daily.change.mean(),
882+
"最大单日换手率": df_daily.change.max(),
883+
"最小单日换手率": df_daily.change.min(),
884+
"日换手详情": df_daily
885885
}
886886

887887
return res

czsc/utils/st_components.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2275,3 +2275,58 @@ def show_portfolio(df: pd.DataFrame, portfolio: str, benchmark: Optional[str] =
22752275
with tabs[3]:
22762276
yearly_days = cal_yearly_days(daily.index.to_list())
22772277
show_daily_return(daily, stat_hold_days=False, plot_cumsum=True, sub_title="", yearly_days=yearly_days)
2278+
2279+
2280+
def show_turnover_rate(df: pd.DataFrame):
2281+
"""显示换手率变化
2282+
2283+
:param df: 权重数据,必须包含 dt, symbol, weight 三列, 其他列忽略
2284+
"""
2285+
import plotly.express as px
2286+
from czsc.eda import turnover_rate
2287+
2288+
res = turnover_rate(df, verbose=True)
2289+
dfc = res['日换手详情'] # 两列:dt, change
2290+
dfc['dt'] = pd.to_datetime(dfc['dt'])
2291+
2292+
# 最近30天换手率
2293+
_sdt_30 = dfc['dt'].max() - pd.Timedelta(days=30)
2294+
_dfc = dfc[dfc['dt'] >= _sdt_30]
2295+
2296+
# 最近一年换手率
2297+
_sdt_year = dfc['dt'].max() - pd.Timedelta(days=365)
2298+
_dfy = dfc[dfc['dt'] >= _sdt_year]
2299+
2300+
m1, m2, m3, m4, m5, m6 = st.columns(6)
2301+
m1.metric("单边换手率", f"{res['单边换手率']:.2f}")
2302+
m2.metric("日均换手率", f"{res['日均换手率']:.2%}")
2303+
m3.metric("最大单日换手率", f"{res['最大单日换手率']:.2%}")
2304+
m4.metric("最小单日换手率", f"{res['最小单日换手率']:.2%}")
2305+
m5.metric("最近30天换手率", f"{_dfc['change'].sum():.2f}", help=f"最近30天换手率,自{_sdt_30}以来")
2306+
m6.metric("最近一年换手率", f"{_dfy['change'].sum():.2f}", help=f"最近一年换手率,自{_sdt_year}以来")
2307+
2308+
p1, p2, p3 = st.columns([2, 3, 1])
2309+
# 日换手的累计变化(X轴不显示)
2310+
df_daily = dfc.copy()
2311+
df_daily['change'] = df_daily['change'].cumsum()
2312+
fig = px.line(df_daily, x='dt', y='change', title='日换手累计曲线')
2313+
fig.update_xaxes(title_text='')
2314+
p1.plotly_chart(fig, use_container_width=True)
2315+
2316+
# 月换手的柱状图
2317+
df_monthly = dfc.copy()
2318+
df_monthly = df_monthly.set_index('dt').resample('M').sum().reset_index()
2319+
fig = px.bar(df_monthly, x='dt', y='change', title='月换手变化')
2320+
fig.update_xaxes(title_text='')
2321+
p2.plotly_chart(fig, use_container_width=True)
2322+
2323+
# 年换手的柱状图
2324+
df_yearly = dfc.copy()
2325+
df_yearly = df_yearly.set_index('dt').resample('Y').sum().reset_index()
2326+
df_yearly['dt'] = df_yearly['dt'].dt.strftime('%Y')
2327+
df_yearly['change'] = df_yearly['change'].round(0)
2328+
fig = px.bar(df_yearly, x='dt', y='change', title='年换手变化', hover_data=['change'])
2329+
fig.update_xaxes(title_text='')
2330+
p3.plotly_chart(fig, use_container_width=True)
2331+
2332+
st.caption("说明:以单边换手率计算")

test/test_eda.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,20 +135,20 @@ def test_turnover_rate_normal():
135135
# 验证结果
136136
assert isinstance(result, dict)
137137
assert "单边换手率" in result
138-
assert "每日换手率" in result
138+
assert "日均换手率" in result
139139
assert "最大单日换手率" in result
140140
assert "最小单日换手率" in result
141-
assert "换手详情" in result
141+
assert "日换手详情" in result
142142

143143
# 验证换手率计算是否正确
144144
# 第一天:1.0 + 0.5 + 0.0 = 1.5
145145
# 第二天:|0.5-1.0| + |1.0-0.5| + |0.5-0.0| = 1.5
146146
# 第三天:|0.0-0.5| + |0.5-1.0| + |1.0-0.5| = 1.5
147147
assert result["单边换手率"] == 4.5 # 1.5 + 1.5 + 1.5
148-
assert result["每日换手率"] == 1.5 # 4.5 / 3
148+
assert result["日均换手率"] == 1.5 # 4.5 / 3
149149
assert result["最大单日换手率"] == 1.5
150150
assert result["最小单日换手率"] == 1.5
151-
print(result['换手详情'])
151+
print(result['日换手详情'])
152152

153153

154154
def test_turnover_rate_verbose():

0 commit comments

Comments
 (0)