mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 03:28:05 +00:00
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:
+296
-296
@@ -1,296 +1,296 @@
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_HandlesGapsCorrectly()
|
||||
{
|
||||
string tempCsv = Path.GetTempFileName() + ".csv";
|
||||
try
|
||||
{
|
||||
// Create CSV with gaps
|
||||
// Date, Open, High, Low, Close, Volume
|
||||
// 2023-01-01 (Sunday)
|
||||
// 2023-01-02 (Monday)
|
||||
// 2023-01-04 (Wednesday) - Gap of Tuesday
|
||||
// 2023-01-05 (Thursday)
|
||||
var lines = new[]
|
||||
{
|
||||
"Date,Open,High,Low,Close,Volume",
|
||||
"2023-01-05,103,104,102,103,1000",
|
||||
"2023-01-04,102,103,101,102,1000",
|
||||
"2023-01-02,101,102,100,101,1000",
|
||||
"2023-01-01,100,101,99,100,1000"
|
||||
};
|
||||
File.WriteAllLines(tempCsv, lines);
|
||||
|
||||
var feed = new CsvFeed(tempCsv);
|
||||
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromDays(1);
|
||||
|
||||
// Fetch 5 bars. Should get 4 bars (Jan 1, 2, 4, 5).
|
||||
var series = feed.Fetch(10, startTime, interval);
|
||||
|
||||
Assert.Equal(4, series.Count);
|
||||
Assert.Equal(startTime, series[0].Time); // Jan 1
|
||||
Assert.Equal(startTime + interval.Ticks, series[1].Time); // Jan 2
|
||||
// Gap here
|
||||
Assert.Equal(startTime + 3 * interval.Ticks, series[2].Time); // Jan 4
|
||||
Assert.Equal(startTime + 4 * interval.Ticks, series[3].Time); // Jan 5
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempCsv))
|
||||
File.Delete(tempCsv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_HandlesGapsCorrectly()
|
||||
{
|
||||
string tempCsv = Path.GetTempFileName() + ".csv";
|
||||
try
|
||||
{
|
||||
// Create CSV with gaps
|
||||
// Date, Open, High, Low, Close, Volume
|
||||
// 2023-01-01 (Sunday)
|
||||
// 2023-01-02 (Monday)
|
||||
// 2023-01-04 (Wednesday) - Gap of Tuesday
|
||||
// 2023-01-05 (Thursday)
|
||||
var lines = new[]
|
||||
{
|
||||
"Date,Open,High,Low,Close,Volume",
|
||||
"2023-01-05,103,104,102,103,1000",
|
||||
"2023-01-04,102,103,101,102,1000",
|
||||
"2023-01-02,101,102,100,101,1000",
|
||||
"2023-01-01,100,101,99,100,1000"
|
||||
};
|
||||
File.WriteAllLines(tempCsv, lines);
|
||||
|
||||
var feed = new CsvFeed(tempCsv);
|
||||
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromDays(1);
|
||||
|
||||
// Fetch 5 bars. Should get 4 bars (Jan 1, 2, 4, 5).
|
||||
var series = feed.Fetch(10, startTime, interval);
|
||||
|
||||
Assert.Equal(4, series.Count);
|
||||
Assert.Equal(startTime, series[0].Time); // Jan 1
|
||||
Assert.Equal(startTime + interval.Ticks, series[1].Time); // Jan 2
|
||||
// Gap here
|
||||
Assert.Equal(startTime + 3 * interval.Ticks, series[2].Time); // Jan 4
|
||||
Assert.Equal(startTime + 4 * interval.Ticks, series[3].Time); // Jan 5
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempCsv))
|
||||
File.Delete(tempCsv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+207
-207
@@ -1,207 +1,207 @@
|
||||
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
|
||||
/// Memory-efficient: reads lines into list, reverses in-place (no LINQ allocations).
|
||||
/// </summary>
|
||||
private static TBarSeries LoadFromCsv(string filePath)
|
||||
{
|
||||
var dataLines = new List<string>();
|
||||
using (var reader = new StreamReader(filePath))
|
||||
{
|
||||
var header = reader.ReadLine();
|
||||
if (header is null)
|
||||
throw new InvalidDataException("CSV file is empty");
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
dataLines.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (dataLines.Count == 0)
|
||||
throw new InvalidDataException("CSV file contains only header, no data");
|
||||
|
||||
// Reverse in-place to chronological order (oldest first)
|
||||
dataLines.Reverse();
|
||||
|
||||
var series = new TBarSeries(dataLines.Count);
|
||||
|
||||
for (int i = 0; i < dataLines.Count; i++)
|
||||
{
|
||||
var line = dataLines[i];
|
||||
|
||||
var parts = line.Split(',');
|
||||
int originalLineNumber = dataLines.Count - i + 1;
|
||||
if (parts.Length != 6)
|
||||
throw new FormatException($"Invalid CSV format at line {originalLineNumber}. Expected 6 columns, found {parts.Length}");
|
||||
|
||||
// Parse timestamp (YYYY-MM-DD format, assume UTC midnight)
|
||||
if (!DateTime.TryParseExact(parts[0].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var timestamp))
|
||||
{
|
||||
throw new FormatException($"Failed to parse timestamp at line {originalLineNumber}: {line}");
|
||||
}
|
||||
|
||||
// Parse OHLCV values
|
||||
if (!double.TryParse(parts[1].Trim(), CultureInfo.InvariantCulture, out double open) ||
|
||||
!double.TryParse(parts[2].Trim(), CultureInfo.InvariantCulture, out double high) ||
|
||||
!double.TryParse(parts[3].Trim(), CultureInfo.InvariantCulture, out double low) ||
|
||||
!double.TryParse(parts[4].Trim(), CultureInfo.InvariantCulture, out double close) ||
|
||||
!double.TryParse(parts[5].Trim(), CultureInfo.InvariantCulture, out double volume))
|
||||
{
|
||||
throw new FormatException($"Failed to parse CSV line {originalLineNumber}: {line}");
|
||||
}
|
||||
|
||||
series.Add(timestamp, open, high, low, close, volume, isNew: true);
|
||||
}
|
||||
|
||||
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 = -1;
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
if (_data[i].Time >= startTime)
|
||||
{
|
||||
startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startIndex == -1)
|
||||
return result;
|
||||
|
||||
// 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 * interval.Ticks;
|
||||
|
||||
if (Math.Abs(bar.Time - expectedTime) <= 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
|
||||
/// Memory-efficient: reads lines into list, reverses in-place (no LINQ allocations).
|
||||
/// </summary>
|
||||
private static TBarSeries LoadFromCsv(string filePath)
|
||||
{
|
||||
var dataLines = new List<string>();
|
||||
using (var reader = new StreamReader(filePath))
|
||||
{
|
||||
var header = reader.ReadLine();
|
||||
if (header is null)
|
||||
throw new InvalidDataException("CSV file is empty");
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
dataLines.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (dataLines.Count == 0)
|
||||
throw new InvalidDataException("CSV file contains only header, no data");
|
||||
|
||||
// Reverse in-place to chronological order (oldest first)
|
||||
dataLines.Reverse();
|
||||
|
||||
var series = new TBarSeries(dataLines.Count);
|
||||
|
||||
for (int i = 0; i < dataLines.Count; i++)
|
||||
{
|
||||
var line = dataLines[i];
|
||||
|
||||
var parts = line.Split(',');
|
||||
int originalLineNumber = dataLines.Count - i + 1;
|
||||
if (parts.Length != 6)
|
||||
throw new FormatException($"Invalid CSV format at line {originalLineNumber}. Expected 6 columns, found {parts.Length}");
|
||||
|
||||
// Parse timestamp (YYYY-MM-DD format, assume UTC midnight)
|
||||
if (!DateTime.TryParseExact(parts[0].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var timestamp))
|
||||
{
|
||||
throw new FormatException($"Failed to parse timestamp at line {originalLineNumber}: {line}");
|
||||
}
|
||||
|
||||
// Parse OHLCV values
|
||||
if (!double.TryParse(parts[1].Trim(), CultureInfo.InvariantCulture, out double open) ||
|
||||
!double.TryParse(parts[2].Trim(), CultureInfo.InvariantCulture, out double high) ||
|
||||
!double.TryParse(parts[3].Trim(), CultureInfo.InvariantCulture, out double low) ||
|
||||
!double.TryParse(parts[4].Trim(), CultureInfo.InvariantCulture, out double close) ||
|
||||
!double.TryParse(parts[5].Trim(), CultureInfo.InvariantCulture, out double volume))
|
||||
{
|
||||
throw new FormatException($"Failed to parse CSV line {originalLineNumber}: {line}");
|
||||
}
|
||||
|
||||
series.Add(timestamp, open, high, low, close, volume, isNew: true);
|
||||
}
|
||||
|
||||
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 = -1;
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
if (_data[i].Time >= startTime)
|
||||
{
|
||||
startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startIndex == -1)
|
||||
return result;
|
||||
|
||||
// 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 * interval.Ticks;
|
||||
|
||||
if (Math.Abs(bar.Time - expectedTime) <= tolerance)
|
||||
{
|
||||
result.Add(bar, isNew: true);
|
||||
collected++;
|
||||
expectedTime += interval.Ticks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset streaming to start of returned data
|
||||
_currentIndex = startIndex;
|
||||
_hasCurrentBar = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+72
-72
@@ -1,72 +1,72 @@
|
||||
# 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));
|
||||
|
||||
Reference in New Issue
Block a user