Skip to content

Custom time frames

The custom time frame API provides types that enable you to display custom OHLC data from any source, including third-party APIs, synthetic calculations or local files, directly in cTrader charts. API entities, such as CustomTimeFrame and TimeFrameManager, allow you to stream bars from a specific data source and display them in cTrader as a fully fledged period.

Important types include:

  • TimeFrameManager.Custom.Add(name) to register a new custom period with a user-defined name.
  • CustomTimeFrame.BarsNeeded to define how and when the data is loaded for the chart.
  • CustomBars.AppendBars(), CustomBars.UpdateLastBar(), CustomBars.PrependBars() to dynamically manage the series, where each CustomBar holds these six fields: time, open, high, low, close and volume.

Once added, custom periods integrate seamlessly into cTrader Period UI and appear in the dropdown alongside built-in periods. Users may then easily switch between standard and custom periods.

Tip

Use custom periods to visualise external markets, simulate experimental bar types, create advanced correlation/arbitrage charts or backtest strategies on bespoke data.

Custom time frame API objects can be used to do the following:

Feature or operation Examples
Displaying alternative market data Load bars from REST APIs (e.g. AlphaVantage)
Visualise off-platform prices
Simulating synthetic bar types Create Heikin-Ashi, Renko or volume bars
Generate bars from sentiment or economic indices
Offline data testing Load custom bars from a CSV or JSON file
Backtest on proprietary datasets
Cross-asset strategy overlays Plot GBP/USD on EUR/USD charts
Develop arbitrage models
Enhanced UI control Toggle, favourite or modify custom periods

Create custom period based on existing data

The plugin here illustrates how to create and display a synthetic intraday period in cTrader.

  • The plugin builds a 25-minute timeframe by fetching native one-minute bars for the active symbol, aggregating them into 25-minute OHLC candles and then streaming the result into a CustomBar series.
  • The new timeframe appears in the drop-down menu for selecting timeframes as 25Min.
  • When users scroll back on the chart, the plugin automatically loads more minute data, prepends older 25-minute bars and keeps the view up to date.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
using System;
using System.Linq;                 
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Plugins
{
    [Plugin(AccessRights = AccessRights.None)]
    public class TF25 : Plugin
    {
        private CustomTimeFrame _tf25Min;
        private bool _isLoadMoreEnabled = true;

        protected override void OnStart()
        {
            // Create the 25-minute TF
            _tf25Min = TimeFrameManager.Custom.Add("25Min");
            _tf25Min.Description = "25‑minute aggregated from 1‑minute bars";

            // Hook initial load and on‑scroll‑back
            _tf25Min.NewChart += OnNewChart;
            _tf25Min.BarsNeeded = OnBarsNeeded;
        }

        private void OnNewChart(CustomTimeFrameNewChartEventArgs args)
        {
            var cb    = args.CustomBars;
            var chart = args.Chart;

            // Show a loading indicator
            var loading = new TextBlock { Text = "Loading 25‑min bars…", FontSize = 12 };
            chart.AddControl(loading);

            // Fetch the 1‑min bars and aggregate
            MarketData.GetBarsAsync(TimeFrame.Minute, cb.Symbol.Name, minuteBars =>
            {
                var aggregated = minuteBars
                    .Select((bar, idx) => new { bar, idx })
                    .GroupBy(x => x.idx / 25)
                    .Select(g =>
                    {
                        var first = g.First().bar;
                        var last  = g.Last().bar;
                        return new CustomBar(
                            time:        first.OpenTime,
                            open:        first.Open,
                            high:        g.Max(x => x.bar.High),
                            low:         g.Min(x => x.bar.Low),
                            close:       last.Close,
                            volume:  g.Sum(x => (int)x.bar.TickVolume)
                        );
                    });

                chart.RemoveControl(loading);


            });
        }

        private void OnBarsNeeded(CustomTimeFrameBarsNeededArgs args)
        {
            var cb = args.CustomBars;

            // Same logic for explicit BarsNeeded triggers
            MarketData.GetBarsAsync(TimeFrame.Minute, cb.Symbol.Name, minuteBars =>
            {
                var aggregated = minuteBars
                    .Select((bar, idx) => new { bar, idx })
                    .GroupBy(x => x.idx / 25)
                    .Select(g => new CustomBar(
                        time:        g.First().bar.OpenTime,
                        open:        g.First().bar.Open,
                        high:        g.Max(x => x.bar.High),
                        low:         g.Min(x => x.bar.Low),
                        close:       g.Last().bar.Close,
                        volume:  g.Sum(x => (int)x.bar.TickVolume)
                    ));
                cb.AppendBars(aggregated);
                 args.CustomBars.NeedsMore += _ =>
                {
                    if (!_isLoadMoreEnabled)
                    {
                        Print("Load‑more disabled; skipping additional bars.");
                        return;
                    }

                    Print("NeedsMore fired; loading more history…");
                    minuteBars.LoadMoreHistoryAsync(loadArgs =>
                    {
                        Print($"Loaded {loadArgs.Count} more 1‑min bars");
                        if (loadArgs.Count == 0)
                            return;

                        Sleep(TimeSpan.FromSeconds(1));
                        args.CustomBars.PrependBars(minuteBars.Take(loadArgs.Count).Select(bar => new CustomBar(bar.OpenTime, bar.Open, bar.High, bar.Low, bar.Close, (int)bar.TickVolume)));
                    });
                };
            });
        }

        protected override void OnStop()
        {
        }
    }
}

