跳转至

回测自定义数据源

回测自定义数据源 API 提供了允许开发人员将自定义价格数据输入到 cTrader 中的回测、优化和市场回放引擎的类型。 这些 API 功能支持使用离线、第三方、合成或实验数据格式的新型强大工作流程。

核心接口和类型包括:

  • BacktestingDataSources.Add(name, options) 用于注册新的自定义源。
  • BacktestingDataSourceOptions 用于配置支持的数据类型、数据时间范围和获取逻辑。
  • BacktestingDataRequest 是引擎在测试期间需要数据时发出的请求。
  • BacktestingTickDataBacktestingBarsData 封装了从插件返回的数据。

回测引擎通过这些组件与提供的数据交互:

  • 数据类型 – 在跳动点(买入/卖出)、m1 开盘价、完整 OHLCV K 线等之间选择。
  • 数据提供者 – 实现回调以按需提供数据。
  • 时间范围管理 – 定义支持哪些时间段。
  • 自定义周期 – 与 TimeFrameManager.Custom 结合使用,模拟新的 K 线类型或分辨率。
  • 自定义交易品种 – 在合成工具上运行模拟。

提示

使用 API 在第三方数据源(如经纪商跳动点数据)上回测您的策略,并在受控环境中应用离线数据集。 在真实反映实时交易条件的数据上重放、优化和压力测试策略,并在熟悉的 cTrader UI 中完成所有操作。

回测自定义数据源 API 支持以下功能:

功能或操作 示例
替代数据集成 使用机构级跳动点数据进行回测。
在经纪商特定数据上运行市场回放。
导入 AlphaVantage 或 Nasdaq Data Link (Quandl) K 线数据。
事件型回测 模拟新闻报告(如 CPI 或 NFP)的影响。
在已知时期注入波动性突发。
替代性柱状图测试 在平均 K 线图、Renko 图或自定义柱状图逻辑上进行回测。
时间周期实验 尝试在非常规时间周期(如 m25、m40、h10 等)上运行策略。
策略稳健性测试 在价格中注入随机噪声。
运行蒙特卡洛模拟。
权益曲线或风险叠加 使用历史策略余额曲线进行回测。

基本工作流程

插件示例

启用后,此插件会在活跃交易品种面板中显示一个带有按钮的组件,让您可以添加和管理自定义数据源,包括用于自定义周期的数据源。

任何添加的自定义数据都可作为市场回放回测窗口中的数据选项,而任何添加的自定义周期都会出现在常规周期用户界面中。

插件代码
  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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Reflection;
using cAlgo.API;
using cAlgo.API.Collections;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;
using System.Linq;

namespace cAlgo.Plugins
{
    [Plugin(AccessRights = AccessRights.FullAccess)]
    public class CustomDataSourceCSVTest : Plugin
    {
        private BacktestingDataSource _dataSource;
        private TextBox _symbolNameTextBox;
        private ComboBox _timeFrameCombobox;
        private string _selectedFilePath;

        private readonly Dictionary<DataId, Data> _addedData = new();
        private readonly Dictionary<DataId, Dictionary<DateOnly, List<BacktestingTick>>> _cachedTickData = new();
        private readonly Dictionary<DataId, List<BacktestingBar>> _cachedBarData = new();

        private StackPanel _panel;
        private Button _selectFileButton;

        private CustomTimeFrame _customTimeFrame;

        protected override void OnStart()
        {
            // System.Diagnostics.Debugger.Launch();

            LoadFromLocalStorage();

            var options = new BacktestingDataSourceOptions(BacktestingDataSourceDataType.Tick | BacktestingDataSourceDataType.M1OpenPrices | BacktestingDataSourceDataType.OpenPrices, OnMinMaxTimeRequested, OnDataRequested);

            _dataSource = Backtesting.DataSources.Add("CSV", options);

            var aspBlock = Asp.SymbolTab.AddBlock("Custom Data Source CSV Test");

            aspBlock.Child = GetAddDataPanel();
            aspBlock.Height = 400;
        }

