/// <summary> /// Returns the max of the feerates calculated with a 60% /// threshold required at target / 2, an 85% threshold required at target and a /// 95% threshold required at 2 * target.Each calculation is performed at the /// shortest time horizon which tracks the required target.Conservative /// estimates, however, required the 95% threshold at 2 * target be met for any /// longer time horizons also. /// </summary> public FeeRate EstimateSmartFee(int confTarget, FeeCalculation feeCalc, bool conservative) { lock (this.lockObject) { if (feeCalc != null) { feeCalc.DesiredTarget = confTarget; feeCalc.ReturnedTarget = confTarget; } double median = -1; EstimationResult tempResult = new EstimationResult(); // Return failure if trying to analyze a target we're not tracking if (confTarget <= 0 || confTarget > this.longStats.GetMaxConfirms()) { return(new FeeRate(0)); // error condition } // It's not possible to get reasonable estimates for confTarget of 1 if (confTarget == 1) { confTarget = 2; } int maxUsableEstimate = MaxUsableEstimate(); if (confTarget > maxUsableEstimate && maxUsableEstimate > 1) { confTarget = maxUsableEstimate; } if (feeCalc != null) { feeCalc.ReturnedTarget = confTarget; } if (confTarget <= 1) { return(new FeeRate(0)); // error condition } Guard.Assert(confTarget > 0); //estimateCombinedFee and estimateConservativeFee take unsigned ints // true is passed to estimateCombined fee for target/2 and target so // that we check the max confirms for shorter time horizons as well. // This is necessary to preserve monotonically increasing estimates. // For non-conservative estimates we do the same thing for 2*target, but // for conservative estimates we want to skip these shorter horizons // checks for 2*target because we are taking the max over all time // horizons so we already have monotonically increasing estimates and // the purpose of conservative estimates is not to let short term // fluctuations lower our estimates by too much. double halfEst = EstimateCombinedFee(confTarget / 2, HalfSuccessPct, true, tempResult); if (feeCalc != null) { feeCalc.Estimation = tempResult; feeCalc.Reason = FeeReason.HalfEstimate; } median = halfEst; double actualEst = EstimateCombinedFee(confTarget, SuccessPct, true, tempResult); if (actualEst > median) { median = actualEst; if (feeCalc != null) { feeCalc.Estimation = tempResult; feeCalc.Reason = FeeReason.FullEstimate; } } double doubleEst = EstimateCombinedFee(2 * confTarget, DoubleSuccessPct, !conservative, tempResult); if (doubleEst > median) { median = doubleEst; if (feeCalc != null) { feeCalc.Estimation = tempResult; feeCalc.Reason = FeeReason.DoubleEstimate; } } if (conservative || median == -1) { double consEst = EstimateConservativeFee(2 * confTarget, tempResult); if (consEst > median) { median = consEst; if (feeCalc != null) { feeCalc.Estimation = tempResult; feeCalc.Reason = FeeReason.Coservative; } } } if (median < 0) { return(new FeeRate(0)); // error condition } return(new FeeRate(Convert.ToInt64(median))); } }
/// <summary> /// Calculate a feerate estimate. Find the lowest value bucket (or range of buckets /// to make sure we have enough data points) whose transactions still have sufficient likelihood /// of being confirmed within the target number of confirmations. /// </summary> /// <param name="confTarget">Target number of confirmations.</param> /// <param name="sufficientTxVal">Required average number of transactions per block in a bucket range.</param> /// <param name="successBreakPoint">The success probability we require.</param> /// <param name="requireGreater">Return the lowest feerate such that all higher values pass minSuccess OR return the highest feerate such that all lower values fail minSuccess.</param> /// <param name="nBlockHeight">The current block height.</param> /// <returns></returns> public double EstimateMedianVal(int confTarget, double sufficientTxVal, double successBreakPoint, bool requireGreater, int nBlockHeight, EstimationResult result) { // Counters for a bucket (or range of buckets) double nConf = 0; // Number of tx's confirmed within the confTarget double totalNum = 0; // Total number of tx's that were ever confirmed int extraNum = 0; // Number of tx's still in mempool for confTarget or longer double failNum = 0; // Number of tx's that were never confirmed but removed from the mempool after confTarget int periodTarget = (confTarget + this.scale - 1) / this.scale; int maxbucketindex = this.buckets.Count - 1; // requireGreater means we are looking for the lowest feerate such that all higher // values pass, so we start at maxbucketindex (highest feerate) and look at successively // smaller buckets until we reach failure. Otherwise, we are looking for the highest // feerate such that all lower values fail, and we go in the opposite direction. int startbucket = requireGreater ? maxbucketindex : 0; int step = requireGreater ? -1 : 1; // We'll combine buckets until we have enough samples. // The near and far variables will define the range we've combined // The best variables are the last range we saw which still had a high // enough confirmation rate to count as success. // The cur variables are the current range we're counting. int curNearBucket = startbucket; int bestNearBucket = startbucket; int curFarBucket = startbucket; int bestFarBucket = startbucket; bool foundAnswer = false; int bins = this.unconfTxs.Count; bool newBucketRange = true; bool passing = true; EstimatorBucket passBucket = new EstimatorBucket();; EstimatorBucket failBucket = new EstimatorBucket(); // Start counting from highest(default) or lowest feerate transactions for (int bucket = startbucket; bucket >= 0 && bucket <= maxbucketindex; bucket += step) { if (newBucketRange) { curNearBucket = bucket; newBucketRange = false; } curFarBucket = bucket; nConf += this.confAvg[periodTarget - 1][bucket]; totalNum += this.txCtAvg[bucket]; failNum += this.failAvg[periodTarget - 1][bucket]; for (int confct = confTarget; confct < this.GetMaxConfirms(); confct++) { extraNum += this.unconfTxs[Math.Abs(nBlockHeight - confct) % bins][bucket]; } extraNum += this.oldUnconfTxs[bucket]; // If we have enough transaction data points in this range of buckets, // we can test for success // (Only count the confirmed data points, so that each confirmation count // will be looking at the same amount of data and same bucket breaks) if (totalNum >= sufficientTxVal / (1 - this.decay)) { double curPct = nConf / (totalNum + failNum + extraNum); // Check to see if we are no longer getting confirmed at the success rate if ((requireGreater && curPct < successBreakPoint) || (!requireGreater && curPct > successBreakPoint)) { if (passing == true) { // First time we hit a failure record the failed bucket int failMinBucket = Math.Min(curNearBucket, curFarBucket); int failMaxBucket = Math.Max(curNearBucket, curFarBucket); failBucket.Start = failMinBucket > 0 ? this.buckets[failMinBucket - 1] : 0; failBucket.End = this.buckets[failMaxBucket]; failBucket.WithinTarget = nConf; failBucket.TotalConfirmed = totalNum; failBucket.InMempool = extraNum; failBucket.LeftMempool = failNum; passing = false; } continue; } // Otherwise update the cumulative stats, and the bucket variables // and reset the counters else { failBucket = new EstimatorBucket(); // Reset any failed bucket, currently passing foundAnswer = true; passing = true; passBucket.WithinTarget = nConf; nConf = 0; passBucket.TotalConfirmed = totalNum; totalNum = 0; passBucket.InMempool = extraNum; passBucket.LeftMempool = failNum; failNum = 0; extraNum = 0; bestNearBucket = curNearBucket; bestFarBucket = curFarBucket; newBucketRange = true; } } } double median = -1; double txSum = 0; // Calculate the "average" feerate of the best bucket range that met success conditions // Find the bucket with the median transaction and then report the average feerate from that bucket // This is a compromise between finding the median which we can't since we don't save all tx's // and reporting the average which is less accurate int minBucket = Math.Min(bestNearBucket, bestFarBucket); int maxBucket = Math.Max(bestNearBucket, bestFarBucket); for (int j = minBucket; j <= maxBucket; j++) { txSum += this.txCtAvg[j]; } if (foundAnswer && txSum != 0) { txSum = txSum / 2; for (int j = minBucket; j <= maxBucket; j++) { if (this.txCtAvg[j] < txSum) { txSum -= this.txCtAvg[j]; } else { // we're in the right bucket median = this.avg[j] / this.txCtAvg[j]; break; } } passBucket.Start = minBucket > 0 ? this.buckets[minBucket - 1] : 0; passBucket.End = this.buckets[maxBucket]; } // If we were passing until we reached last few buckets with insufficient data, then report those as failed if (passing && !newBucketRange) { int failMinBucket = Math.Min(curNearBucket, curFarBucket); int failMaxBucket = Math.Max(curNearBucket, curFarBucket); failBucket.Start = failMinBucket > 0 ? this.buckets[failMinBucket - 1] : 0; failBucket.End = this.buckets[failMaxBucket]; failBucket.WithinTarget = nConf; failBucket.TotalConfirmed = totalNum; failBucket.InMempool = extraNum; failBucket.LeftMempool = failNum; } this.logger.LogInformation( $"FeeEst: {confTarget} {(requireGreater ? $">" : $"<")} " + $"{successBreakPoint} decay {this.decay} feerate: {median}" + $" from ({passBucket.Start} - {passBucket.End}" + $" {100 * passBucket.WithinTarget / (passBucket.TotalConfirmed + passBucket.InMempool + passBucket.LeftMempool)}" + $" {passBucket.WithinTarget}/({passBucket.TotalConfirmed}" + $" {passBucket.InMempool} mem {passBucket.LeftMempool} out) " + $"Fail: ({failBucket.Start} - {failBucket.End} " + $"{100 * failBucket.WithinTarget / (failBucket.TotalConfirmed + failBucket.InMempool + failBucket.LeftMempool)}" + $" {failBucket.WithinTarget}/({failBucket.TotalConfirmed}" + $" {failBucket.InMempool} mem {failBucket.LeftMempool} out)"); if (result != null) { result.Pass = passBucket; result.Fail = failBucket; result.Decay = this.decay; result.Scale = this.scale; } return(median); }
/// <summary> /// Return an estimate fee according to horizon /// </summary> /// <param name="confTarget">The desired number of confirmations to be included in a block</param> public FeeRate EstimateRawFee(int confTarget, double successThreshold, FeeEstimateHorizon horizon, EstimationResult result) { TxConfirmStats stats; double sufficientTxs = SufficientFeeTxs; switch (horizon) { case FeeEstimateHorizon.ShortHalfLife: { stats = this.shortStats; sufficientTxs = SufficientTxsShort; break; } case FeeEstimateHorizon.MedHalfLife: { stats = this.feeStats; break; } case FeeEstimateHorizon.LongHalfLife: { stats = this.longStats; break; } default: { throw new ArgumentException(nameof(horizon)); } } lock (this.lockObject) { // Return failure if trying to analyze a target we're not tracking if (confTarget <= 0 || confTarget > stats.GetMaxConfirms()) { return(new FeeRate(0)); } if (successThreshold > 1) { return(new FeeRate(0)); } double median = stats.EstimateMedianVal(confTarget, sufficientTxs, successThreshold, true, this.nBestSeenHeight, result); if (median < 0) { return(new FeeRate(0)); } return(new FeeRate(Convert.ToInt64(median))); } }