mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 20:18:05 +00:00
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
This commit is contained in:
@@ -60,3 +60,4 @@ When implementing `IFeed`:
|
||||
|
||||
* **`GBM`**: Geometric Brownian Motion generator (Synthetic).
|
||||
* **`CsvFeed`**: Reads OHLCV data from CSV files (Historical).
|
||||
* **`AlphaVantage`**: Fetches OHLCV data from the Alpha Vantage REST API (Historical/Live).
|
||||
|
||||
@@ -20,6 +20,7 @@ public sealed class CsvFeedTests : IDisposable
|
||||
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"];
|
||||
@@ -337,6 +338,26 @@ public sealed class CsvFeedTests : IDisposable
|
||||
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
|
||||
@@ -410,6 +431,26 @@ public sealed class CsvFeedTests : IDisposable
|
||||
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
|
||||
@@ -888,5 +929,20 @@ public sealed class CsvFeedTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[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
|
||||
}
|
||||
|
||||
@@ -664,6 +664,36 @@ public class GBMTests
|
||||
Assert.False(gbm.HasCurrentBar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GBM_FetchThenNext_PriceContinuity()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, seed: 42);
|
||||
long startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
double lastBatchClose = series[4].Close;
|
||||
|
||||
// Next bar after Fetch must open at the last batch close (price continuity)
|
||||
var nextBar = gbm.Next(isNew: true);
|
||||
Assert.Equal(lastBatchClose, nextBar.Open, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GBM_NextThenFetch_PriceContinuity()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, seed: 42);
|
||||
|
||||
_ = gbm.Next(isNew: true);
|
||||
var bar2 = gbm.Next(isNew: true); // _lastPrice = bar2.Close
|
||||
|
||||
long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
|
||||
var series = gbm.Fetch(3, startTime, TimeSpan.FromMinutes(1));
|
||||
|
||||
// First bar of Fetch must open at bar2.Close
|
||||
Assert.Equal(bar2.Close, series[0].Open, 1e-10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IFeed Interface Tests
|
||||
|
||||
@@ -120,6 +120,10 @@ public sealed class GBM : IFeed
|
||||
/// <summary>
|
||||
/// Resets the generator to its initial state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sets the internal time anchor to <see cref="DateTime.UtcNow"/>. For deterministic
|
||||
/// time sequences use <see cref="Reset(long)"/> with an explicit start time.
|
||||
/// </remarks>
|
||||
public void Reset()
|
||||
{
|
||||
_lastPrice = StartPrice;
|
||||
@@ -284,6 +288,15 @@ public sealed class GBM : IFeed
|
||||
/// <returns>A TBarSeries containing the generated bars</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when count is not positive</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when interval is not positive</exception>
|
||||
/// <remarks>
|
||||
/// Price continuity: <c>batch[0].Open</c> equals <c>_lastPrice</c> at call time, so the
|
||||
/// batch begins exactly where the previous <see cref="Next(bool)"/> call left off.
|
||||
/// After the call, <c>_lastPrice</c> and <c>_lastTime</c> are updated to the end of the
|
||||
/// generated batch, enabling seamless continuation via subsequent <see cref="Next(bool)"/>
|
||||
/// calls. <paramref name="startTime"/> need not follow the previous <c>_lastTime</c> —
|
||||
/// this allows replaying a window or generating a non-contiguous batch while preserving
|
||||
/// price continuity.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user