{*********************************************}
{  TeeChart Pro Charting Library              }
{  Renko Financial Series                     }
{  Copyright (c) 2007-2020 by Marjan Slatinek }
{  All Rights Reserved                        }
{*********************************************}
unit TeeRenkoSeries;
{$I TeeDefs.inc}

// The Renko chart financial indicator.
//
// The Renko chart is a trend following technique.
// It got its name from a Japanese word "renga" meaning bricks.
// In Renko chart line brick is drawn in the direction of the prior move
// only if prices move by a minimum amount
// which is equivalent to the box size that are always equal in size.
//
// Renko charts are always based on the closing prices.
// Renko bricks are drawn after comparing, that day’s close
// with the previous brick (high or low). A "box size" which determines
// the minimum price change to show is specified.

interface

uses
  {$IFNDEF LINUX}
  {$IFNDEF FMX}
  Windows, Messages,
  {$ENDIF}
  {$ENDIF}
  SysUtils, Classes,

  {$IFDEF FMX}
  FMX.Types,

  {$IFDEF D19}
  FMX.Graphics,
  {$ENDIF}

  {$ELSE}
  Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls,
  {$ENDIF}

  {$IFDEF D17}
  System.UITypes,
  {$ENDIF} 
  
  {$IFDEF D17}
  System.UIConsts,
  {$ENDIF}

  {$IFDEF FMX}
  FMXTee.Procs, FMXTee.Engine, FMXTee.Canvas
  {$ELSE}
  TeCanvas, TeEngine, TeeProcs
  {$ENDIF};

type
  TRenkoSeries=class(TChartSeries)
  private
    FBoxSize   : Double;
    FDownSwing : TColor;
    FDraw3D    : Boolean;
    FUpSwing   : TColor;

    IMin   : Double;
    IMax   : Double;
    IBlend : TTeeBlend;

    function CalcBricks(Draw:Boolean):Integer;
    procedure CompareMinMax(const Value:Double);
    function GetCloseValues:TChartValueList;
    procedure SetBoxSize(const Value:Double);
    procedure SetDownSwing(const Value:TColor);
    procedure SetDraw3D(const Value:Boolean);
    procedure SetUpSwing(const Value:TColor);
  protected
    Procedure AddSampleValues(NumValues:Integer; OnlyMandatory:Boolean=False); override;
    procedure AssignFormatting(Source:TChartSeries); override;
    function CalcNumBricks(const Current,Previous:Double):Integer;
    Procedure DrawAllValues; override;
    procedure DrawBrick(const low, high: Double; Index:Integer);
    procedure DrawBricks(const Start:Double; StartIndex,NumBricks:Integer; UpTrend:Boolean);
    class function GetEditorClass: String; override;
    Procedure PrepareForGallery; override;
  public
    Constructor Create(AOwner: TComponent); override;

    procedure Assign(Source:TPersistent); override;
    Function CountLegendItems:Integer; override;
    Function LegendItemColor(LegendIndex:Integer):TColor; override;
    Function LegendString( LegendIndex:Integer;
                           LegendTextStyle:TLegendTextStyle ):String; override;
    Function MaxXValue:TChartValue; override;
    Function MinXValue:TChartValue; override;
    Function MaxYValue:TChartValue; override;
    Function MinYValue:TChartValue; override;
    Function NumSampleValues:Integer; override;
    Procedure CalcFirstLastVisibleIndex; override;

    property CloseValues:TChartValueList read GetCloseValues;
  published
    property BoxSize:Double read FBoxSize write SetBoxSize;
    property DownSwingColor:TColor read FDownSwing write SetDownSwing
                 default {$IFDEF FMX}claBlack{$ELSE}clBlack{$ENDIF};
    property Draw3D:Boolean read FDraw3D write SetDraw3D default True;
    property Transparency;
    property UpSwingColor:TColor read FUpSwing write SetUpSwing
                 default {$IFDEF FMX}claWhite{$ELSE}clWhite{$ENDIF};

    { Published declarations }
    property Active;
    property Brush;
    property ColorSource;
    property Cursor;
    property Depth;
    property HorizAxis;
    property Marks;
    property ParentChart;
    property Pen;
    property DataSource;
    property PercentFormat;
    property SeriesColor;
    property ShowInLegend;
    property Title;
    property ValueFormat;
    property VertAxis;
    property XLabelsSource;
    property XValues;
    property YValues;

    { events }
    property AfterDrawValues;
    property BeforeDrawValues;
    property OnAfterAdd;
    property OnBeforeAdd;
    property OnClearValues;
    property OnClick;
    property OnDblClick;
    property OnGetMarkText;
    property OnMouseEnter;
    property OnMouseLeave;
  end;

