量化风险管理方法:应用 VaR 模型优化多货币投资组合(使用 Python 和 MetaTrader 5)·进阶篇
(2/3)· 从 VaR 方程到动态仓位与止损,进阶实操把理论风险模型接进 MetaTrader 5 真实盘口
◍ 用 VaR 最小化和约束条件压住外汇组合风险
在给定预期收益下做组合优化,核心思路是把 VaR 当目标函数、用 SLSQP 求权重,让风险最小化与目标收益之间达成平衡。原文实现的置信水平默认取 0.95,也就是看 95% 情形下的最大潜在回撤。 外汇不像股票能裸卖空随意配,所以额外塞了两条硬约束:单标的仓位下限 0.01 手、组合总杠杆上限 20 倍。这两条直接对应实盘开户的常见限制,不写进优化器里算出来的权重大概率没法挂单。 动态优化那段用了 252 根 K 线做回看窗口、每 20 根重新算一次权重,等于约每 20 个交易日调仓一次(按日线计)。这种滚动窗口能让组合跟住市况变化,但回测里要小心前视偏差——窗口切分必须严格用历史切片。 下面这段 Python 原型可直接丢进 Jupyter 验证逻辑,再考虑往 MT5 的 Python API 接 returns 数据: from scipy.optimize import minimize def optimize_portfolio(returns, target_return, confidence_level=0.95): n = len(returns.columns) def portfolio_var(weights): return monte_carlo_var(returns, weights, confidence_level=confidence_level) def portfolio_return(weights): return np.sum(returns.mean() * weights) constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1}, {'type': 'eq', 'fun': lambda x: portfolio_return(x) - target_return}) bounds = tuple((0, 1) for _ in range(n)) result = minimize(portfolio_var, n * [1./n], method='SLSQP', bounds=bounds, constraints=constraints) return result.x def forex_portfolio_constraints(weights, max_leverage=20, min_position=0.01): leverage_constraint = {'type': 'ineq', 'fun': lambda x: max_leverage - np.sum(np.abs(x))} min_position_constraints = [{'type': 'ineq', 'fun': lambda x: abs(x[i]) - min_position} for i in range(len(weights))] return [leverage_constraint] + min_position_constraints def dynamic_portfolio_optimization(returns, lookback_period=252, rebalance_frequency=20): optimal_weights = [] for i in range(lookback_period, len(returns)): if i % rebalance_frequency == 0: window_returns = returns.iloc[i-lookback_period:i] target_return = window_returns.mean().mean() weights = optimize_portfolio(window_returns, target_return) optimal_weights.append(weights) return pd.DataFrame(optimal_weights, index=returns.index[lookback_period::rebalance_frequency]) 逐行拆一下关键处:optimize_portfolio 里 constraints 第一个 eq 是权重和为 1(满仓),第二个 eq 锁死组合均值收益等于 target_return;bounds 把每腿权重卡在 0~1 不准做空。forex_portfolio_constraints 的 leverage_constraint 用 max_leverage 减绝对值和,保证总敞口不超 20 倍;min_position 那组循环约束让每个权重绝对值至少 0.01。dynamic 函数里 i % rebalance_frequency == 0 就是每 20 根调一次,window_returns 严格取 i-252 到 i 的历史,避免用到未来数据。外汇和贵金属杠杆高、滑点跳空频繁,这套约束只是降低穿仓概率,不保证不亏。
from scipy.optimize class="kw">import minimize def optimize_portfolio(returns, target_return, confidence_level=class="num">0.95): n = len(returns.columns) def portfolio_var(weights): class="kw">return monte_carlo_var(returns, weights, confidence_level=confidence_level) def portfolio_return(weights): class="kw">return np.sum(returns.mean() * weights) constraints = ({&class="macro">#x27;type&class="macro">#x27;: &class="macro">#x27;eq&class="macro">#x27;, &class="macro">#x27;fun&class="macro">#x27;: lambda x: np.sum(x) - class="num">1}, {&class="macro">#x27;type&class="macro">#x27;: &class="macro">#x27;eq&class="macro">#x27;, &class="macro">#x27;fun&class="macro">#x27;: lambda x: portfolio_return(x) - target_return}) bounds = tuple((class="num">0, class="num">1) for _ in range(n)) result = minimize(portfolio_var, n * [class="num">1./n], method=&class="macro">#x27;SLSQP&class="macro">#x27;, bounds=bounds, constraints=constraints) class="kw">return result.x def forex_portfolio_constraints(weights, max_leverage=class="num">20, min_position=class="num">0.01): leverage_constraint = {&class="macro">#x27;type&class="macro">#x27;: &class="macro">#x27;ineq&class="macro">#x27;, &class="macro">#x27;fun&class="macro">#x27;: lambda x: max_leverage - np.sum(np.abs(x))} min_position_constraints = [{&class="macro">#x27;type&class="macro">#x27;: &class="macro">#x27;ineq&class="macro">#x27;, &class="macro">#x27;fun&class="macro">#x27;: lambda x: abs(x[i]) - min_position} for i in range(len(weights))] class="kw">return [leverage_constraint] + min_position_constraints def dynamic_portfolio_optimization(returns, lookback_period=class="num">252, rebalance_frequency=class="num">20): optimal_weights = [] for i in range(lookback_period, len(returns)): if i % rebalance_frequency == class="num">0: window_returns = returns.iloc[i-lookback_period:i] target_return = window_returns.mean().mean() weights = optimize_portfolio(window_returns, target_return) optimal_weights.append(weights) class="kw">return pd.DataFrame(optimal_weights, index=returns.index[lookback_period::rebalance_frequency])
「用 VaR 把每笔亏损锁死在账户百分比内」
把头寸规模绑在 VaR 上,核心目的只有一个:不管某品种波动放大还是收敛,单笔最大亏损都锚定在账户权益的固定比例。上面这套逻辑里,risk_per_trade 默认 0.02,也就是每笔最多吞掉账户 2%,外汇和贵金属杠杆高,这个数值设大一点就可能在一两根 K 线里爆仓,属于典型高风险操作。 函数 dynamic_position_sizing 先抓品种 tick 价值,乘 10 近似出 1 pip 的美元价值;再用账户余额乘风险比例得出 max_loss,除以 VaR 对应的 pip 数,就得到该开多少手。update_positions 则按组合 VaR 重算每个标的的最优仓,和当前仓差超过阈值才动手加减,避免频繁来回刷单。 实盘里建议先把 MIN_POSITION_CHANGE 设成 0.1 手或 10% 当前仓孰大,否则点差和佣金会吃掉你重平衡省下的风险预算。开 MT5 接 Python 跑一遍,用最近 30 天 EURUSD 的日 VaR 回测,能看到仓位随波动率抬升自动缩水。
def dynamic_position_sizing(symbol, var, account_balance, risk_per_trade=class="num">0.02): symbol_info = mt5.symbol_info(symbol) pip_value = symbol_info.trade_tick_value * class="num">10 max_loss = account_balance * risk_per_trade position_size = max_loss / (abs(var) * pip_value) class="kw">return round(position_size, class="num">2) def update_positions(portfolio_var, account_balance): for symbol in portfolio: current_position = get_position_size(symbol) optimal_position = dynamic_position_sizing(symbol, portfolio_var[symbol], account_balance) if abs(current_position - optimal_position) > MIN_POSITION_CHANGE: if current_position < optimal_position: # Increase position mt5.order_send(symbol, mt5.ORDER_TYPE_BUY, optimal_position - current_position) else: # Decrease position mt5.order_send(symbol, mt5.ORDER_TYPE_SELL, current_position - optimal_position)
用 VaR 反推止损获利的距离
把风险价值(VaR)直接当成止损宽度的锚,是动态仓位管理里很实用的一招。市场波动放大时 VaR 走宽,算出来的止损点数自动跟着变大,避免被正常噪声扫掉;波动收敛时止损收紧,腾出更多风险预算。 下面这段逻辑先用 VaR 绝对值除以合约 point 得到止损 pip 数,再按置信水平微调(99% 置信下乘 0.01 的缓冲系数),获利则按风险回报比倍数直接推算。外汇与贵金属杠杆高,VaR 模型若用错历史窗口,止损可能偏窄导致频繁止损,实盘前请在 MT5 策略测试器用近 3 个月数据核对。 别把正态当圣经 VaR 在跳空行情会严重低估尾部风险,贵金属夜盘常出现此类缺口,建议把置信水平从 0.99 调到 0.95 观察止损触发频率变化再定参数。
def calculate_stop_loss(symbol, var, confidence_level=class="num">0.99): symbol_info = mt5.symbol_info(symbol) point = symbol_info.point stop_loss_pips = abs(var) / point class="kw">return round(stop_loss_pips * (class="num">1 + (class="num">1 - confidence_level)), class="num">0) def calculate_take_profit(stop_loss_pips, risk_reward_ratio=class="num">2): class="kw">return round(stop_loss_pips * risk_reward_ratio, class="num">0) def set_sl_tp(symbol, order_type, lot, price, sl_pips, tp_pips): symbol_info = mt5.symbol_info(symbol) point = symbol_info.point if order_type == mt5.ORDER_TYPE_BUY: sl = price - sl_pips * point tp = price + tp_pips * point else: sl = price + sl_pips * point tp = price - tp_pips * point request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": lot, "type": order_type, "price": price, "sl": sl, "tp": tp, } result = mt5.order_send(request) class="kw">return result
◍ 用 VaR 掐住回撤咽喉
把 VaR 接进回撤控制后,账户不再靠人工盯盘硬扛。逻辑很直接:当前组合 VaR 占账户权益的比一旦越过阈值,系统立刻砍敞口,保护本金优先于捕捉收益。 波动环境不是恒定的,所以最大回撤阈值也得动。用近 252 根 K 线的收益标准差除以长期标准差,得到波动比,再乘基准 0.2,高波动期阈值自动收窄、低波动期放宽,系统倾向在乱局里保守、在平稳里激进。 外汇与贵金属自带高杠杆高风险,这套机制只降低概率性爆雷,不消除亏损可能。下面这段 Python 风格伪代码可直接对照 MT5 的订单接口改写成 MQL5 跑起来。 代码逐行拆解: monitor_drawdown 接收账户余额与最大回撤(默认 0.2 即 20%),先算组合 VaR,再用 VaR/余额得出当前回撤率;若超阈值,按超出比例调用减仓。 reduce_exposure 遍历持仓,新仓 = 旧仓×(1-因子),变动大于最小步长才发 ORDER_TYPE_SELL 平多单。 adjust_max_drawdown 取近 252 根收益标准差与全样本标准差之比,返回动态阈值,波动放大时阈值同比放大。
def monitor_drawdown(account_balance, max_drawdown=class="num">0.2): portfolio_var = calculate_portfolio_var(portfolio) current_drawdown = portfolio_var / account_balance if current_drawdown > max_drawdown: reduce_exposure(current_drawdown / max_drawdown) def reduce_exposure(reduction_factor): for symbol in portfolio: current_position = get_position_size(symbol) new_position = current_position * (class="num">1 - reduction_factor) if abs(current_position - new_position) > MIN_POSITION_CHANGE: mt5.order_send(symbol, mt5.ORDER_TYPE_SELL, current_position - new_position) def adjust_max_drawdown(returns, lookback=class="num">252, base_max_drawdown=class="num">0.2): recent_volatility = returns.tail(lookback).std() long_term_volatility = returns.std() volatility_ratio = recent_volatility / long_term_volatility class="kw">return base_max_drawdown * volatility_ratio
「VaR 跑了一年,权重和指标说了什么」
把 VaR 模型在实盘里喂了一年,组合权重分布很歪:AUDUSD 占 51.29%、GBPUSD 28.75%、USDJPY 19.96%,EURUSD 和 USDCAD 接近 0%。模型几乎把半仓押在澳美上,欧美和加元被完全踢出,这种集中度本身就需要回过头查信号逻辑。 核心指标出来后是这样:VaR -0.70%、CVaR 0.04%、VaR 效率 18.1334、盈利因子 1.0291、夏普比率 -73.5999。CVaR 比 VaR 低一大截,说明模型大概率高估了尾部风险;VaR 效率远大于 1,是风险测算失准的另一个信号。 盈利因子刚过 1(1.0291),只是勉强没亏;夏普 -73.6 属于深度负值,组合波动吞噬了所有边际收益。外汇与贵金属本身高风险,这类回测结果只说明模型过于保守,放掉了大量本可捕获的机会,而不是市场给了稳赚窗口。 下面这段是评估用的 Python 函数(非 MQL5,但逻辑可直接移植到 MT5 指标里做验证): [CODE] def var_efficiency(returns, var, confidence_level=0.95): violations = (returns < -var).sum() expected_violations = len(returns) * (1 - confidence_level) return abs(violations - expected_violations) / expected_violations def profit_factor(returns): positive_returns = returns[returns > 0].sum() negative_returns = abs(returns[returns < 0].sum()) return positive_returns / negative_returns def sharpe_ratio(returns, risk_free_rate=0.02): return (returns.mean() - risk_free_rate) / returns.std() * np.sqrt(252) def plot_var_vs_returns(returns, var): fig, ax = plt.subplots(figsize=(12, 6)) ax.plot(returns, label='Actual Returns') ax.axhline(-var, color='red', linestyle='--', label='VaR') ax.fill_between(returns.index, -var, returns, where=returns < -var, color='red', alpha=0.3) ax.legend() ax.set_title('VaR vs Actual Returns') plt.show() def plot_drawdown(returns): drawdown = (returns.cumsum() - returns.cumsum().cummax()) plt.figure(figsize=(12, 6)) plt.plot(drawdown) plt.title('Portfolio Drawdown') plt.show() def plot_cumulative_returns(returns): cumulative_returns = (1 + returns).cumprod() plt.figure(figsize=(12, 6)) plt.plot(cumulative_returns) plt.title('Cumulative Portfolio Returns') plt.ylabel('Cumulative Returns') plt.show() [/CODE] 逐行拆一下:var_efficiency 数出击穿 VaR 的次数,再和 95% 置信度下的期望违规数做差比,值越大越说明模型偏保守;profit_factor 把正收益总和除以负收益绝对值,1.0291 就是这么来的;sharpe_ratio 用日均值减无风险率除标准差再乘 sqrt(252) 年化。画图三个函数分别把实际收益 vs VaR 线、回撤曲线、累计净值画出来,开 MT5 拿历史 tick 跑一遍同逻辑,能直接看到模型漏掉了多少次该出手的机会。
def var_efficiency(returns, var, confidence_level=class="num">0.95): violations = (returns < -var).sum() expected_violations = len(returns) * (class="num">1 - confidence_level) class="kw">return abs(violations - expected_violations) / expected_violations def profit_factor(returns): positive_returns = returns[returns > class="num">0].sum() negative_returns = abs(returns[returns < class="num">0].sum()) class="kw">return positive_returns / negative_returns def sharpe_ratio(returns, risk_free_rate=class="num">0.02): class="kw">return (returns.mean() - risk_free_rate) / returns.std() * np.sqrt(class="num">252) def plot_var_vs_returns(returns, var): fig, ax = plt.subplots(figsize=(class="num">12, class="num">6)) ax.plot(returns, label=&class="macro">#x27;Actual Returns&class="macro">#x27;) ax.axhline(-var, class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;, linestyle=&class="macro">#x27;--&class="macro">#x27;, label=&class="macro">#x27;VaR&class="macro">#x27;) ax.fill_between(returns.index, -var, returns, where=returns < -var, class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;, alpha=class="num">0.3) ax.legend() ax.set_title(&class="macro">#x27;VaR vs Actual Returns&class="macro">#x27;) plt.show() def plot_drawdown(returns): drawdown = (returns.cumsum() - returns.cumsum().cummax()) plt.figure(figsize=(class="num">12, class="num">6)) plt.plot(drawdown) plt.title(&class="macro">#x27;Portfolio Drawdown&class="macro">#x27;) plt.show() def plot_cumulative_returns(returns): cumulative_returns = (class="num">1 + returns).cumprod() plt.figure(figsize=(class="num">12, class="num">6)) plt.plot(cumulative_returns) plt.title(&class="macro">#x27;Cumulative Portfolio Returns&class="macro">#x27;) plt.ylabel(&class="macro">#x27;Cumulative Returns&class="macro">#x27;) plt.show()
按策略改 VaR 才有用
趋势跟踪系统不能直接套标准 VaR。上面那段用滚动均值先剥离局部趋势,再对偏离值取分位数,等于让 VaR 跟着趋势走,而不是被均值假设绑死。
配对交易得换对象:算两个标的收益差(价差)的 VaR,才能把货币对之间的相关性吃进去。网格里两口锅是不是一起翻,靠这个能看出来。
动态压仓的逻辑很直接:网格 VaR 一旦超过阈值,adjust_grid 按 min(限额/实测,1) 等比缩所有持仓,不会手动去盯。
入场价也别写死。var_based_grid_levels 拿当前价乘 (1 ± i*VaR) 铺网格,波动率大了间距自动拉开。
实测上,高波动段夏普从 -73.59 拉到 1.82,系统对市况的适应明显灵活了。外汇和贵金属杠杆高,VaR 只是概率边界,真穿尾还得靠仓位硬约束。
后续想接机器学习预测 VaR,但眼下这套已能给复杂系统更靠谱的风险刻度。MT5 接 Python 跑这段,改 lookback 和 confidence_level 就能比效果。
def trend_adjusted_var(returns, lookback=class="num">20, confidence_level=class="num">0.95): trend = returns.rolling(lookback).mean() deviation = returns - trend var = np.percentile(deviation, (class="num">1 - confidence_level) * class="num">100) class="kw">return trend + var def spread_var(returns_1, returns_2, confidence_level=class="num">0.95): spread = returns_1 - returns_2 class="kw">return np.percentile(spread, (class="num">1 - confidence_level) * class="num">100) def adjust_grid(current_positions, var_limits, grid_var_value): adjustment_factor = min(var_limits / grid_var_value, class="num">1) class="kw">return {pair: pos * adjustment_factor for pair, pos in current_positions.items()} def var_based_grid_levels(price, var, levels=class="num">5): class="kw">return [price * (class="num">1 + i * var) for i in range(-levels, levels+class="num">1)]