#pragma warning disable CS0618 // Tests intentionally use obsolete Next(bool) overload to verify it still works namespace QuanTAlib.Tests; public sealed class CsvFeedTests : IDisposable { private const string TestCsvPath = "daily_IBM.csv"; private readonly List _tempFiles = new(); private bool _disposed; // Static test data arrays to avoid CA1861 (constant arrays as arguments) private static readonly string[] HeaderOnlyData = ["timestamp,open,high,low,close,volume"]; private static readonly string[] MalformedDateData = ["timestamp,open,high,low,close,volume", "not-a-date,100,101,99,100,1000"]; private static readonly string[] MalformedPriceData = ["timestamp,open,high,low,close,volume", "2023-01-01,not-a-number,101,99,100,1000"]; private static readonly string[] MissingColumnsData = ["timestamp,open,high,low,close,volume", "2023-01-01,100,101,99,100"]; private static readonly string[] ExtraColumnsData = ["timestamp,open,high,low,close,volume,extra", "2023-01-01,100,101,99,100,1000,extra_data"]; private static readonly string[] SingleBarData = ["timestamp,open,high,low,close,volume", "2023-01-01,100,101,99,100,1000"]; private static readonly string[] DecimalPrecisionData = ["timestamp,open,high,low,close,volume", "2023-01-01,100.1234,101.5678,99.9999,100.0001,1234567.89"]; private static readonly string[] NegativeValuesData = ["timestamp,open,high,low,close,volume", "2023-01-01,-100,50,-150,-50,1000"]; private static readonly string[] ScientificNotationData = ["timestamp,open,high,low,close,volume", "2023-01-01,1.5e2,2e2,1e2,1.75e2,1e6"]; private static readonly string[] GapDataReversed = ["timestamp,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"]; private static readonly string[] TwoBarsWithGapData = ["timestamp,open,high,low,close,volume", "2023-01-03,101,102,100,101,1000", "2023-01-01,100,101,99,100,1000"]; private static readonly string[] WhitespaceData = ["timestamp,open,high,low,close,volume", " 2023-01-01 , 100 , 101 , 99 , 100 , 1000 "]; private static readonly string[] ZeroValuesData = ["timestamp,open,high,low,close,volume", "2023-01-01,0,0,0,0,0"]; private static readonly string[] LargeValuesData = ["timestamp,open,high,low,close,volume", "2023-01-01,999999999.99,1000000000.01,999999999.00,999999999.50,9999999999999"]; public void Dispose() { if (_disposed) { return; } _disposed = true; GC.SuppressFinalize(this); foreach (var file in _tempFiles) { if (File.Exists(file)) { try { File.Delete(file); } catch { /* ignore */ } } } } private string CreateTempCsv(string[] lines) { string tempPath = Path.GetTempFileName() + ".csv"; File.WriteAllLines(tempPath, lines); _tempFiles.Add(tempPath); return tempPath; } #region Constructor Tests [Fact] public void Constructor_ValidFile_LoadsData() { var feed = new CsvFeed(TestCsvPath); Assert.NotNull(feed); Assert.True(feed.Count > 0); } [Fact] public void Constructor_NonExistentFile_ThrowsFileNotFoundException() { Assert.Throws(() => new CsvFeed("nonexistent.csv")); } [Fact] public void Constructor_NullPath_ThrowsArgumentException() { var ex = Assert.Throws(() => new CsvFeed(null!)); Assert.Equal("filePath", ex.ParamName); } [Fact] public void Constructor_EmptyPath_ThrowsArgumentException() { var ex = Assert.Throws(() => new CsvFeed("")); Assert.Equal("filePath", ex.ParamName); } [Fact] public void Constructor_WhitespacePath_ThrowsArgumentException() { var ex = Assert.Throws(() => new CsvFeed(" ")); Assert.Equal("filePath", ex.ParamName); } [Fact] public void Constructor_EmptyCsv_ThrowsInvalidDataException() { string tempCsv = CreateTempCsv(Array.Empty()); Assert.Throws(() => new CsvFeed(tempCsv)); } [Fact] public void Constructor_HeaderOnlyCsv_ThrowsInvalidDataException() { string tempCsv = CreateTempCsv(HeaderOnlyData); Assert.Throws(() => new CsvFeed(tempCsv)); } [Fact] public void Constructor_MalformedDate_ThrowsFormatException() { string tempCsv = CreateTempCsv(MalformedDateData); Assert.Throws(() => new CsvFeed(tempCsv)); } [Fact] public void Constructor_MalformedPrice_ThrowsFormatException() { string tempCsv = CreateTempCsv(MalformedPriceData); Assert.Throws(() => new CsvFeed(tempCsv)); } [Fact] public void Constructor_MissingColumns_ThrowsFormatException() { string tempCsv = CreateTempCsv(MissingColumnsData); Assert.Throws(() => new CsvFeed(tempCsv)); } [Fact] public void Constructor_ExtraColumns_ThrowsFormatException() { // Extra columns should throw format exception (strict 6-column format) string tempCsv = CreateTempCsv(ExtraColumnsData); Assert.Throws(() => new CsvFeed(tempCsv)); } #endregion #region Property Tests [Fact] public void Count_ReturnsCorrectNumber() { var feed = new CsvFeed(TestCsvPath); Assert.True(feed.Count > 0); // IBM CSV has 100 rows of data Assert.Equal(100, feed.Count); } [Fact] public void FilePath_ReturnsLoadedPath() { var feed = new CsvFeed(TestCsvPath); Assert.Equal(TestCsvPath, feed.FilePath); } [Fact] public void HasMore_TrueAtStart() { var feed = new CsvFeed(TestCsvPath); Assert.True(feed.HasMore); } [Fact] public void HasMore_FalseWhenExhausted() { string tempCsv = CreateTempCsv(SingleBarData); var feed = new CsvFeed(tempCsv); Assert.True(feed.HasMore); feed.Next(isNew: true); Assert.False(feed.HasMore); } [Fact] public void CurrentIndex_StartsAtZero() { var feed = new CsvFeed(TestCsvPath); Assert.Equal(0, feed.CurrentIndex); } [Fact] public void CurrentIndex_IncrementsOnNext() { var feed = new CsvFeed(TestCsvPath); Assert.Equal(0, feed.CurrentIndex); feed.Next(isNew: true); Assert.Equal(1, feed.CurrentIndex); feed.Next(isNew: true); Assert.Equal(2, feed.CurrentIndex); } [Fact] public void CurrentIndex_DoesNotIncrementOnUpdate() { var feed = new CsvFeed(TestCsvPath); feed.Next(isNew: true); int indexAfterFirst = feed.CurrentIndex; feed.Next(isNew: false); Assert.Equal(indexAfterFirst, feed.CurrentIndex); } [Fact] public void HasCurrentBar_FalseAtStart() { var feed = new CsvFeed(TestCsvPath); Assert.False(feed.HasCurrentBar); } [Fact] public void HasCurrentBar_TrueAfterNext() { var feed = new CsvFeed(TestCsvPath); feed.Next(isNew: true); Assert.True(feed.HasCurrentBar); } [Fact] public void Data_ReturnsUnderlyingSeries() { var feed = new CsvFeed(TestCsvPath); var data = feed.Data; Assert.NotNull(data); Assert.Equal(feed.Count, data.Count); } #endregion #region Next Method Tests [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 Next_EmptyData_ReturnsDefaultAndSignalsNoMore() { // Create a mock scenario - but since constructor throws on empty, // we test the behavior when all data is consumed string tempCsv = CreateTempCsv(SingleBarData); var feed = new CsvFeed(tempCsv); // Consume all data bool isNew = true; feed.Next(ref isNew); // Now at end isNew = true; var bar = feed.Next(ref isNew); Assert.False(isNew); Assert.Equal(100.0, bar.Close); // Returns last bar } [Fact] public void Next_DefaultParameter_IsNewTrue() { var feed = new CsvFeed(TestCsvPath); var bar1 = feed.Next(); // Default isNew = true var bar2 = feed.Next(); // Default isNew = true Assert.True(bar2.Time > bar1.Time); } [Fact] public void CsvFeed_ObsoleteOverload_SilentlyMissesEndOfStream() { string tempCsv = CreateTempCsv(SingleBarData); var feed = new CsvFeed(tempCsv); // Value overload cannot detect end — silently returns last bar feed.Next(isNew: true); // bar 1 var postEnd = feed.Next(isNew: true); // past end — no exception, no signal // Must use HasMore to detect end when using value overload Assert.False(feed.HasMore); Assert.Equal(100.0, postEnd.Close); // last bar returned again // Ref overload detects end correctly bool isNew = true; feed.Next(ref isNew); Assert.False(isNew); // ref overload signals end } #endregion #region Fetch Method Tests [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_ZeroCount_ThrowsArgumentException() { var feed = new CsvFeed(TestCsvPath); var startTime = DateTime.UtcNow.Ticks; var interval = TimeSpan.FromDays(1); var ex = Assert.Throws(() => feed.Fetch(0, startTime, interval)); Assert.Equal("count", ex.ParamName); } [Fact] public void Fetch_NegativeCount_ThrowsArgumentException() { var feed = new CsvFeed(TestCsvPath); var startTime = DateTime.UtcNow.Ticks; var interval = TimeSpan.FromDays(1); var ex = Assert.Throws(() => feed.Fetch(-1, startTime, interval)); Assert.Equal("count", ex.ParamName); } [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 Fetch_ResetsHasCurrentBar() { var feed = new CsvFeed(TestCsvPath); feed.Next(isNew: true); Assert.True(feed.HasCurrentBar); var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks; feed.Fetch(5, startTime, TimeSpan.FromDays(1)); Assert.False(feed.HasCurrentBar); } [Fact] public void CsvFeed_FetchThenNext_StreamsFromFetchStartNotEnd() { 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(5, startTime, interval); // After Fetch, streaming replays from the START of the fetched window // (not from after the last returned bar) for (int i = 0; i < series.Count; i++) { bool isNew = true; var bar = feed.Next(ref isNew); Assert.Equal(series[i].Time, bar.Time); Assert.True(isNew); } } #endregion #region Reset Method Tests [Fact] public void Reset_ReturnsToStart() { var feed = new CsvFeed(TestCsvPath); // Advance several bars var firstBar = feed.Next(isNew: true); feed.Next(isNew: true); feed.Next(isNew: true); Assert.Equal(3, feed.CurrentIndex); // Reset feed.Reset(); Assert.Equal(0, feed.CurrentIndex); Assert.True(feed.HasMore); Assert.False(feed.HasCurrentBar); // Next bar should be first bar again var afterReset = feed.Next(isNew: true); Assert.Equal(firstBar.Time, afterReset.Time); Assert.Equal(firstBar.Close, afterReset.Close); } [Fact] public void Reset_WithIndex_SetsCorrectPosition() { var feed = new CsvFeed(TestCsvPath); // Reset to middle const int targetIndex = 50; feed.Reset(targetIndex); Assert.Equal(targetIndex, feed.CurrentIndex); Assert.False(feed.HasCurrentBar); // Next bar should be at that index var bar = feed.Next(isNew: true); var expectedBar = feed.GetBar(targetIndex); Assert.Equal(expectedBar.Time, bar.Time); } [Fact] public void Reset_WithNegativeIndex_ThrowsArgumentOutOfRangeException() { var feed = new CsvFeed(TestCsvPath); var ex = Assert.Throws(() => feed.Reset(-1)); Assert.Equal("index", ex.ParamName); } [Fact] public void Reset_WithIndexBeyondCount_ThrowsArgumentOutOfRangeException() { var feed = new CsvFeed(TestCsvPath); var ex = Assert.Throws(() => feed.Reset(feed.Count + 1)); Assert.Equal("index", ex.ParamName); } [Fact] public void Reset_WithIndexAtCount_IsValid() { // Resetting to exactly Count means "at end" - valid but no more data var feed = new CsvFeed(TestCsvPath); feed.Reset(feed.Count); Assert.Equal(feed.Count, feed.CurrentIndex); Assert.False(feed.HasMore); } #endregion #region GetBar Method Tests [Fact] public void GetBar_ReturnsCorrectBar() { var feed = new CsvFeed(TestCsvPath); // Get bar without affecting streaming var bar0 = feed.GetBar(0); var bar1 = feed.GetBar(1); // Streaming position unchanged Assert.Equal(0, feed.CurrentIndex); // Bars should be in chronological order Assert.True(bar1.Time > bar0.Time); } [Fact] public void GetBar_NegativeIndex_ThrowsArgumentOutOfRangeException() { var feed = new CsvFeed(TestCsvPath); var ex = Assert.Throws(() => feed.GetBar(-1)); Assert.Equal("index", ex.ParamName); } [Fact] public void GetBar_IndexAtCount_ThrowsArgumentOutOfRangeException() { var feed = new CsvFeed(TestCsvPath); var ex = Assert.Throws(() => feed.GetBar(feed.Count)); Assert.Equal("index", ex.ParamName); } [Fact] public void GetBar_DoesNotAffectStreaming() { var feed = new CsvFeed(TestCsvPath); // Stream first bar var streamed = feed.Next(isNew: true); int indexAfter = feed.CurrentIndex; // Random access var bar50 = feed.GetBar(50); Assert.True(bar50.Time > 0); // Streaming position unchanged Assert.Equal(indexAfter, feed.CurrentIndex); // Continue streaming var next = feed.Next(isNew: true); Assert.True(next.Time > streamed.Time); } [Fact] public void GetBar_ConsistentWithNext() { var feed = new CsvFeed(TestCsvPath); // Get bars via random access var bar0 = feed.GetBar(0); var bar1 = feed.GetBar(1); var bar2 = feed.GetBar(2); // Get same bars via streaming var streamed0 = feed.Next(isNew: true); var streamed1 = feed.Next(isNew: true); var streamed2 = feed.Next(isNew: true); Assert.Equal(bar0.Time, streamed0.Time); Assert.Equal(bar1.Time, streamed1.Time); Assert.Equal(bar2.Time, streamed2.Time); } #endregion #region OHLCV Validation Tests [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(); 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 LoadFromCsv_AllBarsHaveValidOHLCV() { var feed = new CsvFeed(TestCsvPath); for (int i = 0; i < feed.Count; i++) { var bar = feed.GetBar(i); Assert.True(double.IsFinite(bar.Open), $"Bar {i} has non-finite Open"); Assert.True(double.IsFinite(bar.High), $"Bar {i} has non-finite High"); Assert.True(double.IsFinite(bar.Low), $"Bar {i} has non-finite Low"); Assert.True(double.IsFinite(bar.Close), $"Bar {i} has non-finite Close"); Assert.True(double.IsFinite(bar.Volume), $"Bar {i} has non-finite Volume"); Assert.True(bar.High >= bar.Low, $"Bar {i}: High ({bar.High}) < Low ({bar.Low})"); Assert.True(bar.High >= bar.Open, $"Bar {i}: High ({bar.High}) < Open ({bar.Open})"); Assert.True(bar.High >= bar.Close, $"Bar {i}: High ({bar.High}) < Close ({bar.Close})"); Assert.True(bar.Low <= bar.Open, $"Bar {i}: Low ({bar.Low}) > Open ({bar.Open})"); Assert.True(bar.Low <= bar.Close, $"Bar {i}: Low ({bar.Low}) > Close ({bar.Close})"); } } [Fact] public void LoadFromCsv_ParsesDecimalsCorrectly() { string tempCsv = CreateTempCsv(DecimalPrecisionData); var feed = new CsvFeed(tempCsv); var bar = feed.Next(isNew: true); Assert.Equal(100.1234, bar.Open, precision: 4); Assert.Equal(101.5678, bar.High, precision: 4); Assert.Equal(99.9999, bar.Low, precision: 4); Assert.Equal(100.0001, bar.Close, precision: 4); Assert.Equal(1234567.89, bar.Volume, precision: 2); } [Fact] public void LoadFromCsv_ParsesNegativeValues() { // While negative prices are unusual, the parser should handle them string tempCsv = CreateTempCsv(NegativeValuesData); var feed = new CsvFeed(tempCsv); var bar = feed.Next(isNew: true); Assert.Equal(-100, bar.Open); Assert.Equal(50, bar.High); Assert.Equal(-150, bar.Low); Assert.Equal(-50, bar.Close); } [Fact] public void LoadFromCsv_ParsesScientificNotation() { string tempCsv = CreateTempCsv(ScientificNotationData); var feed = new CsvFeed(tempCsv); var bar = feed.Next(isNew: true); Assert.Equal(150, bar.Open); Assert.Equal(200, bar.High); Assert.Equal(100, bar.Low); Assert.Equal(175, bar.Close); Assert.Equal(1000000, bar.Volume); } #endregion #region IFeed Interface Tests [Fact] public void CsvFeed_WorksWithIFeedInterface() { CsvFeed 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 CsvFeed_IFeedRefOverload() { CsvFeed feed = new CsvFeed(TestCsvPath); bool isNew = true; var bar1 = feed.Next(ref isNew); Assert.True(bar1.Time > 0); isNew = false; var bar1Update = feed.Next(ref isNew); Assert.Equal(bar1.Time, bar1Update.Time); } [Fact] public void CsvFeed_IFeedFetch() { CsvFeed feed = new CsvFeed(TestCsvPath); var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks; var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1)); Assert.True(series.Count > 0); } #endregion #region Edge Case Tests [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.Empty(series); } [Fact] public void Fetch_HandlesGapsCorrectly() { // Create CSV with gaps using helper string tempCsv = CreateTempCsv(GapDataReversed); var feed = new CsvFeed(tempCsv); var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks; var interval = TimeSpan.FromDays(1); // Fetch 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 (Jan 3 missing) Assert.Equal(startTime + (3 * interval.Ticks), series[2].Time); // Jan 4 Assert.Equal(startTime + (4 * interval.Ticks), series[3].Time); // Jan 5 } [Fact] public void SingleBar_StreamsAndEnds() { string tempCsv = CreateTempCsv(SingleBarData); var feed = new CsvFeed(tempCsv); Assert.Equal(1, feed.Count); Assert.True(feed.HasMore); bool isNew = true; var bar = feed.Next(ref isNew); Assert.True(isNew); Assert.Equal(100.0, bar.Close); Assert.False(feed.HasMore); // Try to get next isNew = true; var noMore = feed.Next(ref isNew); Assert.False(isNew); // Signals end Assert.Equal(bar.Time, noMore.Time); // Returns last bar } [Fact] public void WhitespaceInValues_Trimmed() { string tempCsv = CreateTempCsv(WhitespaceData); var feed = new CsvFeed(tempCsv); var bar = feed.Next(isNew: true); Assert.Equal(100.0, bar.Open); Assert.Equal(101.0, bar.High); Assert.Equal(99.0, bar.Low); Assert.Equal(100.0, bar.Close); Assert.Equal(1000.0, bar.Volume); } [Fact] public void ZeroValues_Accepted() { string tempCsv = CreateTempCsv(ZeroValuesData); var feed = new CsvFeed(tempCsv); var bar = feed.Next(isNew: true); Assert.Equal(0.0, bar.Open); Assert.Equal(0.0, bar.High); Assert.Equal(0.0, bar.Low); Assert.Equal(0.0, bar.Close); Assert.Equal(0.0, bar.Volume); } [Fact] public void VeryLargeValues_Parsed() { string tempCsv = CreateTempCsv(LargeValuesData); var feed = new CsvFeed(tempCsv); var bar = feed.Next(isNew: true); Assert.Equal(999999999.99, bar.Open, precision: 2); Assert.Equal(1000000000.01, bar.High, precision: 2); Assert.Equal(999999999.00, bar.Low, precision: 2); Assert.Equal(999999999.50, bar.Close, precision: 2); Assert.Equal(9999999999999.0, bar.Volume, precision: 0); } [Fact] public void ConsecutiveResets_WorkCorrectly() { var feed = new CsvFeed(TestCsvPath); feed.Next(isNew: true); feed.Next(isNew: true); feed.Reset(); feed.Reset(); feed.Reset(); Assert.Equal(0, feed.CurrentIndex); Assert.False(feed.HasCurrentBar); } [Fact] public void StreamThenResetThenStream_Consistent() { var feed = new CsvFeed(TestCsvPath); // First pass var firstPass = new List(); for (int i = 0; i < 10; i++) { firstPass.Add(feed.Next(isNew: true).Close); } // Reset feed.Reset(); // Second pass var secondPass = new List(); for (int i = 0; i < 10; i++) { secondPass.Add(feed.Next(isNew: true).Close); } // Should be identical for (int i = 0; i < 10; i++) { Assert.Equal(firstPass[i], secondPass[i]); } } [Fact] public void CsvFeed_Fetch_Tolerance_GapBarSkipped() { // Verify gap-skip behavior: Jan 2 is absent, Fetch re-aligns to Jan 3 string tempCsv = CreateTempCsv(TwoBarsWithGapData); var feed = new CsvFeed(tempCsv); var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks; var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1)); // Jan 1 and Jan 3 present; Jan 2 absent → Fetch includes both with gap Assert.Equal(2, series.Count); Assert.Equal(startTime, series[0].Time); Assert.Equal(startTime + (2 * TimeSpan.FromDays(1).Ticks), series[1].Time); } #endregion }