558 lines
No EOL
16 KiB
MQL5
558 lines
No EOL
16 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| C_Chapter3_Liquidity.mqh |
| |
//| Chapter 3 wired to your Chapter 2 exports |
| |
//+------------------------------------------------------------------+
| |
#property strict
| |
#property version "1.00"
| |
| |
#include "AbstractChapter.mqh"
| |
| |
//========================
| |
// Inputs
| |
//========================
| |
input int ch3_eqTolerancePts = 25;
| |
input int ch3_minPoolSeparationBars = 3;
| |
input int ch3_imbLookbackBars = 300;
| |
input int ch3_maxPoolsPerType = 25;
| |
| |
//========================
| |
// Enums
| |
//========================
| |
enum EImbalanceSide { IMB_BULL = 0, IMB_BEAR = 1 };
| |
enum EImbalanceState { IMB_OPEN = 0, IMB_MITIGATED = 1 };
| |
| |
enum ELiquiditySide { LQ_BSL = 0, LQ_SSL = 1 };
| |
enum ELiquidityKind { LQ_EQUALS = 0, LQ_STRUCTURE = 1, LQ_EXTERNAL = 2 };
| |
enum ELiquidityState { LQ_ACTIVE = 0, LQ_SWEPT = 1 };
| |
| |
struct ImbalanceZone {
| |
int id;
| |
EImbalanceSide side;
| |
EImbalanceState state;
| |
| |
int leftBar;
| |
int rightBar;
| |
| |
double top;
| |
double bottom;
| |
| |
int mitigateBar;
| |
};
| |
| |
struct LiquidityPool {
| |
int id;
| |
ELiquiditySide side;
| |
ELiquidityKind kind;
| |
ELiquidityState state;
| |
| |
int leftBar;
| |
int rightBar;
| |
| |
double price;
| |
int sweepBar;
| |
};
| |
| |
//+------------------------------------------------------------------+
| |
//| |
| |
//+------------------------------------------------------------------+
| |
class C_Chapter3 : public CAbstractChapter {
| |
| |
private:
| |
FairValue fairValue;
| |
| |
ImbalanceZone imbalances[];
| |
LiquidityPool pools[];
| |
| |
int nextImbId;
| |
int nextPoolId;
| |
| |
// ERL anchors from Chapter 2
| |
int protectedHigh;
| |
int protectedLow;
| |
| |
private:
| |
//========================
| |
// Keys
| |
//========================
| |
string K_IMB(int id) { return "CH3_IMB_" + (string)id; }
| |
string K_POOL(int id) { return "CH3_POOL_" + (string)id; }
| |
string K_ERL_HIGH() { return "CH3_ERL_HIGH"; }
| |
string K_ERL_LOW() { return "CH3_ERL_LOW"; }
| |
| |
//========================
| |
// Helpers
| |
//========================
| |
double TolerancePrice() {
| |
return ch3_eqTolerancePts * _Point;
| |
}
| |
| |
bool IsValidBar(int index) {
| |
return (index >= 0 && index < bar.Size());
| |
}
| |
| |
int MinInt(int a, int b) {
| |
return (a < b ? a : b);
| |
}
| |
| |
int MaxInt(int a, int b) {
| |
return (a > b ? a : b);
| |
}
| |
| |
bool NearPrice(double a, double b, double tol) {
| |
return MathAbs(a - b) <= tol;
| |
}
| |
| |
void DrawLevel(string key,int fromIndex,int toIndex,double price,string text,color col,int style=1) {
| |
if(!IsValidBar(fromIndex) || !IsValidBar(toIndex)) return;
| |
| |
debug.LineWithCenteredText(
| |
key,
| |
bar.Time(fromIndex), price,
| |
bar.Time(toIndex), price,
| |
text,
| |
style,
| |
col
| |
);
| |
}
| |
| |
void PushImb(ImbalanceZone &z) {
| |
int n = ArraySize(imbalances);
| |
ArrayResize(imbalances, n + 1);
| |
imbalances[n] = z;
| |
}
| |
| |
void PushPool(LiquidityPool &p) {
| |
int n = ArraySize(pools);
| |
ArrayResize(pools, n + 1);
| |
pools[n] = p;
| |
}
| |
| |
bool PoolExists(ELiquiditySide side, ELiquidityKind kind, int leftBar, int rightBar, double price) {
| |
double tol = TolerancePrice();
| |
| |
for(int i = 0; i < ArraySize(pools); i++) {
| |
if(pools[i].side != side) continue;
| |
if(pools[i].kind != kind) continue;
| |
if(pools[i].leftBar != leftBar) continue;
| |
if(pools[i].rightBar != rightBar) continue;
| |
if(NearPrice(pools[i].price, price, tol))
| |
return true;
| |
}
| |
| |
return false;
| |
}
| |
| |
bool ImbalanceExists(EImbalanceSide side, int leftBar, int rightBar, double top, double bottom) {
| |
double tol = TolerancePrice();
| |
| |
for(int i = 0; i < ArraySize(imbalances); i++) {
| |
if(imbalances[i].side != side) continue;
| |
if(imbalances[i].leftBar != leftBar) continue;
| |
if(imbalances[i].rightBar != rightBar) continue;
| |
if(NearPrice(imbalances[i].top, top, tol) && NearPrice(imbalances[i].bottom, bottom, tol))
| |
return true;
| |
}
| |
| |
return false;
| |
}
| |
| |
int CountPoolsOfSide(ELiquiditySide side) {
| |
int count = 0;
| |
for(int i = 0; i < ArraySize(pools); i++) {
| |
if(pools[i].side == side)
| |
count++;
| |
}
| |
return count;
| |
}
| |
| |
//========================
| |
// Imbalances
| |
//========================
| |
void DetectImbalances() {
| |
int end = bar.Size() - 1;
| |
if(end < 3) return;
| |
| |
int start = end - ch3_imbLookbackBars;
| |
if(start < 2) start = 2;
| |
| |
for(int i = start; i <= end; i++) {
| |
Fvg fvg;
| |
int l = i - 1;
| |
| |
fairValue.getBullFvg(fvg, i - 1);
| |
if(fairValue.IsValidValue(fvg)) {
| |
if(!ImbalanceExists(IMB_BULL, l, i, fvg.top, fvg.bottom)) {
| |
ImbalanceZone z;
| |
z.id = nextImbId++;
| |
z.side = IMB_BULL;
| |
z.state = IMB_OPEN;
| |
z.leftBar = l;
| |
z.rightBar = i;
| |
z.top = fvg.top;
| |
z.bottom = fvg.bottom;
| |
z.mitigateBar = -1;
| |
PushImb(z);
| |
}
| |
}
| |
| |
fairValue.getBearFvg(fvg, i - 1);
| |
if(fairValue.IsValidValue(fvg)) {
| |
if(!ImbalanceExists(IMB_BEAR, l, i, fvg.top, fvg.bottom)) {
| |
ImbalanceZone z;
| |
z.id = nextImbId++;
| |
z.side = IMB_BEAR;
| |
z.state = IMB_OPEN;
| |
z.leftBar = l;
| |
z.rightBar = i;
| |
z.top = fvg.top;
| |
z.bottom = fvg.bottom;
| |
z.mitigateBar = -1;
| |
PushImb(z);
| |
}
| |
}
| |
}
| |
}
| |
| |
void UpdateImbalanceMitigation() {
| |
int end = bar.Size() - 1;
| |
| |
for(int k = 0; k < ArraySize(imbalances); k++) {
| |
if(imbalances[k].state == IMB_MITIGATED) continue;
| |
| |
for(int i = imbalances[k].rightBar; i <= end; i++) {
| |
bool touched = false;
| |
| |
if(imbalances[k].side == IMB_BULL) {
| |
if(bar.Low(i) <= imbalances[k].top && bar.High(i) >= imbalances[k].bottom)
| |
touched = true;
| |
} else {
| |
if(bar.High(i) >= imbalances[k].bottom && bar.Low(i) <= imbalances[k].top)
| |
touched = true;
| |
}
| |
| |
if(touched) {
| |
imbalances[k].state = IMB_MITIGATED;
| |
imbalances[k].mitigateBar = i;
| |
break;
| |
}
| |
}
| |
}
| |
}
| |
| |
void DrawImbalances() {
| |
int end = bar.Size() - 1;
| |
| |
for(int i = 0; i < ArraySize(imbalances); i++) {
| |
ImbalanceZone z = imbalances[i];
| |
| |
string txt = (z.side == IMB_BULL ? "IMB BULL" : "IMB BEAR");
| |
if(z.state == IMB_MITIGATED)
| |
txt += " MITIGATED";
| |
| |
int toIndex = (z.mitigateBar >= 0 ? z.mitigateBar : end);
| |
| |
color col = clrSilver;
| |
if(z.state != IMB_MITIGATED)
| |
col = (z.side == IMB_BULL ? clrPaleGreen : clrTomato);
| |
| |
debug.RectWithCenteredText(
| |
symbol,
| |
K_IMB(z.id),
| |
bar.Time(z.leftBar), z.top,
| |
bar.Time(toIndex), z.bottom,
| |
txt,
| |
col,
| |
0.06,
| |
6,
| |
false
| |
);
| |
}
| |
}
| |
| |
//========================
| |
// Liquidity pools
| |
//========================
| |
void DetectEqualHighLowPools() {
| |
double tol = TolerancePrice();
| |
int end = bar.Size() - 1;
| |
if(end < 5) return;
| |
| |
for(int i = 0; i <= end; i++) {
| |
// Equal highs = BSL
| |
if(swing.IsValidValue(swing.HighAt(i))) {
| |
for(int j = i + ch3_minPoolSeparationBars; j <= end; j++) {
| |
if(!swing.IsValidValue(swing.HighAt(j)))
| |
continue;
| |
| |
if(NearPrice(bar.High(i), bar.High(j), tol)) {
| |
if(CountPoolsOfSide(LQ_BSL) >= ch3_maxPoolsPerType)
| |
break;
| |
| |
int leftBar = MinInt(i, j);
| |
int rightBar = MaxInt(i, j);
| |
double px = MathMax(bar.High(i), bar.High(j));
| |
| |
if(!PoolExists(LQ_BSL, LQ_EQUALS, leftBar, rightBar, px)) {
| |
LiquidityPool p;
| |
p.id = nextPoolId++;
| |
p.side = LQ_BSL;
| |
p.kind = LQ_EQUALS;
| |
p.state = LQ_ACTIVE;
| |
p.leftBar = leftBar;
| |
p.rightBar = rightBar;
| |
p.price = px;
| |
p.sweepBar = -1;
| |
PushPool(p);
| |
}
| |
}
| |
}
| |
}
| |
| |
// Equal lows = SSL
| |
if(swing.IsValidValue(swing.LowAt(i))) {
| |
for(int j = i + ch3_minPoolSeparationBars; j <= end; j++) {
| |
if(!swing.IsValidValue(swing.LowAt(j)))
| |
continue;
| |
| |
if(NearPrice(bar.Low(i), bar.Low(j), tol)) {
| |
if(CountPoolsOfSide(LQ_SSL) >= ch3_maxPoolsPerType)
| |
break;
| |
| |
int leftBar = MinInt(i, j);
| |
int rightBar = MaxInt(i, j);
| |
double px = MathMin(bar.Low(i), bar.Low(j));
| |
| |
if(!PoolExists(LQ_SSL, LQ_EQUALS, leftBar, rightBar, px)) {
| |
LiquidityPool p;
| |
p.id = nextPoolId++;
| |
p.side = LQ_SSL;
| |
p.kind = LQ_EQUALS;
| |
p.state = LQ_ACTIVE;
| |
p.leftBar = leftBar;
| |
p.rightBar = rightBar;
| |
p.price = px;
| |
p.sweepBar = -1;
| |
PushPool(p);
| |
}
| |
}
| |
}
| |
}
| |
}
| |
}
| |
| |
void DetectStructureLiquidity() {
| |
// Internal structure highs/lows can also act as liquidity
| |
// We take visible swing highs/lows that have not yet been tagged as equals.
| |
int end = bar.Size() - 1;
| |
if(end < 3) return;
| |
| |
for(int i = 1; i < end - 1; i++) {
| |
if(swing.IsValidValue(swing.HighAt(i))) {
| |
if(CountPoolsOfSide(LQ_BSL) < ch3_maxPoolsPerType) {
| |
if(!PoolExists(LQ_BSL, LQ_STRUCTURE, i, i, bar.High(i))) {
| |
LiquidityPool p;
| |
p.id = nextPoolId++;
| |
p.side = LQ_BSL;
| |
p.kind = LQ_STRUCTURE;
| |
p.state = LQ_ACTIVE;
| |
p.leftBar = i;
| |
p.rightBar = i;
| |
p.price = bar.High(i);
| |
p.sweepBar = -1;
| |
PushPool(p);
| |
}
| |
}
| |
}
| |
| |
if(swing.IsValidValue(swing.LowAt(i))) {
| |
if(CountPoolsOfSide(LQ_SSL) < ch3_maxPoolsPerType) {
| |
if(!PoolExists(LQ_SSL, LQ_STRUCTURE, i, i, bar.Low(i))) {
| |
LiquidityPool p;
| |
p.id = nextPoolId++;
| |
p.side = LQ_SSL;
| |
p.kind = LQ_STRUCTURE;
| |
p.state = LQ_ACTIVE;
| |
p.leftBar = i;
| |
p.rightBar = i;
| |
p.price = bar.Low(i);
| |
p.sweepBar = -1;
| |
PushPool(p);
| |
}
| |
}
| |
}
| |
}
| |
}
| |
| |
void DetectExternalRangeLiquidity() {
| |
if(IsValidBar(protectedHigh)) {
| |
if(!PoolExists(LQ_BSL, LQ_EXTERNAL, protectedHigh, protectedHigh, bar.High(protectedHigh))) {
| |
LiquidityPool p;
| |
p.id = nextPoolId++;
| |
p.side = LQ_BSL;
| |
p.kind = LQ_EXTERNAL;
| |
p.state = LQ_ACTIVE;
| |
p.leftBar = protectedHigh;
| |
p.rightBar = protectedHigh;
| |
p.price = bar.High(protectedHigh);
| |
p.sweepBar = -1;
| |
PushPool(p);
| |
}
| |
}
| |
| |
if(IsValidBar(protectedLow)) {
| |
if(!PoolExists(LQ_SSL, LQ_EXTERNAL, protectedLow, protectedLow, bar.Low(protectedLow))) {
| |
LiquidityPool p;
| |
p.id = nextPoolId++;
| |
p.side = LQ_SSL;
| |
p.kind = LQ_EXTERNAL;
| |
p.state = LQ_ACTIVE;
| |
p.leftBar = protectedLow;
| |
p.rightBar = protectedLow;
| |
p.price = bar.Low(protectedLow);
| |
p.sweepBar = -1;
| |
PushPool(p);
| |
}
| |
}
| |
}
| |
| |
void UpdatePoolSweeps() {
| |
int end = bar.Size() - 1;
| |
| |
for(int i = 0; i < ArraySize(pools); i++) {
| |
if(pools[i].state == LQ_SWEPT) continue;
| |
| |
for(int b = pools[i].rightBar; b <= end; b++) {
| |
bool swept = false;
| |
| |
if(pools[i].side == LQ_BSL) {
| |
if(bar.High(b) > pools[i].price)
| |
swept = true;
| |
} else {
| |
if(bar.Low(b) < pools[i].price)
| |
swept = true;
| |
}
| |
| |
if(swept) {
| |
pools[i].state = LQ_SWEPT;
| |
pools[i].sweepBar = b;
| |
break;
| |
}
| |
}
| |
}
| |
}
| |
| |
void DrawPool(const LiquidityPool &p) {
| |
int end = bar.Size() - 1;
| |
int toIndex = (p.sweepBar >= 0 ? p.sweepBar : end);
| |
| |
string txt;
| |
color col;
| |
int style = 1;
| |
| |
if(p.side == LQ_BSL) {
| |
txt = "BSL";
| |
col = clrTomato;
| |
} else {
| |
txt = "SSL";
| |
col = clrDeepSkyBlue;
| |
}
| |
| |
if(p.kind == LQ_EQUALS) txt += " EQ";
| |
if(p.kind == LQ_STRUCTURE) txt += " SH/SL";
| |
if(p.kind == LQ_EXTERNAL) txt += " ERL";
| |
| |
if(p.state == LQ_SWEPT) {
| |
txt += " SWEPT";
| |
col = clrSilver;
| |
}
| |
| |
DrawLevel(K_POOL(p.id), p.leftBar, toIndex, p.price, txt, col, style);
| |
}
| |
| |
void DrawPools() {
| |
for(int i = 0; i < ArraySize(pools); i++)
| |
DrawPool(pools[i]);
| |
}
| |
| |
void DrawERLAnchorsOnly() {
| |
int end = bar.Size() - 1;
| |
| |
if(IsValidBar(protectedHigh)) {
| |
DrawLevel(K_ERL_HIGH(), protectedHigh, end, bar.High(protectedHigh), "ERL HIGH", clrTomato, 1);
| |
}
| |
| |
if(IsValidBar(protectedLow)) {
| |
DrawLevel(K_ERL_LOW(), protectedLow, end, bar.Low(protectedLow), "ERL LOW", clrDeepSkyBlue, 1);
| |
}
| |
}
| |
| |
public:
| |
C_Chapter3();
| |
~C_Chapter3() {}
| |
| |
void SetExternalRange(int protectedHighIndex, int protectedLowIndex) {
| |
protectedHigh = protectedHighIndex;
| |
protectedLow = protectedLowIndex;
| |
}
| |
| |
void OnCalculate();
| |
int OnInit(string s, ENUM_TIMEFRAMES tf_, int lr = 3, int look = 1);
| |
};
| |
| |
//+------------------------------------------------------------------+
| |
//| |
| |
//+------------------------------------------------------------------+
| |
C_Chapter3::C_Chapter3() {
| |
nextImbId = 1;
| |
nextPoolId = 1;
| |
protectedHigh = -1;
| |
protectedLow = -1;
| |
| |
ArrayResize(imbalances, 0);
| |
ArrayResize(pools, 0);
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| |
| |
//+------------------------------------------------------------------+
| |
int C_Chapter3::OnInit(string s, ENUM_TIMEFRAMES tf_, int lr = 3, int look = 1) {
| |
timeframe = tf_;
| |
symbol = s;
| |
debug.Init(0, "Chapter3", true);
| |
| |
if(fairValue.Init("Fair Value Gaps", s, tf_, lr, look) == -1) {
| |
Print("Error creating Fair Value indicator: ", GetLastError());
| |
return INIT_FAILED;
| |
}
| |
| |
return INIT_SUCCEEDED;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| |
| |
//+------------------------------------------------------------------+
| |
void C_Chapter3::OnCalculate() {
| |
if(OnNewBar())
| |
return;
| |
| |
bar.UpdateFromMonthsBack(symbol, timeframe, 3);
| |
| |
ArrayResize(imbalances, 0);
| |
ArrayResize(pools, 0);
| |
| |
nextImbId = 1;
| |
nextPoolId = 1;
| |
| |
DetectImbalances();
| |
UpdateImbalanceMitigation();
| |
| |
DetectEqualHighLowPools();
| |
DetectStructureLiquidity();
| |
DetectExternalRangeLiquidity();
| |
UpdatePoolSweeps();
| |
| |
DrawERLAnchorsOnly();
| |
DrawPools();
| |
DrawImbalances();
| |
}
| |
//+------------------------------------------------------------------+ |