From 2fff9455793d6e14416ce35818e7372c054ac8d2 Mon Sep 17 00:00:00 2001 From: zhangyangbin Date: Mon, 11 May 2026 11:08:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=9D=E5=AD=98=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 20260509-突破策略/README.md | 21 + 20260509-突破策略/zyb.20260509.breakout.mq5 | 420 ++++++++++ KVB-50036106/20260425.set | Bin 0 -> 2086 bytes KVB-50036106/Gold Snap 300000C.set | Bin 0 -> 1668 bytes KVB-50036106/Gold Snap 500$.set | Bin 0 -> 1670 bytes KVB-50036106/Quantum Queen MT5_3.52_fix.set | Bin 0 -> 2262 bytes tag/20260425-马丁网格策略/20260425.ea.mq5 | 804 ++++++++++++++++++++ tag/20260425-马丁网格策略/README.md | 34 + 8 files changed, 1279 insertions(+) create mode 100644 20260509-突破策略/README.md create mode 100644 20260509-突破策略/zyb.20260509.breakout.mq5 create mode 100644 KVB-50036106/20260425.set create mode 100644 KVB-50036106/Gold Snap 300000C.set create mode 100644 KVB-50036106/Gold Snap 500$.set create mode 100644 KVB-50036106/Quantum Queen MT5_3.52_fix.set create mode 100644 tag/20260425-马丁网格策略/20260425.ea.mq5 create mode 100644 tag/20260425-马丁网格策略/README.md diff --git a/20260509-突破策略/README.md b/20260509-突破策略/README.md new file mode 100644 index 0000000..484af03 --- /dev/null +++ b/20260509-突破策略/README.md @@ -0,0 +1,21 @@ +# 均线突破策略 + +## 基础概念 +- 平台:MQL5 +- 品种:XAU + +## 辅助指标 +- 移动平均线(Moving Average) +- 周期默认设置为:14 + +## 核心逻辑 +- K线周期:支持参数设置,默认为1min +- 突破K线:开仓价在均线之下,收盘价在均线之上,称之为多单突破K线,反之亦然 +- 出现多单突破K线时,开多单,反之开空单,开仓时机是基于K线收盘价 +- 每单都是独立的,不限制单方向只能持有一单,止损放在突破K线的最低点或最高点,具体取决于开单方向 +- 止盈基于盈亏比的设置 + +## 补充逻辑 +- 是否复利,支持参数设置 +- 移动止损:支持设置为开单价格的百分比 + diff --git a/20260509-突破策略/zyb.20260509.breakout.mq5 b/20260509-突破策略/zyb.20260509.breakout.mq5 new file mode 100644 index 0000000..1e998cb --- /dev/null +++ b/20260509-突破策略/zyb.20260509.breakout.mq5 @@ -0,0 +1,420 @@ +//+------------------------------------------------------------------+ +//| 均线突破策略 EA — 逻辑见同目录 README.md | +//| MA(默认14) 突破收盘确认;独立持仓;SL/TP;移动止损=开仓价百分比间距。 | +//+------------------------------------------------------------------+ +#property copyright "zyb-ea" +#property version "2.20" +#property description "MA突破:SL=突破K极值;TP=盈亏比;移动止损=开仓价×百分比间距。" + +#include + +CTrade g_trade; + +int g_hMa = INVALID_HANDLE; +static datetime g_lastWorkBar = 0; + +//------------------------------------------------------------ +input group "工作区" +input ENUM_TIMEFRAMES InpWorkTF = PERIOD_M1; // K 线周期(默认 1 分钟) +input int InpSlippage = 30; // 滑点(点) +input int InpMaxSpreadPoints = 0; // 最大点差(点,0=不限制) +input long InpMagic = 20260509; // Magic 识别码 + +input group "移动平均线" +input int InpMAPeriod = 14; // 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 InpLot = 0.01; // 基准开仓手数(复利关闭时即为实际手数) + +input group "止盈(盈亏比)" +input double InpRewardRiskRatio = 2.0; // 盈亏比:止盈距离 = 入场相对止损的风险宽度 × 本值(≤0 表示不设止盈) + +input group "移动止损" +input double InpTrailPercentOfOpen = 0.0; // 跟踪间距 = 开仓价 × 本%/100(0=关闭);多:SL=BID−间距;空:SL=ASK+间距 + +input group "复利" +input bool InpUseCompound = false; // 开启后按账户净值相对参考净值缩放手数 +input double InpCompoundRefEquity = 10000.0; // 参考净值:实际手数 ≈ InpLot × (当前净值 / 参考净值) + +input group "其它" +input bool InpLog = true; // 是否打印日志 + +//------------------------------------------------------------ +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; +} + +double CalcOrderLot() +{ + double lot = InpLot; + if(InpUseCompound && InpCompoundRefEquity > 1e-8) + { + const double eq = AccountInfoDouble(ACCOUNT_EQUITY); + lot = InpLot * (eq / InpCompoundRefEquity); + } + return NormalizeVolumeVal(lot); +} + +int SpreadPoints() +{ + return (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); +} + +double StopsMinDistancePrice() +{ + const double pt = SymbolInfoDouble(_Symbol, SYMBOL_POINT); + const int st = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); + const int fr = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL); + return (double)(st + fr) * pt; +} + +// 市价多单:SL 须在 Bid 下方且满足最小止损距离 +bool IsBuyStopLossValid(const double sl) +{ + if(sl <= 0.0) + return false; + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + const double need = StopsMinDistancePrice(); + const double slN = NormalizeDouble(sl, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)); + if(slN >= bid - need) + return false; + return true; +} + +// 市价空单:SL 须在 Ask 上方且满足最小止损距离 +bool IsSellStopLossValid(const double sl) +{ + if(sl <= 0.0) + return false; + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double need = StopsMinDistancePrice(); + const double slN = NormalizeDouble(sl, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)); + if(slN <= ask + need) + return false; + return true; +} + +// 多单止盈须在 Ask 上方且满足最小距离 +bool IsBuyTakeProfitValid(const double tp) +{ + if(tp <= 0.0) + return false; + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double need = StopsMinDistancePrice(); + if(tp <= ask + need) + return false; + return true; +} + +// 空单止盈须在 Bid 下方且满足最小距离 +bool IsSellTakeProfitValid(const double tp) +{ + if(tp <= 0.0) + return false; + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + const double need = StopsMinDistancePrice(); + if(tp >= bid - need) + return false; + return true; +} + +// 风险宽 = |入场(Ask) − SL|;TP = Ask + 风险×RR +bool CalcBuyTpByRewardRisk(const double sl, double &tp) +{ + tp = 0.0; + if(InpRewardRiskRatio <= 0.0) + return true; + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double risk = ask - sl; + if(risk <= 1e-12) + return false; + const int dg = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + tp = NormalizeDouble(ask + risk * InpRewardRiskRatio, dg); + return IsBuyTakeProfitValid(tp); +} + +// 风险宽 = SL − Ask;TP = Ask − 风险×RR +bool CalcSellTpByRewardRisk(const double sl, double &tp) +{ + tp = 0.0; + if(InpRewardRiskRatio <= 0.0) + return true; + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double risk = sl - ask; + if(risk <= 1e-12) + return false; + const int dg = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + tp = NormalizeDouble(ask - risk * InpRewardRiskRatio, dg); + return IsSellTakeProfitValid(tp); +} + +/// 按开仓价百分比为间距跟踪止损(仅收紧:上移多单 SL、下移空单 SL) +void TryTrailingStopByOpenPercent() +{ + if(InpTrailPercentOfOpen <= 0.0) + return; + + g_trade.SetExpertMagicNumber((ulong)InpMagic); + g_trade.SetDeviationInPoints((uint)InpSlippage); + + const int dg = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + const double need = StopsMinDistancePrice(); + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double pt = SymbolInfoDouble(_Symbol, SYMBOL_POINT); + + for(int i = (int)PositionsTotal() - 1; i >= 0; 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 double openPx = PositionGetDouble(POSITION_PRICE_OPEN); + const double trailDist = openPx * (InpTrailPercentOfOpen / 100.0); + if(trailDist <= 1e-12) + continue; + + const double slOld = PositionGetDouble(POSITION_SL); + const double tpOld = PositionGetDouble(POSITION_TP); + const long typ = (long)PositionGetInteger(POSITION_TYPE); + + if(slOld <= 0.0) + continue; + + if(typ == POSITION_TYPE_BUY) + { + double newSL = NormalizeDouble(bid - trailDist, dg); + if(newSL <= slOld + pt * 0.5) + continue; + if(newSL >= bid - need) + continue; + if(tpOld > 0.0 && newSL >= tpOld) + continue; + if(!g_trade.PositionModify(ticket, newSL, tpOld)) + { + if(InpLog) + Print("移动止损(多)失败 ticket=", ticket, " err=", GetLastError(), + " newSL=", newSL, " oldSL=", slOld); + } + } + else if(typ == POSITION_TYPE_SELL) + { + double newSL = NormalizeDouble(ask + trailDist, dg); + if(newSL >= slOld - pt * 0.5) + continue; + if(newSL <= ask + need) + continue; + if(tpOld > 0.0 && newSL <= tpOld) + continue; + if(!g_trade.PositionModify(ticket, newSL, tpOld)) + { + if(InpLog) + Print("移动止损(空)失败 ticket=", ticket, " err=", GetLastError(), + " newSL=", newSL, " oldSL=", slOld); + } + } + } +} + +//------------------------------------------------------------ +bool CopyBar1OHLCMA(double &o1, double &c1, double &l1, double &h1, double &ma1) +{ + double o[], c[], l[], h[]; + ArraySetAsSeries(o, true); + ArraySetAsSeries(c, true); + ArraySetAsSeries(l, true); + ArraySetAsSeries(h, true); + if(CopyOpen(_Symbol, InpWorkTF, 1, 1, o) != 1) + return false; + if(CopyHigh(_Symbol, InpWorkTF, 1, 1, h) != 1) + return false; + if(CopyLow(_Symbol, InpWorkTF, 1, 1, l) != 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]; + l1 = l[0]; + h1 = h[0]; + ma1 = m[0]; + return true; +} + +/// 新 K 线开盘:上一根已收盘 K 为突破 K;每单独立,带 SL / 可选 RR 止盈 +void OnNewClosedBar() +{ + if(g_hMa == INVALID_HANDLE) + return; + + double o1 = 0.0, c1 = 0.0, l1 = 0.0, h1 = 0.0, ma1 = 0.0; + if(!CopyBar1OHLCMA(o1, c1, l1, h1, ma1)) + return; + + const bool longBreak = (o1 < ma1 && c1 > ma1); + const bool shortBreak = (o1 > ma1 && c1 < ma1); + + if(!longBreak && !shortBreak) + return; + + g_trade.SetExpertMagicNumber((ulong)InpMagic); + g_trade.SetDeviationInPoints((uint)InpSlippage); + + const double vol = CalcOrderLot(); + if(vol < MinLot() - 1e-12) + { + if(InpLog) + Print("手数过小,跳过开仓"); + return; + } + + const int dg = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + + if(longBreak) + { + double sl = NormalizeDouble(l1, dg); + if(!IsBuyStopLossValid(sl)) + { + if(InpLog) + Print("多单突破: 止损价不满足券商最小距离或无效,跳过 SL=", sl, + " Bid=", SymbolInfoDouble(_Symbol, SYMBOL_BID), + " 突破K最低=", l1); + return; + } + double tp = 0.0; + if(InpRewardRiskRatio > 0.0) + { + if(!CalcBuyTpByRewardRisk(sl, tp)) + { + if(InpLog) + Print("多单突破: 止盈(盈亏比 ", InpRewardRiskRatio, ") 无效或距离不足,跳过"); + return; + } + } + const string cmt = "突破-多"; + if(!g_trade.Buy(vol, _Symbol, 0.0, sl, tp, cmt)) + { + if(InpLog) + Print("开多单失败 err=", GetLastError(), " SL=", sl, " TP=", tp); + } + else if(InpLog) + Print("多单突破 独立开仓 手数=", vol, " SL=", sl, " TP=", tp, + " RR=", InpRewardRiskRatio, + " O=", o1, " C=", c1, " MA=", ma1); + } + else if(shortBreak) + { + double sl = NormalizeDouble(h1, dg); + if(!IsSellStopLossValid(sl)) + { + if(InpLog) + Print("空单突破: 止损价不满足券商最小距离或无效,跳过 SL=", sl, + " Ask=", SymbolInfoDouble(_Symbol, SYMBOL_ASK), + " 突破K最高=", h1); + return; + } + double tp = 0.0; + if(InpRewardRiskRatio > 0.0) + { + if(!CalcSellTpByRewardRisk(sl, tp)) + { + if(InpLog) + Print("空单突破: 止盈(盈亏比 ", InpRewardRiskRatio, ") 无效或距离不足,跳过"); + return; + } + } + const string cmt = "突破-空"; + if(!g_trade.Sell(vol, _Symbol, 0.0, sl, tp, cmt)) + { + if(InpLog) + Print("开空单失败 err=", GetLastError(), " SL=", sl, " TP=", tp); + } + else if(InpLog) + Print("空单突破 独立开仓 手数=", vol, " SL=", sl, " TP=", tp, + " RR=", InpRewardRiskRatio, + " O=", o1, " C=", c1, " MA=", ma1); + } +} + +//------------------------------------------------------------ +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; + if(InpLog) + Print("均线突破 EA v2.2: ", _Symbol, + " 周期=", EnumToString(InpWorkTF), + " MA=", InpMAPeriod, + " 基准手数=", InpLot, + " 盈亏比RR=", InpRewardRiskRatio, + " 移动止损%=", InpTrailPercentOfOpen, + " 复利=", (InpUseCompound ? "开" : "关"), + (InpUseCompound ? StringFormat(" 参考净值=%.2f 当前手数≈%.4f", InpCompoundRefEquity, CalcOrderLot()) : ""), + " 独立持仓 SL=突破K TP=风险×RR"); + 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; + + TryTrailingStopByOpenPercent(); + + const datetime bar0 = iTime(_Symbol, InpWorkTF, 0); + if(bar0 != 0 && bar0 != g_lastWorkBar) + { + g_lastWorkBar = bar0; + OnNewClosedBar(); + } +} + +//+------------------------------------------------------------------+ diff --git a/KVB-50036106/20260425.set b/KVB-50036106/20260425.set new file mode 100644 index 0000000000000000000000000000000000000000..fc9905f9ac33d1442b342dbfaa0e177678be721e GIT binary patch literal 2086 zcmbVNOKVe65FQY^5bV}%mKB5M*@x0awG@OJYi?VSf{?aJ8rr5Ixk>ATAh@Y?(}fn? z`8QmNE{a>xy<2~PpsVWl&E(v3T3ae|Ip@rI%r`UN%sKb>(M?H7M=H{js;olZk|z3` zWY9+C8pa~}EWQ+4Ms8p|k3J&>xg-~b`mwA@11Kq}VcY;xN>&sp266+4+~e4iE>5Q~ zR-}!#iL)_Ud!W4qiFj4aW7Vk3bs)8LwuzM|gH`V%J;|w;RUoFIyMyz@q@@a?>OrRi zly>0p7WTSWrLPS=N&hSsuF*a{q%U=OtUKlvk<*y3Akqf+YoSMGX-ez-JS(ho zXD6%DaYXK)ZHYJHZTU;db3BdsI;Y0vnRNDYr`RLYXWMg;t)l+sVTm=?0dMp?r&U0y zs1i}K0dHiDb6WQwSFK(Qn>Df{_mGuU&5esCf;FyKMK0_wKO8>r&;N1ZLO@O zMM|@~m-l~0L*-WoVO{W6#kgC%S!UT+LwdkS5qHtJaT2p*Yo|-@99gwleLu|VG!QGYEsMZz{g(yv(DY|DvZ_4LTI73rK4+o+ZN#~O*48(?sWE5q=NrRc zWgHph=(#nUUMGS*>hhh+Ln|W_(DCPOe_L)L;tu{4{M*gL2CJHH0`)=$|BYeu|2U5K iX%c?t(Z+Bz&ozavn0jWdGcrnd~dO zw-S`Z#;jA25-S2y1#$_Z?zAp#gLcB2+lp_7ZpF8%trzTscgML3784tQv<#;ik{`7+ zv3JO)f%6FD9GwKaEvxY4CFk}D{WY4ai0y*A4N~zegQmE79Fobm*onu)zK0uUB;*&I z3p|(159}{MmnV;~LuoP~PGX~2=lE5Q$IQx_ynSf%qAV$6Uu&+)@0T!!jm%VDM5ZeUq}p4Tyo>m05fUfQDFL7W26RaZUGpq7y1mkAjA* zj@zAb-$dk*_M?w6-_S=Dv7Oruh=x7qsaOSeRZIsZEDf&_Qj@H z|ISX`wY4)wR_i*K27jOZK3UB5n(n!v$Ml~X!M6z5U5s8Iag(fXISNrOvC>OxnC$NR gQTeOv%VYfa{81EXoaKM1!dvRN;qBhiCI5r+2fo1!`Tzg` literal 0 HcmV?d00001 diff --git a/KVB-50036106/Gold Snap 500$.set b/KVB-50036106/Gold Snap 500$.set new file mode 100644 index 0000000000000000000000000000000000000000..aeb1076bc97177ff81f1c82b73e78d0906758a6e GIT binary patch literal 1670 zcmbW1-EPxB5QXO&iFfb=pvFbCB1OIcp#oBiP*WuCka3&TB7aSs5)jW0{m!hN-N+RP zw6eWBJ9Flo*;)Vo^};e+TWNQ8ZCA_}Hs}4+a-L&5;CsaTkdg7^c4E&MCs?1`V|!$Z zZ|ufuP%<0yor08E_!c&%#-8=l(c&-w*3-&fx#j|!#aSJ+RQ*N;nkBNQvoxn)Q zPgqxEu6ciDeg?WS1%(l%tAIF(jkM0ms~V4at7^*jZdVsoNfrCja#ejl`xx4=ofY;G z|JrUrQnavC^@^v2m1?_zWd(ZKY82NwTq9n-L%ShkL(G~p9WcK6IDqXfXgYi8n6V@F zgSKuuFZn8CI6rkvPEQOx=M=kY6*C_0G8Wb!I!)m#CaCE96pj|Eb&l7>RTsNnRi~;? ze=^j&L)d7(Z^_;EKEb+ltm1)w68o5MYa{Bkf}>8mAe$8HhA2yarJXpUPl(yyN%^Rs z8apSCI%MIwf`*VOypMZ2Lfk1;*%B*;U6h!lbFD&tu@awdEvsxPp9>Jzbk7Gmp=ot@ zaYybQ6om?O2NJyHPtN870zq7;24v7@1 zmDjuLotZOdF8=%Ht0gwJmfc(1E?E!kmU-FAJZo0sU145gB&;`W%U1ca#eUO1+6R+- zX4lq5OJW_~Jv1eD5j16J?xIoO^d8zBw-eqi8}anHoAHcZr3b8}ugyLS5?8i}reWCi zIeC6PP3#A{TIkGp_mR@R%@F>&HNl;*dqh_HWwnKe_sEaYI4YnY@a>LM;qHc;!s>qM zOjv|gxUTFed~t-*{Du7mUU!*qSRbKXmfYi9OJqctghsyKVpp7Zn8l*39dhbCi8V2J z{)$z6`_~3CLet=@^ij7x)xTU@Fc!Qy=j@kRod?Z%Gw+7Eod^!Su z%DTxoWho-tK4zA(y#*-sjU`^t$7A9j)Jfu#!PZ>3hlNl|JW8yS*O`Xopz z%d56BCBHT$ALR~U^AV{_ygcMrMiA6}qnT;6*j5B8ocgLR9^}*Rnx49dtJCslNOGt zYE`!?TQ*M6o6@=7Q|q@_?9KJaJLduUsJ~K`&M`vnJaixRc6I&wgeUQCN-jKeqe^d5 zOnmlOsaw5`(=Imp4D~!&n*T4%d)E=R +#include +#include + +CTrade g_trade; +CSymbolInfo g_sym; +CPositionInfo g_pos; + +// 内置 20 档(解析 InpLotLadder 失败或节数不足时回退) — 与当前默认字符串一致 +const double g_builtinLadder[20] = + { + 0.05, 0.05, 0.05, 0.10, 0.10, 0.10, 0.15, 0.20, 0.20, 0.25, + 0.30, 0.35, 0.45, 0.55, 0.65, 0.75, 0.90, 1.2, 1.4, 1.5 + }; +double g_lotLadder[20]; +#define MGRID_VTP_B "MGvTP_20260425_BUY" +#define MGRID_VTP_S "MGvTP_20260425_SELL" +#define MGRID_TGT_L "MG_20260425_TGTLIST" + +//--- 以下三处为「相对**参考价**的价距%」, 在运行时按当刻价格换算为实际价格步长(非点) +input group "滑点" +input int InpSlippage = 30; // 滑点(点) + +input group "识别" +input long InpMagic = 20260425; // Magics + +input group "首单与统盈(百分比,见底部说明)" +input double InpFirstTpPercent = 0.05; // 首档(仅1~Lot1层)止盈:每单= **该笔开仓价**×% 为价距;>=2层用统盈% ;<=0=不挂首档TP(单档时改按统盈%平) +input double InpAveTpPercent = 0.05; // 统盈:相对**整体成本价** 的价距%(>=2 档整篮; 1档且未挂首档TP时亦用) + +input group "加仓" +input double InpDisPercent = 0.1; // 与上一同向**开仓价** 的间距:价距= lastOpen×%(市价多=Ask,空=Bid 与 last 比) +input double InpAddTimes = 1.2; // 满 20 单后再加仓:在 Lot20 上乘此倍率 (Add_times) +input int InpMaxOrdersPerSide = 20; // 单方向最多同时持仓**笔数**;达到后不再开新仓(不平仓,可手工);0=不限制 + +input group "头寸(第1~20档,一行逗号分隔)" +input string InpLotLadder = "0.05,0.05,0.05,0.10,0.10,0.10,0.15,0.20,0.20,0.25,0.30,0.35,0.45,0.55,0.65,0.75,0.90,1.2,1.4,1.5"; + +input group "拆单" +input int InpOrdersPerAdd = 1; // 每次加仓/首单 同时下几笔同向单(>=1,总手数仍为一档合计) + +input group "其它" +input int InpMaxSpreadPoints = 0; // 最大点差(点),0=不限制 +input bool InpLog = true; // 专家日志 + +input group "图表(统盈参考线)" +input bool InpDrawVtp = true; // 多/空各一条虚拟水平线(仅参考,非真实挂单) +input int InpVtpLineWidth = 1; +input color InpVtpColorLong = clrDodgerBlue; +input color InpVtpColorShort = clrDarkOrange; +input bool InpShowBasketTgtList = true; // 图上列表:每单#、开仓、统盈目标价(与程序整篮价一致,非订单TP栏) +input color InpTgtListColor = clrNavy; +input int InpTgtListFont = 9; + +//+------------------------------------------------------------------+ +double PointValue() + { + if(!g_sym.Name(_Symbol)) + return _Point; + g_sym.RefreshRates(); + return g_sym.Point(); + } + +//+------------------------------------------------------------------+ +// 价距 = 参考价 × (参数百分比/100);参考为各用途各自给定的开仓/成本(见输入说明) +//+------------------------------------------------------------------+ +double PctOfPrice(const double refPrice, const double pct) + { + if(refPrice <= 0.0) + return 0.0; + if(pct <= 0.0) + return 0.0; + return refPrice * (pct / 100.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); } + +//+------------------------------------------------------------------+ +string TrimToken(const string u) + { + string t = u; + const int L0 = (int)StringLen(t); + for(int a = 0; a < L0; a++) + { + if(StringGetCharacter(t, 0) != 32) + break; + t = StringSubstr(t, 1); + } + int L = (int)StringLen(t); + for(int b = 0; b < L; b++) + { + if(StringGetCharacter(t, L - 1) != 32) + break; + t = StringSubstr(t, 0, L - 1); + L = (int)StringLen(t); + } + return t; + } + +//+------------------------------------------------------------------+ +// 自 InpLotLadder 解析 20 档; 节数/数值不对则回退 g_builtinLadder +//+------------------------------------------------------------------+ +void InitLotLadder() + { + for(int a = 0; a < 20; a++) + g_lotLadder[a] = g_builtinLadder[a]; + const int slen = (int)StringLen(InpLotLadder); + if(slen < 1) + { + if(InpLog) + Print("InpLotLadder empty, using builtin 20-lot"); + return; + } + string toks[]; + // StringSplit(…,ushort,[]) 无隐式 string→ushort 警告 + int n = StringSplit(InpLotLadder, (ushort)44, toks); + if(n < 20) + { + n = StringSplit(InpLotLadder, (ushort)59, toks); + } + if(n < 20) + { + if(InpLog) + Print("InpLotLadder need 20 numbers, got ", n, " — builtin"); + return; + } + for(int k = 0; k < 20; k++) + { + const double w = StringToDouble(TrimToken(toks[k])); + if(w <= 0.0) + { + g_lotLadder[k] = g_builtinLadder[k]; + } + else + g_lotLadder[k] = w; + } + } + +//+------------------------------------------------------------------+ +void LogInitDiagnostics() + { + if(!InpLog) + return; + Print("20260425 v1.11 ", _Symbol, " magic=", (long)InpMagic, + " 首档TP%=", InpFirstTpPercent, " 统盈%=", InpAveTpPercent, " 加仓距%=", InpDisPercent, + " 单向最多笔数=", InpMaxOrdersPerSide); + } + +//+------------------------------------------------------------------+ +bool SpreadOk() + { + if(InpMaxSpreadPoints <= 0) + return true; + const int sp = (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); + if(sp > InpMaxSpreadPoints) + return false; + return true; + } + +//+------------------------------------------------------------------+ +// 0-based 第 idx 档同向总手数(第0档=第1单 … 第19档=第20单; 第20档起为 第20档手数*倍率^n) +//+------------------------------------------------------------------+ +double LotForLayerIndex(const int idx) + { + const double m = InpAddTimes; + if(idx < 0) + return MinLot(); + if(idx < 20) + return g_lotLadder[idx]; + // idx >= 20: 以第20档(索引19)为基准 + const int p = idx - 19; + return g_lotLadder[19] * MathPow(m, p); + } + +//+------------------------------------------------------------------+ +double NormalizeVolume(const double v) + { + double o = v; + const double st = LotStep(); + const double lo = MinLot(); + const double hi = MaxLot(); + if(st > 0.0) + o = MathRound(o / st) * st; + o = MathMax(lo, MathMin(hi, o)); + o = MathMax(lo, o); + return o; + } + +//+------------------------------------------------------------------+ +int OrdersPerAdd() + { + return MathMax(1, InpOrdersPerAdd); + } + +//+------------------------------------------------------------------+ +// InpMaxOrdersPerSide<=0 不限制;否则本次若开仓则持仓笔数不超过上限(含拆单) +//+------------------------------------------------------------------+ +bool CanOpenMoreOrders(const int currentOrderCount) + { + if(InpMaxOrdersPerSide <= 0) + return true; + const int nOrd = OrdersPerAdd(); + return (currentOrderCount + nOrd <= InpMaxOrdersPerSide); + } + +//+------------------------------------------------------------------+ +// 将一层总手数拆成多笔(README:加仓可同时下多笔同向单),总和≈ total +//+------------------------------------------------------------------+ +void SplitVolumeToOrders(const double total, double &parts[]) + { + const int n = OrdersPerAdd(); + ArrayResize(parts, n); + if(n == 1) + { + parts[0] = NormalizeVolume(total); + return; + } + const double t = NormalizeVolume(total); + if(t < MinLot() * n - 1e-12) + { + parts[0] = NormalizeVolume(total); + for(int k = 1; k < n; k++) + parts[k] = MinLot(); + return; + } + const double st = MathMax(LotStep(), 0.0000000001); + const int steps = (int)MathRound((t - MinLot() * n) / st); + if(steps < 0) + { + for(int i = 0; i < n; i++) + parts[i] = MinLot(); + return; + } + const int base = steps / n; + int rem = (int)(steps - base * n); + for(int j = 0; j < n; j++) + { + const int add = base + (rem > 0 ? 1 : 0); + if(rem > 0) + rem--; + parts[j] = NormalizeVolume(MinLot() + (double)add * st); + } + } + +//+------------------------------------------------------------------+ +struct SBasket + { + int count; + double lastPrice; // 时间上最后一笔的开仓价 + double avgPrice; // 成本价 + double sumLots; + }; + +//+------------------------------------------------------------------+ +bool BuildBasket(const ENUM_POSITION_TYPE ptype, SBasket &b) + { + b.count = 0; + b.lastPrice = 0.0; + b.avgPrice = 0.0; + b.sumLots = 0.0; + double sumP = 0.0; + datetime tLast = 0; + ulong lastTix = 0; + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_pos.SelectByIndex(i)) + continue; + if(g_pos.Magic() != (ulong)InpMagic) + continue; + if(g_pos.Symbol() != _Symbol) + continue; + if(g_pos.PositionType() != ptype) + continue; + b.count++; + const datetime tt = g_pos.Time(); + const double op = g_pos.PriceOpen(); + const double vl = g_pos.Volume(); + const ulong tix = (ulong)g_pos.Ticket(); + sumP += op * vl; + b.sumLots += vl; + if(tLast == 0 || tt > tLast || (tt == tLast && tix > lastTix)) + { + tLast = tt; + lastTix = tix; + b.lastPrice = op; + } + } + if(b.count < 1 || b.sumLots <= 0.0) + return false; + b.avgPrice = sumP / b.sumLots; + return true; + } + +//+------------------------------------------------------------------+ +bool CloseType(const ENUM_POSITION_TYPE ptype, const string reason) + { + bool ok = true; + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_pos.SelectByIndex(i)) + continue; + if(g_pos.Magic() != (ulong)InpMagic || g_pos.Symbol() != _Symbol) + continue; + if(g_pos.PositionType() != ptype) + continue; + if(!g_trade.PositionClose(g_pos.Ticket(), InpSlippage)) + { + ok = false; + if(InpLog) + Print("20260425 close fail ticket=", (ulong)g_pos.Ticket(), " err=", GetLastError()); + } + } + if(InpLog && ok) + Print("20260425 全部平", ptype == POSITION_TYPE_BUY ? "多" : "空", " 原因: ", reason); + return ok; + } + +//+------------------------------------------------------------------+ +void RemoveTakeProfitsType(const ENUM_POSITION_TYPE ptype) + { + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_pos.SelectByIndex(i)) + continue; + if(g_pos.Magic() != (ulong)InpMagic || g_pos.Symbol() != _Symbol) + continue; + if(g_pos.PositionType() != ptype) + continue; + if(g_pos.TakeProfit() == 0.0) + continue; + g_trade.PositionModify(g_pos.Ticket(), g_pos.StopLoss(), 0.0); + } + } + +//+------------------------------------------------------------------+ +int PriceDigits() { return (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); } + +//+------------------------------------------------------------------+ +bool OpenChunkBuy(const double vol, const string cmt) + { + g_trade.SetExpertMagicNumber((ulong)InpMagic); + g_trade.SetDeviationInPoints((uint)InpSlippage); + g_trade.SetTypeFillingBySymbol(_Symbol); + if(!g_trade.Buy(NormalizeVolume(vol), _Symbol, 0, 0, 0, cmt)) + { + if(InpLog) + Print("20260425 Buy fail vol=", vol, " err=", g_trade.ResultRetcodeDescription()); + return false; + } + g_sym.RefreshRates(); + return true; + } + +//+------------------------------------------------------------------+ +bool OpenChunkSell(const double vol, const string cmt) + { + g_trade.SetExpertMagicNumber((ulong)InpMagic); + g_trade.SetDeviationInPoints((uint)InpSlippage); + g_trade.SetTypeFillingBySymbol(_Symbol); + if(!g_trade.Sell(NormalizeVolume(vol), _Symbol, 0, 0, 0, cmt)) + { + if(InpLog) + Print("20260425 Sell fail vol=", vol, " err=", g_trade.ResultRetcodeDescription()); + return false; + } + g_sym.RefreshRates(); + return true; + } + +//+------------------------------------------------------------------+ +bool OpenBuyLayersForTotal(const int layerIdx, const string tag) + { + const double want = LotForLayerIndex(layerIdx); + if(want < MinLot() - 0.0000001) + { + if(InpLog) + Print("20260425 手数过小 跳过 layer=", layerIdx); + return false; + } + double parts[]; + SplitVolumeToOrders(want, parts); + for(int k = 0; k < ArraySize(parts); k++) + { + if(!OpenChunkBuy(parts[k], "MGb_" + tag + "_" + IntegerToString(k))) + return false; + } + return true; + } + +//+------------------------------------------------------------------+ +bool OpenSellLayersForTotal(const int layerIdx, const string tag) + { + const double want = LotForLayerIndex(layerIdx); + if(want < MinLot() - 0.0000001) + { + if(InpLog) + Print("20260425 手数过小 跳过 layer=", layerIdx); + return false; + } + double parts[]; + SplitVolumeToOrders(want, parts); + for(int k = 0; k < ArraySize(parts); k++) + { + if(!OpenChunkSell(parts[k], "MGs_" + tag + "_" + IntegerToString(k))) + return false; + } + return true; + } + +//+------------------------------------------------------------------+ +void SetFirstLayerBuyTP() + { + // 仅首档(一层):每笔按该笔 **开仓价×首档%** 挂 TP;拆单时各单各自 + if(InpFirstTpPercent <= 0.0) + return; + g_sym.RefreshRates(); + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_pos.SelectByIndex(i)) + continue; + if(g_pos.Magic() != (ulong)InpMagic || g_pos.Symbol() != _Symbol) + continue; + if(g_pos.PositionType() != POSITION_TYPE_BUY) + continue; + const double op = g_pos.PriceOpen(); + const double tpd = PctOfPrice(op, InpFirstTpPercent); + const double tp = op + tpd; + g_trade.PositionModify(g_pos.Ticket(), 0, NormalizeDouble(tp, PriceDigits())); + } + } + +//+------------------------------------------------------------------+ +void SetFirstLayerSellTP() + { + if(InpFirstTpPercent <= 0.0) + return; + g_sym.RefreshRates(); + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_pos.SelectByIndex(i)) + continue; + if(g_pos.Magic() != (ulong)InpMagic || g_pos.Symbol() != _Symbol) + continue; + if(g_pos.PositionType() != POSITION_TYPE_SELL) + continue; + const double op = g_pos.PriceOpen(); + const double tpd = PctOfPrice(op, InpFirstTpPercent); + const double tp = op - tpd; + g_trade.PositionModify(g_pos.Ticket(), 0, NormalizeDouble(tp, PriceDigits())); + } + } + +//+------------------------------------------------------------------+ +void VtpLineDeleteName(const string name) + { + if(ObjectFind(0, name) >= 0) + ObjectDelete(0, name); + } + +//+------------------------------------------------------------------+ +// >=2 档时列表「获利」为 0(程序内统盈此时尚无单笔 TP) — 用水平线标出统盈价供查看 +//+------------------------------------------------------------------+ +void VtpHLineSet(const string name, const double price, const color clr, const string toolTip) + { + const int dd = PriceDigits(); + const double p = NormalizeDouble(price, dd); + if(!ObjectCreate(0, name, OBJ_HLINE, 0, 0, p)) + { + if(ObjectFind(0, name) < 0) + return; + } + ObjectSetDouble(0, name, OBJPROP_PRICE, p); + ObjectSetInteger(0, name, OBJPROP_COLOR, clr); + ObjectSetInteger(0, name, OBJPROP_WIDTH, InpVtpLineWidth); + ObjectSetString(0, name, OBJPROP_TOOLTIP, toolTip); + } + +//+------------------------------------------------------------------+ +void UpdateVirtualTPLines() + { + if(!InpDrawVtp) + { + VtpLineDeleteName(MGRID_VTP_B); + VtpLineDeleteName(MGRID_VTP_S); + ChartRedraw(0); + return; + } + g_sym.RefreshRates(); + const int nO = MathMax(1, InpOrdersPerAdd); + + SBasket bB, bS; + const bool hB = BuildBasket(POSITION_TYPE_BUY, bB); + const bool hS = BuildBasket(POSITION_TYPE_SELL, bS); + + if(hB && bB.count > 0) + { + const int lay = bB.count / nO; + const double aved = PctOfPrice(bB.avgPrice, InpAveTpPercent); + const double tppd = PctOfPrice(bB.avgPrice, InpFirstTpPercent); + double target; + if(lay >= 2) + target = bB.avgPrice + aved; + else + target = bB.avgPrice + (InpFirstTpPercent > 0.0 ? tppd : aved); + VtpHLineSet(MGRID_VTP_B, target, InpVtpColorLong, "buy VTP"); + } + else + VtpLineDeleteName(MGRID_VTP_B); + + if(hS && bS.count > 0) + { + const int layS = bS.count / nO; + const double aved = PctOfPrice(bS.avgPrice, InpAveTpPercent); + const double tppd = PctOfPrice(bS.avgPrice, InpFirstTpPercent); + double tgs; + if(layS >= 2) + tgs = bS.avgPrice - aved; + else + tgs = bS.avgPrice - (InpFirstTpPercent > 0.0 ? tppd : aved); + VtpHLineSet(MGRID_VTP_S, tgs, InpVtpColorShort, "sell VTP"); + } + else + VtpLineDeleteName(MGRID_VTP_S); + + ChartRedraw(0); + } + +//+------------------------------------------------------------------+ +// 统盈同价多笔时不可能全部挂成有效经纪商 TP,故仅图上列出「#—开仓—T目标」(与程序整篮价一致) +//+------------------------------------------------------------------+ +void TgtListDelete() + { + if(ObjectFind(0, MGRID_TGT_L) >= 0) + ObjectDelete(0, MGRID_TGT_L); + } + +//+------------------------------------------------------------------+ +void UpdateBasketTgtListLabel() + { + if(!InpShowBasketTgtList) + { + TgtListDelete(); + return; + } + g_sym.RefreshRates(); + const int d = PriceDigits(); + const int nO = MathMax(1, InpOrdersPerAdd); + + SBasket bB, bS; + const bool hB = BuildBasket(POSITION_TYPE_BUY, bB) && bB.count > 0; + const bool hS = BuildBasket(POSITION_TYPE_SELL, bS) && bS.count > 0; + if(!hB && !hS) + { + TgtListDelete(); + return; + } + string txt = "20260425 统盈(程序整篮)\n(订单「获利」可能为0)\n"; + if(hB) + { + const int layB = bB.count / nO; + const double aved = PctOfPrice(bB.avgPrice, InpAveTpPercent); + const double tppd = PctOfPrice(bB.avgPrice, InpFirstTpPercent); + const double pexB = (layB >= 2) ? bB.avgPrice + aved : bB.avgPrice + (InpFirstTpPercent > 0.0 ? tppd : aved); + txt += "多 目标T " + DoubleToString(pexB, d) + " 成本 " + DoubleToString(bB.avgPrice, d) + "\n"; + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_pos.SelectByIndex(i)) + continue; + if(g_pos.Magic() != (ulong)InpMagic || g_pos.Symbol() != _Symbol) + continue; + if(g_pos.PositionType() != POSITION_TYPE_BUY) + continue; + const ulong t = (ulong)g_pos.Ticket(); + txt += " #" + (string)(ulong)t + " 开" + DoubleToString(g_pos.PriceOpen(), d) + " →T" + DoubleToString(pexB, d) + "\n"; + } + } + if(hS) + { + const int layS = bS.count / nO; + const double avedS = PctOfPrice(bS.avgPrice, InpAveTpPercent); + const double tppdS = PctOfPrice(bS.avgPrice, InpFirstTpPercent); + const double pexS = (layS >= 2) ? bS.avgPrice - avedS : bS.avgPrice - (InpFirstTpPercent > 0.0 ? tppdS : avedS); + txt += "空 目标T " + DoubleToString(pexS, d) + " 成本 " + DoubleToString(bS.avgPrice, d) + "\n"; + for(int j = PositionsTotal() - 1; j >= 0; j--) + { + if(!g_pos.SelectByIndex(j)) + continue; + if(g_pos.Magic() != (ulong)InpMagic || g_pos.Symbol() != _Symbol) + continue; + if(g_pos.PositionType() != POSITION_TYPE_SELL) + continue; + const ulong t2 = (ulong)g_pos.Ticket(); + txt += " #" + (string)(ulong)t2 + " 开" + DoubleToString(g_pos.PriceOpen(), d) + " →T" + DoubleToString(pexS, d) + "\n"; + } + } + const int mlen = 1800; // 防止过长 + if(StringLen(txt) > mlen) + txt = StringSubstr(txt, 0, mlen) + "…"; + + if(!ObjectCreate(0, MGRID_TGT_L, OBJ_LABEL, 0, 0, 0)) + { + if(ObjectFind(0, MGRID_TGT_L) < 0) + return; + } + ObjectSetInteger(0, MGRID_TGT_L, OBJPROP_CORNER, CORNER_LEFT_LOWER); + ObjectSetInteger(0, MGRID_TGT_L, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); + ObjectSetInteger(0, MGRID_TGT_L, OBJPROP_XDISTANCE, 6); + ObjectSetInteger(0, MGRID_TGT_L, OBJPROP_YDISTANCE, 8); + ObjectSetString(0, MGRID_TGT_L, OBJPROP_TEXT, txt); + ObjectSetInteger(0, MGRID_TGT_L, OBJPROP_COLOR, InpTgtListColor); + ObjectSetInteger(0, MGRID_TGT_L, OBJPROP_FONTSIZE, InpTgtListFont); + ObjectSetString(0, MGRID_TGT_L, OBJPROP_FONT, "Arial"); + ChartRedraw(0); + } + +//+------------------------------------------------------------------+ +void ProcessLongSide() + { + SBasket b; + g_sym.RefreshRates(); + const int nOrd = OrdersPerAdd(); + if(!BuildBasket(POSITION_TYPE_BUY, b)) + { + if(!SpreadOk()) + return; + if(!CanOpenMoreOrders(0)) + return; + if(!OpenBuyLayersForTotal(0, "0")) + return; + SetFirstLayerBuyTP(); + return; + } + const int layers = b.count / nOrd; + + const double dis = PctOfPrice(b.lastPrice, InpDisPercent); + const double ave = PctOfPrice(b.avgPrice, InpAveTpPercent); + + // 1) 统盈: >=2 档 用成本价+统盈%;1 档 靠首档TP(或首档%=0 时在此统盈) + g_sym.RefreshRates(); + if(layers >= 2) + { + if(g_sym.Bid() >= b.avgPrice + ave) + { + CloseType(POSITION_TYPE_BUY, "统盈(>=2档)"); + return; + } + } + else + { + if(InpFirstTpPercent <= 0.0 && g_sym.Bid() >= b.avgPrice + ave) + { + CloseType(POSITION_TYPE_BUY, "单档无首档TP%,按成本+统盈%"); + return; + } + } + + // 2) 加仓: 与上一笔成交价(多=Ask)相对 last 至少 上一×Dis% + g_sym.RefreshRates(); + if(g_sym.Ask() <= b.lastPrice - dis) + { + if(!CanOpenMoreOrders(b.count)) + return; + if(!SpreadOk()) + return; + if(b.count > 0) + RemoveTakeProfitsType(POSITION_TYPE_BUY); + const int nextLayer = b.count / nOrd; // 下一档索引 = 已满仓数 + if(!OpenBuyLayersForTotal(nextLayer, IntegerToString(nextLayer))) + return; + } + } + +//+------------------------------------------------------------------+ +void ProcessShortSide() + { + SBasket b; + g_sym.RefreshRates(); + const int nOrd = OrdersPerAdd(); + if(!BuildBasket(POSITION_TYPE_SELL, b)) + { + if(!SpreadOk()) + return; + if(!CanOpenMoreOrders(0)) + return; + if(!OpenSellLayersForTotal(0, "0")) + return; + SetFirstLayerSellTP(); + return; + } + const int layers = b.count / nOrd; + + const double dis = PctOfPrice(b.lastPrice, InpDisPercent); + const double ave = PctOfPrice(b.avgPrice, InpAveTpPercent); + + g_sym.RefreshRates(); + if(layers >= 2) + { + if(g_sym.Ask() <= b.avgPrice - ave) + { + CloseType(POSITION_TYPE_SELL, "统盈(>=2档)"); + return; + } + } + else + { + if(InpFirstTpPercent <= 0.0 && g_sym.Ask() <= b.avgPrice - ave) + { + CloseType(POSITION_TYPE_SELL, "单档无首档TP%,按成本+统盈%"); + return; + } + } + g_sym.RefreshRates(); + if(g_sym.Bid() >= b.lastPrice + dis) + { + if(!CanOpenMoreOrders(b.count)) + return; + if(!SpreadOk()) + return; + if(b.count > 0) + RemoveTakeProfitsType(POSITION_TYPE_SELL); + const int nextLayer = b.count / nOrd; + if(!OpenSellLayersForTotal(nextLayer, IntegerToString(nextLayer))) + return; + } + } + +//+------------------------------------------------------------------+ +int OnInit() + { + g_sym.Name(_Symbol); + g_trade.SetExpertMagicNumber((ulong)InpMagic); + InitLotLadder(); + LogInitDiagnostics(); + if(InpAddTimes < 1.0) + { + Print("20260425: AddTimes 应 >=1"); + return INIT_PARAMETERS_INCORRECT; + } + if(InpDisPercent <= 0.0) + { + Print("20260425: InpDisPercent 应 >0"); + return INIT_PARAMETERS_INCORRECT; + } + if(InpAveTpPercent <= 0.0) + { + Print("20260425: InpAveTpPercent 应 >0"); + return INIT_PARAMETERS_INCORRECT; + } + if(InpFirstTpPercent < 0.0) + { + Print("20260425: InpFirstTpPercent 应 >=0"); + return INIT_PARAMETERS_INCORRECT; + } + if(LotForLayerIndex(0) < MinLot() - 1e-8) + { + Print("20260425: 第1档手数小于平台最小手,请改 InpLotLadder[0] 或品种"); + return INIT_PARAMETERS_INCORRECT; + } + if(InpMaxOrdersPerSide > 0 && OrdersPerAdd() > InpMaxOrdersPerSide) + { + Print("20260425: InpOrdersPerAdd 不能大于 InpMaxOrdersPerSide(否则无法开首单)"); + return INIT_PARAMETERS_INCORRECT; + } + if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) + { + Print("20260425: 自动交易已关闭(终端设置)"); + } + return INIT_SUCCEEDED; + } + +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { + VtpLineDeleteName(MGRID_VTP_B); + VtpLineDeleteName(MGRID_VTP_S); + TgtListDelete(); + ChartRedraw(0); + } + +//+------------------------------------------------------------------+ +void OnTick() + { + g_sym.RefreshRates(); + // 多、空 两边独立 + ProcessLongSide(); + ProcessShortSide(); + UpdateVirtualTPLines(); + UpdateBasketTgtListLabel(); + } + +//+------------------------------------------------------------------+ + diff --git a/tag/20260425-马丁网格策略/README.md b/tag/20260425-马丁网格策略/README.md new file mode 100644 index 0000000..8262e31 --- /dev/null +++ b/tag/20260425-马丁网格策略/README.md @@ -0,0 +1,34 @@ +# 马丁网格策略 + +- **概念**:`InpSlippage` 等仍按「点」= `SYMBOL_POINT`;但 **首档止盈 / 统盈 / 加仓间距** 在 v1.08 起为**相对价**的**百分比(%)**,在运行时以对应参考价(开仓/成本/上一同向开)换算成实际价距。语言 MQL5。 +- **特点**:小周期震荡、同向加仓、整篮按成本统盈(无单张止损)。 + +## 功能概要 + +震荡中同向加仓;无单时开首单;首档按 **InpFirstTpPercent** 相对**该笔开仓**设止盈(价距=开仓价×%);多档后按**加权成本 × InpAveTpPercent%** 整篮平仓;加仓间距= **上一同向单开仓价 × InpDisPercent%**;多空对称(v1.10 起已取消按 K 线数的时间全平)。 + +## 参数(与输入组对应) + +| 思路 | 参数 | +|------|------| +| 滑点 | `InpSlippage` | +| 识别 | `InpMagic` | +| 首档 TP% / 统盈%(相对成本) | `InpFirstTpPercent`(默认0.05), `InpAveTpPercent`(默认0.05) | +| 加仓间距%(相对上笔同向开) / 满 20 档后倍率 / 单方向最大持仓笔数 | `InpDisPercent`(默认0.1), `InpAddTimes`(默认1.2), `InpMaxOrdersPerSide`(默认20,0=不限制;达到后仅停止新开仓,不平仓) | +| 第 1~20 档总手数(一行 20 个数,英文逗号) | `InpLotLadder` | +| 每档拆单笔数 | `InpOrdersPerAdd` | +| 点差上限、日志 | `InpMaxSpreadPoints`, `InpLog` | +| 图表虚拟统盈线 | `InpDrawVtp` 及颜色、线宽 | +| 图表每单统盈价列表 | `InpShowBasketTgtList`, `InpTgtListColor`, `InpTgtListFont`(左下列出 #、开仓、统盈目标 T,与程序整篮价一致,非交易列表「获利」列) | + +## 止盈策略 + +**档**:同向第几批加仓;若 `InpOrdersPerAdd>1`,档数 = 该向笔数 / 拆单。 + +- **仅 1 档** + - `InpFirstTpPercent > 0`:在**订单上**挂 TP(多 `开 + 开×%`,空 `开 − 开×%`)。 + - `InpFirstTpPercent = 0`:不挂首档 TP,改由程序在 **多** `Bid ≥ 成本 + 成本×InpAveTpPercent%`、**空** `Ask ≤ 成本 − 成本×InpAveTpPercent%` 时整篮平仓。 +- **≥2 档**:加第 2 档前会清掉该向各单 TP,之后只在 **多** `Bid ≥ 成本 + 成本×统盈%`、**空** `Ask ≤ 成本 − 成本×统盈%` 时**整篮**平仓。 +- 市价加仓判定:多关注 **Ask** 与上一开间距;空 **Bid** 与上一开(与 v1.08 前仅用对侧价相比更贴实际成价)。 +- **图表线** `InpDrawVtp`:仅作目标价参考,非经纪商挂单。 +- **每单看统盈价**:`InpShowBasketTgtList=true` 时,在**图表左下**用文本列出该向每笔 `#`、开仓、以及**同一条**统盈目标 T(与程序整篮平仓一致)。**交易**窗口里多档时「获利」仍可能全 0:统盈是程序按成本判断,**不能把同一统盈价在每一笔上都设成有效经纪商 TP**(否则或拒单、或只平部分单),故用图上列表作对照。