        private Panel GetAddDataPanel()
        {
            _panel = new StackPanel() {Orientation = Orientation.Vertical, Margin = 3};

            _symbolNameTextBox = new TextBox() {Text = "Enter Symbol Name", Margin = 3};

            _panel.AddChild(_symbolNameTextBox);

            _timeFrameCombobox = new ComboBox() {Margin = 3, Padding = 2};

            foreach (var timeFrame in GetTimeFrames())
            {
                _timeFrameCombobox.AddItem(timeFrame.Name);
            }

            _timeFrameCombobox.SelectedItem = TimeFrame.Minute.Name;

            _panel.AddChild(_timeFrameCombobox);

            _selectFileButton = new Button() {Text = "Select File", Margin = 3};

            _selectFileButton.Click += OnSelectFileButtonClicked;

            _panel.AddChild(_selectFileButton);

            var addButton = new Button() {Text = "Add Data", Margin = 3};

            addButton.Click += OnAddClick;

            _panel.AddChild(addButton);

            var showDataButton = new Button() {Text = "Show Data", Margin = 3};

            showDataButton.Click += OnShowDataClick;

            _panel.AddChild(showDataButton);

            var removeAllDataButton = new Button() {Text = "Remove All Data", Margin = 3};

            removeAllDataButton.Click += OnRemoveAllDataClick;

            _panel.AddChild(removeAllDataButton);

            var clearCacheButton = new Button() {Text = "Clear Cache", Margin = 3};

            clearCacheButton.Click += OnClearCacheClick;

            _panel.AddChild(clearCacheButton);

            var addCustomTimeFrameButton = new Button() {Text = "Add Custom Time Frame", Margin = 3};

            addCustomTimeFrameButton.Click += OnAddCustomTimeFrameClick;

            _panel.AddChild(addCustomTimeFrameButton);

            var removeCustomTimeFrameButton = new Button() {Text = "Remove Custom Time Frame", Margin = 3};

            removeCustomTimeFrameButton.Click += OnRemoveCustomTimeFrameClick;

            _panel.AddChild(removeCustomTimeFrameButton);

            return _panel;
        }

        private void OnRemoveCustomTimeFrameClick(ButtonClickEventArgs obj)
        {
            if (TimeFrameManager.Custom.Remove(_customTimeFrame))
            {
                Print($"Custom time frame removed.");
                return;
            }

            Print($"Removing custom time frame failed.");
        }

        private void OnAddCustomTimeFrameClick(ButtonClickEventArgs obj)
        {
            _customTimeFrame = TimeFrameManager.Custom.Add("Custom Data Source Test (H1)");

            _customTimeFrame.BarsNeeded = args => 
            {
                Print($"Custom time frame bars needed for symbol '{args.CustomBars.Symbol.Name}'.");

                var bars = MarketData.GetBars(TimeFrame.Hour, args.CustomBars.Symbol.Name);

                Print($"Appending custom time frame bars '{bars.Count}' for symbol '{args.CustomBars.Symbol.Name}'.");

                args.CustomBars.AppendBars(bars.Select(b => new CustomBar(b.OpenTime, b.Open, b.High, b.Low, b.Close, b.TickVolume)));
            };

            Print("Custom time frame added.");
        }

        private void OnClearCacheClick(ButtonClickEventArgs obj)
        {
            if (_dataSource.ClearCache())
            {
                Print($"Data source '{_dataSource.Name}' cached data cleared.");
                return;
            }

            Print($"Data source '{_dataSource.Name}' cached data clear failed.");
        }

        private void OnRemoveAllDataClick(ButtonClickEventArgs obj)
        {
            foreach (var (_, data) in _addedData)
            {
                File.Delete(data.FilePath);
            }

            LocalStorage.Remove("Data", LocalStorageScope.Type);
            LocalStorage.Flush(LocalStorageScope.Type);

            _addedData.Clear();

            Print("All added data has been removed.");
        }

        private void OnAddClick(ButtonClickEventArgs obj)
        {
            if (_symbolNameTextBox.Text.Length == 0 || Symbols.GetSymbol(_symbolNameTextBox.Text) is not { } symbol)
            {
                Print($"Invalid or empty symbol name: {_symbolNameTextBox.Text}");
                return;
            }

            if (string.IsNullOrWhiteSpace(_timeFrameCombobox.SelectedItem) || !TimeFrame.TryParse(_timeFrameCombobox.SelectedItem, out var selectedTimeFrame))
            {
                Print($"Invalid time frame: {_timeFrameCombobox.SelectedItem}");
                return;
            }

            if (string.IsNullOrWhiteSpace(_selectedFilePath))
            {
                Print($"Invalid file path: {_selectedFilePath}");
                return;
            }

            var dataType = selectedTimeFrame == TimeFrame.Tick 
                ? BacktestingDataSourceDataType.Tick 
                : selectedTimeFrame == TimeFrame.Minute 
                    ? BacktestingDataSourceDataType.M1OpenPrices 
                    : BacktestingDataSourceDataType.OpenPrices;

            var dataId = new DataId(symbol.Name, selectedTimeFrame.Name, dataType);

            _panel.IsEnabled = false;

            BeginInvokeOnMainThread(() =>
            {
                try
                {
                    var minMaxTime = GetMinMaxTime(_selectedFilePath);
                    var data = new Data(_selectedFilePath, minMaxTime);

                    _addedData[dataId] = data;

                    SaveToLocalStorage(dataId, data);

                    Print($"Data for symbol {symbol} and time frame {selectedTimeFrame} is added: {_selectedFilePath}, Min/Max Time: {minMaxTime}");
                }
                finally
                {
                    _panel.IsEnabled = true;
                }
            });
        }

