YOUSEFUL / DELPHI / SAMPLE PROJECTS
Candle chart export
The Atlas Fin quick start, and the project the tutorial builds step by step. A console program that:
- fills a
TAtlasBarSerieswith 120 weekday bars (a fixed pseudo-random walk, so every run gives the same prices); - 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; - checks that the SMA(20) study line equals the mean of the first twenty closes, that the Bollinger bands from
TAtlasIndicatorsenclose the same average, and that RSI(14) starts at bar 15 and stays within 0..100; - writes the bars to CSV and reads them back;
- saves
candles.png, zooms to the last 40 bars, savescandles-zoom.pngandcandles.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
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:\...\CandleChartExportcandles-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
- Load your own prices with
Chart.Data.LoadCSV(TFile.ReadAllText('prices.csv'))in place ofFillBars. The header must be exactlytime,open,high,low,close,volume, even if the rows leave the volume out. - Change the layout:
Chart.Panes[0].Studies.Add(skEMA, 50)adds a line to the price pane, andChart.Panes.Add('RSI', 80).Studies.Add(skRSI)adds a pane of its own. - Pick another format by extension:
.bmpand.emfwork as well as.pngand.svg. A width or height of 0 uses the control's own size, 640 × 420 by default.
Project files
The ZIP contains every sample project with its README. See getting started for the search path to add.
Source
CandleChartExport.dpr
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.