public static decimal CalculatePowerDifference(ApiStake stake, ApiFund fund, IReadOnlyList <ApiStakeEvent> events, ApiStakeEvent eventItem)
        {
            if (eventItem.Type == StakeEventType.Lockup)
            {
                return(CalculatePower(stake, fund, eventItem));
            }
            else
            {
                var approximateQuantity = eventItem.Release.Quantity + (eventItem.Release.FeeQuantity ?? decimal.Zero);

                var items = events
                            .Where(x => x.Type == StakeEventType.Lockup)
                            .ToList();

                var lockUp = items.Count > 0
                    ? items.Count == 1
                        ? items.Single()
                        : items
                             .OrderBy(x => x.ConfirmedAt)
                             .FirstOrDefault(e => Math.Abs(e.Lock.Quantity - approximateQuantity) <= Precision)
                    : throw new PermanentException($"No existing lockup data could be found for release event {eventItem.Hash}");

                return(-CalculatePower(stake, fund, lockUp));
            }
        }
 public static decimal CalculateEarnings(ApiStake stake, ApiFund fund, IReadOnlyList <ApiStakeEvent> events, DateTime toDate)
 {
     return(CalculateEarnings(
                stake,
                fund,
                events,
                (int)((toDate - StartDate).TotalHours * 2)));
 }
        private static decimal CalculatePower(ApiStake stake, ApiFund fund, ApiStakeEvent eventItem)
        {
            var timeModifier = stake.TimeMultipliers
                               .SingleOrDefault(tm =>
                                                tm.RangeMin <= eventItem.Lock.Duration.Days &&
                                                tm.RangeMax >= eventItem.Lock.Duration.Days)
                               ?.Multiplier ?? 1m;

            var fundModifier = stake.FundMultipliers.ContainsKey(fund.Token.Symbol)
                ? stake.FundMultipliers[fund.Token.Symbol]
                : 1m;

            return(eventItem.Lock.Quantity * timeModifier * fundModifier * fund.Nav.PricePerToken);
        }
        public static decimal CalculateEarnings(ApiStake stake, ApiFund fund, IReadOnlyList <ApiStakeEvent> events, int intervals)
        {
            var icap             = decimal.Zero;
            var stakingPower     = decimal.Zero;
            var icapDistribution = 10000m;
            var endDate          = StartDate.AddMinutes(IntervalMinutes * intervals);

            if (events.Any() && intervals > 0)
            {
                var eventQueue = new Queue <ApiStakeEvent>(events.OrderBy(x => x.ConfirmedAt));
                if (eventQueue.Count == 0)
                {
                    return(decimal.Zero);
                }
                else
                {
                    var eventItem = eventQueue.Dequeue();

                    do
                    {
                        var endItem = eventQueue.Count > 0
                            ? eventQueue.Dequeue()
                            : null;

                        stakingPower += CalculatePowerDifference(stake, fund, events, eventItem);

                        var relativePercentage = stakingPower / stake.Power.Power * 100;

                        var start = NormalizeDate(eventItem.ConfirmedAt);
                        var end   = endItem != null
                            ? NormalizeDate(endItem.ConfirmedAt)
                            : endDate;

                        var intervalsBeforeStart  = ((decimal)(start - StartDate).TotalHours) * 2m;
                        var chargeableIntervals   = Math.Max(intervals - intervalsBeforeStart, decimal.Zero);
                        var remainder             = intervalsBeforeStart % IntervalsPerWeek;
                        var depreciations         = (intervalsBeforeStart - remainder) / IntervalsPerWeek;
                        var intervalsInsideWindow = Math.Min(chargeableIntervals, IntervalsPerWeek - remainder);

                        icapDistribution = Math.Max(icapDistribution * (decimal)Math.Pow((double)DepreciationPercentage, (double)depreciations), MinDistribution);

                        // Add Remaining Ticks of this distribution
                        icap += (icapDistribution / IntervalsPerWeek) / 100 * relativePercentage * intervalsInsideWindow;

                        var ticks        = ((decimal)(end - start).TotalHours * 2m) - intervalsInsideWindow;
                        var endRemainder = ticks % IntervalsPerWeek;
                        var fullRounds   = (ticks - endRemainder) / IntervalsPerWeek;

                        icap += Enumerable
                                .Range(0, (int)fullRounds)
                                .Sum(_ =>
                        {
                            icapDistribution = Math.Max(icapDistribution * DepreciationPercentage, MinDistribution);

                            return(icapDistribution / 100 * relativePercentage);
                        });

                        if (endRemainder != default)
                        {
                            var nextIcapDistribution = Math.Max(icapDistribution * DepreciationPercentage, MinDistribution);

                            icap += nextIcapDistribution / IntervalsPerWeek / 100 * relativePercentage * endRemainder;
                        }

                        eventItem = endItem;
                    }while (eventQueue.Count > 0);
                }
            }

            return(Math.Max(icap, decimal.Zero));
        }