        private void SaveToLocalStorage(DataId dataId, Data data)
        {
            var existingData = LocalStorage.GetObject<List<(DataId DataId, Data Data)>>("Data", LocalStorageScope.Type) 
                               ?? new List<(DataId DataId, Data Data)>();

            existingData.Add((dataId, data));

            LocalStorage.SetObject("Data", existingData, LocalStorageScope.Type);
            LocalStorage.Flush(LocalStorageScope.Type);
        }


        private void OnShowDataClick(ButtonClickEventArgs obj)
        {
            var existingData =
                LocalStorage.GetObject<List<(DataId DataId, Data Data)>>("Data", LocalStorageScope.Type);

            if (existingData is null)
                return;

            Print("Showing data");

            foreach (var (dataId, data) in existingData)
            {
                Print($"Data '{dataId}' with '{data}'");
            }
        }

        private void LoadFromLocalStorage()
        {
            var existingData =
                LocalStorage.GetObject<List<(DataId DataId, Data Data)>>("Data", LocalStorageScope.Type);

            if (existingData is null)
                return;

            foreach (var (dataId, data) in existingData)
            {
                _addedData[dataId] = data with {MinMaxTime = data.MinMaxTime with
                {
                    MinTime = DateTime.SpecifyKind(data.MinMaxTime.MinTime, DateTimeKind.Utc),
                    MaxTime = DateTime.SpecifyKind(data.MinMaxTime.MaxTime, DateTimeKind.Utc)
                }};

                Print($"Added data '{dataId}' from local storage");
            }
        }

        private void OnSelectFileButtonClicked(ButtonClickEventArgs obj)
        {
            var openFileDialog = new OpenFileDialog();

            _panel.IsEnabled = false;
            _selectFileButton.Text = "Please wait...";

            BeginInvokeOnMainThread(() =>
            {
                try
                {
                    var result = openFileDialog.ShowDialog();

                    if (result != FileDialogResult.OK)
                        return;

                    _selectedFilePath = openFileDialog.FileName;
                }
                finally
                {
                    _selectFileButton.Text = "Select File";
                    _panel.IsEnabled = true;
                }
            });
        }

        private IEnumerable<TimeFrame> GetTimeFrames()
        {
            var timeFrameFields = typeof(TimeFrame).GetFields(BindingFlags.Static | BindingFlags.Public);

            foreach (var timeFrameField in timeFrameFields)
            {
                if (timeFrameField.GetValue(null) is not TimeFrame timeFrame)
                    continue;

                yield return timeFrame;
            }
        }

        private void OnDataRequested(BacktestingDataRequest request)
        {
            request.StatusChanged += request_StatusChanged;

            Print($"Data Request from {request.StartTime} to {request.EndTime}");

            var dataId = GetDataId(request.PriceDataSourceId, request.DataType);

            if (!_addedData.TryGetValue(dataId, out var data))
            {
                request.Fail($"Data not found for '{dataId}'.");

                return;
            }

            switch (request.DataType)
            {
                case BacktestingDataSourceDataType.Tick:
                    var ticks = GetRequestTicks(dataId, data, request);
                    Print($"Sending ticks.");

                    request.Complete(ticks);
                    break;
                default:
                    var bars = GetRequestBars(dataId, data, request);
                    Print($"Sending bars.");

                    request.Complete(bars);
                    break;
            }
        }

        private BacktestingData GetRequestTicks(DataId dataId, Data data, BacktestingDataRequest request)
        {
            if (!_cachedTickData.TryGetValue(dataId, out var tickData))
            {
                tickData = CacheTickData(dataId, data);

                _cachedTickData[dataId] = tickData;
            }

            var requestData = new List<BacktestingTick>();

            for (var i = DateOnly.FromDateTime(request.StartTime); i <= DateOnly.FromDateTime(request.EndTime); i = i.AddDays(1))
            {
                if (!tickData.TryGetValue(i, out var ticks))
                    continue;

                requestData.AddRange(ticks);
            }

            return new BacktestingTickData(requestData);
        }

