Trady icon indicating copy to clipboard operation
Trady copied to clipboard

Minute to hour transformation: failure

Open pavlexander opened this issue 4 years ago • 3 comments

Current version of trady (3.2.9?) does not do the minute to hour transformation.

First of all - following part of code in IOhlcvDataExtension was changed previously:

        var tempCandles = new List<IOhlcv>();
        for (int i = 0; i < orderedCandles.Count; i++)
        {
            var indexTime = orderedCandles[i].DateTime;
            if (indexTime >= periodEndTime)
            {
                periodStartTime = indexTime; // previously: periodEndTime
                periodEndTime = periodInstance.NextTimestamp(periodStartTime);

                AddComputedCandleToOutput(outputCandles, tempCandles);
                tempCandles = new List<IOhlcv>();
            }

            tempCandles.Add(orderedCandles[i]);
        }

The code with the old periodEndTime somehow works better :) After transformation you actually get smaller amount of candles. With indexTime you get the same exactly amount of candles as you had before the transformation. This is issue 1.

So, the problem I am describing next - is actually using the old version of code (with periodStartTime = periodEndTime). You can skip it, if issue 1 is not an issue but a feature?:

        string dataString = File.ReadAllText("data.txt");
        var data = JsonSerializer.Deserialize<List<Candle>>(dataString);
        var transformedTradyCandles = data.Transform<PerMinute, Hourly>();

data.txt p.s. In order for deserialization to work - you have to have argument-less Candle constructor.

The data is that I passing to transformation is in time frame: 8:40 - 10:02, total of 83 records.

Expected result - 3 candles:

  • at 9:00 (aggregated 8:40 - 9:00)
  • at 10:00 (aggregated 9:01 - 10:00)
  • at 11:00 (aggregated 10:01 - 10:03)

Actual result - 4 candles, wrong time components:

  • 8:40
  • 8:41
  • 9:00
  • 10:00

I have not actually compared the amount of Volume. But from the looks of it - candles already have incorrect time component.. This is issue 2

So, as I mentioned, you can skip issue 2 if issue 1 is the one that needs fixing. In either way - you can use the data that I have provided and the sample code to verify either of those 2 issues.

using VS 2019, Current version of trady (3.2.9?, i.e. master branch), .Net core 3.1

pavlexander avatar Apr 14 '20 12:04 pavlexander

After some digging through the code I see at least 1 issue that could lead to above problems. In IOhlcvDataExtension:

        var periodStartTime = orderedCandles[0].DateTime;
        var periodEndTime = periodInstance.NextTimestamp(periodStartTime);

The date that is a start date is in format: {12/2/2017 8:40:00 PM +00:00} And the end date is: {12/2/2017 9:00:00 PM +02:00} According to how I understand it - the framework should have just added 1 period to start datetime, i.e. 1 hour. But in addition to adding 1 hour + it also changed the timezone.

EDIT: it seems like that after getting the DateTime from DateTimeOffset - the date comes with unspecified kind. So afterwards after the transformations - the Trady at some points adds +2 hours to the offset. I have fixed the above issue forcefully setting the kind to Utc:

    protected override DateTimeOffset ComputeTimestampByCorrectedPeriodCount(DateTimeOffset dateTime, int correctedPeriodCount)
    {
        var sth = TimeSpan.FromSeconds(NumberOfSecond);
        var sth2 = DateTime.SpecifyKind(dateTime.DateTime.Truncate(sth), DateTimeKind.Utc); // FIX here
        var sth3 = sth2.AddSeconds(correctedPeriodCount * NumberOfSecond);

        return sth3;
    }

but it did not resolve the problem completely, though I have to admit - after transformation number of candles decreased from 83 to just 3 - using the new code (periodStartTime = indexTime).

New actual result

  • 8:40
  • 9:00
  • 10:00

So, something is still not completely right :)

Also I just found this: https://docs.microsoft.com/en-us/dotnet/standard/datetime/converting-between-datetime-and-offset

The DateTime property is most commonly used to perform DateTimeOffset to DateTime conversion. However, it returns a DateTime value whose Kind property is Unspecified, as the following example illustrates.

This means that any information about the DateTimeOffset value's relationship to UTC is lost by the conversion when the DateTime property is used. This affects DateTimeOffset values that correspond to UTC time or to the system's local time because the DateTime structure reflects only those two time zones in its Kind property.

To preserve as much time zone information as possible when converting a DateTimeOffset to a DateTime value, you can use the DateTimeOffset.UtcDateTime and DateTimeOffset.LocalDateTime properties.

so alternative solution (and a better one) would be to use DateTimeOffset.UtcDateTime, for example. You have to evaluate if this change also affects other parts of the Trady framework.

pavlexander avatar Apr 14 '20 13:04 pavlexander

