跳到主要内容

交易成本、滑点与冲击成本

直觉

零成本是回测最大的幻觉。真实交易有三类成本:固定佣金滑点(订单到成交的价差)、市场冲击(你的订单本身推动价格)。一个在零成本下漂亮的策略,加上真实成本可能立刻亏损——这一节量化成本的影响。

成本分解

设换手 τt=wtwt1\tau_t = |w_t - w_{t-1}|

成本t=commissionτt佣金+slippageτt滑点+κτt2冲击(二次)\text{成本}_t = \underbrace{\text{commission}\cdot\tau_t}_{\text{佣金}} + \underbrace{\text{slippage}\cdot\tau_t}_{\text{滑点}} + \underbrace{\kappa\cdot\tau_t^2}_{\text{冲击(二次)}}
  • 佣金、滑点:与换手线性
  • 市场冲击:常建模为换手的二次函数(订单越大、推动越剧烈,小资金常忽略)。

我们的引擎 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 按换手线性扣减;
  • 成本偏爱低换手策略;估成本要保守,零成本回测不可信。