implementation

uses
  Math,
  {$IFDEF FMX}
  FMXTee.ProConstants, FMXTee.Chart
  {$ELSE}
  TeeProCo, Chart
  {$ENDIF};

{ TRenkoSeries }
Constructor TRenkoSeries.Create(AOwner: TComponent);
begin
  inherited;
  FBoxSize:=1.0;
  FUpSwing:={$IFDEF FMX}claWhite{$ELSE}clWhite{$ENDIF};
  FDownSwing:={$IFDEF FMX}claBlack{$ELSE}clBlack{$ENDIF};
  FDraw3D:=True;

  XValues.DateTime:=False;
  AllowSinglePoint:=False;
end;

procedure TRenkoSeries.AssignFormatting(Source:TChartSeries);
begin
  if Source is TRenkoSeries then
  with TRenkoSeries(Source) do
  begin
    Self.FDownSwing:=FDownSwing;
    Self.FDraw3D:=FDraw3D;
    Self.FUpSwing:=FUpSwing;
  end;

  inherited;
end;

procedure TRenkoSeries.Assign(Source:TPersistent);
begin
  if Source is TRenkoSeries then
  with TRenkoSeries(Source) do
  begin
    Self.FBoxSize:=FBoxSize;
  end;

  inherited;
end;

procedure TRenkoSeries.DrawBrick(const low, high: Double; Index:Integer);
var tmp : TRect;
begin
  tmp.Left:=CalcXPosValue(index - 0.5);
  tmp.Right:=CalcXPosValue(index + 0.5);
  tmp.Top:=CalcYPosValue(high);
  tmp.Bottom:=CalcYPosValue(low);

  if Assigned(IBlend) then
     IBlend.SetRectangle(tmp);

  if ParentChart.View3D and FDraw3D then
     ParentChart.Canvas.Cube(tmp, StartZ, endZ, False)
  else
     ParentChart.Canvas.Rectangle(tmp, MiddleZ);

  if Assigned(IBlend) then
     IBlend.DoBlend(Transparency);
end;

procedure TRenkoSeries.SetDraw3D(const Value:Boolean);
begin
  SetBooleanProperty(FDraw3D, Value);
end;

procedure TRenkoSeries.DrawBricks(const Start:Double; StartIndex,NumBricks:Integer;
                                  UpTrend:Boolean);
var t : Integer;
begin
  with ParentChart.Canvas do
  begin
    if UpTrend then
       AssignBrush(Self.Brush,UpSwingColor)
    else
       AssignBrush(Self.Brush,DownSwingColor);

    AssignVisiblePen(Self.Pen);
  end;

  for t:=0 to NumBricks-1 do
      if UpTrend then
         DrawBrick(start + t * boxsize, start + (t + 1) * boxsize, t + startindex)
      else
         DrawBrick(start - (t + 1) * boxsize, start - t * boxsize, t + startindex);
end;

/// <summary>
/// Number of bricks
/// </summary>
/// <param name="cur">Current value</param>
/// <param name="pre">Previous value</param>
/// <returns>Number of bricks</returns>
function TRenkoSeries.CalcNumBricks(const current, previous : Double):Integer;
begin
  //result:=Round(SimpleRoundTo(Abs(current - previous) / FBoxSize));
  //A new Brick should not be plotted until the entirety of the Brick size has been equalled or exceeded.
  //Therefore recommend a trunc.
  result:=Trunc(Abs(current - previous) / FBoxSize);
end;

procedure TRenkoSeries.CompareMinMax(const Value:Double);
begin
  IMin:=Math.Min(IMin, Value);
  IMax:=Math.Max(IMax, Value);
end;

function TRenkoSeries.CalcBricks(Draw:Boolean):Integer;
var i,j : Integer;
    UpTrend, NoBrick   : Boolean;
    Reversal  : Boolean;
    BrickEdge : Double;
    NumBricks : Integer;
