引言:回测里的印钞机,实盘中的碎钞机
几乎每一位EA开发者都经历过同一条曲线:三年回测,年化表现好看,回撤曲线平滑,夏普比率漂亮得让人想立刻上实盘。然后真实账户跑起来,前两个月还算正常,某一天晚上一个数据公布,屏幕上的浮亏在几秒内跳了三倍,EA照旧在开仓,止损像是没生效,等你第二天早上打开终端,账户已经回到了一年前的样子。
我们在自家跟踪的MT5实盘EA失效样本里做过一次归因统计(2024年至2025年,样本为eafxtech实盘账户与客户账户的脱敏交易日志):65%的爆仓来自极端滑点与流动性枯竭,25%来自代码逻辑漏洞——重试导致重复下单、幽灵订单、状态机错乱、断线后状态丢失;真正属于"行情跳空超出预期"的只占10%。这个分布很值得玩味:所谓黑天鹅,绝大多数并不是市场制造的,而是工程制造的。市场只是把那些平时藏在代码里的小裂缝,一次性撕开。
所以这篇文章不打算讨论"怎么提高胜率",而是讨论一个更朴素的问题:当行情变得不像行情的时候,你的EA还能不能活着。我们会用一套"三层漏斗"的框架把风控拆成订单级、账户级、系统级三个层次,逐层讲清楚每一层在极端行情下会怎么失效、代码该怎么写、坑在哪里;然后复盘一个真实的多层网格EA在非农夜被强平的完整过程,收尾给出一份可以直接拿去用的参数配置Checklist和状态切换机制。
一、L1订单级:单笔订单的微观风控
1.1 固定点数止损,为什么在实盘里形同虚设
先看一个非常常见的写法:double sl = 300 * _Point;。这行代码在EURUSD的M15上可能是合理的,300点大约相当于0.0030的价格距离,差不多是亚盘一个小时的波动区间。但同一个300点,放到XAUUSD上只有3美元,而黄金在非农夜一根M1的波动就可以到8到12美元。你的止损距离没变,风险敞口却变了两三倍。
问题不在于"300"这个数字选错了,而在于固定点数这个设计本身假设了波动率是常数。而波动率是时变的:同一个品种,亚洲时段的ATR(14)可能是8个点,非农数据公布那一分钟能到60个点,同一周内相差七倍并不稀奇。用固定点数止损,等于把一个时变的量当成了常数,本质上就是把风险敞口的决定权交给了市场。
正确的做法是把止损距离锚定在波动率上,用ATR做基准,再叠加两个工程约束:经纪商的止损距离下限(SYMBOL_TRADE_STOPS_LEVEL)和当前点差。少考虑任何一个,你的下单请求都会在实盘里被服务器以10016 Invalid stops拒掉。
// 基于ATR的动态止损距离(返回点数)
double CalcAtrStopPoints(const string symbol, const ENUM_TIMEFRAMES tf, const double atrMult)
{
int h = iATR(symbol, tf, 14);
if(h == INVALID_HANDLE) return 0.0;
double buf[];
if(CopyBuffer(h, 0, 0, 1, buf) != 1) return 0.0;
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
if(point <= 0.0) return 0.0;
double atrPts = buf[0] * atrMult / point;
// 与经纪商的止损距离下限、当前点差对齐,避免 Invalid stops
long stopLevel = SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
long spreadPts = SymbolInfoInteger(symbol, SYMBOL_SPREAD);
double floorPts = MathMax((double)stopLevel, (double)spreadPts * 1.5);
return MathMax(atrPts, floorPts);
}
这段代码里有两个细节值得单独说。其一,句柄需要检查INVALID_HANDLE,并且在CopyBuffer失败时直接返回0,让上层逻辑把这个信号丢掉——冷启动阶段历史数据还没下载完,ATR取不到值是常态,此时用一个默认值硬算出来的止损距离,比不开仓危险得多。第二,floorPts取的是stopLevel与1.5倍点差中的较大者,因为点差扩张时,止损被点差直接穿透的情况比想象中普遍。
1.2 SL是触发价,不是成交价
这个认知差异会直接改变你的参数设计。如果你的单笔风险预算是账户的1%,而你在极端行情下的滑点可能达到止损距离的一半,那么你在正常行情下的止损距离就应该按"预算的60%"来算,留出40%的滑点缓冲。很多人抱怨"明明设了1%风险,实际亏了2.5%",原因就在这里。
1.3 硬止损与软止损的双轨设计
所谓硬止损,就是把SL写进OrderSend请求,由服务器托管;所谓软止损,是EA在OnTick里自己判断价格,触发后主动发平仓单。两者不是二选一的关系,而是各自负责不同的失效场景。
bool OpenWithHardStop(const string symbol, const ENUM_ORDER_TYPE type,
const double lots, const double stopPoints)
{
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
MqlTick tick;
if(!SymbolInfoTick(symbol, tick)) return false;
// 硬止损由服务器托管:断网、EA被卸载、终端重启时依然有效
double sl = (type == ORDER_TYPE_BUY)
? tick.ask - stopPoints * point
: tick.bid + stopPoints * point;
CTrade trade;
trade.SetExpertMagicNumber(InpMagic);
trade.SetDeviationInPoints(20); // 允许的滑点预算,单位点
trade.SetTypeFillingBySymbol(symbol); // 按品种支持的成交模式自动选择
return trade.PositionOpen(symbol, type, lots, 0.0, sl, 0.0, "RiskGuard");
}
1.4 移动止损的隐形代价
移动止损(Trailing Stop)是另一个高频踩坑点。很多实现是在OnTick里每个tick都算一次新的SL,只要比旧SL好就发PositionModify。回测里这没有问题,实盘里会出三种麻烦:一是请求频率过高被服务器限流,二是每次Modify都产生一次TRADE_TRANSACTION_ORDER_UPDATE,把你的状态机刷得乱七八糟,三是点差扩张瞬间算出"更优"的SL,把止损推到了价格的错误一侧,直接被拒。
10016和10004的报错基本消失,日志也终于能看了。
二、L2账户级:动态仓位与熔断降档
2.1 手数计算的三个要素,少一个都不对
单笔风险控制的本质是一道除法:风险金额除以(止损距离 × 每点每手价值)。三个要素分别是账户净值、止损点数、以及该品种的每点价值。第三个要素容易出错,因为它跟品种、报价货币、合约大小都相关,硬编码一个"每点10美元"是新手常见的错误——EURUSD标准手是10美元/点,XAUUSD标准手是100美元/点,差了一个数量级。
// 按风险比例反推手数,返回0表示应当放弃本次交易
double CalcLotByRisk(const string symbol, const double stopPoints, const double riskPct)
{
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity <= 0.0 || stopPoints <= 0.0) return 0.0;
double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
if(tickValue <= 0.0 || tickSize <= 0.0 || point <= 0.0) return 0.0;
double valuePerPoint = tickValue * (point / tickSize); // 每点每手价值
double riskMoney = equity * riskPct / 100.0;
double rawLot = riskMoney / (stopPoints * valuePerPoint);
// 手数规整:步长、下限、上限三重约束
double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
double lot = MathFloor(rawLot / step) * step;
lot = MathMin(lot, maxLot);
// 算出来不足下限时直接放弃,不要硬凑到下限手数
if(lot < minLot) return 0.0;
return NormalizeDouble(lot, 2);
}
注意末尾那个if(lot < minLot) return 0.0;。很多实现会写lot = MathMax(lot, minLot),看起来更"健壮",实际上是把风险放大了——当账户变小或者止损变宽时,理论手数可能是0.03,被强行拉到0.1,单笔风险瞬间变成3倍。宁可少做一笔交易,也不要让风险预算失控。
2.2 凯利公式为什么不能直接搬进EA
凯利公式给出的是长期增长率意义下的仓位比例,数学上很优雅,但它的两个输入在实盘里都不可靠:胜率p和赔率b。回测里统计出来的胜率,是在特定样本期内、特定滑点假设下得到的,一旦市场状态切换就会漂移;赔率则取决于你实际拿到的成交价,而成交价本身受滑点影响。输入不可靠,输出自然不可靠,而凯利公式对输入误差极其敏感,p估高几个百分点,仓位就可能成倍放大。
工程上的处理方式是打折扣:用1/4凯利,或者干脆退回固定比例。我们做过一组对照回测(MT5策略测试器,EURUSD与XAUUSD三年M15数据,60%胜率区间):在相同止损逻辑下,动态仓位管理的峰值回撤比固定比例低约34%,但前提是仓位系数的上限被限制在1.5倍基准以内。把上限放开到3倍,回撤反而超过了固定比例——这就是凯利"过度自信惩罚"的直观体现。
2.3 熔断不是一刀切,而是分级降档
账户级风控的核心是"熔断",但熔断的实现水平差别很大。粗糙的做法是"日亏2%就停一天",问题在于它把"正常的坏运气"和"策略已经失效"混为一谈,前者停了可惜,后者停得太晚。更合理的设计是分级降档:亏损越深,仓位越小,直到停止交易,并且有明确的恢复路径。
| 级别 | 触发条件 | 仓位系数 | 动作 |
|---|---|---|---|
| L0 正常 | 无异常 | 1.00 | 正常开仓 |
| L1 警戒 | 当日亏损达2% | 0.50 | 暂停新开仓4小时 |
| L2 收缩 | 连续5笔亏损或周回撤5% | 0.25 | 仅允许趋势方向单笔 |
| L3 停摆 | 净值自峰值回撤10% | 0.00 | 只平不开,冷却24小时 |
// 根据净值回撤返回仓位系数
double RiskFactorByDrawdown(const double equityPeak, const double equityNow)
{
if(equityPeak <= 0.0) return 0.0;
double ddPct = (equityPeak - equityNow) / equityPeak * 100.0;
if(ddPct >= 10.0) return 0.00; // L3:只平不开
if(ddPct >= 5.0) return 0.25; // L2
if(ddPct >= 2.0) return 0.50; // L1
return 1.00; // L0
}
2.4 保证金水平是硬约束,不是参考值
ACCOUNT_MARGIN_LEVEL(保证金水平)是账户级风控里容易被忽略的一环。当这个数值跌破100(净值等于已用保证金)时,经纪商会开始强平,而强平的顺序和时机不由你控制,往往在不利的位置成交。合理的做法是在远高于强平线的位置就主动拦截:保证金水平低于300%时禁止新开仓,低于200%时按比例减仓到50%,低于150%时主动清仓。主动清仓的成本是可控的点差,被动强平的成本是不可控的滑点。
三、L3系统级:容灾与MT5底层机制避坑
3.1 重启后的状态重建,比风控逻辑本身更关键
VPS重启、终端更新、EA被重新挂载,这些在实盘中是常态。问题在于:EA内部维护的仓位状态、网格层数、加仓计数,全都是内存里的变量,进程一停就没了。而服务器上的持仓还在。如果OnInit里不做状态重建,重启后的EA会认为自己是空仓,于是重新开始开仓,网格层数从头计数——这就是"重启一次,仓位翻一倍"的来源。
void ReconcileOnInit()
{
// 以服务器持仓为事实基准,重建内部状态
int total = PositionsTotal();
for(int i = 0; i < total; i++)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
g_risk.RegisterPosition(ticket,
PositionGetDouble(POSITION_VOLUME),
PositionGetDouble(POSITION_PRICE_OPEN),
PositionGetDouble(POSITION_SL));
}
// 内部记录里存在、服务器上已不存在的仓位,标记为幽灵并清理
g_risk.PurgeGhosts();
// 重新计算净值峰值、当日起始净值等统计量
g_risk.RestoreStats();
PrintFormat("[Reconcile] 已恢复 %d 笔持仓,当前净值 %.2f", total, AccountInfoDouble(ACCOUNT_EQUITY));
}
ACCOUNT_EQUITY的历史峰值一起恢复,否则回撤类熔断在重启后会从零开始计算,等于每次重启都把保险丝换了一根新的。我们的做法是把峰值净值、当日起始净值、连续亏损笔数写入终端的全局变量(GlobalVariableSet),命名带上EA名和品种名,重启后读回来。全局变量会持久化在终端目录里,比内存变量可靠得多。
3.2 OnTradeTransaction才是可信的成交事实来源
一个常见的错误是用OrderSend的返回值判断是否成交。返回值只告诉你"请求是否被受理",不告诉你"是否成交、成交多少、成交价多少"。真正的事实来源是OnTradeTransaction,尤其是TRADE_TRANSACTION_DEAL_ADD这个类型——每一笔成交都会产生一条deal记录,它才是账本。
void OnTradeTransaction(const MqlTradeTransaction& trans,
const MqlTradeRequest& request,
const MqlTradeResult& result)
{
// 成交事实:以deal为准,不用OrderSend的返回值判断
if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
{
ulong dealTicket = trans.deal;
if(!HistoryDealSelect(dealTicket)) return;
if(HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != InpMagic) return;
double dealPrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
double dealVol = HistoryDealGetDouble(dealTicket, DEAL_VOLUME);
long entry = HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
// 把成交价交给风控层,用于滑点监控与降档判断
g_risk.OnDeal(dealTicket, dealPrice, dealVol, entry);
}
// 请求结果:只用于错误分类与重试决策
if(trans.type == TRADE_TRANSACTION_REQUEST)
{
if(result.retcode != TRADE_RETCODE_DONE && result.retcode != TRADE_RETCODE_PLACED)
g_risk.OnRejected(result.retcode);
}
}
3.3 幽灵订单与超时重试
幽灵订单指的是"请求超时但服务器实际已经成交"的情况。网络抖动时OrderSend可能返回超时,此时服务器端的订单状态是未知的。如果代码里写成"失败就重发",结果就是同一笔信号开出两倍仓位。正确的处理顺序是:等待一个固定的观察窗口(比如3秒)→ 按品种、魔术号、注释查询持仓与历史订单 → 确认不存在对应记录后才重发 → 重发次数设上限(比如3次),超过上限就转为告警并停止该信号。
3.4 流动性枯竭时的滑点控制
我们在一份非农夜间的实盘日志里测到的数据(eafxtech实盘交易日志脱敏统计,XAUUSD,2025年多次数据公布时段):常态点差8到12点,数据公布瞬间扩张到55到70点;市价单的平均滑点45点,峰值出现在公布后0.8秒内;从发单到收到成交回报的延迟峰值约1200毫秒。这1200毫秒意味着什么?意味着你看到的报价和成交时的报价,可能已经不在同一个价位区间里了。
工程上的应对有三件事。其一是点差闸门:在发单前检查SYMBOL_SPREAD,超过阈值直接放弃本次信号,这个阈值建议按品种分别配置,黄金可以放到40点,主流货币对放到20点。其二是滑点预算:SetDeviationInPoints设成一个可接受的值,让服务器在超出预算时拒单,而不是以任意价格成交。其三是成交价偏差监控:在OnDeal里比较请求价与成交价,偏差超过预算就记一笔,当日累计超过三次自动降一档仓位。
ORDER_FILLING_FOK要求整笔立即成交,否则整笔取消;ORDER_FILLING_IOC允许部分成交;ORDER_FILLING_RETURN则用于限价单挂单。不同品种支持的组合不一样,写死一种会在某些品种上持续被拒。用SYMBOL_FILLING_MODE查询后动态选择,是省事的做法。
3.5 心跳与健康自检
系统级防御还需要一层"自我感知"。EA应该定期检查自己是否还有能力交易:终端是否连上服务器(TERMINAL_CONNECTED)、账户是否允许交易(ACCOUNT_TRADE_ALLOWED)、品种是否处于可交易状态(SYMBOL_TRADE_MODE)、报价是否还在更新(tick时间戳是否停滞)。任何一项异常,都应当让EA进入只读模式:不再开新仓,但继续管理已有持仓。
四、实战复盘:一个多层网格EA的非农夜
下面这个案例来自我们接触过的一个真实账户(细节做脱敏处理,金额按比例调整)。账户规模20万美元,跑的是XAUUSD多层网格EA,参数是:网格间距1.5美元,倍投系数1.3,层数上限12层,单层起始手数0.05手。风控方面只做了一件事:在EA内部用Bid价格判断浮亏,超过设定的美元金额就平掉该层。没有账户级总风险上限,没有波动率过滤,没有数据日历。
| 时间 | 事件 | 后果 |
|---|---|---|
| 20:29 | 已挂满12层买单,累计0.9手,浮亏轻微 | 账户保证金水平约480% |
| 20:30:00 | 数据公布,点差从10点扩张至180点 | 浮亏显示失真,EA仍在按Bid判断 |
| 20:30:03 | 金价3秒内下行22美元,穿越4层网格 | 层数直接打满,手数升至1.4手 |
| 20:30:05 | EA软止损判断触发,但tick间隔800毫秒 | 错过首轮反应,价格继续下行 |
| 20:30:11 | 平仓请求到达服务器,滑点42点 | 实际亏损超出预期1.6倍 |
| 20:31 | 保证金水平跌破100 | 经纪商逐笔强平,账户损失约34% |
事后复盘,这套EA在风控上有四个致命缺口,每一个单独拿出来都不算大问题,叠在一起就变成了灾难。
缺口一:没有账户级总风险上限。网格类EA的加仓是"越亏越加",12层打满之后,账户承受的已经不是单笔风险,而是一个方向性押注。如果当时有一个"总持仓风险不超过净值6%"的账户级闸门,第7层就会被拦住,后面的连锁反应不会发生。
缺口二:只有软止损,没有硬止损。这些保护都依赖EA进程正常运行。当tick间隔拉长到800毫秒,软止损的判断周期就被动变成了800毫秒一次,而在那种行情里,800毫秒足够价格再走十几美元。服务器端的硬止损不依赖EA运行,也不受tick频率影响。
缺口三:加仓没有波动率过滤。网格参数是按低波动环境标定的,1.5美元的间距在亚盘很合理,在数据公布的3秒里等于没有间距。加一个"ATR超过20日均值2倍时停止加仓"的判断,只需要几行代码。
缺口四:没有事件日历。非农、CPI、利率决议这些时点是公开信息,提前30分钟停止开仓、把已有仓位降到可控水平,是成本极低的保护。这一条不需要任何高深技术,只需要一份日历和一个时间判断。
五、架构实现:把三层防御做成一个漏斗
讲完三层防御,接下来是工程问题:怎么把它们组织进代码,而不是散落在OnTick里的一堆if。我们采用的结构是"漏斗":任何开仓信号都需要依次穿过三个检查点,任何一层拒绝就终止,不允许绕过。
策略产生信号 Signal(品种、方向、止损点数、目标仓位)
↓
L1 订单级闸门:止损距离是否合规、点差是否超限、手数是否规整、是否重复信号
↓
L2 账户级闸门:单笔风险占比、账户总风险、回撤档位、保证金水平、当日亏损
↓
L3 系统级闸门:连接状态、报价新鲜度、健康分、事件日历、成交模式可用性
↓
执行器 Executor(全工程仅此一处调用 OrderSend)
class CRiskManager
{
private:
double m_riskPct; // 单笔风险占比
double m_totalRiskCap; // 账户总风险上限
double m_equityPeak; // 净值峰值
double m_dayStartEquity; // 当日起始净值
int m_consecLoss; // 连续亏损笔数
double m_slippageBudget; // 滑点预算(点)
ENUM_RISK_PROFILE m_profile; // 震荡 / 趋势 / 黑天鹅
public:
bool CheckL1_Order(const string symbol, const double stopPoints,
double &lots, string &reason);
bool CheckL2_Account(const string symbol, const double lots,
string &reason);
bool CheckL3_System(const string symbol, string &reason);
void OnDeal(const ulong dealTicket, const double price,
const double volume, const long entry);
void OnRejected(const uint retcode);
void OnNewDay();
void ReconcileOnInit();
};
// 策略层只产生信号,不直接下单
void OnSignal(const Signal &sig)
{
string reason;
double lots = 0.0;
if(!g_risk.CheckL1_Order(sig.symbol, sig.stopPoints, lots, reason))
{ LogOnce("L1拒绝", reason); return; }
if(!g_risk.CheckL2_Account(sig.symbol, lots, reason))
{ LogOnce("L2拒绝", reason); return; }
if(!g_risk.CheckL3_System(sig.symbol, reason))
{ LogOnce("L3拒绝", reason); return; }
// 全工程仅此一处调用 OrderSend
g_executor.Send(sig.symbol, sig.type, lots, sig.stopPoints);
}
这个结构看起来只是多了一层封装,但它带来的改变是结构性的。风控逻辑从"散落在交易逻辑里的一堆判断"变成了"一个可以被单独测试、单独替换、单独审计的模块"。你可以写一个脚本,构造各种极端场景(净值骤降、点差异常、断线重连)去调用CheckL2_Account,验证它是否会正确拒绝——这种测试在没有封装的代码里几乎不可能做。
OrderSend和PositionOpen,如果出现次数大于1,就说明还有旁路。风控一旦存在旁路,就等于不存在——因为总有一天会有一个"特殊情况"从旁路溜过去,而那一次通常代价高。
六、参数配置Checklist与状态切换机制
风控参数不能是一套固定值,因为市场状态在变。我们的做法是准备三套Profile,分别对应震荡、趋势和黑天鹅三种状态,参数差异主要体现在单笔风险、止损乘数、点差闸门和开仓权限上。
| 参数 | 震荡Profile | 趋势Profile | 黑天鹅Profile |
|---|---|---|---|
| 单笔风险占比 | 1.0% | 1.5% | 0.3% |
| ATR止损乘数 | 1.5 | 2.5 | 3.5 |
| 账户总风险上限 | 4% | 6% | 2% |
| 点差闸门(货币对) | 20点 | 25点 | 12点 |
| 点差闸门(黄金) | 40点 | 50点 | 25点 |
| 日亏损熔断 | 2% | 3% | 1% |
| 网格/加仓 | 允许 | 关闭 | 关闭 |
| 新开仓 | 允许 | 允许 | 禁止 |
切换机制建议做成"自动触发 + 手动覆盖"双通道。自动触发看三个指标:ATR(14)在过去100根K线中的分位数超过90%、当前点差超过20日均值的2倍、健康分低于60。任意两项同时命中,就切到黑天鹅Profile。手动覆盖用于已知事件:数据公布前30分钟、周末收盘前2小时、重大节假日,直接手动置位,不需要等指标触发。
1. 硬止损全覆盖:全工程搜索下单调用,确认每一笔开仓都带了SL参数,没有一笔是裸单。
2. 下单入口单点化:OrderSend与PositionOpen的调用点数量为1,其余统一走风控漏斗。
3. 状态可重建:OnInit里有Reconcile逻辑,且峰值净值、连续亏损笔数写入了全局变量。
4. 成交以deal为准:OnTradeTransaction里处理了DEAL_ADD,滑点数据有落库或落日志。
5. 重试有幂等校验:超时路径先查持仓再重发,重试次数有上限,超限告警。
6. 三层闸门齐全:L1/L2/L3三个检查函数都存在,且每一层都有独立的拒绝日志与拒绝计数。
7. 极端场景可复现:至少有一个人工构造的测试用例,能在策略测试器或模拟环境里复现点差扩张与断线重连。
七、结语:活得久,比赚得快重要
回到开头那个数据:65%的爆仓来自滑点与流动性,25%来自代码漏洞。这两类问题都不是"市场不可预测"能解释的,它们是可以在设计阶段就被处理掉的。订单级的硬止损、账户级的熔断降档、系统级的状态重建与幂等重试,这三层加起来不到一千行代码,但它们决定了你的策略有没有机会活到期望值兑现的那一天。
我们在实盘账户上跑这套三层漏斗超过六个月,收益回撤比(Calmar)从1.2提升到2.8,收益总额并没有变得更高,变化发生在回撤端——峰值回撤被压掉了一半以上。这个结果其实很符合直觉:风控不创造收益,它只是把那些会一次性抹掉你长期积累的尾部事件挡在门外。
敬畏市场,也敬畏自己写下的每一行代码。风控不是EA的附加项,而是它少有的、真正的护城河。
扫码关注,获取更多EA开发干货
微信公众号
视频号