        private BacktestingData GetRequestBars(DataId dataId, Data data, BacktestingDataRequest request)
        {
            if (!_cachedBarData.TryGetValue(dataId, out var barsData))
            {
                barsData = CacheBarData(dataId, data);

                _cachedBarData[dataId] = barsData;
            }

            return new BacktestingBarsData(barsData.Where(b => b.Time >= request.StartTime && b.Time <= request.EndTime).ToArray());
        }

        private List<BacktestingBar> CacheBarData(DataId dataId, Data data)
        {
            var result = new List<BacktestingBar>();

            using var fileStream = File.OpenRead(data.FilePath);
            using var streamReader = new StreamReader(fileStream);

            while (streamReader.ReadLine() is {} line)
            {
                var lineSplit = line.Split(',');

                if (lineSplit.Length != 6)
                {
                    throw new InvalidOperationException($"Invalid data format for '{dataId}'.");
                }

                result.Add(new BacktestingBar(DateTime.Parse(lineSplit[0]).ToUniversalTime(), double.Parse(lineSplit[1]), double.Parse(lineSplit[2]), double.Parse(lineSplit[3]), double.Parse(lineSplit[4]), long.Parse(lineSplit[5])));
            }

            return result;
        }

        private Dictionary<DateOnly, List<BacktestingTick>> CacheTickData(DataId dataId, Data data)
        {
            var result = new Dictionary<DateOnly, List<BacktestingTick>>();

            using var fileStream = File.OpenRead(data.FilePath);
            using var streamReader = new StreamReader(fileStream);

            while (streamReader.ReadLine() is {} line)
            {
                var lineSplit = line.Split(',');

                if (lineSplit.Length != 3)
                {
                    throw new InvalidOperationException($"Invalid data format for '{dataId}'.");
                }

                var time = DateTime.Parse(lineSplit[0]).ToUniversalTime();
                var date = DateOnly.FromDateTime(time);

                if (!result.TryGetValue(date, out var dateTicks))
                {
                    dateTicks = new List<BacktestingTick>();

                    result.Add(date, dateTicks);
                }

                dateTicks.Add(new BacktestingTick(time, double.Parse(lineSplit[1]), double.Parse(lineSplit[2])));
            }

            return result;
        }

        private void request_StatusChanged(BacktestingDataRequestStatusChangedEventArgs args)
        {
            Print($"Request '{args.DataRequest.PriceDataSourceId}' status changed: {args.DataRequest.Status}");
        }

        private BacktestingMinMaxTime OnMinMaxTimeRequested(BacktestingDataSourceMinMaxTimeArgs args)
        {
            Print($"Min/Max time requested, {args.PriceDataSourceId}, {args.DataType}");

            var dataId = GetDataId(args.PriceDataSourceId, args.DataType);

            Print($"Min/Max time request DataId: {dataId}");

            if (!_addedData.TryGetValue(dataId, out var data))
            {
                throw new InvalidOperationException("No data");
                //return default;    
            }

            Print($"Min/Max time sent, {data.MinMaxTime}");

            return data.MinMaxTime;
        }

        private DataId GetDataId(PriceDataSourceId priceDataSourceId, BacktestingDataSourceDataType dateType)
        {
            return new DataId(priceDataSourceId.Symbol.Name, dateType switch
            {
                BacktestingDataSourceDataType.Tick => TimeFrame.Tick.Name,
                BacktestingDataSourceDataType.M1OpenPrices => TimeFrame.Minute.Name,
                BacktestingDataSourceDataType.OpenPrices when _customTimeFrame != null && priceDataSourceId.TimeFrame == _customTimeFrame.TimeFrame => TimeFrame.Hour.Name,
                _ => priceDataSourceId.TimeFrame.Name
            }, dateType);
        }

        private BacktestingMinMaxTime GetMinMaxTime(string filePath)
        {
            using var fileStream = File.OpenRead(filePath);
            using var streamReader = new StreamReader(fileStream);

            string firstLine = null;
            string lastLine = null;

            while (streamReader.ReadLine() is {} line)
            {
                if (firstLine == null)
                    firstLine = line;

                lastLine = line;
            }

            Print($"First Line: {firstLine}");
            Print($"Last Line: {lastLine}");

            return new(
                !string.IsNullOrWhiteSpace(firstLine) ? DateTime.Parse(firstLine.Split(',')[0]).ToUniversalTime() : default,
                !string.IsNullOrWhiteSpace(lastLine) ? DateTime.Parse(lastLine.Split(',')[0]).ToUniversalTime() : default);
        }

