交易成本、滑点与冲击成本
直觉
零成本是回测最大的幻觉。真实交易有三类成本:固定佣金、滑点(订单到成交的价差)、市场冲击(你的订单本身推动价格)。一个在零成本下漂亮的策略,加上真实成本可能立刻亏损——这一节量化成本的影响。
成本分解
设换手 :
- 佣金、滑点:与换手线性;
- 市场冲击:常建模为换手的二次函数(订单越大、推动越剧烈,小资金常忽略)。
我们的引擎 cost_bps 统一按换手线性扣减,覆盖佣金+滑点之和。
「人话」解释:为什么成本偏爱慢策略?
换手越高,被扣得越多。所以高频、参数敏感的策略对成本极脆弱——理论上的一点 edge,全被成本吃光。 慢策略(如 60/120 双均线)换手低、抗成本低,实盘表现往往更接近回测。 估成本时要保守:宁可高估一点,也别让回测自欺。
可运行案例:成本扫描,看收益如何塌缩
用双均线策略,把 cost_bps 从 0 扫到 50,看夏普与资金曲线如何随成本衰减。
import quant
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv('/data/spy_daily.csv', parse_dates=['date']).set_index('date')
close = df['adj_close']
raw = (close.rolling(20).mean() > close.rolling(60).mean()).astype(float)
signal = raw.replace(0, np.nan).ffill().fillna(0)
costs = [0, 2, 5, 10, 20, 50]
rows, eqs = [], {}
for c in costs:
r = quant.vector_backtest(close, signal, cost_bps=c, freq=252)
s = quant.sharpe(r['returns'], freq=252)
rows.append({'成本bps': c, '夏普': round(s, 3), '换手': round(r['turnover'], 0),
'末值': round(r['equity'].iloc[-1], 2)})
eqs[c] = r['equity']
print(pd.DataFrame(rows).to_string(index=False))
print("\n→ 成本从 0→50 bps,夏普大幅下滑,零成本的'盈利'多为幻觉。")
plt.figure(figsize=(8, 3.6))
for c in costs:
plt.plot(eqs[c], label=f'{c} bps', lw=1.2)
plt.title('不同成本下的资金曲线'); plt.ylabel('净值'); plt.legend(fontsize=8)
plt.tight_layout(); plt.show()
动手改一改:拖动成本看夏普塌缩
拖动单边成本,看双均线策略的夏普与净值如何随成本衰减——零成本的「盈利」多为幻觉。
# ParamSlider(SSR 预览)
参数: COST_BPS
import quant
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv('/data/spy_daily.csv', parse_dates=['date']).set_index('date')
close = df['adj_close']
raw = (close.rolling(20).mean() > close.rolling(60).mean()).astype(float)
signal = raw.replace(0, np.nan).ffill().fillna(0)
r = quant.vector_backtest(close, signal, cost_bps=COST_BPS, freq=252)
bh = quant.vector_backtest(close, pd.Series(1.0, index=close.index), cost_bps=0, freq=252)
print(f"夏普 = {quant.sharpe(r['returns'], freq=252):.3f} 换手 = {r['turnover']:.0f}")
print(quant.performance_summary(r['equity'], r['returns'], freq=252).round(3))
plt.figure(figsize=(9, 3.6))
plt.plot(r['equity'], label=f'双均线(成本 {COST_BPS} bps)', lw=1.5)
plt.plot(bh['equity'], label='买入持有', alpha=0.7)
plt.ylabel('净值'); plt.legend(fontsize=9); plt.tight_layout(); plt.show()
小结
- 成本 = 佣金 + 滑点(线性于换手)+ 冲击(二次于换手);
- 引擎
cost_bps按换手线性扣减; - 成本偏爱低换手策略;估成本要保守,零成本回测不可信。