558 lines
16 KiB
MQL5
558 lines
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();
| |||
}
| |||
//+------------------------------------------------------------------+
|