diff --git a/20260509-突破策略/README.md b/20260509-突破策略/README.md deleted file mode 100644 index 484af03..0000000 --- a/20260509-突破策略/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# 均线突破策略 - -## 基础概念 -- 平台: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 deleted file mode 100644 index 1e998cb..0000000 --- a/20260509-突破策略/zyb.20260509.breakout.mq5 +++ /dev/null @@ -1,420 +0,0 @@ -//+------------------------------------------------------------------+ -//| 均线突破策略 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/20260516-突破策略/20260516_Breakout.mq5 b/20260516-突破策略/20260516_Breakout.mq5 index 592b065..cbe0072 100644 --- a/20260516-突破策略/20260516_Breakout.mq5 +++ b/20260516-突破策略/20260516_Breakout.mq5 @@ -3,11 +3,17 @@ //| 突破交易策略 - 完整交易版本 | //+------------------------------------------------------------------+ #property copyright "Breakout Strategy" -#property version "1.00" +#property version "1.01" #property strict #include +// 手数计算方式 +enum ENUM_LOT_SIZE_MODE { + LOT_SIZE_FIXED = 0, // 固定手数 + LOT_SIZE_RISK_PERCENT = 1 // 结余风险比例(RiskPercent) +}; + //+------------------------------------------------------------------+ //| 输入参数 | //+------------------------------------------------------------------+ @@ -15,20 +21,21 @@ input group "=== 波段识别参数 ===" input ENUM_TIMEFRAMES InpTimeframe = PERIOD_M1; // K线周期 input int InpMAPeriod = 14; // MA周期 input double InpMinWavePercent = 0.1; // 最小波段阈值百分比(%) -input double InpMaxWavePercent = 1.0; // 最大波段阈值百分比(%) +input double InpMaxWavePercent = 10.0; // 最大波段阈值百分比(%) input double InpPullbackTolerance = 0.0; // 反向突破容忍度(%) 0=不容忍 +input int InpMinWaveBars = 3; // 有效波段最少K线数(含两端极值所在K) input group "=== 风险管理参数 ===" -input double InpStopLossPercent = 0.02; // 止损百分比(%) -input int InpMinStopLossPoints = 10; // 最小止损点数 -input double InpRiskRewardRatio = 1.2; // 盈亏比 -input bool InpUseTrailingStop = false; // 使用移动止损 +input int InpStopLossPoints = 200; // 止损点数 +input int InpTakeProfitPoints = 300; // 止盈点数 +input bool InpUseTrailingStop = true; // 使用移动止损 input group "=== 仓位管理参数 ===" -input bool InpUseCompounding = true; // 使用复利模式 -input double InpFixedLots = 0.01; // 固定手数 -input double InpLotsPer500 = 0.05; // 每500$开仓手数(复利模式) -input int InpMaxPositions = 99; // 最大开仓手数 +input ENUM_LOT_SIZE_MODE InpLotSizeMode = LOT_SIZE_RISK_PERCENT; // 手数模式 +input double InpFixedLots = 0.01; // 固定手数(固定模式) +input double InpRiskPercent = 5.0; // 每笔风险占结余%(风险比例模式) +input double InpMaxLots = 99.0; // 单笔最大手数(0=仅受品种限制) +input int InpMaxPositions = 99; // 最大持仓笔数 input bool InpOnePositionPerDirection = true; // 单方向最多持有一单 input group "=== 连续亏损保护 ===" @@ -59,6 +66,9 @@ struct ValidWaveInfo { ValidWaveInfo latest_wave; // 最新有效波段 +double g_sync_wave_high = 0.0; // 已挂单的波段高价(用于检测换波段) +double g_sync_wave_low = 0.0; + // 连续亏损保护相关变量 int consecutive_loss_count = 0; // 连续亏损计数器 datetime freeze_until_time = 0; // 冷冻结束时间(0表示未冷冻) @@ -74,14 +84,24 @@ struct ExtremePoint { // 函数声明 void UpdateLatestValidWave(); +int CountBarsBetweenExtremeTimes(const MqlRates &rates[], const datetime t1, const datetime t2); +bool IsValidWaveByBarCount(const MqlRates &rates[], const datetime t1, const datetime t2); void DrawExtremeMarkers(ExtremePoint &extremes[]); void DrawLatestValidWave(double high_price, datetime high_time, double low_price, datetime low_time); int CheckBreakout(int index, const MqlRates &rates[], const double &ma[]); void FilterBreakouts(const int &breakout_bars[], const int &breakout_types[], const MqlRates &rates[], int &filtered_bars[], int &filtered_types[]); -void CheckOpenSignals(); -bool OpenPosition(ENUM_ORDER_TYPE order_type, double wave_high, double wave_low, double threshold_points); -double CalculateLotSize(); +void SyncBreakoutPendingOrders(); +void CancelEaPendingOrders(const bool cancel_buy_stop, const bool cancel_sell_stop); +ulong FindEaPendingOrder(const ENUM_ORDER_TYPE order_type); +bool CalcPendingSlTp(const ENUM_ORDER_TYPE pending_type, const double trigger_price, double &sl, double &tp); +bool PlaceBreakoutPendingOrder(const ENUM_ORDER_TYPE pending_type, const double trigger_price, + const double wave_high, const double wave_low); +double GetStopLossOffset(); +double GetTakeProfitOffset(); +double StopLossAmountFromPositionComment(const string &comment); +double CalculateLotSize(const double wave_high, const double wave_low); +double NormalizeVolumeLots(double lots); void ManagePositions(); void CheckTrailingStop(ulong ticket); void CheckAndCloseManualOrders(); @@ -117,20 +137,24 @@ int OnInit() Print("K线周期:", EnumToString(InpTimeframe)); Print("MA周期:", InpMAPeriod); Print("波段阈值范围: ", InpMinWavePercent, "% - ", InpMaxWavePercent, "%"); + Print("有效波段最少K线: ", InpMinWaveBars, " (两极值间含两端,<=1=不限制)"); if(InpPullbackTolerance > 0) Print("反向突破容忍度: ", DoubleToString(InpPullbackTolerance, 1), "% (启用)"); else Print("反向突破容忍度: 0% (禁用 - 保持原有逻辑)"); - Print("止损:", InpStopLossPercent, "% (最小", InpMinStopLossPoints, "点) | 盈亏比:", InpRiskRewardRatio); + Print("止损:", InpStopLossPoints, "点 | 止盈:", InpTakeProfitPoints, "点"); Print("移动止损:", (InpUseTrailingStop ? "启用" : "禁用")); - Print("最大开仓手数:", InpMaxPositions); + Print("最大持仓笔数:", InpMaxPositions, " 单笔最大手数:", InpMaxLots); Print("单方向持仓限制:", (InpOnePositionPerDirection ? "启用 (每方向最多1单)" : "禁用")); if(InpConsecutiveLosses > 0 && InpFreezeBarCount > 0) Print("连续亏损保护: 启用 (", InpConsecutiveLosses, "次亏损→冷冻", InpFreezeBarCount, "根K线)"); else Print("连续亏损保护: 禁用"); - Print("手数模式:", (InpUseCompounding ? "复利" : "固定"), - InpUseCompounding ? StringFormat(" (每500$开%.2f手)", InpLotsPer500) : StringFormat(" (%.2f手)", InpFixedLots)); + if(InpLotSizeMode == LOT_SIZE_RISK_PERCENT) + Print("手数模式: 结余风险比例 ", InpRiskPercent, "% (按止损距离反推手数)"); + else + Print("手数模式: 固定手数 ", InpFixedLots); + Print("开仓方式: 突破挂单 (高点BUY STOP / 低点SELL STOP)"); Print("禁止手工单:", (InpCloseManualOrders ? "启用 (自动平掉手工单)" : "禁用")); Print("========================================"); @@ -147,6 +171,7 @@ void OnDeinit(const int reason) // 删除所有标记 ObjectsDeleteAll(0, "ValidWave_"); + CancelEaPendingOrders(true, true); Print("突破交易策略EA已卸载"); } @@ -158,7 +183,22 @@ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { - // 只在功能启用时处理 + if(trans.type == TRADE_TRANSACTION_DEAL_ADD) { + if(HistoryDealSelect(trans.deal)) { + if(HistoryDealGetString(trans.deal, DEAL_SYMBOL) == _Symbol && + HistoryDealGetInteger(trans.deal, DEAL_MAGIC) == InpMagicNumber && + HistoryDealGetInteger(trans.deal, DEAL_ENTRY) == DEAL_ENTRY_IN) { + const ENUM_DEAL_TYPE deal_type = + (ENUM_DEAL_TYPE)HistoryDealGetInteger(trans.deal, DEAL_TYPE); + if(deal_type == DEAL_TYPE_BUY) + latest_wave.high_used = true; + else if(deal_type == DEAL_TYPE_SELL) + latest_wave.low_used = true; + } + } + } + + // 只在功能启用时处理连续亏损 if(InpConsecutiveLosses <= 0 || InpFreezeBarCount <= 0) return; @@ -236,8 +276,8 @@ void OnTick() // 2. 更新最新有效波段 UpdateLatestValidWave(); - // 3. 检查开仓信号 - CheckOpenSignals(); + // 3. 同步突破挂单(BUY STOP@高 / SELL STOP@低) + SyncBreakoutPendingOrders(); // 4. 管理已有持仓 ManagePositions(); @@ -246,6 +286,26 @@ void OnTick() //+------------------------------------------------------------------+ //| 更新最新有效波段 | //+------------------------------------------------------------------+ +int CountBarsBetweenExtremeTimes(const MqlRates &rates[], const datetime t1, const datetime t2) +{ + const datetime t_lo = (t1 <= t2) ? t1 : t2; + const datetime t_hi = (t1 >= t2) ? t1 : t2; + int count = 0; + const int n = ArraySize(rates); + for(int j = 0; j < n; j++) { + if(rates[j].time >= t_lo && rates[j].time <= t_hi) + count++; + } + return count; +} + +bool IsValidWaveByBarCount(const MqlRates &rates[], const datetime t1, const datetime t2) +{ + if(InpMinWaveBars <= 1) + return true; + return (CountBarsBetweenExtremeTimes(rates, t1, t2) >= InpMinWaveBars); +} + void UpdateLatestValidWave() { int bars = Bars(_Symbol, InpTimeframe); @@ -455,7 +515,8 @@ void UpdateLatestValidWave() double max_threshold = (base_price * InpMaxWavePercent / 100.0) / _Point; // 波段必须在最小和最大阈值之间才是有效波段 - if(price_diff_points >= min_threshold && price_diff_points <= max_threshold) { + if(price_diff_points >= min_threshold && price_diff_points <= max_threshold && + IsValidWaveByBarCount(rates, extremes[i - 1].time, extremes[i].time)) { extremes[i-1].is_valid = true; extremes[i].is_valid = true; } @@ -477,7 +538,8 @@ void UpdateLatestValidWave() double max_threshold = (base_price * InpMaxWavePercent / 100.0) / _Point; // 波段必须在最小和最大阈值之间才是有效波段 - if(price_diff_points >= min_threshold && price_diff_points <= max_threshold) { + if(price_diff_points >= min_threshold && price_diff_points <= max_threshold && + IsValidWaveByBarCount(rates, extremes[i - 1].time, extremes[i].time)) { // 找到最新的有效波段 double high = MathMax(extremes[i].price, extremes[i-1].price); double low = MathMin(extremes[i].price, extremes[i-1].price); @@ -506,6 +568,7 @@ void UpdateLatestValidWave() Print("更新最新有效波段 - 高:", DoubleToString(high, _Digits), " 低:", DoubleToString(low, _Digits), " 价差:", (int)price_diff_points, "点", + " K线数:", CountBarsBetweenExtremeTimes(rates, extremes[i - 1].time, extremes[i].time), " 阈值范围:", StringFormat("%.2f%%-%.2f%% (%.0f-%.0f点)", InpMinWavePercent, InpMaxWavePercent, min_threshold, max_threshold)); } @@ -632,39 +695,143 @@ void FilterBreakouts(const int &breakout_bars[], const int &breakout_types[], } //+------------------------------------------------------------------+ -//| 检查开仓信号 | +//| 突破挂单管理 | //+------------------------------------------------------------------+ -void CheckOpenSignals() +void CancelEaPendingOrders(const bool cancel_buy_stop, const bool cancel_sell_stop) { - if(!latest_wave.exists) - return; + for(int i = OrdersTotal() - 1; i >= 0; i--) { + const ulong ticket = OrderGetTicket(i); + if(ticket == 0 || !OrderSelect(ticket)) + continue; + if(OrderGetString(ORDER_SYMBOL) != _Symbol) + continue; + if(OrderGetInteger(ORDER_MAGIC) != InpMagicNumber) + continue; - // 检查是否处于冷冻期 - if(InpConsecutiveLosses > 0 && InpFreezeBarCount > 0 && freeze_bar_index > 0) - { - int current_bars = Bars(_Symbol, InpTimeframe); - if(current_bars < freeze_bar_index) - { - // 仍在冷冻期内 - 静默拒绝开仓 + const ENUM_ORDER_TYPE ot = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); + if(cancel_buy_stop && ot == ORDER_TYPE_BUY_STOP) + trade.OrderDelete(ticket); + else if(cancel_sell_stop && ot == ORDER_TYPE_SELL_STOP) + trade.OrderDelete(ticket); + } +} + +ulong FindEaPendingOrder(const ENUM_ORDER_TYPE order_type) +{ + for(int i = OrdersTotal() - 1; i >= 0; i--) { + const ulong ticket = OrderGetTicket(i); + if(ticket == 0 || !OrderSelect(ticket)) + continue; + if(OrderGetString(ORDER_SYMBOL) != _Symbol) + continue; + if(OrderGetInteger(ORDER_MAGIC) != InpMagicNumber) + continue; + if((ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE) == order_type) + return ticket; + } + return 0; +} + +bool CalcPendingSlTp(const ENUM_ORDER_TYPE pending_type, const double trigger_price, double &sl, double &tp) +{ + const double stop_loss_amount = GetStopLossOffset(); + const double take_profit_amount = GetTakeProfitOffset(); + const int stops_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); + const double min_stop_distance = stops_level * _Point; + + sl = 0.0; + tp = 0.0; + + if(pending_type == ORDER_TYPE_BUY_STOP) { + sl = NormalizeDouble(trigger_price - stop_loss_amount, _Digits); + tp = NormalizeDouble(trigger_price + take_profit_amount, _Digits); + if(stops_level > 0) { + if(trigger_price - sl < min_stop_distance) + sl = NormalizeDouble(trigger_price - min_stop_distance, _Digits); + if(tp - trigger_price < min_stop_distance) + tp = NormalizeDouble(trigger_price + min_stop_distance, _Digits); + } + } else if(pending_type == ORDER_TYPE_SELL_STOP) { + sl = NormalizeDouble(trigger_price + stop_loss_amount, _Digits); + tp = NormalizeDouble(trigger_price - take_profit_amount, _Digits); + if(stops_level > 0) { + if(sl - trigger_price < min_stop_distance) + sl = NormalizeDouble(trigger_price + min_stop_distance, _Digits); + if(trigger_price - tp < min_stop_distance) + tp = NormalizeDouble(trigger_price - min_stop_distance, _Digits); + } + } else { + return false; + } + return true; +} + +bool PlaceBreakoutPendingOrder(const ENUM_ORDER_TYPE pending_type, const double trigger_price, + const double wave_high, const double wave_low) +{ + const double lots = CalculateLotSize(wave_high, wave_low); + if(lots <= 0.0) + return false; + + double sl = 0.0, tp = 0.0; + if(!CalcPendingSlTp(pending_type, trigger_price, sl, tp)) + return false; + + const int wave_range_points = (int)MathRound(MathAbs(wave_high - wave_low) / _Point); + const string comment = StringFormat("WR%d", wave_range_points); + + bool result = false; + if(pending_type == ORDER_TYPE_BUY_STOP) + result = trade.BuyStop(lots, trigger_price, _Symbol, sl, tp, ORDER_TIME_GTC, 0, comment); + else if(pending_type == ORDER_TYPE_SELL_STOP) + result = trade.SellStop(lots, trigger_price, _Symbol, sl, tp, ORDER_TIME_GTC, 0, comment); + + if(!result) { + Print("挂单失败 ", EnumToString(pending_type), " 触发价:", trigger_price, + " 错误:", trade.ResultRetcode(), " - ", trade.ResultRetcodeDescription()); + return false; + } + + Print("突破挂单成功 ", EnumToString(pending_type), + " 触发:", trigger_price, " 手数:", lots, + " SL:", sl, " TP:", tp, + " 波段 H:", wave_high, " L:", wave_low); + return true; +} + +void SyncBreakoutPendingOrders() +{ + if(!latest_wave.exists) { + CancelEaPendingOrders(true, true); + g_sync_wave_high = 0.0; + g_sync_wave_low = 0.0; + return; + } + + if(InpConsecutiveLosses > 0 && InpFreezeBarCount > 0 && freeze_bar_index > 0) { + const int current_bars = Bars(_Symbol, InpTimeframe); + if(current_bars < freeze_bar_index) { + CancelEaPendingOrders(true, true); return; } - else - { - // 冷冻期结束 - if(freeze_bar_index > 0) - { - Print("【连续亏损保护】冷冻解除,恢复交易"); - consecutive_loss_count = 0; - freeze_bar_index = 0; - freeze_until_time = 0; - } + if(freeze_bar_index > 0) { + Print("【连续亏损保护】冷冻解除,恢复交易"); + consecutive_loss_count = 0; + freeze_bar_index = 0; + freeze_until_time = 0; } } - // 检查当前持仓数量和方向 + if(MathAbs(latest_wave.high_price - g_sync_wave_high) > _Point * 0.5 || + MathAbs(latest_wave.low_price - g_sync_wave_low) > _Point * 0.5) { + CancelEaPendingOrders(true, true); + g_sync_wave_high = latest_wave.high_price; + g_sync_wave_low = latest_wave.low_price; + } + int total_positions = 0; - int buy_positions = 0; // 多单数量 - int sell_positions = 0; // 空单数量 + int buy_positions = 0; + int sell_positions = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { if(!PositionSelectByTicket(PositionGetTicket(i))) @@ -673,11 +840,8 @@ void CheckOpenSignals() continue; if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; - total_positions++; - - // 统计各方向持仓数量 - ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + const ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); if(pos_type == POSITION_TYPE_BUY) buy_positions++; else if(pos_type == POSITION_TYPE_SELL) @@ -685,226 +849,131 @@ void CheckOpenSignals() } if(total_positions >= InpMaxPositions) { + CancelEaPendingOrders(true, true); if(InpShowDebugInfo) - Print("已达最大持仓数量限制: ", total_positions, "/", InpMaxPositions); + Print("已达最大持仓笔数,撤销突破挂单: ", total_positions, "/", InpMaxPositions); return; } - double current_price = SymbolInfoDouble(_Symbol, SYMBOL_BID); + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + const double wave_high = latest_wave.high_price; + const double wave_low = latest_wave.low_price; + const double lots = CalculateLotSize(wave_high, wave_low); - // 计算当前波段的实际阈值(百分比模式:取高低点的平均值作为基准) - double base_price = (latest_wave.high_price + latest_wave.low_price) / 2.0; - double wave_threshold_points = (base_price * InpMinWavePercent / 100.0) / _Point; - - // 检查多单信号:突破高点(且该高点未使用过) - if(!latest_wave.high_used && current_price > latest_wave.high_price) { - // 如果启用了单方向持仓限制,检查多单数量 - if(InpOnePositionPerDirection && buy_positions >= 1) { + // 高点 BUY STOP + if(!latest_wave.high_used && (!InpOnePositionPerDirection || buy_positions < 1)) { + if(ask >= wave_high - _Point * 0.5) { + CancelEaPendingOrders(true, false); if(InpShowDebugInfo) - Print("多单信号被忽略 - 已有多单持仓: ", buy_positions); + Print("价格已越过波段高点,暂不挂BUY STOP Ask:", ask, " 高:", wave_high); } else { - if(InpShowDebugInfo) - Print("检测到多单信号 - 价格:", current_price, " 突破高点:", latest_wave.high_price); - - if(OpenPosition(ORDER_TYPE_BUY, latest_wave.high_price, latest_wave.low_price, wave_threshold_points)) { - latest_wave.high_used = true; // 标记高点已使用,该波段高点失效 - if(InpShowDebugInfo) - Print("高点已使用,等待新的有效波段"); + ulong ticket = FindEaPendingOrder(ORDER_TYPE_BUY_STOP); + if(ticket > 0 && OrderSelect(ticket)) { + const double order_price = OrderGetDouble(ORDER_PRICE_OPEN); + const double order_lots = OrderGetDouble(ORDER_VOLUME_CURRENT); + if(MathAbs(order_price - wave_high) > _Point * 0.5 || + MathAbs(order_lots - lots) > 1e-8) { + trade.OrderDelete(ticket); + ticket = 0; + } } + if(ticket == 0) + PlaceBreakoutPendingOrder(ORDER_TYPE_BUY_STOP, wave_high, wave_high, wave_low); } + } else { + CancelEaPendingOrders(true, false); } - // 检查空单信号:突破低点(且该低点未使用过) - if(!latest_wave.low_used && current_price < latest_wave.low_price) { - // 如果启用了单方向持仓限制,检查空单数量 - if(InpOnePositionPerDirection && sell_positions >= 1) { + // 低点 SELL STOP + if(!latest_wave.low_used && (!InpOnePositionPerDirection || sell_positions < 1)) { + if(bid <= wave_low + _Point * 0.5) { + CancelEaPendingOrders(false, true); if(InpShowDebugInfo) - Print("空单信号被忽略 - 已有空单持仓: ", sell_positions); + Print("价格已越过波段低点,暂不挂SELL STOP Bid:", bid, " 低:", wave_low); } else { - if(InpShowDebugInfo) - Print("检测到空单信号 - 价格:", current_price, " 突破低点:", latest_wave.low_price); - - if(OpenPosition(ORDER_TYPE_SELL, latest_wave.high_price, latest_wave.low_price, wave_threshold_points)) { - latest_wave.low_used = true; // 标记低点已使用,该波段低点失效 - if(InpShowDebugInfo) - Print("低点已使用,等待新的有效波段"); + ulong ticket = FindEaPendingOrder(ORDER_TYPE_SELL_STOP); + if(ticket > 0 && OrderSelect(ticket)) { + const double order_price = OrderGetDouble(ORDER_PRICE_OPEN); + const double order_lots = OrderGetDouble(ORDER_VOLUME_CURRENT); + if(MathAbs(order_price - wave_low) > _Point * 0.5 || + MathAbs(order_lots - lots) > 1e-8) { + trade.OrderDelete(ticket); + ticket = 0; + } } + if(ticket == 0) + PlaceBreakoutPendingOrder(ORDER_TYPE_SELL_STOP, wave_low, wave_high, wave_low); } + } else { + CancelEaPendingOrders(false, true); } } //+------------------------------------------------------------------+ -//| 开仓 | +//| 止损/止盈距离(点数) | //+------------------------------------------------------------------+ -bool OpenPosition(ENUM_ORDER_TYPE order_type, double wave_high, double wave_low, double threshold_points) +double GetStopLossOffset() { - double lots = CalculateLotSize(); - if(lots <= 0) - return false; + if(InpStopLossPoints <= 0) + return 0.0; + return (double)InpStopLossPoints * _Point; +} - // 生成备注信息:盈亏比和最小止损点数 - string comment = StringFormat("R%.1f SL%d", - InpRiskRewardRatio, - InpMinStopLossPoints); +double GetTakeProfitOffset() +{ + if(InpTakeProfitPoints <= 0) + return 0.0; + return (double)InpTakeProfitPoints * _Point; +} - bool result = false; +double StopLossAmountFromPositionComment(const string &comment) +{ + return GetStopLossOffset(); +} - // 先以市价开仓(不带SL/TP),成交后再根据实际成交价设置SL/TP - if(order_type == ORDER_TYPE_BUY) { - result = trade.Buy(lots, _Symbol, 0, 0, 0, comment); - } else { - result = trade.Sell(lots, _Symbol, 0, 0, 0, comment); - } +//+------------------------------------------------------------------+ +//| 手数规范化 | +//+------------------------------------------------------------------+ +double NormalizeVolumeLots(double lots) +{ + const double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + const double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); + const double lot_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); - if(!result) { - Print("开仓失败: ", trade.ResultRetcode(), " - ", trade.ResultRetcodeDescription()); - return false; - } - - // 等待持仓信息更新 - Sleep(100); - - // 通过符号和魔术号查找刚开的持仓 - ulong ticket = 0; - double open_price = 0; - - for(int i = PositionsTotal() - 1; i >= 0; i--) { - if(!PositionSelectByTicket(PositionGetTicket(i))) - continue; - - if(PositionGetString(POSITION_SYMBOL) != _Symbol) - continue; - - if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) - continue; - - // 找到最新的持仓(没有SL/TP的) - if(PositionGetDouble(POSITION_SL) == 0 && PositionGetDouble(POSITION_TP) == 0) { - ticket = PositionGetTicket(i); - open_price = PositionGetDouble(POSITION_PRICE_OPEN); - break; - } - } - - if(ticket == 0) { - Print("无法找到刚开的持仓"); - return false; - } - - // 基于实际成交价计算止损金额 - double stop_loss_amount = open_price * InpStopLossPercent / 100.0; - - // 确保止损金额不小于最小止损点数 - double min_stop_loss = InpMinStopLossPoints * _Point; - if(stop_loss_amount < min_stop_loss) { - stop_loss_amount = min_stop_loss; - } - - // 获取平台的最小止损距离 - int stops_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); - double min_stop_distance = stops_level * _Point; - - // 获取当前市场价格 - double current_bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); - double current_ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); - - double sl = 0, tp = 0; - - if(order_type == ORDER_TYPE_BUY) { - // 先标准化止损价 - sl = NormalizeDouble(open_price - stop_loss_amount, _Digits); - // 根据标准化后的实际止损金额重新计算止盈金额,确保盈亏比准确 - double actual_sl_amount = open_price - sl; - double take_profit_amount = actual_sl_amount * InpRiskRewardRatio; - tp = NormalizeDouble(open_price + take_profit_amount, _Digits); - - // 检查止损距离(多单检查与Bid的距离) - if(stops_level > 0 && (current_bid - sl) < min_stop_distance) { - sl = NormalizeDouble(current_bid - min_stop_distance, _Digits); - // 重新计算止盈 - actual_sl_amount = open_price - sl; - take_profit_amount = actual_sl_amount * InpRiskRewardRatio; - tp = NormalizeDouble(open_price + take_profit_amount, _Digits); - Print("止损距离不足,已调整 - 新止损:", sl); - } - - // 检查止盈距离 - if(stops_level > 0 && (tp - current_ask) < min_stop_distance) { - tp = NormalizeDouble(current_ask + min_stop_distance, _Digits); - Print("止盈距离不足,已调整 - 新止盈:", tp); - } - } else { - // 先标准化止损价 - sl = NormalizeDouble(open_price + stop_loss_amount, _Digits); - // 根据标准化后的实际止损金额重新计算止盈金额,确保盈亏比准确 - double actual_sl_amount = sl - open_price; - double take_profit_amount = actual_sl_amount * InpRiskRewardRatio; - tp = NormalizeDouble(open_price - take_profit_amount, _Digits); - - // 检查止损距离(空单检查与Ask的距离) - if(stops_level > 0 && (sl - current_ask) < min_stop_distance) { - sl = NormalizeDouble(current_ask + min_stop_distance, _Digits); - // 重新计算止盈 - actual_sl_amount = sl - open_price; - take_profit_amount = actual_sl_amount * InpRiskRewardRatio; - tp = NormalizeDouble(open_price - take_profit_amount, _Digits); - Print("止损距离不足,已调整 - 新止损:", sl); - } - - // 检查止盈距离 - if(stops_level > 0 && (current_bid - tp) < min_stop_distance) { - tp = NormalizeDouble(current_bid - min_stop_distance, _Digits); - Print("止盈距离不足,已调整 - 新止盈:", tp); - } - } - - // 修改持仓的SL/TP - if(!trade.PositionModify(ticket, sl, tp)) { - Print("修改SL/TP失败 - Ticket:", ticket, - " 错误码:", trade.ResultRetcode(), - " 描述:", trade.ResultRetcodeDescription(), - " 止损:", sl, " 止盈:", tp, - " 平台最小距离:", stops_level, "点"); - return false; - } - - Print(order_type == ORDER_TYPE_BUY ? "开多单成功" : "开空单成功", - " - 手数:", lots, - " 波段:H:", wave_high, " L:", wave_low, - " 实际成交价:", open_price, - " 止损:", sl, "(", InpStopLossPercent, "%)", - " 止盈:", tp, "(盈亏比", InpRiskRewardRatio, ")"); - - return true; + if(InpMaxLots > 0.0) + lots = MathMin(lots, InpMaxLots); + if(lots < min_lot) + lots = min_lot; + if(lots > max_lot) + lots = max_lot; + if(lot_step > 0.0) + lots = MathFloor(lots / lot_step) * lot_step; + return lots; } //+------------------------------------------------------------------+ //| 计算开仓手数 | //+------------------------------------------------------------------+ -double CalculateLotSize() +double CalculateLotSize(const double wave_high, const double wave_low) { - double lots = 0; + double lots = InpFixedLots; - if(InpUseCompounding) { - // 复利模式 - double balance = AccountInfoDouble(ACCOUNT_BALANCE); - lots = NormalizeDouble((balance / 500.0) * InpLotsPer500, 2); - } else { - // 固定手数模式 - lots = InpFixedLots; + if(InpLotSizeMode == LOT_SIZE_RISK_PERCENT && InpRiskPercent > 0.0) { + const double stop_loss_dist = GetStopLossOffset(); + const double balance = AccountInfoDouble(ACCOUNT_BALANCE); + const double risk_money = balance * InpRiskPercent / 100.0; + + const double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); + const double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); + if(stop_loss_dist > 0.0 && tick_size > 0.0 && tick_value > 0.0 && risk_money > 0.0) { + const double loss_per_lot = (stop_loss_dist / tick_size) * tick_value; + if(loss_per_lot > 0.0) + lots = risk_money / loss_per_lot; + } } - // 检查手数限制 - double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); - double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); - double lot_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); - - if(lots < min_lot) lots = min_lot; - if(lots > max_lot) lots = max_lot; - - lots = MathFloor(lots / lot_step) * lot_step; - - return lots; + return NormalizeVolumeLots(lots); } //+------------------------------------------------------------------+ @@ -947,14 +1016,9 @@ void CheckTrailingStop(ulong ticket) SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK); - // 计算止损金额(开仓价的百分比) - double stop_loss_amount = open_price * InpStopLossPercent / 100.0; - - // 确保止损金额不小于最小止损点数 - double min_stop_loss = InpMinStopLossPoints * _Point; - if(stop_loss_amount < min_stop_loss) { - stop_loss_amount = min_stop_loss; - } + // 计算止损金额(有效波段区间 × 比例,备注中 WR 存区间点数) + const string comment = PositionGetString(POSITION_COMMENT); + const double stop_loss_amount = StopLossAmountFromPositionComment(comment); // 计算浮盈 double profit_amount = 0; diff --git a/20260516-突破策略/README.md b/20260516-突破策略/README.md index e18d257..bd5ae8a 100644 --- a/20260516-突破策略/README.md +++ b/20260516-突破策略/README.md @@ -13,44 +13,68 @@ ## 核心概念 - 突破K线:开盘价在均线下方,收盘价在均线上方,称之为多单突破K线,反之亦然 - 极值点:相邻两根突破K线之间所有K线(包括突破K线)中的最高价或最低价称之为极值点 -- 有效波段:相邻两个极值点的价差达到波段阈值点数时,期间的所有K线(包含突破K线本身)组成的行情,称之为有效波段 +- 有效波段:相邻两个极值点的价差达到波段阈值点数,且**两极值之间至少包含 3 根 K 线**(含两端极值所在 K),期间所有 K 线组成有效波段 ## 开仓逻辑 -- 价格突破有效波段的高点开多单,价格突破有效波点的低点开空单 -- 该开仓逻辑仅适用最新的一个有效波段 -- 止损和止盈基于开仓价的百分比计算,止盈 = 止损金额 × 盈亏比 -- 浮盈达到止损金额后,即可将止损移动至成本价 +- 识别最新有效波段后,在**波段高点挂 BUY STOP**、**波段低点挂 SELL STOP**(突破触发) +- 挂单时一并设置 SL/TP(固定止损/止盈点数) +- 该逻辑仅适用最新的一个有效波段;换波段时撤销旧挂单并重挂 +- 价格已越过极值(无法挂 STOP)时暂不挂单,等高/低侧标记使用后等待新波段 +- 浮盈达到止损点数对应距离后,即可将止损移动至成本价 - 已经开仓后,继续监控新的有效波段突破,每单开仓后就与有效波段无关了 +## 参数默认值对照表 + +| 分组 | 参数 | 默认 | +|------|------|------| +| 波段 | K线周期 / MA周期 | M1 / 14 | +| 波段 | 最小/最大波段% | 0.1 / **10** | +| 波段 | 反向突破容忍度% / 最少K线 | 0 / **3** | +| 风险 | 止损/止盈点数 | **200** / **300** | +| 风险 | 移动止损 | **true** | +| 仓位 | 手数模式 | **结余风险比例** | +| 仓位 | 固定手数 / 风险% | 0.01 / **5** | +| 仓位 | 单笔最大手数 / 最大持仓笔数 | 99 / 99 | +| 仓位 | 单方向最多一单 | **true** | +| 保护 | 连亏冷冻次数 / 冷冻K线 | 3 / 60 | +| 调试 | 调试信息 / 极值标记 / Magic / 禁手工单 | false / **true** / 20260516 / **true** | + ## 参数设置 ### 波段识别参数 - K线周期:支持参数设置,不跟随图表,默认 **1 Minute** - MA周期:默认 **14** -- **最小波段阈值百分比:默认 0.1%**(波段必须 >= 此值才可能是有效波段) -- **最大波段阈值百分比:默认 1.0%**(波段必须 <= 此值才可能是有效波段) -- **有效波段范围:0.1% - 1.0%**(只交易此范围内的波段) -- **反向突破容忍度:默认0.0%(新功能 - 详见下方说明)** +- **最小波段阈值百分比:默认 0.1%** +- **最大波段阈值百分比:默认 10%** +- **有效波段范围:0.1% - 10%** +- **有效波段最少 K 线数:默认 3** +- **反向突破容忍度:默认 0%** ### 风险管理参数 -- 止损百分比:默认 **0.05%**(基于开仓价计算,最小1美元/100点) -- 盈亏比:默认 **1.5**(止盈 = 止损金额 × 盈亏比) -- 使用移动止损:默认 **false**(禁用移动止损功能) -- 最大持仓时间:默认 **5分钟** +- 止损点数:默认 **200** +- 止盈点数:默认 **300** +- 使用移动止损:默认 **true**(浮盈达止损点数后移至成本价) ### 仓位管理参数 -- 使用复利模式:默认 **true**(启用复利模式) +- 手数模式:默认 **结余风险比例 RiskPercent**(可选固定手数) - 固定手数:默认 **0.01手** -- 每500$开仓手数:默认 **0.05手**(复利模式有效) -- 最大开仓手数:默认 **99手** -- 单方向最多持有一单:默认 **true**(每个方向最多持有1单) +- 每笔风险占结余%:默认 **5%** +- 单笔最大手数:默认 **99手** +- 最大持仓笔数:默认 **99** +- 单方向最多持有一单:默认 **true** + +**RiskPercent 手数公式:** +`风险金额 = 结余 × 风险%`;`手数 = 风险金额 ÷ (止损点数对应的每手亏损)` ### 连续亏损保护参数 -- 连续亏损次数触发冷冻:默认 **3**(设为0可禁用该功能) -- 冷冻K线根数:默认 **60**(设为0可禁用该功能) +- 连续亏损次数触发冷冻:默认 **3** +- 冷冻K线根数:默认 **60** -### 手工单管理参数 -- 禁止手工单:默认 **true**(自动平掉所有手工单) +### 调试选项 +- 显示调试信息:默认 **false** +- 显示极值点标记:默认 **true** +- EA魔术号:默认 **20260516** +- 禁止手工单:默认 **true** ## 补充说明 @@ -59,6 +83,8 @@ - 多头有效波段之间,仅允许出现一个多单突破K线和一个空单突破K线,反之亦然 ### 有效波段规则 +- 相邻极值点价差须在最小~最大波段阈值范围内 +- 两极值点之间(含两端极值所在 K)**至少 3 根 K 线**,否则不构成有效波段 - 一个有效波段只能开一单(无论是高点还是低点) - 开仓后该波段立即失效 - 必须等新的有效波段形成才能再次开仓 @@ -67,12 +93,8 @@ 此参数用于控制同一方向的持仓数量,避免单边风险过大。 **参数说明:** -- **false(默认)**:允许同方向开多单,仅受"最大开仓手数"限制 - - 例如:最大开仓手数=3时,可以同时持有3个多单,或3个空单,或2多1空等 -- **true**:每个方向最多持有1单 - - 多单方向:最多持有1个多单 - - 空单方向:最多持有1个空单 - - 最多同时持有1多+1空=2单 +- **false**:允许同方向开多单,仅受「单笔最大手数」「最大持仓笔数」限制 +- **true(默认)**:每个方向最多持有 1 单(最多 1 多 + 1 空) **使用场景:** - **保守型交易者**:建议设置为 **true** @@ -342,12 +364,12 @@ #### 示例说明 假设设置: - 最小阈值:0.1% -- 最大阈值:1.0% +- 最大阈值:10% - 当前价格:2600 **价格2600时的阈值范围:** - 最小阈值 = 2600 × 0.1% = 2.6美元(260点) -- 最大阈值 = 2600 × 1.0% = 26美元(2600点) +- 最大阈值 = 2600 × 10% = 260美元(26000点) **波段筛选结果:** - 波段 2.0美元(200点):< 最小阈值 → ❌ 不交易(波段太小) @@ -357,44 +379,18 @@ **价格4700时的阈值范围:** - 最小阈值 = 4700 × 0.1% = 4.7美元(470点) -- 最大阈值 = 4700 × 1.0% = 47美元(4700点) +- 最大阈值 = 4700 × 10% = 470美元(47000点) #### 参数调整建议 -- **保守型**:0.15% - 0.5%(只交易中小波段) -- **标准型**:0.1% - 1.0%(默认,覆盖大多数波段) -- **激进型**:0.08% - 2.0%(交易范围更广) +- **保守型**:0.15% - 0.5% +- **标准型**:0.1% - 10%(默认) +- **激进型**:0.08% - 15% ### 止损止盈说明 -本策略使用**百分比模式**计算止损和止盈: +- **止损**:开仓价(或挂单触发价)± **止损点数** × Point +- **止盈**:开仓价(或挂单触发价)± **止盈点数** × Point +- **移动止损**(可选):浮盈 ≥ 止损点数对应的价格距离时,将止损移至成本价(仅一次) -- **止损计算**:止损金额 = MAX(开仓价 × 止损百分比, 1美元) - - 示例1(高价位):开仓价2600,止损0.05%,计算值 = 2600 × 0.05% = 1.3美元 > 1美元,使用1.3美元 - - 多单止损价 = 2600 - 1.3 = 2598.7 - - 空单止损价 = 2600 + 1.3 = 2601.3 - - 示例2(低价位):开仓价1500,止损0.05%,计算值 = 1500 × 0.05% = 0.75美元 < 1美元,使用最小值1美元 - - 多单止损价 = 1500 - 1 = 1499.0 - - 空单止损价 = 1500 + 1 = 1501.0 - -- **止盈计算**:止盈金额 = 止损金额 × 盈亏比 - - 示例1:止损金额1.3美元,盈亏比1.5,止盈金额 = 1.3 × 1.5 = 1.95美元 - - 多单止盈价 = 2600 + 1.95 = 2601.95 - - 空单止盈价 = 2600 - 1.95 = 2598.05 - - 示例2:止损金额1美元,盈亏比1.5,止盈金额 = 1 × 1.5 = 1.5美元 - - 多单止盈价 = 1500 + 1.5 = 1501.5 - - 空单止盈价 = 1500 - 1.5 = 1498.5 - -- **移动止损**(可选功能,默认启用): - - **触发条件**:当浮盈 >= 止损金额时触发 - - **移动目标**:将止损移至成本价(开仓价) - - **移动次数**:仅移动一次(保本策略) - - **示例**:开仓价2603.24,止损金额1.30美元 - - 当价格涨至2604.54(浮盈1.30)时,止损从2601.94移至2603.24 - - 之后价格无论涨到多高,止损都保持在2603.24 - - 即使回调至成本价,也能保本离场,不会亏损 - - **开关控制**:可通过参数"使用移动止损"关闭此功能 - -- **最小止损保护**:无论百分比设置多小,止损金额始终不低于1美元(100点),避免止损过小导致频繁触发 - -- **移动止损的作用**: - - ✅ 启用时:浮盈达标后锁定盈亏平衡,避免盈利单变亏损 - - ❌ 禁用时:止损始终保持在初始位置,追求更高止盈,但风险更大 +示例(多单,触发价 2610,止损 200 点、止盈 300 点): +- 止损价 ≈ 2610 − 2.00 = **2608.00**(黄金 2 位小数时按品种 Point 换算) +- 止盈价 ≈ 2610 + 3.00 = **2613.00** diff --git a/20260519-突破策略/20260519_Breakout.mq5 b/20260519-突破策略/20260519_Breakout.mq5 deleted file mode 100644 index d2b1615..0000000 --- a/20260519-突破策略/20260519_Breakout.mq5 +++ /dev/null @@ -1,1555 +0,0 @@ -//+------------------------------------------------------------------+ -//| 20260516_Trade.mq5 | -//| 突破交易策略 - 完整交易版本 | -//+------------------------------------------------------------------+ -#property copyright "Breakout Strategy" -#property version "1.00" -#property strict - -#include - -//+------------------------------------------------------------------+ -//| 输入参数 | -//+------------------------------------------------------------------+ -input group "=== 波段识别参数 ===" -input ENUM_TIMEFRAMES InpTimeframe = PERIOD_M1; // K线周期 -input int InpMAPeriod = 14; // MA周期 -input double InpMinWavePercent = 0.1; // 最小波段阈值百分比(%) -input double InpMaxWavePercent = 1.0; // 最大波段阈值百分比(%) -input double InpPullbackTolerance = 0.0; // 反向突破容忍度(%) 0=不容忍 - -input group "=== 风险管理参数 ===" -input double InpStopLossPercent = 0.02; // 止损百分比(%) -input int InpMinStopLossPoints = 10; // 最小止损点数 -input double InpRiskRewardRatio = 1.2; // 盈亏比 -input bool InpUseTrailingStop = false; // 使用移动止损 - -input group "=== 仓位管理参数 ===" -input bool InpUseCompounding = true; // 使用复利模式 -input double InpFixedLots = 0.01; // 固定手数 -input double InpLotsPer500 = 0.05; // 每500$开仓手数(复利模式) -input int InpMaxPositions = 99; // 最大开仓手数 -input bool InpOnePositionPerDirection = true; // 单方向最多持有一单 - -input group "=== 连续亏损保护 ===" -input int InpConsecutiveLosses = 3; // 连续亏损次数触发冷冻(0=禁用) -input int InpFreezeBarCount = 60; // 冷冻K线根数(0=禁用) - -input group "=== 补偿机制参数 ===" -input bool InpEnableCompensation = true; // 启用补偿机制 -input int InpMinCompensationQueueSize = 1; // 补偿队列最小长度(>=此值才开补偿单) -input double InpMaxCompensationLots = 5.0; // 补偿单最大手数 -input int InpCompensationStopLossPips = 100; // 补偿单止损点数 -input int InpMaxCompensationTakeProfitPips = 300; // 补偿单最大止盈点数 -input int InpMaxConsecutiveCompensations = 5; // 连续补偿次数限制(超过则重置) - -input group "=== 调试选项 ===" -input bool InpShowDebugInfo = false; // 显示调试信息 -input bool InpShowMarkers = true; // 显示极值点标记 -input int InpMagicNumber = 20260516; // EA魔术号 -input bool InpCloseManualOrders = true; // 禁止手工单(自动平掉) - -//+------------------------------------------------------------------+ -//| 全局变量 | -//+------------------------------------------------------------------+ -int ma_handle; // MA指标句柄 -CTrade trade; // 交易对象 - -// 最新有效波段信息 -struct ValidWaveInfo { - bool exists; // 是否存在有效波段 - double high_price; // 高点价格 - double low_price; // 低点价格 - datetime update_time; // 更新时间 - bool high_used; // 高点是否已使用 - bool low_used; // 低点是否已使用 -}; - -ValidWaveInfo latest_wave; // 最新有效波段 - -// 连续亏损保护相关变量 -int consecutive_loss_count = 0; // 连续亏损计数器 -datetime freeze_until_time = 0; // 冷冻结束时间(0表示未冷冻) -int freeze_bar_index = 0; // 冷冻起始K线索引 - -// 极值点结构体定义 -struct ExtremePoint { - datetime time; - double price; - int type; - bool is_valid; -}; - -// 补偿队列项结构体定义 -struct CompensationItem { - int ticket; // 订单号 - double loss; // 亏损金额(正数,美元) - double lots; // 手数 - double stopLossAmount;// 止损金额(正数,美元) - int direction; // 方向(1=多单,-1=空单) - datetime closeTime; // 平仓时间 -}; - -// 补偿机制全局变量 -CompensationItem g_compensationQueue[]; // 补偿队列(待处理) -CompensationItem g_currentCompensationSnapshot[];// 当前补偿单对应的队列快照 -int g_currentCompensationTicket = -1; // 当前补偿单ticket(-1表示无补偿单) -int g_consecutiveCompensationCount = 0; // 连续补偿计数器 - -// 函数声明 -void UpdateLatestValidWave(); -void DrawExtremeMarkers(ExtremePoint &extremes[]); -void DrawLatestValidWave(double high_price, datetime high_time, double low_price, datetime low_time); -int CheckBreakout(int index, const MqlRates &rates[], const double &ma[]); -void FilterBreakouts(const int &breakout_bars[], const int &breakout_types[], - const MqlRates &rates[], int &filtered_bars[], int &filtered_types[]); -void CheckOpenSignals(); -bool OpenPosition(ENUM_ORDER_TYPE order_type, double wave_high, double wave_low, double threshold_points); -double CalculateLotSize(); -void ManagePositions(); -void CheckTrailingStop(ulong ticket); -void CheckAndCloseManualOrders(); - -// 补偿机制函数声明 -void AddToCompensationQueue(ulong ticket); -bool OpenCompensationOrder(); -void CheckCompensationOrderStatus(); -void ClearCompensationQueue(); - -//+------------------------------------------------------------------+ -//| Expert initialization function | -//+------------------------------------------------------------------+ -int OnInit() -{ - // 创建MA指标(使用指定的K线周期) - ma_handle = iMA(_Symbol, InpTimeframe, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); - if(ma_handle == INVALID_HANDLE) { - Print("创建MA指标失败"); - return(INIT_FAILED); - } - - // 设置交易参数 - trade.SetExpertMagicNumber(InpMagicNumber); - trade.SetDeviationInPoints(10); - trade.SetTypeFilling(ORDER_FILLING_IOC); - - // 初始化最新有效波段 - latest_wave.exists = false; - latest_wave.high_price = 0; - latest_wave.low_price = 0; - latest_wave.update_time = 0; - latest_wave.high_used = false; - latest_wave.low_used = false; - - Print("========================================"); - Print("突破交易策略EA初始化成功"); - Print("品种:", _Symbol); - Print("K线周期:", EnumToString(InpTimeframe)); - Print("MA周期:", InpMAPeriod); - Print("波段阈值范围: ", InpMinWavePercent, "% - ", InpMaxWavePercent, "%"); - if(InpPullbackTolerance > 0) - Print("反向突破容忍度: ", DoubleToString(InpPullbackTolerance, 1), "% (启用)"); - else - Print("反向突破容忍度: 0% (禁用 - 保持原有逻辑)"); - Print("止损:", InpStopLossPercent, "% (最小", InpMinStopLossPoints, "点) | 盈亏比:", InpRiskRewardRatio); - Print("移动止损:", (InpUseTrailingStop ? "启用" : "禁用")); - Print("最大开仓手数:", InpMaxPositions); - Print("单方向持仓限制:", (InpOnePositionPerDirection ? "启用 (每方向最多1单)" : "禁用")); - if(InpConsecutiveLosses > 0 && InpFreezeBarCount > 0) - Print("连续亏损保护: 启用 (", InpConsecutiveLosses, "次亏损→冷冻", InpFreezeBarCount, "根K线)"); - else - Print("连续亏损保护: 禁用"); - Print("手数模式:", (InpUseCompounding ? "复利" : "固定"), - InpUseCompounding ? StringFormat(" (每500$开%.2f手)", InpLotsPer500) : StringFormat(" (%.2f手)", InpFixedLots)); - Print("禁止手工单:", (InpCloseManualOrders ? "启用 (自动平掉手工单)" : "禁用")); - if(InpEnableCompensation) - Print("补偿机制: 启用 (队列最小长度:", InpMinCompensationQueueSize, - " 最大手数:", InpMaxCompensationLots, - " 止损:", InpCompensationStopLossPips, "点", - " 最大止盈:", InpMaxCompensationTakeProfitPips, "点", - " 连续限制:", InpMaxConsecutiveCompensations, "次)"); - else - Print("补偿机制: 禁用"); - Print("========================================"); - - return(INIT_SUCCEEDED); -} - -//+------------------------------------------------------------------+ -//| Expert deinitialization function | -//+------------------------------------------------------------------+ -void OnDeinit(const int reason) -{ - if(ma_handle != INVALID_HANDLE) - IndicatorRelease(ma_handle); - - // 删除所有标记 - ObjectsDeleteAll(0, "ValidWave_"); - - Print("突破交易策略EA已卸载"); -} - -//+------------------------------------------------------------------+ -//| 交易事务处理函数 - 用于检测亏损单 | -//+------------------------------------------------------------------+ -void OnTradeTransaction(const MqlTradeTransaction& trans, - const MqlTradeRequest& request, - const MqlTradeResult& result) -{ - // 只在功能启用时处理 - if(InpConsecutiveLosses <= 0 || InpFreezeBarCount <= 0) - return; - - // 只处理订单成交事件 - if(trans.type != TRADE_TRANSACTION_DEAL_ADD) - return; - - // 需要先选择历史记录 - if(!HistorySelect(0, TimeCurrent())) - return; - - // 获取最新的Deal - int total_deals = HistoryDealsTotal(); - if(total_deals <= 0) - return; - - ulong deal_ticket = HistoryDealGetTicket(total_deals - 1); - if(deal_ticket == 0) - return; - - // 检查魔术号 - long deal_magic = HistoryDealGetInteger(deal_ticket, DEAL_MAGIC); - if(deal_magic != InpMagicNumber) - return; - - // 检查是否是平仓交易 - ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); - if(entry != DEAL_ENTRY_OUT) - return; - - // 获取交易详情 - double profit = HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); - double commission = HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); - double swap = HistoryDealGetDouble(deal_ticket, DEAL_SWAP); - double net_profit = profit + commission + swap; - - // 获取持仓ID用于补偿机制 - long position_id = HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); - - // 获取平仓原因 - ENUM_DEAL_REASON reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(deal_ticket, DEAL_REASON); - - // 判断是亏损还是盈利(基于是否触发止损) - if(reason == DEAL_REASON_SL) - { - // 检查是否是补偿单(补偿单不计入连续亏损统计) - string comment = HistoryDealGetString(deal_ticket, DEAL_COMMENT); - bool is_compensation = (StringFind(comment, "[补偿单]") >= 0); - - if(!is_compensation) - { - // 只有正常单亏损才增加计数器 - consecutive_loss_count++; - Print("【连续亏损保护】正常单亏损 ", consecutive_loss_count, "/", InpConsecutiveLosses, - " | 净亏损: $", DoubleToString(net_profit, 2)); - - // 检查是否达到冷冻阈值 - if(consecutive_loss_count >= InpConsecutiveLosses) - { - // 进入冷冻期 - freeze_bar_index = Bars(_Symbol, InpTimeframe) + InpFreezeBarCount; - freeze_until_time = TimeCurrent(); - - Print("!!! 触发交易冷冻 !!! 冷冻", InpFreezeBarCount, "根K线"); - } - } - else - { - Print("【补偿机制】补偿单亏损 - 不计入连续亏损统计 | 净亏损: $", DoubleToString(net_profit, 2)); - } - - // 补偿机制:将亏损单加入补偿队列(正常单和补偿单都加入) - if(InpEnableCompensation) - { - AddToCompensationQueue(position_id); - } - } - else if(reason == DEAL_REASON_TP) - { - // 检查是否是补偿单 - string comment = HistoryDealGetString(deal_ticket, DEAL_COMMENT); - bool is_compensation = (StringFind(comment, "[补偿单]") >= 0); - - if(!is_compensation) - { - // 只有正常单盈利才重置计数器 - if(consecutive_loss_count > 0) - { - Print("【连续亏损保护】正常单止盈 - 计数器重置: ", consecutive_loss_count, " → 0 | 净盈利: $", DoubleToString(net_profit, 2)); - consecutive_loss_count = 0; - } - } - else - { - Print("【补偿机制】补偿单止盈 - 不影响连续亏损计数器 | 净盈利: $", DoubleToString(net_profit, 2)); - } - } -} - -//+------------------------------------------------------------------+ -//| Expert tick function | -//+------------------------------------------------------------------+ -void OnTick() -{ - // 1. 检查并关闭手工单(如果启用) - if(InpCloseManualOrders) - CheckAndCloseManualOrders(); - - // 2. 检查补偿单状态(优先处理补偿机制) - if(InpEnableCompensation) - CheckCompensationOrderStatus(); - - // 3. 更新最新有效波段 - UpdateLatestValidWave(); - - // 4. 检查开仓信号 - CheckOpenSignals(); - - // 5. 管理已有持仓 - ManagePositions(); -} - -//+------------------------------------------------------------------+ -//| 更新最新有效波段 | -//+------------------------------------------------------------------+ -void UpdateLatestValidWave() -{ - int bars = Bars(_Symbol, InpTimeframe); - if(bars < InpMAPeriod + 2) - return; - - // 限制处理的K线数量 - int process_bars = MathMin(bars, 500); - - // 获取价格数据(使用指定的K线周期) - MqlRates rates[]; - ArraySetAsSeries(rates, true); - if(CopyRates(_Symbol, InpTimeframe, 0, process_bars, rates) <= 0) - return; - - // 获取MA数据 - double ma_array[]; - ArraySetAsSeries(ma_array, true); - if(CopyBuffer(ma_handle, 0, 0, process_bars, ma_array) <= 0) - return; - - // 识别突破K线 - int breakout_bars[]; - int breakout_types[]; - ArrayResize(breakout_bars, 0); - ArrayResize(breakout_types, 0); - - for(int i = process_bars - InpMAPeriod - 1; i >= 1; i--) { - int breakout_type = CheckBreakout(i, rates, ma_array); - if(breakout_type != 0) { - int size = ArraySize(breakout_bars); - ArrayResize(breakout_bars, size + 1); - ArrayResize(breakout_types, size + 1); - breakout_bars[size] = i; - breakout_types[size] = breakout_type; - } - } - - // 过滤连续同向突破 - int filtered_bars[]; - int filtered_types[]; - FilterBreakouts(breakout_bars, breakout_types, rates, filtered_bars, filtered_types); - - // 计算极值点(支持反向突破容忍度) - ExtremePoint extremes[]; - ArrayResize(extremes, 0); - - // 如果容忍度为0,使用原有逻辑(相邻突破K线之间的极值) - if(InpPullbackTolerance <= 0.0) - { - for(int i = 0; i < ArraySize(filtered_bars) - 1; i++) { - int current_bar = filtered_bars[i]; - int current_type = filtered_types[i]; - int next_bar = filtered_bars[i + 1]; - - double extreme_price = 0; - datetime extreme_time = 0; - - if(current_type == 1) { - extreme_price = rates[current_bar].high; - extreme_time = rates[current_bar].time; - for(int j = current_bar; j >= next_bar; j--) { - if(rates[j].high > extreme_price) { - extreme_price = rates[j].high; - extreme_time = rates[j].time; - } - } - } else { - extreme_price = rates[current_bar].low; - extreme_time = rates[current_bar].time; - for(int j = current_bar; j >= next_bar; j--) { - if(rates[j].low < extreme_price) { - extreme_price = rates[j].low; - extreme_time = rates[j].time; - } - } - } - - int size = ArraySize(extremes); - ArrayResize(extremes, size + 1); - extremes[size].time = extreme_time; - extremes[size].price = extreme_price; - extremes[size].type = current_type; - extremes[size].is_valid = false; - } - } - else - { - // 容忍模式:跨越反向突破K线计算波段 - for(int i = 0; i < ArraySize(filtered_bars); i++) { - int start_bar = filtered_bars[i]; - int start_type = filtered_types[i]; - - double wave_high = rates[start_bar].high; - double wave_low = rates[start_bar].low; - datetime wave_high_time = rates[start_bar].time; - datetime wave_low_time = rates[start_bar].time; - int end_bar = 0; - bool wave_terminated = false; - - // 向后扫描,直到遇到不可容忍的反向突破 - for(int j = i + 1; j < ArraySize(filtered_bars); j++) { - int current_bar = filtered_bars[j]; - int current_type = filtered_types[j]; - - // 更新波段的高低点 - if(rates[current_bar].high > wave_high) { - wave_high = rates[current_bar].high; - wave_high_time = rates[current_bar].time; - } - if(rates[current_bar].low < wave_low) { - wave_low = rates[current_bar].low; - wave_low_time = rates[current_bar].time; - } - - // 检查中间所有K线的极值 - int prev_bar = (j > 0) ? filtered_bars[j-1] : start_bar; - for(int k = prev_bar; k >= current_bar; k--) { - if(rates[k].high > wave_high) { - wave_high = rates[k].high; - wave_high_time = rates[k].time; - } - if(rates[k].low < wave_low) { - wave_low = rates[k].low; - wave_low_time = rates[k].time; - } - } - - // 如果遇到反向突破K线,检查回撤是否可容忍 - if(current_type != start_type) { - double wave_range = wave_high - wave_low; - double pullback_percent = 0; - - if(start_type == 1) { - // 多头波段遇到空单突破K线 - pullback_percent = ((wave_high - rates[current_bar].close) / wave_range) * 100.0; - } else { - // 空头波段遇到多单突破K线 - pullback_percent = ((rates[current_bar].close - wave_low) / wave_range) * 100.0; - } - - if(pullback_percent > InpPullbackTolerance) { - // 回撤超过容忍度,终止波段 - end_bar = current_bar; - wave_terminated = true; - break; - } - // 否则继续,忽略此反向突破 - } - } - - // 添加极值点 - if(start_type == 1) { - // 多头波段:先低点后高点 - int size = ArraySize(extremes); - ArrayResize(extremes, size + 1); - extremes[size].time = wave_low_time; - extremes[size].price = wave_low; - extremes[size].type = -1; // 低点 - extremes[size].is_valid = false; - - ArrayResize(extremes, size + 2); - extremes[size + 1].time = wave_high_time; - extremes[size + 1].price = wave_high; - extremes[size + 1].type = 1; // 高点 - extremes[size + 1].is_valid = false; - } else { - // 空头波段:先高点后低点 - int size = ArraySize(extremes); - ArrayResize(extremes, size + 1); - extremes[size].time = wave_high_time; - extremes[size].price = wave_high; - extremes[size].type = 1; // 高点 - extremes[size].is_valid = false; - - ArrayResize(extremes, size + 2); - extremes[size + 1].time = wave_low_time; - extremes[size + 1].price = wave_low; - extremes[size + 1].type = -1; // 低点 - extremes[size + 1].is_valid = false; - } - - // 如果波段被终止,跳到终止点继续 - if(wave_terminated) { - // 找到end_bar在filtered_bars中的索引 - for(int k = i + 1; k < ArraySize(filtered_bars); k++) { - if(filtered_bars[k] == end_bar) { - i = k - 1; // -1因为循环会++ - break; - } - } - } else { - // 波段延续到最后 - break; - } - } - } - - // 判断有效波段并标记 - for(int i = 1; i < ArraySize(extremes); i++) { - double price_diff = MathAbs(extremes[i].price - extremes[i-1].price); - double price_diff_points = price_diff / _Point; - - // 计算阈值(百分比模式:以前一个极值点价格为基准计算百分比) - double base_price = extremes[i-1].price; - double min_threshold = (base_price * InpMinWavePercent / 100.0) / _Point; - double max_threshold = (base_price * InpMaxWavePercent / 100.0) / _Point; - - // 波段必须在最小和最大阈值之间才是有效波段 - if(price_diff_points >= min_threshold && price_diff_points <= max_threshold) { - extremes[i-1].is_valid = true; - extremes[i].is_valid = true; - } - } - - // 绘制所有极值点标记 - if(InpShowMarkers) { - DrawExtremeMarkers(extremes); - } - - // 查找最新的有效波段 - for(int i = ArraySize(extremes) - 1; i >= 1; i--) { - double price_diff = MathAbs(extremes[i].price - extremes[i-1].price); - double price_diff_points = price_diff / _Point; - - // 计算阈值(百分比模式) - double base_price = extremes[i-1].price; - double min_threshold = (base_price * InpMinWavePercent / 100.0) / _Point; - double max_threshold = (base_price * InpMaxWavePercent / 100.0) / _Point; - - // 波段必须在最小和最大阈值之间才是有效波段 - if(price_diff_points >= min_threshold && price_diff_points <= max_threshold) { - // 找到最新的有效波段 - double high = MathMax(extremes[i].price, extremes[i-1].price); - double low = MathMin(extremes[i].price, extremes[i-1].price); - datetime high_time = (extremes[i].price > extremes[i-1].price) ? extremes[i].time : extremes[i-1].time; - datetime low_time = (extremes[i].price < extremes[i-1].price) ? extremes[i].time : extremes[i-1].time; - - // 检查是否是新的波段 - if(latest_wave.exists == false || - extremes[i].time > latest_wave.update_time || - high != latest_wave.high_price || - low != latest_wave.low_price) { - - latest_wave.exists = true; - latest_wave.high_price = high; - latest_wave.low_price = low; - latest_wave.update_time = extremes[i].time; - latest_wave.high_used = false; // 新波段,重置使用状态 - latest_wave.low_used = false; - - // 绘制最新有效波段 - if(InpShowMarkers) { - DrawLatestValidWave(high, high_time, low, low_time); - } - - if(InpShowDebugInfo) { - Print("更新最新有效波段 - 高:", DoubleToString(high, _Digits), - " 低:", DoubleToString(low, _Digits), - " 价差:", (int)price_diff_points, "点", - " 阈值范围:", StringFormat("%.2f%%-%.2f%% (%.0f-%.0f点)", - InpMinWavePercent, InpMaxWavePercent, min_threshold, max_threshold)); - } - } - break; - } - } -} - -//+------------------------------------------------------------------+ -//| 绘制所有极值点标记 | -//+------------------------------------------------------------------+ -void DrawExtremeMarkers(ExtremePoint &extremes[]) -{ - // 删除旧的标记 - ObjectsDeleteAll(0, "ValidWave_Extreme_"); - - for(int i = 0; i < ArraySize(extremes); i++) { - string obj_name = "ValidWave_Extreme_" + IntegerToString(i); - - if(extremes[i].type == 1) { - // 高点 - 画下箭头 - ObjectCreate(0, obj_name, OBJ_ARROW, 0, extremes[i].time, extremes[i].price); - ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 234); - ObjectSetInteger(0, obj_name, OBJPROP_COLOR, extremes[i].is_valid ? clrRed : clrDarkRed); - ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, extremes[i].is_valid ? 3 : 1); - ObjectSetInteger(0, obj_name, OBJPROP_ANCHOR, ANCHOR_BOTTOM); - } else { - // 低点 - 画上箭头 - ObjectCreate(0, obj_name, OBJ_ARROW, 0, extremes[i].time, extremes[i].price); - ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 233); - ObjectSetInteger(0, obj_name, OBJPROP_COLOR, extremes[i].is_valid ? clrLime : clrDarkGreen); - ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, extremes[i].is_valid ? 3 : 1); - ObjectSetInteger(0, obj_name, OBJPROP_ANCHOR, ANCHOR_TOP); - } - } -} - -//+------------------------------------------------------------------+ -//| 绘制最新有效波段 | -//+------------------------------------------------------------------+ -void DrawLatestValidWave(double high_price, datetime high_time, double low_price, datetime low_time) -{ - // 删除旧的最新波段标记 - ObjectDelete(0, "ValidWave_Latest_High"); - ObjectDelete(0, "ValidWave_Latest_Low"); - ObjectDelete(0, "ValidWave_Latest_Line"); - - // 标记最新有效波段的高点(更大更亮的箭头) - ObjectCreate(0, "ValidWave_Latest_High", OBJ_ARROW, 0, high_time, high_price); - ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_ARROWCODE, 234); - ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_COLOR, clrYellow); - ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_WIDTH, 4); - ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_ANCHOR, ANCHOR_BOTTOM); - - // 标记最新有效波段的低点 - ObjectCreate(0, "ValidWave_Latest_Low", OBJ_ARROW, 0, low_time, low_price); - ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_ARROWCODE, 233); - ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_COLOR, clrYellow); - ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_WIDTH, 4); - ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_ANCHOR, ANCHOR_TOP); - - // 绘制连接线 - ObjectCreate(0, "ValidWave_Latest_Line", OBJ_TREND, 0, high_time, high_price, low_time, low_price); - ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_COLOR, clrYellow); - ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_WIDTH, 2); - ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_STYLE, STYLE_SOLID); - ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_RAY_RIGHT, false); - ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_BACK, true); -} - -//+------------------------------------------------------------------+ -//| 检查是否是突破K线 | -//+------------------------------------------------------------------+ -int CheckBreakout(int index, const MqlRates &rates[], const double &ma[]) -{ - if(rates[index].open < ma[index] && rates[index].close > ma[index]) - return 1; - if(rates[index].open > ma[index] && rates[index].close < ma[index]) - return -1; - return 0; -} - -//+------------------------------------------------------------------+ -//| 过滤连续同向突破 | -//+------------------------------------------------------------------+ -void FilterBreakouts(const int &breakout_bars[], const int &breakout_types[], - const MqlRates &rates[], int &filtered_bars[], int &filtered_types[]) -{ - int total = ArraySize(breakout_bars); - ArrayResize(filtered_bars, 0); - ArrayResize(filtered_types, 0); - - for(int i = 0; i < total; i++) { - int current_bar = breakout_bars[i]; - int current_type = breakout_types[i]; - - bool skip = false; - for(int j = i + 1; j < total; j++) { - if(breakout_types[j] != current_type) - break; - - if(current_type == 1) { - if(rates[breakout_bars[j]].low < rates[current_bar].low) { - skip = true; - break; - } - } else { - if(rates[breakout_bars[j]].high > rates[current_bar].high) { - skip = true; - break; - } - } - } - - if(!skip) { - int size = ArraySize(filtered_bars); - ArrayResize(filtered_bars, size + 1); - ArrayResize(filtered_types, size + 1); - filtered_bars[size] = current_bar; - filtered_types[size] = current_type; - } - } -} - -//+------------------------------------------------------------------+ -//| 检查开仓信号 | -//+------------------------------------------------------------------+ -void CheckOpenSignals() -{ - if(!latest_wave.exists) - return; - - // 检查是否处于冷冻期 - if(InpConsecutiveLosses > 0 && InpFreezeBarCount > 0 && freeze_bar_index > 0) - { - int current_bars = Bars(_Symbol, InpTimeframe); - if(current_bars < freeze_bar_index) - { - // 仍在冷冻期内 - 静默拒绝开仓 - return; - } - else - { - // 冷冻期结束 - if(freeze_bar_index > 0) - { - Print("【连续亏损保护】冷冻解除,恢复交易"); - consecutive_loss_count = 0; - freeze_bar_index = 0; - freeze_until_time = 0; - } - } - } - - // 检查当前持仓数量和方向 - int total_positions = 0; - int buy_positions = 0; // 多单数量 - int sell_positions = 0; // 空单数量 - - for(int i = PositionsTotal() - 1; i >= 0; i--) { - if(!PositionSelectByTicket(PositionGetTicket(i))) - continue; - if(PositionGetString(POSITION_SYMBOL) != _Symbol) - continue; - if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) - continue; - - total_positions++; - - // 统计各方向持仓数量 - ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); - if(pos_type == POSITION_TYPE_BUY) - buy_positions++; - else if(pos_type == POSITION_TYPE_SELL) - sell_positions++; - } - - if(total_positions >= InpMaxPositions) { - if(InpShowDebugInfo) - Print("已达最大持仓数量限制: ", total_positions, "/", InpMaxPositions); - return; - } - - double current_price = SymbolInfoDouble(_Symbol, SYMBOL_BID); - - // 计算当前波段的实际阈值(百分比模式:取高低点的平均值作为基准) - double base_price = (latest_wave.high_price + latest_wave.low_price) / 2.0; - double wave_threshold_points = (base_price * InpMinWavePercent / 100.0) / _Point; - - // 检查多单信号:突破高点(且该高点未使用过) - if(!latest_wave.high_used && current_price > latest_wave.high_price) { - // 如果启用了单方向持仓限制,检查多单数量 - if(InpOnePositionPerDirection && buy_positions >= 1) { - if(InpShowDebugInfo) - Print("多单信号被忽略 - 已有多单持仓: ", buy_positions); - } else { - if(InpShowDebugInfo) - Print("检测到多单信号 - 价格:", current_price, " 突破高点:", latest_wave.high_price); - - if(OpenPosition(ORDER_TYPE_BUY, latest_wave.high_price, latest_wave.low_price, wave_threshold_points)) { - latest_wave.high_used = true; // 标记高点已使用,该波段高点失效 - if(InpShowDebugInfo) - Print("高点已使用,等待新的有效波段"); - } - } - } - - // 检查空单信号:突破低点(且该低点未使用过) - if(!latest_wave.low_used && current_price < latest_wave.low_price) { - // 如果启用了单方向持仓限制,检查空单数量 - if(InpOnePositionPerDirection && sell_positions >= 1) { - if(InpShowDebugInfo) - Print("空单信号被忽略 - 已有空单持仓: ", sell_positions); - } else { - if(InpShowDebugInfo) - Print("检测到空单信号 - 价格:", current_price, " 突破低点:", latest_wave.low_price); - - if(OpenPosition(ORDER_TYPE_SELL, latest_wave.high_price, latest_wave.low_price, wave_threshold_points)) { - latest_wave.low_used = true; // 标记低点已使用,该波段低点失效 - if(InpShowDebugInfo) - Print("低点已使用,等待新的有效波段"); - } - } - } -} - -//+------------------------------------------------------------------+ -//| 开仓 | -//+------------------------------------------------------------------+ -bool OpenPosition(ENUM_ORDER_TYPE order_type, double wave_high, double wave_low, double threshold_points) -{ - double lots = CalculateLotSize(); - if(lots <= 0) - return false; - - // 生成备注信息:盈亏比(止损点数会在设置SL后从历史记录计算) - string comment = StringFormat("R%.1f", InpRiskRewardRatio); - - bool result = false; - - // 先以市价开仓(不带SL/TP),成交后再根据实际成交价设置SL/TP - if(order_type == ORDER_TYPE_BUY) { - result = trade.Buy(lots, _Symbol, 0, 0, 0, comment); - } else { - result = trade.Sell(lots, _Symbol, 0, 0, 0, comment); - } - - if(!result) { - Print("开仓失败: ", trade.ResultRetcode(), " - ", trade.ResultRetcodeDescription()); - return false; - } - - // 等待持仓信息更新 - Sleep(100); - - // 通过符号和魔术号查找刚开的持仓 - ulong ticket = 0; - double open_price = 0; - - for(int i = PositionsTotal() - 1; i >= 0; i--) { - if(!PositionSelectByTicket(PositionGetTicket(i))) - continue; - - if(PositionGetString(POSITION_SYMBOL) != _Symbol) - continue; - - if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) - continue; - - // 找到最新的持仓(没有SL/TP的) - if(PositionGetDouble(POSITION_SL) == 0 && PositionGetDouble(POSITION_TP) == 0) { - ticket = PositionGetTicket(i); - open_price = PositionGetDouble(POSITION_PRICE_OPEN); - break; - } - } - - if(ticket == 0) { - Print("无法找到刚开的持仓"); - return false; - } - - // 基于实际成交价计算止损金额 - double stop_loss_amount = open_price * InpStopLossPercent / 100.0; - - // 确保止损金额不小于最小止损点数 - double min_stop_loss = InpMinStopLossPoints * _Point; - if(stop_loss_amount < min_stop_loss) { - stop_loss_amount = min_stop_loss; - } - - // 获取平台的最小止损距离 - int stops_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); - double min_stop_distance = stops_level * _Point; - - // 获取当前市场价格 - double current_bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); - double current_ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); - - double sl = 0, tp = 0; - - if(order_type == ORDER_TYPE_BUY) { - // 先标准化止损价 - sl = NormalizeDouble(open_price - stop_loss_amount, _Digits); - // 根据标准化后的实际止损金额重新计算止盈金额,确保盈亏比准确 - double actual_sl_amount = open_price - sl; - double take_profit_amount = actual_sl_amount * InpRiskRewardRatio; - tp = NormalizeDouble(open_price + take_profit_amount, _Digits); - - // 检查止损距离(多单检查与Bid的距离) - if(stops_level > 0 && (current_bid - sl) < min_stop_distance) { - sl = NormalizeDouble(current_bid - min_stop_distance, _Digits); - // 重新计算止盈 - actual_sl_amount = open_price - sl; - take_profit_amount = actual_sl_amount * InpRiskRewardRatio; - tp = NormalizeDouble(open_price + take_profit_amount, _Digits); - Print("止损距离不足,已调整 - 新止损:", sl); - } - - // 检查止盈距离 - if(stops_level > 0 && (tp - current_ask) < min_stop_distance) { - tp = NormalizeDouble(current_ask + min_stop_distance, _Digits); - Print("止盈距离不足,已调整 - 新止盈:", tp); - } - } else { - // 先标准化止损价 - sl = NormalizeDouble(open_price + stop_loss_amount, _Digits); - // 根据标准化后的实际止损金额重新计算止盈金额,确保盈亏比准确 - double actual_sl_amount = sl - open_price; - double take_profit_amount = actual_sl_amount * InpRiskRewardRatio; - tp = NormalizeDouble(open_price - take_profit_amount, _Digits); - - // 检查止损距离(空单检查与Ask的距离) - if(stops_level > 0 && (sl - current_ask) < min_stop_distance) { - sl = NormalizeDouble(current_ask + min_stop_distance, _Digits); - // 重新计算止盈 - actual_sl_amount = sl - open_price; - take_profit_amount = actual_sl_amount * InpRiskRewardRatio; - tp = NormalizeDouble(open_price - take_profit_amount, _Digits); - Print("止损距离不足,已调整 - 新止损:", sl); - } - - // 检查止盈距离 - if(stops_level > 0 && (current_bid - tp) < min_stop_distance) { - tp = NormalizeDouble(current_bid - min_stop_distance, _Digits); - Print("止盈距离不足,已调整 - 新止盈:", tp); - } - } - - // 修改持仓的SL/TP - if(!trade.PositionModify(ticket, sl, tp)) { - Print("修改SL/TP失败 - Ticket:", ticket, - " 错误码:", trade.ResultRetcode(), - " 描述:", trade.ResultRetcodeDescription(), - " 止损:", sl, " 止盈:", tp, - " 平台最小距离:", stops_level, "点"); - return false; - } - - Print(order_type == ORDER_TYPE_BUY ? "开多单成功" : "开空单成功", - " - 手数:", lots, - " 波段:H:", wave_high, " L:", wave_low, - " 实际成交价:", open_price, - " 止损:", sl, "(", InpStopLossPercent, "%)", - " 止盈:", tp, "(盈亏比", InpRiskRewardRatio, ")"); - - return true; -} - -//+------------------------------------------------------------------+ -//| 计算开仓手数 | -//+------------------------------------------------------------------+ -double CalculateLotSize() -{ - double lots = 0; - - if(InpUseCompounding) { - // 复利模式 - double balance = AccountInfoDouble(ACCOUNT_BALANCE); - lots = NormalizeDouble((balance / 500.0) * InpLotsPer500, 2); - } else { - // 固定手数模式 - lots = InpFixedLots; - } - - // 检查手数限制 - double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); - double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); - double lot_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); - - if(lots < min_lot) lots = min_lot; - if(lots > max_lot) lots = max_lot; - - lots = MathFloor(lots / lot_step) * lot_step; - - return lots; -} - -//+------------------------------------------------------------------+ -//| 管理持仓 | -//+------------------------------------------------------------------+ -void ManagePositions() -{ - for(int i = PositionsTotal() - 1; i >= 0; i--) { - if(!PositionSelectByTicket(PositionGetTicket(i))) - continue; - - if(PositionGetString(POSITION_SYMBOL) != _Symbol) - continue; - - if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) - continue; - - // 检查移动止损 - ulong ticket = PositionGetTicket(i); - CheckTrailingStop(ticket); - } -} - -//+------------------------------------------------------------------+ -//| 检查移动止损 | -//+------------------------------------------------------------------+ -void CheckTrailingStop(ulong ticket) -{ - // 如果未开启移动止损,直接返回 - if(!InpUseTrailingStop) - return; - - if(!PositionSelectByTicket(ticket)) - return; - - // 补偿单不使用移动止损(需要达到完整止盈目标) - string comment = PositionGetString(POSITION_COMMENT); - if(StringFind(comment, "[补偿单]") >= 0) - return; - - double open_price = PositionGetDouble(POSITION_PRICE_OPEN); - double current_sl = PositionGetDouble(POSITION_SL); - ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); - double current_price = (pos_type == POSITION_TYPE_BUY) ? - SymbolInfoDouble(_Symbol, SYMBOL_BID) : - SymbolInfoDouble(_Symbol, SYMBOL_ASK); - - // 计算止损金额(开仓价的百分比) - double stop_loss_amount = open_price * InpStopLossPercent / 100.0; - - // 确保止损金额不小于最小止损点数 - double min_stop_loss = InpMinStopLossPoints * _Point; - if(stop_loss_amount < min_stop_loss) { - stop_loss_amount = min_stop_loss; - } - - // 计算浮盈 - double profit_amount = 0; - if(pos_type == POSITION_TYPE_BUY) { - profit_amount = current_price - open_price; - } else { - profit_amount = open_price - current_price; - } - - // 浮盈达到止损金额,移动止损至成本价 - if(profit_amount >= stop_loss_amount) { - double new_sl = open_price; - - // 检查是否需要更新 - bool need_update = false; - if(pos_type == POSITION_TYPE_BUY && (current_sl < new_sl || current_sl == 0)) { - need_update = true; - } else if(pos_type == POSITION_TYPE_SELL && (current_sl > new_sl || current_sl == 0)) { - need_update = true; - } - - if(need_update) { - double tp = PositionGetDouble(POSITION_TP); - if(trade.PositionModify(ticket, new_sl, tp)) { - Print("移动止损至成本价 - Ticket:", ticket, " 新止损:", new_sl); - } - } - } -} - -//+------------------------------------------------------------------+ -//| 检查并关闭手工单 | -//+------------------------------------------------------------------+ -void CheckAndCloseManualOrders() -{ - for(int i = PositionsTotal() - 1; i >= 0; i--) { - if(!PositionSelectByTicket(PositionGetTicket(i))) - continue; - - // 只处理本品种 - if(PositionGetString(POSITION_SYMBOL) != _Symbol) - continue; - - // 检查magic number:0表示手工单 - long magic = PositionGetInteger(POSITION_MAGIC); - if(magic == 0) { - ulong ticket = PositionGetTicket(i); - - // 平仓手工单 - if(trade.PositionClose(ticket)) { - Print("【禁止手工单】已平掉手工单 - Ticket:", ticket, - " 类型:", (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? "多单" : "空单")); - } else { - Print("【禁止手工单】平仓失败 - Ticket:", ticket, " 错误:", GetLastError()); - } - } - } -} - -//+------------------------------------------------------------------+ -//| 将亏损单加入补偿队列 | -//+------------------------------------------------------------------+ -void AddToCompensationQueue(ulong ticket) -{ - if(!InpEnableCompensation) - return; - - // 需要选择历史记录才能获取已平仓订单的信息 - if(!HistorySelectByPosition(ticket)) - return; - - // 查找该持仓的平仓deal - int total_deals = HistoryDealsTotal(); - if(total_deals <= 0) - return; - - // 从最新的deal开始找 - for(int i = total_deals - 1; i >= 0; i--) - { - ulong deal_ticket = HistoryDealGetTicket(i); - if(deal_ticket == 0) - continue; - - // 检查是否是该持仓的平仓交易 - long deal_position = HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); - if(deal_position != (long)ticket) - continue; - - // 检查是否是平仓 - ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); - if(entry != DEAL_ENTRY_OUT) - continue; - - // 获取盈亏信息 - double profit = HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); - double commission = HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); - double swap = HistoryDealGetDouble(deal_ticket, DEAL_SWAP); - double net_profit = profit + commission + swap; - - // 获取平仓原因 - ENUM_DEAL_REASON reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(deal_ticket, DEAL_REASON); - - // 只处理触发止损的单子 - if(reason != DEAL_REASON_SL) - return; - - // 获取订单信息 - double lots = HistoryDealGetDouble(deal_ticket, DEAL_VOLUME); - long deal_type = HistoryDealGetInteger(deal_ticket, DEAL_TYPE); - int direction = (deal_type == DEAL_TYPE_BUY) ? 1 : -1; - - // 从历史记录中查找开仓和平仓价格,计算实际止损金额 - double open_price = 0; - double close_price = 0; - - // 查找该持仓的所有deal - int total_deals_temp = HistoryDealsTotal(); - for(int k = 0; k < total_deals_temp; k++) - { - ulong temp_ticket = HistoryDealGetTicket(k); - if(HistoryDealGetInteger(temp_ticket, DEAL_POSITION_ID) == (long)ticket) - { - ENUM_DEAL_ENTRY temp_entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(temp_ticket, DEAL_ENTRY); - if(temp_entry == DEAL_ENTRY_IN) - { - open_price = HistoryDealGetDouble(temp_ticket, DEAL_PRICE); - } - else if(temp_entry == DEAL_ENTRY_OUT) - { - close_price = HistoryDealGetDouble(temp_ticket, DEAL_PRICE); - } - } - } - - // 计算实际止损金额 = |平仓价 - 开仓价|(亏损单的止损距离) - double stop_loss_amount = MathAbs(close_price - open_price); - - // 加入补偿队列 - int size = ArraySize(g_compensationQueue); - ArrayResize(g_compensationQueue, size + 1); - - g_compensationQueue[size].ticket = (int)ticket; - g_compensationQueue[size].loss = MathAbs(net_profit); - g_compensationQueue[size].lots = lots; - g_compensationQueue[size].stopLossAmount = stop_loss_amount; - g_compensationQueue[size].direction = direction; - g_compensationQueue[size].closeTime = TimeCurrent(); - - Print("【补偿机制】亏损单加入队列 - Ticket:", ticket, - " 亏损:$", DoubleToString(MathAbs(net_profit), 2), - " 手数:", lots, - " 止损金额:$", DoubleToString(stop_loss_amount, 2), - " (", (int)(stop_loss_amount / _Point), "点)", - " 方向:", (direction == 1 ? "多单" : "空单"), - " 开仓:", open_price, " 平仓:", close_price, - " 队列大小:", ArraySize(g_compensationQueue)); - - break; - } -} - -//+------------------------------------------------------------------+ -//| 开立补偿单 | -//+------------------------------------------------------------------+ -bool OpenCompensationOrder() -{ - if(!InpEnableCompensation) - return false; - - // 检查补偿队列长度是否达到最小要求 - int queue_size = ArraySize(g_compensationQueue); - if(queue_size < InpMinCompensationQueueSize) - { - if(queue_size > 0 && InpShowDebugInfo) - Print("【补偿机制】队列长度不足 (", queue_size, "/", InpMinCompensationQueueSize, ") - 暂不开补偿单"); - return false; - } - - // 检查连续补偿次数是否超限 - if(InpMaxConsecutiveCompensations > 0 && g_consecutiveCompensationCount >= InpMaxConsecutiveCompensations) - { - Print("【补偿机制】连续补偿次数达到限制 (", g_consecutiveCompensationCount, "/", InpMaxConsecutiveCompensations, ")"); - Print("【补偿机制】重置补偿队列和计数器,停止补偿"); - - // 清空队列和快照 - ArrayResize(g_compensationQueue, 0); - ArrayResize(g_currentCompensationSnapshot, 0); - - // 重置计数器 - g_consecutiveCompensationCount = 0; - g_currentCompensationTicket = -1; - - return false; - } - - // 检查是否已有补偿单在持仓(双重检查机制) - // 如果有补偿单,则不能开新单(队列累积) - if(g_currentCompensationTicket >= 0) - { - // 检查补偿单是否还在持仓中 - if(PositionSelectByTicket(g_currentCompensationTicket)) - return false; // 还在持仓,不开新单 - else - g_currentCompensationTicket = -1; // 已平仓,重置 - } - - // 二次检查:遍历所有持仓,确保没有其他补偿单 - for(int i = PositionsTotal() - 1; i >= 0; i--) - { - if(!PositionSelectByTicket(PositionGetTicket(i))) - continue; - - if(PositionGetString(POSITION_SYMBOL) != _Symbol) - continue; - - if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) - continue; - - // 检查是否是补偿单 - string pos_comment = PositionGetString(POSITION_COMMENT); - if(StringFind(pos_comment, "[补偿单]") >= 0) - { - Print("【补偿机制】检测到已有补偿单在持仓 - Ticket:", PositionGetTicket(i), " 暂不开新单"); - g_currentCompensationTicket = (int)PositionGetTicket(i); // 同步ticket - return false; // 已有补偿单,拒绝开新单 - } - } - - // 保存当前队列快照(这些亏损单将由本次补偿单负责) - int snapshot_size = ArraySize(g_compensationQueue); - ArrayResize(g_currentCompensationSnapshot, snapshot_size); - for(int i = 0; i < snapshot_size; i++) - { - g_currentCompensationSnapshot[i] = g_compensationQueue[i]; - } - - // 清空待处理队列(快照已保存,新的亏损会加入队列等待下一次补偿) - ArrayResize(g_compensationQueue, 0); - - // 计算补偿单参数(基于快照,此时 g_compensationQueue 已清空) - double lots = 0; - double sl_pips = 0; - double tp_price = 0; - int direction = 0; - - // 计算补偿单参数(基于快照) - double total_loss = 0; - double total_lots = 0; - - for(int i = 0; i < snapshot_size; i++) - { - total_loss += g_currentCompensationSnapshot[i].loss; - total_lots += g_currentCompensationSnapshot[i].lots; - } - - // 补偿单手数 = min(队列总手数, 最大手数限制) - lots = MathMin(total_lots, InpMaxCompensationLots); - - // 补偿单止损点数:使用固定参数 - sl_pips = InpCompensationStopLossPips; - - // 补偿单方向:最近一个亏损单的反方向 - direction = -g_currentCompensationSnapshot[snapshot_size - 1].direction; - - Print("【补偿机制】计算补偿单参数 - 快照大小:", snapshot_size, - " 总手数:", total_lots, - " 补偿手数:", lots, (lots < total_lots ? " (受限于最大手数)" : ""), - " 总亏损:$", DoubleToString(total_loss, 2)); - - if(lots <= 0 || direction == 0) - return false; - - Print("【补偿机制】队列快照已保存 - 快照大小:", snapshot_size, " 待处理队列已清空"); - - // 开仓 - ENUM_ORDER_TYPE order_type = (direction == 1) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; - string comment = StringFormat("[补偿单] Q%d", snapshot_size); - - bool result = false; - if(order_type == ORDER_TYPE_BUY) { - result = trade.Buy(lots, _Symbol, 0, 0, 0, comment); - } else { - result = trade.Sell(lots, _Symbol, 0, 0, 0, comment); - } - - if(!result) { - Print("【补偿机制】开补偿单失败: ", trade.ResultRetcode(), " - ", trade.ResultRetcodeDescription()); - return false; - } - - // 等待持仓更新 - Sleep(100); - - // 查找刚开的补偿单 - ulong ticket = 0; - double open_price = 0; - - for(int i = PositionsTotal() - 1; i >= 0; i--) { - if(!PositionSelectByTicket(PositionGetTicket(i))) - continue; - - if(PositionGetString(POSITION_SYMBOL) != _Symbol) - continue; - - if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) - continue; - - // 查找备注包含"[补偿单]"的持仓 - string pos_comment = PositionGetString(POSITION_COMMENT); - if(StringFind(pos_comment, "[补偿单]") >= 0 && - PositionGetDouble(POSITION_SL) == 0 && - PositionGetDouble(POSITION_TP) == 0) { - ticket = PositionGetTicket(i); - open_price = PositionGetDouble(POSITION_PRICE_OPEN); - break; - } - } - - if(ticket == 0) { - Print("【补偿机制】找不到刚开的补偿单"); - return false; - } - - // 计算止损止盈价格(基于实际成交价和固定参数) - // 止损金额 = 固定止损点数 × 点值 - double sl_amount = InpCompensationStopLossPips * _Point; - - // 计算止盈点数:需要盈利 = 快照队列总亏损 - double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); - double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); - double point_value = tick_value / tick_size * _Point; - double tp_pips_needed = total_loss / (lots * point_value); - - // 限制止盈点数不超过最大值 - bool is_capped = false; - if(tp_pips_needed > InpMaxCompensationTakeProfitPips) { - is_capped = true; - tp_pips_needed = InpMaxCompensationTakeProfitPips; - } - - Print("【补偿机制】止盈计算 - 总亏损:$", DoubleToString(total_loss, 2), - " 手数:", lots, - " Tick价值:$", tick_value, - " Tick大小:", tick_size, - " Point:", _Point, - " 每点价值:$", DoubleToString(point_value, 4), - " 需要止盈:", (int)tp_pips_needed, "点", - is_capped ? " (受限于最大止盈)" : ""); - - double sl = 0; - double tp = 0; - - if(order_type == ORDER_TYPE_BUY) { - sl = NormalizeDouble(open_price - sl_amount, _Digits); - tp = NormalizeDouble(open_price + tp_pips_needed * _Point, _Digits); - } else { - sl = NormalizeDouble(open_price + sl_amount, _Digits); - tp = NormalizeDouble(open_price - tp_pips_needed * _Point, _Digits); - } - - // 获取平台最小止损距离 - int stops_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); - double min_stop_distance = stops_level * _Point; - double current_bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); - double current_ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); - - // 检查并调整止损距离 - if(order_type == ORDER_TYPE_BUY) { - if(stops_level > 0 && (current_bid - sl) < min_stop_distance) { - sl = NormalizeDouble(current_bid - min_stop_distance, _Digits); - Print("【补偿机制】止损距离不足,已调整 - 新止损:", sl); - } - if(stops_level > 0 && (tp - current_ask) < min_stop_distance) { - tp = NormalizeDouble(current_ask + min_stop_distance, _Digits); - Print("【补偿机制】止盈距离不足,已调整 - 新止盈:", tp); - } - } else { - if(stops_level > 0 && (sl - current_ask) < min_stop_distance) { - sl = NormalizeDouble(current_ask + min_stop_distance, _Digits); - Print("【补偿机制】止损距离不足,已调整 - 新止损:", sl); - } - if(stops_level > 0 && (current_bid - tp) < min_stop_distance) { - tp = NormalizeDouble(current_bid - min_stop_distance, _Digits); - Print("【补偿机制】止盈距离不足,已调整 - 新止盈:", tp); - } - } - - // 修改止损止盈 - if(!trade.PositionModify(ticket, sl, tp)) { - Print("【补偿机制】修改SL/TP失败 - Ticket:", ticket, - " 错误码:", trade.ResultRetcode(), - " 描述:", trade.ResultRetcodeDescription(), - " 开仓价:", open_price, - " 止损:", sl, - " 止盈:", tp, - " 平台最小距离:", stops_level, "点"); - return false; - } - - // 记录当前补偿单 - g_currentCompensationTicket = (int)ticket; - - // 增加连续补偿计数器 - g_consecutiveCompensationCount++; - - Print("【补偿机制】开补偿单成功 - Ticket:", ticket, - " 类型:", (order_type == ORDER_TYPE_BUY ? "多单" : "空单"), - " 手数:", lots, - " 开仓价:", open_price, - " 止损:", sl, " (", InpCompensationStopLossPips, "点)", - " 止盈:", tp, " (", (int)tp_pips_needed, "点)", - " 需补偿:$", DoubleToString(total_loss, 2), - " 连续补偿:", g_consecutiveCompensationCount, "/", InpMaxConsecutiveCompensations); - - return true; -} - -//+------------------------------------------------------------------+ -//| 检查补偿单状态 | -//+------------------------------------------------------------------+ -void CheckCompensationOrderStatus() -{ - if(!InpEnableCompensation) - return; - - // 如果没有补偿单在追踪,尝试开单 - if(g_currentCompensationTicket < 0) - { - // OpenCompensationOrder 内部会检查队列长度是否达到最小要求 - OpenCompensationOrder(); - return; - } - - // 检查补偿单是否还在持仓 - if(PositionSelectByTicket(g_currentCompensationTicket)) - return; // 还在持仓 - - // 补偿单已平仓,检查盈亏 - if(!HistorySelectByPosition(g_currentCompensationTicket)) - { - g_currentCompensationTicket = -1; - return; - } - - // 查找平仓deal - int total_deals = HistoryDealsTotal(); - for(int i = total_deals - 1; i >= 0; i--) - { - ulong deal_ticket = HistoryDealGetTicket(i); - if(deal_ticket == 0) - continue; - - long deal_position = HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); - if(deal_position != g_currentCompensationTicket) - continue; - - ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); - if(entry != DEAL_ENTRY_OUT) - continue; - - // 获取盈亏 - double profit = HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); - double commission = HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); - double swap = HistoryDealGetDouble(deal_ticket, DEAL_SWAP); - double net_profit = profit + commission + swap; - - // 获取平仓原因 - ENUM_DEAL_REASON reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(deal_ticket, DEAL_REASON); - - if(reason == DEAL_REASON_TP) - { - // 补偿单止盈:快照队列成功补偿,清空快照,重置计数器 - Print("【补偿机制】补偿单止盈 - 盈利:$", DoubleToString(net_profit, 2), - " 快照队列已补偿完成 (", ArraySize(g_currentCompensationSnapshot), "单)", - " 待处理队列:", ArraySize(g_compensationQueue), "单"); - ArrayResize(g_currentCompensationSnapshot, 0); - - // 止盈成功,重置连续补偿计数器 - Print("【补偿机制】补偿成功,重置连续补偿计数器: ", g_consecutiveCompensationCount, " → 0"); - g_consecutiveCompensationCount = 0; - } - else if(reason == DEAL_REASON_SL) - { - // 补偿单止损:快照队列需要重新补偿 + 补偿单本身也亏损了 - Print("【补偿机制】补偿单止损 - 亏损:$", DoubleToString(MathAbs(net_profit), 2), - " 快照队列 (", ArraySize(g_currentCompensationSnapshot), "单) 将重新加入待处理队列"); - - // 1. 先把快照队列重新加入待处理队列 - int current_queue_size = ArraySize(g_compensationQueue); - int snapshot_size = ArraySize(g_currentCompensationSnapshot); - ArrayResize(g_compensationQueue, current_queue_size + snapshot_size); - - for(int j = 0; j < snapshot_size; j++) - { - g_compensationQueue[current_queue_size + j] = g_currentCompensationSnapshot[j]; - } - - // 2. 补偿单本身的亏损也加入队列 - AddToCompensationQueue(g_currentCompensationTicket); - - // 3. 清空快照 - ArrayResize(g_currentCompensationSnapshot, 0); - - Print("【补偿机制】待处理队列更新 - 当前大小:", ArraySize(g_compensationQueue), "单"); - } - - g_currentCompensationTicket = -1; - break; - } -} - -//+------------------------------------------------------------------+ -//| 清空补偿队列 | -//+------------------------------------------------------------------+ -void ClearCompensationQueue() -{ - ArrayResize(g_compensationQueue, 0); - Print("【补偿机制】补偿队列已清空"); -} diff --git a/20260519-突破策略/README.md b/20260519-突破策略/README.md deleted file mode 100644 index e18d257..0000000 --- a/20260519-突破策略/README.md +++ /dev/null @@ -1,400 +0,0 @@ -# 突破交易策略 - -## 基础概念 -- 平台:MQL5 -- 品种:XAU黄金 -- 1美金:4100与4101之间相差1美金 -- 1美金 = 100点 - -## 辅助指标 -- 移动平均线(Moving Average) -- 周期默认设置为:14 - -## 核心概念 -- 突破K线:开盘价在均线下方,收盘价在均线上方,称之为多单突破K线,反之亦然 -- 极值点:相邻两根突破K线之间所有K线(包括突破K线)中的最高价或最低价称之为极值点 -- 有效波段:相邻两个极值点的价差达到波段阈值点数时,期间的所有K线(包含突破K线本身)组成的行情,称之为有效波段 - -## 开仓逻辑 -- 价格突破有效波段的高点开多单,价格突破有效波点的低点开空单 -- 该开仓逻辑仅适用最新的一个有效波段 -- 止损和止盈基于开仓价的百分比计算,止盈 = 止损金额 × 盈亏比 -- 浮盈达到止损金额后,即可将止损移动至成本价 -- 已经开仓后,继续监控新的有效波段突破,每单开仓后就与有效波段无关了 - -## 参数设置 - -### 波段识别参数 -- K线周期:支持参数设置,不跟随图表,默认 **1 Minute** -- MA周期:默认 **14** -- **最小波段阈值百分比:默认 0.1%**(波段必须 >= 此值才可能是有效波段) -- **最大波段阈值百分比:默认 1.0%**(波段必须 <= 此值才可能是有效波段) -- **有效波段范围:0.1% - 1.0%**(只交易此范围内的波段) -- **反向突破容忍度:默认0.0%(新功能 - 详见下方说明)** - -### 风险管理参数 -- 止损百分比:默认 **0.05%**(基于开仓价计算,最小1美元/100点) -- 盈亏比:默认 **1.5**(止盈 = 止损金额 × 盈亏比) -- 使用移动止损:默认 **false**(禁用移动止损功能) -- 最大持仓时间:默认 **5分钟** - -### 仓位管理参数 -- 使用复利模式:默认 **true**(启用复利模式) -- 固定手数:默认 **0.01手** -- 每500$开仓手数:默认 **0.05手**(复利模式有效) -- 最大开仓手数:默认 **99手** -- 单方向最多持有一单:默认 **true**(每个方向最多持有1单) - -### 连续亏损保护参数 -- 连续亏损次数触发冷冻:默认 **3**(设为0可禁用该功能) -- 冷冻K线根数:默认 **60**(设为0可禁用该功能) - -### 手工单管理参数 -- 禁止手工单:默认 **true**(自动平掉所有手工单) - -## 补充说明 - -### 突破K线处理 -- 如果出现多个多单突破K线,仅保留最低价更低的那个K线,反之亦然 -- 多头有效波段之间,仅允许出现一个多单突破K线和一个空单突破K线,反之亦然 - -### 有效波段规则 -- 一个有效波段只能开一单(无论是高点还是低点) -- 开仓后该波段立即失效 -- 必须等新的有效波段形成才能再次开仓 - -### 单方向持仓限制 -此参数用于控制同一方向的持仓数量,避免单边风险过大。 - -**参数说明:** -- **false(默认)**:允许同方向开多单,仅受"最大开仓手数"限制 - - 例如:最大开仓手数=3时,可以同时持有3个多单,或3个空单,或2多1空等 -- **true**:每个方向最多持有1单 - - 多单方向:最多持有1个多单 - - 空单方向:最多持有1个空单 - - 最多同时持有1多+1空=2单 - -**使用场景:** -- **保守型交易者**:建议设置为 **true** - - 严格控制风险敞口 - - 避免单边持仓过重 - - 适合波动较大的行情 - -- **激进型交易者**:可设置为 **false** - - 允许同方向加仓 - - 在强趋势行情中获取更多盈利 - - 但需要配合"最大开仓手数"参数控制总风险 - -**示例:** -``` -设置:单方向最多持有一单 = true - -场景1:当前持有1个多单 -- 新的多单信号出现 → ❌ 被忽略(已有多单) -- 新的空单信号出现 → ✅ 可以开仓(空单方向无持仓) - -场景2:当前持有1个多单 + 1个空单 -- 新的多单信号出现 → ❌ 被忽略(已有多单) -- 新的空单信号出现 → ❌ 被忽略(已有空单) -``` - -### 连续亏损保护功能 ⭐ - -此功能用于在连续亏损后自动暂停交易,避免在不利行情中持续亏损。 - -**核心机制:** -- 实时监控每笔交易的盈亏结果(通过 `OnTradeTransaction` 事件) -- 当连续亏损达到设定次数时,自动进入"冷冻期" -- 冷冻期内禁止开新仓,直到指定数量的K线走完 -- 冷冻期结束后自动解除,连续亏损计数器重置为0 - -**参数说明:** - -1. **连续亏损次数触发冷冻**:默认 `3` - - 设置为 `0`:功能完全禁用 - - 设置为 `3`:连续亏损3笔后触发冷冻(推荐值) - - 任何盈利单都会将计数器归零 - -2. **冷冻K线根数**:默认 `60` - - 设置为 `0`:功能完全禁用 - - 设置为 `60`:冷冻期间禁止开仓,直到60根K线走完(M1周期约1小时) - - K线周期取决于"K线周期"参数设置 - -**工作流程:** - -``` -步骤1:监控交易结果 -- 订单平仓后,检测盈亏(包含手续费和隔夜利息) -- 亏损 → 连续亏损计数器 +1 -- 盈利 → 连续亏损计数器归零 - -步骤2:触发冷冻条件 -- 当连续亏损计数器 >= 设定次数时 -- 记录当前K线数量 -- 计算冷冻结束K线数 = 当前K线数 + 冷冻K线根数 -- 打印冷冻提示信息 - -步骤3:冷冻期检查 -- 每次开仓前检查是否处于冷冻期 -- 如果当前K线数 < 冷冻结束K线数 → 拒绝开仓 -- 如果当前K线数 >= 冷冻结束K线数 → 解除冷冻,重置计数器 - -步骤4:冷冻解除 -- 自动打印解除提示 -- 连续亏损计数器重置为0 -- 恢复正常交易 -``` - -**使用场景:** - -1. **震荡行情保护**(默认配置) - - 设置:连续亏损3次 + 冷冻60根K线 - - 效果:在频繁假突破的震荡行情中,连续亏损3次后暂停1小时(M1周期) - - 避免在不利行情中持续开仓亏损 - -2. **回测优化** - - 通过历史数据测试不同参数组合 - - 找到最佳的连续亏损阈值和冷冻时长 - - 提高策略整体盈利能力 - -3. **风险控制** - - 配合"最大开仓手数"和"单方向持仓限制" - - 多层次控制交易风险 - - 保守型交易者建议启用 - -**示例:** - -``` -参数设置: -- 连续亏损次数触发冷冻 = 3 -- 冷冻K线根数 = 10 -- K线周期 = M1 - -交易流程: -时间 09:00 → 开仓多单 → 止损平仓(亏损)→ 连续亏损计数 = 1 -时间 09:05 → 开仓空单 → 止损平仓(亏损)→ 连续亏损计数 = 2 -时间 09:10 → 开仓多单 → 止损平仓(亏损)→ 连续亏损计数 = 3 - -触发冷冻! -- 当前K线数:150 -- 冷冻至K线数:160(150 + 10) -- 打印:=== 触发交易冷冻 === - -冷冻期内(09:10 - 09:20): -- 所有开仓信号被忽略 -- 调试模式下打印:交易冷冻中 - 剩余K线数: X - -时间 09:20(K线数达到160): -- 自动解除冷冻 -- 打印:=== 交易冷冻解除 === -- 连续亏损计数器重置为0 -- 恢复正常交易 -``` - -**注意事项:** - -1. **功能启用条件** - - 两个参数都必须 > 0 才会启用 - - 任一参数为 0,功能完全禁用 - -2. **计数逻辑** - - 仅统计"净亏损"(盈亏 + 手续费 + 隔夜利息) - - 盈亏持平(净盈利约等于0)不会重置计数器 - - 任何盈利单(净盈利 > 0)立即重置计数器 - -3. **冷冻期计算** - - 基于K线根数,不是绝对时间 - - 周末或节假日市场休市时,K线不增加,冷冻期相应延长 - - 建议根据具体交易品种和周期调整参数 - -4. **与其他功能的关系** - - 冷冻期内不影响已有持仓的管理(止损、止盈、移动止损仍正常工作) - - 仅阻止新开仓,不会强制平仓现有持仓 - - 可配合"单方向持仓限制"等功能使用 - -### 禁止手工单功能说明 ⭐ - -**问题背景:** -在EA自动交易过程中,手工开单可能会干扰EA的整体策略逻辑和风险管理,导致: -- 破坏EA的仓位管理计划 -- 影响资金管理和风险控制 -- 与EA策略产生冲突 - -**功能说明:** -- 启用后,EA会自动检测并平掉所有手工单(magic number = 0的订单) -- 仅处理当前品种(_Symbol)的手工单 -- 平仓后会在日志中打印详细信息 - -**识别原理:** -在MQL5中,订单都有一个magic number(魔术号)属性: -- **手工单**:magic number = 0(默认值) -- **EA开单**:magic number = EA设定的值(本策略默认20260516) - -**参数说明:** -- **true(默认)**:启用保护,自动平掉手工单 - - 每个Tick检查一次 - - 发现手工单立即平仓 - - 打印平仓日志:`【禁止手工单】已平掉手工单 - Ticket:xxx 类型:多单/空单` - -- **false**:允许手工单与EA单共存 - - 手工单和EA单可以同时存在 - - 手工单不会被EA管理(不会移动止损等) - - 适合需要手工干预的场景 - -**使用场景:** - -1. **完全自动化交易**(推荐设置:true) - - 避免误操作影响EA策略 - - 确保严格执行EA逻辑 - - 防止情绪化交易 - -2. **半自动化交易**(设置:false) - - 允许手工单和EA单并存 - - 可以手工补仓或对冲 - - 需要手动管理手工单的风险 - -**示例:** - -``` -参数设置:禁止手工单 = true - -情况1:用户在MT5手动开了1个多单 -- EA在下一个Tick检测到该订单的magic=0 -- 自动平掉该订单 -- 打印:【禁止手工单】已平掉手工单 - Ticket:12345 类型:多单 - -情况2:EA自动开了1个多单(magic=20260516) -- EA检测到magic != 0,确认是EA自己开的单 -- 不会平仓,正常管理(移动止损等) - -情况3:其他EA开的单(magic=99999) -- EA检测到magic != 0,确认不是手工单 -- 不会平仓,但也不会管理 -``` - -**注意事项:** - -1. **仅处理当前品种** - - 只平掉当前EA运行品种的手工单 - - 不会影响其他品种的手工单 - -2. **实时检测** - - 每个Tick都会检查 - - 手工单会在极短时间内被平掉 - -3. **与其他功能的关系** - - 不影响EA自身的开仓逻辑 - - 不影响EA的风险管理功能 - - 可配合所有其他参数使用 - -4. **平仓失败处理** - - 如果平仓失败(网络问题等),会打印错误日志 - - 下一个Tick会继续尝试平仓 - -### 反向突破容忍度(新功能)⭐ - -**问题背景:** -在趋势行情中,可能会出现反向突破K线(回调),导致原本完整的波段被"切断",使策略无法识别出肉眼可见的有效波段。 - -**示例:** -``` -价格从2600上涨到2650(涨幅50点) -中间出现一根空单突破K线(回调到2640) - -原有逻辑: -- 波段被切断为:2600→2640 和 2640→2650 -- 每段都不足1000点,无法识别为有效波段 ✗ - -容忍模式(设置30%): -- 回调10点 ÷ 波段50点 = 20% < 30% -- 回撤在容忍范围内,继续计算完整波段 -- 识别出2600→2650的有效波段 ✓ -``` - -**参数说明:** -- **0%(默认)**:不容忍任何反向突破,保持原有逻辑 -- **10-20%**:容忍小幅回调,适合短期趋势 -- **30-40%**:容忍中等回调,适合日内波段 -- **50%以上**:容忍大幅回调,可能识别出更大的波段(但也可能误判) - -**推荐设置:** -- 保守型:0-10% -- 标准型:20-30% -- 激进型:30-50% - -**注意事项:** -1. 设置为0时,完全保持原有逻辑,不影响现有回测结果 -2. 容忍度越高,识别的波段越大,但也可能包含更多噪音 -3. 建议先回测不同容忍度,找到最适合的数值 -4. 容忍度只影响波段识别,不影响止损止盈等其他逻辑 - -### 波段阈值范围说明 -本策略使用**波段范围筛选**机制,只交易特定大小的波段: - -#### 为什么需要波段范围? -- **单一阈值的问题**:传统策略只设置最小阈值(如0.1%),导致0.1%的小波段和5%的大波段都交易,风险收益特征差异巨大 -- **范围筛选的优势**:通过设置最小和最大阈值,只交易特定大小的波段,风险更可控 - -#### 计算方式 -- 以前一个极值点价格为基准,按百分比计算阈值范围 -- 动态适应不同价格水平 - -#### 示例说明 -假设设置: -- 最小阈值:0.1% -- 最大阈值:1.0% -- 当前价格:2600 - -**价格2600时的阈值范围:** -- 最小阈值 = 2600 × 0.1% = 2.6美元(260点) -- 最大阈值 = 2600 × 1.0% = 26美元(2600点) - -**波段筛选结果:** -- 波段 2.0美元(200点):< 最小阈值 → ❌ 不交易(波段太小) -- 波段 5.2美元(520点):在范围内 → ✅ 有效波段,可交易 -- 波段 15美元(1500点):在范围内 → ✅ 有效波段,可交易 -- 波段 30美元(3000点):> 最大阈值 → ❌ 不交易(波段太大) - -**价格4700时的阈值范围:** -- 最小阈值 = 4700 × 0.1% = 4.7美元(470点) -- 最大阈值 = 4700 × 1.0% = 47美元(4700点) - -#### 参数调整建议 -- **保守型**:0.15% - 0.5%(只交易中小波段) -- **标准型**:0.1% - 1.0%(默认,覆盖大多数波段) -- **激进型**:0.08% - 2.0%(交易范围更广) - -### 止损止盈说明 -本策略使用**百分比模式**计算止损和止盈: - -- **止损计算**:止损金额 = MAX(开仓价 × 止损百分比, 1美元) - - 示例1(高价位):开仓价2600,止损0.05%,计算值 = 2600 × 0.05% = 1.3美元 > 1美元,使用1.3美元 - - 多单止损价 = 2600 - 1.3 = 2598.7 - - 空单止损价 = 2600 + 1.3 = 2601.3 - - 示例2(低价位):开仓价1500,止损0.05%,计算值 = 1500 × 0.05% = 0.75美元 < 1美元,使用最小值1美元 - - 多单止损价 = 1500 - 1 = 1499.0 - - 空单止损价 = 1500 + 1 = 1501.0 - -- **止盈计算**:止盈金额 = 止损金额 × 盈亏比 - - 示例1:止损金额1.3美元,盈亏比1.5,止盈金额 = 1.3 × 1.5 = 1.95美元 - - 多单止盈价 = 2600 + 1.95 = 2601.95 - - 空单止盈价 = 2600 - 1.95 = 2598.05 - - 示例2:止损金额1美元,盈亏比1.5,止盈金额 = 1 × 1.5 = 1.5美元 - - 多单止盈价 = 1500 + 1.5 = 1501.5 - - 空单止盈价 = 1500 - 1.5 = 1498.5 - -- **移动止损**(可选功能,默认启用): - - **触发条件**:当浮盈 >= 止损金额时触发 - - **移动目标**:将止损移至成本价(开仓价) - - **移动次数**:仅移动一次(保本策略) - - **示例**:开仓价2603.24,止损金额1.30美元 - - 当价格涨至2604.54(浮盈1.30)时,止损从2601.94移至2603.24 - - 之后价格无论涨到多高,止损都保持在2603.24 - - 即使回调至成本价,也能保本离场,不会亏损 - - **开关控制**:可通过参数"使用移动止损"关闭此功能 - -- **最小止损保护**:无论百分比设置多小,止损金额始终不低于1美元(100点),避免止损过小导致频繁触发 - -- **移动止损的作用**: - - ✅ 启用时:浮盈达标后锁定盈亏平衡,避免盈利单变亏损 - - ❌ 禁用时:止损始终保持在初始位置,追求更高止盈,但风险更大 diff --git a/20260519-突破策略/XAU_1min_v1.set b/20260519-突破策略/XAU_1min_v1.set deleted file mode 100644 index 6408a79..0000000 Binary files a/20260519-突破策略/XAU_1min_v1.set and /dev/null differ diff --git a/20260520-突破马丁/XAU_1min_v2.set b/20260520-突破马丁/XAU_1min_v2.set deleted file mode 100644 index f617872..0000000 Binary files a/20260520-突破马丁/XAU_1min_v2.set and /dev/null differ diff --git a/20260522-突破马丁/20260522_Breakout.mq5 b/20260522-突破马丁/20260522_Breakout.mq5 new file mode 100644 index 0000000..484067e --- /dev/null +++ b/20260522-突破马丁/20260522_Breakout.mq5 @@ -0,0 +1,2010 @@ +//+------------------------------------------------------------------+ +//| 20260520_Breakout.mq5 | +//| 突破策略 - 单笔突破开仓 | +//+------------------------------------------------------------------+ +#property copyright "Breakout Strategy" +#property version "3.18" +#property strict + +#include + +// 破高/破低开仓方向 +enum ENUM_BREAKOUT_DIRECTION { + BREAKOUT_DIR_FOLLOW = 0, // 顺势(破高多/破低空) + BREAKOUT_DIR_REVERSE = 1, // 反向(破高空/破低多) + BREAKOUT_DIR_RANDOM = 2, // 随机(每次突破独立随机) + BREAKOUT_DIR_PREV_DAY_MA = 3, // 前日收线相对日MA(下方只空/上方只多) + BREAKOUT_DIR_DAILY_MA_DEVIATION = 4 // 日MA偏离率反向(偏离超阈值才开反向单) +}; + +// 当前轮多空同向均达满档后的处理 +enum ENUM_DUAL_FULL_ACTION { + DUAL_FULL_CLEAR_ALL = 0, // 清仓(平掉全部EA持仓) + DUAL_FULL_NEW_ROUND = 1 // 不处理旧仓,开始下一轮 +}; + +//+------------------------------------------------------------------+ +//| 输入参数 | +//+------------------------------------------------------------------+ +input group "=== 波段识别参数 ===" +input ENUM_TIMEFRAMES InpTimeframe = PERIOD_M1; // K线周期 +input int InpMAPeriod = 14; // MA周期 +input double InpMinWavePercent = 0.1; // 最小波段阈值百分比(%) +input double InpMaxWavePercent = 0.25; // 最大波段阈值百分比(%) +input double InpPullbackTolerance = 0.05; // 反向突破容忍度(%) 0=不容忍 +input int InpMinWaveBars = 3; // 有效波段最少K线数(含两端极值所在K) + +input group "=== 突破交易参数 ===" +input ENUM_BREAKOUT_DIRECTION InpBreakoutDirection = BREAKOUT_DIR_RANDOM; // 开仓方向 +input double InpDailyMADevThreshold = 1.0; // 日MA偏离率阈值(%),|偏离|超过才触发反向,0=关 +input int InpBatchTakeProfitPoints = 300; // 按批统盈点数(0=关):均价±点数平该批 +input double InpStopLossBalancePct = 30.0; // 止损:持仓浮亏达结余比例(%)全部清仓,0=关 +input double InpLargeLotMinVolume = 1.0; // 大单独立止盈手数阈值(>=,0=关) +input int InpLargeLotTakeProfitPoints = 500; // 大单最大止盈点数(0=关,不参与统盈) +input bool InpEnablePullbackReverse = true; // 启用突破回落反向 +input int InpPullbackReverseWatchBars = 3; // 回落监测K线数(突破K+后2根=3) + +input group "=== 仓位管理参数 ===" +input string InpTierLotsList = + "0.05,0.05,0.05,0.8,0.8,1.1,1,2,2,1,1,1,2,2,1,1,1,5,5"; // 各档手数(逗号分隔,档数=最多开仓笔数) +input double InpMaxLots = 99.0; // 同向最大合计手数(0=不限制) +input double InpMinSameDirAddSpacingPercent = 0.0; // 同向最小开仓间距(%) 0=不限制 +input ENUM_DUAL_FULL_ACTION InpDualFullAction = DUAL_FULL_CLEAR_ALL; // 双向满档后处理 + +input group "=== 调试选项 ===" +input bool InpShowDebugInfo = false; // 显示调试信息 +input bool InpShowMarkers = true; // 显示极值点标记 +input int InpMagicNumber = 20260520; // EA魔术号 +input bool InpCloseManualOrders = true; // 禁止手工单(自动平掉) + +//+------------------------------------------------------------------+ +//| 全局变量 | +//+------------------------------------------------------------------+ +int ma_handle; // MA指标句柄 +int daily_ma_handle; // 日K MA(前日收线方向过滤) +CTrade trade; // 交易对象 + +// 最新有效波段信息 +struct ValidWaveInfo { + bool exists; // 是否存在有效波段 + double high_price; // 高点价格 + double low_price; // 低点价格 + datetime update_time; // 更新时间 + bool high_breakout_used; // 高点-突破开仓已用(每极值限1次) + bool low_breakout_used; // 低点-突破开仓已用(每极值限1次) + bool high_pullback_reverse_used; // 高点-回落反向已用(每极值限1次) + bool low_pullback_reverse_used; // 低点-回落反向已用(每极值限1次) +}; + +struct PullbackWatchState { + bool active; + double extreme_price; + int bars_checked; +}; + +ValidWaveInfo latest_wave; // 最新有效波段 + +datetime g_breakout_bar_time_high = 0; // 破高:同K线限1笔 +bool g_breakout_opened_on_bar_high = false; +datetime g_breakout_bar_time_low = 0; // 破低:同K线限1笔 +bool g_breakout_opened_on_bar_low = false; + +int g_trade_round = 1; // 当前交易轮次(新轮仅统计该轮持仓) +bool g_dual_full_handled = false; // 本轮双向满档是否已处理 + +double g_tier_lots[]; +int g_tier_lots_count = 0; + +PullbackWatchState g_high_pullback_watch; +PullbackWatchState g_low_pullback_watch; +datetime g_pullback_bar_time_high = 0; +bool g_pullback_opened_on_bar_high = false; +datetime g_pullback_bar_time_low = 0; +bool g_pullback_opened_on_bar_low = false; + +int g_prev_day_ma_bias = 0; // -1=前日收<日MA 1=前日收>日MA +datetime g_prev_day_ma_update_d1 = 0; + +// 极值点结构体定义 +struct ExtremePoint { + datetime time; + double price; + int type; + bool is_valid; +}; + +// 函数声明 +void UpdateLatestValidWave(); +int CountBarsBetweenExtremeTimes(const MqlRates &rates[], const datetime t1, const datetime t2); +bool IsValidWaveByBarCount(const MqlRates &rates[], const datetime t1, const datetime t2); +void DrawExtremeMarkers(ExtremePoint &extremes[]); +void DrawLatestValidWave(double high_price, datetime high_time, double low_price, datetime low_time); +int CheckBreakout(int index, const MqlRates &rates[], const double &ma[]); +void FilterBreakouts(const int &breakout_bars[], const int &breakout_types[], + const MqlRates &rates[], int &filtered_bars[], int &filtered_types[]); +ENUM_ORDER_TYPE RandomBreakoutOrderType(); +int ParseRoundFromComment(const string &comment); +bool IsEaBreakoutComment(const string &comment); +string FormatBreakoutComment(const int round); +int CountBreakoutPositions(const long pos_type_filter = -1); +int CountAllBreakoutPositions(); +double TotalLotsInDirection(const long pos_type); +bool IsDirectionFullActive(const long pos_type); +void ResetBreakoutWaveEntryFlags(); +void CheckDualSideFullCapacity(); +double NormalizeVolume(double lots); +bool InitTierLotsFromInput(); +int GetMaxTierSlots(); +double LotForTierIndex(const int tier_index); +double CalculateLotSize(const long pos_type); +bool IsSameDirectionTierSlotsFull(const long pos_type); +long PosTypeOnHighBreakout(); +long PosTypeOnLowBreakout(); +void CheckBreakoutSignals(); +void CheckPullbackReverseSignals(); +void TryArmPullbackWatch(); +void ProcessPullbackReverseOnNewBar(); +ENUM_ORDER_TYPE OrderTypeOnHighPullbackReverse(); +ENUM_ORDER_TYPE OrderTypeOnLowPullbackReverse(); +string FormatPullbackReverseComment(const int round); +bool IsPullbackBarOpenAllowed(const bool for_high_extreme); +void MarkPullbackBarOpened(const bool for_high_extreme); +bool OpenPullbackReversePosition(ENUM_ORDER_TYPE order_type, const bool from_high_extreme, + const double extreme_price); +ENUM_ORDER_TYPE OrderTypeOnHighBreakout(); +ENUM_ORDER_TYPE OrderTypeOnLowBreakout(); +void CheckAndCloseManualOrders(); +double TotalBreakoutFloatingPL(); +bool CloseAllBreakoutPositions(const string reason); +void CollectAllTradeRounds(int &rounds[], int &n_rounds); +bool BatchRoundAvgPrice(const int round_id, const long pos_type, double &avg, double &sum_lots); +bool CloseBreakoutRoundBatch(const int round_id, const long pos_type, const string reason); +void CheckBreakoutBatchTakeProfit(); +void CheckBreakoutBalanceStopLoss(); +bool IsLargeLotVolume(const double volume); +double CalcTakeProfitPrice(const long pos_type, const double open_price, const int tp_points); +bool SetLargeLotTakeProfitOnTicket(const ulong ticket); +bool ApplyLargeLotTakeProfitAfterOpen(const long pos_type); +void CheckLargeLotTakeProfit(); +bool OpenBreakoutPosition(ENUM_ORDER_TYPE order_type, double wave_high, double wave_low, + const bool from_high_extreme); +void SyncBreakoutBarLock(const bool for_high_extreme); +bool IsBreakoutBarOpenAllowed(const bool for_high_extreme); +void MarkBreakoutBarOpened(const bool for_high_extreme); +bool GetLastSameDirectionOpenPrice(const long pos_type, double &last_open); +bool IsSameDirMinSpacingSatisfied(const long pos_type); +bool IsPrevDayMAFilterMode(); +void UpdatePrevDayMACloseBias(); +int GetPrevDayMACloseBias(); +bool ShouldOpenHighBreakoutPrevDayMA(); +bool ShouldOpenLowBreakoutPrevDayMA(); +bool ShouldOpenHighPullbackPrevDayMA(); +bool ShouldOpenLowPullbackPrevDayMA(); +bool IsDailyMADevFilterMode(); +double GetDailyMADeviationPercent(); +bool ShouldOpenHighBreakoutDailyMADev(); +bool ShouldOpenLowBreakoutDailyMADev(); +bool ShouldOpenHighPullbackDailyMADev(); +bool ShouldOpenLowPullbackDailyMADev(); +bool IsHighBreakoutSignalAllowed(); +bool IsLowBreakoutSignalAllowed(); +bool IsHighPullbackSignalAllowed(); +bool IsLowPullbackSignalAllowed(); + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() +{ + // 创建MA指标(使用指定的K线周期) + ma_handle = iMA(_Symbol, InpTimeframe, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); + if(ma_handle == INVALID_HANDLE) { + Print("创建MA指标失败"); + return(INIT_FAILED); + } + + daily_ma_handle = iMA(_Symbol, PERIOD_D1, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); + if(daily_ma_handle == INVALID_HANDLE) { + Print("创建日K MA指标失败"); + IndicatorRelease(ma_handle); + return(INIT_FAILED); + } + + g_prev_day_ma_bias = 0; + g_prev_day_ma_update_d1 = 0; + + // 设置交易参数 + trade.SetExpertMagicNumber(InpMagicNumber); + trade.SetDeviationInPoints(10); + trade.SetTypeFilling(ORDER_FILLING_IOC); + + // 初始化最新有效波段 + latest_wave.exists = false; + latest_wave.high_price = 0; + latest_wave.low_price = 0; + latest_wave.update_time = 0; + latest_wave.high_breakout_used = false; + latest_wave.low_breakout_used = false; + latest_wave.high_pullback_reverse_used = false; + latest_wave.low_pullback_reverse_used = false; + g_high_pullback_watch.active = false; + g_low_pullback_watch.active = false; + + MathSrand((int)(TimeLocal() ^ GetTickCount())); + + g_trade_round = 1; + g_dual_full_handled = false; + + if(!InitTierLotsFromInput()) { + Print("解析各档手数失败,请检查 InpTierLotsList"); + return(INIT_FAILED); + } + + Print("========================================"); + Print("突破EA初始化成功 (极值点突破开仓)"); + Print("品种:", _Symbol); + Print("K线周期:", EnumToString(InpTimeframe)); + Print("MA周期:", InpMAPeriod); + Print("波段阈值范围: ", InpMinWavePercent, "% - ", InpMaxWavePercent, "%"); + Print("有效波段最少K线: ", InpMinWaveBars, " (两极值间含两端,<=1=不限制)"); + if(InpPullbackTolerance > 0) + Print("反向突破容忍度: ", DoubleToString(InpPullbackTolerance, 1), "% (启用)"); + else + Print("反向突破容忍度: 0% (禁用)"); + if(InpBreakoutDirection == BREAKOUT_DIR_FOLLOW) + Print("开仓方向: 顺势(破高多/破低空)"); + else if(InpBreakoutDirection == BREAKOUT_DIR_REVERSE) + Print("开仓方向: 反向(破高空/破低多)"); + else if(InpBreakoutDirection == BREAKOUT_DIR_PREV_DAY_MA) + Print("开仓方向: 前日收线相对日MA(下方:破低空+高极值反向;上方:破高多+低极值反向)"); + else if(InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION) + Print("开仓方向: 日MA偏离率反向(偏离>", InpDailyMADevThreshold, + "%:价高于MA做空;偏离<-", InpDailyMADevThreshold, "%:价低于MA做多)"); + else + Print("开仓方向: 随机(每次突破独立随机)"); + Print("开仓限制: 每个极值点(破高/破低)各最多1笔,可多笔并存"); + Print("按批统盈点数:", InpBatchTakeProfitPoints, " (0=关闭,均价±点数平该轮该向)"); + Print("止损(结余%):", InpStopLossBalancePct, " (0=关闭,达标全部清仓)"); + if(InpLargeLotMinVolume > 0.0 && InpLargeLotTakeProfitPoints > 0) + Print("大单独立止盈: >=", InpLargeLotMinVolume, "手 止盈", InpLargeLotTakeProfitPoints, + "点(不参与统盈,平仓后马丁/统盈按剩余持仓重计)"); + else + Print("大单独立止盈: 关"); + if(InpEnablePullbackReverse) + Print("回落反向: 开 破极值后监测", InpPullbackReverseWatchBars, + "根K收盘回到极值内开反向单(与突破开仓独立)"); + else + Print("回落反向: 关"); + Print("同向各档手数: 共", g_tier_lots_count, "档(第1档=", g_tier_lots[0], + " 末档=", g_tier_lots[g_tier_lots_count - 1], ")"); + if(InpMaxLots > 0.0) + Print("同向合计手数上限:", InpMaxLots); + else + Print("同向合计手数上限: 不限制"); + if(InpMinSameDirAddSpacingPercent > 0.0) + Print("同向最小间距%:", InpMinSameDirAddSpacingPercent, + " (多:现价须≤上一笔×(1-%); 空:现价须≥上一笔×(1+%))"); + else + Print("同向最小间距%: 0 (不限制)"); + if(InpDualFullAction == DUAL_FULL_CLEAR_ALL) + Print("双向满档后: 清仓"); + else + Print("双向满档后: 不处理旧仓,开始下一轮(备注带轮次)"); + Print("禁止手工单:", (InpCloseManualOrders ? "启用 (自动平掉手工单)" : "禁用")); + Print("========================================"); + + return(INIT_SUCCEEDED); +} + +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + if(ma_handle != INVALID_HANDLE) + IndicatorRelease(ma_handle); + if(daily_ma_handle != INVALID_HANDLE) + IndicatorRelease(daily_ma_handle); + + // 删除所有标记 + ObjectsDeleteAll(0, "ValidWave_"); + + Print("突破EA已卸载"); +} + +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() +{ + // 1. 检查并关闭手工单(如果启用) + if(InpCloseManualOrders) + CheckAndCloseManualOrders(); + + // 2. 前日收线相对日MA方向(仅该模式) + if(IsPrevDayMAFilterMode()) + UpdatePrevDayMACloseBias(); + + // 3. 更新最新有效波段 + UpdateLatestValidWave(); + + // 4. 大单独立止盈 / 按批统盈(均价+点数) / 结余比例止损(全部持仓) + CheckLargeLotTakeProfit(); + CheckBreakoutBatchTakeProfit(); + CheckBreakoutBalanceStopLoss(); + + // 5. 当前轮双向同向均满档 → 清仓或开新轮 + CheckDualSideFullCapacity(); + + // 6. 突破回落反向(破极值后K线收盘回到极值内) + CheckPullbackReverseSignals(); + + // 7. 突破信号(破极值开仓,每极值点各限1笔,与回落反向独立) + CheckBreakoutSignals(); +} + +//+------------------------------------------------------------------+ +//| 更新最新有效波段 | +//+------------------------------------------------------------------+ +int CountBarsBetweenExtremeTimes(const MqlRates &rates[], const datetime t1, const datetime t2) +{ + const datetime t_lo = (t1 <= t2) ? t1 : t2; + const datetime t_hi = (t1 >= t2) ? t1 : t2; + int count = 0; + const int n = ArraySize(rates); + for(int j = 0; j < n; j++) { + if(rates[j].time >= t_lo && rates[j].time <= t_hi) + count++; + } + return count; +} + +bool IsValidWaveByBarCount(const MqlRates &rates[], const datetime t1, const datetime t2) +{ + if(InpMinWaveBars <= 1) + return true; + return (CountBarsBetweenExtremeTimes(rates, t1, t2) >= InpMinWaveBars); +} + +void UpdateLatestValidWave() +{ + int bars = Bars(_Symbol, InpTimeframe); + if(bars < InpMAPeriod + 2) + return; + + // 限制处理的K线数量 + int process_bars = MathMin(bars, 500); + + // 获取价格数据(使用指定的K线周期) + MqlRates rates[]; + ArraySetAsSeries(rates, true); + if(CopyRates(_Symbol, InpTimeframe, 0, process_bars, rates) <= 0) + return; + + // 获取MA数据 + double ma_array[]; + ArraySetAsSeries(ma_array, true); + if(CopyBuffer(ma_handle, 0, 0, process_bars, ma_array) <= 0) + return; + + // 识别突破K线 + int breakout_bars[]; + int breakout_types[]; + ArrayResize(breakout_bars, 0); + ArrayResize(breakout_types, 0); + + for(int i = process_bars - InpMAPeriod - 1; i >= 1; i--) { + int breakout_type = CheckBreakout(i, rates, ma_array); + if(breakout_type != 0) { + int size = ArraySize(breakout_bars); + ArrayResize(breakout_bars, size + 1); + ArrayResize(breakout_types, size + 1); + breakout_bars[size] = i; + breakout_types[size] = breakout_type; + } + } + + // 过滤连续同向突破 + int filtered_bars[]; + int filtered_types[]; + FilterBreakouts(breakout_bars, breakout_types, rates, filtered_bars, filtered_types); + + // 计算极值点(支持反向突破容忍度) + ExtremePoint extremes[]; + ArrayResize(extremes, 0); + + // 如果容忍度为0,使用原有逻辑(相邻突破K线之间的极值) + if(InpPullbackTolerance <= 0.0) + { + for(int i = 0; i < ArraySize(filtered_bars) - 1; i++) { + int current_bar = filtered_bars[i]; + int current_type = filtered_types[i]; + int next_bar = filtered_bars[i + 1]; + + double extreme_price = 0; + datetime extreme_time = 0; + + if(current_type == 1) { + extreme_price = rates[current_bar].high; + extreme_time = rates[current_bar].time; + for(int j = current_bar; j >= next_bar; j--) { + if(rates[j].high > extreme_price) { + extreme_price = rates[j].high; + extreme_time = rates[j].time; + } + } + } else { + extreme_price = rates[current_bar].low; + extreme_time = rates[current_bar].time; + for(int j = current_bar; j >= next_bar; j--) { + if(rates[j].low < extreme_price) { + extreme_price = rates[j].low; + extreme_time = rates[j].time; + } + } + } + + int size = ArraySize(extremes); + ArrayResize(extremes, size + 1); + extremes[size].time = extreme_time; + extremes[size].price = extreme_price; + extremes[size].type = current_type; + extremes[size].is_valid = false; + } + } + else + { + // 容忍模式:跨越反向突破K线计算波段 + for(int i = 0; i < ArraySize(filtered_bars); i++) { + int start_bar = filtered_bars[i]; + int start_type = filtered_types[i]; + + double wave_high = rates[start_bar].high; + double wave_low = rates[start_bar].low; + datetime wave_high_time = rates[start_bar].time; + datetime wave_low_time = rates[start_bar].time; + int end_bar = 0; + bool wave_terminated = false; + + // 向后扫描,直到遇到不可容忍的反向突破 + for(int j = i + 1; j < ArraySize(filtered_bars); j++) { + int current_bar = filtered_bars[j]; + int current_type = filtered_types[j]; + + // 更新波段的高低点 + if(rates[current_bar].high > wave_high) { + wave_high = rates[current_bar].high; + wave_high_time = rates[current_bar].time; + } + if(rates[current_bar].low < wave_low) { + wave_low = rates[current_bar].low; + wave_low_time = rates[current_bar].time; + } + + // 检查中间所有K线的极值 + int prev_bar = (j > 0) ? filtered_bars[j-1] : start_bar; + for(int k = prev_bar; k >= current_bar; k--) { + if(rates[k].high > wave_high) { + wave_high = rates[k].high; + wave_high_time = rates[k].time; + } + if(rates[k].low < wave_low) { + wave_low = rates[k].low; + wave_low_time = rates[k].time; + } + } + + // 如果遇到反向突破K线,检查回撤是否可容忍 + if(current_type != start_type) { + double wave_range = wave_high - wave_low; + double pullback_percent = 0; + + if(start_type == 1) { + // 多头波段遇到空单突破K线 + pullback_percent = ((wave_high - rates[current_bar].close) / wave_range) * 100.0; + } else { + // 空头波段遇到多单突破K线 + pullback_percent = ((rates[current_bar].close - wave_low) / wave_range) * 100.0; + } + + if(pullback_percent > InpPullbackTolerance) { + // 回撤超过容忍度,终止波段 + end_bar = current_bar; + wave_terminated = true; + break; + } + // 否则继续,忽略此反向突破 + } + } + + // 添加极值点 + if(start_type == 1) { + // 多头波段:先低点后高点 + int size = ArraySize(extremes); + ArrayResize(extremes, size + 1); + extremes[size].time = wave_low_time; + extremes[size].price = wave_low; + extremes[size].type = -1; // 低点 + extremes[size].is_valid = false; + + ArrayResize(extremes, size + 2); + extremes[size + 1].time = wave_high_time; + extremes[size + 1].price = wave_high; + extremes[size + 1].type = 1; // 高点 + extremes[size + 1].is_valid = false; + } else { + // 空头波段:先高点后低点 + int size = ArraySize(extremes); + ArrayResize(extremes, size + 1); + extremes[size].time = wave_high_time; + extremes[size].price = wave_high; + extremes[size].type = 1; // 高点 + extremes[size].is_valid = false; + + ArrayResize(extremes, size + 2); + extremes[size + 1].time = wave_low_time; + extremes[size + 1].price = wave_low; + extremes[size + 1].type = -1; // 低点 + extremes[size + 1].is_valid = false; + } + + // 如果波段被终止,跳到终止点继续 + if(wave_terminated) { + // 找到end_bar在filtered_bars中的索引 + for(int k = i + 1; k < ArraySize(filtered_bars); k++) { + if(filtered_bars[k] == end_bar) { + i = k - 1; // -1因为循环会++ + break; + } + } + } else { + // 波段延续到最后 + break; + } + } + } + + // 判断有效波段并标记 + for(int i = 1; i < ArraySize(extremes); i++) { + double price_diff = MathAbs(extremes[i].price - extremes[i-1].price); + double price_diff_points = price_diff / _Point; + + // 计算阈值(百分比模式:以前一个极值点价格为基准计算百分比) + double base_price = extremes[i-1].price; + double min_threshold = (base_price * InpMinWavePercent / 100.0) / _Point; + double max_threshold = (base_price * InpMaxWavePercent / 100.0) / _Point; + + // 波段必须在最小和最大阈值之间才是有效波段 + if(price_diff_points >= min_threshold && price_diff_points <= max_threshold && + IsValidWaveByBarCount(rates, extremes[i - 1].time, extremes[i].time)) { + extremes[i-1].is_valid = true; + extremes[i].is_valid = true; + } + } + + // 绘制所有极值点标记 + if(InpShowMarkers) { + DrawExtremeMarkers(extremes); + } + + // 查找最新的有效波段 + for(int i = ArraySize(extremes) - 1; i >= 1; i--) { + double price_diff = MathAbs(extremes[i].price - extremes[i-1].price); + double price_diff_points = price_diff / _Point; + + // 计算阈值(百分比模式) + double base_price = extremes[i-1].price; + double min_threshold = (base_price * InpMinWavePercent / 100.0) / _Point; + double max_threshold = (base_price * InpMaxWavePercent / 100.0) / _Point; + + // 波段必须在最小和最大阈值之间才是有效波段 + if(price_diff_points >= min_threshold && price_diff_points <= max_threshold && + IsValidWaveByBarCount(rates, extremes[i - 1].time, extremes[i].time)) { + // 找到最新的有效波段 + double high = MathMax(extremes[i].price, extremes[i-1].price); + double low = MathMin(extremes[i].price, extremes[i-1].price); + datetime high_time = (extremes[i].price > extremes[i-1].price) ? extremes[i].time : extremes[i-1].time; + datetime low_time = (extremes[i].price < extremes[i-1].price) ? extremes[i].time : extremes[i-1].time; + + // 检查是否是新的波段 + if(latest_wave.exists == false || + extremes[i].time > latest_wave.update_time || + high != latest_wave.high_price || + low != latest_wave.low_price) { + + const double prev_high = latest_wave.high_price; + const double prev_low = latest_wave.low_price; + + latest_wave.exists = true; + latest_wave.high_price = high; + latest_wave.low_price = low; + latest_wave.update_time = extremes[i].time; + + // 极值价格更新 → 该侧可再开1笔(与是否已有其它持仓无关) + if(high != prev_high) { + latest_wave.high_breakout_used = false; + latest_wave.high_pullback_reverse_used = false; + g_high_pullback_watch.active = false; + } + if(low != prev_low) { + latest_wave.low_breakout_used = false; + latest_wave.low_pullback_reverse_used = false; + g_low_pullback_watch.active = false; + } + + // 绘制最新有效波段 + if(InpShowMarkers) { + DrawLatestValidWave(high, high_time, low, low_time); + } + + if(InpShowDebugInfo) { + Print("更新最新有效波段 - 高:", DoubleToString(high, _Digits), + " 低:", DoubleToString(low, _Digits), + " 价差:", (int)price_diff_points, "点", + " K线数:", CountBarsBetweenExtremeTimes(rates, extremes[i - 1].time, extremes[i].time), + " 阈值范围:", StringFormat("%.2f%%-%.2f%% (%.0f-%.0f点)", + InpMinWavePercent, InpMaxWavePercent, min_threshold, max_threshold)); + } + } + break; + } + } +} + +//+------------------------------------------------------------------+ +//| 绘制所有极值点标记 | +//+------------------------------------------------------------------+ +void DrawExtremeMarkers(ExtremePoint &extremes[]) +{ + // 删除旧的标记 + ObjectsDeleteAll(0, "ValidWave_Extreme_"); + + for(int i = 0; i < ArraySize(extremes); i++) { + string obj_name = "ValidWave_Extreme_" + IntegerToString(i); + + if(extremes[i].type == 1) { + // 高点 - 画下箭头 + ObjectCreate(0, obj_name, OBJ_ARROW, 0, extremes[i].time, extremes[i].price); + ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 234); + ObjectSetInteger(0, obj_name, OBJPROP_COLOR, extremes[i].is_valid ? clrRed : clrDarkRed); + ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, extremes[i].is_valid ? 3 : 1); + ObjectSetInteger(0, obj_name, OBJPROP_ANCHOR, ANCHOR_BOTTOM); + } else { + // 低点 - 画上箭头 + ObjectCreate(0, obj_name, OBJ_ARROW, 0, extremes[i].time, extremes[i].price); + ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 233); + ObjectSetInteger(0, obj_name, OBJPROP_COLOR, extremes[i].is_valid ? clrLime : clrDarkGreen); + ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, extremes[i].is_valid ? 3 : 1); + ObjectSetInteger(0, obj_name, OBJPROP_ANCHOR, ANCHOR_TOP); + } + } +} + +//+------------------------------------------------------------------+ +//| 绘制最新有效波段 | +//+------------------------------------------------------------------+ +void DrawLatestValidWave(double high_price, datetime high_time, double low_price, datetime low_time) +{ + // 删除旧的最新波段标记 + ObjectDelete(0, "ValidWave_Latest_High"); + ObjectDelete(0, "ValidWave_Latest_Low"); + ObjectDelete(0, "ValidWave_Latest_Line"); + + // 标记最新有效波段的高点(更大更亮的箭头) + ObjectCreate(0, "ValidWave_Latest_High", OBJ_ARROW, 0, high_time, high_price); + ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_ARROWCODE, 234); + ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_WIDTH, 4); + ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_ANCHOR, ANCHOR_BOTTOM); + + // 标记最新有效波段的低点 + ObjectCreate(0, "ValidWave_Latest_Low", OBJ_ARROW, 0, low_time, low_price); + ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_ARROWCODE, 233); + ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_WIDTH, 4); + ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_ANCHOR, ANCHOR_TOP); + + // 绘制连接线 + ObjectCreate(0, "ValidWave_Latest_Line", OBJ_TREND, 0, high_time, high_price, low_time, low_price); + ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_WIDTH, 2); + ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_RAY_RIGHT, false); + ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_BACK, true); +} + +//+------------------------------------------------------------------+ +//| 检查是否是突破K线 | +//+------------------------------------------------------------------+ +int CheckBreakout(int index, const MqlRates &rates[], const double &ma[]) +{ + if(rates[index].open < ma[index] && rates[index].close > ma[index]) + return 1; + if(rates[index].open > ma[index] && rates[index].close < ma[index]) + return -1; + return 0; +} + +//+------------------------------------------------------------------+ +//| 过滤连续同向突破 | +//+------------------------------------------------------------------+ +void FilterBreakouts(const int &breakout_bars[], const int &breakout_types[], + const MqlRates &rates[], int &filtered_bars[], int &filtered_types[]) +{ + int total = ArraySize(breakout_bars); + ArrayResize(filtered_bars, 0); + ArrayResize(filtered_types, 0); + + for(int i = 0; i < total; i++) { + int current_bar = breakout_bars[i]; + int current_type = breakout_types[i]; + + bool skip = false; + for(int j = i + 1; j < total; j++) { + if(breakout_types[j] != current_type) + break; + + if(current_type == 1) { + if(rates[breakout_bars[j]].low < rates[current_bar].low) { + skip = true; + break; + } + } else { + if(rates[breakout_bars[j]].high > rates[current_bar].high) { + skip = true; + break; + } + } + } + + if(!skip) { + int size = ArraySize(filtered_bars); + ArrayResize(filtered_bars, size + 1); + ArrayResize(filtered_types, size + 1); + filtered_bars[size] = current_bar; + filtered_types[size] = current_type; + } + } +} + +ENUM_ORDER_TYPE RandomBreakoutOrderType() +{ + return ((MathRand() & 1) != 0) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; +} + +bool IsPrevDayMAFilterMode() +{ + return (InpBreakoutDirection == BREAKOUT_DIR_PREV_DAY_MA); +} + +void UpdatePrevDayMACloseBias() +{ + const datetime d1_time = iTime(_Symbol, PERIOD_D1, 0); + if(d1_time == 0) + return; + if(g_prev_day_ma_update_d1 == d1_time) + return; + + g_prev_day_ma_update_d1 = d1_time; + g_prev_day_ma_bias = 0; + + const double prev_close = iClose(_Symbol, PERIOD_D1, 1); + if(prev_close <= 0.0) + return; + + double ma_buf[]; + ArraySetAsSeries(ma_buf, true); + if(CopyBuffer(daily_ma_handle, 0, 1, 1, ma_buf) <= 0) + return; + + const double daily_ma = ma_buf[0]; + if(daily_ma <= 0.0) + return; + + if(prev_close < daily_ma - _Point * 0.5) + g_prev_day_ma_bias = -1; + else if(prev_close > daily_ma + _Point * 0.5) + g_prev_day_ma_bias = 1; + else + g_prev_day_ma_bias = 0; + + if(InpShowDebugInfo) { + Print("【前日MA方向】前日收:", DoubleToString(prev_close, _Digits), + " 日MA(", InpMAPeriod, "):", DoubleToString(daily_ma, _Digits), + " 偏向:", (g_prev_day_ma_bias < 0 ? "只空" : (g_prev_day_ma_bias > 0 ? "只多" : "中性"))); + } +} + +int GetPrevDayMACloseBias() +{ + return g_prev_day_ma_bias; +} + +bool ShouldOpenHighBreakoutPrevDayMA() +{ + return (GetPrevDayMACloseBias() > 0); +} + +bool ShouldOpenLowBreakoutPrevDayMA() +{ + return (GetPrevDayMACloseBias() < 0); +} + +bool ShouldOpenHighPullbackPrevDayMA() +{ + return (GetPrevDayMACloseBias() < 0); +} + +bool ShouldOpenLowPullbackPrevDayMA() +{ + return (GetPrevDayMACloseBias() > 0); +} + +bool IsDailyMADevFilterMode() +{ + return (InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION); +} + +double GetDailyMADeviationPercent() +{ + if(InpDailyMADevThreshold <= 0.0) + return 0.0; + + double ma_buf[]; + ArraySetAsSeries(ma_buf, true); + if(CopyBuffer(daily_ma_handle, 0, 0, 1, ma_buf) <= 0) + return 0.0; + + const double daily_ma = ma_buf[0]; + if(daily_ma <= 0.0) + return 0.0; + + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double price = (bid + ask) * 0.5; + return (price - daily_ma) / daily_ma * 100.0; +} + +bool ShouldOpenHighBreakoutDailyMADev() +{ + if(InpDailyMADevThreshold <= 0.0) + return false; + return (GetDailyMADeviationPercent() > InpDailyMADevThreshold); +} + +bool ShouldOpenLowBreakoutDailyMADev() +{ + if(InpDailyMADevThreshold <= 0.0) + return false; + return (GetDailyMADeviationPercent() < -InpDailyMADevThreshold); +} + +bool ShouldOpenHighPullbackDailyMADev() +{ + return ShouldOpenHighBreakoutDailyMADev(); +} + +bool ShouldOpenLowPullbackDailyMADev() +{ + return ShouldOpenLowBreakoutDailyMADev(); +} + +bool IsHighBreakoutSignalAllowed() +{ + if(InpBreakoutDirection == BREAKOUT_DIR_PREV_DAY_MA) + return ShouldOpenHighBreakoutPrevDayMA(); + if(InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION) + return ShouldOpenHighBreakoutDailyMADev(); + return true; +} + +bool IsLowBreakoutSignalAllowed() +{ + if(InpBreakoutDirection == BREAKOUT_DIR_PREV_DAY_MA) + return ShouldOpenLowBreakoutPrevDayMA(); + if(InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION) + return ShouldOpenLowBreakoutDailyMADev(); + return true; +} + +bool IsHighPullbackSignalAllowed() +{ + if(InpBreakoutDirection == BREAKOUT_DIR_PREV_DAY_MA) + return ShouldOpenHighPullbackPrevDayMA(); + if(InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION) + return ShouldOpenHighPullbackDailyMADev(); + return true; +} + +bool IsLowPullbackSignalAllowed() +{ + if(InpBreakoutDirection == BREAKOUT_DIR_PREV_DAY_MA) + return ShouldOpenLowPullbackPrevDayMA(); + if(InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION) + return ShouldOpenLowPullbackDailyMADev(); + return true; +} + +int ParseRoundFromComment(const string &comment) +{ + if(StringFind(comment, "[突破") != 0) + return 0; + const int i_r = StringFind(comment, "-R"); + if(i_r < 0) + return 1; + const int start = i_r + 2; + const int end_br = StringFind(comment, "]", start); + if(end_br <= start) + return 1; + const int r = (int)StringToInteger(StringSubstr(comment, start, end_br - start)); + return (r >= 1) ? r : 1; +} + +bool IsEaBreakoutComment(const string &comment) +{ + return (StringFind(comment, "[突破") == 0); +} + +string FormatBreakoutComment(const int round) +{ + if(round <= 1) + return "[突破]"; + return StringFormat("[突破-R%d]", round); +} + +string FormatPullbackReverseComment(const int round) +{ + if(round <= 1) + return "[突破-回落]"; + return StringFormat("[突破-回落-R%d]", round); +} + +ENUM_ORDER_TYPE OrderTypeOnHighPullbackReverse() +{ + return ORDER_TYPE_SELL; +} + +ENUM_ORDER_TYPE OrderTypeOnLowPullbackReverse() +{ + return ORDER_TYPE_BUY; +} + +void SyncPullbackBarLock(const bool for_high_extreme) +{ + const datetime bar_time = iTime(_Symbol, InpTimeframe, 0); + if(bar_time == 0) + return; + if(for_high_extreme) { + if(bar_time != g_pullback_bar_time_high) { + g_pullback_bar_time_high = bar_time; + g_pullback_opened_on_bar_high = false; + } + } else { + if(bar_time != g_pullback_bar_time_low) { + g_pullback_bar_time_low = bar_time; + g_pullback_opened_on_bar_low = false; + } + } +} + +bool IsPullbackBarOpenAllowed(const bool for_high_extreme) +{ + SyncPullbackBarLock(for_high_extreme); + if(for_high_extreme) + return !g_pullback_opened_on_bar_high; + return !g_pullback_opened_on_bar_low; +} + +void MarkPullbackBarOpened(const bool for_high_extreme) +{ + SyncPullbackBarLock(for_high_extreme); + if(for_high_extreme) + g_pullback_opened_on_bar_high = true; + else + g_pullback_opened_on_bar_low = true; +} + +bool OpenPullbackReversePosition(ENUM_ORDER_TYPE order_type, const bool from_high_extreme, + const double extreme_price) +{ + if(!IsPullbackBarOpenAllowed(from_high_extreme)) + return false; + + const long pos_type = (order_type == ORDER_TYPE_BUY) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; + const double lots = CalculateLotSize(pos_type); + if(lots <= 0.0) + return false; + + if(InpMaxLots > 0.0) { + const double cur_lots = TotalLotsInDirection(pos_type); + if(cur_lots + lots > InpMaxLots + 1e-8) { + if(InpShowDebugInfo) + Print("【回落反向】开仓跳过 - 同向合计将超上限 ", InpMaxLots); + return false; + } + } + + if(!IsSameDirMinSpacingSatisfied(pos_type)) { + if(InpShowDebugInfo) + Print("【回落反向】开仓跳过 - 未达同向最小间距%"); + return false; + } + + const string comment = FormatPullbackReverseComment(g_trade_round); + bool result = false; + if(order_type == ORDER_TYPE_BUY) + result = trade.Buy(lots, _Symbol, 0, 0, 0, comment); + else + result = trade.Sell(lots, _Symbol, 0, 0, 0, comment); + + if(!result) { + Print("【回落反向】开仓失败: ", trade.ResultRetcode(), " - ", + trade.ResultRetcodeDescription()); + return false; + } + + MarkPullbackBarOpened(from_high_extreme); + ApplyLargeLotTakeProfitAfterOpen(pos_type); + Print("【回落反向】开仓成功 ", (order_type == ORDER_TYPE_BUY ? "多" : "空"), + " 手数:", lots, " 极值:", extreme_price, " 备注:", comment); + return true; +} + +void TryArmPullbackWatch() +{ + if(!InpEnablePullbackReverse || !latest_wave.exists) + return; + + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + + if(ask > latest_wave.high_price && !latest_wave.high_pullback_reverse_used) { + if(!g_high_pullback_watch.active || + g_high_pullback_watch.extreme_price != latest_wave.high_price) { + g_high_pullback_watch.active = true; + g_high_pullback_watch.extreme_price = latest_wave.high_price; + g_high_pullback_watch.bars_checked = 0; + if(InpShowDebugInfo) + Print("【回落反向】开始监测破高 极值:", latest_wave.high_price, + " 共", InpPullbackReverseWatchBars, "根K"); + } + } + + if(bid < latest_wave.low_price && !latest_wave.low_pullback_reverse_used) { + if(!g_low_pullback_watch.active || + g_low_pullback_watch.extreme_price != latest_wave.low_price) { + g_low_pullback_watch.active = true; + g_low_pullback_watch.extreme_price = latest_wave.low_price; + g_low_pullback_watch.bars_checked = 0; + if(InpShowDebugInfo) + Print("【回落反向】开始监测破低 极值:", latest_wave.low_price, + " 共", InpPullbackReverseWatchBars, "根K"); + } + } +} + +void ProcessPullbackReverseOnNewBar() +{ + if(!InpEnablePullbackReverse || !latest_wave.exists) + return; + + const int max_bars = MathMax(1, InpPullbackReverseWatchBars); + + if(g_high_pullback_watch.active && !latest_wave.high_pullback_reverse_used) { + const double close1 = iClose(_Symbol, InpTimeframe, 1); + if(close1 > 0.0 && close1 <= g_high_pullback_watch.extreme_price) { + if(InpBreakoutDirection == BREAKOUT_DIR_PREV_DAY_MA && !IsHighPullbackSignalAllowed()) { + if(InpShowDebugInfo) + Print("【回落反向】破高反向跳过 - 前日收线在日MA上方,仅处理低极值回落多"); + latest_wave.high_pullback_reverse_used = true; + } else if(InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION && + !IsHighPullbackSignalAllowed()) { + if(InpShowDebugInfo) + Print("【回落反向】破高反向跳过 - 日MA偏离未超阈值或方向不符 偏离率:", + DoubleToString(GetDailyMADeviationPercent(), 2), "%"); + latest_wave.high_pullback_reverse_used = true; + } else if(OpenPullbackReversePosition(OrderTypeOnHighPullbackReverse(), true, + g_high_pullback_watch.extreme_price)) { + latest_wave.high_pullback_reverse_used = true; + } + g_high_pullback_watch.active = false; + } else { + g_high_pullback_watch.bars_checked++; + if(g_high_pullback_watch.bars_checked >= max_bars) { + if(InpShowDebugInfo) + Print("【回落反向】破高监测结束 未回落极值内"); + g_high_pullback_watch.active = false; + } + } + } + + if(g_low_pullback_watch.active && !latest_wave.low_pullback_reverse_used) { + const double close1 = iClose(_Symbol, InpTimeframe, 1); + if(close1 > 0.0 && close1 >= g_low_pullback_watch.extreme_price) { + if(InpBreakoutDirection == BREAKOUT_DIR_PREV_DAY_MA && !IsLowPullbackSignalAllowed()) { + if(InpShowDebugInfo) + Print("【回落反向】破低反向跳过 - 前日收线在日MA下方,仅处理高极值回落空"); + latest_wave.low_pullback_reverse_used = true; + } else if(InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION && + !IsLowPullbackSignalAllowed()) { + if(InpShowDebugInfo) + Print("【回落反向】破低反向跳过 - 日MA偏离未超阈值或方向不符 偏离率:", + DoubleToString(GetDailyMADeviationPercent(), 2), "%"); + latest_wave.low_pullback_reverse_used = true; + } else if(OpenPullbackReversePosition(OrderTypeOnLowPullbackReverse(), false, + g_low_pullback_watch.extreme_price)) { + latest_wave.low_pullback_reverse_used = true; + } + g_low_pullback_watch.active = false; + } else { + g_low_pullback_watch.bars_checked++; + if(g_low_pullback_watch.bars_checked >= max_bars) { + if(InpShowDebugInfo) + Print("【回落反向】破低监测结束 未回升极值内"); + g_low_pullback_watch.active = false; + } + } + } +} + +void CheckPullbackReverseSignals() +{ + if(!InpEnablePullbackReverse) + return; + + TryArmPullbackWatch(); + + static datetime s_last_bar_time = 0; + const datetime bar_time = iTime(_Symbol, InpTimeframe, 0); + if(bar_time == 0 || bar_time == s_last_bar_time) + return; + s_last_bar_time = bar_time; + + ProcessPullbackReverseOnNewBar(); +} + +void ResetBreakoutWaveEntryFlags() +{ + latest_wave.high_breakout_used = false; + latest_wave.low_breakout_used = false; + latest_wave.high_pullback_reverse_used = false; + latest_wave.low_pullback_reverse_used = false; + g_high_pullback_watch.active = false; + g_low_pullback_watch.active = false; +} + +int CountAllBreakoutPositions() +{ + int count = 0; + const int total = PositionsTotal(); + for(int i = 0; i < total; i++) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + if(!IsEaBreakoutComment(PositionGetString(POSITION_COMMENT))) + continue; + count++; + } + return count; +} + +int CountBreakoutPositions(const long pos_type_filter) +{ + int count = 0; + const int total = PositionsTotal(); + for(int i = 0; i < total; i++) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + const string c = PositionGetString(POSITION_COMMENT); + if(ParseRoundFromComment(c) != g_trade_round) + continue; + if(pos_type_filter >= 0 && PositionGetInteger(POSITION_TYPE) != pos_type_filter) + continue; + count++; + } + return count; +} + +double TotalLotsInDirection(const long pos_type) +{ + double sum = 0.0; + const int total = PositionsTotal(); + for(int i = 0; i < total; i++) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + if(PositionGetInteger(POSITION_TYPE) != pos_type) + continue; + if(ParseRoundFromComment(PositionGetString(POSITION_COMMENT)) != g_trade_round) + continue; + sum += PositionGetDouble(POSITION_VOLUME); + } + return sum; +} + +bool IsSameDirectionTierSlotsFull(const long pos_type) +{ + const int tier = CountBreakoutPositions(pos_type); + if(tier >= g_tier_lots_count) + return true; + const double next_lot = LotForTierIndex(tier); + if(next_lot <= 0.0) + return true; + if(InpMaxLots > 0.0) { + const double cur = TotalLotsInDirection(pos_type); + if(cur >= InpMaxLots - 1e-8) + return true; + if(cur + next_lot > InpMaxLots + 1e-8) + return true; + } + return false; +} + +bool IsDirectionFullActive(const long pos_type) +{ + return IsSameDirectionTierSlotsFull(pos_type); +} + +void CheckDualSideFullCapacity() +{ + const bool buy_full = IsDirectionFullActive(POSITION_TYPE_BUY); + const bool sell_full = IsDirectionFullActive(POSITION_TYPE_SELL); + + if(!buy_full || !sell_full) { + g_dual_full_handled = false; + return; + } + + if(g_dual_full_handled) + return; + + g_dual_full_handled = true; + + const double buy_lots = TotalLotsInDirection(POSITION_TYPE_BUY); + const double sell_lots = TotalLotsInDirection(POSITION_TYPE_SELL); + + if(InpDualFullAction == DUAL_FULL_CLEAR_ALL) { + CloseAllBreakoutPositions( + StringFormat("双向满档清仓 本轮多%.2f空%.2f上限%.2f", buy_lots, sell_lots, InpMaxLots)); + g_trade_round = 1; + ResetBreakoutWaveEntryFlags(); + g_breakout_opened_on_bar_high = false; + g_breakout_opened_on_bar_low = false; + return; + } + + g_trade_round++; + ResetBreakoutWaveEntryFlags(); + g_breakout_opened_on_bar_high = false; + g_breakout_opened_on_bar_low = false; + Print("【突破】双向满档 开始第", g_trade_round, "轮 (旧仓保留,本轮从基准手数重计 多", + DoubleToString(buy_lots, 2), " 空", DoubleToString(sell_lots, 2), " 上限", InpMaxLots, ")"); +} + +long PosTypeOnHighBreakout() +{ + if(InpBreakoutDirection == BREAKOUT_DIR_REVERSE || + InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION) + return POSITION_TYPE_SELL; + return POSITION_TYPE_BUY; +} + +long PosTypeOnLowBreakout() +{ + if(InpBreakoutDirection == BREAKOUT_DIR_REVERSE || + InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION) + return POSITION_TYPE_BUY; + return POSITION_TYPE_SELL; +} + +ENUM_ORDER_TYPE OrderTypeOnHighBreakout() +{ + return (ENUM_ORDER_TYPE)PosTypeOnHighBreakout(); +} + +ENUM_ORDER_TYPE OrderTypeOnLowBreakout() +{ + return (ENUM_ORDER_TYPE)PosTypeOnLowBreakout(); +} + +//+------------------------------------------------------------------+ +//| 突破信号:破极值开仓(每极值点各限1笔,可多笔并存) | +//+------------------------------------------------------------------+ +void CheckBreakoutSignals() +{ + if(!latest_wave.exists) + return; + + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + + if(ask > latest_wave.high_price) { + if(!latest_wave.high_breakout_used && IsBreakoutBarOpenAllowed(true)) { + if(!IsHighBreakoutSignalAllowed()) { + if(InpShowDebugInfo) { + if(InpBreakoutDirection == BREAKOUT_DIR_PREV_DAY_MA) + Print("【突破】破高跳过 - 前日收线在日MA下方,仅处理破低空/高极值回落反向"); + else if(InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION) + Print("【突破】破高跳过 - 日MA偏离未超+", InpDailyMADevThreshold, + "% 当前:", DoubleToString(GetDailyMADeviationPercent(), 2), "%"); + } + latest_wave.high_breakout_used = true; + } else { + ENUM_ORDER_TYPE ot; + if(InpBreakoutDirection == BREAKOUT_DIR_RANDOM) + ot = RandomBreakoutOrderType(); + else + ot = OrderTypeOnHighBreakout(); + if(InpShowDebugInfo) + Print("【突破】破高开仓 ", (ot == ORDER_TYPE_BUY ? "多" : "空"), + " Ask:", ask, " 高点:", latest_wave.high_price); + if(OpenBreakoutPosition(ot, latest_wave.high_price, latest_wave.low_price, true)) + latest_wave.high_breakout_used = true; + } + } + } + + if(bid < latest_wave.low_price) { + if(!latest_wave.low_breakout_used && IsBreakoutBarOpenAllowed(false)) { + if(!IsLowBreakoutSignalAllowed()) { + if(InpShowDebugInfo) { + if(InpBreakoutDirection == BREAKOUT_DIR_PREV_DAY_MA) + Print("【突破】破低跳过 - 前日收线在日MA上方,仅处理破高多/低极值回落反向"); + else if(InpBreakoutDirection == BREAKOUT_DIR_DAILY_MA_DEVIATION) + Print("【突破】破低跳过 - 日MA偏离未超-", InpDailyMADevThreshold, + "% 当前:", DoubleToString(GetDailyMADeviationPercent(), 2), "%"); + } + latest_wave.low_breakout_used = true; + } else { + ENUM_ORDER_TYPE ot; + if(InpBreakoutDirection == BREAKOUT_DIR_RANDOM) + ot = RandomBreakoutOrderType(); + else + ot = OrderTypeOnLowBreakout(); + if(InpShowDebugInfo) + Print("【突破】破低开仓 ", (ot == ORDER_TYPE_BUY ? "多" : "空"), + " Bid:", bid, " 低点:", latest_wave.low_price); + if(OpenBreakoutPosition(ot, latest_wave.high_price, latest_wave.low_price, false)) + latest_wave.low_breakout_used = true; + } + } + } +} + +//+------------------------------------------------------------------+ +//| 手数规范化 | +//+------------------------------------------------------------------+ +double NormalizeVolume(double lots) +{ + const double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + const double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); + const double lot_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); + if(lots < min_lot) + lots = min_lot; + if(lots > max_lot) + lots = max_lot; + if(lot_step > 0.0) + lots = MathFloor(lots / lot_step) * lot_step; + return lots; +} + +bool InitTierLotsFromInput() +{ + ArrayResize(g_tier_lots, 0); + g_tier_lots_count = 0; + + string parts[]; + const int n = StringSplit(InpTierLotsList, ',', parts); + if(n <= 0) + return false; + + for(int i = 0; i < n; i++) { + string token = parts[i]; + StringTrimLeft(token); + StringTrimRight(token); + if(StringLen(token) == 0) + continue; + const double v = StringToDouble(token); + if(v <= 0.0) + continue; + ArrayResize(g_tier_lots, g_tier_lots_count + 1); + g_tier_lots[g_tier_lots_count++] = v; + } + return (g_tier_lots_count > 0); +} + +int GetMaxTierSlots() +{ + return g_tier_lots_count; +} + +double LotForTierIndex(const int tier_index) +{ + if(tier_index < 0 || tier_index >= g_tier_lots_count) + return 0.0; + double vol = g_tier_lots[tier_index]; + if(InpMaxLots > 0.0) + vol = MathMin(vol, InpMaxLots); + return NormalizeVolume(vol); +} + +//+------------------------------------------------------------------+ +//| 按同向已有笔数取下一档手数(tier=0为第1笔) | +//+------------------------------------------------------------------+ +double CalculateLotSize(const long pos_type) +{ + const int tier = CountBreakoutPositions(pos_type); + return LotForTierIndex(tier); +} + +bool GetLastSameDirectionOpenPrice(const long pos_type, double &last_open) +{ + last_open = 0.0; + datetime last_tm = 0; + bool found = false; + const int total = PositionsTotal(); + for(int i = 0; i < total; i++) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + if(PositionGetInteger(POSITION_TYPE) != pos_type) + continue; + if(ParseRoundFromComment(PositionGetString(POSITION_COMMENT)) != g_trade_round) + continue; + const datetime tm = (datetime)PositionGetInteger(POSITION_TIME); + if(!found || tm >= last_tm) { + last_tm = tm; + last_open = PositionGetDouble(POSITION_PRICE_OPEN); + found = true; + } + } + return found; +} + +bool IsSameDirMinSpacingSatisfied(const long pos_type) +{ + if(InpMinSameDirAddSpacingPercent <= 0.0) + return true; + + if(CountBreakoutPositions(pos_type) <= 0) + return true; + + double last_open = 0.0; + if(!GetLastSameDirectionOpenPrice(pos_type, last_open)) + return true; + + const double ratio = InpMinSameDirAddSpacingPercent / 100.0; + if(pos_type == POSITION_TYPE_BUY) { + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double need = last_open * (1.0 - ratio); + return (ask <= need); + } + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + const double need = last_open * (1.0 + ratio); + return (bid >= need); +} + +double TotalBreakoutFloatingPL() +{ + double sum = 0.0; + const int total = PositionsTotal(); + for(int i = 0; i < total; i++) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + if(!IsEaBreakoutComment(PositionGetString(POSITION_COMMENT))) + continue; + sum += PositionGetDouble(POSITION_PROFIT); + sum += PositionGetDouble(POSITION_SWAP); + } + return sum; +} + +bool CloseAllBreakoutPositions(const string reason) +{ + ulong tickets[]; + int n = 0; + const int total = PositionsTotal(); + for(int i = 0; i < total; i++) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + if(!IsEaBreakoutComment(PositionGetString(POSITION_COMMENT))) + continue; + ArrayResize(tickets, n + 1); + tickets[n++] = PositionGetTicket(i); + } + bool ok = true; + for(int j = 0; j < n; j++) { + if(!trade.PositionClose(tickets[j])) + ok = false; + } + if(ok && n > 0) { + g_trade_round = 1; + g_dual_full_handled = false; + ResetBreakoutWaveEntryFlags(); + Print("【突破】全部清仓 笔数=", n, " 原因:", reason); + } + return ok && n > 0; +} + +bool IsLargeLotVolume(const double volume) +{ + return (InpLargeLotMinVolume > 0.0 && volume >= InpLargeLotMinVolume - 1e-8); +} + +double CalcTakeProfitPrice(const long pos_type, const double open_price, const int tp_points) +{ + if(tp_points <= 0) + return 0.0; + + const double tp_off = (double)tp_points * _Point; + double tp = (pos_type == POSITION_TYPE_BUY) ? (open_price + tp_off) : (open_price - tp_off); + + const int stops_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); + if(stops_level > 0) { + const double min_dist = (double)stops_level * _Point; + if(pos_type == POSITION_TYPE_BUY) { + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + if(tp - bid < min_dist) + tp = bid + min_dist; + } else { + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + if(ask - tp < min_dist) + tp = ask - min_dist; + } + } + return NormalizeDouble(tp, _Digits); +} + +bool SetLargeLotTakeProfitOnTicket(const ulong ticket) +{ + if(InpLargeLotMinVolume <= 0.0 || InpLargeLotTakeProfitPoints <= 0) + return true; + if(!PositionSelectByTicket(ticket)) + return false; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + return false; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + return false; + if(!IsEaBreakoutComment(PositionGetString(POSITION_COMMENT))) + return false; + + const double vol = PositionGetDouble(POSITION_VOLUME); + if(!IsLargeLotVolume(vol)) + return true; + + const long pos_type = PositionGetInteger(POSITION_TYPE); + const double open = PositionGetDouble(POSITION_PRICE_OPEN); + const double tp = CalcTakeProfitPrice(pos_type, open, InpLargeLotTakeProfitPoints); + if(tp <= 0.0) + return false; + + const double cur_tp = PositionGetDouble(POSITION_TP); + if(MathAbs(cur_tp - tp) <= _Point * 0.5) + return true; + + const double sl = PositionGetDouble(POSITION_SL); + if(!trade.PositionModify(ticket, sl, tp)) { + Print("【大单止盈】设置TP失败 Ticket:", ticket, " 手数:", vol, + " TP:", tp, " 错误:", trade.ResultRetcodeDescription()); + return false; + } + Print("【大单止盈】已设TP Ticket:", ticket, " 手数:", vol, + " 开仓:", open, " 止盈:", tp, " (", InpLargeLotTakeProfitPoints, "点,不参与统盈)"); + return true; +} + +bool ApplyLargeLotTakeProfitAfterOpen(const long pos_type) +{ + if(InpLargeLotMinVolume <= 0.0 || InpLargeLotTakeProfitPoints <= 0) + return true; + + ulong best_ticket = 0; + datetime best_time = 0; + const int total = PositionsTotal(); + for(int i = 0; i < total; i++) { + const ulong ticket = PositionGetTicket(i); + if(!PositionSelectByTicket(ticket)) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + if(PositionGetInteger(POSITION_TYPE) != pos_type) + continue; + if(ParseRoundFromComment(PositionGetString(POSITION_COMMENT)) != g_trade_round) + continue; + if(!IsEaBreakoutComment(PositionGetString(POSITION_COMMENT))) + continue; + const datetime tm = (datetime)PositionGetInteger(POSITION_TIME); + if(tm >= best_time) { + best_time = tm; + best_ticket = ticket; + } + } + if(best_ticket == 0) + return false; + return SetLargeLotTakeProfitOnTicket(best_ticket); +} + +void CheckLargeLotTakeProfit() +{ + if(InpLargeLotMinVolume <= 0.0 || InpLargeLotTakeProfitPoints <= 0) + return; + + const double tp_off = (double)InpLargeLotTakeProfitPoints * _Point; + const int total = PositionsTotal(); + for(int i = total - 1; i >= 0; i--) { + const ulong ticket = PositionGetTicket(i); + if(!PositionSelectByTicket(ticket)) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + if(!IsEaBreakoutComment(PositionGetString(POSITION_COMMENT))) + continue; + + const double vol = PositionGetDouble(POSITION_VOLUME); + if(!IsLargeLotVolume(vol)) + continue; + + SetLargeLotTakeProfitOnTicket(ticket); + + const long pos_type = PositionGetInteger(POSITION_TYPE); + const double open = PositionGetDouble(POSITION_PRICE_OPEN); + bool hit = false; + if(pos_type == POSITION_TYPE_BUY) { + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + hit = (bid >= open + tp_off - 1e-12); + } else { + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + hit = (ask <= open - tp_off + 1e-12); + } + if(!hit) + continue; + + const int round_id = ParseRoundFromComment(PositionGetString(POSITION_COMMENT)); + if(trade.PositionClose(ticket)) { + Print("【大单止盈】平仓 Ticket:", ticket, " 手数:", vol, + " 轮次:", round_id, " 开仓:", open, " 止盈点数:", InpLargeLotTakeProfitPoints, + " (剩余持仓重计马丁档/统盈均价)"); + if(round_id == g_trade_round) + g_dual_full_handled = false; + } + } +} + +void CollectAllTradeRounds(int &rounds[], int &n_rounds) +{ + n_rounds = 0; + ArrayResize(rounds, 0); + const int total = PositionsTotal(); + for(int i = 0; i < total; i++) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + const int r = ParseRoundFromComment(PositionGetString(POSITION_COMMENT)); + if(r < 1) + continue; + bool found = false; + for(int j = 0; j < n_rounds; j++) { + if(rounds[j] == r) { + found = true; + break; + } + } + if(!found) { + ArrayResize(rounds, n_rounds + 1); + rounds[n_rounds++] = r; + } + } +} + +bool BatchRoundAvgPrice(const int round_id, const long pos_type, double &avg, double &sum_lots) +{ + avg = 0.0; + sum_lots = 0.0; + double sum_px_vol = 0.0; + const int total = PositionsTotal(); + for(int i = 0; i < total; i++) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + if(PositionGetInteger(POSITION_TYPE) != pos_type) + continue; + if(ParseRoundFromComment(PositionGetString(POSITION_COMMENT)) != round_id) + continue; + const double v = PositionGetDouble(POSITION_VOLUME); + if(IsLargeLotVolume(v)) + continue; + const double p = PositionGetDouble(POSITION_PRICE_OPEN); + sum_px_vol += p * v; + sum_lots += v; + } + if(sum_lots <= 1e-12) + return false; + avg = sum_px_vol / sum_lots; + return true; +} + +bool CloseBreakoutRoundBatch(const int round_id, const long pos_type, const string reason) +{ + ulong tickets[]; + int n = 0; + const int total = PositionsTotal(); + for(int i = 0; i < total; i++) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + if(PositionGetInteger(POSITION_TYPE) != pos_type) + continue; + if(ParseRoundFromComment(PositionGetString(POSITION_COMMENT)) != round_id) + continue; + ArrayResize(tickets, n + 1); + tickets[n++] = PositionGetTicket(i); + } + if(n <= 0) + return false; + + bool ok = true; + for(int j = 0; j < n; j++) { + if(!trade.PositionClose(tickets[j])) + ok = false; + } + if(ok) { + Print("【突破】", (pos_type == POSITION_TYPE_BUY ? "多单" : "空单"), + " 第", round_id, "轮批平仓 笔数=", n, " 原因:", reason); + if(round_id == g_trade_round && CountBreakoutPositions(pos_type) == 0) + g_dual_full_handled = false; + } + return ok; +} + +void CheckBreakoutBatchTakeProfitForRoundDir(const int round_id, const long pos_type) +{ + double avg = 0.0, sum_lots = 0.0; + if(!BatchRoundAvgPrice(round_id, pos_type, avg, sum_lots)) + return; + + const double tp_off = (double)InpBatchTakeProfitPoints * _Point; + bool hit = false; + + if(pos_type == POSITION_TYPE_BUY) { + const double px = SymbolInfoDouble(_Symbol, SYMBOL_BID); + hit = (px >= avg + tp_off); + } else { + const double px = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + hit = (px <= avg - tp_off); + } + + if(!hit) + return; + + const double target = (pos_type == POSITION_TYPE_BUY) ? (avg + tp_off) : (avg - tp_off); + CloseBreakoutRoundBatch(round_id, pos_type, + StringFormat("统盈 均价%s+%d点 目标%s", + DoubleToString(avg, _Digits), InpBatchTakeProfitPoints, + DoubleToString(target, _Digits))); +} + +void CheckBreakoutBatchTakeProfit() +{ + if(InpBatchTakeProfitPoints <= 0) + return; + + int rounds[]; + int n_rounds = 0; + CollectAllTradeRounds(rounds, n_rounds); + + for(int i = 0; i < n_rounds; i++) { + CheckBreakoutBatchTakeProfitForRoundDir(rounds[i], POSITION_TYPE_BUY); + CheckBreakoutBatchTakeProfitForRoundDir(rounds[i], POSITION_TYPE_SELL); + } +} + +void CheckBreakoutBalanceStopLoss() +{ + if(InpStopLossBalancePct <= 0.0) + return; + + if(CountAllBreakoutPositions() == 0) + return; + + const double balance = AccountInfoDouble(ACCOUNT_BALANCE); + if(balance <= 0.0) + return; + + const double fpl = TotalBreakoutFloatingPL(); + const double sl_need = balance * (InpStopLossBalancePct / 100.0); + if(fpl <= -sl_need) + CloseAllBreakoutPositions( + StringFormat("止损:浮亏%.2f达结余%.2f%%", fpl, InpStopLossBalancePct)); +} + +void SyncBreakoutBarLock(const bool for_high_extreme) +{ + const datetime bar_time = iTime(_Symbol, InpTimeframe, 0); + if(bar_time == 0) + return; + + if(for_high_extreme) { + if(bar_time != g_breakout_bar_time_high) { + g_breakout_bar_time_high = bar_time; + g_breakout_opened_on_bar_high = false; + } + } else { + if(bar_time != g_breakout_bar_time_low) { + g_breakout_bar_time_low = bar_time; + g_breakout_opened_on_bar_low = false; + } + } +} + +bool IsBreakoutBarOpenAllowed(const bool for_high_extreme) +{ + SyncBreakoutBarLock(for_high_extreme); + if(for_high_extreme) + return !g_breakout_opened_on_bar_high; + return !g_breakout_opened_on_bar_low; +} + +void MarkBreakoutBarOpened(const bool for_high_extreme) +{ + SyncBreakoutBarLock(for_high_extreme); + if(for_high_extreme) + g_breakout_opened_on_bar_high = true; + else + g_breakout_opened_on_bar_low = true; +} + +bool OpenBreakoutPosition(ENUM_ORDER_TYPE order_type, double wave_high, double wave_low, + const bool from_high_extreme) +{ + if(!IsBreakoutBarOpenAllowed(from_high_extreme)) + return false; + + const long pos_type = (order_type == ORDER_TYPE_BUY) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; + const double lots = CalculateLotSize(pos_type); + if(lots <= 0.0) + return false; + + if(lots <= 0.0) { + Print("【突破】开仓跳过 - 同向已达最大档数 ", g_tier_lots_count); + return false; + } + if(InpMaxLots > 0.0) { + const double cur_lots = TotalLotsInDirection(pos_type); + if(cur_lots + lots > InpMaxLots + 1e-8) { + Print("【突破】开仓跳过 - 同向合计将超上限 ", InpMaxLots, + " 当前:", cur_lots, " 拟开:", lots); + return false; + } + } + + if(!IsSameDirMinSpacingSatisfied(pos_type)) { + if(InpShowDebugInfo) { + double last_open = 0.0; + GetLastSameDirectionOpenPrice(pos_type, last_open); + Print("【突破】开仓跳过 - 未达同向最小间距% ", InpMinSameDirAddSpacingPercent, + " 上一笔:", last_open, " 当前:", + (pos_type == POSITION_TYPE_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) + : SymbolInfoDouble(_Symbol, SYMBOL_BID))); + } + return false; + } + + const string comment = FormatBreakoutComment(g_trade_round); + bool result = false; + if(order_type == ORDER_TYPE_BUY) + result = trade.Buy(lots, _Symbol, 0, 0, 0, comment); + else + result = trade.Sell(lots, _Symbol, 0, 0, 0, comment); + + if(!result) { + Print("【突破】开仓失败: ", trade.ResultRetcode(), " - ", trade.ResultRetcodeDescription()); + return false; + } + + MarkBreakoutBarOpened(from_high_extreme); + ApplyLargeLotTakeProfitAfterOpen(pos_type); + const int tier = CountBreakoutPositions(pos_type); + Print("【突破】开仓成功 ", (order_type == ORDER_TYPE_BUY ? "多" : "空"), + " 手数:", lots, " 轮次:", g_trade_round, " 同向第", tier, "/", g_tier_lots_count, "档", + " 备注:", comment, " 波段 H:", wave_high, " L:", wave_low); + return true; +} + +//+------------------------------------------------------------------+ +//| 检查并关闭手工单 | +//+------------------------------------------------------------------+ +void CheckAndCloseManualOrders() +{ + for(int i = PositionsTotal() - 1; i >= 0; i--) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + + // 只处理本品种 + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + + // 检查magic number:0表示手工单 + long magic = PositionGetInteger(POSITION_MAGIC); + if(magic == 0) { + ulong ticket = PositionGetTicket(i); + + // 平仓手工单 + if(trade.PositionClose(ticket)) { + Print("【禁止手工单】已平掉手工单 - Ticket:", ticket, + " 类型:", (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? "多单" : "空单")); + } else { + Print("【禁止手工单】平仓失败 - Ticket:", ticket, " 错误:", GetLastError()); + } + } + } +} + +//+------------------------------------------------------------------+ +//| 成交事件(大单被平台TP平仓时重置满档状态) | +//+------------------------------------------------------------------+ +void OnTradeTransaction(const MqlTradeTransaction& trans, + const MqlTradeRequest& request, + const MqlTradeResult& result) +{ + if(trans.type != TRADE_TRANSACTION_DEAL_ADD) + return; + if(!HistoryDealSelect(trans.deal)) + return; + if(HistoryDealGetString(trans.deal, DEAL_SYMBOL) != _Symbol) + return; + if(HistoryDealGetInteger(trans.deal, DEAL_MAGIC) != InpMagicNumber) + return; + if(HistoryDealGetInteger(trans.deal, DEAL_ENTRY) != DEAL_ENTRY_OUT) + return; + if(HistoryDealGetInteger(trans.deal, DEAL_REASON) != DEAL_REASON_TP) + return; + + const string comment = HistoryDealGetString(trans.deal, DEAL_COMMENT); + if(!IsEaBreakoutComment(comment)) + return; + + const double vol = HistoryDealGetDouble(trans.deal, DEAL_VOLUME); + if(!IsLargeLotVolume(vol)) + return; + + const int round_id = ParseRoundFromComment(comment); + if(round_id == g_trade_round) + g_dual_full_handled = false; + + Print("【大单止盈】成交平仓 手数:", vol, " 轮次:", round_id, + " 备注:", comment, " (剩余持仓重计马丁档/统盈均价)"); +} diff --git a/20260522-突破马丁/README.md b/20260522-突破马丁/README.md new file mode 100644 index 0000000..9ead0db --- /dev/null +++ b/20260522-突破马丁/README.md @@ -0,0 +1,57 @@ +# 突破策略(极值点突破开仓) + +MQL5 EA:`20260522_Breakout.mq5`(v3.18)。 + +## 参数默认值(与输入界面一致) + +| 分组 | 参数 | 默认 | +|------|------|------| +| 波段 | K线周期 / MA周期 | M1 / 14 | +| 波段 | 最小/最大波段% | 0.1 / **0.25** | +| 波段 | 有效波段最少K线 | **3** | +| 波段 | 反向突破容忍度% | **0.05** | +| 交易 | 开仓方向 | **随机**(可选:顺势 / 反向 / 前日收线相对日MA / **日MA偏离率反向**) | +| 交易 | 日MA偏离率阈值% | **1.0**(\|偏离\|超过才触发,0=关) | +| 交易 | 按批统盈点数 / 止损% | **300** / **30** | +| 交易 | 大单止盈阈值 / 点数 | **1.0手** / **500点** | +| 交易 | 回落反向 / 监测K线数 | **开** / **3** | +| 仓位 | 各档手数(19档) | 见下 | +| 仓位 | 同向最大合计 / 最小间距% / 满档后 | 99 / **0** / **清仓** | +| 调试 | 调试 / 极值标记 / Magic / 禁手工单 | false / true / 20260520 / true | + +**各档手数:** `0.05,0.05,0.05,0.8,0.8,1.1,1,2,2,1,1,1,2,2,1,1,1,5,5`(19 档) + +## 前日收线相对日MA(开仓方向选项) + +取**前一交易日**日 K **收盘价**与**日 K 上同周期 MA**(默认 MA14)比较: + +| 前日收线位置 | 允许开仓 | +|-------------|----------| +| **在日 MA 下方** | 破**低**极值 → 空;**高**极值出现回落反向 → 空 | +| **在日 MA 上方** | 破**高**极值 → 多;**低**极值出现回落反向 → 多 | + +- 偏空时不做「破高」突破单,等高极值**回落反向**或等破低;偏多时不做「破低」突破单,等低极值**回落反向**或等破高。 +- 前日收线恰在 MA 上(中性)时,该模式不开仓。 + +## 日 MA 偏离率反向(开仓方向选项) + +**偏离率** = (当前价 − 日 MA) ÷ 日 MA × 100%(当前价取买卖中间价,日 MA 周期同 `InpMAPeriod`)。 + +| 偏离率 | 含义 | 允许开仓(反向/均值回归) | +|--------|------|--------------------------| +| **> +阈值%** | 价显著高于日 MA | 破**高** → 空;**高**极值回落反向 → 空 | +| **< −阈值%** | 价显著低于日 MA | 破**低** → 多;**低**极值回落反向 → 多 | +| \|偏离\| ≤ 阈值 | 未超阈值 | **不开仓** | + +参数 `日MA偏离率阈值` 默认 **1.0%**,设为 **0** 关闭此逻辑。 + +## 大单独立止盈 + +- 开仓手数 **≥ 大单止盈阈值**(默认 1 手)的单子,按 **大单最大止盈点数** 单独止盈,**不参与按批统盈**的加权均价计算。 +- 大单止盈平仓后(平台 TP 或 EA 监测触发),**下一档马丁手数**与**统盈均价**均仅按**当前仍持仓**的单子重算,与已平大单无关。 +- 阈值或点数设为 **0** 时关闭此功能,所有单子统一走按批统盈。 + +## 说明 + +- 突破开仓与回落反向共用档位手数;列表长度 = 同向最多开仓笔数。 +- 按批统盈、结余%止损、双向满档逻辑见 EA 输入注释。 diff --git a/20260520-突破马丁/XAU_1min_300000.set b/20260522-突破马丁/XAU_1min_300000.set similarity index 100% rename from 20260520-突破马丁/XAU_1min_300000.set rename to 20260522-突破马丁/XAU_1min_300000.set diff --git a/20260522.png b/20260522.png new file mode 100644 index 0000000..68bf7a6 Binary files /dev/null and b/20260522.png differ diff --git a/20260523.jpg b/20260523.jpg new file mode 100644 index 0000000..27c06fa Binary files /dev/null and b/20260523.jpg differ diff --git a/20260524.png b/20260524.png new file mode 100644 index 0000000..a7cd0eb Binary files /dev/null and b/20260524.png differ diff --git a/EXNESS链接 (10).txt b/EXNESS链接 (10).txt deleted file mode 100644 index e9ace60..0000000 --- a/EXNESS链接 (10).txt +++ /dev/null @@ -1,81 +0,0 @@ -EXNESS平台自动反用到帐时间:平仓后的第二天晚上十二点之前会自动反 用到交易帐号里面,请注意查收! - - -2026年EXNESS自动日反开户链接:(可开代理) - -https://one.exnessonelink.com/intl/zh/a/xiao2 - -合作伙伴代码:xiao2 - -EXNESS的后台又升级了,用这个链接开出来的帐号不需要人工上报申请日反了,是平台直接自动日反到帐号。 - - - - - - -2026年以前老代理名下客户注册链接: - -https://www.extraders.market/pa/a/10795827 - -https://www.extraders.direct/pa/a/10795827 - -https://www.extraders.direct/a/10795827 - -https://www.extraders.asia/a/10795827 - -https://www.ex-markets.guide/a/10795827 - -https://www.ex-markets.pro/a/10795827 - -https://www.extrading.pro/a/10795827 - -https://www.ex-markets.direct/a/10795827 - -https://www.ex-markets.digital/a/10795827 - -https://one.exness-track.com/a/10795827 - -https://www.extrading.markets/a/10795827 - -https://www.ex-markets.expert/a/10795827 - -https://one.exnesstrack.org/boarding/sign-up/a/10795827 - -注册时请填写合作伙伴代码:10795827 - -一定要记得打开链接之后第一时间去注册,这个很重要 - - -EXNESS开户需要:身份证正面和反面照片,邮箱,手机号 - -打开链接后,请直接点击:开立帐户 - -关于内转: - -第一是,平台不允许我们这样做 - -第二是,我们也被骗过 - -第三,为了客户的资金安全,我们也不会和任何客户发现任何金钱往来 - -注意:谁先付款都有隐患,因为没有第三方担保。所以我们一般都会建议客户直接通过平台进行出入米,客户和平台之间有金钱往来,出了问题,平台还可以去查,去处理。 - - - -EXNESS代理: -1.您和您下面客户开户,都需要找我拿开户链接(只能用我们的开户链接开户) - -2.您客户开完户之后,需要把资料发给我查询是否在我们代理下,同步把客户的交易帐号绑定到您的代理返佣后台(请在第一时间去绑定,以免影响到您的返佣) - -3.您本人的交易帐号是平台自动日返到交易帐号,您下面客户的返佣是周返到您的指定交易帐号(需绑定代理返佣后台) - -注:开户之后请第一时间发帐号给我核查代理归属,并上报平台反用,谢谢 - - -关于EXNESS平台情况说明:身为代理,我们只提供基础服务:链接和返佣 - - 任何平台方面的问题(入金,出金,品种,交易,服务器等等),请客户自己联系平台客服处理,因为平台现在提供的服务人员只有平台客服,没有其它,全球所有的客户都是一样的,只能自己联系平台客服去处理问题,我们这么大一个代理,有事儿找平台也是联系平台客服进行处理! - -(不要说什么我是在你这儿开的户,出了问题,你就要处理的这种话,我们只是代理,但是代表不了平台。我们重来没有强迫任何客户去开户,开户是你自己自愿开的,你不开也没人强迫你开,交易是你自己操作的,我们重来不会干预客户的任何交易,不闻,不问,不管,不干预!你来找我要链接,我就给,你来找我处理反用,我就上报,没有其它更多的了) - diff --git a/20260520-突破马丁/20260520_Breakout.mq5 b/KVB-50036106/20260522-突破马丁/20260522_Breakout.mq5 similarity index 100% rename from 20260520-突破马丁/20260520_Breakout.mq5 rename to KVB-50036106/20260522-突破马丁/20260522_Breakout.mq5 diff --git a/20260520-突破马丁/README.md b/KVB-50036106/20260522-突破马丁/README.md similarity index 100% rename from 20260520-突破马丁/README.md rename to KVB-50036106/20260522-突破马丁/README.md diff --git a/20260520-突破马丁/XAU_1min_v1.set b/KVB-50036106/20260522-突破马丁/XAU_1min_300000.set similarity index 58% rename from 20260520-突破马丁/XAU_1min_v1.set rename to KVB-50036106/20260522-突破马丁/XAU_1min_300000.set index ac0538d..1f40d2d 100644 Binary files a/20260520-突破马丁/XAU_1min_v1.set and b/KVB-50036106/20260522-突破马丁/XAU_1min_300000.set differ diff --git a/KVB-50036106/TwisterPro Scalper 1.8 500$.set b/KVB-50036106/TwisterPro Scalper 1.8 500$.set new file mode 100644 index 0000000..e8aa0bd Binary files /dev/null and b/KVB-50036106/TwisterPro Scalper 1.8 500$.set differ diff --git a/KVB-50036106/TwisterPro Scalper 1.8.ex5 b/KVB-50036106/TwisterPro Scalper 1.8.ex5 new file mode 100644 index 0000000..f9dafff Binary files /dev/null and b/KVB-50036106/TwisterPro Scalper 1.8.ex5 differ diff --git a/tag/20260425-马丁网格策略/20260425.ea.mq5 b/tag/20260425-马丁网格策略/20260425.ea.mq5 deleted file mode 100644 index b5f333e..0000000 --- a/tag/20260425-马丁网格策略/20260425.ea.mq5 +++ /dev/null @@ -1,804 +0,0 @@ -//+------------------------------------------------------------------+ -//| 20260425.ea.mq5 | -//| 马丁网格:小周期震荡、加仓后整体盈利平仓;K 线周期可独立于图表 | -//+------------------------------------------------------------------+ -#property copyright "EA-2026" -#property link "" -#property version "1.11" -#property description "Martin grid: TP/统盈/加仓间距=%% of ref price; max orders/side; no K-timeout close (v1.11+)" - -#include -#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 deleted file mode 100644 index 8262e31..0000000 --- a/tag/20260425-马丁网格策略/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# 马丁网格策略 - -- **概念**:`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**(否则或拒单、或只平部分单),故用图上列表作对照。 diff --git a/马丁策略/README.md b/马丁策略/README.md deleted file mode 100644 index ed56ee8..0000000 --- a/马丁策略/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# 马丁网格策略 - -## 基础概念 -- 平台: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 deleted file mode 100644 index ea6091f..0000000 --- a/马丁策略/zyb.mading.mq5 +++ /dev/null @@ -1,562 +0,0 @@ -//+------------------------------------------------------------------+ -//| 马丁网格策略 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(); -} - -//+------------------------------------------------------------------+