277 行
10 KiB
MQL5
#property strict
| |||
#property indicator_separate_window
| |||
#property indicator_plots 0
| |||
#property indicator_buffers 0
| |||
| |||
input int BoxSizePoints = 40;
| |||
input int InstanceID = 1;
| |||
input int MaxBricks = 3000;
| |||
input int BrickWidthPx = 10;
| |||
input int BrickGapPx = 1;
| |||
input int LeftMarginPx = 8;
| |||
input int RightMarginPx = 8;
| |||
input int TopMarginPx = 8;
| |||
input int BottomMarginPx = 8;
| |||
input datetime AnchorDateTime = D'2026.01.01 00:00';
| |||
| |||
struct RenkoBrick
| |||
{
| |||
double open;
| |||
double close;
| |||
int direction;
| |||
};
| |||
| |||
RenkoBrick Bricks[];
| |||
double BoxSize=0.0;
| |||
bool HasAnchor=false;
| |||
double Anchor=0.0;
| |||
double LastClose=0.0;
| |||
int LastDirection=0;
| |||
ulong LastTickMsc=0;
| |||
double LastTickBid=0.0;
| |||
int SubWindow=-1;
| |||
bool HistoryLoaded=false;
| |||
int HistoryAttempts=0;
| |||
datetime HistoryFrom=0;
| |||
datetime HistoryTo=0;
| |||
int LastCopiedTicks=-1;
| |||
int StableCopyCount=0;
| |||
| |||
string ObjectPrefix(){ return "RENKO40_I"+IntegerToString(InstanceID)+"_"; }
| |||
string IndicatorName(){ return "Renko 40 #"+IntegerToString(InstanceID); }
| |||
| |||
void AddBrick(const double open_price,const double close_price,const int direction)
| |||
{
| |||
int count=ArraySize(Bricks);
| |||
if(count>=MaxBricks)
| |||
{
| |||
for(int i=1;i<count;i++) Bricks[i-1]=Bricks[i];
| |||
count--;
| |||
ArrayResize(Bricks,count);
| |||
}
| |||
ArrayResize(Bricks,count+1);
| |||
Bricks[count].open=open_price;
| |||
Bricks[count].close=close_price;
| |||
Bricks[count].direction=direction;
| |||
}
| |||
| |||
void ProcessPrice(const double price)
| |||
{
| |||
if(price<=0.0) return;
| |||
if(!HasAnchor){ Anchor=price; HasAnchor=true; return; }
| |||
| |||
if(LastDirection==0)
| |||
{
| |||
if(price>=Anchor+BoxSize){ AddBrick(Anchor,Anchor+BoxSize,+1); LastClose=Anchor+BoxSize; LastDirection=+1; }
| |||
else if(price<=Anchor-BoxSize){ AddBrick(Anchor,Anchor-BoxSize,-1); LastClose=Anchor-BoxSize; LastDirection=-1; }
| |||
return;
| |||
}
| |||
| |||
if(LastDirection>0)
| |||
{
| |||
while(price>=LastClose+BoxSize){ AddBrick(LastClose,LastClose+BoxSize,+1); LastClose+=BoxSize; }
| |||
if(price<=LastClose-2.0*BoxSize)
| |||
{
| |||
AddBrick(LastClose-BoxSize,LastClose-2.0*BoxSize,-1);
| |||
LastClose-=2.0*BoxSize; LastDirection=-1;
| |||
while(price<=LastClose-BoxSize){ AddBrick(LastClose,LastClose-BoxSize,-1); LastClose-=BoxSize; }
| |||
}
| |||
return;
| |||
}
| |||
| |||
while(price<=LastClose-BoxSize){ AddBrick(LastClose,LastClose-BoxSize,-1); LastClose-=BoxSize; }
| |||
if(price>=LastClose+2.0*BoxSize)
| |||
{
| |||
AddBrick(LastClose+BoxSize,LastClose+2.0*BoxSize,+1);
| |||
LastClose+=2.0*BoxSize; LastDirection=+1;
| |||
while(price>=LastClose+BoxSize){ AddBrick(LastClose,LastClose+BoxSize,+1); LastClose+=BoxSize; }
| |||
}
| |||
}
| |||
| |||
void ProcessTick(const ulong time_msc,const double bid)
| |||
{
| |||
if(bid<=0.0) return;
| |||
if(time_msc==LastTickMsc && bid==LastTickBid) return;
| |||
LastTickMsc=time_msc; LastTickBid=bid; ProcessPrice(bid);
| |||
}
| |||
| |||
void DeleteObjects()
| |||
{
| |||
if(SubWindow<0) return;
| |||
string prefix=ObjectPrefix();
| |||
int total=ObjectsTotal(0,SubWindow,-1);
| |||
for(int i=total-1;i>=0;i--)
| |||
{
| |||
string name=ObjectName(0,i,SubWindow,-1);
| |||
if(StringFind(name,prefix)==0) ObjectDelete(0,name);
| |||
}
| |||
}
| |||
| |||
int GetEffectiveRightMarginPx(const int width)
| |||
{
| |||
int margin=RightMarginPx;
| |||
bool chart_shift=(bool)ChartGetInteger(0,CHART_SHIFT,0);
| |||
if(chart_shift)
| |||
{
| |||
double shift_size=ChartGetDouble(0,CHART_SHIFT_SIZE,0);
| |||
if(shift_size>0.0) margin+=(int)MathRound(width*shift_size/100.0);
| |||
}
| |||
return margin;
| |||
}
| |||
| |||
void DrawBrick(const int index,const int slot,const double min_price,const double max_price,const int height)
| |||
{
| |||
if(index<0 || index>=ArraySize(Bricks)) return;
| |||
if(height<=TopMarginPx+BottomMarginPx+5) return;
| |||
double top=MathMax(Bricks[index].open,Bricks[index].close);
| |||
double bottom=MathMin(Bricks[index].open,Bricks[index].close);
| |||
double range=max_price-min_price;
| |||
if(range<=0.0) return;
| |||
double usable=height-TopMarginPx-BottomMarginPx;
| |||
int y1=TopMarginPx+(int)MathRound((max_price-top)/range*usable);
| |||
int y2=TopMarginPx+(int)MathRound((max_price-bottom)/range*usable);
| |||
int brick_height=y2-y1; if(brick_height<2) brick_height=2;
| |||
int x=LeftMarginPx+slot*(BrickWidthPx+BrickGapPx);
| |||
string name=ObjectPrefix()+IntegerToString(index);
| |||
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,SubWindow,0,0)) return;
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y1);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,BrickWidthPx);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,brick_height);
| |||
if(Bricks[index].direction>0){ ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrLimeGreen); ObjectSetInteger(0,name,OBJPROP_COLOR,clrGreen); }
| |||
else { ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrTomato); ObjectSetInteger(0,name,OBJPROP_COLOR,clrRed); }
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
}
| |||
| |||
void DrawRenko()
| |||
{
| |||
if(SubWindow<0) return;
| |||
int total=ArraySize(Bricks); if(total<=0) return;
| |||
int width=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
| |||
int height=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,SubWindow);
| |||
if(width<=0 || height<=0) return;
| |||
int step=BrickWidthPx+BrickGapPx; if(step<=0) return;
| |||
int effective_right_margin=GetEffectiveRightMarginPx(width);
| |||
int available_slots=(width-LeftMarginPx-effective_right_margin)/step;
| |||
if(available_slots<1) available_slots=1;
| |||
int visible=available_slots;
| |||
if(visible>total) visible=total;
| |||
int start=total-visible;
| |||
int slot_offset=available_slots-visible;
| |||
double min_price=DBL_MAX,max_price=-DBL_MAX;
| |||
for(int i=start;i<total;i++)
| |||
{
| |||
min_price=MathMin(min_price,MathMin(Bricks[i].open,Bricks[i].close));
| |||
max_price=MathMax(max_price,MathMax(Bricks[i].open,Bricks[i].close));
| |||
}
| |||
if(min_price==DBL_MAX || max_price==-DBL_MAX) return;
| |||
double range=max_price-min_price; if(range<=0.0) range=BoxSize;
| |||
double padding=range*0.05; min_price-=padding; max_price+=padding;
| |||
IndicatorSetDouble(INDICATOR_MINIMUM,min_price);
| |||
IndicatorSetDouble(INDICATOR_MAXIMUM,max_price);
| |||
DeleteObjects();
| |||
for(int i=start,slot=slot_offset;i<total;i++,slot++) DrawBrick(i,slot,min_price,max_price,height);
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void ResetRenkoState()
| |||
{
| |||
ArrayResize(Bricks,0); HasAnchor=false; Anchor=0.0; LastClose=0.0; LastDirection=0; LastTickMsc=0; LastTickBid=0.0;
| |||
}
| |||
| |||
bool PrepareHistoryRange()
| |||
{
| |||
if(AnchorDateTime<=0) return false;
| |||
datetime now=TimeCurrent(); if(now<=0) return false;
| |||
if(AnchorDateTime>now) return false;
| |||
HistoryFrom=AnchorDateTime;
| |||
HistoryTo=now;
| |||
return true;
| |||
}
| |||
| |||
bool LoadHistory()
| |||
{
| |||
if(HistoryFrom==0 || HistoryTo==0)
| |||
{
| |||
if(!PrepareHistoryRange()){ Print("Renko40 #",InstanceID,": invalid anchor date/time"); return false; }
| |||
}
| |||
| |||
MqlTick ticks[];
| |||
ResetLastError();
| |||
int copied=CopyTicksRange(_Symbol,ticks,COPY_TICKS_INFO,(ulong)HistoryFrom*1000,(ulong)HistoryTo*1000);
| |||
int error=GetLastError();
| |||
HistoryAttempts++;
| |||
| |||
if(copied<=0)
| |||
{
| |||
Print("Renko40 #",InstanceID,": history not ready. Attempt=",HistoryAttempts," Error=",error);
| |||
return false;
| |||
}
| |||
if(error!=0)
| |||
{
| |||
Print("Renko40 #",InstanceID,": history synchronization incomplete. Attempt=",HistoryAttempts," copied=",copied," Error=",error);
| |||
return false;
| |||
}
| |||
| |||
if(copied==LastCopiedTicks) StableCopyCount++; else StableCopyCount=0;
| |||
LastCopiedTicks=copied;
| |||
Print("Renko40 #",InstanceID,": CopyTicksRange attempt=",HistoryAttempts," copied=",copied," stable=",StableCopyCount);
| |||
if(StableCopyCount<2) return false;
| |||
| |||
ResetRenkoState();
| |||
int processed=0;
| |||
for(int i=0;i<copied;i++)
| |||
{
| |||
if(ticks[i].bid<=0.0) continue;
| |||
ProcessTick(ticks[i].time_msc,ticks[i].bid);
| |||
processed++;
| |||
}
| |||
LastTickMsc=ticks[copied-1].time_msc;
| |||
LastTickBid=ticks[copied-1].bid;
| |||
HistoryLoaded=true;
| |||
Print("Renko40 #",InstanceID,": HISTORICAL INITIALIZATION COMPLETE. AnchorDateTime=",TimeToString(AnchorDateTime,TIME_DATE|TIME_SECONDS)," Ticks=",copied," Processed=",processed," Bricks=",ArraySize(Bricks));
| |||
DrawRenko();
| |||
return true;
| |||
}
| |||
| |||
void ProcessCurrentTick()
| |||
{
| |||
if(!HistoryLoaded) return;
| |||
MqlTick tick; if(!SymbolInfoTick(_Symbol,tick)) return;
| |||
double bid=tick.bid; if(bid<=0.0) bid=tick.last; if(bid<=0.0) return;
| |||
ProcessTick(tick.time_msc,bid);
| |||
}
| |||
| |||
int OnInit()
| |||
{
| |||
if(InstanceID<1 || AnchorDateTime<=0 || AnchorDateTime>TimeCurrent()){ Print("Renko40: invalid parameters or AnchorDateTime"); return INIT_PARAMETERS_INCORRECT; }
| |||
BoxSize=BoxSizePoints*_Point;
| |||
if(BoxSize<=0.0){ Print("Renko40: invalid BoxSize"); return INIT_PARAMETERS_INCORRECT; }
| |||
IndicatorSetString(INDICATOR_SHORTNAME,IndicatorName());
| |||
ResetRenkoState(); HistoryLoaded=false; HistoryAttempts=0; HistoryFrom=0; HistoryTo=0; LastCopiedTicks=-1; StableCopyCount=0;
| |||
SubWindow=ChartWindowFind(); EventSetTimer(1);
| |||
Print("Renko40 Separate started. InstanceID=",InstanceID," SubWindow=",SubWindow," Box=",BoxSizePoints," points"," AnchorDateTime=",TimeToString(AnchorDateTime,TIME_DATE|TIME_SECONDS)," Prefix=",ObjectPrefix());
| |||
return INIT_SUCCEEDED;
| |||
}
| |||
| |||
void OnDeinit(const int reason){ EventKillTimer(); if(SubWindow>=0) DeleteObjects(); ChartRedraw(0); }
| |||
| |||
void OnTimer()
| |||
{
| |||
if(SubWindow<0) SubWindow=ChartWindowFind(); if(SubWindow<0) return;
| |||
if(!HistoryLoaded){ LoadHistory(); return; }
| |||
ProcessCurrentTick(); DrawRenko();
| |||
}
| |||
| |||
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(SubWindow<0) SubWindow=ChartWindowFind();
| |||
if(!HistoryLoaded){ LoadHistory(); return rates_total; }
| |||
ProcessCurrentTick();
| |||
return rates_total;
| |||
}
|