Create custom period using external data

The plugin here demonstrates how to fetch and display external financial data in cTrader charts using custom time frame API objects.

  • The plugin creates a custom period in cTrader by retrieving historical daily OHLC data for IBM from the AlphaVantage API.
  • It parses the CSV response and loads the data into a CustomBar series, which is then displayed as a selectable custom period called AlphaVantage in the chart UI.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using System;
using System.Collections.Generic;
using cAlgo.API;
using cAlgo.API.Collections;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo.Plugins
{
    [Plugin(AccessRights = AccessRights.None)]
    public class CustomTimeFrameAlphaVantage : Plugin
    {
        private const string ApiKey = "YOUR_ALPHA_VANTAGE_KEY";
        private const string Symbol = "IBM";
        private const string Endpoint = $"https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol={Symbol}&apikey={ApiKey}&datatype=csv&outputsize=full";

        private CustomTimeFrame _customTimeFrame;

        protected override void OnStart()
        {
            _customTimeFrame = TimeFrameManager.Custom.Add("AlphaVantage");

            _customTimeFrame.Description = "Loads data from AlphaVantage";

            _customTimeFrame.BarsNeeded = args => 
            {
                var response = Http.Get(new Uri(Endpoint));

                if (!response.IsSuccessful)
                {
                    Print($"Error loading data from AlphaVantage, status code: {response.StatusCode}, body: {response.Body}");
                    return;
                }

                Print($"Response Received: {response.StatusCode}");

                var ohlcData = response.Body.Split(Environment.NewLine)[1..];

                Array.Reverse(ohlcData);

                Print($"ohlcData Count: {ohlcData.Length}");

                var bars = new List<CustomBar>();

                foreach (var ohlc in ohlcData)
                {
                    var ohlcSplit = ohlc.Split(',');

                    if (ohlcSplit.Length < 6) continue;

                    var time = DateTime.Parse(ohlcSplit[0]);
                    var open = double.Parse(ohlcSplit[1]);
                    var high = double.Parse(ohlcSplit[2]);
                    var low = double.Parse(ohlcSplit[3]);
                    var close = double.Parse(ohlcSplit[4]);
                    var volume = long.Parse(ohlcSplit[5]);

                    bars.Add(new CustomBar(time, open, high, low, close, volume));
                }

                Print($"AppendBars Count: {bars.Count}");

                args.CustomBars.AppendBars(bars);
            };
        }

        protected override void OnStop()
        {
            // Handle Plugin stop here
        }
    }
}