Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aggregate using TimeSpan #554

Merged
merged 19 commits into from
Sep 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ IEnumerable<Quote> dayBarQuotes =
minuteBarQuotes.Aggregate(PeriodSize.Day);
```

An alternate version of this utility is provided where you can use any native `TimeSpan` value that is greater than `TimeSpan.Zero`.

```csharp
// alternate usage
IEnumerable<Quote> dayBarQuotes =
minuteBarQuotes.Aggregate(TimeSpan timeSpan);
```

:warning: **Warning**: Partially populated period windows at the beginning, end, and market open/close points in `quotes` can be misleading when aggregated. For example, if you are aggregating intraday minute bars into 15 minute bars and there is a single 4:00pm minute bar at the end, the resulting 4:00pm 15-minute bar will only have one minute of data in it whereas the previous 3:45pm bar will have all 15 minutes of bars aggregated (3:45-3:59pm).

#### PeriodSize options (for newSize)
Expand Down
28 changes: 20 additions & 8 deletions src/_common/Quotes/Quotes.Functions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,27 +48,39 @@ public static IEnumerable<Quote> Aggregate<TQuote>(
PeriodSize newSize)
where TQuote : IQuote
{
// parameter conversion
TimeSpan newTimeSpan = newSize.ToTimeSpan();

// convert
return quotes.Aggregate(newTimeSpan);
}

// aggregation (quantization) using TimeSpan
/// <include file='./info.xml' path='info/type[@name="AggregateTimeSpan"]/*' />
///
public static IEnumerable<Quote> Aggregate<TQuote>(
this IEnumerable<TQuote> quotes,
TimeSpan timeSpan)
where TQuote : IQuote
{

// handle no quotes scenario
if (quotes == null || !quotes.Any())
{
return new List<Quote>();
}

// parameter validation
TimeSpan newPeriod = newSize.ToTimeSpan();

if (newPeriod == TimeSpan.Zero)
if (timeSpan <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(newSize), newSize,
"Historical quotes Aggregation must use a New Size value of at least " +
"one minute and not more than one week.");
throw new ArgumentOutOfRangeException(nameof(timeSpan), timeSpan,
"Historical quotes Aggregation must use a new size value " +
"that is greater than zero (0).");
}

// return aggregation
return quotes
.OrderBy(x => x.Date)
.GroupBy(x => x.Date.RoundDown(newPeriod))
.GroupBy(x => x.Date.RoundDown(timeSpan))
.Select(x => new Quote
{
Date = x.Key,
Expand Down
15 changes: 14 additions & 1 deletion src/_common/Quotes/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,17 @@
<returns>Time series of historical quote values.</returns>
<exception cref="ArgumentOutOfRangeException">Invalid parameter value provided.</exception>
</type>
</info>
<type name="AggregateTimeSpan">
<summary>
Converts historical quotes into larger bar sizes.
See
<see href="https://daveskender.github.io/Stock.Indicators/docs/UTILITIES.html#resize-quote-history">documentation</see>
for more information.
</summary>
<typeparam name="TQuote">Configurable Quote type. See Guide for more information.</typeparam>
<param name="quotes">Historical price quotes.</param>
<param name="timeSpan">TimeSpan representing the new bar size.</param>
<returns>Time series of historical quote values.</returns>
<exception cref="ArgumentOutOfRangeException">Invalid parameter value provided.</exception>
</type>
</info>
30 changes: 30 additions & 0 deletions tests/external/Test.PublicClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,36 @@ public void CustomQuoteAggregate()
Assert.AreEqual(369.04m, r19.Low);
}

[TestMethod]
public void CustomQuoteAggregateTimeSpan()
{
List<MyGenericQuote> myGenericHistory = TestData.GetIntraday()
.Select(x => new MyGenericQuote
{
CloseDate = x.Date,
Open = x.Open,
High = x.High,
Low = x.Low,
CloseValue = x.Close,
Volume = x.Volume,
MyOtherProperty = 123456
})
.ToList();

List<Quote> quotesList = myGenericHistory
.Aggregate(TimeSpan.FromHours(2))
.ToList();

// assertions

// proper quantities
// should always be the same number of results as there is quotes
Assert.AreEqual(20, quotesList.Count);

// sample values
Quote r19 = quotesList[19];
Assert.AreEqual(369.04m, r19.Low);
}

[TestMethod]
public void DerivedIndicatorClass()
Expand Down
2 changes: 1 addition & 1 deletion tests/indicators/_common/Test.Functions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Skender.Stock.Indicators;
Expand Down
47 changes: 46 additions & 1 deletion tests/indicators/_common/Test.QuoteHistory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -158,6 +158,51 @@ public void Aggregate()
Assert.IsFalse(noResults.Any());
}

[TestMethod]
public void AggregateTimeSpan()
{
IEnumerable<Quote> quotes = TestData.GetIntraday();

// aggregate
List<Quote> results = quotes.Aggregate(TimeSpan.FromMinutes(15))
.ToList();

// assertions

// should always be the same number of results as there is quotes
Assert.AreEqual(108, results.Count);

// sample values
Quote r0 = results[0];
Assert.AreEqual(DateTime.Parse("2020-12-15 09:30", EnglishCulture), r0.Date);
Assert.AreEqual(367.40m, r0.Open);
Assert.AreEqual(367.775m, r0.High);
Assert.AreEqual(367.02m, r0.Low);
Assert.AreEqual(367.24m, r0.Close);
Assert.AreEqual(2401786m, r0.Volume);

Quote r1 = results[1];
Assert.AreEqual(DateTime.Parse("2020-12-15 09:45", EnglishCulture), r1.Date);
Assert.AreEqual(367.25m, r1.Open);
Assert.AreEqual(367.44m, r1.High);
Assert.AreEqual(366.69m, r1.Low);
Assert.AreEqual(366.86m, r1.Close);
Assert.AreEqual(1669983m, r1.Volume);

Quote r2 = results[2];
Assert.AreEqual(DateTime.Parse("2020-12-15 10:00", EnglishCulture), r2.Date);
Assert.AreEqual(366.85m, r2.Open);
Assert.AreEqual(367.17m, r2.High);
Assert.AreEqual(366.57m, r2.Low);
Assert.AreEqual(366.97m, r2.Close);
Assert.AreEqual(1396993m, r2.Volume);

// no history scenario
List<Quote> noQuotes = new();
IEnumerable<Quote> noResults = noQuotes.Aggregate(TimeSpan.FromDays(1));
Assert.IsFalse(noResults.Any());
}

[TestMethod]
public void ConvertToBasic()
{
Expand Down