Skip to content

Commit 3c0136c

Browse files
committed
Migrated project to .NET 10, updated benchmarks and README accordingly, optimized SymbolDataSplitterV2 by removing redundant .ToList() calls, and improved candlestick processing efficiency with in-place modifications.
1 parent e74f73e commit 3c0136c

File tree

3 files changed

+29
-35
lines changed

3 files changed

+29
-35
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22
<PropertyGroup>
33
<!-- Common settings for all projects -->
4-
<TargetFramework>net9.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<LangVersion>latestmajor</LangVersion>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![NuGet](https://img.shields.io/nuget/v/Backtest.Net.svg)](https://www.nuget.org/packages/Backtest.Net/)
44
[![NuGet Downloads](https://img.shields.io/nuget/dt/Backtest.Net.svg)](https://www.nuget.org/packages/Backtest.Net/)
55
[![Build Status](https://github.com/islero/High-Performance-Backtest.Net/actions/workflows/ci.yml/badge.svg)](https://github.com/islero/High-Performance-Backtest.Net/actions/workflows/ci.yml)
6-
[![.NET](https://img.shields.io/badge/.NET-9.0-512BD4)](https://dotnet.microsoft.com/)
6+
[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/)
77

88
A high-performance backtesting engine for algorithmic trading strategies in .NET.
99

@@ -61,7 +61,7 @@ dotnet add package Backtest.Net
6161
```
6262

6363
> [!NOTE]
64-
> Requires .NET 9.0 or later.
64+
> Requires .NET 10.0 or later.
6565
6666
---
6767

@@ -165,28 +165,29 @@ await engine.RunAsync(splitData, cts.Token);
165165

166166
## Benchmarks
167167

168-
Performance benchmarks run on Apple M3 Max with .NET 9.0, processing **4 million candlesticks** (1 symbol × 4 timeframes × 1,000,000 candles each):
168+
Performance benchmarks run on Apple M3 Max with .NET 10.0, processing **4 million candlesticks** (1 symbol × 4 timeframes × 1,000,000 candles each):
169169

170-
| Method | Mean | Error | StdDev | Gen0 | Allocated |
171-
|------------- |---------:|--------:|--------:|-------:|----------:|
172-
| EngineV8Run | 125.7 ns | 0.45 ns | 0.42 ns | 0.0620 | 520 B |
173-
| EngineV9Run | 128.4 ns | 2.31 ns | 2.16 ns | 0.0629 | 528 B |
174-
| EngineV10Run | 102.0 ns | 2.00 ns | 1.96 ns | 0.0545 | 456 B |
170+
| Method | Mean | Error | StdDev | Gen0 | Allocated |
171+
|------------- |---------:|---------:|---------:|-------:|----------:|
172+
| EngineV8Run | 99.78 ns | 1.564 ns | 1.463 ns | 0.0621 | 520 B |
173+
| EngineV9Run | 96.69 ns | 1.933 ns | 2.148 ns | 0.0631 | 528 B |
174+
| EngineV10Run | 80.16 ns | 1.553 ns | 1.453 ns | 0.0545 | 456 B |
175175

176176
**Key findings:**
177-
- **EngineV10** is ~19% faster than EngineV8 and ~21% faster than EngineV9
177+
- **EngineV10** is ~20% faster than EngineV8 and ~17% faster than EngineV9
178178
- **EngineV10** allocates 12% less memory than EngineV8
179-
- All engines maintain sub-microsecond per-tick latency
179+
- All engines maintain sub-100ns per-tick latency
180+
- **.NET 10 migration** improved all engines by ~20% compared to .NET 9
180181

181-
> Benchmarks run with BenchmarkDotNet v0.15.8 on macOS Tahoe 26.2, Apple M3 Max, .NET 9.0.8
182+
> Benchmarks run with BenchmarkDotNet v0.15.8 on macOS Tahoe 26.2, Apple M3 Max, .NET 10.0
182183
183184
---
184185

185186
## Development
186187

187188
### Prerequisites
188189

189-
- [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) or later
190+
- [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) or later
190191
- Git
191192

192193
### Build

src/Backtest.Net/SymbolDataSplitters/SymbolDataSplitterV2.cs

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,12 @@ public class SymbolDataSplitterV2(
2929
/// <returns></returns>
3030
public Task<List<List<SymbolDataV2>>> SplitAsyncV2(List<SymbolDataV2> symbolsData)
3131
{
32-
// --- Enumerating Symbols Data
33-
var symbolsDataList = symbolsData.ToList();
34-
3532
// --- Quick Symbol Data validation
36-
if (!QuickSymbolDataValidationV2(symbolsDataList))
33+
if (!QuickSymbolDataValidationV2(symbolsData))
3734
throw new ArgumentException("symbolsData argument contains invalid or not properly sorted data");
3835

3936
// --- Symbol or timeframe duplicates validation
40-
if (IsThereSymbolTimeframeDuplicatesV2(symbolsDataList))
37+
if (IsThereSymbolTimeframeDuplicatesV2(symbolsData))
4138
throw new ArgumentException("symbolsData contain duplicated symbols or timeframes");
4239

4340
// --- Creating Result Split Symbols Data
@@ -65,19 +62,19 @@ public Task<List<List<SymbolDataV2>>> SplitAsyncV2(List<SymbolDataV2> symbolsDat
6562
}
6663

6764
// --- Getting the correct warmup timeframe
68-
WarmupTimeframe = GetWarmupTimeframeV2(symbolsDataList);
65+
WarmupTimeframe = GetWarmupTimeframeV2(symbolsData);
6966

7067
DateTime ongoingBacktestingTime = BacktestingStartDateTime;
71-
while (!AreAllSymbolDataReachedHistoryEndV2(symbolsDataList))
68+
while (!AreAllSymbolDataReachedHistoryEndV2(symbolsData))
7269
{
7370
var symbolsDataPart = new List<SymbolDataV2>();
74-
foreach (SymbolDataV2 symbol in symbolsDataList)
71+
foreach (SymbolDataV2 symbol in symbolsData)
7572
{
7673
// --- Checking if there is any symbol with no more history
7774
if (symbol.Timeframes.Any(x => x.NoMoreHistory))
7875
{
7976
// --- Adding days per split to ongoing backtesting time
80-
ongoingBacktestingTime = AddDaysToOngoingBacktestingTime(ongoingBacktestingTime, symbol == symbolsDataList.Last());
77+
ongoingBacktestingTime = AddDaysToOngoingBacktestingTime(ongoingBacktestingTime, symbol == symbolsData.Last());
8178

8279
continue;
8380
}
@@ -127,8 +124,8 @@ public Task<List<List<SymbolDataV2>>> SplitAsyncV2(List<SymbolDataV2> symbolsDat
127124
// --- Deleting source candles and readjusting indexes
128125
if (timeframe.StartIndex > 0)
129126
{
130-
// --- Note that candles readjusting is corrupting source candles perform readjusting
131-
timeframe.Candlesticks = timeframe.Candlesticks.Skip(timeframe.StartIndex).ToList();
127+
// --- Remove source candles in-place (more efficient than Skip().ToList())
128+
timeframe.Candlesticks.RemoveRange(0, timeframe.StartIndex);
132129

133130
// --- Perform reindexing
134131
timeframe.Index -= timeframe.StartIndex;
@@ -159,7 +156,7 @@ public Task<List<List<SymbolDataV2>>> SplitAsyncV2(List<SymbolDataV2> symbolsDat
159156
symbolsDataPart.Add(symbolDataPart);
160157

161158
// --- Adding days per split to ongoing backtesting time
162-
ongoingBacktestingTime = AddDaysToOngoingBacktestingTime(ongoingBacktestingTime, symbol == symbolsDataList.Last());
159+
ongoingBacktestingTime = AddDaysToOngoingBacktestingTime(ongoingBacktestingTime, symbol == symbolsData.Last());
163160
}
164161

165162
// --- Append symbolsDataPart if it contains any record
@@ -218,10 +215,9 @@ private static bool QuickSymbolDataValidationV2(List<SymbolDataV2> symbolsData)
218215
/// <returns></returns>
219216
private static bool IsThereSymbolTimeframeDuplicatesV2(List<SymbolDataV2> symbolsData)
220217
{
221-
var symbolDataList = symbolsData.ToList();
222-
bool symbolDuplicatesExist = symbolDataList.GroupBy(x => x.Symbol).Any(symbol => symbol.Count() > 1);
218+
bool symbolDuplicatesExist = symbolsData.GroupBy(x => x.Symbol).Any(symbol => symbol.Count() > 1);
223219
bool timeframeDuplicatesExist = false;
224-
foreach (SymbolDataV2 symbol in symbolDataList)
220+
foreach (SymbolDataV2 symbol in symbolsData)
225221
{
226222
// Validating
227223
if (timeframeDuplicatesExist) continue;
@@ -242,8 +238,7 @@ private static bool IsThereSymbolTimeframeDuplicatesV2(List<SymbolDataV2> symbol
242238
/// <returns></returns>
243239
private static int GetCandlesticksIndexByOpenTimeV2(List<CandlestickV2> candlesticks, DateTime targetDateTime)
244240
{
245-
var candlesticksList = candlesticks.ToList();
246-
int index = candlesticksList.FindIndex(candle => candle.OpenTime >= targetDateTime);
241+
int index = candlesticks.FindIndex(candle => candle.OpenTime >= targetDateTime);
247242

248243
if (index < 0) index = 0;
249244

@@ -263,10 +258,9 @@ private CandlestickInterval GetWarmupTimeframeV2(List<SymbolDataV2> symbolsData)
263258
return WarmupTimeframe.Value;
264259

265260
// --- Setting the lowest symbolsData timeframe
266-
var symbolDataList = symbolsData.ToList();
267-
CandlestickInterval potentialWarmupTimeframe = symbolDataList.Min(x => x.Timeframes.Min(y => y.Timeframe));
261+
CandlestickInterval potentialWarmupTimeframe = symbolsData.Min(x => x.Timeframes.Min(y => y.Timeframe));
268262

269-
foreach (SymbolDataV2 symbol in symbolDataList)
263+
foreach (SymbolDataV2 symbol in symbolsData)
270264
{
271265
foreach (TimeframeV2 timeframe in symbol.Timeframes)
272266
{
@@ -305,7 +299,6 @@ private static bool AreAllSymbolDataReachedHistoryEndV2(List<SymbolDataV2> symbo
305299
/// <returns></returns>
306300
private static int GetCandlesticksIndexByCloseTimeV2(List<CandlestickV2> candlesticks, DateTime targetDateTime)
307301
{
308-
var candlesticksList = candlesticks.ToList();
309-
return candlesticksList.FindIndex(candle => candle.CloseTime >= targetDateTime);
302+
return candlesticks.FindIndex(candle => candle.CloseTime >= targetDateTime);
310303
}
311304
}

0 commit comments

Comments
 (0)