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);
|
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)
|
if (answerFoundAtTarget)
|
||||||
*answerFoundAtTarget = confTarget;
|
*answerFoundAtTarget = confTarget;
|
||||||
|
|
||||||
double median = -1;
|
double median = -1;
|
||||||
|
|
||||||
{
|
{
|
||||||
LOCK(cs_feeEstimator);
|
LOCK(cs_feeEstimator);
|
||||||
|
|
||||||
// Return failure if trying to analyze a target we're not tracking
|
// 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);
|
return CFeeRate(0);
|
||||||
|
|
||||||
// It's not possible to get reasonable estimates for confTarget of 1
|
// It's not possible to get reasonable estimates for confTarget of 1
|
||||||
if (confTarget == 1)
|
if (confTarget == 1)
|
||||||
confTarget = 2;
|
confTarget = 2;
|
||||||
|
|
||||||
while (median < 0 && (unsigned int)confTarget <= feeStats->GetMaxConfirms()) {
|
assert(confTarget > 0); //estimateCombinedFee and estimateConservativeFee take unsigned ints
|
||||||
median = feeStats->EstimateMedianVal(confTarget++, SUFFICIENT_FEETXS, DOUBLE_SUCCESS_PCT, true, nBestSeenHeight);
|
|
||||||
|
/** 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
|
} // Must unlock cs_feeEstimator before taking mempool locks
|
||||||
|
|
||||||
if (answerFoundAtTarget)
|
if (answerFoundAtTarget)
|
||||||
*answerFoundAtTarget = confTarget - 1;
|
*answerFoundAtTarget = confTarget;
|
||||||
|
|
||||||
// If mempool is limiting txs , return at least the min feerate from the mempool
|
// 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();
|
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);
|
return CFeeRate(median);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool CBlockPolicyEstimator::Write(CAutoFile& fileout) const
|
bool CBlockPolicyEstimator::Write(CAutoFile& fileout) const
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -155,7 +155,7 @@ public:
|
||||||
* confTarget blocks. If no answer can be given at confTarget, return an
|
* confTarget blocks. If no answer can be given at confTarget, return an
|
||||||
* estimate at the lowest target where one can be given.
|
* 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.
|
/** 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*/
|
/** Process a transaction confirmed in a block*/
|
||||||
bool processBlockTx(unsigned int nBlockHeight, const CTxMemPoolEntry* entry);
|
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
|
class FeeFilterRounder
|
||||||
|
|
|
@ -106,6 +106,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||||
{ "getrawmempool", 0, "verbose" },
|
{ "getrawmempool", 0, "verbose" },
|
||||||
{ "estimatefee", 0, "nblocks" },
|
{ "estimatefee", 0, "nblocks" },
|
||||||
{ "estimatesmartfee", 0, "nblocks" },
|
{ "estimatesmartfee", 0, "nblocks" },
|
||||||
|
{ "estimatesmartfee", 1, "conservative" },
|
||||||
{ "estimaterawfee", 0, "nblocks" },
|
{ "estimaterawfee", 0, "nblocks" },
|
||||||
{ "estimaterawfee", 1, "threshold" },
|
{ "estimaterawfee", 1, "threshold" },
|
||||||
{ "estimaterawfee", 2, "horizon" },
|
{ "estimaterawfee", 2, "horizon" },
|
||||||
|
|
|
@ -828,16 +828,20 @@ UniValue estimatefee(const JSONRPCRequest& request)
|
||||||
|
|
||||||
UniValue estimatesmartfee(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(
|
throw std::runtime_error(
|
||||||
"estimatesmartfee nblocks\n"
|
"estimatesmartfee nblocks (conservative)\n"
|
||||||
"\nWARNING: This interface is unstable and may disappear or change!\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"
|
"\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"
|
"confirmation within nblocks blocks if possible and return the number of blocks\n"
|
||||||
"for which the estimate is valid. Uses virtual transaction size as defined\n"
|
"for which the estimate is valid. Uses virtual transaction size as defined\n"
|
||||||
"in BIP 141 (witness data is discounted).\n"
|
"in BIP 141 (witness data is discounted).\n"
|
||||||
"\nArguments:\n"
|
"\nArguments:\n"
|
||||||
"1. nblocks (numeric)\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"
|
"\nResult:\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
" \"feerate\" : x.x, (numeric) estimate fee-per-kilobyte (in BTC)\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));
|
RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VNUM));
|
||||||
|
|
||||||
int nBlocks = request.params[0].get_int();
|
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);
|
UniValue result(UniValue::VOBJ);
|
||||||
int answerFound;
|
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("feerate", feeRate == CFeeRate(0) ? -1.0 : ValueFromAmount(feeRate.GetFeePerK())));
|
||||||
result.push_back(Pair("blocks", answerFound));
|
result.push_back(Pair("blocks", answerFound));
|
||||||
return result;
|
return result;
|
||||||
|
@ -951,7 +960,7 @@ static const CRPCCommand commands[] =
|
||||||
{ "generating", "generatetoaddress", &generatetoaddress, true, {"nblocks","address","maxtries"} },
|
{ "generating", "generatetoaddress", &generatetoaddress, true, {"nblocks","address","maxtries"} },
|
||||||
|
|
||||||
{ "util", "estimatefee", &estimatefee, true, {"nblocks"} },
|
{ "util", "estimatefee", &estimatefee, true, {"nblocks"} },
|
||||||
{ "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks"} },
|
{ "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks", "conservative"} },
|
||||||
|
|
||||||
{ "hidden", "estimaterawfee", &estimaterawfee, true, {"nblocks", "threshold", "horizon"} },
|
{ "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(1) == CFeeRate(0));
|
||||||
BOOST_CHECK(feeEst.estimateFee(2).GetFeePerK() < 9*baseRate.GetFeePerK() + deltaFee);
|
BOOST_CHECK(feeEst.estimateFee(2).GetFeePerK() < 9*baseRate.GetFeePerK() + deltaFee);
|
||||||
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);
|
mpool.removeForBlock(block, ++blocknum);
|
||||||
}
|
}
|
||||||
|
|
||||||
int answerFound;
|
|
||||||
for (int i = 1; i < 10;i++) {
|
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.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
|
// Mine all those transactions
|
||||||
|
@ -194,7 +187,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
|
||||||
mpool.TrimToSize(1);
|
mpool.TrimToSize(1);
|
||||||
BOOST_CHECK(mpool.GetMinFee(1).GetFeePerK() > feeV[5]);
|
BOOST_CHECK(mpool.GetMinFee(1).GetFeePerK() > feeV[5]);
|
||||||
for (int i = 1; i < 10; i++) {
|
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());
|
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
|
for i,e in enumerate(all_estimates): # estimate is for i+1
|
||||||
if e >= 0:
|
if e >= 0:
|
||||||
valid_estimate = True
|
valid_estimate = True
|
||||||
# estimatesmartfee should return the same result
|
if i >= 13: # for n>=14 estimatesmartfee(n/2) should be at least as high as estimatefee(n)
|
||||||
assert_equal(node.estimatesmartfee(i+1)["feerate"], e)
|
assert(node.estimatesmartfee((i+1)//2)["feerate"] > float(e) - delta)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
invalid_estimates += 1
|
invalid_estimates += 1
|
||||||
|
|
Loading…
Reference in a new issue