Refactor code formatting and improve consistency across various test files

- Removed unnecessary blank lines in multiple test files to enhance readability.
- Ensured consistent spacing and formatting in the `Trima`, `Usf`, `Vidya`, `Wma`, and `Atr` test classes.
- Updated comments for clarity and consistency in the `Atr` and `Adl` classes.
- Adjusted project files for better structure and maintainability.
This commit is contained in:
Miha Kralj
2025-12-28 17:44:08 -08:00
parent ad6eebf812
commit 13d7c1215d
169 changed files with 10815 additions and 10814 deletions
+15 -15
View File
@@ -113,7 +113,7 @@ public class SimdExtensionsTests
double[] data = new double[1000];
for (int i = 0; i < data.Length; i++)
data[i] = i + 1.0;
var span = new ReadOnlySpan<double>(data);
double expected = 1000.0 * 1001.0 / 2.0;
Assert.Equal(expected, span.SumSIMD(), precision: 8);
@@ -324,7 +324,7 @@ public class SimdExtensionsTests
{
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
var span = new ReadOnlySpan<double>(data);
double variance = span.VarianceSIMD();
Assert.True(Math.Abs(variance - 4.571428) < 0.0001);
}
@@ -334,10 +334,10 @@ public class SimdExtensionsTests
{
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
var span = new ReadOnlySpan<double>(data);
double mean = 5.0;
double variance = span.VarianceSIMD(mean);
Assert.True(variance > 0);
}
@@ -379,7 +379,7 @@ public class SimdExtensionsTests
{
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
var span = new ReadOnlySpan<double>(data);
double stdDev = span.StdDevSIMD();
Assert.True(Math.Abs(stdDev - 2.138) < 0.01);
}
@@ -389,7 +389,7 @@ public class SimdExtensionsTests
{
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
var span = new ReadOnlySpan<double>(data);
double stdDev = span.StdDevSIMD(5.0);
Assert.True(stdDev > 0);
}
@@ -556,14 +556,14 @@ public class SimdExtensionsTests
public void SIMD_WorksWithTSeriesValues()
{
var series = new TSeries(100);
for (int i = 0; i < 100; i++)
{
series.Add(DateTime.UtcNow.Ticks + i, i + 1.0);
}
var values = series.Values;
double sum = values.SumSIMD();
double avg = values.AverageSIMD();
double min = values.MinSIMD();
@@ -587,7 +587,7 @@ public class SimdExtensionsTests
var bars = gbm.Fetch(1000, startTime, interval);
var closeValues = bars.Close.Values;
double sum = closeValues.SumSIMD();
double avg = closeValues.AverageSIMD();
double min = closeValues.MinSIMD();
@@ -611,7 +611,7 @@ public class SimdExtensionsTests
_ = closeValues.SumSIMD();
var sw = System.Diagnostics.Stopwatch.StartNew();
double sum = closeValues.SumSIMD();
double avg = closeValues.AverageSIMD();
double min = closeValues.MinSIMD();
@@ -619,7 +619,7 @@ public class SimdExtensionsTests
var (minAlt, maxAlt) = closeValues.MinMaxSIMD();
double variance = closeValues.VarianceSIMD();
double stdDev = closeValues.StdDevSIMD();
sw.Stop();
Assert.True(sum > 0);
@@ -630,8 +630,8 @@ public class SimdExtensionsTests
Assert.Equal(max, maxAlt);
Assert.True(variance > 0);
Assert.True(stdDev > 0);
Assert.True(sw.ElapsedMilliseconds < 50,
Assert.True(sw.ElapsedMilliseconds < 50,
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 50ms");
}
@@ -640,12 +640,12 @@ public class SimdExtensionsTests
{
double[] data = [1.0, 2.0, 3.0];
var span = new ReadOnlySpan<double>(data);
Assert.Equal(6.0, span.SumSIMD());
Assert.Equal(1.0, span.MinSIMD());
Assert.Equal(3.0, span.MaxSIMD());
Assert.Equal(2.0, span.AverageSIMD());
var (min, max) = span.MinMaxSIMD();
Assert.Equal(1.0, min);
Assert.Equal(3.0, max);
+52 -52
View File
@@ -1,52 +1,52 @@
# SimdExtensions Class
`SimdExtensions` provides high-performance, SIMD-accelerated extension methods for `ReadOnlySpan<double>`. It leverages .NET's `Vector<T>` to achieve 4-8x speedups on supported hardware (AVX2, AVX-512) while automatically falling back to scalar implementations on older hardware.
## Key Features
- **Hardware Acceleration**: Uses CPU vector registers to process multiple elements in parallel.
- **Automatic Fallback**: Gracefully handles non-SIMD hardware or small arrays.
- **Zero-Allocation**: Operates directly on spans without creating new arrays.
- **Aggressive Inlining**: Methods are marked for inlining to minimize call overhead.
## Available Methods
| Method | Description |
|--------|-------------|
| `ContainsNonFinite()` | Checks if span contains any non-finite values (NaN or Infinity). |
| `SumSIMD()` | Calculates the sum of elements. |
| `MinSIMD()` | Finds the minimum value. |
| `MaxSIMD()` | Finds the maximum value. |
| `MinMaxSIMD()` | Finds both min and max in a single pass (more efficient than separate calls). |
| `AverageSIMD()` | Calculates the arithmetic mean. |
| `VarianceSIMD()` | Calculates the sample variance. |
| `StdDevSIMD()` | Calculates the sample standard deviation. |
| `DotProduct()` | Calculates the dot product of two spans. |
## Performance
On modern CPUs (e.g., Intel Core i7/i9, AMD Ryzen), these methods typically outperform standard LINQ or scalar loops by a factor of 4 to 8 for large arrays.
## Usage
```csharp
using QuanTAlib;
double[] data = { 1.0, 2.0, 3.0, 4.0, 5.0, ... };
ReadOnlySpan<double> span = data;
// Calculate sum
double sum = span.SumSIMD();
// Calculate min and max in one pass
var (min, max) = span.MinMaxSIMD();
// Calculate standard deviation
double stdDev = span.StdDevSIMD();
// Check for valid data
bool hasInvalid = span.ContainsNonFinite();
// Calculate dot product
double dot = span.DotProduct(otherSpan);
```
# SimdExtensions Class
`SimdExtensions` provides high-performance, SIMD-accelerated extension methods for `ReadOnlySpan<double>`. It leverages .NET's `Vector<T>` to achieve 4-8x speedups on supported hardware (AVX2, AVX-512) while automatically falling back to scalar implementations on older hardware.
## Key Features
- **Hardware Acceleration**: Uses CPU vector registers to process multiple elements in parallel.
- **Automatic Fallback**: Gracefully handles non-SIMD hardware or small arrays.
- **Zero-Allocation**: Operates directly on spans without creating new arrays.
- **Aggressive Inlining**: Methods are marked for inlining to minimize call overhead.
## Available Methods
| Method | Description |
|--------|-------------|
| `ContainsNonFinite()` | Checks if span contains any non-finite values (NaN or Infinity). |
| `SumSIMD()` | Calculates the sum of elements. |
| `MinSIMD()` | Finds the minimum value. |
| `MaxSIMD()` | Finds the maximum value. |
| `MinMaxSIMD()` | Finds both min and max in a single pass (more efficient than separate calls). |
| `AverageSIMD()` | Calculates the arithmetic mean. |
| `VarianceSIMD()` | Calculates the sample variance. |
| `StdDevSIMD()` | Calculates the sample standard deviation. |
| `DotProduct()` | Calculates the dot product of two spans. |
## Performance
On modern CPUs (e.g., Intel Core i7/i9, AMD Ryzen), these methods typically outperform standard LINQ or scalar loops by a factor of 4 to 8 for large arrays.
## Usage
```csharp
using QuanTAlib;
double[] data = { 1.0, 2.0, 3.0, 4.0, 5.0, ... };
ReadOnlySpan<double> span = data;
// Calculate sum
double sum = span.SumSIMD();
// Calculate min and max in one pass
var (min, max) = span.MinMaxSIMD();
// Calculate standard deviation
double stdDev = span.StdDevSIMD();
// Check for valid data
bool hasInvalid = span.ContainsNonFinite();
// Calculate dot product
double dot = span.DotProduct(otherSpan);
```
+2 -2
View File
@@ -168,9 +168,9 @@ public class TBarTests
{
long time = DateTime.UtcNow.Ticks;
var bar = new TBar(time, 100, 110, 90, 105, 1000);
TValue tv = bar;
Assert.Equal(time, tv.Time);
Assert.Equal(105.0, tv.Value);
}
+112 -112
View File
@@ -1,112 +1,112 @@
# TBar: OHLCV Bar Struct
## What It Does
`TBar` is a lightweight, immutable struct representing a single OHLCV (Open, High, Low, Close, Volume) bar. It serves as the fundamental unit for price data in QuanTAlib, designed to hold market data with minimal memory overhead while providing convenient accessors for common price derivations.
## Design Philosophy
Financial data processing often involves millions of bars. Storing these as classes would create massive GC pressure and memory fragmentation. `TBar` is designed as a **pure data struct** to ensure:
* **Compactness**: Occupies exactly 48 bytes (1 `long` + 5 `double`s), fitting efficiently in memory.
* **Immutability**: Thread-safe by default; values cannot change once created.
* **Zero-Cost Abstractions**: Computed properties (like `HL2`) are calculated on-demand, requiring no extra storage.
## How It Works
`TBar` is a `readonly record struct` that stores:
* **Time**: Timestamp in ticks.
* **Open, High, Low, Close**: Price components.
* **Volume**: Traded volume.
It includes implicit conversions to `double` (defaulting to Close price) and `TValue` (Time + Close), allowing it to be used interchangeably with simpler types in many contexts.
## Structure
### Definition
```csharp
public readonly record struct TBar(long Time, double Open, double High, double Low, double Close, double Volume);
```
### Core Properties
| Property | Type | Description |
|----------|------|-------------|
| `Time` | `long` | Timestamp in ticks (UTC). |
| `Open` | `double` | Opening price. |
| `High` | `double` | Highest price. |
| `Low` | `double` | Lowest price. |
| `Close` | `double` | Closing price. |
| `Volume` | `double` | Traded volume. |
### Computed Properties (Zero-Storage)
| Property | Formula | Description |
|----------|---------|-------------|
| `HL2` | `(H + L) / 2` | Median Price. |
| `OC2` | `(O + C) / 2` | Midpoint Price. |
| `OHL3` | `(O + H + L) / 3` | Typical Price (Variant). |
| `HLC3` | `(H + L + C) / 3` | Typical Price. |
| `OHLC4` | `(O + H + L + C) / 4` | Weighted Close. |
| `HLCC4` | `(H + L + 2C) / 4` | Weighted Close (Variant). |
### TValue Accessors
Efficiently extracts components as `TValue` pairs:
* `O`, `H`, `L`, `C`, `V`
## Usage
### Creating a Bar
```csharp
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
```
### Implicit Conversions
```csharp
TBar bar = ...;
// Treat as double (uses Close price)
double price = bar;
// Treat as TValue (Time + Close)
TValue tv = bar;
// Treat as DateTime
DateTime dt = bar;
```
### Using Computed Properties
```csharp
// Calculate Typical Price on the fly
double typical = bar.HLC3;
```
## Performance Profile
* **Memory**: 48 bytes per instance.
* **Allocation**: 0 bytes (Stack allocated).
* **Access**: Direct field access (no property overhead).
## Integration
`TBar` is the primary input for:
* **TBarSeries**: A collection of bars.
* **Indicators**: Some indicators (like ATR) require full `TBar` input rather than just a single value.
## Architecture Notes
* **SkipLocalsInit**: Marked with `[SkipLocalsInit]` for performance in tight loops.
* **AggressiveInlining**: All computed properties are inlined to ensure they are as fast as writing the formula manually.
## References
* [OHLC Chart](https://en.wikipedia.org/wiki/Open-high-low-close_chart)
* [C# Record Structs](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record)
# TBar: OHLCV Bar Struct
## What It Does
`TBar` is a lightweight, immutable struct representing a single OHLCV (Open, High, Low, Close, Volume) bar. It serves as the fundamental unit for price data in QuanTAlib, designed to hold market data with minimal memory overhead while providing convenient accessors for common price derivations.
## Design Philosophy
Financial data processing often involves millions of bars. Storing these as classes would create massive GC pressure and memory fragmentation. `TBar` is designed as a **pure data struct** to ensure:
* **Compactness**: Occupies exactly 48 bytes (1 `long` + 5 `double`s), fitting efficiently in memory.
* **Immutability**: Thread-safe by default; values cannot change once created.
* **Zero-Cost Abstractions**: Computed properties (like `HL2`) are calculated on-demand, requiring no extra storage.
## How It Works
`TBar` is a `readonly record struct` that stores:
* **Time**: Timestamp in ticks.
* **Open, High, Low, Close**: Price components.
* **Volume**: Traded volume.
It includes implicit conversions to `double` (defaulting to Close price) and `TValue` (Time + Close), allowing it to be used interchangeably with simpler types in many contexts.
## Structure
### Definition
```csharp
public readonly record struct TBar(long Time, double Open, double High, double Low, double Close, double Volume);
```
### Core Properties
| Property | Type | Description |
|----------|------|-------------|
| `Time` | `long` | Timestamp in ticks (UTC). |
| `Open` | `double` | Opening price. |
| `High` | `double` | Highest price. |
| `Low` | `double` | Lowest price. |
| `Close` | `double` | Closing price. |
| `Volume` | `double` | Traded volume. |
### Computed Properties (Zero-Storage)
| Property | Formula | Description |
|----------|---------|-------------|
| `HL2` | `(H + L) / 2` | Median Price. |
| `OC2` | `(O + C) / 2` | Midpoint Price. |
| `OHL3` | `(O + H + L) / 3` | Typical Price (Variant). |
| `HLC3` | `(H + L + C) / 3` | Typical Price. |
| `OHLC4` | `(O + H + L + C) / 4` | Weighted Close. |
| `HLCC4` | `(H + L + 2C) / 4` | Weighted Close (Variant). |
### TValue Accessors
Efficiently extracts components as `TValue` pairs:
* `O`, `H`, `L`, `C`, `V`
## Usage
### Creating a Bar
```csharp
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
```
### Implicit Conversions
```csharp
TBar bar = ...;
// Treat as double (uses Close price)
double price = bar;
// Treat as TValue (Time + Close)
TValue tv = bar;
// Treat as DateTime
DateTime dt = bar;
```
### Using Computed Properties
```csharp
// Calculate Typical Price on the fly
double typical = bar.HLC3;
```
## Performance Profile
* **Memory**: 48 bytes per instance.
* **Allocation**: 0 bytes (Stack allocated).
* **Access**: Direct field access (no property overhead).
## Integration
`TBar` is the primary input for:
* **TBarSeries**: A collection of bars.
* **Indicators**: Some indicators (like ATR) require full `TBar` input rather than just a single value.
## Architecture Notes
* **SkipLocalsInit**: Marked with `[SkipLocalsInit]` for performance in tight loops.
* **AggressiveInlining**: All computed properties are inlined to ensure they are as fast as writing the formula manually.
## References
* [OHLC Chart](https://en.wikipedia.org/wiki/Open-high-low-close_chart)
* [C# Record Structs](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record)
+39 -39
View File
@@ -40,9 +40,9 @@ public class TBarSeriesTests
{
var series = new TBarSeries();
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
series.Add(bar, isNew: true);
Assert.Single(series);
Assert.Equal(105.0, series.Last.Close);
}
@@ -54,10 +54,10 @@ public class TBarSeriesTests
long time = DateTime.UtcNow.Ticks;
var bar1 = new TBar(time, 100, 110, 90, 105, 1000);
var bar2 = new TBar(time, 100, 112, 90, 108, 1200);
series.Add(bar1, isNew: true);
series.Add(bar2, isNew: false);
Assert.Single(series);
Assert.Equal(108.0, series.Last.Close);
Assert.Equal(112.0, series.Last.High);
@@ -68,9 +68,9 @@ public class TBarSeriesTests
{
var series = new TBarSeries();
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
series.Add(bar, isNew: false);
Assert.Single(series);
}
@@ -79,9 +79,9 @@ public class TBarSeriesTests
{
var series = new TBarSeries();
long time = DateTime.UtcNow.Ticks;
series.Add(time, 100, 110, 90, 105, 1000, isNew: true);
Assert.Single(series);
Assert.Equal(time, series.Last.Time);
}
@@ -91,9 +91,9 @@ public class TBarSeriesTests
{
var series = new TBarSeries();
var dt = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
series.Add(dt, 100, 110, 90, 105, 1000, isNew: true);
Assert.Single(series);
Assert.Equal(dt.Ticks, series.Last.Time);
}
@@ -108,9 +108,9 @@ public class TBarSeriesTests
var lows = new double[] { 5, 15, 25 };
var closes = new double[] { 12, 22, 32 };
var volumes = new double[] { 100, 200, 300 };
series.Add(times, opens, highs, lows, closes, volumes);
Assert.Equal(3, series.Count);
Assert.Equal(10, series[0].Open);
Assert.Equal(32, series[2].Close);
@@ -121,15 +121,15 @@ public class TBarSeriesTests
{
var series = new TBarSeries();
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
series.Add(bar, isNew: true);
Assert.Single(series.Open);
Assert.Single(series.High);
Assert.Single(series.Low);
Assert.Single(series.Close);
Assert.Single(series.Volume);
Assert.Equal(100.0, series.Open.Last.Value);
Assert.Equal(110.0, series.High.Last.Value);
Assert.Equal(90.0, series.Low.Last.Value);
@@ -143,7 +143,7 @@ public class TBarSeriesTests
var series = new TBarSeries();
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
series.Add(bar, isNew: true);
Assert.Same(series.Open, series.O);
Assert.Same(series.High, series.H);
Assert.Same(series.Low, series.L);
@@ -155,7 +155,7 @@ public class TBarSeriesTests
public void SubSeries_HaveCorrectNames()
{
var series = new TBarSeries();
Assert.Equal("Open", series.Open.Name);
Assert.Equal("High", series.High.Name);
Assert.Equal("Low", series.Low.Name);
@@ -167,9 +167,9 @@ public class TBarSeriesTests
public void Last_EmptySeries_ReturnsDefault()
{
var series = new TBarSeries();
var last = series.Last;
Assert.Equal(0, last.Time);
Assert.Equal(0.0, last.Open);
Assert.Equal(0.0, last.Close);
@@ -181,9 +181,9 @@ public class TBarSeriesTests
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
var last = series.Last;
Assert.Equal(200, last.Time);
Assert.Equal(22.0, last.Close);
}
@@ -201,7 +201,7 @@ public class TBarSeriesTests
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
Assert.Equal(200, series.LastTime);
}
@@ -218,7 +218,7 @@ public class TBarSeriesTests
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
Assert.Equal(20.0, series.LastOpen);
}
@@ -235,7 +235,7 @@ public class TBarSeriesTests
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
Assert.Equal(25.0, series.LastHigh);
}
@@ -252,7 +252,7 @@ public class TBarSeriesTests
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
Assert.Equal(15.0, series.LastLow);
}
@@ -269,7 +269,7 @@ public class TBarSeriesTests
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
Assert.Equal(22.0, series.LastClose);
}
@@ -286,7 +286,7 @@ public class TBarSeriesTests
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
Assert.Equal(200.0, series.LastVolume);
}
@@ -297,7 +297,7 @@ public class TBarSeriesTests
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
series.Add(300, 30, 35, 25, 32, 300);
Assert.Equal(100, series[0].Time);
Assert.Equal(10.0, series[0].Open);
Assert.Equal(200, series[1].Time);
@@ -310,12 +310,12 @@ public class TBarSeriesTests
public void Count_ReturnsCorrectValue()
{
var series = new TBarSeries();
Assert.Empty(series);
series.Add(100, 10, 15, 5, 12, 100);
Assert.Single(series);
series.Add(200, 20, 25, 15, 22, 200);
Assert.Equal(2, series.Count);
}
@@ -327,9 +327,9 @@ public class TBarSeriesTests
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
series.Add(300, 30, 35, 25, 32, 300);
var list = series.ToList();
Assert.Equal(3, list.Count);
Assert.Equal(10.0, list[0].Open);
Assert.Equal(22.0, list[1].Close);
@@ -349,7 +349,7 @@ public class TBarSeriesTests
{
list.Add(item);
}
Assert.Equal(2, list.Count);
}
@@ -362,7 +362,7 @@ public class TBarSeriesTests
var barToAdd = new TBar(100, 10, 15, 5, 12, 100);
series.Add(barToAdd, isNew: true);
Assert.NotNull(received);
Assert.Equal(100, received.Value.Time);
Assert.Equal(12.0, received.Value.Close);
@@ -377,7 +377,7 @@ public class TBarSeriesTests
series.Pub += (object? sender, in TBarEventArgs args) => received = args.Value;
series.Add(100, 10, 18, 5, 15, 150, isNew: false);
Assert.NotNull(received);
Assert.Equal(15.0, received.Value.Close);
Assert.Equal(18.0, received.Value.High);
@@ -389,7 +389,7 @@ public class TBarSeriesTests
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
Assert.Equal(series.Open.Times[0], series.Close.Times[0]);
Assert.Equal(series.High.Times[1], series.Volume.Times[1]);
}
@@ -398,11 +398,11 @@ public class TBarSeriesTests
public void Add_MultipleBars_MaintainsOrder()
{
var series = new TBarSeries();
series.Add(100, 10, 15, 5, 12, 100);
series.Add(200, 20, 25, 15, 22, 200);
series.Add(300, 30, 35, 25, 32, 300);
Assert.Equal(3, series.Count);
Assert.Equal(100, series[0].Time);
Assert.Equal(200, series[1].Time);
+121 -121
View File
@@ -1,121 +1,121 @@
# TBarSeries: OHLCV Data Container
## What It Does
`TBarSeries` is a high-performance collection of OHLCV bars. It is the primary data structure for managing historical and real-time market data in QuanTAlib. It uses a **Structure of Arrays (SoA)** layout to optimize memory access and enable efficient SIMD operations across individual price components.
## Design Philosophy
A naive implementation of a bar series would be a `List<TBar>`. However, this is inefficient for technical analysis. Most indicators only need one component at a time (e.g., SMA uses Close prices). Iterating over a `List<TBar>` to get Close prices loads unnecessary Open, High, Low, and Volume data into the CPU cache, wasting bandwidth.
`TBarSeries` solves this by storing each component in its own contiguous array. This allows:
* **Component Views**: You can access `Close` prices as a `TSeries` without copying data.
* **Cache Efficiency**: Iterating over `Close` prices loads *only* Close prices.
* **Unified Time**: All component series share a single Time array, ensuring synchronization.
## How It Works
Internally, `TBarSeries` maintains six parallel lists:
1. `_t` (Time)
2. `_o` (Open)
3. `_h` (High)
4. `_l` (Low)
5. `_c` (Close)
6. `_v` (Volume)
It exposes these internal lists as `TSeries` properties (`Open`, `High`, `Low`, `Close`, `Volume`), which act as read-only views into the master data.
## Structure
### Definition
```csharp
public class TBarSeries : IReadOnlyList<TBar>
{
// Component Views (TSeries)
public TSeries Open { get; }
public TSeries High { get; }
public TSeries Low { get; }
public TSeries Close { get; }
public TSeries Volume { get; }
// Aliases
public TSeries O => Open;
public TSeries H => High;
public TSeries L => Low;
public TSeries C => Close;
public TSeries V => Volume;
}
```
### Core Methods
| Method | Description |
|--------|-------------|
| `Add(TBar bar, bool isNew)` | Adds a bar or updates the last one. |
| `Add(DateTime time, double o, double h, double l, double c, double v)` | Adds raw values directly. |
| `Count` | Returns the number of bars. |
| `Last` | Returns the most recent `TBar`. |
## Usage
### Creating and Populating
```csharp
var bars = new TBarSeries();
// Add a new bar
bars.Add(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
// Add raw values
bars.Add(DateTime.UtcNow, 100, 105, 95, 102, 1000);
```
### Accessing Data
```csharp
// Get the last full bar
TBar lastBar = bars.Last;
// Get the Close series (Zero-Copy)
TSeries closes = bars.Close;
// Calculate SMA on Close prices
var sma = new Sma(14);
var result = sma.Calculate(bars.Close);
```
### Streaming Updates
```csharp
// New minute starts
bars.Add(newBar, isNew: true);
// Price updates within the same minute
bars.Add(updatedBar, isNew: false); // Updates the last bar in place
```
## Performance Profile
* **Memory Layout**: SoA (Structure of Arrays).
* **Component Access**: Zero-copy `TSeries` views.
* **Iteration**: Cache-friendly for single-component analysis.
## Integration
`TBarSeries` is the standard input for multi-input indicators (like ATR, ADX) and the primary data source for trading strategies.
* **Indicators**: Can be passed to indicators that require full bar data.
* **Strategies**: Provides the historical context needed for signal generation.
## Architecture Notes
* **Shared Storage**: The `TSeries` views (`Open`, `Close`, etc.) do not own their data; they point to the internal lists of the `TBarSeries`. This means modifying the `TBarSeries` automatically updates all views.
* **Synchronization**: Because all views share the same `_t` (Time) list, they are guaranteed to be perfectly synchronized.
## References
* [Structure of Arrays (SoA)](https://en.wikipedia.org/wiki/AOS_and_SOA)
* [Data Locality](https://gameprogrammingpatterns.com/data-locality.html)
# TBarSeries: OHLCV Data Container
## What It Does
`TBarSeries` is a high-performance collection of OHLCV bars. It is the primary data structure for managing historical and real-time market data in QuanTAlib. It uses a **Structure of Arrays (SoA)** layout to optimize memory access and enable efficient SIMD operations across individual price components.
## Design Philosophy
A naive implementation of a bar series would be a `List<TBar>`. However, this is inefficient for technical analysis. Most indicators only need one component at a time (e.g., SMA uses Close prices). Iterating over a `List<TBar>` to get Close prices loads unnecessary Open, High, Low, and Volume data into the CPU cache, wasting bandwidth.
`TBarSeries` solves this by storing each component in its own contiguous array. This allows:
* **Component Views**: You can access `Close` prices as a `TSeries` without copying data.
* **Cache Efficiency**: Iterating over `Close` prices loads *only* Close prices.
* **Unified Time**: All component series share a single Time array, ensuring synchronization.
## How It Works
Internally, `TBarSeries` maintains six parallel lists:
1. `_t` (Time)
2. `_o` (Open)
3. `_h` (High)
4. `_l` (Low)
5. `_c` (Close)
6. `_v` (Volume)
It exposes these internal lists as `TSeries` properties (`Open`, `High`, `Low`, `Close`, `Volume`), which act as read-only views into the master data.
## Structure
### Definition
```csharp
public class TBarSeries : IReadOnlyList<TBar>
{
// Component Views (TSeries)
public TSeries Open { get; }
public TSeries High { get; }
public TSeries Low { get; }
public TSeries Close { get; }
public TSeries Volume { get; }
// Aliases
public TSeries O => Open;
public TSeries H => High;
public TSeries L => Low;
public TSeries C => Close;
public TSeries V => Volume;
}
```
### Core Methods
| Method | Description |
|--------|-------------|
| `Add(TBar bar, bool isNew)` | Adds a bar or updates the last one. |
| `Add(DateTime time, double o, double h, double l, double c, double v)` | Adds raw values directly. |
| `Count` | Returns the number of bars. |
| `Last` | Returns the most recent `TBar`. |
## Usage
### Creating and Populating
```csharp
var bars = new TBarSeries();
// Add a new bar
bars.Add(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
// Add raw values
bars.Add(DateTime.UtcNow, 100, 105, 95, 102, 1000);
```
### Accessing Data
```csharp
// Get the last full bar
TBar lastBar = bars.Last;
// Get the Close series (Zero-Copy)
TSeries closes = bars.Close;
// Calculate SMA on Close prices
var sma = new Sma(14);
var result = sma.Calculate(bars.Close);
```
### Streaming Updates
```csharp
// New minute starts
bars.Add(newBar, isNew: true);
// Price updates within the same minute
bars.Add(updatedBar, isNew: false); // Updates the last bar in place
```
## Performance Profile
* **Memory Layout**: SoA (Structure of Arrays).
* **Component Access**: Zero-copy `TSeries` views.
* **Iteration**: Cache-friendly for single-component analysis.
## Integration
`TBarSeries` is the standard input for multi-input indicators (like ATR, ADX) and the primary data source for trading strategies.
* **Indicators**: Can be passed to indicators that require full bar data.
* **Strategies**: Provides the historical context needed for signal generation.
## Architecture Notes
* **Shared Storage**: The `TSeries` views (`Open`, `Close`, etc.) do not own their data; they point to the internal lists of the `TBarSeries`. This means modifying the `TBarSeries` automatically updates all views.
* **Synchronization**: Because all views share the same `_t` (Time) list, they are guaranteed to be perfectly synchronized.
## References
* [Structure of Arrays (SoA)](https://en.wikipedia.org/wiki/AOS_and_SOA)
* [Data Locality](https://gameprogrammingpatterns.com/data-locality.html)
+2 -1
View File
@@ -16,9 +16,9 @@ public readonly struct TValueEventArgs
// Performance-focused event args struct; not derived from EventArgs by design.
// We intentionally deviate from the standard EventArgs pattern here for perf.
// MA0046 suppressed: struct-based args avoid heap allocations in high-frequency events.
#pragma warning disable MA0046 // The second parameter must be of type 'System.EventArgs' or a derived type
public delegate void TValuePublishedHandler(object? sender, in TValueEventArgs args);
#pragma warning restore MA0046
/// <summary>
/// Interface for objects that publish TValue updates.
@@ -30,3 +30,4 @@ public interface ITValuePublisher
/// </summary>
event TValuePublishedHandler? Pub;
}
#pragma warning restore MA0046
+2 -2
View File
@@ -298,7 +298,7 @@ public class TSeriesTests
{
var series = new TSeries();
TValue? received = null;
series.Pub += (object? sender, TValueEventArgs args) => received = args.Value;
series.Pub += (object? sender, in TValueEventArgs args) => received = args.Value;
series.Add(100, 42.0);
@@ -313,7 +313,7 @@ public class TSeriesTests
var series = new TSeries();
TValue? received = null;
series.Add(100, 42.0);
series.Pub += (object? sender, TValueEventArgs args) => received = args.Value;
series.Pub += (object? sender, in TValueEventArgs args) => received = args.Value;
series.Add(100, 43.0, isNew: false);
+114 -114
View File
@@ -1,114 +1,114 @@
# TSeries: Time Series Data Container
## What It Does
`TSeries` is a high-performance, memory-efficient container for time-series data. Unlike standard collections (like `List<TValue>`), it uses a **Structure of Arrays (SoA)** layout internally. This means it stores timestamps and values in separate contiguous arrays, optimizing memory access patterns for numerical processing and SIMD vectorization.
## Design Philosophy
Standard object-oriented collections (Array of Structures - AoS) are cache-inefficient for numerical algorithms. When calculating a moving average, the CPU only needs the values, but an AoS layout forces it to load interleaved timestamps into the cache, wasting bandwidth.
`TSeries` solves this by decoupling time and value storage:
* **Cache Locality**: Iterating over values loads only values.
* **SIMD Readiness**: The internal value array can be exposed directly as a `Span<double>` for AVX/SSE processing.
* **Zero-Copy Views**: Data is accessed without defensive copying, ensuring maximum throughput.
## How It Works
`TSeries` maintains two parallel internal lists:
1. `List<long> _t`: Stores timestamps.
2. `List<double> _v`: Stores values.
It implements `IReadOnlyList<TValue>`, allowing it to be treated as a standard collection of `TValue` structs when needed, but its true power lies in its column-oriented properties (`Values`, `Times`).
## Structure
### Definition
```csharp
public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
```
### Core Properties
| Property | Type | Description |
|----------|------|-------------|
| `Values` | `ReadOnlySpan<double>` | Direct access to the value array (SIMD-ready). |
| `Times` | `ReadOnlySpan<long>` | Direct access to the timestamp array. |
| `Last` | `TValue` | The most recent time-value pair. |
| `Count` | `int` | Number of elements in the series. |
| `Name` | `string` | Optional identifier for the series. |
### Events
| Event | Type | Description |
|-------|------|-------------|
| `Pub` | `Action<TValue>` | Fired whenever a new value is added or updated. |
## Usage
### Creating and Populating
```csharp
var series = new TSeries();
// Add a new bar (isNew = true by default)
series.Add(DateTime.UtcNow, 100.0);
// Add multiple values
series.Add(new List<double> { 1.0, 2.0, 3.0 });
```
### Streaming Updates (Real-time)
`TSeries` supports "bar updates" where the last value changes until the bar closes.
```csharp
// New minute starts
series.Add(time, 100.0, isNew: true);
// Price updates within the same minute
series.Add(time, 101.0, isNew: false); // Overwrites last value
series.Add(time, 102.0, isNew: false); // Overwrites last value
```
### SIMD Processing
```csharp
// Calculate average using SIMD (via Span)
double sum = 0;
foreach (var v in series.Values) { sum += v; } // Compiler vectorizes this
```
### Reactive Subscription
```csharp
series.Pub += (item) => Console.WriteLine($"New value: {item}");
```
## Performance Profile
* **Memory Layout**: SoA (Structure of Arrays).
* **Access Speed**: O(1) for random access.
* **Iteration**: Cache-friendly linear scan.
* **SIMD**: Fully supported via `Values` span.
## Integration
`TSeries` is the standard output format for all indicators in QuanTAlib.
* **Input**: Can be fed into indicators via `Update(TSeries)`.
* **Output**: Indicators return `TSeries` from their `Calculate` methods.
* **Visualization**: Easily mappable to charting libraries due to separate Time/Value arrays.
## Architecture Notes
* **CollectionsMarshal**: Uses `CollectionsMarshal.AsSpan` to expose internal list storage as spans without copying. This is unsafe if the list is modified during span access, but provides maximum performance for single-threaded algorithms.
* **Virtual Methods**: `Add` is virtual to allow derived classes (like `TBarSeries` components) to intercept updates if necessary.
## References
* [Data-Oriented Design](https://en.wikipedia.org/wiki/Data-oriented_design)
* [SIMD in .NET](https://learn.microsoft.com/en-us/dotnet/standard/simd)
# TSeries: Time Series Data Container
## What It Does
`TSeries` is a high-performance, memory-efficient container for time-series data. Unlike standard collections (like `List<TValue>`), it uses a **Structure of Arrays (SoA)** layout internally. This means it stores timestamps and values in separate contiguous arrays, optimizing memory access patterns for numerical processing and SIMD vectorization.
## Design Philosophy
Standard object-oriented collections (Array of Structures - AoS) are cache-inefficient for numerical algorithms. When calculating a moving average, the CPU only needs the values, but an AoS layout forces it to load interleaved timestamps into the cache, wasting bandwidth.
`TSeries` solves this by decoupling time and value storage:
* **Cache Locality**: Iterating over values loads only values.
* **SIMD Readiness**: The internal value array can be exposed directly as a `Span<double>` for AVX/SSE processing.
* **Zero-Copy Views**: Data is accessed without defensive copying, ensuring maximum throughput.
## How It Works
`TSeries` maintains two parallel internal lists:
1. `List<long> _t`: Stores timestamps.
2. `List<double> _v`: Stores values.
It implements `IReadOnlyList<TValue>`, allowing it to be treated as a standard collection of `TValue` structs when needed, but its true power lies in its column-oriented properties (`Values`, `Times`).
## Structure
### Definition
```csharp
public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
```
### Core Properties
| Property | Type | Description |
|----------|------|-------------|
| `Values` | `ReadOnlySpan<double>` | Direct access to the value array (SIMD-ready). |
| `Times` | `ReadOnlySpan<long>` | Direct access to the timestamp array. |
| `Last` | `TValue` | The most recent time-value pair. |
| `Count` | `int` | Number of elements in the series. |
| `Name` | `string` | Optional identifier for the series. |
### Events
| Event | Type | Description |
|-------|------|-------------|
| `Pub` | `Action<TValue>` | Fired whenever a new value is added or updated. |
## Usage
### Creating and Populating
```csharp
var series = new TSeries();
// Add a new bar (isNew = true by default)
series.Add(DateTime.UtcNow, 100.0);
// Add multiple values
series.Add(new List<double> { 1.0, 2.0, 3.0 });
```
### Streaming Updates (Real-time)
`TSeries` supports "bar updates" where the last value changes until the bar closes.
```csharp
// New minute starts
series.Add(time, 100.0, isNew: true);
// Price updates within the same minute
series.Add(time, 101.0, isNew: false); // Overwrites last value
series.Add(time, 102.0, isNew: false); // Overwrites last value
```
### SIMD Processing
```csharp
// Calculate average using SIMD (via Span)
double sum = 0;
foreach (var v in series.Values) { sum += v; } // Compiler vectorizes this
```
### Reactive Subscription
```csharp
series.Pub += (item) => Console.WriteLine($"New value: {item}");
```
## Performance Profile
* **Memory Layout**: SoA (Structure of Arrays).
* **Access Speed**: O(1) for random access.
* **Iteration**: Cache-friendly linear scan.
* **SIMD**: Fully supported via `Values` span.
## Integration
`TSeries` is the standard output format for all indicators in QuanTAlib.
* **Input**: Can be fed into indicators via `Update(TSeries)`.
* **Output**: Indicators return `TSeries` from their `Calculate` methods.
* **Visualization**: Easily mappable to charting libraries due to separate Time/Value arrays.
## Architecture Notes
* **CollectionsMarshal**: Uses `CollectionsMarshal.AsSpan` to expose internal list storage as spans without copying. This is unsafe if the list is modified during span access, but provides maximum performance for single-threaded algorithms.
* **Virtual Methods**: `Add` is virtual to allow derived classes (like `TBarSeries` components) to intercept updates if necessary.
## References
* [Data-Oriented Design](https://en.wikipedia.org/wiki/Data-oriented_design)
* [SIMD in .NET](https://learn.microsoft.com/en-us/dotnet/standard/simd)
+2 -2
View File
@@ -54,9 +54,9 @@ namespace QuanTAlib.Tests
public void ImplicitConversion_ToDouble_ReturnsValue()
{
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
double val = tValue;
Assert.Equal(42.0, val);
}
+99 -99
View File
@@ -1,99 +1,99 @@
# TValue: Time-Value Pair
## What It Does
`TValue` is the fundamental atomic unit of data in QuanTAlib. It represents a single point in a time series, consisting of a timestamp and a double-precision floating-point value. It serves as the standard input and output format for all indicators and data streams.
## Design Philosophy
In high-frequency trading and quantitative analysis, memory allocation is a critical bottleneck. `TValue` is designed as a **lightweight, immutable struct** to ensure:
* **Zero Heap Allocation**: Being a struct, it lives on the stack or embedded in arrays, avoiding Garbage Collector (GC) pressure.
* **Thread Safety**: Immutability guarantees safe concurrent access.
* **Minimal Footprint**: Occupies exactly 16 bytes (8 bytes for `long` Time + 8 bytes for `double` Value), fitting efficiently in CPU cache lines.
## How It Works
`TValue` is implemented as a `readonly record struct`. It encapsulates:
* **Time**: A `long` representing ticks (UTC).
* **Value**: A `double` representing the data magnitude.
It supports implicit conversions to `double` (extracting the value) and `DateTime` (extracting the time), making it syntactically fluid to use in calculations.
## Structure
### Definition
```csharp
public readonly record struct TValue(long Time, double Value);
```
### Properties
| Property | Type | Description |
|----------|------|-------------|
| `Time` | `long` | Timestamp in ticks (UTC). |
| `Value` | `double` | The data value. |
| `AsDateTime` | `DateTime` | Helper to view `Time` as a `DateTime` object. |
### Constructors
| Constructor | Description |
|-------------|-------------|
| `new TValue(long time, double value)` | Creates a TValue from raw ticks. |
| `new TValue(DateTime time, double value)` | Creates a TValue from a DateTime object. |
## Usage
### Creating TValues
```csharp
// From DateTime
var t1 = new TValue(DateTime.UtcNow, 100.5);
// From Ticks
var t2 = new TValue(DateTime.UtcNow.Ticks, 100.5);
```
### Implicit Conversions
```csharp
TValue tv = new TValue(DateTime.UtcNow, 42.0);
// Implicitly converts to double
double val = tv; // 42.0
// Implicitly converts to DateTime
DateTime dt = tv; // DateTime object
```
### String Representation
```csharp
Console.WriteLine(tv); // Output: "[2024-01-01 12:00:00, 42.00]"
```
## Performance Profile
* **Memory**: 16 bytes per instance.
* **Allocation**: 0 bytes (Stack allocated).
* **Copying**: Cheap (fits in two 64-bit registers).
## Integration
`TValue` is the primary currency of the library:
* **Indicators**: `Update(TValue input)` accepts it.
* **Series**: `TSeries` stores collections of it.
* **Events**: `ITValuePublisher` broadcasts it.
## Architecture Notes
* **SkipLocalsInit**: The struct is marked with `[SkipLocalsInit]` to suppress zero-initialization of locals, squeezing out nanoseconds in tight loops.
* **AggressiveInlining**: All accessors and operators are inlined to ensure zero abstraction penalty.
## References
* [Structure of Arrays (SoA)](https://en.wikipedia.org/wiki/AOS_and_SOA)
* [C# Struct Performance](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct)
# TValue: Time-Value Pair
## What It Does
`TValue` is the fundamental atomic unit of data in QuanTAlib. It represents a single point in a time series, consisting of a timestamp and a double-precision floating-point value. It serves as the standard input and output format for all indicators and data streams.
## Design Philosophy
In high-frequency trading and quantitative analysis, memory allocation is a critical bottleneck. `TValue` is designed as a **lightweight, immutable struct** to ensure:
* **Zero Heap Allocation**: Being a struct, it lives on the stack or embedded in arrays, avoiding Garbage Collector (GC) pressure.
* **Thread Safety**: Immutability guarantees safe concurrent access.
* **Minimal Footprint**: Occupies exactly 16 bytes (8 bytes for `long` Time + 8 bytes for `double` Value), fitting efficiently in CPU cache lines.
## How It Works
`TValue` is implemented as a `readonly record struct`. It encapsulates:
* **Time**: A `long` representing ticks (UTC).
* **Value**: A `double` representing the data magnitude.
It supports implicit conversions to `double` (extracting the value) and `DateTime` (extracting the time), making it syntactically fluid to use in calculations.
## Structure
### Definition
```csharp
public readonly record struct TValue(long Time, double Value);
```
### Properties
| Property | Type | Description |
|----------|------|-------------|
| `Time` | `long` | Timestamp in ticks (UTC). |
| `Value` | `double` | The data value. |
| `AsDateTime` | `DateTime` | Helper to view `Time` as a `DateTime` object. |
### Constructors
| Constructor | Description |
|-------------|-------------|
| `new TValue(long time, double value)` | Creates a TValue from raw ticks. |
| `new TValue(DateTime time, double value)` | Creates a TValue from a DateTime object. |
## Usage
### Creating TValues
```csharp
// From DateTime
var t1 = new TValue(DateTime.UtcNow, 100.5);
// From Ticks
var t2 = new TValue(DateTime.UtcNow.Ticks, 100.5);
```
### Implicit Conversions
```csharp
TValue tv = new TValue(DateTime.UtcNow, 42.0);
// Implicitly converts to double
double val = tv; // 42.0
// Implicitly converts to DateTime
DateTime dt = tv; // DateTime object
```
### String Representation
```csharp
Console.WriteLine(tv); // Output: "[2024-01-01 12:00:00, 42.00]"
```
## Performance Profile
* **Memory**: 16 bytes per instance.
* **Allocation**: 0 bytes (Stack allocated).
* **Copying**: Cheap (fits in two 64-bit registers).
## Integration
`TValue` is the primary currency of the library:
* **Indicators**: `Update(TValue input)` accepts it.
* **Series**: `TSeries` stores collections of it.
* **Events**: `ITValuePublisher` broadcasts it.
## Architecture Notes
* **SkipLocalsInit**: The struct is marked with `[SkipLocalsInit]` to suppress zero-initialization of locals, squeezing out nanoseconds in tight loops.
* **AggressiveInlining**: All accessors and operators are inlined to ensure zero abstraction penalty.
## References
* [Structure of Arrays (SoA)](https://en.wikipedia.org/wiki/AOS_and_SOA)
* [C# Struct Performance](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct)
+1 -1
View File
@@ -14,7 +14,7 @@ public readonly record struct TValue(long Time, double Value)
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue(DateTime time, double value)
public TValue(DateTime time, double value)
: this(time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks, value)
{
}