begin
  result:=0;

  IMin:=CloseValues.MinValue;
  IMax:=CloseValues.MaxValue;

  if Count > 1 then
  begin
    // First, we must construct initial brick
    i:=1;
    while (CalcNumBricks(CloseValues[i],CloseValues[0]) = 0) and (i < Count) do
      Inc(i);

    // At least one brick to draw
    if i < Count then
    begin
      IMin:=CloseValues[0];
      IMax:=IMin;

      UpTrend:= (CloseValues[i] - CloseValues[0]) > 0;

      //Reversal:=False;

      BrickEdge := CloseValues[0];
      NumBricks := CalcNumBricks(BrickEdge, CloseValues[i]);

      if Draw then
         DrawBricks(BrickEdge, result, NumBricks, UpTrend);

      if UpTrend then
         BrickEdge:=Brickedge + numbricks * boxsize
      else
         BrickEdge:=Brickedge - numbricks * boxsize;

      CompareMinMax(brickedge);
      Inc(result, NumBricks);

      // Process the rest of the points
      for j:= i + 1 to Count-1 do
      begin
        Reversal := (CloseValues.Value[j] > BrickEdge) <> UpTrend;
        UpTrend := CloseValues.Value[j] > BrickEdge;

        NoBrick := CloseValues.Value[j] = brickedge;

        if Reversal then
           if UpTrend then
              BrickEdge:=BrickEdge + FBoxSize
           else
              BrickEdge:=BrickEdge - FBoxSize;

        if (NoBrick) then
          Numbricks := 0
        else
        NumBricks := CalcNumBricks(BrickEdge, CloseValues[j]);

        if NumBricks > 0 then
        begin
          if Draw then
             DrawBricks(brickedge, result, NumBricks, UpTrend);

          if UpTrend then
             BrickEdge := BrickEdge + NumBricks * BoxSize
          else
             BrickEdge := BrickEdge - NumBricks * BoxSize;

          CompareMinMax(BrickEdge);
          Inc(result, NumBricks);
        end;
      end;
    end;
  end;
end;

procedure TRenkoSeries.DrawAllValues;
begin
  inherited;

 if Transparency>0 then
    IBlend:=ParentChart.Canvas.BeginBlending(TeeZeroRect,Transparency)
 else
    IBlend:=nil;

  CalcBricks(True);

  if Assigned(IBlend) then
     IBlend.Free;
end;

/// <summary>
/// Seems to work fine ... but better check it so it doesn't break something else.
/// </summary>
procedure TRenkoSeries.CalcFirstLastVisibleIndex;
begin
  inherited;
  FFirstVisibleIndex:=0;
  FLastVisibleIndex:=CalcBricks(False)-1;
end;

function TRenkoSeries.CountLegendItems:Integer;
begin
  result:=2;
end;

function TRenkoSeries.LegendItemColor(LegendIndex:Integer):TColor;
begin
  if LegendIndex=0 then result:=UpSwingColor
                   else result:=DownSwingcolor;
end;

function TRenkoSeries.NumSampleValues:Integer;
begin
  result:=8;
end;

procedure TRenkoSeries.PrepareForGallery;
begin
  inherited;
  FillSampleValues(5);
end;

procedure TRenkoSeries.AddSampleValues(NumValues:Integer; OnlyMandatory:Boolean=False);
var r : TSeriesRandomBounds;
    t : Integer;
begin
  r:=RandomBounds(numValues);

  // some sample values to see something in design mode...
  for t:=1 to NumValues do
  begin
    AddXY(r.tmpX, (Random + 1) * (NumValues + 2));

    r.tmpX:=r.tmpX + r.StepX;
  end;
end;

function TRenkoSeries.MaxXValue:TChartValue;
begin
  result:=CalcBricks(False) -0.5;
end;

function TRenkoSeries.MinXValue:TChartValue;
begin
  result:= -0.5;
end;

function TRenkoSeries.MaxYValue:TChartValue;
begin
  CalcBricks(False);
  result:=IMax;
end;

function TRenkoSeries.MinYValue:TChartValue;
begin
  CalcBricks(False);
  result:=IMin;
end;

function TRenkoSeries.LegendString( LegendIndex:Integer;
                                    LegendTextStyle:TLegendTextStyle ):String;
begin
  if LegendIndex=0 then result:=TeeMsg_Up
                   else result:=TeeMsg_Down;
end;

// Renko chart box size.
procedure TRenkoSeries.SetBoxSize(const Value:Double);
begin
  SetDoubleProperty(FBoxSize, Value);
end;

// Color for upward trend bricks.
// Color for upward trend (current close higher than previous close) bricks.
procedure TRenkoSeries.SetUpSwing(const Value:TColor);
begin
  SetColorProperty(FUpSwing, Value);
end;

// Color for downward trend bricks.
// Color for downward trend (current close lower than previous close) bricks.
procedure TRenkoSeries.SetDownSwing(const Value:TColor);
begin
  SetColorProperty(FDownSwing, Value);
end;

// Trading close values.
// Gets and sets all Stock market closing values.
function TRenkoSeries.GetCloseValues:TChartValueList;
begin
  result:=YValues;
end;

class function TRenkoSeries.GetEditorClass: String;
begin
  result:='TRenkoSeriesEditor';
end;

initialization
  RegisterTeeSeries( TRenkoSeries, @TeeMsg_GalleryRenko,
                                   @TeeMsg_GalleryFinancial, 1 );
finalization
  UnRegisterTeeSeries([TRenkoSeries]);
end.

