global: fixes

This commit is contained in:
nym21
2026-04-27 19:19:14 +02:00
parent 6c8afc942c
commit 66494c081c
16 changed files with 1453 additions and 5399 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -41,7 +41,7 @@ use super::{
metrics::AvgAmountMetrics, metrics::AvgAmountMetrics,
}; };
const VERSION: Version = Version::new(23); const VERSION: Version = Version::new(24);
#[derive(Traversable)] #[derive(Traversable)]
pub struct AddrMetricsVecs<M: StorageMode = Rw> { pub struct AddrMetricsVecs<M: StorageMode = Rw> {

View File

@@ -10,7 +10,9 @@ use std::{
}; };
use brk_rpc::Client; use brk_rpc::Client;
use brk_types::FeeRate;
use parking_lot::RwLock; use parking_lot::RwLock;
use tracing::warn;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
use self::projected_blocks::verify::Verifier; use self::projected_blocks::verify::Verifier;
@@ -72,6 +74,11 @@ impl Rebuilder {
self.dirty.store(false, Ordering::Release); self.dirty.store(false, Ordering::Release);
let min_fee = client.get_mempool_min_fee().unwrap_or_else(|e| {
warn!("getmempoolinfo failed, falling back to FeeRate::MIN: {e}");
FeeRate::MIN
});
let built = { let built = {
let entries = entries.read(); let entries = entries.read();
let entries_slice = entries.entries(); let entries_slice = entries.entries();
@@ -82,7 +89,7 @@ impl Rebuilder {
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
let _ = client; let _ = client;
Snapshot::build(blocks, entries_slice) Snapshot::build(blocks, entries_slice, min_fee)
}; };
*self.snapshot.write() = Arc::new(built); *self.snapshot.write() = Arc::new(built);

View File

@@ -2,21 +2,74 @@ use brk_types::{FeeRate, RecommendedFees};
use super::stats::BlockStats; use super::stats::BlockStats;
/// Compute recommended fees from block stats (mempool.space style). /// Output rounding granularity in sat/vB. mempool.space's
pub fn compute_recommended_fees(stats: &[BlockStats]) -> RecommendedFees { /// `/api/v1/fees/recommended` uses `1.0`; their `/precise`
/// variant uses `0.001`. bitview always emits precise.
const MIN_INCREMENT: FeeRate = FeeRate::new(0.001);
/// `getPreciseRecommendedFee` adds this to `fastestFee` and
/// half of it to `halfHourFee`, then floors them. Compensates
/// for sub-1-sat/vB fees mined by hashrate that ignores the
/// relay floor.
const PRIORITY_FACTOR: FeeRate = FeeRate::new(0.5);
const MIN_FASTEST_FEE: FeeRate = FeeRate::new(1.0);
const MIN_HALF_HOUR_FEE: FeeRate = FeeRate::new(0.5);
/// Literal port of mempool.space's `getPreciseRecommendedFee`
/// (backend/src/api/fee-api.ts). `min_fee` is bitcoind's live
/// `mempoolminfee` in sat/vB and acts as a floor for every tier
/// while the mempool is purging by fee.
pub fn compute_recommended_fees(stats: &[BlockStats], min_fee: FeeRate) -> RecommendedFees {
let purge_rate = min_fee.ceil_to(MIN_INCREMENT);
let minimum_fee = purge_rate.max(MIN_INCREMENT);
let first = stats.first().map_or(minimum_fee, |b| {
optimize_median_fee(b, stats.get(1), None, minimum_fee)
});
let second = stats.get(1).map_or(minimum_fee, |b| {
optimize_median_fee(b, stats.get(2), Some(first), minimum_fee)
});
let third = stats.get(2).map_or(minimum_fee, |b| {
optimize_median_fee(b, stats.get(3), Some(second), minimum_fee)
});
let mut fastest = minimum_fee.max(first);
let mut half_hour = minimum_fee.max(second);
let mut hour = minimum_fee.max(third);
let economy = third.clamp(minimum_fee, minimum_fee * 2.0);
fastest = fastest.max(half_hour).max(hour).max(economy);
half_hour = half_hour.max(hour).max(economy);
hour = hour.max(economy);
let fastest = (fastest + PRIORITY_FACTOR).max(MIN_FASTEST_FEE);
let half_hour = (half_hour + PRIORITY_FACTOR / 2.0).max(MIN_HALF_HOUR_FEE);
RecommendedFees { RecommendedFees {
fastest_fee: median_fee_for_block(stats, 0), fastest_fee: fastest.round_milli(),
half_hour_fee: median_fee_for_block(stats, 2), half_hour_fee: half_hour.round_milli(),
hour_fee: median_fee_for_block(stats, 5), hour_fee: hour.round_milli(),
economy_fee: median_fee_for_block(stats, 7), economy_fee: economy.round_milli(),
minimum_fee: FeeRate::MIN, minimum_fee: minimum_fee.round_milli(),
} }
} }
/// Get the median fee rate for block N. /// Pick the fee for one projected block, smoothing toward the
fn median_fee_for_block(stats: &[BlockStats], block_index: usize) -> FeeRate { /// previous tier and discounting partially-full final blocks.
stats fn optimize_median_fee(
.get(block_index) block: &BlockStats,
.map(|s| s.median_fee_rate()) next_block: Option<&BlockStats>,
.unwrap_or_else(|| FeeRate::MIN) previous_fee: Option<FeeRate>,
min_fee: FeeRate,
) -> FeeRate {
let median = block.median_fee_rate();
let use_fee = previous_fee.map_or(median, |prev| FeeRate::mean(median, prev));
let vsize = u64::from(block.total_vsize);
if vsize <= 500_000 || median < min_fee {
return min_fee;
}
if vsize <= 950_000 && next_block.is_none() {
let multiplier = (vsize - 500_000) as f64 / 500_000.0;
return (use_fee * multiplier).round_to(MIN_INCREMENT).max(min_fee);
}
use_fee.ceil_to(MIN_INCREMENT).max(min_fee)
} }

View File

@@ -1,6 +1,6 @@
use std::hash::{DefaultHasher, Hash, Hasher}; use std::hash::{DefaultHasher, Hash, Hasher};
use brk_types::RecommendedFees; use brk_types::{FeeRate, RecommendedFees};
use super::{ use super::{
super::block_builder::Package, super::block_builder::Package,
@@ -27,13 +27,15 @@ pub struct Snapshot {
impl Snapshot { impl Snapshot {
/// Build a snapshot from packages grouped by projected block. /// Build a snapshot from packages grouped by projected block.
pub fn build(blocks: Vec<Vec<Package>>, entries: &[Option<Entry>]) -> Self { /// `min_fee` is bitcoind's live `mempoolminfee`, used as the floor
/// for every recommended-fee tier.
pub fn build(blocks: Vec<Vec<Package>>, entries: &[Option<Entry>], min_fee: FeeRate) -> Self {
let block_stats: Vec<BlockStats> = blocks let block_stats: Vec<BlockStats> = blocks
.iter() .iter()
.map(|block| stats::compute_block_stats(block, entries)) .map(|block| stats::compute_block_stats(block, entries))
.collect(); .collect();
let fees = fees::compute_recommended_fees(&block_stats); let fees = fees::compute_recommended_fees(&block_stats, min_fee);
let blocks: Vec<Vec<TxIndex>> = blocks let blocks: Vec<Vec<TxIndex>> = blocks
.into_iter() .into_iter()

View File

@@ -1,4 +1,7 @@
use std::hash::{DefaultHasher, Hash, Hasher}; use std::{
collections::hash_map::Entry as MapEntry,
hash::{DefaultHasher, Hash, Hasher},
};
use brk_types::{AddrBytes, AddrMempoolStats, Transaction, TxOut, Txid}; use brk_types::{AddrBytes, AddrMempoolStats, Transaction, TxOut, Txid};
use derive_more::Deref; use derive_more::Deref;
@@ -51,7 +54,6 @@ impl AddrTracker {
} }
fn update(&mut self, tx: &Transaction, txid: &Txid, is_addition: bool) { fn update(&mut self, tx: &Transaction, txid: &Txid, is_addition: bool) {
// Inputs: track sending
for txin in &tx.input { for txin in &tx.input {
let Some(prevout) = txin.prevout.as_ref() else { let Some(prevout) = txin.prevout.as_ref() else {
continue; continue;
@@ -59,33 +61,57 @@ impl AddrTracker {
let Some(bytes) = prevout.addr_bytes() else { let Some(bytes) = prevout.addr_bytes() else {
continue; continue;
}; };
self.apply(bytes, txid, is_addition, |stats| {
let (stats, txids) = self.0.entry(bytes).or_default(); if is_addition {
if is_addition { stats.sending(prevout);
txids.insert(txid.clone()); } else {
stats.sending(prevout); stats.sent(prevout);
} else { }
txids.remove(txid); });
stats.sent(prevout);
}
stats.update_tx_count(txids.len() as u32);
} }
// Outputs: track receiving
for txout in &tx.output { for txout in &tx.output {
let Some(bytes) = txout.addr_bytes() else { let Some(bytes) = txout.addr_bytes() else {
continue; continue;
}; };
self.apply(bytes, txid, is_addition, |stats| {
if is_addition {
stats.receiving(txout);
} else {
stats.received(txout);
}
});
}
}
let (stats, txids) = self.0.entry(bytes).or_default(); fn apply(
if is_addition { &mut self,
txids.insert(txid.clone()); bytes: AddrBytes,
stats.receiving(txout); txid: &Txid,
} else { is_addition: bool,
txids.remove(txid); update_stats: impl FnOnce(&mut AddrMempoolStats),
stats.received(txout); ) {
let mut entry = match self.0.entry(bytes) {
MapEntry::Occupied(e) => e,
MapEntry::Vacant(v) => {
if !is_addition {
return;
}
v.insert_entry(Default::default())
} }
stats.update_tx_count(txids.len() as u32); };
let (stats, txids) = entry.get_mut();
if is_addition {
txids.insert(txid.clone());
} else {
txids.remove(txid);
}
update_stats(stats);
let len = txids.len();
if len == 0 {
entry.remove();
} else {
stats.update_tx_count(len as u32);
} }
} }
} }

View File

@@ -2,10 +2,11 @@ use std::{thread::sleep, time::Duration};
use bitcoin::{consensus::encode, hex::FromHex}; use bitcoin::{consensus::encode, hex::FromHex};
use brk_error::{Error, Result}; use brk_error::{Error, Result};
use brk_types::{Bitcoin, BlockHash, Height, MempoolEntryInfo, Sats, Txid, Vout}; use brk_types::{Bitcoin, BlockHash, FeeRate, Height, MempoolEntryInfo, Sats, Txid, Vout};
use corepc_types::v30::{ use corepc_types::v30::{
GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, GetBlockVerboseOne,
GetBlockVerboseZero, GetBlockchainInfo, GetRawMempool, GetRawMempoolVerbose, GetTxOut, GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose,
GetTxOut,
}; };
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::Deserialize; use serde::Deserialize;
@@ -172,6 +173,15 @@ impl Client {
} }
} }
/// Live `mempoolminfee` in sat/vB, already maxed against `minrelaytxfee`
/// per Core's contract. Wallets must pay at least this rate or bitcoind
/// will reject the broadcast; rises above the relay floor when the
/// mempool is purging by fee.
pub fn get_mempool_min_fee(&self) -> Result<FeeRate> {
let r: GetMempoolInfo = self.0.call_with_retry("getmempoolinfo", &[])?;
Ok(FeeRate::from(r.mempool_min_fee * 100_000.0))
}
/// Get txids of all transactions in a memory pool /// Get txids of all transactions in a memory pool
pub fn get_raw_mempool(&self) -> Result<Vec<Txid>> { pub fn get_raw_mempool(&self) -> Result<Vec<Txid>> {
let r: GetRawMempool = self.0.call_with_retry("getrawmempool", &[])?; let r: GetRawMempool = self.0.call_with_retry("getrawmempool", &[])?;

View File

@@ -119,10 +119,7 @@ fn cost_basis_formatted(
FxHashMap::with_capacity_and_hasher(raw.map.len(), Default::default()); FxHashMap::with_capacity_and_hasher(raw.map.len(), Default::default());
for (&price_cents, &sats) in &raw.map { for (&price_cents, &sats) in &raw.map {
let price = Cents::from(price_cents); let price = Cents::from(price_cents);
let key = match agg { let key = agg.bucket_floor(price);
UrpdAggregation::Raw => price,
_ => agg.bucket_floor(price).unwrap_or(price),
};
let entry = bucketed.entry(key).or_insert((Sats::ZERO, Dollars::ZERO)); let entry = bucketed.entry(key).or_insert((Sats::ZERO, Dollars::ZERO));
entry.0 += sats; entry.0 += sats;
if needs_realized { if needs_realized {

View File

@@ -1,6 +1,6 @@
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
ops::{Add, AddAssign, Div}, ops::{Add, AddAssign, Div, Mul},
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
@@ -24,9 +24,35 @@ pub struct FeeRate(f64);
impl FeeRate { impl FeeRate {
pub const MIN: Self = Self(0.1); pub const MIN: Self = Self(0.1);
pub fn new(fr: f64) -> Self { pub const fn new(fr: f64) -> Self {
Self(fr) Self(fr)
} }
/// Round up to the nearest multiple of `nearest`.
#[inline]
pub fn ceil_to(self, nearest: Self) -> Self {
Self((self.0 / nearest.0).ceil() * nearest.0)
}
/// Round to the nearest multiple of `nearest`.
#[inline]
pub fn round_to(self, nearest: Self) -> Self {
Self((self.0 / nearest.0).round() * nearest.0)
}
/// Round to 3 decimal places via `(x * 1000).round() / 1000`.
/// Divide-by-integer at the end is more numerically stable
/// than `round_to(0.001)`'s closing `* 0.001`.
#[inline]
pub fn round_milli(self) -> Self {
Self((self.0 * 1000.0).round() / 1000.0)
}
/// Arithmetic mean of two rates.
#[inline]
pub fn mean(a: Self, b: Self) -> Self {
Self((a.0 + b.0) / 2.0)
}
} }
impl From<(Sats, VSize)> for FeeRate { impl From<(Sats, VSize)> for FeeRate {
@@ -88,6 +114,20 @@ impl Div<usize> for FeeRate {
} }
} }
impl Div<f64> for FeeRate {
type Output = Self;
fn div(self, rhs: f64) -> Self::Output {
Self(self.0 / rhs)
}
}
impl Mul<f64> for FeeRate {
type Output = Self;
fn mul(self, rhs: f64) -> Self::Output {
Self(self.0 * rhs)
}
}
impl From<usize> for FeeRate { impl From<usize> for FeeRate {
#[inline] #[inline]
fn from(value: usize) -> Self { fn from(value: usize) -> Self {

View File

@@ -1,18 +1,18 @@
use std::collections::BTreeMap;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{Bitcoin, Cents, Cohort, Date, Dollars, Sats, UrpdAggregation, UrpdBucket, UrpdRaw}; use crate::{
Bitcoin, Cents, CentsSats, CentsSigned, Cohort, Date, Dollars, Sats, UrpdAggregation,
UrpdBucket, UrpdRaw,
};
/// UTXO Realized Price Distribution for a cohort on a specific date. /// UTXO Realized Price Distribution for a cohort on a specific date.
/// ///
/// Supply is grouped by the close price at which each UTXO was last moved. /// Supply is grouped by the close price at which each UTXO was last moved.
/// Each bucket exposes three values derived from the same `(price_floor, supply)` /// Each bucket exposes three values: supply in BTC, realized cap contribution
/// pairs: supply in BTC, realized cap contribution in USD (`price_floor * supply`), /// in USD (sum of `realized_price * supply` over the coins in the bucket), and
/// and unrealized P&L against that date's close in USD /// unrealized P&L in USD (`close * supply - realized_cap`, can be negative).
/// (`(close - price_floor) * supply`, can be negative).
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Urpd { pub struct Urpd {
pub cohort: Cohort, pub cohort: Cohort,
@@ -26,6 +26,12 @@ pub struct Urpd {
pub buckets: Vec<UrpdBucket>, pub buckets: Vec<UrpdBucket>,
} }
#[derive(Default, Clone, Copy)]
struct BucketAccum {
supply: Sats,
realized_cap: CentsSats,
}
impl Urpd { impl Urpd {
/// Build from the raw on-disk distribution plus context. /// Build from the raw on-disk distribution plus context.
pub fn build( pub fn build(
@@ -35,39 +41,44 @@ impl Urpd {
raw: &UrpdRaw, raw: &UrpdRaw,
aggregation: UrpdAggregation, aggregation: UrpdAggregation,
) -> Self { ) -> Self {
let mut agg: FxHashMap<Cents, Sats> = let mut agg: FxHashMap<Cents, BucketAccum> =
FxHashMap::with_capacity_and_hasher(raw.map.len(), Default::default()); FxHashMap::with_capacity_and_hasher(raw.map.len(), Default::default());
for (&price_cents, &sats) in &raw.map { for (&price_cents, &sats) in &raw.map {
let price = Cents::from(price_cents); let price = Cents::from(price_cents);
let key = match aggregation { let key = aggregation.bucket_floor(price);
UrpdAggregation::Raw => price, let slot = agg.entry(key).or_default();
_ => aggregation.bucket_floor(price).unwrap_or(price), slot.supply += sats;
}; slot.realized_cap += CentsSats::from_price_sats(price, sats);
*agg.entry(key).or_insert(Sats::ZERO) += sats;
} }
let sorted: BTreeMap<Cents, Sats> = agg.into_iter().collect(); let mut sorted: Vec<_> = agg.into_iter().collect();
sorted.sort_unstable_by_key(|&(price, _)| price);
let close = Dollars::from(close_cents); let close = Dollars::from(close_cents);
let total_supply: Sats = raw.map.values().copied().sum();
let mut total_sats = Sats::ZERO; let buckets = sorted
let mut buckets = Vec::with_capacity(sorted.len()); .into_iter()
for (price_floor_cents, supply) in sorted { .map(|(price_floor_cents, slot)| {
total_sats += supply; let realized_cap_cents = slot.realized_cap.to_cents();
let price_floor = Dollars::from(price_floor_cents); let close_mc_cents = CentsSats::from_price_sats(close_cents, slot.supply).to_cents();
buckets.push(UrpdBucket { let pnl = CentsSigned::from(close_mc_cents.inner())
price_floor, - CentsSigned::from(realized_cap_cents.inner());
supply: Bitcoin::from(supply), UrpdBucket {
realized_cap: price_floor * supply, price_floor: Dollars::from(price_floor_cents),
unrealized_pnl: (close - price_floor) * supply, supply: Bitcoin::from(slot.supply),
}); realized_cap: Dollars::from(realized_cap_cents),
} unrealized_pnl: Dollars::from(pnl),
}
})
.collect();
Self { Self {
cohort, cohort,
date, date,
aggregation, aggregation,
close, close,
total_supply: Bitcoin::from(total_sats), total_supply: Bitcoin::from(total_supply),
buckets, buckets,
} }
} }

View File

@@ -46,26 +46,24 @@ impl UrpdAggregation {
} }
} }
/// Compute bucket floor for a given price in cents. /// Compute the bucket floor for a price in cents.
/// Returns None for Raw (no bucketing). /// `Raw` is the identity (no bucketing).
pub fn bucket_floor(&self, price_cents: Cents) -> Option<Cents> { pub fn bucket_floor(&self, price_cents: Cents) -> Cents {
match self { match self {
Self::Raw => None, Self::Raw => price_cents,
Self::Lin200 | Self::Lin500 | Self::Lin1000 => { Self::Lin200 | Self::Lin500 | Self::Lin1000 => {
let size = self.linear_size_cents().unwrap(); let size = self.linear_size_cents().unwrap();
Some((price_cents / size) * size) (price_cents / size) * size
} }
Self::Log10 | Self::Log50 | Self::Log100 | Self::Log200 => { Self::Log10 | Self::Log50 | Self::Log100 | Self::Log200 => {
if price_cents == Cents::ZERO { if price_cents == Cents::ZERO {
return Some(Cents::ZERO); return Cents::ZERO;
} }
let n = self.log_buckets_per_decade().unwrap(); let n = self.log_buckets_per_decade().unwrap();
// Bucket index = floor(n * log10(price))
// Floor = 10^(bucket_index / n)
let log_price = f64::from(price_cents).log10(); let log_price = f64::from(price_cents).log10();
let bucket_idx = (n as f64 * log_price).floor() as i32; let bucket_idx = (n as f64 * log_price).floor() as i32;
let floor = 10_f64.powf(bucket_idx as f64 / n as f64); let floor = 10_f64.powf(bucket_idx as f64 / n as f64);
Some(Cents::from(floor.round() as u64)) Cents::from(floor.round() as u64)
} }
} }
} }

View File

@@ -6,12 +6,12 @@ use crate::{Bitcoin, Dollars};
/// A single bucket in a URPD snapshot. /// A single bucket in a URPD snapshot.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct UrpdBucket { pub struct UrpdBucket {
/// Inclusive lower bound of the bucket, in USD. /// Lower bound of the bucket, in USD. Equals the exact realized price for `Raw`.
pub price_floor: Dollars, pub price_floor: Dollars,
/// Supply held with a last-move price inside this bucket, in BTC. /// Supply held with a last-move price inside this bucket, in BTC.
pub supply: Bitcoin, pub supply: Bitcoin,
/// Realized cap contribution in USD: `price_floor * supply`. /// Realized cap contribution in USD: sum of `realized_price * supply` over the coins in this bucket.
pub realized_cap: Dollars, pub realized_cap: Dollars,
/// Unrealized P&L in USD against the close on the snapshot date: `(close - price_floor) * supply`. Can be negative. /// Unrealized P&L in USD against the close on the snapshot date: `close * supply - realized_cap`. Can be negative.
pub unrealized_pnl: Dollars, pub unrealized_pnl: Dollars,
} }

View File

@@ -1187,10 +1187,9 @@ replaced it. Omitted on the root of an RBF response.
* UTXO Realized Price Distribution for a cohort on a specific date. * UTXO Realized Price Distribution for a cohort on a specific date.
* *
* Supply is grouped by the close price at which each UTXO was last moved. * Supply is grouped by the close price at which each UTXO was last moved.
* Each bucket exposes three values derived from the same `(price_floor, supply)` * Each bucket exposes three values: supply in BTC, realized cap contribution
* pairs: supply in BTC, realized cap contribution in USD (`price_floor * supply`), * in USD (sum of `realized_price * supply` over the coins in the bucket), and
* and unrealized P&L against that date's close in USD * unrealized P&L in USD (`close * supply - realized_cap`, can be negative).
* (`(close - price_floor) * supply`, can be negative).
* *
* @typedef {Object} Urpd * @typedef {Object} Urpd
* @property {Cohort} cohort * @property {Cohort} cohort
@@ -1211,10 +1210,10 @@ replaced it. Omitted on the root of an RBF response.
* A single bucket in a URPD snapshot. * A single bucket in a URPD snapshot.
* *
* @typedef {Object} UrpdBucket * @typedef {Object} UrpdBucket
* @property {Dollars} priceFloor - Inclusive lower bound of the bucket, in USD. * @property {Dollars} priceFloor - Lower bound of the bucket, in USD. Equals the exact realized price for `Raw`.
* @property {Bitcoin} supply - Supply held with a last-move price inside this bucket, in BTC. * @property {Bitcoin} supply - Supply held with a last-move price inside this bucket, in BTC.
* @property {Dollars} realizedCap - Realized cap contribution in USD: `price_floor * supply`. * @property {Dollars} realizedCap - Realized cap contribution in USD: sum of `realized_price * supply` over the coins in this bucket.
* @property {Dollars} unrealizedPnl - Unrealized P&L in USD against the close on the snapshot date: `(close - price_floor) * supply`. Can be negative. * @property {Dollars} unrealizedPnl - Unrealized P&L in USD against the close on the snapshot date: `close * supply - realized_cap`. Can be negative.
*/ */
/** /**
* Path parameters for per-cohort URPD endpoints. * Path parameters for per-cohort URPD endpoints.
@@ -7272,7 +7271,7 @@ function createTransferPattern(client, acc) {
* @extends BrkClientBase * @extends BrkClientBase
*/ */
class BrkClient extends BrkClientBase { class BrkClient extends BrkClientBase {
VERSION = "v0.3.0-beta.6"; VERSION = "v0.3.0-beta.7";
INDEXES = /** @type {const} */ ([ INDEXES = /** @type {const} */ ([
"minute10", "minute10",

View File

@@ -40,5 +40,5 @@
"url": "git+https://github.com/bitcoinresearchkit/brk.git" "url": "git+https://github.com/bitcoinresearchkit/brk.git"
}, },
"type": "module", "type": "module",
"version": "0.3.0-beta.6" "version": "0.3.0-beta.7"
} }

View File

@@ -1573,10 +1573,10 @@ class UrpdBucket(TypedDict):
A single bucket in a URPD snapshot. A single bucket in a URPD snapshot.
Attributes: Attributes:
price_floor: Inclusive lower bound of the bucket, in USD. price_floor: Lower bound of the bucket, in USD. Equals the exact realized price for `Raw`.
supply: Supply held with a last-move price inside this bucket, in BTC. supply: Supply held with a last-move price inside this bucket, in BTC.
realized_cap: Realized cap contribution in USD: `price_floor * supply`. realized_cap: Realized cap contribution in USD: sum of `realized_price * supply` over the coins in this bucket.
unrealized_pnl: Unrealized P&L in USD against the close on the snapshot date: `(close - price_floor) * supply`. Can be negative. unrealized_pnl: Unrealized P&L in USD against the close on the snapshot date: `close * supply - realized_cap`. Can be negative.
""" """
price_floor: Dollars price_floor: Dollars
supply: Bitcoin supply: Bitcoin
@@ -1588,10 +1588,9 @@ class Urpd(TypedDict):
UTXO Realized Price Distribution for a cohort on a specific date. UTXO Realized Price Distribution for a cohort on a specific date.
Supply is grouped by the close price at which each UTXO was last moved. Supply is grouped by the close price at which each UTXO was last moved.
Each bucket exposes three values derived from the same `(price_floor, supply)` Each bucket exposes three values: supply in BTC, realized cap contribution
pairs: supply in BTC, realized cap contribution in USD (`price_floor * supply`), in USD (sum of `realized_price * supply` over the coins in the bucket), and
and unrealized P&L against that date's close in USD unrealized P&L in USD (`close * supply - realized_cap`, can be negative).
(`(close - price_floor) * supply`, can be negative).
Attributes: Attributes:
aggregation: Aggregation strategy applied to the buckets. aggregation: Aggregation strategy applied to the buckets.
@@ -6523,7 +6522,7 @@ class SeriesTree:
class BrkClient(BrkClientBase): class BrkClient(BrkClientBase):
"""Main BRK client with series tree and API methods.""" """Main BRK client with series tree and API methods."""
VERSION = "v0.3.0-beta.6" VERSION = "v0.3.0-beta.7"
INDEXES = [ INDEXES = [
"minute10", "minute10",

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "brk-client" name = "brk-client"
version = "0.3.0-beta.6" version = "0.3.0-beta.7"
description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index" description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"