updates from mac

This commit is contained in:
Miha Kralj
2025-11-28 13:35:16 -08:00
parent 74b49d2bb4
commit acac3e610c
55 changed files with 126278 additions and 126081 deletions
+35 -35
View File
@@ -1,35 +1,35 @@
namespace QuanTAlib;
/// <summary>
/// Interface for data feeds that provide TBar (OHLCV) data.
/// Implementations include synthetic generators (GBM), API-based feeds (AlphaVantage),
/// file readers (CSV), and real-time streams (WebSocket).
/// </summary>
public interface IFeed
{
/// <summary>
/// Gets the next bar from the feed with full bidirectional control.
/// </summary>
/// <param name="isNew">
/// Input: Request for new bar (true) or update current bar (false).
/// Output: Actual behavior - may differ if feed cannot honor request (e.g., end of data).
/// </param>
/// <returns>The bar (new or updated)</returns>
TBar Next(ref bool isNew);
/// <summary>
/// Gets the next bar from the feed with simple control.
/// </summary>
/// <param name="isNew">Request for new bar (true) or update current bar (false). Defaults to true.</param>
/// <returns>The bar (new or updated)</returns>
TBar Next(bool isNew = true);
/// <summary>
/// Gets multiple bars in batch with explicit time parameters.
/// </summary>
/// <param name="count">Number of bars to retrieve</param>
/// <param name="startTime">Starting timestamp for first bar (in ticks)</param>
/// <param name="interval">Time interval between bars</param>
/// <returns>Series containing the requested bars</returns>
TBarSeries Fetch(int count, long startTime, TimeSpan interval);
}
namespace QuanTAlib;
/// <summary>
/// Interface for data feeds that provide TBar (OHLCV) data.
/// Implementations include synthetic generators (GBM), API-based feeds (AlphaVantage),
/// file readers (CSV), and real-time streams (WebSocket).
/// </summary>
public interface IFeed
{
/// <summary>
/// Gets the next bar from the feed with full bidirectional control.
/// </summary>
/// <param name="isNew">
/// Input: Request for new bar (true) or update current bar (false).
/// Output: Actual behavior - may differ if feed cannot honor request (e.g., end of data).
/// </param>
/// <returns>The bar (new or updated)</returns>
TBar Next(ref bool isNew);
/// <summary>
/// Gets the next bar from the feed with simple control.
/// </summary>
/// <param name="isNew">Request for new bar (true) or update current bar (false). Defaults to true.</param>
/// <returns>The bar (new or updated)</returns>
TBar Next(bool isNew = true);
/// <summary>
/// Gets multiple bars in batch with explicit time parameters.
/// </summary>
/// <param name="count">Number of bars to retrieve</param>
/// <param name="startTime">Starting timestamp for first bar (in ticks)</param>
/// <param name="interval">Time interval between bars</param>
/// <returns>Series containing the requested bars</returns>
TBarSeries Fetch(int count, long startTime, TimeSpan interval);
}
+45 -45
View File
@@ -1,45 +1,45 @@
# IFeed Interface
`IFeed` defines the standard contract for all data feeds in QuanTAlib, ensuring consistent behavior across different data sources (synthetic, file-based, or live API).
## Key Concepts
- **Bidirectional Control**: The `Next(ref bool isNew)` method allows the consumer to request a new bar (`isNew = true`) or an update to the current bar (`isNew = false`).
- **Streaming**: Designed for bar-by-bar processing, simulating real-time data flow.
- **Batching**: Supports fetching historical data ranges via `Fetch()`.
## Interface Definition
```csharp
public interface IFeed
{
/// <summary>
/// Gets the next bar with full control over new/update state.
/// </summary>
TBar Next(ref bool isNew);
/// <summary>
/// Convenience overload for simple next-bar requests.
/// </summary>
TBar Next(bool isNew = true);
/// <summary>
/// Retrieves a batch of historical bars.
/// </summary>
TBarSeries Fetch(int count, long startTime, TimeSpan interval);
}
```
## Implementation Guidelines
When implementing `IFeed`:
1. **State Management**: Maintain the current position in the data source.
2. **End of Data**: When data is exhausted, `Next` should return the last valid bar and set `isNew` to `false`.
3. **Intra-bar Updates**: If the source supports it (e.g., live ticks), `Next(isNew: false)` should return the updated state of the current bar. If not supported (e.g., CSV), it should return the current bar unchanged.
4. **Thread Safety**: Implementations are generally not required to be thread-safe unless specified.
## Implementations
- **`GBM`**: Geometric Brownian Motion generator (Synthetic).
- **`CsvFeed`**: Reads OHLCV data from CSV files (Historical).
# IFeed Interface
`IFeed` defines the standard contract for all data feeds in QuanTAlib, ensuring consistent behavior across different data sources (synthetic, file-based, or live API).
## Key Concepts
- **Bidirectional Control**: The `Next(ref bool isNew)` method allows the consumer to request a new bar (`isNew = true`) or an update to the current bar (`isNew = false`).
- **Streaming**: Designed for bar-by-bar processing, simulating real-time data flow.
- **Batching**: Supports fetching historical data ranges via `Fetch()`.
## Interface Definition
```csharp
public interface IFeed
{
/// <summary>
/// Gets the next bar with full control over new/update state.
/// </summary>
TBar Next(ref bool isNew);
/// <summary>
/// Convenience overload for simple next-bar requests.
/// </summary>
TBar Next(bool isNew = true);
/// <summary>
/// Retrieves a batch of historical bars.
/// </summary>
TBarSeries Fetch(int count, long startTime, TimeSpan interval);
}
```
## Implementation Guidelines
When implementing `IFeed`:
1. **State Management**: Maintain the current position in the data source.
2. **End of Data**: When data is exhausted, `Next` should return the last valid bar and set `isNew` to `false`.
3. **Intra-bar Updates**: If the source supports it (e.g., live ticks), `Next(isNew: false)` should return the updated state of the current bar. If not supported (e.g., CSV), it should return the current bar unchanged.
4. **Thread Safety**: Implementations are generally not required to be thread-safe unless specified.
## Implementations
- **`GBM`**: Geometric Brownian Motion generator (Synthetic).
- **`CsvFeed`**: Reads OHLCV data from CSV files (Historical).
+254 -254
View File
@@ -1,254 +1,254 @@
using Xunit;
namespace QuanTAlib.Tests;
public class CsvFeedTests
{
private const string TestCsvPath = "daily_IBM.csv";
[Fact]
public void Constructor_ValidFile_LoadsData()
{
var feed = new CsvFeed(TestCsvPath);
Assert.NotNull(feed);
}
[Fact]
public void Constructor_NonExistentFile_ThrowsFileNotFoundException()
{
Assert.Throws<FileNotFoundException>(() => new CsvFeed("nonexistent.csv"));
}
[Fact]
public void Constructor_NullPath_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new CsvFeed(null!));
}
[Fact]
public void Constructor_EmptyPath_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new CsvFeed(""));
}
[Fact]
public void Next_StreamsDataChronologically()
{
var feed = new CsvFeed(TestCsvPath);
// Get first bar
var bar1 = feed.Next(isNew: true);
Assert.True(bar1.Time > 0);
// Get second bar - should be later in time
var bar2 = feed.Next(isNew: true);
Assert.True(bar2.Time > bar1.Time);
// Get third bar
var bar3 = feed.Next(isNew: true);
Assert.True(bar3.Time > bar2.Time);
}
[Fact]
public void Next_WithRefParameter_StreamsCorrectly()
{
var feed = new CsvFeed(TestCsvPath);
bool isNew = true;
var bar1 = feed.Next(ref isNew);
Assert.True(isNew); // Should still be true
Assert.True(bar1.Time > 0);
isNew = true;
var bar2 = feed.Next(ref isNew);
Assert.True(isNew);
Assert.True(bar2.Time > bar1.Time);
}
[Fact]
public void Next_UpdateCurrentBar_ReturnsSameBar()
{
var feed = new CsvFeed(TestCsvPath);
// Get first bar
var bar1 = feed.Next(isNew: true);
// Update current bar (should return same bar)
var bar2 = feed.Next(isNew: false);
Assert.Equal(bar1.Time, bar2.Time);
Assert.Equal(bar1.Close, bar2.Close);
// Get next bar
var bar3 = feed.Next(isNew: true);
Assert.True(bar3.Time > bar1.Time);
}
[Fact]
public void Next_EndOfData_SignalsNoMoreData()
{
var feed = new CsvFeed(TestCsvPath);
// Stream through all data
TBar lastBar = default;
bool isNew = true;
int count = 0;
while (isNew && count < 200) // Safety limit
{
lastBar = feed.Next(ref isNew);
count++;
}
// Should have reached end and isNew should be false
Assert.False(isNew);
Assert.True(lastBar.Time > 0);
// Calling again should return same bar with isNew=false
isNew = true;
var finalBar = feed.Next(ref isNew);
Assert.False(isNew);
Assert.Equal(lastBar.Time, finalBar.Time);
}
[Fact]
public void Fetch_ReturnsCorrectNumberOfBars()
{
var feed = new CsvFeed(TestCsvPath);
var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
var interval = TimeSpan.FromDays(1);
var series = feed.Fetch(10, startTime, interval);
Assert.True(series.Count > 0);
Assert.True(series.Count <= 10);
}
[Fact]
public void Fetch_InvalidCount_ThrowsArgumentException()
{
var feed = new CsvFeed(TestCsvPath);
var startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromDays(1);
Assert.Throws<ArgumentException>(() => feed.Fetch(0, startTime, interval));
Assert.Throws<ArgumentException>(() => feed.Fetch(-1, startTime, interval));
}
[Fact]
public void Fetch_ResetsStreamingPosition()
{
var feed = new CsvFeed(TestCsvPath);
// Stream a few bars
feed.Next(isNew: true);
feed.Next(isNew: true);
feed.Next(isNew: true);
// Fetch from start
var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
// Next should now stream from fetched position
var bar = feed.Next(isNew: true);
Assert.True(bar.Time >= startTime);
}
[Fact]
public void LoadFromCsv_ParsesValuesCorrectly()
{
var feed = new CsvFeed(TestCsvPath);
// Get first bar (oldest in chronological order)
var bar = feed.Next(isNew: true);
// Verify it has valid OHLCV data
Assert.True(bar.Open > 0);
Assert.True(bar.High >= bar.Open);
Assert.True(bar.High >= bar.Close);
Assert.True(bar.Low <= bar.Open);
Assert.True(bar.Low <= bar.Close);
Assert.True(bar.Close > 0);
Assert.True(bar.Volume > 0);
}
[Fact]
public void LoadFromCsv_DataInChronologicalOrder()
{
var feed = new CsvFeed(TestCsvPath);
var bars = new List<TBar>();
bool isNew = true;
// Collect first 10 bars
for (int i = 0; i < 10 && isNew; i++)
{
bars.Add(feed.Next(ref isNew));
}
// Verify chronological order (each bar later than previous)
for (int i = 1; i < bars.Count; i++)
{
Assert.True(bars[i].Time > bars[i - 1].Time,
$"Bar {i} time ({bars[i].AsDateTime}) should be after bar {i-1} time ({bars[i-1].AsDateTime})");
}
}
[Fact]
public void CsvFeed_WorksWithIFeedInterface()
{
IFeed feed = new CsvFeed(TestCsvPath);
var bar1 = feed.Next(isNew: true);
Assert.True(bar1.Time > 0);
var bar2 = feed.Next(isNew: true);
Assert.True(bar2.Time > bar1.Time);
}
[Fact]
public void Next_MixedNewAndUpdate_WorksCorrectly()
{
var feed = new CsvFeed(TestCsvPath);
var bar1 = feed.Next(isNew: true);
var bar1Update = feed.Next(isNew: false);
Assert.Equal(bar1.Time, bar1Update.Time);
var bar2 = feed.Next(isNew: true);
Assert.True(bar2.Time > bar1.Time);
var bar2Update = feed.Next(isNew: false);
Assert.Equal(bar2.Time, bar2Update.Time);
var bar3 = feed.Next(isNew: true);
Assert.True(bar3.Time > bar2.Time);
}
[Fact]
public void Fetch_WithEarlyStartTime_ReturnsData()
{
var feed = new CsvFeed(TestCsvPath);
// Start from very early date (before any data)
var startTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
// Should return data starting from first available bar
Assert.True(series.Count > 0);
}
[Fact]
public void Fetch_WithFutureStartTime_ReturnsEmpty()
{
var feed = new CsvFeed(TestCsvPath);
// Start from future date (after all data)
var startTime = new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
// Should return empty or minimal data
Assert.True(series.Count == 0);
}
}
using Xunit;
namespace QuanTAlib.Tests;
public class CsvFeedTests
{
private const string TestCsvPath = "daily_IBM.csv";
[Fact]
public void Constructor_ValidFile_LoadsData()
{
var feed = new CsvFeed(TestCsvPath);
Assert.NotNull(feed);
}
[Fact]
public void Constructor_NonExistentFile_ThrowsFileNotFoundException()
{
Assert.Throws<FileNotFoundException>(() => new CsvFeed("nonexistent.csv"));
}
[Fact]
public void Constructor_NullPath_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new CsvFeed(null!));
}
[Fact]
public void Constructor_EmptyPath_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new CsvFeed(""));
}
[Fact]
public void Next_StreamsDataChronologically()
{
var feed = new CsvFeed(TestCsvPath);
// Get first bar
var bar1 = feed.Next(isNew: true);
Assert.True(bar1.Time > 0);
// Get second bar - should be later in time
var bar2 = feed.Next(isNew: true);
Assert.True(bar2.Time > bar1.Time);
// Get third bar
var bar3 = feed.Next(isNew: true);
Assert.True(bar3.Time > bar2.Time);
}
[Fact]
public void Next_WithRefParameter_StreamsCorrectly()
{
var feed = new CsvFeed(TestCsvPath);
bool isNew = true;
var bar1 = feed.Next(ref isNew);
Assert.True(isNew); // Should still be true
Assert.True(bar1.Time > 0);
isNew = true;
var bar2 = feed.Next(ref isNew);
Assert.True(isNew);
Assert.True(bar2.Time > bar1.Time);
}
[Fact]
public void Next_UpdateCurrentBar_ReturnsSameBar()
{
var feed = new CsvFeed(TestCsvPath);
// Get first bar
var bar1 = feed.Next(isNew: true);
// Update current bar (should return same bar)
var bar2 = feed.Next(isNew: false);
Assert.Equal(bar1.Time, bar2.Time);
Assert.Equal(bar1.Close, bar2.Close);
// Get next bar
var bar3 = feed.Next(isNew: true);
Assert.True(bar3.Time > bar1.Time);
}
[Fact]
public void Next_EndOfData_SignalsNoMoreData()
{
var feed = new CsvFeed(TestCsvPath);
// Stream through all data
TBar lastBar = default;
bool isNew = true;
int count = 0;
while (isNew && count < 200) // Safety limit
{
lastBar = feed.Next(ref isNew);
count++;
}
// Should have reached end and isNew should be false
Assert.False(isNew);
Assert.True(lastBar.Time > 0);
// Calling again should return same bar with isNew=false
isNew = true;
var finalBar = feed.Next(ref isNew);
Assert.False(isNew);
Assert.Equal(lastBar.Time, finalBar.Time);
}
[Fact]
public void Fetch_ReturnsCorrectNumberOfBars()
{
var feed = new CsvFeed(TestCsvPath);
var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
var interval = TimeSpan.FromDays(1);
var series = feed.Fetch(10, startTime, interval);
Assert.True(series.Count > 0);
Assert.True(series.Count <= 10);
}
[Fact]
public void Fetch_InvalidCount_ThrowsArgumentException()
{
var feed = new CsvFeed(TestCsvPath);
var startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromDays(1);
Assert.Throws<ArgumentException>(() => feed.Fetch(0, startTime, interval));
Assert.Throws<ArgumentException>(() => feed.Fetch(-1, startTime, interval));
}
[Fact]
public void Fetch_ResetsStreamingPosition()
{
var feed = new CsvFeed(TestCsvPath);
// Stream a few bars
feed.Next(isNew: true);
feed.Next(isNew: true);
feed.Next(isNew: true);
// Fetch from start
var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
// Next should now stream from fetched position
var bar = feed.Next(isNew: true);
Assert.True(bar.Time >= startTime);
}
[Fact]
public void LoadFromCsv_ParsesValuesCorrectly()
{
var feed = new CsvFeed(TestCsvPath);
// Get first bar (oldest in chronological order)
var bar = feed.Next(isNew: true);
// Verify it has valid OHLCV data
Assert.True(bar.Open > 0);
Assert.True(bar.High >= bar.Open);
Assert.True(bar.High >= bar.Close);
Assert.True(bar.Low <= bar.Open);
Assert.True(bar.Low <= bar.Close);
Assert.True(bar.Close > 0);
Assert.True(bar.Volume > 0);
}
[Fact]
public void LoadFromCsv_DataInChronologicalOrder()
{
var feed = new CsvFeed(TestCsvPath);
var bars = new List<TBar>();
bool isNew = true;
// Collect first 10 bars
for (int i = 0; i < 10 && isNew; i++)
{
bars.Add(feed.Next(ref isNew));
}
// Verify chronological order (each bar later than previous)
for (int i = 1; i < bars.Count; i++)
{
Assert.True(bars[i].Time > bars[i - 1].Time,
$"Bar {i} time ({bars[i].AsDateTime}) should be after bar {i-1} time ({bars[i-1].AsDateTime})");
}
}
[Fact]
public void CsvFeed_WorksWithIFeedInterface()
{
IFeed feed = new CsvFeed(TestCsvPath);
var bar1 = feed.Next(isNew: true);
Assert.True(bar1.Time > 0);
var bar2 = feed.Next(isNew: true);
Assert.True(bar2.Time > bar1.Time);
}
[Fact]
public void Next_MixedNewAndUpdate_WorksCorrectly()
{
var feed = new CsvFeed(TestCsvPath);
var bar1 = feed.Next(isNew: true);
var bar1Update = feed.Next(isNew: false);
Assert.Equal(bar1.Time, bar1Update.Time);
var bar2 = feed.Next(isNew: true);
Assert.True(bar2.Time > bar1.Time);
var bar2Update = feed.Next(isNew: false);
Assert.Equal(bar2.Time, bar2Update.Time);
var bar3 = feed.Next(isNew: true);
Assert.True(bar3.Time > bar2.Time);
}
[Fact]
public void Fetch_WithEarlyStartTime_ReturnsData()
{
var feed = new CsvFeed(TestCsvPath);
// Start from very early date (before any data)
var startTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
// Should return data starting from first available bar
Assert.True(series.Count > 0);
}
[Fact]
public void Fetch_WithFutureStartTime_ReturnsEmpty()
{
var feed = new CsvFeed(TestCsvPath);
// Start from future date (after all data)
var startTime = new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
// Should return empty or minimal data
Assert.True(series.Count == 0);
}
}
+196 -196
View File
@@ -1,196 +1,196 @@
using System.Globalization;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// CSV file feed for loading historical OHLCV data.
/// Loads data in constructor and streams through it with Next() or returns batches with Fetch().
/// CSV format: timestamp,open,high,low,close,volume (header required)
/// Timestamp format: YYYY-MM-DD (UTC midnight assumed)
/// </summary>
public class CsvFeed : IFeed
{
private readonly TBarSeries _data;
// Streaming state
private int _currentIndex;
private TBar _currentBar;
private bool _hasCurrentBar;
/// <summary>
/// Loads CSV file and prepares data for streaming.
/// Data is reversed to chronological order (oldest first).
/// </summary>
/// <param name="filePath">Path to CSV file</param>
public CsvFeed(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
if (!File.Exists(filePath))
throw new FileNotFoundException($"CSV file not found: {filePath}", filePath);
_data = LoadFromCsv(filePath);
_currentIndex = 0;
}
/// <summary>
/// Parses CSV file into TBarSeries.
/// Expected format: timestamp,open,high,low,close,volume
/// </summary>
private static TBarSeries LoadFromCsv(string filePath)
{
var lines = File.ReadAllLines(filePath);
if (lines.Length == 0)
throw new InvalidDataException("CSV file is empty");
// Skip header, reverse to chronological order (oldest first)
var dataLines = lines.Skip(1).Reverse().ToArray();
if (dataLines.Length == 0)
throw new InvalidDataException("CSV file contains only header, no data");
var series = new TBarSeries(dataLines.Length);
for (int i = 0; i < dataLines.Length; i++)
{
var line = dataLines[i];
if (string.IsNullOrWhiteSpace(line))
continue;
var parts = line.Split(',');
if (parts.Length != 6)
throw new FormatException($"Invalid CSV format at line {i + 2}. Expected 6 columns, found {parts.Length}");
try
{
// Parse timestamp (YYYY-MM-DD format, assume UTC midnight)
var timestamp = DateTime.ParseExact(parts[0].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
// Parse OHLCV values
double open = double.Parse(parts[1].Trim(), CultureInfo.InvariantCulture);
double high = double.Parse(parts[2].Trim(), CultureInfo.InvariantCulture);
double low = double.Parse(parts[3].Trim(), CultureInfo.InvariantCulture);
double close = double.Parse(parts[4].Trim(), CultureInfo.InvariantCulture);
double volume = double.Parse(parts[5].Trim(), CultureInfo.InvariantCulture);
series.Add(timestamp, open, high, low, close, volume, isNew: true);
}
catch (Exception ex) when (ex is FormatException or OverflowException)
{
throw new FormatException($"Failed to parse CSV line {i + 2}: {line}", ex);
}
}
return series;
}
/// <summary>
/// Gets the next bar with full bidirectional control.
/// When end of data reached, returns last bar and sets isNew=false.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(ref bool isNew)
{
if (_data.Count == 0)
{
isNew = false;
return default;
}
if (isNew || !_hasCurrentBar)
{
// Request for new bar
if (_currentIndex >= _data.Count)
{
// End of data - return last bar and signal no more data
isNew = false;
return _currentBar;
}
_currentBar = _data[_currentIndex];
_currentIndex++;
_hasCurrentBar = true;
}
else
{
// Update current bar - CSV has no intra-bar updates, return same bar
// No change to _currentBar or _currentIndex
}
return _currentBar;
}
/// <summary>
/// Gets the next bar with simple control.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(bool isNew = true)
{
return Next(ref isNew);
}
/// <summary>
/// Returns a filtered subset of data matching the criteria.
/// Resets streaming position to start of returned data.
/// </summary>
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
{
if (count <= 0)
throw new ArgumentException("Count must be positive", nameof(count));
var result = new TBarSeries(count);
// Find starting index
int startIndex = 0;
for (int i = 0; i < _data.Count; i++)
{
if (_data[i].Time >= startTime)
{
startIndex = i;
break;
}
}
// Collect bars matching interval
long expectedTime = startTime;
int collected = 0;
for (int i = startIndex; i < _data.Count && collected < count; i++)
{
var bar = _data[i];
// Check if bar time matches expected time (within tolerance)
long timeDiff = Math.Abs(bar.Time - expectedTime);
long tolerance = interval.Ticks / 2; // Allow 50% tolerance
if (timeDiff <= tolerance)
{
result.Add(bar, isNew: true);
collected++;
expectedTime += interval.Ticks;
}
else if (bar.Time > expectedTime)
{
// Gap in data - skip forward
long gaps = (bar.Time - expectedTime) / interval.Ticks;
expectedTime += (gaps + 1) * interval.Ticks;
if (Math.Abs(bar.Time - expectedTime + interval.Ticks) <= tolerance)
{
result.Add(bar, isNew: true);
collected++;
expectedTime += interval.Ticks;
}
}
}
// Reset streaming to start of returned data
_currentIndex = startIndex;
_hasCurrentBar = false;
return result;
}
}
using System.Globalization;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// CSV file feed for loading historical OHLCV data.
/// Loads data in constructor and streams through it with Next() or returns batches with Fetch().
/// CSV format: timestamp,open,high,low,close,volume (header required)
/// Timestamp format: YYYY-MM-DD (UTC midnight assumed)
/// </summary>
public class CsvFeed : IFeed
{
private readonly TBarSeries _data;
// Streaming state
private int _currentIndex;
private TBar _currentBar;
private bool _hasCurrentBar;
/// <summary>
/// Loads CSV file and prepares data for streaming.
/// Data is reversed to chronological order (oldest first).
/// </summary>
/// <param name="filePath">Path to CSV file</param>
public CsvFeed(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
if (!File.Exists(filePath))
throw new FileNotFoundException($"CSV file not found: {filePath}", filePath);
_data = LoadFromCsv(filePath);
_currentIndex = 0;
}
/// <summary>
/// Parses CSV file into TBarSeries.
/// Expected format: timestamp,open,high,low,close,volume
/// </summary>
private static TBarSeries LoadFromCsv(string filePath)
{
var lines = File.ReadAllLines(filePath);
if (lines.Length == 0)
throw new InvalidDataException("CSV file is empty");
// Skip header, reverse to chronological order (oldest first)
var dataLines = lines.Skip(1).Reverse().ToArray();
if (dataLines.Length == 0)
throw new InvalidDataException("CSV file contains only header, no data");
var series = new TBarSeries(dataLines.Length);
for (int i = 0; i < dataLines.Length; i++)
{
var line = dataLines[i];
if (string.IsNullOrWhiteSpace(line))
continue;
var parts = line.Split(',');
if (parts.Length != 6)
throw new FormatException($"Invalid CSV format at line {i + 2}. Expected 6 columns, found {parts.Length}");
try
{
// Parse timestamp (YYYY-MM-DD format, assume UTC midnight)
var timestamp = DateTime.ParseExact(parts[0].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
// Parse OHLCV values
double open = double.Parse(parts[1].Trim(), CultureInfo.InvariantCulture);
double high = double.Parse(parts[2].Trim(), CultureInfo.InvariantCulture);
double low = double.Parse(parts[3].Trim(), CultureInfo.InvariantCulture);
double close = double.Parse(parts[4].Trim(), CultureInfo.InvariantCulture);
double volume = double.Parse(parts[5].Trim(), CultureInfo.InvariantCulture);
series.Add(timestamp, open, high, low, close, volume, isNew: true);
}
catch (Exception ex) when (ex is FormatException or OverflowException)
{
throw new FormatException($"Failed to parse CSV line {i + 2}: {line}", ex);
}
}
return series;
}
/// <summary>
/// Gets the next bar with full bidirectional control.
/// When end of data reached, returns last bar and sets isNew=false.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(ref bool isNew)
{
if (_data.Count == 0)
{
isNew = false;
return default;
}
if (isNew || !_hasCurrentBar)
{
// Request for new bar
if (_currentIndex >= _data.Count)
{
// End of data - return last bar and signal no more data
isNew = false;
return _currentBar;
}
_currentBar = _data[_currentIndex];
_currentIndex++;
_hasCurrentBar = true;
}
else
{
// Update current bar - CSV has no intra-bar updates, return same bar
// No change to _currentBar or _currentIndex
}
return _currentBar;
}
/// <summary>
/// Gets the next bar with simple control.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(bool isNew = true)
{
return Next(ref isNew);
}
/// <summary>
/// Returns a filtered subset of data matching the criteria.
/// Resets streaming position to start of returned data.
/// </summary>
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
{
if (count <= 0)
throw new ArgumentException("Count must be positive", nameof(count));
var result = new TBarSeries(count);
// Find starting index
int startIndex = 0;
for (int i = 0; i < _data.Count; i++)
{
if (_data[i].Time >= startTime)
{
startIndex = i;
break;
}
}
// Collect bars matching interval
long expectedTime = startTime;
int collected = 0;
for (int i = startIndex; i < _data.Count && collected < count; i++)
{
var bar = _data[i];
// Check if bar time matches expected time (within tolerance)
long timeDiff = Math.Abs(bar.Time - expectedTime);
long tolerance = interval.Ticks / 2; // Allow 50% tolerance
if (timeDiff <= tolerance)
{
result.Add(bar, isNew: true);
collected++;
expectedTime += interval.Ticks;
}
else if (bar.Time > expectedTime)
{
// Gap in data - skip forward
long gaps = (bar.Time - expectedTime) / interval.Ticks;
expectedTime += (gaps + 1) * interval.Ticks;
if (Math.Abs(bar.Time - expectedTime + interval.Ticks) <= tolerance)
{
result.Add(bar, isNew: true);
collected++;
expectedTime += interval.Ticks;
}
}
}
// Reset streaming to start of returned data
_currentIndex = startIndex;
_hasCurrentBar = false;
return result;
}
}
+68 -68
View File
@@ -1,68 +1,68 @@
# CsvFeed Class
`CsvFeed` is a file-based feed implementation that loads historical OHLCV data from CSV files. It supports both streaming access (simulating real-time playback) and batch retrieval.
## Key Features
- **Historical Data Loading**: Reads standard OHLCV CSV files.
- **Chronological Ordering**: Automatically reverses data if needed (assumes newest-first in file, provides oldest-first).
- **Streaming Interface**: Implements `IFeed` for consistent usage with other feed types.
- **Batch Retrieval**: Supports fetching specific time ranges via `Fetch()`.
## CSV Format Requirements
The file must have a header row and follow this column order:
`timestamp, open, high, low, close, volume`
- **Timestamp**: `YYYY-MM-DD` (assumed UTC midnight)
- **Prices/Volume**: Numeric values
Example:
```csv
Date,Open,High,Low,Close,Volume
2024-01-01,100.0,105.0,99.0,102.5,10000
2024-01-02,102.5,103.0,101.0,101.5,8500
```
## Class Definition
```csharp
public class CsvFeed : IFeed
{
public CsvFeed(string filePath);
public TBar Next(bool isNew = true);
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
}
```
## Usage
### 1. Loading Data
```csharp
var feed = new CsvFeed("path/to/data.csv");
```
### 2. Streaming Data (Simulation)
```csharp
// Get first bar
var bar = feed.Next(isNew: true);
// Loop through all data
while (true)
{
// Process bar...
Console.WriteLine(bar);
// Get next bar
bool isNew = true;
bar = feed.Next(ref isNew);
// Stop if no more new data
if (!isNew) break;
}
```
### 3. Fetching a Batch
```csharp
long startTime = new DateTime(2024, 1, 1).Ticks;
var batch = feed.Fetch(10, startTime, TimeSpan.FromDays(1));
# CsvFeed Class
`CsvFeed` is a file-based feed implementation that loads historical OHLCV data from CSV files. It supports both streaming access (simulating real-time playback) and batch retrieval.
## Key Features
- **Historical Data Loading**: Reads standard OHLCV CSV files.
- **Chronological Ordering**: Automatically reverses data if needed (assumes newest-first in file, provides oldest-first).
- **Streaming Interface**: Implements `IFeed` for consistent usage with other feed types.
- **Batch Retrieval**: Supports fetching specific time ranges via `Fetch()`.
## CSV Format Requirements
The file must have a header row and follow this column order:
`timestamp, open, high, low, close, volume`
- **Timestamp**: `YYYY-MM-DD` (assumed UTC midnight)
- **Prices/Volume**: Numeric values
Example:
```csv
Date,Open,High,Low,Close,Volume
2024-01-01,100.0,105.0,99.0,102.5,10000
2024-01-02,102.5,103.0,101.0,101.5,8500
```
## Class Definition
```csharp
public class CsvFeed : IFeed
{
public CsvFeed(string filePath);
public TBar Next(bool isNew = true);
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
}
```
## Usage
### 1. Loading Data
```csharp
var feed = new CsvFeed("path/to/data.csv");
```
### 2. Streaming Data (Simulation)
```csharp
// Get first bar
var bar = feed.Next(isNew: true);
// Loop through all data
while (true)
{
// Process bar...
Console.WriteLine(bar);
// Get next bar
bool isNew = true;
bar = feed.Next(ref isNew);
// Stop if no more new data
if (!isNew) break;
}
```
### 3. Fetching a Batch
```csharp
long startTime = new DateTime(2024, 1, 1).Ticks;
var batch = feed.Fetch(10, startTime, TimeSpan.FromDays(1));
+101 -101
View File
@@ -1,101 +1,101 @@
timestamp,open,high,low,close,volume
2025-11-25,304.1250,306.0000,297.0600,304.4800,2825322
2025-11-24,299.1800,307.1800,297.5100,304.1200,6050640
2025-11-21,293.4800,300.4800,291.8900,297.4400,5710903
2025-11-20,294.6400,300.7100,290.1600,290.4000,5597028
2025-11-19,290.5000,291.1099,288.0700,288.5300,3595912
2025-11-18,297.0000,297.0000,289.9200,289.9500,4861928
2025-11-17,305.5900,306.0000,296.5100,297.1700,3909741
2025-11-14,300.0000,307.7200,297.5900,305.6900,3592455
2025-11-13,312.2900,314.6000,303.6800,304.8600,5310150
2025-11-12,319.8900,324.9000,314.5324,314.9800,6042686
2025-11-11,309.0000,317.9100,308.4300,313.7200,4381913
2025-11-10,306.8200,309.9400,304.2300,309.1300,2975188
2025-11-07,309.6800,310.0000,302.6301,306.3800,5070773
2025-11-06,306.7500,315.4400,301.0900,312.4200,6818521
2025-11-05,301.3800,307.2000,299.7100,306.7700,4633195
2025-11-04,300.0000,303.1700,296.0000,300.8500,5677330
2025-11-03,308.0000,312.1411,304.2300,304.7300,4957958
2025-10-31,312.0000,313.5000,301.6300,307.4100,7697499
2025-10-30,306.6500,313.7500,305.0200,310.0600,4694275
2025-10-29,312.7900,314.3300,307.5200,308.2100,4135948
2025-10-28,312.6000,319.3500,311.4100,312.5700,6044770
2025-10-27,307.8000,313.5000,302.8800,313.0900,9868151
2025-10-24,283.7700,310.7500,282.2100,307.4600,16914243
2025-10-23,264.9500,285.5791,263.5623,285.0000,16676394
2025-10-22,281.9900,289.1700,281.3500,287.5100,10538480
2025-10-21,283.3100,285.3100,281.6000,282.0500,4080981
2025-10-20,281.2500,285.5000,280.9600,283.6500,3494336
2025-10-17,276.1500,283.4000,275.3500,281.2800,5309565
2025-10-16,281.1100,282.5600,275.6000,275.9700,2956923
2025-10-15,278.3800,285.4500,277.0000,280.7500,3346753
2025-10-14,275.5200,277.5300,272.5469,276.1500,3058149
2025-10-13,279.7900,282.4399,274.6400,277.2200,4333836
2025-10-10,288.9700,290.3850,277.5000,277.8200,4508506
2025-10-09,289.8200,290.1300,283.3200,288.2300,4912375
2025-10-08,294.1600,294.2000,286.4730,289.4600,5297030
2025-10-07,295.5500,301.0425,293.2850,293.8700,7190126
2025-10-06,288.6100,291.4500,287.8000,289.4200,2881947
2025-10-03,287.5000,293.3200,287.3000,288.3700,4375082
2025-10-02,285.7900,288.5400,282.7900,286.7200,3814232
2025-10-01,280.2000,286.5900,280.1500,286.4900,4381338
2025-09-30,280.8800,286.0250,280.5200,282.1600,5926924
2025-09-29,286.0000,286.0000,279.6600,279.8000,6022125
2025-09-26,280.5100,288.8500,280.1100,284.3100,9063938
2025-09-25,272.9350,284.2300,271.1480,281.4400,11506192
2025-09-24,272.6200,273.6499,267.3000,267.5300,3159924
2025-09-23,272.7000,273.2962,269.2650,272.2400,5394121
2025-09-22,266.6200,272.3100,266.0000,271.3700,5030540
2025-09-19,266.0500,267.8700,263.6400,266.4000,9858112
2025-09-18,258.8600,265.2300,256.8004,265.0000,4988421
2025-09-17,257.4950,260.9644,257.0100,259.0800,3974785
2025-09-16,256.2600,258.0000,254.4100,257.5200,2719918
2025-09-15,254.0200,259.0500,254.0000,256.2400,4028365
2025-09-12,256.9500,257.2500,252.4250,253.4400,3433300
2025-09-11,257.5600,258.5450,255.6550,257.0100,3576048
2025-09-10,259.6500,260.0800,254.5600,256.8800,5185420
2025-09-09,256.1200,260.6600,254.8800,259.1100,4931105
2025-09-08,248.6300,257.1500,247.0200,256.0900,6940270
2025-09-05,248.2300,249.0300,245.4500,248.5300,3147478
2025-09-04,245.4200,249.2800,242.8500,247.1800,4765087
2025-09-03,240.0200,244.2500,239.4100,244.1000,3156289
2025-09-02,240.9000,241.5500,238.2500,241.5000,3469501
2025-08-29,245.2300,245.4599,241.7200,243.4900,2967558
2025-08-28,245.4300,245.8800,243.3600,245.7300,2820817
2025-08-27,242.8700,245.9600,242.0000,244.8400,3698372
2025-08-26,241.0200,244.9800,240.3800,242.6300,5386582
2025-08-25,242.5650,242.5650,239.4300,239.4300,3513327
2025-08-22,240.7400,243.6800,240.2200,242.0900,3134882
2025-08-21,242.2100,242.5000,238.6500,239.4000,2991902
2025-08-20,242.1100,242.8800,240.3400,242.5500,3240064
2025-08-19,240.0000,242.8300,239.4900,241.2800,3328305
2025-08-18,239.5700,241.4200,239.1158,239.4500,3569594
2025-08-15,237.6100,240.6200,236.7700,239.7200,4344322
2025-08-14,238.2500,239.0000,235.6200,237.1100,4556725
2025-08-13,236.2000,240.8411,236.2000,240.0700,5663562
2025-08-12,236.5300,237.9600,233.3600,234.7700,8800597
2025-08-11,242.2400,243.1500,234.7000,236.3000,9381960
2025-08-08,248.8800,249.4800,241.6500,242.2700,6828390
2025-08-07,252.8100,255.0000,248.8750,250.1600,6251285
2025-08-06,251.5300,254.3200,249.2800,252.2800,3692105
2025-08-05,252.0000,252.8000,248.9950,250.6700,5823016
2025-08-04,251.0500,252.0800,248.1100,251.9800,5280588
2025-08-01,251.4050,251.4791,245.6100,250.0500,9683404
2025-07-31,259.5700,259.9900,252.2200,253.1500,6739092
2025-07-30,261.6000,262.0000,258.9000,260.2600,3718290
2025-07-29,264.3000,265.7999,261.0200,262.4100,4627265
2025-07-28,260.3000,264.0000,259.6100,263.2100,5192516
2025-07-25,260.0200,260.8000,256.3500,259.7200,7758653
2025-07-24,261.2500,262.0486,252.7500,260.5100,22647720
2025-07-23,284.3000,288.0800,281.4400,282.0100,8105906
2025-07-22,284.7400,284.8800,281.2500,281.9600,4824219
2025-07-21,286.2900,287.7300,284.3800,284.7100,3051791
2025-07-18,283.3800,287.1600,282.2200,285.8700,4478165
2025-07-17,281.5000,283.4566,280.9000,282.0000,3337168
2025-07-16,282.7500,283.8700,279.8700,281.9200,2804831
2025-07-15,283.7700,284.1550,280.7301,282.7000,2864106
2025-07-14,282.8300,284.9250,281.7100,283.7900,2857401
2025-07-11,285.0100,287.4300,282.9200,283.5900,3790679
2025-07-10,288.9000,288.9000,282.2100,287.4300,3489068
2025-07-09,291.3900,291.6000,288.6300,290.1400,2971309
2025-07-08,293.1000,295.6100,289.4900,290.4200,2925329
timestamp,open,high,low,close,volume
2025-11-25,304.1250,306.0000,297.0600,304.4800,2825322
2025-11-24,299.1800,307.1800,297.5100,304.1200,6050640
2025-11-21,293.4800,300.4800,291.8900,297.4400,5710903
2025-11-20,294.6400,300.7100,290.1600,290.4000,5597028
2025-11-19,290.5000,291.1099,288.0700,288.5300,3595912
2025-11-18,297.0000,297.0000,289.9200,289.9500,4861928
2025-11-17,305.5900,306.0000,296.5100,297.1700,3909741
2025-11-14,300.0000,307.7200,297.5900,305.6900,3592455
2025-11-13,312.2900,314.6000,303.6800,304.8600,5310150
2025-11-12,319.8900,324.9000,314.5324,314.9800,6042686
2025-11-11,309.0000,317.9100,308.4300,313.7200,4381913
2025-11-10,306.8200,309.9400,304.2300,309.1300,2975188
2025-11-07,309.6800,310.0000,302.6301,306.3800,5070773
2025-11-06,306.7500,315.4400,301.0900,312.4200,6818521
2025-11-05,301.3800,307.2000,299.7100,306.7700,4633195
2025-11-04,300.0000,303.1700,296.0000,300.8500,5677330
2025-11-03,308.0000,312.1411,304.2300,304.7300,4957958
2025-10-31,312.0000,313.5000,301.6300,307.4100,7697499
2025-10-30,306.6500,313.7500,305.0200,310.0600,4694275
2025-10-29,312.7900,314.3300,307.5200,308.2100,4135948
2025-10-28,312.6000,319.3500,311.4100,312.5700,6044770
2025-10-27,307.8000,313.5000,302.8800,313.0900,9868151
2025-10-24,283.7700,310.7500,282.2100,307.4600,16914243
2025-10-23,264.9500,285.5791,263.5623,285.0000,16676394
2025-10-22,281.9900,289.1700,281.3500,287.5100,10538480
2025-10-21,283.3100,285.3100,281.6000,282.0500,4080981
2025-10-20,281.2500,285.5000,280.9600,283.6500,3494336
2025-10-17,276.1500,283.4000,275.3500,281.2800,5309565
2025-10-16,281.1100,282.5600,275.6000,275.9700,2956923
2025-10-15,278.3800,285.4500,277.0000,280.7500,3346753
2025-10-14,275.5200,277.5300,272.5469,276.1500,3058149
2025-10-13,279.7900,282.4399,274.6400,277.2200,4333836
2025-10-10,288.9700,290.3850,277.5000,277.8200,4508506
2025-10-09,289.8200,290.1300,283.3200,288.2300,4912375
2025-10-08,294.1600,294.2000,286.4730,289.4600,5297030
2025-10-07,295.5500,301.0425,293.2850,293.8700,7190126
2025-10-06,288.6100,291.4500,287.8000,289.4200,2881947
2025-10-03,287.5000,293.3200,287.3000,288.3700,4375082
2025-10-02,285.7900,288.5400,282.7900,286.7200,3814232
2025-10-01,280.2000,286.5900,280.1500,286.4900,4381338
2025-09-30,280.8800,286.0250,280.5200,282.1600,5926924
2025-09-29,286.0000,286.0000,279.6600,279.8000,6022125
2025-09-26,280.5100,288.8500,280.1100,284.3100,9063938
2025-09-25,272.9350,284.2300,271.1480,281.4400,11506192
2025-09-24,272.6200,273.6499,267.3000,267.5300,3159924
2025-09-23,272.7000,273.2962,269.2650,272.2400,5394121
2025-09-22,266.6200,272.3100,266.0000,271.3700,5030540
2025-09-19,266.0500,267.8700,263.6400,266.4000,9858112
2025-09-18,258.8600,265.2300,256.8004,265.0000,4988421
2025-09-17,257.4950,260.9644,257.0100,259.0800,3974785
2025-09-16,256.2600,258.0000,254.4100,257.5200,2719918
2025-09-15,254.0200,259.0500,254.0000,256.2400,4028365
2025-09-12,256.9500,257.2500,252.4250,253.4400,3433300
2025-09-11,257.5600,258.5450,255.6550,257.0100,3576048
2025-09-10,259.6500,260.0800,254.5600,256.8800,5185420
2025-09-09,256.1200,260.6600,254.8800,259.1100,4931105
2025-09-08,248.6300,257.1500,247.0200,256.0900,6940270
2025-09-05,248.2300,249.0300,245.4500,248.5300,3147478
2025-09-04,245.4200,249.2800,242.8500,247.1800,4765087
2025-09-03,240.0200,244.2500,239.4100,244.1000,3156289
2025-09-02,240.9000,241.5500,238.2500,241.5000,3469501
2025-08-29,245.2300,245.4599,241.7200,243.4900,2967558
2025-08-28,245.4300,245.8800,243.3600,245.7300,2820817
2025-08-27,242.8700,245.9600,242.0000,244.8400,3698372
2025-08-26,241.0200,244.9800,240.3800,242.6300,5386582
2025-08-25,242.5650,242.5650,239.4300,239.4300,3513327
2025-08-22,240.7400,243.6800,240.2200,242.0900,3134882
2025-08-21,242.2100,242.5000,238.6500,239.4000,2991902
2025-08-20,242.1100,242.8800,240.3400,242.5500,3240064
2025-08-19,240.0000,242.8300,239.4900,241.2800,3328305
2025-08-18,239.5700,241.4200,239.1158,239.4500,3569594
2025-08-15,237.6100,240.6200,236.7700,239.7200,4344322
2025-08-14,238.2500,239.0000,235.6200,237.1100,4556725
2025-08-13,236.2000,240.8411,236.2000,240.0700,5663562
2025-08-12,236.5300,237.9600,233.3600,234.7700,8800597
2025-08-11,242.2400,243.1500,234.7000,236.3000,9381960
2025-08-08,248.8800,249.4800,241.6500,242.2700,6828390
2025-08-07,252.8100,255.0000,248.8750,250.1600,6251285
2025-08-06,251.5300,254.3200,249.2800,252.2800,3692105
2025-08-05,252.0000,252.8000,248.9950,250.6700,5823016
2025-08-04,251.0500,252.0800,248.1100,251.9800,5280588
2025-08-01,251.4050,251.4791,245.6100,250.0500,9683404
2025-07-31,259.5700,259.9900,252.2200,253.1500,6739092
2025-07-30,261.6000,262.0000,258.9000,260.2600,3718290
2025-07-29,264.3000,265.7999,261.0200,262.4100,4627265
2025-07-28,260.3000,264.0000,259.6100,263.2100,5192516
2025-07-25,260.0200,260.8000,256.3500,259.7200,7758653
2025-07-24,261.2500,262.0486,252.7500,260.5100,22647720
2025-07-23,284.3000,288.0800,281.4400,282.0100,8105906
2025-07-22,284.7400,284.8800,281.2500,281.9600,4824219
2025-07-21,286.2900,287.7300,284.3800,284.7100,3051791
2025-07-18,283.3800,287.1600,282.2200,285.8700,4478165
2025-07-17,281.5000,283.4566,280.9000,282.0000,3337168
2025-07-16,282.7500,283.8700,279.8700,281.9200,2804831
2025-07-15,283.7700,284.1550,280.7301,282.7000,2864106
2025-07-14,282.8300,284.9250,281.7100,283.7900,2857401
2025-07-11,285.0100,287.4300,282.9200,283.5900,3790679
2025-07-10,288.9000,288.9000,282.2100,287.4300,3489068
2025-07-09,291.3900,291.6000,288.6300,290.1400,2971309
2025-07-08,293.1000,295.6100,289.4900,290.4200,2925329
1 timestamp open high low close volume
2 2025-11-25 304.1250 306.0000 297.0600 304.4800 2825322
3 2025-11-24 299.1800 307.1800 297.5100 304.1200 6050640
4 2025-11-21 293.4800 300.4800 291.8900 297.4400 5710903
5 2025-11-20 294.6400 300.7100 290.1600 290.4000 5597028
6 2025-11-19 290.5000 291.1099 288.0700 288.5300 3595912
7 2025-11-18 297.0000 297.0000 289.9200 289.9500 4861928
8 2025-11-17 305.5900 306.0000 296.5100 297.1700 3909741
9 2025-11-14 300.0000 307.7200 297.5900 305.6900 3592455
10 2025-11-13 312.2900 314.6000 303.6800 304.8600 5310150
11 2025-11-12 319.8900 324.9000 314.5324 314.9800 6042686
12 2025-11-11 309.0000 317.9100 308.4300 313.7200 4381913
13 2025-11-10 306.8200 309.9400 304.2300 309.1300 2975188
14 2025-11-07 309.6800 310.0000 302.6301 306.3800 5070773
15 2025-11-06 306.7500 315.4400 301.0900 312.4200 6818521
16 2025-11-05 301.3800 307.2000 299.7100 306.7700 4633195
17 2025-11-04 300.0000 303.1700 296.0000 300.8500 5677330
18 2025-11-03 308.0000 312.1411 304.2300 304.7300 4957958
19 2025-10-31 312.0000 313.5000 301.6300 307.4100 7697499
20 2025-10-30 306.6500 313.7500 305.0200 310.0600 4694275
21 2025-10-29 312.7900 314.3300 307.5200 308.2100 4135948
22 2025-10-28 312.6000 319.3500 311.4100 312.5700 6044770
23 2025-10-27 307.8000 313.5000 302.8800 313.0900 9868151
24 2025-10-24 283.7700 310.7500 282.2100 307.4600 16914243
25 2025-10-23 264.9500 285.5791 263.5623 285.0000 16676394
26 2025-10-22 281.9900 289.1700 281.3500 287.5100 10538480
27 2025-10-21 283.3100 285.3100 281.6000 282.0500 4080981
28 2025-10-20 281.2500 285.5000 280.9600 283.6500 3494336
29 2025-10-17 276.1500 283.4000 275.3500 281.2800 5309565
30 2025-10-16 281.1100 282.5600 275.6000 275.9700 2956923
31 2025-10-15 278.3800 285.4500 277.0000 280.7500 3346753
32 2025-10-14 275.5200 277.5300 272.5469 276.1500 3058149
33 2025-10-13 279.7900 282.4399 274.6400 277.2200 4333836
34 2025-10-10 288.9700 290.3850 277.5000 277.8200 4508506
35 2025-10-09 289.8200 290.1300 283.3200 288.2300 4912375
36 2025-10-08 294.1600 294.2000 286.4730 289.4600 5297030
37 2025-10-07 295.5500 301.0425 293.2850 293.8700 7190126
38 2025-10-06 288.6100 291.4500 287.8000 289.4200 2881947
39 2025-10-03 287.5000 293.3200 287.3000 288.3700 4375082
40 2025-10-02 285.7900 288.5400 282.7900 286.7200 3814232
41 2025-10-01 280.2000 286.5900 280.1500 286.4900 4381338
42 2025-09-30 280.8800 286.0250 280.5200 282.1600 5926924
43 2025-09-29 286.0000 286.0000 279.6600 279.8000 6022125
44 2025-09-26 280.5100 288.8500 280.1100 284.3100 9063938
45 2025-09-25 272.9350 284.2300 271.1480 281.4400 11506192
46 2025-09-24 272.6200 273.6499 267.3000 267.5300 3159924
47 2025-09-23 272.7000 273.2962 269.2650 272.2400 5394121
48 2025-09-22 266.6200 272.3100 266.0000 271.3700 5030540
49 2025-09-19 266.0500 267.8700 263.6400 266.4000 9858112
50 2025-09-18 258.8600 265.2300 256.8004 265.0000 4988421
51 2025-09-17 257.4950 260.9644 257.0100 259.0800 3974785
52 2025-09-16 256.2600 258.0000 254.4100 257.5200 2719918
53 2025-09-15 254.0200 259.0500 254.0000 256.2400 4028365
54 2025-09-12 256.9500 257.2500 252.4250 253.4400 3433300
55 2025-09-11 257.5600 258.5450 255.6550 257.0100 3576048
56 2025-09-10 259.6500 260.0800 254.5600 256.8800 5185420
57 2025-09-09 256.1200 260.6600 254.8800 259.1100 4931105
58 2025-09-08 248.6300 257.1500 247.0200 256.0900 6940270
59 2025-09-05 248.2300 249.0300 245.4500 248.5300 3147478
60 2025-09-04 245.4200 249.2800 242.8500 247.1800 4765087
61 2025-09-03 240.0200 244.2500 239.4100 244.1000 3156289
62 2025-09-02 240.9000 241.5500 238.2500 241.5000 3469501
63 2025-08-29 245.2300 245.4599 241.7200 243.4900 2967558
64 2025-08-28 245.4300 245.8800 243.3600 245.7300 2820817
65 2025-08-27 242.8700 245.9600 242.0000 244.8400 3698372
66 2025-08-26 241.0200 244.9800 240.3800 242.6300 5386582
67 2025-08-25 242.5650 242.5650 239.4300 239.4300 3513327
68 2025-08-22 240.7400 243.6800 240.2200 242.0900 3134882
69 2025-08-21 242.2100 242.5000 238.6500 239.4000 2991902
70 2025-08-20 242.1100 242.8800 240.3400 242.5500 3240064
71 2025-08-19 240.0000 242.8300 239.4900 241.2800 3328305
72 2025-08-18 239.5700 241.4200 239.1158 239.4500 3569594
73 2025-08-15 237.6100 240.6200 236.7700 239.7200 4344322
74 2025-08-14 238.2500 239.0000 235.6200 237.1100 4556725
75 2025-08-13 236.2000 240.8411 236.2000 240.0700 5663562
76 2025-08-12 236.5300 237.9600 233.3600 234.7700 8800597
77 2025-08-11 242.2400 243.1500 234.7000 236.3000 9381960
78 2025-08-08 248.8800 249.4800 241.6500 242.2700 6828390
79 2025-08-07 252.8100 255.0000 248.8750 250.1600 6251285
80 2025-08-06 251.5300 254.3200 249.2800 252.2800 3692105
81 2025-08-05 252.0000 252.8000 248.9950 250.6700 5823016
82 2025-08-04 251.0500 252.0800 248.1100 251.9800 5280588
83 2025-08-01 251.4050 251.4791 245.6100 250.0500 9683404
84 2025-07-31 259.5700 259.9900 252.2200 253.1500 6739092
85 2025-07-30 261.6000 262.0000 258.9000 260.2600 3718290
86 2025-07-29 264.3000 265.7999 261.0200 262.4100 4627265
87 2025-07-28 260.3000 264.0000 259.6100 263.2100 5192516
88 2025-07-25 260.0200 260.8000 256.3500 259.7200 7758653
89 2025-07-24 261.2500 262.0486 252.7500 260.5100 22647720
90 2025-07-23 284.3000 288.0800 281.4400 282.0100 8105906
91 2025-07-22 284.7400 284.8800 281.2500 281.9600 4824219
92 2025-07-21 286.2900 287.7300 284.3800 284.7100 3051791
93 2025-07-18 283.3800 287.1600 282.2200 285.8700 4478165
94 2025-07-17 281.5000 283.4566 280.9000 282.0000 3337168
95 2025-07-16 282.7500 283.8700 279.8700 281.9200 2804831
96 2025-07-15 283.7700 284.1550 280.7301 282.7000 2864106
97 2025-07-14 282.8300 284.9250 281.7100 283.7900 2857401
98 2025-07-11 285.0100 287.4300 282.9200 283.5900 3790679
99 2025-07-10 288.9000 288.9000 282.2100 287.4300 3489068
100 2025-07-09 291.3900 291.6000 288.6300 290.1400 2971309
101 2025-07-08 293.1000 295.6100 289.4900 290.4200 2925329
+67 -67
View File
@@ -1,67 +1,67 @@
# GBM Class
`GBM` (Geometric Brownian Motion) is a synthetic data generator that simulates realistic financial price movements. It is useful for testing indicators, strategies, and system performance without relying on external data files.
## Key Features
- **Geometric Brownian Motion**: Uses the standard mathematical model for asset price dynamics.
- **Configurable Parameters**: Control drift (trend) and volatility (noise).
- **Stateless Design**: Minimal memory footprint; only maintains state needed for continuity.
- **Dual Modes**: Supports both streaming (bar-by-bar) and batch generation.
- **Intra-bar Updates**: Can simulate real-time price updates within a single bar.
## Mathematical Model
The price evolution follows the stochastic differential equation:
$$ dS_t = \mu S_t dt + \sigma S_t dW_t $$
Where:
- $S_t$: Asset price at time $t$
- $\mu$: Drift (expected return)
- $\sigma$: Volatility (standard deviation of returns)
- $W_t$: Wiener process (Brownian motion)
## Class Definition
```csharp
public class GBM : IFeed
{
public GBM(double startPrice = 100.0, double mu = 0.05, double sigma = 0.2, TimeSpan? defaultTimeframe = null);
public TBar Next(bool isNew = true);
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
}
```
## Usage
### 1. Initialization
```csharp
// Default: Start at 100, 5% drift, 20% volatility
var gbm = new GBM();
// Custom: Start at 50, 10% drift, 50% volatility
var volatileGbm = new GBM(startPrice: 50.0, mu: 0.10, sigma: 0.50);
```
### 2. Streaming Generation
```csharp
// Generate a new bar
var bar = gbm.Next(isNew: true);
// Simulate intra-bar updates (e.g., real-time ticks)
for (int i = 0; i < 5; i++)
{
var updatedBar = gbm.Next(isNew: false);
Console.WriteLine($"Update: {updatedBar.Close}");
}
```
### 3. Batch Generation
```csharp
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
// Generate 1000 bars
var history = gbm.Fetch(1000, startTime, interval);
# GBM Class
`GBM` (Geometric Brownian Motion) is a synthetic data generator that simulates realistic financial price movements. It is useful for testing indicators, strategies, and system performance without relying on external data files.
## Key Features
- **Geometric Brownian Motion**: Uses the standard mathematical model for asset price dynamics.
- **Configurable Parameters**: Control drift (trend) and volatility (noise).
- **Stateless Design**: Minimal memory footprint; only maintains state needed for continuity.
- **Dual Modes**: Supports both streaming (bar-by-bar) and batch generation.
- **Intra-bar Updates**: Can simulate real-time price updates within a single bar.
## Mathematical Model
The price evolution follows the stochastic differential equation:
$$ dS_t = \mu S_t dt + \sigma S_t dW_t $$
Where:
- $S_t$: Asset price at time $t$
- $\mu$: Drift (expected return)
- $\sigma$: Volatility (standard deviation of returns)
- $W_t$: Wiener process (Brownian motion)
## Class Definition
```csharp
public class GBM : IFeed
{
public GBM(double startPrice = 100.0, double mu = 0.05, double sigma = 0.2, TimeSpan? defaultTimeframe = null);
public TBar Next(bool isNew = true);
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
}
```
## Usage
### 1. Initialization
```csharp
// Default: Start at 100, 5% drift, 20% volatility
var gbm = new GBM();
// Custom: Start at 50, 10% drift, 50% volatility
var volatileGbm = new GBM(startPrice: 50.0, mu: 0.10, sigma: 0.50);
```
### 2. Streaming Generation
```csharp
// Generate a new bar
var bar = gbm.Next(isNew: true);
// Simulate intra-bar updates (e.g., real-time ticks)
for (int i = 0; i < 5; i++)
{
var updatedBar = gbm.Next(isNew: false);
Console.WriteLine($"Update: {updatedBar.Close}");
}
```
### 3. Batch Generation
```csharp
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
// Generate 1000 bars
var history = gbm.Fetch(1000, startTime, interval);
+283 -283
View File
@@ -1,283 +1,283 @@
using System;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class GBMTests
{
[Fact]
public void Next_DefaultParameter_GeneratesNewBar()
{
var gbm = new GBM(startPrice: 100.0);
var bar1 = gbm.Next();
var bar2 = gbm.Next();
Assert.NotEqual(bar1.Time, bar2.Time);
Assert.True(bar2.Time > bar1.Time);
}
[Fact]
public void Next_IsNewTrue_AdvancesToNewBar()
{
var gbm = new GBM(startPrice: 100.0);
var bar1 = gbm.Next(isNew: true);
var bar2 = gbm.Next(isNew: true);
Assert.NotEqual(bar1.Time, bar2.Time);
Assert.True(bar2.Time > bar1.Time);
}
[Fact]
public void Next_IsNewFalse_UpdatesCurrentBar()
{
var gbm = new GBM(startPrice: 100.0);
var bar1 = gbm.Next(isNew: true);
long initialTime = bar1.Time;
var bar2 = gbm.Next(isNew: false);
Assert.Equal(initialTime, bar2.Time);
// Price likely changed (GBM random walk)
Assert.NotEqual(bar1.Close, bar2.Close);
}
[Fact]
public void Next_RefBool_HonorsRequest()
{
var gbm = new GBM(startPrice: 100.0);
// GBM always honors isNew - parameter should remain unchanged
bool isNew1 = true;
var bar1 = gbm.Next(ref isNew1);
Assert.True(isNew1, "GBM should honor isNew=true request");
bool isNew2 = false;
long time1 = bar1.Time;
var bar2 = gbm.Next(ref isNew2);
Assert.False(isNew2, "GBM should honor isNew=false request");
Assert.Equal(time1, bar2.Time);
bool isNew3 = true;
var bar3 = gbm.Next(ref isNew3);
Assert.True(isNew3, "GBM should honor isNew=true request");
Assert.NotEqual(time1, bar3.Time);
}
[Fact]
public void Fetch_GeneratesCorrectCount()
{
var gbm = new GBM(startPrice: 100.0);
int count = 10;
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(count, startTime, interval);
Assert.Equal(count, series.Count);
}
[Fact]
public void Fetch_GeneratesSequentialBars()
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(5, startTime, interval);
// Verify time sequence
for (int i = 1; i < series.Count; i++)
{
Assert.True(series[i].Time > series[i - 1].Time);
}
}
[Fact]
public void Fetch_RespectsInterval()
{
var gbm = new GBM(startPrice: 100.0);
var interval = TimeSpan.FromHours(1);
long startTime = DateTime.UtcNow.Ticks;
var series = gbm.Fetch(5, startTime, interval);
// Verify interval spacing
for (int i = 1; i < series.Count; i++)
{
long expectedDiff = interval.Ticks;
long actualDiff = series[i].Time - series[i - 1].Time;
Assert.Equal(expectedDiff, actualDiff);
}
}
[Fact]
public void Fetch_StartsAtSpecifiedTime()
{
var gbm = new GBM(startPrice: 100.0);
var startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
var interval = TimeSpan.FromMinutes(5);
var series = gbm.Fetch(3, startTime, interval);
Assert.Equal(startTime, series[0].Time);
Assert.Equal(startTime + interval.Ticks, series[1].Time);
Assert.Equal(startTime + 2 * interval.Ticks, series[2].Time);
}
[Fact]
public void Fetch_WithDifferentIntervals_WorksCorrectly()
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
// Test different intervals
var intervals = new[] {
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(5),
TimeSpan.FromHours(1)
};
foreach (var interval in intervals)
{
var series = gbm.Fetch(3, startTime, interval);
// Verify spacing
for (int i = 1; i < series.Count; i++)
{
long expectedDiff = interval.Ticks;
long actualDiff = series[i].Time - series[i - 1].Time;
Assert.Equal(expectedDiff, actualDiff);
}
}
}
[Fact]
public void GeneratesRealisticOHLCV()
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(10, startTime, interval);
for (int i = 0; i < series.Count; i++)
{
var bar = series[i];
// High should be >= max(Open, Close)
Assert.True(bar.High >= Math.Max(bar.Open, bar.Close));
// Low should be <= min(Open, Close)
Assert.True(bar.Low <= Math.Min(bar.Open, bar.Close));
// Volume should be positive
Assert.True(bar.Volume > 0);
// All prices should be positive
Assert.True(bar.Open > 0);
Assert.True(bar.High > 0);
Assert.True(bar.Low > 0);
Assert.True(bar.Close > 0);
}
}
[Fact]
public void IntraBarUpdates_ModifyCurrentBar()
{
var gbm = new GBM(startPrice: 100.0);
var bar1 = gbm.Next(isNew: true);
long initialTime = bar1.Time;
double initialClose = bar1.Close;
// Loop until price changes (random walk might stay same but unlikely)
bool changed = false;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: false);
Assert.Equal(initialTime, bar.Time);
if (Math.Abs(bar.Close - initialClose) > double.Epsilon)
{
changed = true;
break;
}
}
Assert.True(changed, "Price should change during intra-bar updates");
}
[Fact]
public void MixedStreamingAndBatch_WorksCorrectly()
{
var gbm = new GBM(startPrice: 100.0);
// Start with streaming
var bar1 = gbm.Next();
var bar2 = gbm.Next();
// Batch generation with explicit time
long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(3, startTime, interval);
Assert.True(series[0].Time > bar2.Time);
Assert.Equal(3, series.Count);
// Continue streaming after batch (uses internal state)
var bar3 = gbm.Next();
Assert.True(bar3.Time > series[2].Time);
}
[Fact]
public void DriftAndVolatility_AffectPriceMovement()
{
// High volatility should produce more price variation
var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.01);
var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var seriesLow = gbmLowVol.Fetch(100, startTime, interval);
var seriesHigh = gbmHighVol.Fetch(100, startTime, interval);
// Calculate price ranges
double rangeLow = seriesLow[99].Close - seriesLow[0].Open;
double rangeHigh = seriesHigh[99].Close - seriesHigh[0].Open;
// High volatility should generally produce larger absolute movements
Assert.True(Math.Abs(rangeHigh) > Math.Abs(rangeLow) * 0.5);
}
[Fact]
public void ConsecutiveCalls_MaintainContinuity()
{
var gbm = new GBM(startPrice: 100.0);
var bar1 = gbm.Next();
var bar2 = gbm.Next();
// bar2.Open should equal bar1.Close (continuity)
Assert.Equal(bar1.Close, bar2.Open);
}
[Fact]
public void Stateless_NoHistoryStorage()
{
var gbm = new GBM(startPrice: 100.0);
// Generate multiple bars
for (int i = 0; i < 100; i++)
{
gbm.Next();
}
// GBM should not expose any history storage
var type = gbm.GetType();
var barsProperty = type.GetProperty("Bars");
Assert.Null(barsProperty);
}
}
using System;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class GBMTests
{
[Fact]
public void Next_DefaultParameter_GeneratesNewBar()
{
var gbm = new GBM(startPrice: 100.0);
var bar1 = gbm.Next();
var bar2 = gbm.Next();
Assert.NotEqual(bar1.Time, bar2.Time);
Assert.True(bar2.Time > bar1.Time);
}
[Fact]
public void Next_IsNewTrue_AdvancesToNewBar()
{
var gbm = new GBM(startPrice: 100.0);
var bar1 = gbm.Next(isNew: true);
var bar2 = gbm.Next(isNew: true);
Assert.NotEqual(bar1.Time, bar2.Time);
Assert.True(bar2.Time > bar1.Time);
}
[Fact]
public void Next_IsNewFalse_UpdatesCurrentBar()
{
var gbm = new GBM(startPrice: 100.0);
var bar1 = gbm.Next(isNew: true);
long initialTime = bar1.Time;
var bar2 = gbm.Next(isNew: false);
Assert.Equal(initialTime, bar2.Time);
// Price likely changed (GBM random walk)
Assert.NotEqual(bar1.Close, bar2.Close);
}
[Fact]
public void Next_RefBool_HonorsRequest()
{
var gbm = new GBM(startPrice: 100.0);
// GBM always honors isNew - parameter should remain unchanged
bool isNew1 = true;
var bar1 = gbm.Next(ref isNew1);
Assert.True(isNew1, "GBM should honor isNew=true request");
bool isNew2 = false;
long time1 = bar1.Time;
var bar2 = gbm.Next(ref isNew2);
Assert.False(isNew2, "GBM should honor isNew=false request");
Assert.Equal(time1, bar2.Time);
bool isNew3 = true;
var bar3 = gbm.Next(ref isNew3);
Assert.True(isNew3, "GBM should honor isNew=true request");
Assert.NotEqual(time1, bar3.Time);
}
[Fact]
public void Fetch_GeneratesCorrectCount()
{
var gbm = new GBM(startPrice: 100.0);
int count = 10;
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(count, startTime, interval);
Assert.Equal(count, series.Count);
}
[Fact]
public void Fetch_GeneratesSequentialBars()
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(5, startTime, interval);
// Verify time sequence
for (int i = 1; i < series.Count; i++)
{
Assert.True(series[i].Time > series[i - 1].Time);
}
}
[Fact]
public void Fetch_RespectsInterval()
{
var gbm = new GBM(startPrice: 100.0);
var interval = TimeSpan.FromHours(1);
long startTime = DateTime.UtcNow.Ticks;
var series = gbm.Fetch(5, startTime, interval);
// Verify interval spacing
for (int i = 1; i < series.Count; i++)
{
long expectedDiff = interval.Ticks;
long actualDiff = series[i].Time - series[i - 1].Time;
Assert.Equal(expectedDiff, actualDiff);
}
}
[Fact]
public void Fetch_StartsAtSpecifiedTime()
{
var gbm = new GBM(startPrice: 100.0);
var startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
var interval = TimeSpan.FromMinutes(5);
var series = gbm.Fetch(3, startTime, interval);
Assert.Equal(startTime, series[0].Time);
Assert.Equal(startTime + interval.Ticks, series[1].Time);
Assert.Equal(startTime + 2 * interval.Ticks, series[2].Time);
}
[Fact]
public void Fetch_WithDifferentIntervals_WorksCorrectly()
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
// Test different intervals
var intervals = new[] {
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(5),
TimeSpan.FromHours(1)
};
foreach (var interval in intervals)
{
var series = gbm.Fetch(3, startTime, interval);
// Verify spacing
for (int i = 1; i < series.Count; i++)
{
long expectedDiff = interval.Ticks;
long actualDiff = series[i].Time - series[i - 1].Time;
Assert.Equal(expectedDiff, actualDiff);
}
}
}
[Fact]
public void GeneratesRealisticOHLCV()
{
var gbm = new GBM(startPrice: 100.0);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(10, startTime, interval);
for (int i = 0; i < series.Count; i++)
{
var bar = series[i];
// High should be >= max(Open, Close)
Assert.True(bar.High >= Math.Max(bar.Open, bar.Close));
// Low should be <= min(Open, Close)
Assert.True(bar.Low <= Math.Min(bar.Open, bar.Close));
// Volume should be positive
Assert.True(bar.Volume > 0);
// All prices should be positive
Assert.True(bar.Open > 0);
Assert.True(bar.High > 0);
Assert.True(bar.Low > 0);
Assert.True(bar.Close > 0);
}
}
[Fact]
public void IntraBarUpdates_ModifyCurrentBar()
{
var gbm = new GBM(startPrice: 100.0);
var bar1 = gbm.Next(isNew: true);
long initialTime = bar1.Time;
double initialClose = bar1.Close;
// Loop until price changes (random walk might stay same but unlikely)
bool changed = false;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: false);
Assert.Equal(initialTime, bar.Time);
if (Math.Abs(bar.Close - initialClose) > double.Epsilon)
{
changed = true;
break;
}
}
Assert.True(changed, "Price should change during intra-bar updates");
}
[Fact]
public void MixedStreamingAndBatch_WorksCorrectly()
{
var gbm = new GBM(startPrice: 100.0);
// Start with streaming
var bar1 = gbm.Next();
var bar2 = gbm.Next();
// Batch generation with explicit time
long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
var interval = TimeSpan.FromMinutes(1);
var series = gbm.Fetch(3, startTime, interval);
Assert.True(series[0].Time > bar2.Time);
Assert.Equal(3, series.Count);
// Continue streaming after batch (uses internal state)
var bar3 = gbm.Next();
Assert.True(bar3.Time > series[2].Time);
}
[Fact]
public void DriftAndVolatility_AffectPriceMovement()
{
// High volatility should produce more price variation
var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.01);
var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5);
long startTime = DateTime.UtcNow.Ticks;
var interval = TimeSpan.FromMinutes(1);
var seriesLow = gbmLowVol.Fetch(100, startTime, interval);
var seriesHigh = gbmHighVol.Fetch(100, startTime, interval);
// Calculate price ranges
double rangeLow = seriesLow[99].Close - seriesLow[0].Open;
double rangeHigh = seriesHigh[99].Close - seriesHigh[0].Open;
// High volatility should generally produce larger absolute movements
Assert.True(Math.Abs(rangeHigh) > Math.Abs(rangeLow) * 0.5);
}
[Fact]
public void ConsecutiveCalls_MaintainContinuity()
{
var gbm = new GBM(startPrice: 100.0);
var bar1 = gbm.Next();
var bar2 = gbm.Next();
// bar2.Open should equal bar1.Close (continuity)
Assert.Equal(bar1.Close, bar2.Open);
}
[Fact]
public void Stateless_NoHistoryStorage()
{
var gbm = new GBM(startPrice: 100.0);
// Generate multiple bars
for (int i = 0; i < 100; i++)
{
gbm.Next();
}
// GBM should not expose any history storage
var type = gbm.GetType();
var barsProperty = type.GetProperty("Bars");
Assert.Null(barsProperty);
}
}
+211 -211
View File
@@ -1,211 +1,211 @@
using System;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// Geometric Brownian Motion (GBM) generator for simulating OHLCV data.
/// Generates realistic price data for testing indicators and strategies.
/// Stateless design - only maintains minimal state needed for price continuity.
/// </summary>
public class GBM : IFeed
{
private readonly Random _rnd = new();
private double _lastPrice;
private long _lastTime;
private readonly double _mu;
private readonly double _sigma;
private readonly double _dt;
// Precomputed GBM constants
private readonly double _drift;
private readonly double _vol;
private readonly long _defaultTimeStep;
// State for streaming bar formation (only when isNew=false)
private TBar _currentBar;
private bool _hasCurrentBar;
// Box-Muller optimization: cache second normal
private double _cachedZ;
private bool _hasCachedZ;
/// <summary>
/// Creates a new GBM generator.
/// </summary>
/// <param name="startPrice">Initial price (default: 100.0)</param>
/// <param name="mu">Annual drift/return rate (default: 0.05 = 5%)</param>
/// <param name="sigma">Annual volatility (default: 0.2 = 20%)</param>
/// <param name="defaultTimeframe">Default timeframe for bars (default: 1 minute)</param>
public GBM(
double startPrice = 100.0,
double mu = 0.05,
double sigma = 0.2,
TimeSpan? defaultTimeframe = null)
{
_lastPrice = startPrice;
_lastTime = DateTime.UtcNow.Ticks;
_mu = mu;
_sigma = sigma;
// Use provided timeframe or default to 1 minute
var timeframe = defaultTimeframe ?? TimeSpan.FromMinutes(1);
_defaultTimeStep = timeframe.Ticks;
// Calculate dt based on timeframe (assuming 252 trading days/year, 6.5 hours/day)
double minutesPerYear = 252.0 * 6.5 * 60.0;
_dt = timeframe.TotalMinutes / minutesPerYear;
_drift = (mu - 0.5 * sigma * sigma) * _dt;
_vol = sigma * Math.Sqrt(_dt);
}
/// <summary>
/// Generates next standard normal using Box-Muller transform with caching.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double NextNormal()
{
if (_hasCachedZ)
{
_hasCachedZ = false;
return _cachedZ;
}
double u1 = 1.0 - _rnd.NextDouble();
double u2 = 1.0 - _rnd.NextDouble();
double mag = Math.Sqrt(-2.0 * Math.Log(u1));
double angle = 2.0 * Math.PI * u2;
_cachedZ = mag * Math.Sin(angle);
_hasCachedZ = true;
return mag * Math.Cos(angle);
}
/// <summary>
/// Gets the next bar with full bidirectional control.
/// GBM always honors the request - isNew parameter unchanged on return.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(ref bool isNew)
{
// GBM always honors request - parameter unchanged
if (isNew || !_hasCurrentBar)
{
// Generate new bar
long currentTime = _lastTime + _defaultTimeStep;
double z = NextNormal();
double price = _lastPrice * Math.Exp(_drift + _vol * z);
double volume = 1000 + _rnd.NextDouble() * 1000;
double open = _lastPrice;
double close = price;
double high = Math.Max(open, close) * (1.0 + _rnd.NextDouble() * 0.01);
double low = Math.Min(open, close) * (1.0 - _rnd.NextDouble() * 0.01);
_currentBar = new TBar(currentTime, open, high, low, close, volume);
_hasCurrentBar = true;
_lastPrice = close;
_lastTime = currentTime;
}
else
{
// Update current bar (intra-bar tick)
double z = NextNormal();
double price = _lastPrice * Math.Exp(_drift + _vol * z);
double volume = 1000 + _rnd.NextDouble() * 1000;
var bar = _currentBar;
double newClose = price;
double newHigh = Math.Max(bar.High, newClose);
double newLow = Math.Min(bar.Low, newClose);
_currentBar = new TBar(bar.Time, bar.Open, newHigh, newLow, newClose, volume);
_lastPrice = newClose;
}
return _currentBar;
}
/// <summary>
/// Gets the next bar with simple control.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(bool isNew = true)
{
// Delegate to ref version
return Next(ref isNew);
}
/// <summary>
/// Generates a batch of bars using optimized batch processing with explicit time parameters.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
{
if (count <= 0)
throw new ArgumentException("Count must be positive", nameof(count));
var series = new TBarSeries(count);
// Pre-allocate arrays for SoA layout
long[] t = new long[count];
double[] o = new double[count];
double[] h = new double[count];
double[] l = new double[count];
double[] c = new double[count];
double[] v = new double[count];
// Calculate dt for this specific interval
double minutesPerYear = 252.0 * 6.5 * 60.0;
double dt = interval.TotalMinutes / minutesPerYear;
double drift = (_mu - 0.5 * _sigma * _sigma) * dt;
double vol = _sigma * Math.Sqrt(dt);
long timeStep = interval.Ticks;
double currentPrice = _lastPrice;
long currentTime = startTime;
for (int i = 0; i < count; i++)
{
double z = NextNormal();
double price = currentPrice * Math.Exp(drift + vol * z);
double open = currentPrice;
double close = price;
double rnd1 = _rnd.NextDouble();
double rnd2 = _rnd.NextDouble();
double rnd3 = _rnd.NextDouble();
t[i] = currentTime;
o[i] = open;
c[i] = close;
h[i] = Math.Max(open, close) * (1.0 + rnd1 * 0.01);
l[i] = Math.Min(open, close) * (1.0 - rnd2 * 0.01);
v[i] = 1000 + rnd3 * 1000;
currentPrice = price;
currentTime += timeStep;
}
// Update internal state to continue from end of batch
_lastPrice = currentPrice;
_lastTime = currentTime - timeStep; // Last bar time, not next bar time
// Bulk add to series
series.Add(t, o, h, l, c, v);
// Reset streaming state after batch
_hasCurrentBar = false;
return series;
}
}
using System;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// Geometric Brownian Motion (GBM) generator for simulating OHLCV data.
/// Generates realistic price data for testing indicators and strategies.
/// Stateless design - only maintains minimal state needed for price continuity.
/// </summary>
public class GBM : IFeed
{
private readonly Random _rnd = new();
private double _lastPrice;
private long _lastTime;
private readonly double _mu;
private readonly double _sigma;
private readonly double _dt;
// Precomputed GBM constants
private readonly double _drift;
private readonly double _vol;
private readonly long _defaultTimeStep;
// State for streaming bar formation (only when isNew=false)
private TBar _currentBar;
private bool _hasCurrentBar;
// Box-Muller optimization: cache second normal
private double _cachedZ;
private bool _hasCachedZ;
/// <summary>
/// Creates a new GBM generator.
/// </summary>
/// <param name="startPrice">Initial price (default: 100.0)</param>
/// <param name="mu">Annual drift/return rate (default: 0.05 = 5%)</param>
/// <param name="sigma">Annual volatility (default: 0.2 = 20%)</param>
/// <param name="defaultTimeframe">Default timeframe for bars (default: 1 minute)</param>
public GBM(
double startPrice = 100.0,
double mu = 0.05,
double sigma = 0.2,
TimeSpan? defaultTimeframe = null)
{
_lastPrice = startPrice;
_lastTime = DateTime.UtcNow.Ticks;
_mu = mu;
_sigma = sigma;
// Use provided timeframe or default to 1 minute
var timeframe = defaultTimeframe ?? TimeSpan.FromMinutes(1);
_defaultTimeStep = timeframe.Ticks;
// Calculate dt based on timeframe (assuming 252 trading days/year, 6.5 hours/day)
double minutesPerYear = 252.0 * 6.5 * 60.0;
_dt = timeframe.TotalMinutes / minutesPerYear;
_drift = (mu - 0.5 * sigma * sigma) * _dt;
_vol = sigma * Math.Sqrt(_dt);
}
/// <summary>
/// Generates next standard normal using Box-Muller transform with caching.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double NextNormal()
{
if (_hasCachedZ)
{
_hasCachedZ = false;
return _cachedZ;
}
double u1 = 1.0 - _rnd.NextDouble();
double u2 = 1.0 - _rnd.NextDouble();
double mag = Math.Sqrt(-2.0 * Math.Log(u1));
double angle = 2.0 * Math.PI * u2;
_cachedZ = mag * Math.Sin(angle);
_hasCachedZ = true;
return mag * Math.Cos(angle);
}
/// <summary>
/// Gets the next bar with full bidirectional control.
/// GBM always honors the request - isNew parameter unchanged on return.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(ref bool isNew)
{
// GBM always honors request - parameter unchanged
if (isNew || !_hasCurrentBar)
{
// Generate new bar
long currentTime = _lastTime + _defaultTimeStep;
double z = NextNormal();
double price = _lastPrice * Math.Exp(_drift + _vol * z);
double volume = 1000 + _rnd.NextDouble() * 1000;
double open = _lastPrice;
double close = price;
double high = Math.Max(open, close) * (1.0 + _rnd.NextDouble() * 0.01);
double low = Math.Min(open, close) * (1.0 - _rnd.NextDouble() * 0.01);
_currentBar = new TBar(currentTime, open, high, low, close, volume);
_hasCurrentBar = true;
_lastPrice = close;
_lastTime = currentTime;
}
else
{
// Update current bar (intra-bar tick)
double z = NextNormal();
double price = _lastPrice * Math.Exp(_drift + _vol * z);
double volume = 1000 + _rnd.NextDouble() * 1000;
var bar = _currentBar;
double newClose = price;
double newHigh = Math.Max(bar.High, newClose);
double newLow = Math.Min(bar.Low, newClose);
_currentBar = new TBar(bar.Time, bar.Open, newHigh, newLow, newClose, volume);
_lastPrice = newClose;
}
return _currentBar;
}
/// <summary>
/// Gets the next bar with simple control.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar Next(bool isNew = true)
{
// Delegate to ref version
return Next(ref isNew);
}
/// <summary>
/// Generates a batch of bars using optimized batch processing with explicit time parameters.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
{
if (count <= 0)
throw new ArgumentException("Count must be positive", nameof(count));
var series = new TBarSeries(count);
// Pre-allocate arrays for SoA layout
long[] t = new long[count];
double[] o = new double[count];
double[] h = new double[count];
double[] l = new double[count];
double[] c = new double[count];
double[] v = new double[count];
// Calculate dt for this specific interval
double minutesPerYear = 252.0 * 6.5 * 60.0;
double dt = interval.TotalMinutes / minutesPerYear;
double drift = (_mu - 0.5 * _sigma * _sigma) * dt;
double vol = _sigma * Math.Sqrt(dt);
long timeStep = interval.Ticks;
double currentPrice = _lastPrice;
long currentTime = startTime;
for (int i = 0; i < count; i++)
{
double z = NextNormal();
double price = currentPrice * Math.Exp(drift + vol * z);
double open = currentPrice;
double close = price;
double rnd1 = _rnd.NextDouble();
double rnd2 = _rnd.NextDouble();
double rnd3 = _rnd.NextDouble();
t[i] = currentTime;
o[i] = open;
c[i] = close;
h[i] = Math.Max(open, close) * (1.0 + rnd1 * 0.01);
l[i] = Math.Min(open, close) * (1.0 - rnd2 * 0.01);
v[i] = 1000 + rnd3 * 1000;
currentPrice = price;
currentTime += timeStep;
}
// Update internal state to continue from end of batch
_lastPrice = currentPrice;
_lastTime = currentTime - timeStep; // Last bar time, not next bar time
// Bulk add to series
series.Add(t, o, h, l, c, v);
// Reset streaming state after batch
_hasCurrentBar = false;
return series;
}
}