490 lines
16 KiB
C#
490 lines
16 KiB
C#
using FluentAssertions;
|
|
using Managing.Domain.Candles;
|
|
using Xunit;
|
|
using static Managing.Common.Enums;
|
|
|
|
namespace Managing.Domain.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for CandleHelpers static utility class.
|
|
/// Covers time calculations, intervals, grain keys, and candle boundary logic.
|
|
/// Critical for ensuring accurate candle fetching, bot synchronization, and backtest timing.
|
|
/// </summary>
|
|
public class CandleHelpersTests
|
|
{
|
|
#region GetBaseIntervalInSeconds Tests
|
|
|
|
[Theory]
|
|
[InlineData(Timeframe.FiveMinutes, 300)]
|
|
[InlineData(Timeframe.FifteenMinutes, 900)]
|
|
[InlineData(Timeframe.ThirtyMinutes, 1800)]
|
|
[InlineData(Timeframe.OneHour, 3600)]
|
|
[InlineData(Timeframe.FourHour, 14400)]
|
|
[InlineData(Timeframe.OneDay, 86400)]
|
|
public void GetBaseIntervalInSeconds_WithValidTimeframe_ReturnsCorrectSeconds(Timeframe timeframe, int expectedSeconds)
|
|
{
|
|
// Act
|
|
var result = CandleHelpers.GetBaseIntervalInSeconds(timeframe);
|
|
|
|
// Assert
|
|
result.Should().Be(expectedSeconds);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetUnixInterval Tests
|
|
|
|
[Theory]
|
|
[InlineData(Timeframe.FiveMinutes, 300)]
|
|
[InlineData(Timeframe.FifteenMinutes, 900)]
|
|
[InlineData(Timeframe.OneHour, 3600)]
|
|
[InlineData(Timeframe.FourHour, 14400)]
|
|
[InlineData(Timeframe.OneDay, 86400)]
|
|
public void GetUnixInterval_WithValidTimeframe_ReturnsCorrectInterval(Timeframe timeframe, int expectedInterval)
|
|
{
|
|
// Act
|
|
var result = timeframe.GetUnixInterval();
|
|
|
|
// Assert
|
|
result.Should().Be(expectedInterval);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetUnixInterval_WithThirtyMinutes_ThrowsNotImplementedException()
|
|
{
|
|
// Act
|
|
Action act = () => Timeframe.ThirtyMinutes.GetUnixInterval();
|
|
|
|
// Assert
|
|
act.Should().Throw<NotImplementedException>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetIntervalInMinutes Tests
|
|
|
|
[Theory]
|
|
[InlineData(Timeframe.FiveMinutes, 1.0)] // 300 / 5 / 60 = 1 minute
|
|
[InlineData(Timeframe.FifteenMinutes, 3.0)] // 900 / 5 / 60 = 3 minutes
|
|
[InlineData(Timeframe.ThirtyMinutes, 6.0)] // 1800 / 5 / 60 = 6 minutes
|
|
[InlineData(Timeframe.OneHour, 12.0)] // 3600 / 5 / 60 = 12 minutes
|
|
[InlineData(Timeframe.FourHour, 48.0)] // 14400 / 5 / 60 = 48 minutes
|
|
[InlineData(Timeframe.OneDay, 288.0)] // 86400 / 5 / 60 = 288 minutes
|
|
public void GetIntervalInMinutes_WithValidTimeframe_ReturnsOneFifthOfCandleDuration(Timeframe timeframe, double expectedMinutes)
|
|
{
|
|
// Act
|
|
var result = CandleHelpers.GetIntervalInMinutes(timeframe);
|
|
|
|
// Assert
|
|
result.Should().Be(expectedMinutes);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetIntervalFromTimeframe Tests
|
|
|
|
[Theory]
|
|
[InlineData(Timeframe.FiveMinutes, 60000)] // 300 / 5 * 1000 = 60000 ms
|
|
[InlineData(Timeframe.FifteenMinutes, 180000)] // 900 / 5 * 1000 = 180000 ms
|
|
[InlineData(Timeframe.OneHour, 720000)] // 3600 / 5 * 1000 = 720000 ms
|
|
public void GetIntervalFromTimeframe_ReturnsMillisecondsForOneFifthOfCandleDuration(Timeframe timeframe, int expectedMilliseconds)
|
|
{
|
|
// Act
|
|
var result = CandleHelpers.GetIntervalFromTimeframe(timeframe);
|
|
|
|
// Assert
|
|
result.Should().Be(expectedMilliseconds);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetMinimalDays Tests
|
|
|
|
[Theory]
|
|
[InlineData(Timeframe.FiveMinutes, -1)]
|
|
[InlineData(Timeframe.FifteenMinutes, -5)]
|
|
[InlineData(Timeframe.ThirtyMinutes, -10)]
|
|
[InlineData(Timeframe.OneHour, -30)]
|
|
[InlineData(Timeframe.FourHour, -60)]
|
|
[InlineData(Timeframe.OneDay, -360)]
|
|
public void GetMinimalDays_WithValidTimeframe_ReturnsCorrectNegativeDays(Timeframe timeframe, double expectedDays)
|
|
{
|
|
// Act
|
|
var result = CandleHelpers.GetMinimalDays(timeframe);
|
|
|
|
// Assert
|
|
result.Should().Be(expectedDays);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetBotPreloadSinceFromTimeframe Tests
|
|
|
|
[Fact]
|
|
public void GetBotPreloadSinceFromTimeframe_WithFiveMinutes_ReturnsOneDayAgo()
|
|
{
|
|
// Arrange
|
|
var before = DateTime.UtcNow.AddDays(-1);
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetBotPreloadSinceFromTimeframe(Timeframe.FiveMinutes);
|
|
|
|
// Assert
|
|
var after = DateTime.UtcNow.AddDays(-1);
|
|
result.Should().BeOnOrAfter(before).And.BeOnOrBefore(after);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetBotPreloadSinceFromTimeframe_WithOneHour_Returns30DaysAgo()
|
|
{
|
|
// Arrange
|
|
var before = DateTime.UtcNow.AddDays(-30);
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetBotPreloadSinceFromTimeframe(Timeframe.OneHour);
|
|
|
|
// Assert
|
|
var after = DateTime.UtcNow.AddDays(-30);
|
|
result.Should().BeOnOrAfter(before).And.BeOnOrBefore(after);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetBotPreloadSinceFromTimeframe_WithOneDay_Returns360DaysAgo()
|
|
{
|
|
// Arrange
|
|
var before = DateTime.UtcNow.AddDays(-360);
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetBotPreloadSinceFromTimeframe(Timeframe.OneDay);
|
|
|
|
// Assert
|
|
var after = DateTime.UtcNow.AddDays(-360);
|
|
result.Should().BeOnOrAfter(before).And.BeOnOrBefore(after);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetPreloadSinceFromTimeframe Tests
|
|
|
|
[Fact]
|
|
public void GetPreloadSinceFromTimeframe_WithFiveMinutes_ReturnsOneDayAgo()
|
|
{
|
|
// Arrange
|
|
var expectedDays = -1;
|
|
var before = DateTime.UtcNow.AddDays(expectedDays);
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetPreloadSinceFromTimeframe(Timeframe.FiveMinutes);
|
|
|
|
// Assert
|
|
var after = DateTime.UtcNow.AddDays(expectedDays);
|
|
result.Should().BeOnOrAfter(before).And.BeOnOrBefore(after);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetPreloadSinceFromTimeframe_UsesGetMinimalDays()
|
|
{
|
|
// Arrange
|
|
var timeframe = Timeframe.OneHour;
|
|
var minimalDays = CandleHelpers.GetMinimalDays(timeframe);
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetPreloadSinceFromTimeframe(timeframe);
|
|
|
|
// Assert
|
|
var expectedBefore = DateTime.UtcNow.AddDays(minimalDays);
|
|
var expectedAfter = DateTime.UtcNow.AddDays(minimalDays);
|
|
result.Should().BeOnOrAfter(expectedBefore.AddSeconds(-1)).And.BeOnOrBefore(expectedAfter.AddSeconds(1));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetCandleStoreGrainKey & ParseCandleStoreGrainKey Tests
|
|
|
|
[Theory]
|
|
[InlineData(TradingExchanges.Binance, Ticker.BTC, Timeframe.OneHour, "Binance-BTC-OneHour")]
|
|
[InlineData(TradingExchanges.Kraken, Ticker.ETH, Timeframe.FiveMinutes, "Kraken-ETH-FiveMinutes")]
|
|
[InlineData(TradingExchanges.GmxV2, Ticker.SOL, Timeframe.OneDay, "GmxV2-SOL-OneDay")]
|
|
public void GetCandleStoreGrainKey_WithValidParameters_ReturnsCorrectKey(
|
|
TradingExchanges exchange, Ticker ticker, Timeframe timeframe, string expectedKey)
|
|
{
|
|
// Act
|
|
var result = CandleHelpers.GetCandleStoreGrainKey(exchange, ticker, timeframe);
|
|
|
|
// Assert
|
|
result.Should().Be(expectedKey);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Binance-BTC-OneHour", TradingExchanges.Binance, Ticker.BTC, Timeframe.OneHour)]
|
|
[InlineData("Kraken-ETH-FiveMinutes", TradingExchanges.Kraken, Ticker.ETH, Timeframe.FiveMinutes)]
|
|
[InlineData("GmxV2-SOL-OneDay", TradingExchanges.GmxV2, Ticker.SOL, Timeframe.OneDay)]
|
|
public void ParseCandleStoreGrainKey_WithValidKey_ReturnsCorrectComponents(
|
|
string grainKey, TradingExchanges expectedExchange, Ticker expectedTicker, Timeframe expectedTimeframe)
|
|
{
|
|
// Act
|
|
var (exchange, ticker, timeframe) = CandleHelpers.ParseCandleStoreGrainKey(grainKey);
|
|
|
|
// Assert
|
|
exchange.Should().Be(expectedExchange);
|
|
ticker.Should().Be(expectedTicker);
|
|
timeframe.Should().Be(expectedTimeframe);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetCandleStoreGrainKey_RoundTrip_PreservesOriginalValues()
|
|
{
|
|
// Arrange
|
|
var originalExchange = TradingExchanges.Binance;
|
|
var originalTicker = Ticker.BTC;
|
|
var originalTimeframe = Timeframe.FifteenMinutes;
|
|
|
|
// Act
|
|
var grainKey = CandleHelpers.GetCandleStoreGrainKey(originalExchange, originalTicker, originalTimeframe);
|
|
var (parsedExchange, parsedTicker, parsedTimeframe) = CandleHelpers.ParseCandleStoreGrainKey(grainKey);
|
|
|
|
// Assert
|
|
parsedExchange.Should().Be(originalExchange);
|
|
parsedTicker.Should().Be(originalTicker);
|
|
parsedTimeframe.Should().Be(originalTimeframe);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetNextExpectedCandleTime Tests
|
|
|
|
[Fact]
|
|
public void GetNextExpectedCandleTime_WithFiveMinutes_AlignsToFiveMinuteBoundary()
|
|
{
|
|
// Arrange - Use a specific time to ensure predictable results
|
|
var now = new DateTime(2024, 1, 1, 12, 3, 30, DateTimeKind.Utc); // 12:03:30
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetNextExpectedCandleTime(Timeframe.FiveMinutes, now);
|
|
|
|
// Assert
|
|
// Next 5-minute boundary is 12:05:00, minus 1 second = 12:04:59
|
|
var expected = new DateTime(2024, 1, 1, 12, 4, 59, DateTimeKind.Utc);
|
|
result.Should().Be(expected);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetNextExpectedCandleTime_WithFifteenMinutes_AlignsToFifteenMinuteBoundary()
|
|
{
|
|
// Arrange
|
|
var now = new DateTime(2024, 1, 1, 12, 8, 0, DateTimeKind.Utc); // 12:08:00
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetNextExpectedCandleTime(Timeframe.FifteenMinutes, now);
|
|
|
|
// Assert
|
|
// Next 15-minute boundary is 12:15:00, minus 1 second = 12:14:59
|
|
var expected = new DateTime(2024, 1, 1, 12, 14, 59, DateTimeKind.Utc);
|
|
result.Should().Be(expected);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetNextExpectedCandleTime_WithOneHour_AlignsToHourBoundary()
|
|
{
|
|
// Arrange
|
|
var now = new DateTime(2024, 1, 1, 12, 30, 0, DateTimeKind.Utc); // 12:30:00
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetNextExpectedCandleTime(Timeframe.OneHour, now);
|
|
|
|
// Assert
|
|
// Next hour boundary is 13:00:00, minus 1 second = 12:59:59
|
|
var expected = new DateTime(2024, 1, 1, 12, 59, 59, DateTimeKind.Utc);
|
|
result.Should().Be(expected);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetNextExpectedCandleTime_WithFourHour_AlignsToFourHourBoundary()
|
|
{
|
|
// Arrange
|
|
var now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc); // 10:00:00
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetNextExpectedCandleTime(Timeframe.FourHour, now);
|
|
|
|
// Assert
|
|
// Next 4-hour boundary is 12:00:00, minus 1 second = 11:59:59
|
|
var expected = new DateTime(2024, 1, 1, 11, 59, 59, DateTimeKind.Utc);
|
|
result.Should().Be(expected);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetNextExpectedCandleTime_WithOneDay_AlignsToDayBoundary()
|
|
{
|
|
// Arrange
|
|
var now = new DateTime(2024, 1, 1, 15, 0, 0, DateTimeKind.Utc); // Jan 1, 15:00
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetNextExpectedCandleTime(Timeframe.OneDay, now);
|
|
|
|
// Assert
|
|
// Next day boundary is Jan 2 00:00:00, minus 1 second = Jan 1 23:59:59
|
|
var expected = new DateTime(2024, 1, 1, 23, 59, 59, DateTimeKind.Utc);
|
|
result.Should().Be(expected);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetNextExpectedCandleTime_WithoutNowParameter_UsesCurrentTime()
|
|
{
|
|
// Act
|
|
var result = CandleHelpers.GetNextExpectedCandleTime(Timeframe.FiveMinutes);
|
|
|
|
// Assert
|
|
result.Should().BeAfter(DateTime.UtcNow.AddMinutes(-1));
|
|
result.Should().BeBefore(DateTime.UtcNow.AddMinutes(10));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetDueTimeForTimeframe Tests
|
|
|
|
[Fact]
|
|
public void GetDueTimeForTimeframe_WithFiveMinutes_ReturnsTimeToNextBoundary()
|
|
{
|
|
// Arrange - Use a specific time
|
|
var now = new DateTime(2024, 1, 1, 12, 3, 30, DateTimeKind.Utc); // 12:03:30
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetDueTimeForTimeframe(Timeframe.FiveMinutes, now);
|
|
|
|
// Assert
|
|
// Next 5-minute boundary is 12:05:00, plus 1 second = 12:05:01
|
|
// Time from 12:03:30 to 12:05:01 = 1 minute 31 seconds
|
|
result.TotalSeconds.Should().BeApproximately(91, 1);
|
|
result.Should().BePositive();
|
|
}
|
|
|
|
[Fact]
|
|
public void GetDueTimeForTimeframe_WithOneHour_ReturnsTimeToNextHourBoundary()
|
|
{
|
|
// Arrange
|
|
var now = new DateTime(2024, 1, 1, 12, 30, 0, DateTimeKind.Utc); // 12:30:00
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetDueTimeForTimeframe(Timeframe.OneHour, now);
|
|
|
|
// Assert
|
|
// Next hour boundary is 13:00:00, plus 1 second = 13:00:01
|
|
// Time from 12:30:00 to 13:00:01 = 30 minutes 1 second
|
|
result.TotalMinutes.Should().BeApproximately(30, 1);
|
|
result.Should().BePositive();
|
|
}
|
|
|
|
[Fact]
|
|
public void GetDueTimeForTimeframe_ResultIsAlwaysPositive()
|
|
{
|
|
// Arrange - Test at various times
|
|
var testTimes = new[]
|
|
{
|
|
new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
|
new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc),
|
|
new DateTime(2024, 1, 1, 23, 59, 0, DateTimeKind.Utc)
|
|
};
|
|
|
|
foreach (var testTime in testTimes)
|
|
{
|
|
// Act
|
|
var result = CandleHelpers.GetDueTimeForTimeframe(Timeframe.FifteenMinutes, testTime);
|
|
|
|
// Assert
|
|
result.Should().BePositive($"time {testTime} should produce positive due time");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void GetDueTimeForTimeframe_WithFourHour_CalculatesCorrectDueTime()
|
|
{
|
|
// Arrange
|
|
var now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc); // 10:00:00
|
|
|
|
// Act
|
|
var result = CandleHelpers.GetDueTimeForTimeframe(Timeframe.FourHour, now);
|
|
|
|
// Assert
|
|
// Next 4-hour boundary is 12:00:00, plus 1 second = 12:00:01
|
|
// Time from 10:00:00 to 12:00:01 = 2 hours 1 second
|
|
result.TotalHours.Should().BeApproximately(2, 0.1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Edge Cases and Integration Tests
|
|
|
|
[Fact]
|
|
public void GetBaseIntervalInSeconds_AndGetUnixInterval_ReturnSameValuesForSupportedTimeframes()
|
|
{
|
|
// Arrange
|
|
var supportedTimeframes = new[]
|
|
{
|
|
Timeframe.FiveMinutes,
|
|
Timeframe.FifteenMinutes,
|
|
Timeframe.OneHour,
|
|
Timeframe.FourHour,
|
|
Timeframe.OneDay
|
|
};
|
|
|
|
foreach (var timeframe in supportedTimeframes)
|
|
{
|
|
// Act
|
|
var baseInterval = CandleHelpers.GetBaseIntervalInSeconds(timeframe);
|
|
var unixInterval = timeframe.GetUnixInterval();
|
|
|
|
// Assert
|
|
baseInterval.Should().Be(unixInterval, $"{timeframe} should return consistent values");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void GetIntervalFromTimeframe_ReturnsConsistentlyOneFifthOfBaseInterval()
|
|
{
|
|
// Arrange
|
|
var timeframes = new[]
|
|
{
|
|
Timeframe.FiveMinutes,
|
|
Timeframe.FifteenMinutes,
|
|
Timeframe.ThirtyMinutes,
|
|
Timeframe.OneHour,
|
|
Timeframe.FourHour,
|
|
Timeframe.OneDay
|
|
};
|
|
|
|
foreach (var timeframe in timeframes)
|
|
{
|
|
// Act
|
|
var intervalMs = CandleHelpers.GetIntervalFromTimeframe(timeframe);
|
|
var baseIntervalSeconds = CandleHelpers.GetBaseIntervalInSeconds(timeframe);
|
|
var expectedIntervalMs = (baseIntervalSeconds / 5) * 1000;
|
|
|
|
// Assert
|
|
intervalMs.Should().Be(expectedIntervalMs, $"{timeframe} should be 1/5th of base interval");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TimeCalculationMethods_AreConsistentWithEachOther()
|
|
{
|
|
// Arrange
|
|
var timeframe = Timeframe.FifteenMinutes;
|
|
|
|
// Act
|
|
var baseSeconds = CandleHelpers.GetBaseIntervalInSeconds(timeframe);
|
|
var intervalMinutes = CandleHelpers.GetIntervalInMinutes(timeframe);
|
|
var intervalMs = CandleHelpers.GetIntervalFromTimeframe(timeframe);
|
|
|
|
// Assert
|
|
// intervalMinutes should be baseSeconds / 5 / 60
|
|
intervalMinutes.Should().Be((double)baseSeconds / 5 / 60);
|
|
|
|
// intervalMs should be baseSeconds / 5 * 1000
|
|
intervalMs.Should().Be((baseSeconds / 5) * 1000);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|