//+------------------------------------------------------------------+ //| SpikeDetectorEA.mq5 | //| Copyright 2025, Your Name/Company | //| https://www.example.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Your Name/Company" #property link "https://www.example.com" #property version "1.01" // Incremented version #property description "Expert Advisor to detect and draw price spikes (pin bars)." //--- Input Parameters input group "Spike Definition" input double InpMinWickToBodyRatio = 2.0; // Minimum ratio of the main wick to the body (e.g., 2.0 means wick is 2x body) input double InpMaxOppositeWickToBodyRatio = 1.0; // Maximum ratio of the opposite wick to the body (e.g., 1.0 means opposite wick <= body) input double InpMaxBodyToTotalRangeRatio = 0.33; // Maximum ratio of body to total candle range (H-L) (e.g., 0.33 means body is < 1/3 of range) input group "Drawing Settings" input int InpArrowOffsetPoints = 10; // Arrow offset from High/Low in points input color InpBullishSpikeColor = clrLimeGreen; // Color for bullish spike arrows input color InpBearishSpikeColor = clrRed; // Color for bearish spike arrows input int InpLookbackForDrawing = 200; // How many past bars to check on EA load/recompile // Wingdings Arrow Codes (commonly used for OBJ_ARROW) #define WINGDINGS_ARROW_UP 241 #define WINGDINGS_ARROW_DOWN 242 //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- // You can add any one-time initialization logic here if needed Print("SpikeDetectorEA initialized. Symbol: ", _Symbol, ", Timeframe: ", EnumToString((ENUM_TIMEFRAMES)Period())); //--- return (INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- // Optional: Clean up objects when EA is removed or chart is closed // For more targeted deletion, you might loop through objects and check names // ObjectsDeleteAll(0, "SpikeArrow_Bearish_" + _Symbol); // ObjectsDeleteAll(0, "SpikeArrow_Bullish_" + _Symbol); // A more robust way if you have many EAs or indicators: long chart_id = ChartID(); for (int i = ObjectsTotal(chart_id, -1, OBJ_ARROW) - 1; i >= 0; i--) { string obj_name = ObjectName(chart_id, i, -1, OBJ_ARROW); if (StringFind(obj_name, "SpikeArrow_Bearish_" + _Symbol + "_" + EnumToString((ENUM_TIMEFRAMES)Period())) == 0 || StringFind(obj_name, "SpikeArrow_Bullish_" + _Symbol + "_" + EnumToString((ENUM_TIMEFRAMES)Period())) == 0) { ObjectDelete(chart_id, obj_name); } } Print("SpikeDetectorEA deinitialized. Reason: ", reason); } //+------------------------------------------------------------------+ //| Expert tick function (not used for bar analysis in this EA) | //+------------------------------------------------------------------+ void OnTick() { //--- // We will use OnCalculate for bar-based analysis } //+------------------------------------------------------------------+ //| Chart event function (not strictly needed for this EA) | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- // Can be used to handle chart events if necessary } //+------------------------------------------------------------------+ //| Expert new bar function / Indicator calculation event | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, // Size of the price[] arrays const int prev_calculated, // Bars calculated at the previous call const datetime &time[], // Time array const double &open[], // Open price array const double &high[], // High price array const double &low[], // Low price array const double &close[], // Close price array const long &tick_volume[], // Tick Volume array const long &volume[], // Real Volume array const int &spread[]) // Spread array { //--- Check for minimal rates_total if (rates_total < 2) // Need at least one closed bar to compare return (0); //--- Determine how many bars to calculate int first_bar_to_calculate; // On the very first call of OnCalculate (prev_calculated == 0) or if history was changed drastically if (prev_calculated == 0 || prev_calculated > rates_total || rates_total - prev_calculated > InpLookbackForDrawing + 10) // Added a buffer for safety { first_bar_to_calculate = MathMax(0, rates_total - InpLookbackForDrawing); Print("SpikeDetectorEA: First run or history refresh. Processing up to ", InpLookbackForDrawing, " bars from bar index ", first_bar_to_calculate); // Clean up old arrows if doing a full refresh to avoid orphaned objects if bar times change long chart_id = ChartID(); for (int k = ObjectsTotal(chart_id, -1, OBJ_ARROW) - 1; k >= 0; k--) { string obj_name_check = ObjectName(chart_id, k, -1, OBJ_ARROW); if (StringFind(obj_name_check, "SpikeArrow_Bearish_" + _Symbol + "_" + EnumToString((ENUM_TIMEFRAMES)Period())) == 0 || StringFind(obj_name_check, "SpikeArrow_Bullish_" + _Symbol + "_" + EnumToString((ENUM_TIMEFRAMES)Period())) == 0) { ObjectDelete(chart_id, obj_name_check); } } } else { // prev_calculated is the number of bars calculated by the previous call. // We want to start from one bar before the last one calculated by the previous call, // to catch updates to the forming bar that has now closed, plus any new bars. // rates_total - prev_calculated gives the number of new bars. // We process (rates_total - prev_calculated) + 1 bars (the forming bar that closed + new bars) // So, start from rates_total - (rates_total - prev_calculated + 1) = prev_calculated - 1 first_bar_to_calculate = prev_calculated - 1; } // Ensure we don't go out of bounds (e.g. if prev_calculated was 0 due to error or very few bars) first_bar_to_calculate = MathMax(0, first_bar_to_calculate); // We don't want to process the current, still-forming bar (index rates_total - 1 usually) // So loop up to rates_total - 1 (or < rates_total) // If we are processing historical bars, i can go up to rates_total - 1 // If we are processing only new bars, the last bar is rates_total - 1. //--- Main loop for processing bars // We iterate up to rates_total-1 because the last bar (rates_total-1) is the current forming bar // We are interested in closed bars. So, the last closed bar is at index rates_total-2 if rates_total > 1. // However, if we start from prev_calculated-1, this logic handles it correctly. // The loop should go up to `rates_total - 1` to process all available historical bars. // The current bar (index rates_total - 1) will be processed on the next tick once it closes and a new bar forms. for (int i = first_bar_to_calculate; i < rates_total; i++) { // Skip the current forming bar (bar 0 in terminal, index rates_total-1 in array if processing live) // This logic is implicitly handled by how OnCalculate is typically called. // The loop goes from an older bar up to the most recent *available* bar data. // The last bar in the `time`, `open`, etc. arrays is `rates_total - 1`. // If `i == rates_total - 1`, it's the current, potentially unclosed bar. // We generally want to analyze closed bars. Let's adjust the loop to `i < rates_total -1` if we only want confirmed closed bars. // However, for drawing on historical data, `i < rates_total` is fine. // The key is that `prev_calculated` helps us only recalculate what's necessary. // Let's assume we process up to the last available bar, and if it's the current forming bar, // its spike status might change. The object deletion logic handles this. // If it's the very first bar available (index 0) and it has no history to form wicks properly, skip. // This check might be too simplistic, but helps avoid issues with incomplete data at the start of history. // if (i == 0 && rates_total < 3) continue; // This might be too restrictive. // Define object names for this bar to manage them // Using time ensures uniqueness per bar. Adding symbol and timeframe for robustness. string obj_time_str = TimeToString(time[i], TIME_DATE | TIME_MINUTES | TIME_SECONDS); // Added seconds for more uniqueness string bearish_obj_name = StringFormat("SpikeArrow_Bearish_%s_%s_%s", _Symbol, EnumToString((ENUM_TIMEFRAMES)Period()), obj_time_str); string bullish_obj_name = StringFormat("SpikeArrow_Bullish_%s_%s_%s", _Symbol, EnumToString((ENUM_TIMEFRAMES)Period()), obj_time_str); // Delete existing arrows for this bar before re-evaluating // This allows settings changes to reflect immediately and removes arrows if a spike no longer qualifies ObjectDelete(0, bearish_obj_name); ObjectDelete(0, bullish_obj_name); // Get OHLC for the current bar i double O = open[i]; double H = high[i]; double L = low[i]; double C = close[i]; // Calculate candle components double body_size = MathAbs(O - C); double total_range = H - L; double upper_wick = H - MathMax(O, C); double lower_wick = MathMin(O, C) - L; // Avoid division by zero or issues with flat candles (doji, etc.) if (total_range < _Point * 0.1) // If candle range is very small, skip (e.g. less than 0.1 points) continue; bool is_bearish_spike = false; bool is_bullish_spike = false; double body_plus_epsilon = body_size + (_Point * 0.01); // Add a very small value related to point size // --- Bearish Spike Detection (Spike High) --- // 1. Body is a small part of the total range // 2. Upper wick is significantly larger than the body // 3. Lower wick is small compared to the body (or upper wick) if (body_size / total_range <= InpMaxBodyToTotalRangeRatio && upper_wick / body_plus_epsilon >= InpMinWickToBodyRatio && lower_wick / body_plus_epsilon <= InpMaxOppositeWickToBodyRatio && upper_wick > lower_wick) // Ensure upper wick is dominant { is_bearish_spike = true; } // --- Bullish Spike Detection (Spike Low) --- // 1. Body is a small part of the total range // 2. Lower wick is significantly larger than the body // 3. Upper wick is small compared to the body (or lower wick) if (body_size / total_range <= InpMaxBodyToTotalRangeRatio && lower_wick / body_plus_epsilon >= InpMinWickToBodyRatio && upper_wick / body_plus_epsilon <= InpMaxOppositeWickToBodyRatio && lower_wick > upper_wick) // Ensure lower wick is dominant { is_bullish_spike = true; } // --- Drawing --- double arrow_offset_actual = InpArrowOffsetPoints * _Point; if (is_bearish_spike) { if (ObjectCreate(0, bearish_obj_name, OBJ_ARROW, 0, time[i], H + arrow_offset_actual)) { ObjectSetInteger(0, bearish_obj_name, OBJPROP_ARROWCODE, WINGDINGS_ARROW_DOWN); // Corrected ObjectSetInteger(0, bearish_obj_name, OBJPROP_COLOR, InpBearishSpikeColor); ObjectSetInteger(0, bearish_obj_name, OBJPROP_WIDTH, 1); ObjectSetInteger(0, bearish_obj_name, OBJPROP_STYLE, STYLE_SOLID); ObjectSetString(0, bearish_obj_name, OBJPROP_TOOLTIP, "Bearish Spike"); } // else { PrintFormat("Failed to create bearish spike arrow for bar at %s. Error: %d", TimeToString(time[i]), GetLastError()); } } if (is_bullish_spike) { if (ObjectCreate(0, bullish_obj_name, OBJ_ARROW, 0, time[i], L - arrow_offset_actual)) { ObjectSetInteger(0, bullish_obj_name, OBJPROP_ARROWCODE, WINGDINGS_ARROW_UP); // Corrected ObjectSetInteger(0, bullish_obj_name, OBJPROP_COLOR, InpBullishSpikeColor); ObjectSetInteger(0, bullish_obj_name, OBJPROP_WIDTH, 1); ObjectSetInteger(0, bullish_obj_name, OBJPROP_STYLE, STYLE_SOLID); ObjectSetString(0, bullish_obj_name, OBJPROP_TOOLTIP, "Bullish Spike"); } // else { PrintFormat("Failed to create bullish spike arrow for bar at %s. Error: %d", TimeToString(time[i]), GetLastError()); } } } //--- Return value of prev_calculated for next call return (rates_total); } //+------------------------------------------------------------------+