//+------------------------------------------------------------------+
//| Point-and-Figure.mq5 |
//| Copyright © 2025|
//+------------------------------------------------------------------+
#property copyright "Copyright © 2025"
#property link "https://t.me/ForexEaPremium"
#property version "2.01"
#property description "Point-and-Figure is a basic point-and-figure charting indicator based on real ticks data."
#property description "It supports full customization and all types of alerts."
#property indicator_chart_window
#property indicator_plots 0
// Define directions:
#define NONE 0
#define UP 1
#define DOWN -1
enum enum_price
{
Bid,
Ask,
Midprice,
BidAsk // Bid/Ask
};
input int BoxSize = 60; // Box size, points
input int Reversal = 3; // Number of boxes for reversal
input int Days = 1; // Days to look back.
input enum_price PriceToUse = Bid; // Price to use
input bool AlertOnXO = false; // Alert on new X/O
input bool AlertOnReversal = false; // Alert on reversal
input bool EnableNativeAlerts = false; // Enable native alerts
input bool EnableEmailAlerts = false; // Enable email alerts
input bool EnablePushAlerts = false; // Enable push-notification alerts
input color ColorUp = clrGreen; // X Color
input color ColorDown = clrRed; // O Color
input int FontSize = 15; // Font size
input string Font = "Arial"; // Font
input string X = "x"; // Up Symbol
input string O = "o"; // Down Symbol
input bool SilentMode = true; // Don't print information about each X/O
input int MaxObjects = 10000; // Maximum allowed number of X/O chart objects
input string ObjectPrefix = "PNF-";
// Converted parameters:
double cBoxSize;
double cReversal;
double LastPrice; // Stores last price where a box was drawn.
double CurrentDirection;
double FirstBoxUp;
double FirstBoxDown;
ulong LastEndTime_msc;
int Number; // Number of objects.
int LastBars; // Number of bars.
datetime LastBarTime; // To check if it were some old bars downloading or new ones.
int OnInit()
{
Number = 0;
LastBars = 0;
LastPrice = 0;
CurrentDirection = NONE;
FirstBoxUp = 0;
FirstBoxDown = 0;
LastEndTime_msc = 0;
LastBarTime = 0;
cBoxSize = BoxSize * _Point;
cReversal = Reversal * cBoxSize;
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
ObjectsDeleteAll(ChartID(), ObjectPrefix);
ChartRedraw();
}
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 (iBarShift(Symbol(), Period(), iTime(Symbol(), Period(), 0), true) < 0) // iBarShift failure.
{
// When chart data is in normal state, there shouldn't be an error when searching for the current bar with iBarShift.
Print("Syncing...");
return prev_calculated;
}
// rates_total is unreliable - sometimes, if the number of bars becomes greater than max number of bars possible, and a lot of new bars arrive, rates_total gets reset to that max number.
if (Number > 0) // At least one object exists.
{
if (Number > MaxObjects) Print("Warning: Too many X/O objects!");
datetime pt = (int)ObjectGetInteger(ChartID(), ObjectPrefix + IntegerToString(Number - 1), OBJPROP_TIME, 0);
int bar = iBarShift(Symbol(), Period(), pt, true);
if (bar < 0) // Error.
{
Print("Syncing...");
return prev_calculated;
}
if (bar > 0) // Objects should be moved.
{
Print("Moving all objects ", bar, " times.");
while (bar > 0)
{
if (!MoveAllRight()) return prev_calculated; // Failed to move. Should wait for the chart to load fully.
bar--;
}
}
}
MqlTick ticks_array[];
int end_time_seconds = (int)TimeCurrent();
ulong begin_time_msc;
if (LastEndTime_msc == 0)
{
begin_time_msc = ulong(end_time_seconds - Days * 24 * 3600) * 1000; // First time.
if (begin_time_msc / 1000 < (ulong)Time[0]) // Time[0] - oldest bar.
{
Print("Requested Days go beyond the available chart data. Either reduce Days or load more bars.");
return prev_calculated;
}
}
else begin_time_msc = LastEndTime_msc + 1;
// CopyTicks() has inconsistent behavior, so everything is handled with CopyTicksRange().
int n = CopyTicksRange(Symbol(), ticks_array, COPY_TICKS_ALL, begin_time_msc, (ulong)end_time_seconds * 1000);
if (n < 0)
{
Print("Waiting for ticks... ");
return prev_calculated;
}
else if (n == 0)
{
return rates_total; // No new ticks.
}
else
{
LastEndTime_msc = ticks_array[n - 1].time_msc;
}
for (int i = 0; i < n; i++)
{
double Price = 0, B = 0, A = 0;
switch (PriceToUse)
{
case Bid:
Price = ticks_array[i].bid;
break;
case Ask:
Price = ticks_array[i].ask;
break;
case Midprice:
Price = (ticks_array[i].bid + ticks_array[i].ask) / 2;
break;
case BidAsk: // Difficult case - use Bid for X's and Ask for O's.
B = ticks_array[i].bid;
A = ticks_array[i].ask;
break;
default:
Price = 0;
A = 0;
B = 0;
break;
}
if (CurrentDirection == NONE)
{
// Draw first X:
if (PriceToUse == BidAsk) Price = B;
if ((Price >= FirstBoxUp) && (FirstBoxUp != 0))
{
LastPrice = FirstBoxUp;
while (LastPrice <= Price) // Grow a stack of X's until the next X would be above the current Bid.
{
DrawX(LastPrice);
if ((AlertOnXO) && (TimeCurrent() - ticks_array[i].time <= 5)) DoAlerts("X"); // Within last 5 seconds.
LastPrice = NormalizeDouble(LastPrice + cBoxSize, _Digits);
}
}
// Draw first O:
else
{
if (PriceToUse == BidAsk) Price = A;
if ((Price <= FirstBoxDown) && (FirstBoxDown != 0))
{
LastPrice = FirstBoxDown;
while (LastPrice >= Price) // Put down O's until the next O would be below the current Bid.
{
DrawO(LastPrice);
if ((AlertOnXO) && (TimeCurrent() - ticks_array[i].time <= 5)) DoAlerts("O"); // Within last 5 seconds.
LastPrice = NormalizeDouble(LastPrice - cBoxSize, _Digits);
}
}
}
// Set boundaries for the first X/O:
if (FirstBoxUp == 0)
{
if (PriceToUse == BidAsk) Price = B;
FirstBoxUp = NormalizeDouble(MathCeil(Price / cBoxSize) * cBoxSize, _Digits);
}
if (FirstBoxDown == 0)
{
if (PriceToUse == BidAsk) Price = A;
FirstBoxDown = NormalizeDouble(MathFloor(Price / cBoxSize) * cBoxSize, _Digits);
}
}
// Subsequent X/O should be drawn on normalized levels only.
else if (CurrentDirection == UP)
{
if (PriceToUse == BidAsk) Price = B;
while (Price >= LastPrice) // Grow a stack of X's until the next X would be above the current Bid.
{
DrawX(LastPrice);
if ((AlertOnXO) && (TimeCurrent() - ticks_array[i].time <= 5)) DoAlerts("X"); // Within last 5 seconds.
LastPrice = NormalizeDouble(LastPrice + cBoxSize, _Digits);
}
if (Price <= LastPrice - cBoxSize - cReversal) // - cBoxSize because LastPrice has already been incremented.
{
if ((AlertOnReversal) && (TimeCurrent() - ticks_array[i].time <= 5)) DoAlerts("X->O Reversal"); // Within last 5 seconds.
LastPrice = NormalizeDouble(LastPrice - 2 * cBoxSize, _Digits); // First reversal O is drawn below the last X.
while (Price <= LastPrice)
{
DrawO(LastPrice);
LastPrice = NormalizeDouble(LastPrice - cBoxSize, _Digits);
}
}
}
else if (CurrentDirection == DOWN)
{
if (PriceToUse == BidAsk) Price = A;
while (Price <= LastPrice) // Put down O's until the next O would be below the current Bid.
{
DrawO(LastPrice);
if ((AlertOnXO) && (TimeCurrent() - ticks_array[i].time <= 5)) DoAlerts("O"); // Within last 5 seconds.
LastPrice = NormalizeDouble(LastPrice - cBoxSize, _Digits);
}
if (Price >= LastPrice + cBoxSize + cReversal)
{
if ((AlertOnReversal) && (TimeCurrent() - ticks_array[i].time <= 5)) DoAlerts("O->X Reversal"); // Within last 5 seconds.
LastPrice = NormalizeDouble(LastPrice + cBoxSize * 2, _Digits); // First reversal X is drawn above the last O.
while (Price >= LastPrice)
{
DrawX(LastPrice);
LastPrice = NormalizeDouble(LastPrice + cBoxSize, _Digits);
}
}
}
if (Number > MaxObjects) break;
}
return rates_total;
}
void DrawX(double price)
{
if (CurrentDirection == DOWN) MoveAllLeft(); // Reversal - need to form a new column.
ObjectCreate(ChartID(), ObjectPrefix + IntegerToString(Number), OBJ_TEXT, 0, iTime(Symbol(), Period(), 0), price);
ObjectSetString(ChartID(), ObjectPrefix + IntegerToString(Number), OBJPROP_TEXT, X);
ObjectSetInteger(ChartID(), ObjectPrefix + IntegerToString(Number), OBJPROP_FONTSIZE, FontSize);
ObjectSetString(ChartID(), ObjectPrefix + IntegerToString(Number), OBJPROP_FONT, Font);
ObjectSetInteger(ChartID(), ObjectPrefix + IntegerToString(Number), OBJPROP_COLOR, ColorUp);
Number++;
CurrentDirection = UP;
if (!SilentMode) Print("X: ", price);
}
void DrawO(double price)
{
if (CurrentDirection == UP) MoveAllLeft(); // Reversal - need to form a new column
ObjectCreate(ChartID(), ObjectPrefix + IntegerToString(Number), OBJ_TEXT, 0, iTime(Symbol(), Period(), 0), price);
ObjectSetString(ChartID(), ObjectPrefix + IntegerToString(Number), OBJPROP_TEXT, O);
ObjectSetInteger(ChartID(), ObjectPrefix + IntegerToString(Number), OBJPROP_FONTSIZE, FontSize);
ObjectSetString(ChartID(), ObjectPrefix + IntegerToString(Number), OBJPROP_FONT, Font);
ObjectSetInteger(ChartID(), ObjectPrefix + IntegerToString(Number), OBJPROP_COLOR, ColorDown);
Number++;
CurrentDirection = DOWN;
if (!SilentMode) Print("O: ", price);
}
bool MoveAllRight()
{
if (!SilentMode) Print("Moving all X/O to the right...");
for (int i = 0; i < Number; i++)
{
datetime pt = (int)ObjectGetInteger(ChartID(), ObjectPrefix + IntegerToString(i), OBJPROP_TIME, 0);
int bar = iBarShift(Symbol(), Period(), pt, true);
if (bar > 0) bar--;
else if (bar == 0) Print("Can't move right!");
else if (bar < 0)
{
Print("iBarShift error! ", __FUNCTION__, " ", pt);
return false;
}
pt = iTime(Symbol(), Period(), bar);
ObjectSetInteger(ChartID(), ObjectPrefix + IntegerToString(i), OBJPROP_TIME, 0, (int)pt);
}
return true;
}
void MoveAllLeft()
{
if (!SilentMode) Print("Moving all X/O to the left...");
int max_bar = iBars(Symbol(), Period()) - 1;
for (int i = 0; i < Number; i++)
{
datetime pt = (datetime)ObjectGetInteger(ChartID(), ObjectPrefix + IntegerToString(i), OBJPROP_TIME, 0);
int bar = iBarShift(Symbol(), Period(), pt, true);
if (bar == -1)
{
Print("iBarShift error! ", __FUNCTION__, " ", pt);
return;
}
else if (bar < max_bar) bar++;
else
{
Print("Failed to increment the bar value: bar ", bar, " >= ", max_bar);
return;
}
pt = iTime(Symbol(), Period(), bar);
if (pt == 0) Print(bar);
ObjectSetInteger(ChartID(), ObjectPrefix + IntegerToString(i), OBJPROP_TIME, 0, (int)pt);
}
}
void DoAlerts(string text)
{
string Text, TextNative;
Text = "Point-and-Figure: " + Symbol() + " - " + text + " @ " + DoubleToString(LastPrice, _Digits);
TextNative = text + " @ " + DoubleToString(LastPrice, _Digits);
if (EnableNativeAlerts) Alert(TextNative);
if (EnableEmailAlerts) SendMail("Point-and-Figure Alert", Text);
if (EnablePushAlerts) SendNotification(Text);
}
//+------------------------------------------------------------------+
Comments