YOUSEFUL / DELPHI

Atlas Fin: FAQ, examples and tutorial

Learning center · Component reference

Atlas Fin draws financial charts in VCL applications. TAtlasFinChart stacks price, volume and indicator panes on one shared time axis without gaps for weekends or holidays. Five indicators are built in: SMA, EMA, Bollinger bands, RSI and MACD. Users scroll, zoom and read values with a crosshair, and code exports the chart to PNG, BMP, EMF or SVG without a window. Add Atlas.FinData (the validated OHLCV series TAtlasBarSeries and EAtlasFinError) and Atlas.FinChart (the chart, its panes and studies) to uses. Add Atlas.FinIndicators only if you compute indicators yourself. The component sits on the Atlas Plot palette page. Atlas Fin requires Atlas Plot, whose scale and drawing code it reuses. Atlas Plot also supplies the general line, bar, area, pie and donut charts in TAtlasPlot; Atlas Fin does not duplicate them.

FAQ

What can a chart show? Four price styles (candles, OHLC bars, a close line, a close area), volume bars, and the SMA, EMA, Bollinger, RSI and MACD indicators, all computed from the closing prices. Each pane holds any mix of these as studies, listed in TAtlasStudyKind. That is the whole list. Drawing tools (trend lines, Fibonacci, annotations), other indicators (stochastics, ATR, ADX, Ichimoku, VWAP), custom study callbacks, Heikin-Ashi, Renko and point-and-figure charts, overlays of several symbols, TDataSet binding, printing, VCL styles and accessibility support are not implemented.

Why are there no gaps for weekends? The horizontal axis is the bar index, not time: every bar gets the same width, however much time lies between two bars. The labels come from calendar boundaries among the visible bars: years for views longer than three years, months above 45 days, days above two days, and clock times below that. The month label that starts a new year carries the year (Jan 2026) and wins when labels collide. Intraday session breaks and time zones are not marked.

Which bars does TAtlasBarSeries accept? Only consistent ones. Every value must be finite and within ±1E100, High must be at least Open, Close and Low, Low at most Open and Close, and Volume zero or more. Times must be strictly increasing within the years 1 to 9999. Anything else raises EAtlasFinError and changes nothing; for LoadCSV that holds for the whole file. Bars can be appended, the newest replaced and all cleared, but a single older bar cannot be inserted, deleted or edited.

How do I show live prices? Call Data.UpdateLast while a bar is forming and Data.Add when the next one opens, on the main thread (the series is not thread-safe). A view whose newest bar is the last bar moves along with new bars; any other view stays where the user put it, and a view that shows everything keeps showing everything. Moving along does not raise OnViewChange. Never assign Chart.Data.OnChange: the chart uses that event to repaint and follow.

What CSV does LoadCSV read? A header line time,open,high,low,close,volume (case and spaces do not matter, a byte order mark is removed), then rows of five or six comma-separated fields. The volume may be left out of a row, but not out of the header. Times are ISO 8601: 2026-09-21, 2026-09-21T09:30:00, or with Z or an offset, which is converted to UTC; text without a zone is kept as written. Numbers always use .. Quoted fields are not supported. ToCSV writes the same format, to the second, with up to 15 significant digits.

How are the indicators defined? TAtlasIndicators in Atlas.FinIndicators implements them over any array of values. SMA is the plain mean of the last Period values. EMA uses the factor 2/(Period+1) and is seeded with the first SMA. Bollinger bands are the SMA plus and minus Deviations population standard deviations. RSI uses Wilder's smoothing. MACD is EMA(fast) minus EMA(slow), with an EMA signal line and their difference as a histogram. Results are as long as the input, with NaN wherever the indicator is not yet defined: SMA(20) starts at index 19, RSI(14) at index 14, the MACD(12, 26, 9) line at 25 and its signal at 33.

How does each pane choose its scale? From the bars in view, recomputed at every render: the highs and lows for candles and OHLC bars, the closes for the close line and area, the volumes, and the values of the visible indicator lines, with 5 % padding, rounded out to tidy steps. Hidden studies do not count. Volume and MACD panes always include zero, and a pane that holds only RSI studies is fixed at 0..100. Logarithmic rounds the range out to whole powers of ten, so prices that stay within one decade get only a thin band of the pane. It helps only when prices vary tenfold or more.

What does the form file keep? The panes and their studies with all their settings, so a layout built in the Object Inspector comes back at run time. Bars are never stored; load them in code. A MACD study whose fast period is 26 or more cannot be read back: Period is read before SlowPeriod and checked against the default slow period, and loading raises EReadError. Set such periods in code. The design package compiles, but installing it and editing in the IDE designer have not been tested.