        private readonly record struct DataId(string SymbolName, string TimeFrameName, BacktestingDataSourceDataType DataType);
        private readonly record struct Data(string FilePath, BacktestingMinMaxTime MinMaxTime);
    }
}

操作

注册数据源

基本语法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var options = new BacktestingDataSourceOptions(
    SupportedDataTypes: BacktestingDataSourceDataType.Tick | BacktestingDataSourceDataType.OpenPrices,
    MinMaxTimeHandler: args => new BacktestingMinMaxTime(DateTime.Parse("2023-01-01"), DateTime.Parse("2024-01-01")),
    DataRequestHandler: request => 
    {
        var data = LoadFromCsv(request); // your logic
        request.Complete(data);
    }
);

var dataSource = Backtesting.DataSources.Add("MyCSVData", options);

在插件内:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
protected override void OnStart()
{

    LoadFromLocalStorage();

    var options = new BacktestingDataSourceOptions(
        BacktestingDataSourceDataType.Tick 
        | BacktestingDataSourceDataType.M1OpenPrices 
        | BacktestingDataSourceDataType.OpenPrices, 
        OnMinMaxTimeRequested, 
        OnDataRequested
    );

    _dataSource = Backtesting.DataSources.Add("CSV", options);

    var aspBlock = Asp.SymbolTab.AddBlock("Custom Data Source CSV Test");

    aspBlock.Child = GetAddDataPanel();
    aspBlock.Height = 400;
}

按需提供数据

每个 BacktestingDataRequest 包含:

  • .StartTime.EndTime – 请求的时间段
  • .PriceDataSourceId – 交易品种/时间周期组合
  • .DateType – 跳动点、m1 或 OHLCV
  • .Complete(data) – 数据准备就绪时调用此方法
  • .Fail("reason") – 无法满足请求时调用此方法

处理程序示例:

1
2
3
4
5
void OnDataRequested(BacktestingDataRequest request)
{
    var bars = LoadBarsFromCsv("mydata.csv", request.StartTime, request.EndTime);
    request.Complete(new BacktestingBarsData(bars));
}

在插件内:

 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
private void OnDataRequested(BacktestingDataRequest request)
{
    request.StatusChanged += request_StatusChanged;

    Print($"Data Request from {request.StartTime} to {request.EndTime}");

    var dataId = GetDataId(request.PriceDataSourceId, request.DataType);

    if (!_addedData.TryGetValue(dataId, out var data))
    {
        request.Fail($"Data not found for '{dataId}'.");
        return;
    }

    switch (request.DataType)
    {
        case BacktestingDataSourceDataType.Tick:
            var ticks = GetRequestTicks(dataId, data, request);
            Print($"Sending ticks.");
            request.Complete(ticks);
            break;

        default:
            var bars = GetRequestBars(dataId, data, request);
            Print($"Sending bars.");
            request.Complete(bars);
            break;
    }
}

处理时间范围可用性

这告诉引擎回测可用的时间段,并防止出现超出范围的错误。

基本语法:

1
2
BacktestingMinMaxTime MinMax(DateTime min, DateTime max)
    => new BacktestingMinMaxTime(min, max);

在插件内:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private BacktestingMinMaxTime OnMinMaxTimeRequested(BacktestingDataSourceMinMaxTimeArgs args)
{
    Print($"Min/Max time requested, {args.PriceDataSourceId}, {args.DataType}");

    var dataId = GetDataId(args.PriceDataSourceId, args.DataType);

    Print($"Min/Max time request DataId: {dataId}");

    if (!_addedData.TryGetValue(dataId, out var data))
    {
        throw new InvalidOperationException("No data");
    }

    Print($"Min/Max time sent, {data.MinMaxTime}");

    return data.MinMaxTime;
}

与自定义周期结合(可选)

您可以将数据源绑定到自定义周期

基本语法:

1
2
3
4
5
6
7
8
var customTf = TimeFrameManager.Custom.Add("My 45min");

customTf.BarsNeeded = args =>
{
    var baseBars = LoadBarsFromCsv("source.csv");
    var grouped = AggregateBarsTo45min(baseBars);
    args.CustomBars.AppendBars(grouped);
};

