YOUSEFUL / DELPHI / SAMPLE PROJECTS

Candle chart export

Console program Self-checking Product: Atlas Fin · Components: TAtlasFinChart, TAtlasBarSeries, TAtlasIndicators

The Atlas Fin quick start, and the project the tutorial builds step by step. A console program that:

  1. fills a TAtlasBarSeries with 120 weekday bars (a fixed pseudo-random walk, so every run gives the same prices);
  2. lays out a chart with SetupDefault (price with candles and SMA(20), volume, RSI(14)), then adds Bollinger bands to the price pane and a MACD pane;
  3. checks that the SMA(20) study line equals the mean of the first twenty closes, that the Bollinger bands from TAtlasIndicators enclose the same average, and that RSI(14) starts at bar 15 and stays within 0..100;
  4. writes the bars to CSV and reads them back;
  5. saves candles.png, zooms to the last 40 bars, saves candles-zoom.png and candles.svg, and checks that the files exist and that the SVG text names the studies.

The chart is never shown and has no parent: SaveToFile and ToSVG draw into a bitmap or an SVG document, so a console program or a service can produce chart images. Every step checks its own result. The program exits with code 0 when all checks pass, or prints the failed check and exits with code 1. It needs no input or network.

Build and run

Open CandleChartExport.dpr in Delphi 13; the IDE creates the project file. Add the Atlas Fin src folder and the Atlas Plot src folder to the project's search path, then build and run for Win32 or Win64. No packages need to be installed. The program needs no input. It checks its own results and stops with an error if any of them differs from the output below.

Expected output

Example
120 bars from 2026-01-05 to 2026-06-19
  ok: four panes: Price, Volume, Momentum, MACD
  ok: SMA(20) starts at bar 20 with the mean of the closes
  ok: Bollinger bands enclose the SMA
  ok: RSI(14) starts at bar 15 and stays within 0..100
  last close 119.44, SMA(20) 117.61, RSI(14) 61.3
  ok: CSV round trip
  ok: candles.png written
  ok: the view shows the last 40 bars
  ok: the SVG names the studies in the pane headers
  ok: candles.svg written
Files written to C:\...\CandleChartExport

candles-zoom.png shows four panes. At the top is the price pane with 40 candles, the SMA(20) line and the three Bollinger lines; its header reads Price O 118.06 H 119.86 L 117.77 C 119.44 SMA(20) 117.61 BB(20, 2) 117.61 120.21 115.01. Below it are the volume bars, the RSI line on a fixed 0..100 scale, and the MACD line, signal line and histogram. Month names label the shared time axis. With no crosshair, the header values are those of the newest visible bar.

The CSV check compares prices to within 1E-9, not bit for bit. ToCSV writes numbers with FloatToStr (15 significant digits), so a computed price such as 119.4412... can come back differing in its last bits.

Adapting it

Project files

Download all 34 projects

The ZIP contains every sample project with its README. See getting started for the search path to add.

Source

CandleChartExport.dpr

pascal
program CandleChartExport;

{$APPTYPE CONSOLE}

// Atlas Fin quick start: build OHLCV bars in code, add studies, verify the
// indicator values and export the chart to PNG and SVG without a window.

uses
  System.SysUtils, System.Classes, System.Math, System.DateUtils, System.IOUtils,
  Atlas.FinData, Atlas.FinIndicators, Atlas.FinChart;

procedure Check(Condition: Boolean; const Message: string);
begin
  if not Condition then raise Exception.Create('Check failed: ' + Message);
  Writeln('  ok: ', Message);
end;

// Deterministic weekday bars: the same prices on every run and platform.
procedure FillBars(Data: TAtlasBarSeries; Count: Integer);
var
  Seed: Cardinal;
  Day: TDateTime;
  Price, Open, Close, High, Low: Double;
  I: Integer;
  function Next: Double;
  begin
    Seed := Cardinal((UInt64(Seed) * 1664525 + 1013904223) and $FFFFFFFF);
    Result := (Seed shr 8) / 16777216;
  end;
begin
  Seed := 2026;
  Day := EncodeDate(2026, 1, 5); // a Monday
  Price := 100;
  Data.BeginUpdate;
  try
    Data.Clear;
    I := 0;
    while I < Count do
    begin
      if DayOfTheWeek(Day) <= 5 then
      begin
        Open := Price * (1 + (Next - 0.5) * 0.01);
        Close := Open * (1 + (Next - 0.48) * 0.03);
        High := Max(Open, Close) * (1 + Next * 0.01);
        Low := Min(Open, Close) * (1 - Next * 0.01);
        Data.Add(Day, Open, High, Low, Close, Round(500000 + Next * 500000));
        Price := Close;
        Inc(I);
      end;
      Day := Day + 1;
    end;
  finally
    Data.EndUpdate;
  end;