Can I create images without showing the chart? Yes. A chart created with TAtlasFinChart.Create(nil) and no parent draws with SaveToFile (.png, .bmp, .emf, .svg), ToSVG, CreateBitmap and RenderTo, in a console program or a service. Exports never include the crosshair. Sizes run from 1 to 16384 pixels; a size of 0 in SaveToFile uses the control's Width and Height, 640 × 420 by default. A failed export leaves an existing file in place.

What can the user do with the mouse and keyboard? Drag to scroll, turn the wheel to zoom about the cursor, double-click to show all bars, and drag the border between two panes to change their heights. With the focus, which the chart takes when clicked: Left and Right scroll one bar, Ctrl with an arrow scrolls a page, Home and End jump to either end, Up or keypad + zooms in, and Down or keypad - zooms out. The main keyboard's + and - keys do nothing. Moving over a pane shows the crosshair and puts that bar's values in every pane header.

Where do the colours come from? Rising bars (close at or above open) use UpColor, falling bars DownColor; volume bars use a lighter shade of the same two colours. Indicator lines use the study's Color or, left at clDefault, the next of six palette colours in the order of the studies across the chart. The MACD signal line always takes a palette colour.

Examples

Build a chart in code on any form. SetupDefault gives a price pane with candles and SMA(20), a volume pane and an RSI(14) pane. Assigning Data copies the bars.

pascal
uses Vcl.Forms, Vcl.Controls, Atlas.FinData, Atlas.FinChart;

function AddPriceChart(Form: TForm; Bars: TAtlasBarSeries): TAtlasFinChart;
begin
  Result := TAtlasFinChart.Create(Form);
  Result.Parent := Form;
  Result.Align := alClient;
  Result.SetupDefault;
  Result.Data := Bars;
  Result.ZoomTo(Result.Data.Count - 90, 90);
end;

The form shows three panes titled Price, Volume and Momentum over the newest 90 bars. Bars stays yours to free.

Build your own layout. Changes between BeginUpdate and EndUpdate repaint once. The MACD fast period is passed to Add and must stay below the slow period, which is 26 until you change it.

pascal
uses Vcl.Graphics, Atlas.FinChart;

procedure BuildLayout(Chart: TAtlasFinChart);
var
  Price: TAtlasFinPane;
  Macd: TAtlasFinStudy;
begin
  Chart.Panes.BeginUpdate;
  try
    Chart.Panes.Clear;
    Price := Chart.Panes.Add('Price', 300);
    Price.Studies.Add(skOhlc);
    Price.Studies.Add(skEMA, 21).Color := clNavy;
    Price.Studies.Add(skBollinger, 20).Deviations := 2.5;
    Chart.Panes.Add('Volume', 80).Studies.Add(skVolume);
    Macd := Chart.Panes.Add('MACD', 120).Studies.Add(skMACD, 8);
    Macd.SlowPeriod := 21;
    Macd.SignalPeriod := 5;
  finally
    Chart.Panes.EndUpdate;
  end;
end;

The headers read Price ... EMA(21) ... BB(20, 2.5) ..., Volume Vol ... and MACD MACD(8, 21, 5) .... For a fast period of 26 or more, set SlowPeriod above it first.

Load a CSV file and show weekly bars. A bad row raises EAtlasFinError with its line number and leaves Daily empty.

pascal
uses System.SysUtils, System.IOUtils, Atlas.FinData, Atlas.FinChart;

procedure ShowWeekly(Chart: TAtlasFinChart; const FileName: string);
var
  Daily, Weekly: TAtlasBarSeries;
begin
  Daily := TAtlasBarSeries.Create;
  try
    Daily.LoadCSV(TFile.ReadAllText(FileName));
    Weekly := Daily.Aggregate(bpWeek);
    try
      Chart.Data := Weekly;
      Chart.ShowAll;
    finally
      Weekly.Free;
    end;
  finally
    Daily.Free;
  end;
end;

Each weekly bar is stamped with the Monday of its week and combines the first open, highest high, lowest low, last close and total volume.

Turn trades into minute bars. Call this on the main thread; UpdateLast changes the forming bar, and the chart follows new bars if its view shows the newest one.

pascal
uses System.Math, System.DateUtils, Atlas.FinData;

procedure AddTrade(Data: TAtlasBarSeries; const Time: TDateTime; const Price, Size: Double);
var
  Minute: TDateTime;
  Bar: TAtlasBar;
