Rewrite estimateSmartFee
Change the logic of estimateSmartFee to check a 60% threshold at half the target, a 85% threshold at the target and a 95% threshold at double the target. Always check the shortest time horizon possible and ensure that estimates are monotonically decreasing. Add a conservative mode, which makes sure that the 95% threshold is also met at longer time horizons as well.
This commit is contained in:
parent
c7447ec303
commit
3810e976d6
6 changed files with 104 additions and 22 deletions
|
@ -658,31 +658,107 @@ CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThr
|
|||
return CFeeRate(median);
|
||||
}
|
||||
|
||||
CFeeRate CBlockPolicyEstimator::estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool) const
|
||||
|
||||
/** Return a fee estimate at the required successThreshold from the shortest
|
||||
* time horizon which tracks confirmations up to the desired target. If
|
||||
* checkShorterHorizon is requested, also allow short time horizon estimates
|
||||
* for a lower target to reduce the given answer */
|
||||
double CBlockPolicyEstimator::estimateCombinedFee(unsigned int confTarget, double successThreshold, bool checkShorterHorizon) const
|
||||
{
|
||||
double estimate = -1;
|
||||
if (confTarget >= 1 && confTarget <= longStats->GetMaxConfirms()) {
|
||||
// Find estimate from shortest time horizon possible
|
||||
if (confTarget <= shortStats->GetMaxConfirms()) { // short horizon
|
||||
estimate = shortStats->EstimateMedianVal(confTarget, SUFFICIENT_TXS_SHORT, successThreshold, true, nBestSeenHeight);
|
||||
}
|
||||
else if (confTarget <= feeStats->GetMaxConfirms()) { // medium horizon
|
||||
estimate = feeStats->EstimateMedianVal(confTarget, SUFFICIENT_FEETXS, successThreshold, true, nBestSeenHeight);
|
||||
}
|
||||
else { // long horizon
|
||||
estimate = longStats->EstimateMedianVal(confTarget, SUFFICIENT_FEETXS, successThreshold, true, nBestSeenHeight);
|
||||
}
|
||||
if (checkShorterHorizon) {
|
||||
// If a lower confTarget from a more recent horizon returns a lower answer use it.
|
||||
if (confTarget > feeStats->GetMaxConfirms()) {
|
||||
double medMax = feeStats->EstimateMedianVal(feeStats->GetMaxConfirms(), SUFFICIENT_FEETXS, successThreshold, true, nBestSeenHeight);
|
||||
if (medMax > 0 && (estimate == -1 || medMax < estimate))
|
||||
estimate = medMax;
|
||||
}
|
||||
if (confTarget > shortStats->GetMaxConfirms()) {
|
||||
double shortMax = shortStats->EstimateMedianVal(shortStats->GetMaxConfirms(), SUFFICIENT_TXS_SHORT, successThreshold, true, nBestSeenHeight);
|
||||
if (shortMax > 0 && (estimate == -1 || shortMax < estimate))
|
||||
estimate = shortMax;
|
||||
}
|
||||
}
|
||||
}
|
||||
return estimate;
|
||||
}
|
||||
|
||||
double CBlockPolicyEstimator::estimateConservativeFee(unsigned int doubleTarget) const
|
||||
{
|
||||
double estimate = -1;
|
||||
if (doubleTarget <= shortStats->GetMaxConfirms()) {
|
||||
estimate = feeStats->EstimateMedianVal(doubleTarget, SUFFICIENT_FEETXS, DOUBLE_SUCCESS_PCT, true, nBestSeenHeight);
|
||||
}
|
||||
if (doubleTarget <= feeStats->GetMaxConfirms()) {
|
||||
double longEstimate = longStats->EstimateMedianVal(doubleTarget, SUFFICIENT_FEETXS, DOUBLE_SUCCESS_PCT, true, nBestSeenHeight);
|
||||
if (longEstimate > estimate) {
|
||||
estimate = longEstimate;
|
||||
}
|
||||
}
|
||||
return estimate;
|
||||
}
|
||||
|
||||
CFeeRate CBlockPolicyEstimator::estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool, bool conservative) const
|
||||
{
|
||||
if (answerFoundAtTarget)
|
||||
*answerFoundAtTarget = confTarget;
|
||||
|
||||
double median = -1;
|
||||
|
||||
{
|
||||
LOCK(cs_feeEstimator);
|
||||
|
||||
// Return failure if trying to analyze a target we're not tracking
|
||||
if (confTarget <= 0 || (unsigned int)confTarget > feeStats->GetMaxConfirms())
|
||||
if (confTarget <= 0 || (unsigned int)confTarget > longStats->GetMaxConfirms())
|
||||
return CFeeRate(0);
|
||||
|
||||
// It's not possible to get reasonable estimates for confTarget of 1
|
||||
if (confTarget == 1)
|
||||
confTarget = 2;
|
||||
|
||||
while (median < 0 && (unsigned int)confTarget <= feeStats->GetMaxConfirms()) {
|
||||
median = feeStats->EstimateMedianVal(confTarget++, SUFFICIENT_FEETXS, DOUBLE_SUCCESS_PCT, true, nBestSeenHeight);
|
||||
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 becuase 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, HALF_SUCCESS_PCT, true);
|
||||
double actualEst = estimateCombinedFee(confTarget, SUCCESS_PCT, true);
|
||||
double doubleEst = estimateCombinedFee(2 * confTarget, DOUBLE_SUCCESS_PCT, !conservative);
|
||||
median = halfEst;
|
||||
if (actualEst > median) {
|
||||
median = actualEst;
|
||||
}
|
||||
if (doubleEst > median) {
|
||||
median = doubleEst;
|
||||
}
|
||||
|
||||
if (conservative || median == -1) {
|
||||
double consEst = estimateConservativeFee(2 * confTarget);
|
||||
if (consEst > median) {
|
||||
median = consEst;
|
||||
}
|
||||
}
|
||||
} // Must unlock cs_feeEstimator before taking mempool locks
|
||||
|
||||
if (answerFoundAtTarget)
|
||||
*answerFoundAtTarget = confTarget - 1;
|
||||
*answerFoundAtTarget = confTarget;
|
||||
|
||||
// If mempool is limiting txs , return at least the min feerate from the mempool
|
||||
CAmount minPoolFee = pool.GetMinFee(GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000).GetFeePerK();
|
||||
|
@ -695,6 +771,7 @@ CFeeRate CBlockPolicyEstimator::estimateSmartFee(int confTarget, int *answerFoun
|
|||
return CFeeRate(median);
|
||||
}
|
||||
|
||||
|
||||
bool CBlockPolicyEstimator::Write(CAutoFile& fileout) const
|
||||
{
|
||||
try {
|
||||
|
|
|
@ -155,7 +155,7 @@ public:
|
|||
* confTarget blocks. If no answer can be given at confTarget, return an
|
||||
* estimate at the lowest target where one can be given.
|
||||
*/
|
||||
CFeeRate estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool) const;
|
||||
CFeeRate estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool, bool conservative = true) const;
|
||||
|
||||
/** Return a specific fee estimate calculation with a given success threshold and time horizon.
|
||||
*/
|
||||
|
@ -199,6 +199,8 @@ private:
|
|||
/** Process a transaction confirmed in a block*/
|
||||
bool processBlockTx(unsigned int nBlockHeight, const CTxMemPoolEntry* entry);
|
||||
|
||||
double estimateCombinedFee(unsigned int confTarget, double successThreshold, bool checkShorterHorizon) const;
|
||||
double estimateConservativeFee(unsigned int doubleTarget) const;
|
||||
};
|
||||
|
||||
class FeeFilterRounder
|
||||
|
|
|
@ -106,6 +106,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||
{ "getrawmempool", 0, "verbose" },
|
||||
{ "estimatefee", 0, "nblocks" },
|
||||
{ "estimatesmartfee", 0, "nblocks" },
|
||||
{ "estimatesmartfee", 1, "conservative" },
|
||||
{ "estimaterawfee", 0, "nblocks" },
|
||||
{ "estimaterawfee", 1, "threshold" },
|
||||
{ "estimaterawfee", 2, "horizon" },
|
||||
|
|
|
@ -828,9 +828,9 @@ UniValue estimatefee(const JSONRPCRequest& request)
|
|||
|
||||
UniValue estimatesmartfee(const JSONRPCRequest& request)
|
||||
{
|
||||
if (request.fHelp || request.params.size() != 1)
|
||||
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
|
||||
throw std::runtime_error(
|
||||
"estimatesmartfee nblocks\n"
|
||||
"estimatesmartfee nblocks (conservative)\n"
|
||||
"\nWARNING: This interface is unstable and may disappear or change!\n"
|
||||
"\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n"
|
||||
"confirmation within nblocks blocks if possible and return the number of blocks\n"
|
||||
|
@ -838,6 +838,10 @@ UniValue estimatesmartfee(const JSONRPCRequest& request)
|
|||
"in BIP 141 (witness data is discounted).\n"
|
||||
"\nArguments:\n"
|
||||
"1. nblocks (numeric)\n"
|
||||
"2. conservative (bool, optional, default=true) Whether to return a more conservative estimate which\n"
|
||||
" also satisfies a longer history. A conservative estimate potentially returns a higher\n"
|
||||
" feerate and is more likely to be sufficient for the desired target, but is not as\n"
|
||||
" responsive to short term drops in the prevailing fee market\n"
|
||||
"\nResult:\n"
|
||||
"{\n"
|
||||
" \"feerate\" : x.x, (numeric) estimate fee-per-kilobyte (in BTC)\n"
|
||||
|
@ -854,10 +858,15 @@ UniValue estimatesmartfee(const JSONRPCRequest& request)
|
|||
RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VNUM));
|
||||
|
||||
int nBlocks = request.params[0].get_int();
|
||||
bool conservative = true;
|
||||
if (request.params.size() > 1 && !request.params[1].isNull()) {
|
||||
RPCTypeCheckArgument(request.params[1], UniValue::VBOOL);
|
||||
conservative = request.params[1].get_bool();
|
||||
}
|
||||
|
||||
UniValue result(UniValue::VOBJ);
|
||||
int answerFound;
|
||||
CFeeRate feeRate = ::feeEstimator.estimateSmartFee(nBlocks, &answerFound, ::mempool);
|
||||
CFeeRate feeRate = ::feeEstimator.estimateSmartFee(nBlocks, &answerFound, ::mempool, conservative);
|
||||
result.push_back(Pair("feerate", feeRate == CFeeRate(0) ? -1.0 : ValueFromAmount(feeRate.GetFeePerK())));
|
||||
result.push_back(Pair("blocks", answerFound));
|
||||
return result;
|
||||
|
@ -951,7 +960,7 @@ static const CRPCCommand commands[] =
|
|||
{ "generating", "generatetoaddress", &generatetoaddress, true, {"nblocks","address","maxtries"} },
|
||||
|
||||
{ "util", "estimatefee", &estimatefee, true, {"nblocks"} },
|
||||
{ "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks"} },
|
||||
{ "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks", "conservative"} },
|
||||
|
||||
{ "hidden", "estimaterawfee", &estimaterawfee, true, {"nblocks", "threshold", "horizon"} },
|
||||
};
|
||||
|
|
|
@ -83,11 +83,6 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
|
|||
BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0));
|
||||
BOOST_CHECK(feeEst.estimateFee(2).GetFeePerK() < 9*baseRate.GetFeePerK() + deltaFee);
|
||||
BOOST_CHECK(feeEst.estimateFee(2).GetFeePerK() > 9*baseRate.GetFeePerK() - deltaFee);
|
||||
int answerFound;
|
||||
BOOST_CHECK(feeEst.estimateSmartFee(1, &answerFound, mpool) == feeEst.estimateFee(2) && answerFound == 2);
|
||||
BOOST_CHECK(feeEst.estimateSmartFee(2, &answerFound, mpool) == feeEst.estimateFee(2) && answerFound == 2);
|
||||
BOOST_CHECK(feeEst.estimateSmartFee(4, &answerFound, mpool) == feeEst.estimateFee(4) && answerFound == 4);
|
||||
BOOST_CHECK(feeEst.estimateSmartFee(8, &answerFound, mpool) == feeEst.estimateFee(8) && answerFound == 8);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,10 +138,8 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
|
|||
mpool.removeForBlock(block, ++blocknum);
|
||||
}
|
||||
|
||||
int answerFound;
|
||||
for (int i = 1; i < 10;i++) {
|
||||
BOOST_CHECK(feeEst.estimateFee(i) == CFeeRate(0) || feeEst.estimateFee(i).GetFeePerK() > origFeeEst[i-1] - deltaFee);
|
||||
BOOST_CHECK(feeEst.estimateSmartFee(i, &answerFound, mpool).GetFeePerK() > origFeeEst[answerFound-1] - deltaFee);
|
||||
}
|
||||
|
||||
// Mine all those transactions
|
||||
|
@ -194,7 +187,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
|
|||
mpool.TrimToSize(1);
|
||||
BOOST_CHECK(mpool.GetMinFee(1).GetFeePerK() > feeV[5]);
|
||||
for (int i = 1; i < 10; i++) {
|
||||
BOOST_CHECK(feeEst.estimateSmartFee(i, NULL, mpool).GetFeePerK() >= feeEst.estimateFee(i).GetFeePerK());
|
||||
BOOST_CHECK(feeEst.estimateSmartFee(i, NULL, mpool).GetFeePerK() >= feeEst.estimateRawFee(i, 0.85, FeeEstimateHorizon::MED_HALFLIFE).GetFeePerK());
|
||||
BOOST_CHECK(feeEst.estimateSmartFee(i, NULL, mpool).GetFeePerK() >= mpool.GetMinFee(1).GetFeePerK());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,8 +116,8 @@ def check_estimates(node, fees_seen, max_invalid, print_estimates = True):
|
|||
for i,e in enumerate(all_estimates): # estimate is for i+1
|
||||
if e >= 0:
|
||||
valid_estimate = True
|
||||
# estimatesmartfee should return the same result
|
||||
assert_equal(node.estimatesmartfee(i+1)["feerate"], e)
|
||||
if i >= 13: # for n>=14 estimatesmartfee(n/2) should be at least as high as estimatefee(n)
|
||||
assert(node.estimatesmartfee((i+1)//2)["feerate"] > float(e) - delta)
|
||||
|
||||
else:
|
||||
invalid_estimates += 1
|
||||
|
|
Loading…
Reference in a new issue