end;

var
  Chart: TAtlasFinChart;
  Loaded: TAtlasBarSeries;
  Sma, Rsi: TArray<Double>;
  Bands: TAtlasBands;
  Expected: Double;
  I, Last: Integer;
  Svg: string;
begin
  try
    Chart := TAtlasFinChart.Create(nil);
    try
      FillBars(Chart.Data, 120);
      Last := Chart.Data.Count - 1;
      Writeln(Format('%d bars from %s to %s', [Chart.Data.Count,
        FormatDateTime('yyyy-mm-dd', Chart.Data[0].Time), FormatDateTime('yyyy-mm-dd', Chart.Data[Last].Time)]));

      // Price pane with candles and SMA(20), a volume pane and an RSI(14) pane.
      Chart.SetupDefault;
      Chart.Panes[0].Studies.Add(skBollinger, 20);
      Chart.Panes.Add('MACD', 100).Studies.Add(skMACD);
      Check(Chart.Panes.Count = 4, 'four panes: Price, Volume, Momentum, MACD');

      // The SMA(20) study line equals the mean of the closes it covers.
      Sma := Chart.Panes[0].Studies[1].Lines(Chart.Data)[0];
      Expected := 0;
      for I := 0 to 19 do Expected := Expected + Chart.Data[I].Close / 20;
      Check(IsNan(Sma[18]) and SameValue(Sma[19], Expected, 1E-9), 'SMA(20) starts at bar 20 with the mean of the closes');

      // The same indicators are available without a chart.
      Bands := TAtlasIndicators.Bollinger(Chart.Data.Closes, 20, 2);
      Check((Bands.Upper[Last] > Bands.Middle[Last]) and (Bands.Lower[Last] < Bands.Middle[Last]) and
        SameValue(Bands.Middle[Last], Sma[Last], 1E-9), 'Bollinger bands enclose the SMA');
      Rsi := TAtlasIndicators.RSI(Chart.Data.Closes, 14);
      Check(IsNan(Rsi[13]) and (Rsi[14] >= 0) and (Rsi[14] <= 100), 'RSI(14) starts at bar 15 and stays within 0..100');
      Writeln(Format('  last close %.2f, SMA(20) %.2f, RSI(14) %.1f', [Chart.Data[Last].Close, Sma[Last], Rsi[Last]]));

      // CSV out and back in. ToCSV writes 15 significant digits, so computed
      // prices come back equal to within rounding, not bit for bit.
      Loaded := TAtlasBarSeries.Create;
      try
        Loaded.LoadCSV(Chart.Data.ToCSV);
        Check((Loaded.Count = Chart.Data.Count) and (Loaded[Last].Time = Chart.Data[Last].Time) and
          SameValue(Loaded[Last].Close, Chart.Data[Last].Close, 1E-9), 'CSV round trip');
      finally
        Loaded.Free;
      end;

      // Export: the chart needs no parent or window for this.
      Chart.SaveToFile('candles.png', 900, 600);
      Check(TFile.Exists('candles.png') and (TFile.GetSize('candles.png') > 5000), 'candles.png written');
      Chart.ZoomTo(Chart.Data.Count - 40, 40);
      Check((Chart.FirstBar = 80) and (Chart.VisibleBars = 40), 'the view shows the last 40 bars');
      Chart.SaveToFile('candles-zoom.png', 900, 600);
      Svg := Chart.ToSVG(900, 600);
      Check((Pos('<svg', Svg) > 0) and (Pos('SMA(20)', Svg) > 0) and (Pos('MACD(12, 26, 9)', Svg) > 0),
        'the SVG names the studies in the pane headers');
      Chart.SaveToFile('candles.svg', 900, 600);
      Check(TFile.Exists('candles.svg') and (TFile.GetSize('candles.svg') > 2000), 'candles.svg written');
      Writeln('Files written to ', GetCurrentDir);
    finally
      Chart.Free;
    end;
  except
    on E: Exception do
    begin
      Writeln(E.ClassName, ': ', E.Message);
      ExitCode := 1;
    end;
  end;
end.