在插件内:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void OnAddCustomTimeFrameClick(ButtonClickEventArgs obj)
{
    _customTimeFrame = TimeFrameManager.Custom.Add("Custom Data Source Test (h1)");

    _customTimeFrame.BarsNeeded = args => 
    {
        Print($"Custom time frame bars needed for symbol '{args.CustomBars.Symbol.Name}'.");

        var bars = MarketData.GetBars(TimeFrame.Hour, args.CustomBars.Symbol.Name);

        Print($"Appending custom time frame bars '{bars.Count}' for symbol '{args.CustomBars.Symbol.Name}'.");

        args.CustomBars.AppendBars(bars.Select(b => new CustomBar(
            b.OpenTime,
            b.Open,
            b.High,
            b.Low,
            b.Close,
            b.TickVolume
        )));
    };

    Print("Custom time frame added.");
}

提示

  • 更新数据时使用 BacktestingDataSource.ClearCache() 清除旧结果。

  • 使用 LocalStorage 持久化源引用,以便重启时重新加载。

  • 使用 PluginPanelTextBoxFileDialog 构建可视化数据选择器,以提高便利性。

实用想法

将柱状图聚合为自定义周期

您可以通过聚合现有的 m1 数据来创建定制的图表分辨率,例如 45 分钟柱状图。 这种方法让您可以在非标准间隔上回测您的策略,您可能会发现在常见周期(如 m30 或 h1)上不会出现的价格模式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
_customTimeFrame = TimeFrameManager.Custom.Add("Custom 45-min");

_customTimeFrame.BarsNeeded = args =>
{
    var m1Bars = MarketData.GetBars(TimeFrame.Minute, args.CustomBars.Symbol.Name);
    var groupedBars = m1Bars
        .GroupBy(bar => bar.OpenTime.Ticks / TimeSpan.FromMinutes(45).Ticks)
        .Select(group => new CustomBar(
            group.First().OpenTime,
            group.First().Open,
            group.Max(b => b.High),
            group.Min(b => b.Low),
            group.Last().Close,
            group.Sum(b => b.TickVolume)
        ));

    args.CustomBars.AppendBars(groupedBars);
};

交易由多个交易品种构建的合成指数

考虑通过平均或组合多个交易品种的价格数据来回测自定义指数上的策略。 例如,您可以使用 EURUSD、GBPUSD 和 AUDUSD 创建一个合成风险指数,然后像回测任何其他交易品种一样对其进行回测。

 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
var base1 = Symbols.GetSymbol("EURUSD");
var base2 = Symbols.GetSymbol("GBPUSD");
var base3 = Symbols.GetSymbol("AUDUSD");

var syntheticSymbol = CustomSymbols.Add("SyntheticRiskOn", Account.Asset, Assets.GetAsset("USD"));
syntheticSymbol.BarsNeeded = args =>
{
    MarketData.GetBarsAsync(TimeFrame.Minute, base1.Name, bars1 =>
    {
        MarketData.GetBarsAsync(TimeFrame.Minute, base2.Name, bars2 =>
        {
            MarketData.GetBarsAsync(TimeFrame.Minute, base3.Name, bars3 =>
            {
                var bars = bars1.Zip(bars2, (b1, b2) => (b1, b2))
                                .Zip(bars3, (pair, b3) =>
                                {
                                    var (b1, b2) = pair;
                                    return new CustomBar(
                                        b1.OpenTime,
                                        (b1.Open + b2.Open + b3.Open) / 3,
                                        (b1.High + b2.High + b3.High) / 3,
                                        (b1.Low + b2.Low + b3.Low) / 3,
                                        (b1.Close + b2.Close + b3.Close) / 3,
                                        (b1.TickVolume + b2.TickVolume + b3.TickVolume)
                                    );
                                });

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

如果您获得了积极的回测结果,您可以继续交易自定义交易品种

运行带有价格噪声的蒙特卡洛模拟

在历史价格数据中注入轻微的随机性,以模拟真实世界的不完美情况,如滑点或价格突发。 使用不同的噪声模式重复进行回测有助于发现脆弱的策略,并验证在不确定性下的稳健性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Random rand = new Random();
foreach (var bar in bars)
{
    var noise = 1 + rand.NextDouble() * 0.001 - 0.0005;
    modifiedBars.Add(new BacktestingBar(
        bar.Time,
        bar.Open * noise,
        bar.High * noise,
        bar.Low * noise,
        bar.Close * noise,
        bar.Volume
    ));
}