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:
Alex Morcos 2017-03-07 11:33:44 -05:00
parent c7447ec303
commit 3810e976d6
6 changed files with 104 additions and 22 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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" },

View file

@ -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"} },
};

View file

@ -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());
}
}

View file

@ -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