begin
  Minute := RecodeSecond(RecodeMilliSecond(Time, 0), 0);
  if (Data.Count > 0) and (Data[Data.Count - 1].Time = Minute) then
  begin
    Bar := Data[Data.Count - 1];
    Data.UpdateLast(Bar.Open, Max(Bar.High, Price), Min(Bar.Low, Price), Price, Bar.Volume + Size);
  end
  else
    Data.Add(Minute, Price, Price, Price, Price, Size);
end;

Pass Chart.Data as Data. A trade older than the newest bar raises EAtlasFinError ("Bars must be added in strictly increasing time order").

Show the bar under the crosshair in a label. The handler belongs to a form with Chart: TAtlasFinChart and Status: TLabel, assigned with Chart.OnCrosshair := ChartCrosshair.

pascal
uses System.SysUtils, Atlas.FinData, Atlas.FinChart;

procedure TMainForm.ChartCrosshair(Sender: TObject; BarIndex: Integer);
var
  Bar: TAtlasBar;
begin
  if BarIndex < 0 then
    Status.Caption := ''
  else
  begin
    Bar := Chart.Data[BarIndex];
    Status.Caption := Format('%s  O %.2f  H %.2f  L %.2f  C %.2f',
      [DateToStr(Bar.Time), Bar.Open, Bar.High, Bar.Low, Bar.Close]);
  end;
end;

The label changes each time the mouse moves onto another bar and clears when the mouse leaves the panes.

Read the value under the mouse. OnMouseMove fires before the chart updates its crosshair, so ask the pixel queries rather than CrosshairBar.

pascal
uses System.SysUtils, System.Classes, Atlas.FinChart;

procedure TMainForm.ChartMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
var
  Pane, Bar: Integer;
begin
  Pane := Chart.PaneAtPixel(Y);
  Bar := Chart.BarAtPixel(X);
  if (Pane >= 0) and (Bar >= 0) then
    Status.Caption := Format('%s %.2f at bar %d', [Chart.Panes[Pane].Title, Chart.PixelToValue(Pane, Y), Bar]);
end;

The label shows the pane title, the value at the mouse in that pane's scale and the bar index.

Export images and copy one to the clipboard.

pascal
uses Vcl.Graphics, Vcl.Clipbrd, Atlas.FinChart;

procedure ExportChart(Chart: TAtlasFinChart);
var
  Bitmap: Vcl.Graphics.TBitmap;
begin
  Chart.SaveToFile('chart.png', 1200, 800);
  Chart.SaveToFile('chart.svg', 1200, 800);
  Chart.SaveToFile('chart.emf');
  Bitmap := Chart.CreateBitmap(600, 400);
  try
    Clipboard.Assign(Bitmap);
  finally
    Bitmap.Free;
  end;
end;

Three files appear in the current folder; the EMF has the control's own size. The on-screen chart is not disturbed.

Compute indicators without a chart, for example to report signals.

pascal
uses System.SysUtils, System.Math, Atlas.FinData, Atlas.FinIndicators;

procedure PrintMacdCrossings(Bars: TAtlasBarSeries);
var
  Closes, Rsi: TArray<Double>;
  Macd: TAtlasMacd;
  I: Integer;
begin
  Closes := Bars.Closes;
  Rsi := TAtlasIndicators.RSI(Closes, 14);
  Macd := TAtlasIndicators.MACD(Closes, 12, 26, 9);
  for I := 1 to High(Closes) do
    if not IsNan(Macd.Histogram[I - 1]) and (Macd.Histogram[I - 1] < 0) and (Macd.Histogram[I] >= 0) then
      Writeln(DateToStr(Bars[I].Time), ': MACD above its signal, RSI ', FormatFloat('0.0', Rsi[I]));
end;

One line appears for each bar where the MACD histogram turns from negative to zero or above. The first bar that can qualify has index 34, because the histogram starts at index 33.

Tutorial

Sample project: Candle chart export.