Alright, a question:

  • Is the a reason why after the transformation - we take the first candle date-time for the resulting candle datetime? Isn't a candle - a summary of the previous period? So in that sense - candle date-tome is always the latest datetime, not the datetime of the first candle... In either case - I don't think that earliest candle datetime shall be used. It's either the period start or the end :) Convince me otherwise.

With that idea in mind - I have applied some fixes to the code:

    public static IReadOnlyList<IOhlcv> Transform<TSourcePeriod, TTargetPeriod>(this                 IEnumerable<IOhlcv> candles)
        where TSourcePeriod : IPeriod
        where TTargetPeriod : IPeriod
    {
        // ....

        DateTimeOffset periodStartTime = orderedCandles[0].DateTime;
        DateTimeOffset periodEndTime = periodInstance.NextTimestamp(periodStartTime);

        var tempCandles = new List<IOhlcv>();
        for (int i = 0; i < orderedCandles.Count; i++)
        {
            var indexTime = orderedCandles[i].DateTime;
            if (indexTime > periodEndTime) // FIX. make end period inclusive! (old: >=)
            {
                // CORRECTED order
                AddComputedCandleToOutput(outputCandles, tempCandles, periodEndTime); // CORRECTED
                tempCandles = new List<IOhlcv>();

                periodStartTime = indexTime;
                periodEndTime = periodInstance.NextTimestamp(periodStartTime);
            }

            tempCandles.Add(orderedCandles[i]);
        }

        if (tempCandles.Any())
            AddComputedCandleToOutput(outputCandles, tempCandles, periodEndTime); // CORRECTED

        return outputCandles;
    }

and

    private static void AddComputedCandleToOutput(List<IOhlcv> outputCandles, List<IOhlcv> tempCandles, DateTimeOffset periodEndTime) // CORRECTED
    {
        var computedCandle = ComputeCandles(tempCandles, periodEndTime);
        if (computedCandle != null)
            outputCandles.Add(computedCandle);
    }

and

    private static IOhlcv ComputeCandles(IEnumerable<IOhlcv> candles, DateTimeOffset periodEndTime) // CORRECTED
    {
        if (!candles.Any())
            return null;

        var dateTime = periodEndTime.UtcDateTime; // CORRECTED !!!
        var open = candles.First().Open;
        var high = candles.Max(stick => stick.High);
        var low = candles.Min(stick => stick.Low);
        var close = candles.Last().Close;
        var volume = candles.Sum(stick => stick.Volume);
        return new Candle(dateTime, open, high, low, close, volume);
    }

This, plus the fix applied in previous response:

    protected override DateTimeOffset ComputeTimestampByCorrectedPeriodCount(DateTimeOffset dateTime, int correctedPeriodCount)
    {
        var sth = TimeSpan.FromSeconds(NumberOfSecond);
        var sth2 = dateTime.UtcDateTime.Truncate(sth); // FIX here
        var sth3 = sth2.AddSeconds(correctedPeriodCount * NumberOfSecond);

        return sth3;
    }

New actual result:

  • at 9:00 (aggregated 8:40 - 9:00)
  • at 10:00 (aggregated 9:01 - 10:00)
  • at 11:00 (aggregated 10:01 - 10:03)

sorry for spam. I could have made it a pull request as well. Just interested in your opinion on this topic before any commits..

EDIT: Also, using ElementAt without proper optimizations is many times slower than accessing element by index. If you could change this aspect of Trady - you could increase it's performance tenfold. I had to fix following method:

    private static bool IsTimeframesValid<TPeriod>(IEnumerable<IOhlcv> candles, out IOhlcv err)
        where TPeriod : IPeriod
    {
        var candlesList = candles
            .ToList(); // FIX this

        var periodInstance = Activator.CreateInstance<TPeriod>();
        err = default;
        var offset = candlesList.Any() ? candlesList.First().DateTime.Offset.Hours : 0;
        for (int i = 0; i < candlesList.Count() - 1; i++)
        {
            var chandle = candlesList[i];

            var nextTime = periodInstance.NextTimestamp(chandle.DateTime);
            var candleEndTime = new DateTimeOffset(nextTime.Date, TimeSpan.FromHours(offset));
            if (candleEndTime > candlesList[i + 1].DateTime)
            {
                err = chandle;
                return false;
            }
        }
        return true;
    }

In my case I have 1_232_239 minute ticks. Waited for 1 minute for the transformation to finish on 8700k 4 core CPU... After the fix it takes a second at most. I have not inspected the code, but I suppose that this is not the only place where you are using this approach..

pavlexander avatar Apr 14 '20 13:04 pavlexander

Tested all, seems to work well nice job. Will probably need a thorough test but also fixes my issue you tagged above. Your solution is much nicer than the one I hacked together meanwhile haha

JeremyCrookshank avatar May 09 '20 09:05 JeremyCrookshank