420 lines
13 KiB
MQL5
420 lines
13 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| External.mq5 |
| |
//| External swings from internal Swing iCustom (structure-based) |
| |
//| 1) External High: highest among N swing-highs on BOTH sides |
| |
//| 2) External Low : lowest among N swing-lows on BOTH sides |
| |
//| 3) If consecutive external highs -> insert lowest low between |
| |
//| If consecutive external lows -> insert highest high between |
| |
//| NO ArraySetAsSeries() |
| |
//+------------------------------------------------------------------+
| |
#property copyright "Copyright 2025, MetaQuotes Ltd."
| |
#property link "https://www.mql5.com"
| |
#property version "1.40"
| |
#property indicator_chart_window
| |
| |
#property indicator_plots 2
| |
#property indicator_buffers 5
| |
| |
//--- Plot 0: External Swing High
| |
#property indicator_label1 "SwingHigh"
| |
#property indicator_type1 DRAW_ARROW
| |
#property indicator_color1 clrCrimson
| |
#property indicator_width1 1
| |
| |
//--- Plot 1: External Swing Low
| |
#property indicator_label2 "SwingLow"
| |
#property indicator_type2 DRAW_ARROW
| |
#property indicator_color2 clrMediumSeaGreen
| |
#property indicator_width2 1
| |
| |
input int InpLeftRightBars = 3; // passed to internal Swing indicator
| |
input int InpSwingWindow = 2; // N swings on EACH side (2 => compare 2 left + 2 right)
| |
input ushort InpArrowCodeHigh = 172;
| |
input ushort InpArrowCodeLow = 172;
| |
| |
//--- plotted (external) buffers
| |
double SwingHighBuffer[];
| |
double SwingLowBuffer[];
| |
double MergedSwingBuffer[];
| |
| |
//--- internal swing buffers copied from iCustom Swing
| |
double HighBuffer[];
| |
double LowBuffer[];
| |
| |
//--- iCustom handle
| |
int handle = INVALID_HANDLE;
| |
datetime last_bar_time = 0;
| |
| |
//----------------------------- Helpers ------------------------------//
| |
bool HasValue(const double v) {
| |
if(v == EMPTY_VALUE) return false;
| |
if(v <= 0.0) return false; // protects against zero fillers
| |
return true;
| |
}
| |
| |
// series indices: 0 newest, rates_total-1 oldest
| |
// return min low index between two bar indices (inclusive or exclusive pivot bars)
| |
int FindLowestLowIndexBetween(const double &low[], const int idxA, const int idxB) {
| |
int start = (idxA > idxB) ? idxA : idxB; // older (larger index)
| |
int end = (idxA > idxB) ? idxB : idxA; // newer (smaller index)
| |
| |
// Prefer excluding the pivot bars themselves; keep at least one bar scan if possible
| |
int s = start - 1;
| |
int e = end + 1;
| |
if(s < e) {
| |
s = start; // fallback if too tight
| |
e = end;
| |
}
| |
| |
double best = DBL_MAX;
| |
int bestIdx = -1;
| |
| |
for(int i = s; i >= e; i--) {
| |
if(low[i] < best) {
| |
best = low[i];
| |
bestIdx = i;
| |
}
| |
}
| |
return bestIdx;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| |
| |
//+------------------------------------------------------------------+
| |
int FindHighestHighIndexBetween(const double &high[], const int idxA, const int idxB) {
| |
int start = (idxA > idxB) ? idxA : idxB;
| |
int end = (idxA > idxB) ? idxB : idxA;
| |
| |
int s = start - 1;
| |
int e = end + 1;
| |
if(s < e) {
| |
s = start;
| |
e = end;
| |
}
| |
| |
double best = -DBL_MAX;
| |
int bestIdx = -1;
| |
| |
for(int i = s; i >= e; i--) {
| |
if(high[i] > best) {
| |
best = high[i];
| |
bestIdx = i;
| |
}
| |
}
| |
return bestIdx;
| |
}
| |
| |
// Swing point list (only highs list or lows list)
| |
struct SwingPoint {
| |
int index; // bar index
| |
double price;
| |
};
| |
| |
// External point (mixed highs/lows)
| |
enum ExtType { EXT_HIGH = 1, EXT_LOW = -1 };
| |
| |
struct ExtPoint {
| |
int index;
| |
double price;
| |
ExtType type;
| |
};
| |
| |
// Insert in chronological order (oldest->newest) into ext[] by index (descending index)
| |
void ExtInsertChrono(ExtPoint &ext[], const ExtPoint &p) {
| |
int n = ArraySize(ext);
| |
ArrayResize(ext, n + 1);
| |
| |
// find insert position: ext sorted by index descending (older bigger index first)
| |
int pos = n;
| |
for(int i = 0; i < n; i++) {
| |
if(p.index > ext[i].index) {
| |
pos = i;
| |
break;
| |
}
| |
}
| |
| |
// shift right
| |
for(int i = n; i > pos; i--)
| |
ext[i] = ext[i - 1];
| |
| |
ext[pos] = p;
| |
}
| |
| |
// If an ext point already exists at index with same type, keep more extreme.
| |
// If exists with opposite type (rare), keep both by shifting (we avoid that in logic).
| |
void ExtUpsert(ExtPoint &ext[], const ExtPoint &p) {
| |
int n = ArraySize(ext);
| |
for(int i = 0; i < n; i++) {
| |
if(ext[i].index == p.index && ext[i].type == p.type) {
| |
if(p.type == EXT_HIGH) {
| |
if(p.price > ext[i].price) ext[i].price = p.price;
| |
} else {
| |
if(p.price < ext[i].price) ext[i].price = p.price;
| |
}
| |
return;
| |
}
| |
}
| |
ExtInsertChrono(ext, p);
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Init |
| |
//+------------------------------------------------------------------+
| |
int OnInit() {
| |
// map plotted buffers
| |
SetIndexBuffer(0, SwingHighBuffer, INDICATOR_DATA);
| |
SetIndexBuffer(1, SwingLowBuffer, INDICATOR_DATA);
| |
| |
// map internal copy buffers (calculation only)
| |
SetIndexBuffer(2, HighBuffer, INDICATOR_CALCULATIONS);
| |
SetIndexBuffer(3, LowBuffer, INDICATOR_CALCULATIONS);
| |
| |
SetIndexBuffer(4, MergedSwingBuffer, INDICATOR_CALCULATIONS);
| |
| |
PlotIndexSetInteger(0, PLOT_ARROW, InpArrowCodeHigh);
| |
PlotIndexSetInteger(1, PLOT_ARROW, InpArrowCodeLow);
| |
| |
PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
| |
PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
| |
| |
// iCustom handle to your internal swing indicator
| |
handle = iCustom(_Symbol, _Period, "Arjo Indicators\\Swing", InpLeftRightBars);
| |
if(handle == INVALID_HANDLE) {
| |
Print("Error creating Swing iCustom handle. LastError=", GetLastError());
| |
return(INIT_FAILED);
| |
}
| |
| |
if(InpSwingWindow < 1) {
| |
Print("InpSwingWindow must be >= 1. Using 1.");
| |
}
| |
| |
return(INIT_SUCCEEDED);
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Deinit |
| |
//+------------------------------------------------------------------+
| |
void OnDeinit(const int reason) {
| |
if(handle != INVALID_HANDLE) {
| |
IndicatorRelease(handle);
| |
handle = INVALID_HANDLE;
| |
}
| |
}
| |
//+------------------------------------------------------------------+
| |
//| |
| |
//+------------------------------------------------------------------+
| |
void MergeSwings(const double &swingHigh[],
| |
const double &swingLow[],
| |
double &merged[],
| |
int rates_total) {
| |
ArrayInitialize(merged, 0.0);
| |
| |
for(int i = 0; i < rates_total; i++) {
| |
if(swingHigh[i] != 0.0) {
| |
merged[i] = swingHigh[i];
| |
Print("high");
| |
} else if(swingLow[i] != 0.0) {
| |
merged[i] = swingLow[i];
| |
Print("low");
| |
}
| |
}
| |
}
| |
//+------------------------------------------------------------------+
| |
//| Calculate |
| |
//+------------------------------------------------------------------+
| |
int OnCalculate(const int rates_total,
| |
const int prev_calculated,
| |
const datetime &time[],
| |
const double &open[],
| |
const double &high[],
| |
const double &low[],
| |
const double &close[],
| |
const long &tick_volume[],
| |
const long &volume[],
| |
const int &spread[]) {
| |
if(rates_total <= 0) return 0;
| |
if(handle == INVALID_HANDLE) return 0;
| |
| |
// new-bar gate (time[0] is newest)
| |
if(time[rates_total - 1] == last_bar_time)
| |
return rates_total;
| |
| |
// copy internal swing buffers
| |
if(CopyBuffer(handle, 0, 0, rates_total, HighBuffer) <= 0) {
| |
Print("Error copying Swing HIGH buffer. LastError=", GetLastError());
| |
return 0;
| |
}
| |
if(CopyBuffer(handle, 1, 0, rates_total, LowBuffer) <= 0) {
| |
Print("Error copying Swing LOW buffer. LastError=", GetLastError());
| |
return 0;
| |
}
| |
| |
// clear plotted buffers
| |
for(int i = 0; i < rates_total; i++) {
| |
SwingHighBuffer[i] = EMPTY_VALUE;
| |
SwingLowBuffer[i] = EMPTY_VALUE;
| |
}
| |
| |
// Build chronological lists of swing highs and swing lows (oldest -> newest)
| |
SwingPoint highs[];
| |
SwingPoint lows[];
| |
ArrayResize(highs, 0);
| |
ArrayResize(lows, 0);
| |
| |
for(int i = rates_total - 1; i >= 0; i--) { // oldest -> newest
| |
if(HasValue(HighBuffer[i])) {
| |
int n = ArraySize(highs);
| |
ArrayResize(highs, n + 1);
| |
highs[n].index = i;
| |
highs[n].price = HighBuffer[i];
| |
}
| |
if(HasValue(LowBuffer[i])) {
| |
int n = ArraySize(lows);
| |
ArrayResize(lows, n + 1);
| |
lows[n].index = i;
| |
lows[n].price = LowBuffer[i];
| |
}
| |
}
| |
| |
int win = (InpSwingWindow < 1) ? 1 : InpSwingWindow;
| |
| |
// Collect externals (mixed list)
| |
ExtPoint ext[];
| |
ArrayResize(ext, 0);
| |
| |
// External highs: cur > all highs in [j-win..j-1] and > all highs in [j+1..j+win]
| |
int hc = ArraySize(highs);
| |
if(hc >= (2 * win + 1)) {
| |
for(int j = win; j <= hc - 1 - win; j++) {
| |
double cur = highs[j].price;
| |
bool ok = true;
| |
| |
for(int k = j - win; k <= j - 1; k++) {
| |
if(highs[k].price >= cur) {
| |
ok = false;
| |
break;
| |
}
| |
}
| |
if(ok) {
| |
for(int k = j + 1; k <= j + win; k++) {
| |
if(highs[k].price >= cur) {
| |
ok = false;
| |
break;
| |
}
| |
}
| |
}
| |
| |
if(ok) {
| |
ExtPoint p;
| |
p.index = highs[j].index;
| |
p.price = cur;
| |
p.type = EXT_HIGH;
| |
ExtInsertChrono(ext, p);
| |
}
| |
}
| |
}
| |
| |
// External lows: cur < all lows in [j-win..j-1] and < all lows in [j+1..j+win]
| |
int lc = ArraySize(lows);
| |
if(lc >= (2 * win + 1)) {
| |
for(int j = win; j <= lc - 1 - win; j++) {
| |
double cur = lows[j].price;
| |
bool ok = true;
| |
| |
for(int k = j - win; k <= j - 1; k++) {
| |
if(lows[k].price <= cur) {
| |
ok = false;
| |
break;
| |
}
| |
}
| |
if(ok) {
| |
for(int k = j + 1; k <= j + win; k++) {
| |
if(lows[k].price <= cur) {
| |
ok = false;
| |
break;
| |
}
| |
}
| |
}
| |
| |
if(ok) {
| |
ExtPoint p;
| |
p.index = lows[j].index;
| |
p.price = cur;
| |
p.type = EXT_LOW;
| |
ExtInsertChrono(ext, p);
| |
}
| |
}
| |
}
| |
| |
// Post-process:
| |
// If consecutive external points have the same type, insert opposite extreme between them:
| |
// - HIGH then HIGH -> insert LOW at lowest low between their indices
| |
// - LOW then LOW -> insert HIGH at highest high between their indices
| |
int ec = ArraySize(ext);
| |
if(ec >= 2) {
| |
// We'll build a new list with insertions
| |
ExtPoint fixed[];
| |
ArrayResize(fixed, 0);
| |
| |
// ext is chronological: older first (bigger index first)
| |
ExtInsertChrono(fixed, ext[0]);
| |
| |
for(int i = 1; i < ec; i++) {
| |
// find last in fixed (chronologically newest element is at the end because we always insert chrono;
| |
// but easier: pick the previous ext in chrono order by scanning last inserted with same index logic)
| |
// We'll simply get the most recent point in fixed by choosing the smallest index in fixed.
| |
// However because fixed is kept chrono, newest is last element.
| |
int fn = ArraySize(fixed);
| |
ExtPoint prev = fixed[fn - 1];
| |
ExtPoint cur = ext[i];
| |
| |
if(prev.type == cur.type) {
| |
if(cur.type == EXT_HIGH) {
| |
int lowIdx = FindLowestLowIndexBetween(low, prev.index, cur.index);
| |
if(lowIdx >= 0) {
| |
ExtPoint mid;
| |
mid.index = lowIdx;
| |
mid.price = low[lowIdx];
| |
mid.type = EXT_LOW;
| |
ExtUpsert(fixed, mid);
| |
}
| |
} else { // EXT_LOW
| |
int highIdx = FindHighestHighIndexBetween(high, prev.index, cur.index);
| |
if(highIdx >= 0) {
| |
ExtPoint mid;
| |
mid.index = highIdx;
| |
mid.price = high[highIdx];
| |
mid.type = EXT_HIGH;
| |
ExtUpsert(fixed, mid);
| |
}
| |
}
| |
}
| |
| |
// always add current
| |
ExtUpsert(fixed, cur);
| |
}
| |
| |
// Replace ext with fixed (already chrono)
| |
//ext = fixed;
| |
ArrayCopy(ext, fixed);
| |
ec = ArraySize(ext);
| |
}
| |
| |
// Plot results
| |
for(int i = 0; i < ec; i++) {
| |
if(ext[i].index < 0 || ext[i].index >= rates_total) continue;
| |
| |
if(ext[i].type == EXT_HIGH) {
| |
SwingHighBuffer[ext[i].index] = ext[i].price;
| |
MergedSwingBuffer[ext[i].index] = ext[i].price;
| |
} else {
| |
SwingLowBuffer[ext[i].index] = ext[i].price;
| |
MergedSwingBuffer[ext[i].index] = ext[i].price;
| |
}
| |
}
| |
// MergeSwings(SwingHighBuffer, SwingLowBuffer, MergedSwingBuffer, rates_total);
| |
last_bar_time = time[rates_total - 1];
| |
| |
return rates_total;
| |
}
| |
//+------------------------------------------------------------------+
|