"""
低回撤多资产风险平价策略(精简可运行版)
三层风控:风险平价分散 → 趋势过滤降仓 → 回撤熔断空仓
资产池:红利低波ETF + 沪深300ETF + 短债ETF + 国债ETF + 黄金ETF + 现金
"""
import numpy as np
import pandas as pd
============ 资产池 ============
ASSETS = {
'512890.SH': 'equity', # 红利低波ETF
'510300.SH': 'equity', # 沪深300ETF
'511360.SH': 'bond', # 短债ETF
'511010.SH': 'bond', # 国债ETF
'518880.SH': 'commodity',# 黄金ETF
'511880.SH': 'cash', # 现金ETF
}
ASSET_CODES = list(ASSETS.keys())
EQUITY_CODES = [c for c, t in ASSETS.items() if t == 'equity']
============ 参数 ============
VOL_WINDOW = 60 # 波动率计算窗口
TREND_FAST = 20 # 短期均线
TREND_SLOW = 200 # 长期均线(趋势过滤)
MAX_EQUITY = 0.25 # 权益仓位上限
DD_WARN = -0.03 # 回撤3%→防御模式
DD_CUT = -0.05 # 回撤5%→熔断
MIN_TRADE_PCT = 0.02 # 权重偏离<2%不调仓
SLIPPAGE = 0.002 # 滑点0.2%
def init(context):
g.last_month = get_datetime().strftime('%Y%m')
g.peak = context.portfolio.portfolio_value
g.state = 'NORMAL'
g.weights = {}
set_benchmark('000300.SH')
set_slippage(PriceSlippage(SLIPPAGE))
set_commission(PerShare(type='stock', cost=0.0002, min_trade_cost=5.0))
run_daily(daily_task)
log.info('[初始化] 低回撤多资产风险平价策略就绪')
def daily_task(context, bar_dict):
"""每日执行:更新状态 → 判断是否调仓 → 执行"""
update_state(context)
month = get_datetime().strftime('%Y%m')
触发条件:月度再平衡 OR 状态切换 OR 权益仓位超限
need = False
if month != g.last_month:
need = True
g.last_month = month
if g.state in ('DEFENSIVE', 'SAFE'):
eq = equity_ratio(context)
limit = 0.05 if g.state == 'SAFE' else 0.15
if eq > limit:
need = True
if need:
execute_rebalance(context, bar_dict)
def update_state(context):
"""更新风控状态:回撤熔断 > 趋势过滤 > 正常"""
pv = context.portfolio.portfolio_value
if pv > g.peak:
g.peak = pv
dd = (pv - g.peak) / g.peak if g.peak > 0 else 0
trend_ok = check_trend()
if dd <= DD_CUT:
g.state = 'SAFE'
elif dd <= DD_WARN or not trend_ok:
g.state = 'DEFENSIVE'
else:
g.state = 'NORMAL'
def check_trend():
"""沪深300是否处于上升趋势(20日线>200日线且20日线向上)"""
p = history('000300.SH', ['close'], TREND_SLOW + 30, '1d', False, 'pre')
if len(p['close']) < TREND_SLOW + 5:
return True
c = p['close']
ma20 = c.tail(TREND_FAST).mean()
ma200 = c.tail(TREND_SLOW).mean()
ma20_prev = c.iloc[-TREND_FAST - 1:-1].mean()
return c.iloc[-1] > ma200 and ma20 > ma200 and ma20 > ma20_prev
def equity_ratio(context):
pv = context.portfolio.portfolio_value
if pv <= 0:
return 0
eq_val = 0.0
for c in EQUITY_CODES:
pos = context.portfolio.positions.get(c)
if pos is not None and pos.amount > 0:
eq_val += pos.amount * pos.last_price
return eq_val / pv
def execute_rebalance(context, bar_dict):
批量拉取所有资产的收盘价
prices = fetch_closes(ASSET_CODES, VOL_WINDOW + 10)
weights = target_weights(prices, g.state)
if not weights:
return
pv = context.portfolio.portfolio_value
for code, target_w in weights.items():
pos = context.portfolio.positions.get(code)
cur_val = pos.amount * pos.last_price if pos is not None and pos.amount > 0 else 0
cur_w = cur_val / pv if pv > 0 else 0
if abs(cur_w - target_w) < MIN_TRADE_PCT:
continue
target_val = pv * target_w
order_target_value(code, target_val)
g.weights = weights
log.info('[调仓] 状态=%s 权益=%.1f%% 权重=%s' % (
g.state, equity_ratio(context) * 100,
{c: '%.1f%%' % (w * 100) for c, w in weights.items() if w > 0.01}))
def fetch_closes(codes, count):
"""批量获取多资产收盘价,对齐日期后返回 DataFrame(行=日期, 列=代码)"""
series = {}
for c in codes:
h = history(c, ['close'], count, '1d', False, 'pre')
if h is not None and len(h['close']) >= count // 2:
series[c] = h['close']
if not series:
return pd.DataFrame()
return pd.concat({c: s for c, s in series.items()}, axis=1).dropna(how='all')
def target_weights(prices, state):
"""计算目标权重"""
codes = [c for c in ASSET_CODES if c in prices.columns]
if state == 'SAFE':
return _preset_weights({
'511360.SH': 0.60, '511010.SH': 0.20,
'518880.SH': 0.12, '511880.SH': 0.08,
})
if state == 'DEFENSIVE':
w = _rp_weights(prices[codes]) if len(codes) >= 3 else None
if w is None:
return _preset_weights({
'512890.SH': 0.05, '510300.SH': 0.05,
'511360.SH': 0.55, '511010.SH': 0.15,
'518880.SH': 0.10, '511880.SH': 0.10,
})
eq_sum = sum(w.get(c, 0) for c in EQUITY_CODES)
if eq_sum > 0.10:
scale = 0.10 / eq_sum
for c in EQUITY_CODES:
if c in w:
w[c] *= scale
deficit = 1 - sum(w.values())
w['511360.SH'] = w.get('511360.SH', 0) + deficit
return w
NORMAL
w = _rp_weights(prices[codes]) if len(codes) >= 3 else None
if w is None:
return _preset_weights({
'512890.SH': 0.15, '510300.SH': 0.10,
'511360.SH': 0.40, '511010.SH': 0.20,
'518880.SH': 0.10, '511880.SH': 0.05,
})
eq_sum = sum(w.get(c, 0) for c in EQUITY_CODES)
if eq_sum > MAX_EQUITY:
scale = MAX_EQUITY / eq_sum
for c in EQUITY_CODES:
if c in w:
w[c] *= scale
deficit = 1 - sum(w.values())
w['511360.SH'] = w.get('511360.SH', 0) + deficit
return w
def _rp_weights(price_df):
"""简化风险平价:波动率倒数加权"""
if price_df.empty or price_df.shape[1] < 3:
return None
returns = price_df.pct_change().dropna(how='all').tail(VOL_WINDOW)
if returns.empty or len(returns) < VOL_WINDOW // 2:
return None
vol = returns.std() * np.sqrt(252)
vol = vol.replace(0, np.nan)
med = vol.median()
if np.isnan(med) or med <= 0:
return None
vol = vol.fillna(med)
inv_vol = 1.0 / vol
raw = inv_vol / inv_vol.sum()
w = {c: float(raw[c]) if c in raw.index else 0 for c in ASSET_CODES}
total = sum(w.values())
if total <= 0:
return None
return {k: v / total for k, v in w.items()}
def _preset_weights(template):
"""预设权重模板 → 完整7资产权重"""
w = {c: template.get(c, 0) for c in ASSET_CODES}
total = sum(w.values())
if total <= 0:
w['511360.SH'] = 1.0
return w
return {k: v / total for k, v in w.items()}
def handle_bar(context, bar_dict):
pass

