diff --git a/tag/MTCommander盈亏统计指标.ex5 b/tag/MTCommander盈亏统计指标.ex5 new file mode 100644 index 0000000..1be05a7 Binary files /dev/null and b/tag/MTCommander盈亏统计指标.ex5 differ diff --git a/马丁策略/README.md b/马丁策略/README.md new file mode 100644 index 0000000..ed56ee8 --- /dev/null +++ b/马丁策略/README.md @@ -0,0 +1,25 @@ +# 马丁网格策略 + +## 基础概念 +- 平台:MQL5 +- 品种:支持跟随图表窗口 +- 所有日志、参数说明均需使用中文 + +## 辅助指标 +- 移动平均线(Moving Average) + +## 核心逻辑 +- K线周期:支持参数设置,默认为1min +- 突破K线:开仓价在均线之下,收盘价在均线之上,称之为多单突破K线,反之亦然 +- 出现突破K线时,开首单,如果已有同方向单,不开首单 +- 价格往反方向运行 首单价*补仓比例(支持参数设置) 时,即为满足加仓条件 +- 首单价开仓手数支持参数设置 +- 加仓放大手数支持参数设置,默认为 0.05手,相比上一组的手数 + 加仓放大手数;**不是每一网格档都乘一次**,而是与下条「档间隔」配合使用。 +- **档间隔(参数 N)**:控制隔多少档才把「加仓放大手数」用上一次。例如 **N = 5** 表示每 **5 个网格档**为一组,**组内手数相同**;每进入新的一组,手数 = **上一组手数 + 加仓放大手数**(首组 = 首单手数)。用于减缓手数膨胀。 +- 平仓逻辑:使用统盈的策略,多单止盈价 = 加权平均成本 + 加权平均成本 * 止盈比例(支持参数设置),反之亦然 +- 多空单互不影响 +- 支持参数设置是否复利,复利开启后,只会影响首单的开仓手数,不影响加仓的开仓手数 + +## 补充逻辑 +- 单向持仓已达「单向最大档数」时,**每个 Tick** 比较该向**浮动盈亏**(各单获利+库存费之和,账户货币)与 **账户结余 × 浮盈比例**(参数 `InpMaxLayersFloatPct`,百分比;为 **0** 表示关闭本规则)。若浮盈不低于阈值,则**平掉该方向、本品种、本 EA Magic 下全部持仓**,随后可再由突破逻辑开新首单。 +- **结余**指 `ACCOUNT_BALANCE`。与统盈可同时存在:若价格已达统盈会先整篮平掉,否则在满档下仍可能触发本条。 diff --git a/马丁策略/zyb.mading.mq5 b/马丁策略/zyb.mading.mq5 new file mode 100644 index 0000000..ea6091f --- /dev/null +++ b/马丁策略/zyb.mading.mq5 @@ -0,0 +1,562 @@ +//+------------------------------------------------------------------+ +//| 马丁网格策略 EA — 逻辑见同目录 README.md | +//| 需对冲账户(同一方向可多笔持仓);日志与参数说明为中文。 | +//+------------------------------------------------------------------+ +#property copyright "zyb-ea" +#property version "1.00" +#property description "马丁网格:MA 突破首单 + 首单价比例间距补仓 + 档间隔每组+#加仓放大手数 + 统盈;可选复利仅首单。" + +#include + +CTrade g_trade; + +int g_hMa = INVALID_HANDLE; + +//------------------------------------------------------------ +input group "工作区" +input ENUM_TIMEFRAMES InpWorkTF = PERIOD_M1; // 工作周期(K 线:突破判定、新 K 检测;默认 1 分钟) +input int InpSlippage = 30; // 滑点(点) +input int InpMaxSpreadPoints = 0; // 最大点差(点,0=不限制) +input long InpMagic = 20260507; // Magic 识别码 + +input group "移动平均线" +input int InpMAPeriod = 20; // MA 周期 +input int InpMAShift = 0; // MA 位移 +input ENUM_MA_METHOD InpMAMethod = MODE_SMA; // MA 算法 +input ENUM_APPLIED_PRICE InpMAPrice = PRICE_CLOSE; // 应用于 + +input group "头寸与补仓" +input double InpFirstLot = 0.01; // 首单手数(复利关闭时为固定手数) +input double InpStepPercent = 2.0; // 补仓比例(%):每档价距 = 首单开仓价 × 本参数%(相对上一同向开仓反向计数) +input double InpAddLotBoost = 0.05; // 加仓放大手数:每新一组在上一组手数上 + 本参数(组内 N 档相同;首组=首单) +input int InpTierInterval = 5; // 档间隔 N:每 N 个网格档为一组,组满后下一组手数 + 加仓放大手数 +input int InpMaxLayers = 50; // 单向最大档数(含首单,安全上限) + +input group "统盈平仓" +input double InpTpPercent = 10.0; // 止盈比例(%):多单目标 = 加权成本 × (1+本%);空单反之 + +input group "补充逻辑(满档浮盈)" +input double InpMaxLayersFloatPct = 0.0; // 浮盈比例(%,相对账户结余):该向已达「单向最大档」时,若该向浮盈(含库存费)≥结余×本%/100 则平掉该向全仓;0=关闭 + +input group "复利(仅首单)" +input bool InpUseCompound = false; // 开启后只按比例放大「首单」手数;加仓档仍用本节基准手数递推 +input double InpCompoundRefEquity = 10000.0; // 参考净值:首单手数 ≈ InpFirstLot × (当前净值 / 参考净值) + +input group "其它" +input bool InpLog = true; // 是否打印日志 + +//------------------------------------------------------------ +double g_longBaseLot = 0.0; // 本轮回多单首单基准手数(加仓按此递推) +double g_shortBaseLot = 0.0; +static datetime g_lastWorkBar = 0; + +//------------------------------------------------------------ +double MinLot() { return SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); } +double MaxLot() { return SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); } +double LotStep() { return SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); } + +double NormalizeVolumeVal(const double v) +{ + const double lo = MinLot(); + const double hi = MaxLot(); + const double st = LotStep(); + if(st <= 0.0 || hi < lo) + return v; + if(v < lo - 1e-12) + return 0.0; + double x = MathMin(v, hi); + x = MathFloor(x / st + 1e-12) * st; + return x; +} + +int SpreadPoints() +{ + return (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); +} + +//------------------------------------------------------------ +// 复利仅影响首单:返回本轮「基准手数」,加仓用 LotForTier(..., 此基准) +double CalcBasketBaseLot() +{ + double lot = InpFirstLot; + if(InpUseCompound && InpCompoundRefEquity > 1e-8) + { + const double eq = AccountInfoDouble(ACCOUNT_EQUITY); + lot = InpFirstLot * (eq / InpCompoundRefEquity); + } + return NormalizeVolumeVal(lot); +} + +// tierIndex:0=首单,1=第 2 笔 …;grp=tierIndex/N 为组号,组内手数相同,每组手数=首单基准+grp×加仓放大手数 +double LotForTier(const int tierIndex, const double basketBaseLot) +{ + if(tierIndex < 0 || basketBaseLot <= 0.0) + return 0.0; + const int n = MathMax(1, InpTierInterval); + const int grp = tierIndex / n; + if(InpAddLotBoost <= 0.0) + return NormalizeVolumeVal(basketBaseLot); + double vol = basketBaseLot + (double)grp * InpAddLotBoost; + vol = MathMin(vol, MaxLot()); + return NormalizeVolumeVal(vol); +} + +//------------------------------------------------------------ +int CountPositions(const long typeFilter) // POSITION_TYPE_BUY / SELL,-1 表示不限方向但同 magic 同品种 +{ + int c = 0; + const int total = (int)PositionsTotal(); + for(int i = 0; i < total; i++) + { + const ulong ticket = PositionGetTicket(i); + if(ticket == 0 || !PositionSelectByTicket(ticket)) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if((long)PositionGetInteger(POSITION_MAGIC) != InpMagic) + continue; + const long typ = (long)PositionGetInteger(POSITION_TYPE); + if(typeFilter >= 0 && typ != typeFilter) + continue; + c++; + } + return c; +} + +bool GetBasketEdges(const long posType, double &firstOpen, double &lastOpen, datetime &firstTime, datetime &lastTime) +{ + firstOpen = 0.0; + lastOpen = 0.0; + firstTime = 0; + lastTime = 0; + bool any = false; + const int total = (int)PositionsTotal(); + for(int i = 0; i < total; i++) + { + const ulong ticket = PositionGetTicket(i); + if(ticket == 0 || !PositionSelectByTicket(ticket)) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if((long)PositionGetInteger(POSITION_MAGIC) != InpMagic) + continue; + if((long)PositionGetInteger(POSITION_TYPE) != posType) + continue; + const double op = PositionGetDouble(POSITION_PRICE_OPEN); + const datetime tm = (datetime)PositionGetInteger(POSITION_TIME); + if(!any) + { + firstOpen = lastOpen = op; + firstTime = lastTime = tm; + any = true; + } + else + { + if(tm <= firstTime) + { + firstTime = tm; + firstOpen = op; + } + if(tm >= lastTime) + { + lastTime = tm; + lastOpen = op; + } + } + } + return any; +} + +bool BasketAvgPrice(const long posType, double &avg, double &sumLots) +{ + avg = 0.0; + sumLots = 0.0; + double sumPxVol = 0.0; + const int total = (int)PositionsTotal(); + for(int i = 0; i < total; i++) + { + const ulong ticket = PositionGetTicket(i); + if(ticket == 0 || !PositionSelectByTicket(ticket)) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if((long)PositionGetInteger(POSITION_MAGIC) != InpMagic) + continue; + if((long)PositionGetInteger(POSITION_TYPE) != posType) + continue; + const double v = PositionGetDouble(POSITION_VOLUME); + const double p = PositionGetDouble(POSITION_PRICE_OPEN); + sumPxVol += p * v; + sumLots += v; + } + if(sumLots <= 1e-12) + return false; + avg = sumPxVol / sumLots; + return true; +} + +// 该向持仓浮动盈亏(账户货币,含库存费) +double BasketFloatingPL(const long posType) +{ + double sum = 0.0; + const int total = (int)PositionsTotal(); + for(int i = 0; i < total; i++) + { + const ulong ticket = PositionGetTicket(i); + if(ticket == 0 || !PositionSelectByTicket(ticket)) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if((long)PositionGetInteger(POSITION_MAGIC) != InpMagic) + continue; + if((long)PositionGetInteger(POSITION_TYPE) != posType) + continue; + sum += PositionGetDouble(POSITION_PROFIT); + sum += PositionGetDouble(POSITION_SWAP); + } + return sum; +} + +bool CloseAllDirection(const long posType, const string reason) +{ + ulong tickets[]; + int n = 0; + const int total = (int)PositionsTotal(); + for(int i = 0; i < total; i++) + { + const ulong ticket = PositionGetTicket(i); + if(ticket == 0 || !PositionSelectByTicket(ticket)) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if((long)PositionGetInteger(POSITION_MAGIC) != InpMagic) + continue; + if((long)PositionGetInteger(POSITION_TYPE) != posType) + continue; + ArrayResize(tickets, n + 1); + tickets[n++] = ticket; + } + g_trade.SetExpertMagicNumber((ulong)InpMagic); + g_trade.SetDeviationInPoints((uint)InpSlippage); + bool ok = true; + for(int j = 0; j < n; j++) + { + if(!g_trade.PositionClose(tickets[j], (uint)InpSlippage)) + { + ok = false; + if(InpLog) + Print("平仓失败 ticket=", tickets[j], " err=", GetLastError()); + } + } + if(InpLog && ok && n > 0) + Print("批量平仓 ", (posType == POSITION_TYPE_BUY ? "多" : "空"), " 笔数=", n, " 原因: ", reason); + return ok; +} + +//------------------------------------------------------------ +void CheckBasketTakeProfit() +{ + const int nb = CountPositions(POSITION_TYPE_BUY); + if(nb > 0) + { + double avg = 0.0, sumL = 0.0; + if(BasketAvgPrice(POSITION_TYPE_BUY, avg, sumL)) + { + const double target = avg * (1.0 + InpTpPercent / 100.0); + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + if(bid >= target) + CloseAllDirection(POSITION_TYPE_BUY, "多单统盈"); + } + } + + const int ns = CountPositions(POSITION_TYPE_SELL); + if(ns > 0) + { + double avg = 0.0, sumL = 0.0; + if(BasketAvgPrice(POSITION_TYPE_SELL, avg, sumL)) + { + const double target = avg * (1.0 - InpTpPercent / 100.0); + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + if(ask <= target) + CloseAllDirection(POSITION_TYPE_SELL, "空单统盈"); + } + } + + if(CountPositions(POSITION_TYPE_BUY) == 0) + g_longBaseLot = 0.0; + if(CountPositions(POSITION_TYPE_SELL) == 0) + g_shortBaseLot = 0.0; +} + +// 已达单向最大档时:浮盈 ≥ 结余×比例 则清该向仓位(重头在平仓后由突破逻辑再开) +void CheckMaxLayersBalanceFloatExit() +{ + if(InpMaxLayersFloatPct <= 0.0) + return; + + const double bal = AccountInfoDouble(ACCOUNT_BALANCE); + if(bal <= 0.0) + return; + + const double need = bal * (InpMaxLayersFloatPct / 100.0); + + const int nb = CountPositions(POSITION_TYPE_BUY); + if(nb == InpMaxLayers) + { + const double fpl = BasketFloatingPL(POSITION_TYPE_BUY); + if(fpl >= need) + { + if(InpLog) + Print("满档多单补充平仓: 浮盈=", fpl, " 阈值(结余×", InpMaxLayersFloatPct, "%)=", need); + CloseAllDirection(POSITION_TYPE_BUY, "满档-浮盈达结余×设定比例"); + } + } + + const int ns = CountPositions(POSITION_TYPE_SELL); + if(ns == InpMaxLayers) + { + const double fpl = BasketFloatingPL(POSITION_TYPE_SELL); + if(fpl >= need) + { + if(InpLog) + Print("满档空单补充平仓: 浮盈=", fpl, " 阈值(结余×", InpMaxLayersFloatPct, "%)=", need); + CloseAllDirection(POSITION_TYPE_SELL, "满档-浮盈达结余×设定比例"); + } + } + + if(CountPositions(POSITION_TYPE_BUY) == 0) + g_longBaseLot = 0.0; + if(CountPositions(POSITION_TYPE_SELL) == 0) + g_shortBaseLot = 0.0; +} + +//------------------------------------------------------------ +bool CopyBar1OcMa(double &o1, double &c1, double &ma1) +{ + double o[], c[]; + ArraySetAsSeries(o, true); + ArraySetAsSeries(c, true); + if(CopyOpen(_Symbol, InpWorkTF, 1, 1, o) != 1) + return false; + if(CopyClose(_Symbol, InpWorkTF, 1, 1, c) != 1) + return false; + double m[]; + ArraySetAsSeries(m, true); + if(CopyBuffer(g_hMa, 0, 1, 1, m) != 1) + return false; + o1 = o[0]; + c1 = c[0]; + ma1 = m[0]; + return true; +} + +// 新 K 线开盘时:用上一根已收盘 K 做突破判定;仅在无同向持仓时开首单 +void TryOpenFirstOnBreakout() +{ + if(g_hMa == INVALID_HANDLE) + return; + + double o1 = 0.0, c1 = 0.0, ma1 = 0.0; + if(!CopyBar1OcMa(o1, c1, ma1)) + return; + + const bool longBreak = (o1 < ma1 && c1 > ma1); + const bool shortBreak = (o1 > ma1 && c1 < ma1); + + g_trade.SetExpertMagicNumber((ulong)InpMagic); + g_trade.SetDeviationInPoints((uint)InpSlippage); + + if(longBreak && CountPositions(POSITION_TYPE_BUY) == 0) + { + g_longBaseLot = CalcBasketBaseLot(); + if(g_longBaseLot >= MinLot() - 1e-12) + { + const string cmt = "马丁-多首单-突破"; + if(g_trade.Buy(g_longBaseLot, _Symbol, 0.0, 0.0, 0.0, cmt)) + { + if(InpLog) + Print("开多单首单 手数=", g_longBaseLot); + } + else if(InpLog) + Print("开多单首单失败 err=", GetLastError()); + } + else + { + if(InpLog) + Print("多单首单手数过小,跳过"); + g_longBaseLot = 0.0; + } + } + + if(shortBreak && CountPositions(POSITION_TYPE_SELL) == 0) + { + g_shortBaseLot = CalcBasketBaseLot(); + if(g_shortBaseLot >= MinLot() - 1e-12) + { + const string cmt = "马丁-空首单-突破"; + if(g_trade.Sell(g_shortBaseLot, _Symbol, 0.0, 0.0, 0.0, cmt)) + { + if(InpLog) + Print("开空单首单 手数=", g_shortBaseLot); + } + else if(InpLog) + Print("开空单首单失败 err=", GetLastError()); + } + else + { + if(InpLog) + Print("空单首单手数过小,跳过"); + g_shortBaseLot = 0.0; + } + } +} + +//------------------------------------------------------------ +void TryGridAddBuy() +{ + const int n = CountPositions(POSITION_TYPE_BUY); + if(n < 1 || n >= InpMaxLayers) + return; + double fo = 0.0, lo = 0.0; + datetime ft, lt; + if(!GetBasketEdges(POSITION_TYPE_BUY, fo, lo, ft, lt)) + return; + if(g_longBaseLot <= 0.0) + g_longBaseLot = CalcBasketBaseLot(); // 若未设置(如手工单),尽力 fallback + + const double dPrice = fo * (InpStepPercent / 100.0); + if(dPrice <= 0.0) + return; + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + + if(bid <= lo - dPrice) + { + const int nextTier = n; // 当前已有 n 笔,下一笔档位索引为 n(首单为 0) + double vol = LotForTier(nextTier, g_longBaseLot); + if(vol < MinLot() - 1e-12) + { + if(InpLog) + Print("多单加仓手数过小 tier=", nextTier, " 跳过"); + return; + } + g_trade.SetExpertMagicNumber((ulong)InpMagic); + g_trade.SetDeviationInPoints((uint)InpSlippage); + const bool lastToCap = (n + 1 == InpMaxLayers); // 本次成交后达到单向最大档数(含首单) + const string cmt = lastToCap ? "马丁-多加仓-已达最大档(末笔·不再加仓)" : "马丁-多加仓"; + if(g_trade.Buy(vol, _Symbol, 0.0, 0.0, 0.0, cmt)) + { + if(InpLog) + Print("多加仓 第", (nextTier + 1), "/", InpMaxLayers, "笔 手数=", vol, + (lastToCap ? " 【已达单向最大档,后续不再加仓】" : ""), + " bid=", bid, " 上开=", lo, " 首开=", fo); + } + else if(InpLog) + Print("多加仓失败 err=", GetLastError()); + } +} + +void TryGridAddSell() +{ + const int n = CountPositions(POSITION_TYPE_SELL); + if(n < 1 || n >= InpMaxLayers) + return; + double fo = 0.0, lo = 0.0; + datetime ft, lt; + if(!GetBasketEdges(POSITION_TYPE_SELL, fo, lo, ft, lt)) + return; + if(g_shortBaseLot <= 0.0) + g_shortBaseLot = CalcBasketBaseLot(); + + const double dPrice = fo * (InpStepPercent / 100.0); + if(dPrice <= 0.0) + return; + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + + if(ask >= lo + dPrice) + { + const int nextTier = n; + double vol = LotForTier(nextTier, g_shortBaseLot); + if(vol < MinLot() - 1e-12) + { + if(InpLog) + Print("空单加仓手数过小 tier=", nextTier, " 跳过"); + return; + } + g_trade.SetExpertMagicNumber((ulong)InpMagic); + g_trade.SetDeviationInPoints((uint)InpSlippage); + const bool lastToCap = (n + 1 == InpMaxLayers); + const string cmt = lastToCap ? "马丁-空加仓-已达最大档(末笔·不再加仓)" : "马丁-空加仓"; + if(g_trade.Sell(vol, _Symbol, 0.0, 0.0, 0.0, cmt)) + { + if(InpLog) + Print("空加仓 第", (nextTier + 1), "/", InpMaxLayers, "笔 手数=", vol, + (lastToCap ? " 【已达单向最大档,后续不再加仓】" : ""), + " ask=", ask, " 上开=", lo, " 首开=", fo); + } + else if(InpLog) + Print("空加仓失败 err=", GetLastError()); + } +} + +//------------------------------------------------------------ +int OnInit() +{ + g_trade.SetExpertMagicNumber((ulong)InpMagic); + g_trade.SetDeviationInPoints((uint)InpSlippage); + const int filling = (int)SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE); + if((filling & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK) + g_trade.SetTypeFilling(ORDER_FILLING_FOK); + else if((filling & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC) + g_trade.SetTypeFilling(ORDER_FILLING_IOC); + else + g_trade.SetTypeFilling(ORDER_FILLING_RETURN); + + g_hMa = iMA(_Symbol, InpWorkTF, InpMAPeriod, InpMAShift, InpMAMethod, InpMAPrice); + if(g_hMa == INVALID_HANDLE) + { + Print("初始化 MA 句柄失败"); + return INIT_FAILED; + } + g_lastWorkBar = 0; + g_longBaseLot = 0.0; + g_shortBaseLot = 0.0; + if(InpLog) + Print("马丁网格 EA 初始化: ", _Symbol, " 工作周期=", EnumToString(InpWorkTF), + " MA=", InpMAPeriod, + " 补仓%=", InpStepPercent, + " 加仓放大手数=", InpAddLotBoost, " 档间隔=", InpTierInterval, + " 统盈%=", InpTpPercent, + " 满档浮盈阈值%=", InpMaxLayersFloatPct, + " 复利=", (InpUseCompound ? "开" : "关")); + return INIT_SUCCEEDED; +} + +void OnDeinit(const int reason) +{ + if(g_hMa != INVALID_HANDLE) + { + IndicatorRelease(g_hMa); + g_hMa = INVALID_HANDLE; + } +} + +void OnTick() +{ + if(InpMaxSpreadPoints > 0 && SpreadPoints() > InpMaxSpreadPoints) + return; + + CheckBasketTakeProfit(); + CheckMaxLayersBalanceFloatExit(); + + const datetime bar0 = iTime(_Symbol, InpWorkTF, 0); + if(bar0 != 0 && bar0 != g_lastWorkBar) + { + g_lastWorkBar = bar0; + TryOpenFirstOnBreakout(); + } + + TryGridAddBuy(); + TryGridAddSell(); +} + +//+------------------------------------------------------------------+