diff --git a/20260519-突破策略/20260519_Breakout.mq5 b/20260519-突破策略/20260519_Breakout.mq5 index 592b065..d2b1615 100644 --- a/20260519-突破策略/20260519_Breakout.mq5 +++ b/20260519-突破策略/20260519_Breakout.mq5 @@ -35,6 +35,14 @@ 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; // 显示极值点标记 @@ -72,6 +80,22 @@ struct ExtremePoint { 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[]); @@ -86,6 +110,12 @@ void ManagePositions(); void CheckTrailingStop(ulong ticket); void CheckAndCloseManualOrders(); +// 补偿机制函数声明 +void AddToCompensationQueue(ulong ticket); +bool OpenCompensationOrder(); +void CheckCompensationOrderStatus(); +void ClearCompensationQueue(); + //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ @@ -132,6 +162,14 @@ int OnInit() 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); @@ -195,31 +233,65 @@ void OnTradeTransaction(const MqlTradeTransaction& trans, double swap = HistoryDealGetDouble(deal_ticket, DEAL_SWAP); double net_profit = profit + commission + swap; - // 判断是亏损还是盈利 - if(net_profit < 0) + // 获取持仓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) { - // 亏损:增加计数器 - consecutive_loss_count++; - Print("【连续亏损保护】亏损 ", consecutive_loss_count, "/", InpConsecutiveLosses, - " | 净亏损: $", DoubleToString(net_profit, 2)); + // 检查是否是补偿单(补偿单不计入连续亏损统计) + string comment = HistoryDealGetString(deal_ticket, DEAL_COMMENT); + bool is_compensation = (StringFind(comment, "[补偿单]") >= 0); - // 检查是否达到冷冻阈值 - if(consecutive_loss_count >= InpConsecutiveLosses) + if(!is_compensation) { - // 进入冷冻期 - freeze_bar_index = Bars(_Symbol, InpTimeframe) + InpFreezeBarCount; - freeze_until_time = TimeCurrent(); + // 只有正常单亏损才增加计数器 + consecutive_loss_count++; + Print("【连续亏损保护】正常单亏损 ", consecutive_loss_count, "/", InpConsecutiveLosses, + " | 净亏损: $", DoubleToString(net_profit, 2)); - Print("!!! 触发交易冷冻 !!! 冷冻", InpFreezeBarCount, "根K线"); + // 检查是否达到冷冻阈值 + 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(net_profit > 0) + else if(reason == DEAL_REASON_TP) { - // 盈利:重置计数器 - if(consecutive_loss_count > 0) + // 检查是否是补偿单 + string comment = HistoryDealGetString(deal_ticket, DEAL_COMMENT); + bool is_compensation = (StringFind(comment, "[补偿单]") >= 0); + + if(!is_compensation) { - Print("【连续亏损保护】盈利 - 计数器重置: ", consecutive_loss_count, " → 0 | 净盈利: $", DoubleToString(net_profit, 2)); - consecutive_loss_count = 0; + // 只有正常单盈利才重置计数器 + if(consecutive_loss_count > 0) + { + Print("【连续亏损保护】正常单止盈 - 计数器重置: ", consecutive_loss_count, " → 0 | 净盈利: $", DoubleToString(net_profit, 2)); + consecutive_loss_count = 0; + } + } + else + { + Print("【补偿机制】补偿单止盈 - 不影响连续亏损计数器 | 净盈利: $", DoubleToString(net_profit, 2)); } } } @@ -233,13 +305,17 @@ void OnTick() if(InpCloseManualOrders) CheckAndCloseManualOrders(); - // 2. 更新最新有效波段 + // 2. 检查补偿单状态(优先处理补偿机制) + if(InpEnableCompensation) + CheckCompensationOrderStatus(); + + // 3. 更新最新有效波段 UpdateLatestValidWave(); - // 3. 检查开仓信号 + // 4. 检查开仓信号 CheckOpenSignals(); - // 4. 管理已有持仓 + // 5. 管理已有持仓 ManagePositions(); } @@ -742,10 +818,8 @@ bool OpenPosition(ENUM_ORDER_TYPE order_type, double wave_high, double wave_low, if(lots <= 0) return false; - // 生成备注信息:盈亏比和最小止损点数 - string comment = StringFormat("R%.1f SL%d", - InpRiskRewardRatio, - InpMinStopLossPoints); + // 生成备注信息:盈亏比(止损点数会在设置SL后从历史记录计算) + string comment = StringFormat("R%.1f", InpRiskRewardRatio); bool result = false; @@ -940,6 +1014,11 @@ void CheckTrailingStop(ulong ticket) 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); @@ -1013,3 +1092,464 @@ void CheckAndCloseManualOrders() } } } + +//+------------------------------------------------------------------+ +//| 将亏损单加入补偿队列 | +//+------------------------------------------------------------------+ +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-突破策略/XAU_1H.set b/20260519-突破策略/XAU_1H.set deleted file mode 100644 index 8e715d6..0000000 Binary files a/20260519-突破策略/XAU_1H.set and /dev/null differ diff --git a/20260519-突破策略/XAU_1min.set b/20260519-突破策略/XAU_1min_v1.set similarity index 61% rename from 20260519-突破策略/XAU_1min.set rename to 20260519-突破策略/XAU_1min_v1.set index 69070d1..6408a79 100644 Binary files a/20260519-突破策略/XAU_1min.set and b/20260519-突破策略/XAU_1min_v1.set differ diff --git a/20260519-突破策略/XAU_1min_v2.set b/20260519-突破策略/XAU_1min_v2.set deleted file mode 100644 index cffb218..0000000 Binary files a/20260519-突破策略/XAU_1min_v2.set and /dev/null differ diff --git a/20260519-突破策略/xau_1min_0.05.set b/20260519-突破策略/xau_1min_0.05.set deleted file mode 100644 index 74ff9ac..0000000 Binary files a/20260519-突破策略/xau_1min_0.05.set and /dev/null differ diff --git a/20260520-突破马丁/20260520_Breakout.mq5 b/20260520-突破马丁/20260520_Breakout.mq5 new file mode 100644 index 0000000..341cb37 --- /dev/null +++ b/20260520-突破马丁/20260520_Breakout.mq5 @@ -0,0 +1,1110 @@ +//+------------------------------------------------------------------+ +//| 20260520_Breakout.mq5 | +//| 突破策略 - 单笔突破开仓 | +//+------------------------------------------------------------------+ +#property copyright "Breakout Strategy" +#property version "3.07" +#property strict + +#include + +// 破高/破低开仓方向 +enum ENUM_BREAKOUT_DIRECTION { + BREAKOUT_DIR_FOLLOW = 0, // 顺势(破高多/破低空) + BREAKOUT_DIR_REVERSE = 1, // 反向(破高空/破低多) + BREAKOUT_DIR_RANDOM = 2 // 随机(每次突破独立随机) +}; + +// 当前轮多空同向均达满档后的处理 +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 = 1.0; // 最大波段阈值百分比(%) +input double InpPullbackTolerance = 0.05; // 反向突破容忍度(%) 0=不容忍 + +input group "=== 突破交易参数 ===" +input ENUM_BREAKOUT_DIRECTION InpBreakoutDirection = BREAKOUT_DIR_FOLLOW; // 开仓方向 +input double InpTakeProfitBalancePct = 1.0; // 止盈:持仓浮盈达结余比例(%)全部清仓,0=关 +input double InpStopLossBalancePct = 30.0; // 止损:持仓浮亏达结余比例(%)全部清仓,0=关 + +input group "=== 仓位管理参数 ===" +input double InpFixedLots = 0.5; // 基准手数(同向首笔/每组首笔) +input double InpMartingaleAddLotBoost = 2.0; // 同向加码:每组+手数(0=始终基准) +input int InpMartingaleTierInterval = 5; // 同向加码:档间隔N(每N笔为一组) +input double InpMaxLots = 30.0; // 同向最大合计手数 +input double InpMinSameDirAddSpacingPercent = 0.1; // 同向最小开仓间距(%) 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指标句柄 +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次) +}; + +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; // 本轮双向满档是否已处理 + +// 极值点结构体定义 +struct ExtremePoint { + datetime time; + double price; + int type; + bool is_valid; +}; + +// 函数声明 +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[]); +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); +double LotForSameDirectionTier(const int tier_index, const double base_lot); +double CalculateLotSize(const long pos_type); +long PosTypeOnHighBreakout(); +long PosTypeOnLowBreakout(); +void CheckBreakoutSignals(); +ENUM_ORDER_TYPE OrderTypeOnHighBreakout(); +ENUM_ORDER_TYPE OrderTypeOnLowBreakout(); +void CheckAndCloseManualOrders(); +double TotalBreakoutFloatingPL(); +bool CloseAllBreakoutPositions(const string reason); +void CheckBreakoutBalanceExit(); +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); + +//+------------------------------------------------------------------+ +//| 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_breakout_used = false; + latest_wave.low_breakout_used = false; + + MathSrand((int)(TimeLocal() ^ GetTickCount())); + + g_trade_round = 1; + g_dual_full_handled = 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% (禁用)"); + if(InpBreakoutDirection == BREAKOUT_DIR_FOLLOW) + Print("开仓方向: 顺势(破高多/破低空)"); + else if(InpBreakoutDirection == BREAKOUT_DIR_REVERSE) + Print("开仓方向: 反向(破高空/破低多)"); + else + Print("开仓方向: 随机(每次突破独立随机)"); + Print("开仓限制: 每个极值点(破高/破低)各最多1笔,可多笔并存"); + Print("止盈(结余%):", InpTakeProfitBalancePct, " (0=关闭,达标全部清仓)"); + Print("止损(结余%):", InpStopLossBalancePct, " (0=关闭,达标全部清仓)"); + Print("同向手数: 基准=", InpFixedLots, " 档间隔=", InpMartingaleTierInterval, + " 每组+", InpMartingaleAddLotBoost, " 同向上限=", InpMaxLots); + 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); + + // 删除所有标记 + ObjectsDeleteAll(0, "ValidWave_"); + + Print("突破EA已卸载"); +} + +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() +{ + // 1. 检查并关闭手工单(如果启用) + if(InpCloseManualOrders) + CheckAndCloseManualOrders(); + + // 2. 更新最新有效波段 + UpdateLatestValidWave(); + + // 3. 结余比例止盈/止损(全部持仓,含历史轮) + CheckBreakoutBalanceExit(); + + // 4. 当前轮双向同向均满档 → 清仓或开新轮 + CheckDualSideFullCapacity(); + + // 5. 突破信号(破极值开仓,每极值点各限1笔) + CheckBreakoutSignals(); +} + +//+------------------------------------------------------------------+ +//| 更新最新有效波段 | +//+------------------------------------------------------------------+ +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) { + + 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; + if(low != prev_low) + latest_wave.low_breakout_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; + } + } +} + +ENUM_ORDER_TYPE RandomBreakoutOrderType() +{ + return ((MathRand() & 1) != 0) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; +} + +int ParseRoundFromComment(const string &comment) +{ + const string prefix = "[突破-R"; + const int i = StringFind(comment, prefix); + if(i == 0) { + const int start = i + StringLen(prefix); + const int end_br = StringFind(comment, "]", start); + if(end_br > start) + return (int)StringToInteger(StringSubstr(comment, start, end_br - start)); + } + if(StringFind(comment, "[突破]") == 0) + return 1; + return 0; +} + +bool IsEaBreakoutComment(const string &comment) +{ + return (ParseRoundFromComment(comment) > 0); +} + +string FormatBreakoutComment(const int round) +{ + if(round <= 1) + return "[突破]"; + return StringFormat("[突破-R%d]", round); +} + +void ResetBreakoutWaveEntryFlags() +{ + latest_wave.high_breakout_used = false; + latest_wave.low_breakout_used = 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 IsDirectionFullActive(const long pos_type) +{ + const double cur = TotalLotsInDirection(pos_type); + if(cur >= InpMaxLots - 1e-8) + return true; + // 按下一笔实际加码手数判断(非仅基准手): 如19.5手+下一笔4.5>20亦视为满档 + const double next_lot = CalculateLotSize(pos_type); + if(next_lot <= 0.0) + return (cur > 0.0); + return (cur + next_lot > InpMaxLots + 1e-8); +} + +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() +{ + return (InpBreakoutDirection == BREAKOUT_DIR_REVERSE) ? POSITION_TYPE_SELL : POSITION_TYPE_BUY; +} + +long PosTypeOnLowBreakout() +{ + return (InpBreakoutDirection == BREAKOUT_DIR_REVERSE) ? POSITION_TYPE_BUY : 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)) { + 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)) { + 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; +} + +// tier_index: 0=该方向首笔, 1=第2笔…;grp=tier/N,组内手数相同,每组=基准+grp×加码 +double LotForSameDirectionTier(const int tier_index, const double base_lot) +{ + if(tier_index < 0 || base_lot <= 0.0) + return 0.0; + const int n = MathMax(1, InpMartingaleTierInterval); + const int grp = tier_index / n; + if(InpMartingaleAddLotBoost <= 0.0) + return NormalizeVolume(base_lot); + double vol = base_lot + (double)grp * InpMartingaleAddLotBoost; + return NormalizeVolume(MathMin(vol, InpMaxLots)); +} + +//+------------------------------------------------------------------+ +//| 按同向持仓笔数(档)计算本笔手数 | +//+------------------------------------------------------------------+ +double CalculateLotSize(const long pos_type) +{ + const int tier = CountBreakoutPositions(pos_type); + return LotForSameDirectionTier(tier, InpFixedLots); +} + +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; +} + +void CheckBreakoutBalanceExit() +{ + if(CountAllBreakoutPositions() == 0) + return; + + const double balance = AccountInfoDouble(ACCOUNT_BALANCE); + if(balance <= 0.0) + return; + + const double fpl = TotalBreakoutFloatingPL(); + + if(InpTakeProfitBalancePct > 0.0) { + const double tp_need = balance * (InpTakeProfitBalancePct / 100.0); + if(fpl >= tp_need) { + CloseAllBreakoutPositions( + StringFormat("止盈:浮盈%.2f达结余%.2f%%", fpl, InpTakeProfitBalancePct)); + return; + } + } + + if(InpStopLossBalancePct > 0.0) { + 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; + + 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); + const int tier = CountBreakoutPositions(pos_type) - 1; + const int grp = (tier >= 0) ? (tier / MathMax(1, InpMartingaleTierInterval)) : 0; + Print("【突破】开仓成功 ", (order_type == ORDER_TYPE_BUY ? "多" : "空"), + " 手数:", lots, " 轮次:", g_trade_round, " 同向第", (tier + 1), "笔/组", (grp + 1), + " 备注:", 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()); + } + } + } +} diff --git a/20260520-突破马丁/README.md b/20260520-突破马丁/README.md new file mode 100644 index 0000000..e565360 --- /dev/null +++ b/20260520-突破马丁/README.md @@ -0,0 +1,62 @@ +# 突破策略(极值点突破开仓) + +MQL5 EA:`20260520_Breakout.mq5`(v3.07)。 + +## 参数默认值(与输入界面一致) + +| 分组 | 参数 | 默认 | +|------|------|------| +| 波段 | K线周期 / MA周期 | M1 / 14 | +| 波段 | 最小/最大波段% | 0.1 / **1.0** | +| 波段 | 反向突破容忍度% | **0.05** | +| 交易 | 开仓方向 | **顺势**(破高多/破低空) | +| 交易 | 止盈:浮盈达结余比例(%) | **1**(0=关) | +| 交易 | 止损:浮亏达结余比例(%) | **30**(0=关) | +| 仓位 | 基准手数 / 每组+ / 档间隔N / 同向上限 / 同向最小间距% | **0.5** / **2** / 5 / **30** / **0.1** | +| 仓位 | 双向满档后处理 | **清仓**(平掉全部 EA 持仓) | +| 调试 | 调试信息 / 极值标记 / Magic / 禁手工单 | false / true / 20260520 / true | + +## 开仓 + +- 识别**最新有效波段**后,价格**破高**或**破低**即尝试开仓。 +- **破高、破低各算一个极值点,每个极值点最多对应 1 笔**(当前轮内)。 +- 可同时持有多笔;**仅统计当前轮**的手数、笔数、间距、满档。 +- 第 1 轮备注 `[突破]`,第 2 轮起 `[突破-R2]`、`[突破-R3]`… + +### 同向加码手数(类马丁) + +- 按**当前轮、本方向已有笔数**定档。 +- `组号 = tier ÷ 档间隔N`;**本笔手数** = `基准 + 组号 × 每组+`。 +- **当前轮**该方向合计不得超过 `同向最大合计手数`。 + +### 同向最小开仓间距 + +- **0 = 不限制**;仅相对**当前轮**最近一笔同向开仓价判断。 + +### 开仓方向 + +| 选项 | 破高 | 破低 | +|------|------|------| +| **顺势** | 多 | 空 | +| **反向** | 空 | 多 | +| **随机** | 每次突破独立随机 | | + +## 双向满档(当前轮多空均达同向上限) + +当**当前轮**多单、空单均**无法再开下一笔**(合计已达上限,或「当前合计 + 下一档手数」会超限,例如 19.5+4.5>20)时触发一次: + +| 模式 | 行为 | +|------|------| +| **清仓**(默认) | 平掉本 EA 全部持仓(各轮一并平掉),轮次重置为 1,极值标记重置 | +| **不处理旧仓,开始下一轮** | 旧仓保留;轮次 +1;新单从基准手数重新加码 | + +触发后需等当前轮任一侧未满档,才会在再次双向满档时重复处理。 + +## 平仓(结余比例) + +- 统计**全部轮次**持仓浮盈浮亏(含 swap)。 +- 止盈 / 止损达标 → **全部清仓**(各轮一并平掉)。 + +## 手工单 + +- `禁止手工单=true` 时自动平掉本品种 Magic=0 的手工单。 diff --git a/20260520-突破马丁/XAU_1min_300000.set b/20260520-突破马丁/XAU_1min_300000.set new file mode 100644 index 0000000..20d167d Binary files /dev/null and b/20260520-突破马丁/XAU_1min_300000.set differ diff --git a/20260520-突破马丁/XAU_1min_v1.set b/20260520-突破马丁/XAU_1min_v1.set new file mode 100644 index 0000000..ac0538d Binary files /dev/null and b/20260520-突破马丁/XAU_1min_v1.set differ diff --git a/20260520-突破马丁/XAU_1min_v2.set b/20260520-突破马丁/XAU_1min_v2.set new file mode 100644 index 0000000..f617872 Binary files /dev/null and b/20260520-突破马丁/XAU_1min_v2.set differ