mql5/Indicators/Time at Price.mq5

600 lines
No EOL
24 KiB
MQL5

//+------------------------------------------------------------------+
//| MarketProfileTAP.mq5 |
//| Time-at-Price (TAP) Market Profile v1.0 |
//| |
//| Core mechanic: |
//| A 1-second timer fires every second. Price is sampled ONCE |
//| and ONE unit of time is credited to the corresponding price |
//| bucket. Rapid price oscillation within a second counts as |
//| ONE second — not many ticks. The result is a profile that |
//| shows WHERE price spent time, not how frantically it moved. |
//| |
//| Rolling window: credits older than InpWindowHours are expired |
//| from both the circular buffer and the live histogram. |
//| |
//| Persistence: the buffer is saved to a binary file on deinit |
//| and reloaded on init so the profile survives indicator reloads. |
//| It does NOT fill gaps from while the terminal was closed. |
//+------------------------------------------------------------------+
#property copyright "TAP Market Profile"
#property version "1.00"
#property description "Time-at-Price Market Profile | 1s sampling | rolling window"
#property indicator_chart_window
#property indicator_plots 0
//-------------------------------------------------------------------
// Enums
//-------------------------------------------------------------------
enum ENUM_TAP_PRICE
{
TAP_BID = 0, // Bid
TAP_ASK = 1, // Ask
TAP_MID = 2 // Mid (bid+ask)/2
};
enum ENUM_TAP_ASSIGN
{
TAP_LOWER = 0, // Lower bucket (floor)
TAP_UPPER = 1, // Upper bucket (ceiling of lower → next bucket up)
TAP_SPLIT = 2 // Split 50/50 between lower and upper bucket
};
enum ENUM_TAP_DISPLAY
{
TAP_BANDS = 0, // Full-width horizontal bands (like heatmap)
TAP_PANEL = 1, // Right-side panel bars (classic market profile)
TAP_BOTH = 2 // Both: bands behind + panel on right
};
//-------------------------------------------------------------------
// Inputs
//-------------------------------------------------------------------
input ENUM_TAP_PRICE InpPriceSource = TAP_MID; // Price source
input ENUM_TAP_ASSIGN InpBucketAssign = TAP_LOWER; // Bucket assignment
input int InpBucketPts = 10; // Bucket size (points)
input int InpWindowHours = 8; // Rolling window (hours)
input ENUM_TAP_DISPLAY InpDisplayMode = TAP_BOTH; // Display style
input int InpPanelBars = 40; // Panel max width (chart bars)
input int InpFutureBars = 10; // Right extension (bars, bands mode)
input bool InpShowPOC = true; // Highlight POC
input bool InpShowVA = true; // Show Value Area lines
input double InpVAPct = 70.0; // Value Area %
input color InpColLow = clrBlack; // Colour – low time
input color InpColMid = clrTeal; // Colour – mid time
input color InpColHigh = clrCrimson; // Colour – high time
input color InpColPOC = clrYellow; // Colour – POC line
input color InpColVA = clrGoldenrod; // Colour – VA lines
input bool InpPersist = true; // Save/load data to file
//-------------------------------------------------------------------
// Sizing limits
// MAX_BUF = 2 × 24h × 3600 s (factor-2 for split mode overhead)
// MAX_HIST = max distinct price buckets tracked simultaneously
//-------------------------------------------------------------------
#define MAX_BUF 172800
#define MAX_HIST 4000
//-------------------------------------------------------------------
// Object name prefixes / singletons
//-------------------------------------------------------------------
#define PFX_BAND "TAP_b_"
#define PFX_PNL "TAP_p_"
#define OBJ_POC "TAP_poc"
#define OBJ_VAH "TAP_vah"
#define OBJ_VAL "TAP_val"
#define OBJ_LBL "TAP_lbl"
//-------------------------------------------------------------------
// Circular buffer – rolling window of sampled seconds
//-------------------------------------------------------------------
datetime g_bTs [MAX_BUF]; // sample timestamp (GMT)
double g_bKey[MAX_BUF]; // bucket lower-boundary price
float g_bWt [MAX_BUF]; // weight: 1.0 or 0.5 (split)
int g_bHead = 0; // index of oldest valid entry
int g_bTail = 0; // index of next write slot
int g_bCount = 0; // valid entries in use
//-------------------------------------------------------------------
// Live histogram – sorted by bucket key for O(log n) lookup
//-------------------------------------------------------------------
double g_hKey[MAX_HIST]; // bucket lower-boundary price (sorted asc)
double g_hCnt[MAX_HIST]; // accumulated seconds (fractional for split)
int g_hSz = 0; // number of active buckets
//-------------------------------------------------------------------
// Derived config + state
//-------------------------------------------------------------------
double g_bucketSize = 0.0; // bucket height in price units
int g_windowSecs = 0; // InpWindowHours in seconds
datetime g_lastSample = 0; // timestamp of last successful sample
//+------------------------------------------------------------------+
//| Delete chart objects whose name begins with prefix |
//+------------------------------------------------------------------+
void PurgePrefix(const string pfx)
{
for(int i = ObjectsTotal(0,0,-1)-1; i >= 0; i--)
{
string n = ObjectName(0,i,0,-1);
if(StringFind(n, pfx) == 0) ObjectDelete(0, n);
}
}
//+------------------------------------------------------------------+
//| Three-stop colour blend |
//+------------------------------------------------------------------+
color ThermalColor(double ratio)
{
ratio = MathMax(0.0, MathMin(1.0, ratio));
color c1, c2; double t;
if(ratio < 0.5){ c1=InpColLow; c2=InpColMid; t=ratio*2.0; }
else { c1=InpColMid; c2=InpColHigh; t=(ratio-0.5)*2.0; }
int r=(int)(( c1 &0xFF)*(1-t)+( c2 &0xFF)*t);
int g=(int)(((c1>> 8)&0xFF)*(1-t)+((c2>> 8)&0xFF)*t);
int b=(int)(((c1>>16)&0xFF)*(1-t)+((c2>>16)&0xFF)*t);
return (color)(r|(g<<8)|(b<<16));
}
//+------------------------------------------------------------------+
//| Upsert a filled rectangle |
//+------------------------------------------------------------------+
void UpsertRect(const string nm, datetime t1, double p1,
datetime t2, double p2, color clr, bool back)
{
if(ObjectFind(0,nm)<0) ObjectCreate(0,nm,OBJ_RECTANGLE,0,t1,p1,t2,p2);
ObjectMove(0,nm,0,t1,p1); ObjectMove(0,nm,1,t2,p2);
ObjectSetInteger(0,nm,OBJPROP_COLOR, clr);
ObjectSetInteger(0,nm,OBJPROP_FILL, true);
ObjectSetInteger(0,nm,OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0,nm,OBJPROP_WIDTH, 1);
ObjectSetInteger(0,nm,OBJPROP_BACK, back);
ObjectSetInteger(0,nm,OBJPROP_SELECTABLE, false);
ObjectSetInteger(0,nm,OBJPROP_HIDDEN, true);
}
//+------------------------------------------------------------------+
//| Upsert a horizontal line |
//+------------------------------------------------------------------+
void UpsertHLine(const string nm, double price, color clr,
int w, ENUM_LINE_STYLE sty)
{
if(ObjectFind(0,nm)<0) ObjectCreate(0,nm,OBJ_HLINE,0,0,price);
ObjectSetDouble (0,nm,OBJPROP_PRICE, price);
ObjectSetInteger(0,nm,OBJPROP_COLOR, clr);
ObjectSetInteger(0,nm,OBJPROP_WIDTH, w);
ObjectSetInteger(0,nm,OBJPROP_STYLE, sty);
ObjectSetInteger(0,nm,OBJPROP_BACK, false);
ObjectSetInteger(0,nm,OBJPROP_SELECTABLE, false);
ObjectSetInteger(0,nm,OBJPROP_HIDDEN, true);
}
//-------------------------------------------------------------------
// H I S T O G R A M H E L P E R S
//-------------------------------------------------------------------
//+------------------------------------------------------------------+
//| Binary search. Returns index if found, -(ins+1) if missing. |
//+------------------------------------------------------------------+
int HistFind(double key)
{
double eps = g_bucketSize * 0.001;
int lo=0, hi=g_hSz-1;
while(lo<=hi)
{
int mid=(lo+hi)/2;
if(MathAbs(g_hKey[mid]-key)<eps) return mid;
if(g_hKey[mid]<key) lo=mid+1;
else hi=mid-1;
}
return -(lo+1);
}
//+------------------------------------------------------------------+
//| Add weight to bucket – inserts if bucket is new |
//+------------------------------------------------------------------+
void HistAdd(double key, double w)
{
int idx=HistFind(key);
if(idx>=0){ g_hCnt[idx]+=w; return; }
int ins=-idx-1;
if(g_hSz>=MAX_HIST) return;
for(int i=g_hSz;i>ins;i--){ g_hKey[i]=g_hKey[i-1]; g_hCnt[i]=g_hCnt[i-1]; }
g_hKey[ins]=key; g_hCnt[ins]=w; g_hSz++;
}
//+------------------------------------------------------------------+
//| Remove weight from bucket – removes bucket when it reaches zero |
//+------------------------------------------------------------------+
void HistRemove(double key, double w)
{
int idx=HistFind(key);
if(idx<0) return;
g_hCnt[idx]-=w;
if(g_hCnt[idx]<1e-6)
{
for(int i=idx;i<g_hSz-1;i++){ g_hKey[i]=g_hKey[i+1]; g_hCnt[i]=g_hCnt[i+1]; }
g_hSz--;
}
}
//-------------------------------------------------------------------
// C I R C U L A R B U F F E R H E L P E R S
//-------------------------------------------------------------------
//+------------------------------------------------------------------+
//| Push one entry to the circular buffer and update histogram |
//+------------------------------------------------------------------+
void BufPush(datetime ts, double key, float wt)
{
// If buffer is completely full, silently expire the oldest entry
if(g_bCount>=MAX_BUF)
{
HistRemove(g_bKey[g_bHead], g_bWt[g_bHead]);
g_bHead=(g_bHead+1)%MAX_BUF;
g_bCount--;
}
g_bTs [g_bTail]=ts;
g_bKey[g_bTail]=key;
g_bWt [g_bTail]=(float)wt;
g_bTail=(g_bTail+1)%MAX_BUF;
g_bCount++;
HistAdd(key, wt);
}
//+------------------------------------------------------------------+
//| Remove all buffer entries older than the rolling window |
//+------------------------------------------------------------------+
void BufExpire()
{
datetime cutoff=TimeGMT()-(datetime)g_windowSecs;
while(g_bCount>0 && g_bTs[g_bHead]<cutoff)
{
HistRemove(g_bKey[g_bHead], g_bWt[g_bHead]);
g_bHead=(g_bHead+1)%MAX_BUF;
g_bCount--;
}
}
//-------------------------------------------------------------------
// S A M P L I N G
//-------------------------------------------------------------------
//+------------------------------------------------------------------+
//| Get current price based on InpPriceSource |
//+------------------------------------------------------------------+
double GetPrice()
{
double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
if(bid<=0.0 || ask<=0.0) return 0.0; // market closed / no quote
switch(InpPriceSource)
{
case TAP_BID: return bid;
case TAP_ASK: return ask;
default: return (bid+ask)*0.5;
}
}
//+------------------------------------------------------------------+
//| Compute lower-boundary key of the bucket containing 'price' |
//+------------------------------------------------------------------+
double LowerKey(double price)
{
return MathFloor(price/g_bucketSize)*g_bucketSize;
}
//+------------------------------------------------------------------+
//| Sample price and push to buffer |
//+------------------------------------------------------------------+
void SampleOnce()
{
double price=GetPrice();
if(price<=0.0) return;
datetime now=TimeGMT();
if(now==g_lastSample) return; // guard: only once per second
g_lastSample=now;
double lo=LowerKey(price);
double hi=lo+g_bucketSize;
switch(InpBucketAssign)
{
case TAP_LOWER: BufPush(now,lo,1.0f); break;
case TAP_UPPER: BufPush(now,hi,1.0f); break;
case TAP_SPLIT: BufPush(now,lo,0.5f);
BufPush(now,hi,0.5f); break;
}
}
//-------------------------------------------------------------------
// P E R S I S T E N C E
//-------------------------------------------------------------------
string FileName()
{
return StringFormat("TAP_%s_%d.dat",_Symbol,InpBucketPts);
}
//+------------------------------------------------------------------+
//| Save current buffer to binary file |
//| Format: [int32 count] [count × {int64 ts, double key, float wt}]|
//+------------------------------------------------------------------+
void SaveBuffer()
{
if(!InpPersist || g_bCount<=0) return;
int fh=FileOpen(FileName(),FILE_WRITE|FILE_BIN);
if(fh==INVALID_HANDLE) return;
FileWriteInteger(fh,g_bCount,INT_VALUE);
int idx=g_bHead;
for(int i=0;i<g_bCount;i++)
{
FileWriteLong (fh,(long)g_bTs [idx]);
FileWriteDouble(fh, g_bKey[idx]);
FileWriteFloat (fh, g_bWt [idx]);
idx=(idx+1)%MAX_BUF;
}
FileClose(fh);
}
//+------------------------------------------------------------------+
//| Load buffer from file, discarding entries outside window |
//+------------------------------------------------------------------+
void LoadBuffer()
{
if(!InpPersist) return;
if(!FileIsExist(FileName())) return;
int fh=FileOpen(FileName(),FILE_READ|FILE_BIN);
if(fh==INVALID_HANDLE) return;
datetime cutoff=TimeGMT()-(datetime)g_windowSecs;
int count=FileReadInteger(fh,INT_VALUE);
for(int i=0;i<count;i++)
{
datetime ts =(datetime)FileReadLong (fh);
double key= FileReadDouble(fh);
float wt = FileReadFloat (fh);
if(ts>=cutoff) BufPush(ts,key,wt); // only load if still in window
}
FileClose(fh);
}
//-------------------------------------------------------------------
// D R A W I N G
//-------------------------------------------------------------------
//+------------------------------------------------------------------+
//| Find POC index in histogram |
//+------------------------------------------------------------------+
int FindPOC()
{
int poc=0;
for(int i=1;i<g_hSz;i++) if(g_hCnt[i]>g_hCnt[poc]) poc=i;
return poc;
}
//+------------------------------------------------------------------+
//| Compute Value Area – returns true and fills vaLo/vaHi |
//+------------------------------------------------------------------+
bool CalcVA(int pocIdx, int &vaLo, int &vaHi)
{
if(g_hSz<=0) return false;
double tot=0.0; for(int i=0;i<g_hSz;i++) tot+=g_hCnt[i];
double target=tot*(InpVAPct/100.0);
vaLo=pocIdx; vaHi=pocIdx;
double acc=g_hCnt[pocIdx];
while(acc<target)
{
double aHi=(vaHi<g_hSz-1)?g_hCnt[vaHi+1]:0.0;
double aLo=(vaLo>0) ?g_hCnt[vaLo-1]:0.0;
if(aHi==0.0&&aLo==0.0) break;
if(aHi>=aLo){vaHi++;acc+=g_hCnt[vaHi];}
else {vaLo--;acc+=g_hCnt[vaLo];}
}
return true;
}
//+------------------------------------------------------------------+
//| Full redraw |
//+------------------------------------------------------------------+
void DrawAll()
{
if(g_hSz<=0)
{
// Nothing sampled yet – show a waiting label and exit
if(ObjectFind(0,OBJ_LBL)<0)
ObjectCreate(0,OBJ_LBL,OBJ_LABEL,0,0,0);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_XDISTANCE,6);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_YDISTANCE,4);
ObjectSetString (0,OBJ_LBL,OBJPROP_TEXT,
"TAP Market Profile – sampling… (0 s recorded)");
ObjectSetInteger(0,OBJ_LBL,OBJPROP_COLOR, clrSilver);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_FONTSIZE, 8);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_HIDDEN, true);
ChartRedraw(0);
return;
}
// ----- shared geometry -----------------------------------------
int totalBars=Bars(_Symbol,PERIOD_CURRENT);
if(totalBars<2) return;
long firstVis =ChartGetInteger(0,CHART_FIRST_VISIBLE_BAR);
int barLeft =(int)MathMin(firstVis,totalBars-2);
datetime tLeft =iTime(_Symbol,PERIOD_CURRENT,barLeft);
datetime tRight =iTime(_Symbol,PERIOD_CURRENT,0);
if(tLeft==0||tRight==0) return;
int pSec =PeriodSeconds(PERIOD_CURRENT);
datetime tEnd =tRight+(datetime)(InpFutureBars*pSec);
datetime panelR =tRight+(datetime)(InpFutureBars*pSec);
datetime panelMax=(datetime)(InpPanelBars*pSec);
// ----- max count for normalisation ----------------------------
double maxCnt=0.0;
for(int i=0;i<g_hSz;i++) if(g_hCnt[i]>maxCnt) maxCnt=g_hCnt[i];
if(maxCnt<=0.0) return;
// ----- POC / VA -----------------------------------------------
int pocIdx=FindPOC();
int vaLo=pocIdx,vaHi=pocIdx;
if(InpShowVA) CalcVA(pocIdx,vaLo,vaHi);
// ----- purge stale objects ------------------------------------
PurgePrefix(PFX_BAND);
PurgePrefix(PFX_PNL);
// ----- draw each bucket ---------------------------------------
for(int i=0;i<g_hSz;i++)
{
double pBot=g_hKey[i];
double pTop=pBot+g_bucketSize;
double ratio=g_hCnt[i]/maxCnt;
// Slight brightness boost for Value Area rows
double drawR=ratio;
if(InpShowVA && i>=vaLo && i<=vaHi)
drawR=MathMin(1.0,ratio*1.15);
color clr=ThermalColor(drawR);
// --- BANDS mode ---
if(InpDisplayMode==TAP_BANDS || InpDisplayMode==TAP_BOTH)
UpsertRect(PFX_BAND+IntegerToString(i),
tLeft,pBot,tEnd,pTop,clr,true);
// --- PANEL mode ---
if(InpDisplayMode==TAP_PANEL || InpDisplayMode==TAP_BOTH)
{
datetime barW =(datetime)(ratio*(double)panelMax);
datetime barL =panelR-barW;
// In BOTH mode use a brighter version for the panel overlay
color pClr=(InpDisplayMode==TAP_BOTH)?ThermalColor(MathMin(1.0,ratio*1.4)):clr;
UpsertRect(PFX_PNL+IntegerToString(i),
barL,pBot,panelR,pTop,pClr,false);
}
}
// ----- POC line -----------------------------------------------
if(InpShowPOC)
UpsertHLine(OBJ_POC, g_hKey[pocIdx]+g_bucketSize*0.5,
InpColPOC, 2, STYLE_SOLID);
else ObjectDelete(0,OBJ_POC);
// ----- VA lines -----------------------------------------------
if(InpShowVA)
{
UpsertHLine(OBJ_VAH, g_hKey[vaHi]+g_bucketSize, InpColVA, 1, STYLE_DASH);
UpsertHLine(OBJ_VAL, g_hKey[vaLo], InpColVA, 1, STYLE_DASH);
}
else{ ObjectDelete(0,OBJ_VAH); ObjectDelete(0,OBJ_VAL); }
// ----- info label ---------------------------------------------
if(ObjectFind(0,OBJ_LBL)<0)
ObjectCreate(0,OBJ_LBL,OBJ_LABEL,0,0,0);
double pocPrice=g_hKey[pocIdx]+g_bucketSize*0.5;
double totalSec=0.0; for(int i=0;i<g_hSz;i++) totalSec+=g_hCnt[i];
string src=(InpPriceSource==TAP_BID)?"Bid":(InpPriceSource==TAP_ASK)?"Ask":"Mid";
string asgn=(InpBucketAssign==TAP_LOWER)?"Lower":(InpBucketAssign==TAP_UPPER)?"Upper":"Split";
string disp=(InpDisplayMode==TAP_BANDS)?"Bands":(InpDisplayMode==TAP_PANEL)?"Panel":"Both";
string info=StringFormat(
"TAP Market Profile | %s %s | Bucket: %d pts"
" | Window: %dh | Sampled: %.0f s | Buckets: %d | POC: %s | Mode: %s",
src, asgn, InpBucketPts, InpWindowHours,
totalSec, g_hSz,
DoubleToString(pocPrice,_Digits),
disp);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_XDISTANCE, 6);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_YDISTANCE, 4);
ObjectSetString (0,OBJ_LBL,OBJPROP_TEXT, info);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_COLOR, (color)0x00C0C0C0);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_FONTSIZE, 7);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_SELECTABLE, false);
ObjectSetInteger(0,OBJ_LBL,OBJPROP_HIDDEN, true);
ChartRedraw(0);
}
//-------------------------------------------------------------------
// L I F E C Y C L E
//-------------------------------------------------------------------
int OnInit()
{
// Validate inputs
if(InpBucketPts<=0)
{ Alert("TAP: InpBucketPts must be > 0"); return INIT_PARAMETERS_INCORRECT; }
if(InpWindowHours<=0 || InpWindowHours>240)
{ Alert("TAP: InpWindowHours must be 1-240"); return INIT_PARAMETERS_INCORRECT; }
g_bucketSize = InpBucketPts * _Point;
g_windowSecs = InpWindowHours * 3600;
// Warm the symbol data feed
double tmp=SymbolInfoDouble(_Symbol,SYMBOL_BID);
// Load persisted data (replays the buffer through the window filter)
LoadBuffer();
BufExpire(); // prune anything older than window before first draw
EventSetTimer(1); // 1-second heartbeat
DrawAll();
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
EventKillTimer();
SaveBuffer();
PurgePrefix(PFX_BAND);
PurgePrefix(PFX_PNL);
ObjectDelete(0,OBJ_POC);
ObjectDelete(0,OBJ_VAH);
ObjectDelete(0,OBJ_VAL);
ObjectDelete(0,OBJ_LBL);
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| 1-second heartbeat |
//| 1. Sample price (the core TAP logic) |
//| 2. Expire entries outside the rolling window |
//| 3. Redraw |
//+------------------------------------------------------------------+
void OnTimer()
{
SampleOnce();
BufExpire();
DrawAll();
}
//+------------------------------------------------------------------+
//| OnCalculate – not used for logic but required by MQL5 |
//+------------------------------------------------------------------+
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[])
{
return rates_total;
}
//+------------------------------------------------------------------+
//| Scroll / zoom triggers a visual refresh (no re-sampling) |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lp,const double &dp,const string &sp)
{
if(id==CHARTEVENT_CHART_CHANGE) DrawAll();
}
//+------------------------------------------------------------------+