Goal: build a price series in code, lay out a four-pane chart with indicators, check the indicator values and export the chart to PNG and SVG from a console program.

  1. Create a console application and save it as CandleChartExport.dpr. Add the Atlas Fin src folder and the Atlas Plot src folder to the project's search path, or install both runtime packages. Replace the generated code with this header and a small check routine that stops the program on the first failed expectation:

    pascal
    program CandleChartExport;
    
    {$APPTYPE CONSOLE}
    
    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;
  2. Add a routine that fills a series with weekday bars. A fixed linear congruential generator makes the prices the same on every run and on both platforms. Each bar's high and low enclose its open and close, as Add requires, and BeginUpdate / EndUpdate turn the 120 additions into one change notification:

    pascal
    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;
  3. Write the main block. The chart is created without an owner or parent: it never appears on screen. It is filled through its own Data series. The except handler turns any failure into exit code 1. Steps 4 to 7 go where the comment is:

    pascal
    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)]));
          // steps 4 to 7
        finally
          Chart.Free;
        end;
      except
        on E: Exception do
        begin
          Writeln(E.ClassName, ': ', E.Message);
          ExitCode := 1;
        end;
      end;
    end.

    Running it now prints 120 bars from 2026-01-05 to 2026-06-19: 120 weekdays, no weekend dates.

  4. Lay out the panes. SetupDefault creates Price (candles and SMA(20)), Volume and Momentum (RSI(14)). Then add Bollinger bands to the price pane and a MACD pane of its own:

    pascal
          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');
  5. Check the indicators. The SMA study's Lines holds one line as long as the data, NaN until the 20th bar. The same indicators can be computed without the chart through TAtlasIndicators:

    pascal
          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');
    
          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]]));
  6. Write the bars as CSV and read them into a second series. ToCSV writes up to 15 significant digits, so compare prices to within a small tolerance:

    pascal
          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;
  7. Export. SaveToFile picks the format from the extension. ZoomTo narrows the view to the newest 40 bars before the second picture, and the SVG text carries the study names from the pane headers:

    pascal
          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);
  8. Run the program. It prints each check and exits with code 0:

    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

    Open candles-zoom.png: 40 candles with the SMA line and three Bollinger lines, then the volume bars, the RSI line on its 0..100 scale and the MACD pane, over a time axis labelled May and Jun. The price 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. To adapt it, replace FillBars with Chart.Data.LoadCSV(TFile.ReadAllText('prices.csv')) and put your own studies in step 4. For an interactive version with crosshair, zoom, scroll and live bars, see the Interactive financial chart sample.

Troubleshooting

The chart shows only "No panes" or "No data". A new chart has no panes and no bars. Call SetupDefault or add panes with Panes.Add and studies with Studies.Add, and fill Data. Bars are never loaded from the form file, even when the layout is.

The chart, or an exported image, is blank. Each pane needs at least three text lines of height, and the plot at least six lines of width. On a smaller control or image, only the background is drawn and the pixel queries return -1. Make the target larger, use fewer panes or a smaller Font.

EAtlasFinError: "Bars must be added in strictly increasing time order". A bar's time equals or precedes the last bar's time. Sort the input, drop duplicates, and use UpdateLast, not Add, to change the newest bar. For CSV files the message is prefixed with the line number.

EAtlasFinError: "High must not be below Open, Close or Low" or "Low must not be above Open or Close". The bar is inconsistent, often because the columns are in another order. Add takes time, open, high, low, close, volume, and CSV rows use the same order.

LoadCSV rejects a file with "CSV must start with the header ...". The header must read exactly time,open,high,low,close,volume, including the volume column when the rows have none, and the separator must be a comma. Semicolons, extra columns and other column names are refused.

Times from a CSV file are shifted by some hours. The times carry Z or an offset such as +02:00, and LoadCSV converts those to UTC. Remove the zone from the text to keep local exchange times as written.

"The fast period must be shorter than the slow period" or "MACD needs Period below SlowPeriod; set the periods first". For MACD, Period (the fast period) must stay below SlowPeriod. Studies.Add(skMACD, 30) fails because the slow period is still 26. Add the study with a small period, set SlowPeriod first, then Period.

Loading a form raises EReadError on TAtlasFinStudy.Period. The form holds a MACD study with a fast period of 26 or more. Period is read before SlowPeriod and is checked against the default 26, so it cannot be loaded. Keep such MACD periods out of the form file and set them in code at run time, SlowPeriod first.

New bars arrive but the chart does not repaint or follow them. Either something assigned Chart.Data.OnChange, which replaces the chart's own handler (put your notification elsewhere), or the view is not showing the newest bar. Only a view whose last bar is the newest one follows; call ScrollBy(MaxInt) to park it there. OnViewChange is not raised when the view follows, so refresh anything you derive from FirstBar yourself.

A logarithmic price pane squeezes the bars into a thin band. The logarithmic scale is rounded out to whole powers of ten, so prices between 250 and 300 sit on a 100 to 1000 scale. Use the linear scale unless the prices in view vary tenfold or more.

The keyboard does nothing. The chart must have the focus. It takes it when clicked if TabStop is True, or you call SetFocus. Zoom with Up and Down or the numeric keypad's + and -; the main keyboard's plus and minus keys are not handled. An OnKeyDown handler that sets Key := 0 also stops the chart from acting.

Study values read through Lines belong to a different series. TAtlasFinStudy.Lines caches its result by Data.Version alone. Pass the chart's own Data; for other series call TAtlasIndicators directly.