diff --git a/20260519-突破策略/20260519_Breakout.mq5 b/20260519-突破策略/20260519_Breakout.mq5 new file mode 100644 index 0000000..592b065 --- /dev/null +++ b/20260519-突破策略/20260519_Breakout.mq5 @@ -0,0 +1,1015 @@ +//+------------------------------------------------------------------+ +//| 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 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; +}; + +// 函数声明 +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(); + +//+------------------------------------------------------------------+ +//| 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 ? "启用 (自动平掉手工单)" : "禁用")); + 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; + + // 判断是亏损还是盈利 + if(net_profit < 0) + { + // 亏损:增加计数器 + 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 if(net_profit > 0) + { + // 盈利:重置计数器 + if(consecutive_loss_count > 0) + { + Print("【连续亏损保护】盈利 - 计数器重置: ", consecutive_loss_count, " → 0 | 净盈利: $", DoubleToString(net_profit, 2)); + consecutive_loss_count = 0; + } + } +} + +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() +{ + // 1. 检查并关闭手工单(如果启用) + if(InpCloseManualOrders) + CheckAndCloseManualOrders(); + + // 2. 更新最新有效波段 + UpdateLatestValidWave(); + + // 3. 检查开仓信号 + CheckOpenSignals(); + + // 4. 管理已有持仓 + 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; + + // 生成备注信息:盈亏比和最小止损点数 + string comment = StringFormat("R%.1f SL%d", + InpRiskRewardRatio, + InpMinStopLossPoints); + + 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; + + 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()); + } + } + } +} diff --git a/20260519-突破策略/README.md b/20260519-突破策略/README.md new file mode 100644 index 0000000..e18d257 --- /dev/null +++ b/20260519-突破策略/README.md @@ -0,0 +1,400 @@ +# 突破交易策略 + +## 基础概念 +- 平台: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_1H.set b/20260519-突破策略/XAU_1H.set new file mode 100644 index 0000000..8e715d6 Binary files /dev/null and b/20260519-突破策略/XAU_1H.set differ diff --git a/20260519-突破策略/XAU_1min.set b/20260519-突破策略/XAU_1min.set new file mode 100644 index 0000000..69070d1 Binary files /dev/null and b/20260519-突破策略/XAU_1min.set differ diff --git a/20260519-突破策略/XAU_1min_v2.set b/20260519-突破策略/XAU_1min_v2.set new file mode 100644 index 0000000..cffb218 Binary files /dev/null and b/20260519-突破策略/XAU_1min_v2.set differ diff --git a/20260519-突破策略/xau_1min_0.05.set b/20260519-突破策略/xau_1min_0.05.set new file mode 100644 index 0000000..74ff9ac Binary files /dev/null and b/20260519-突破策略/xau_1min_0.05.set differ