mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 06:14:47 -07:00
global: fixes
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,7 @@ use super::{
|
||||
metrics::AvgAmountMetrics,
|
||||
};
|
||||
|
||||
const VERSION: Version = Version::new(23);
|
||||
const VERSION: Version = Version::new(24);
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct AddrMetricsVecs<M: StorageMode = Rw> {
|
||||
|
||||
@@ -10,7 +10,9 @@ use std::{
|
||||
};
|
||||
|
||||
use brk_rpc::Client;
|
||||
use brk_types::FeeRate;
|
||||
use parking_lot::RwLock;
|
||||
use tracing::warn;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use self::projected_blocks::verify::Verifier;
|
||||
@@ -72,6 +74,11 @@ impl Rebuilder {
|
||||
|
||||
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 entries = entries.read();
|
||||
let entries_slice = entries.entries();
|
||||
@@ -82,7 +89,7 @@ impl Rebuilder {
|
||||
#[cfg(not(debug_assertions))]
|
||||
let _ = client;
|
||||
|
||||
Snapshot::build(blocks, entries_slice)
|
||||
Snapshot::build(blocks, entries_slice, min_fee)
|
||||
};
|
||||
|
||||
*self.snapshot.write() = Arc::new(built);
|
||||
|
||||
@@ -2,21 +2,74 @@ use brk_types::{FeeRate, RecommendedFees};
|
||||
|
||||
use super::stats::BlockStats;
|
||||
|
||||
/// Compute recommended fees from block stats (mempool.space style).
|
||||
pub fn compute_recommended_fees(stats: &[BlockStats]) -> RecommendedFees {
|
||||
/// Output rounding granularity in sat/vB. mempool.space's
|
||||
/// `/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 {
|
||||
fastest_fee: median_fee_for_block(stats, 0),
|
||||
half_hour_fee: median_fee_for_block(stats, 2),
|
||||
hour_fee: median_fee_for_block(stats, 5),
|
||||
economy_fee: median_fee_for_block(stats, 7),
|
||||
minimum_fee: FeeRate::MIN,
|
||||
fastest_fee: fastest.round_milli(),
|
||||
half_hour_fee: half_hour.round_milli(),
|
||||
hour_fee: hour.round_milli(),
|
||||
economy_fee: economy.round_milli(),
|
||||
minimum_fee: minimum_fee.round_milli(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the median fee rate for block N.
|
||||
fn median_fee_for_block(stats: &[BlockStats], block_index: usize) -> FeeRate {
|
||||
stats
|
||||
.get(block_index)
|
||||
.map(|s| s.median_fee_rate())
|
||||
.unwrap_or_else(|| FeeRate::MIN)
|
||||
/// Pick the fee for one projected block, smoothing toward the
|
||||
/// previous tier and discounting partially-full final blocks.
|
||||
fn optimize_median_fee(
|
||||
block: &BlockStats,
|
||||
next_block: Option<&BlockStats>,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
|
||||
use brk_types::RecommendedFees;
|
||||
use brk_types::{FeeRate, RecommendedFees};
|
||||
|
||||
use super::{
|
||||
super::block_builder::Package,
|
||||
@@ -27,13 +27,15 @@ pub struct Snapshot {
|
||||
|
||||
impl Snapshot {
|
||||
/// 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
|
||||
.iter()
|
||||
.map(|block| stats::compute_block_stats(block, entries))
|
||||
.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
|
||||
.into_iter()
|
||||
|
||||
@@ -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 derive_more::Deref;
|
||||
@@ -51,7 +54,6 @@ impl AddrTracker {
|
||||
}
|
||||
|
||||
fn update(&mut self, tx: &Transaction, txid: &Txid, is_addition: bool) {
|
||||
// Inputs: track sending
|
||||
for txin in &tx.input {
|
||||
let Some(prevout) = txin.prevout.as_ref() else {
|
||||
continue;
|
||||
@@ -59,33 +61,57 @@ impl AddrTracker {
|
||||
let Some(bytes) = prevout.addr_bytes() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (stats, txids) = self.0.entry(bytes).or_default();
|
||||
if is_addition {
|
||||
txids.insert(txid.clone());
|
||||
stats.sending(prevout);
|
||||
} else {
|
||||
txids.remove(txid);
|
||||
stats.sent(prevout);
|
||||
}
|
||||
stats.update_tx_count(txids.len() as u32);
|
||||
self.apply(bytes, txid, is_addition, |stats| {
|
||||
if is_addition {
|
||||
stats.sending(prevout);
|
||||
} else {
|
||||
stats.sent(prevout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Outputs: track receiving
|
||||
for txout in &tx.output {
|
||||
let Some(bytes) = txout.addr_bytes() else {
|
||||
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();
|
||||
if is_addition {
|
||||
txids.insert(txid.clone());
|
||||
stats.receiving(txout);
|
||||
} else {
|
||||
txids.remove(txid);
|
||||
stats.received(txout);
|
||||
fn apply(
|
||||
&mut self,
|
||||
bytes: AddrBytes,
|
||||
txid: &Txid,
|
||||
is_addition: bool,
|
||||
update_stats: impl FnOnce(&mut AddrMempoolStats),
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ use std::{thread::sleep, time::Duration};
|
||||
|
||||
use bitcoin::{consensus::encode, hex::FromHex};
|
||||
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::{
|
||||
GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, GetBlockVerboseOne,
|
||||
GetBlockVerboseZero, GetBlockchainInfo, GetRawMempool, GetRawMempoolVerbose, GetTxOut,
|
||||
GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose,
|
||||
GetTxOut,
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
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
|
||||
pub fn get_raw_mempool(&self) -> Result<Vec<Txid>> {
|
||||
let r: GetRawMempool = self.0.call_with_retry("getrawmempool", &[])?;
|
||||
|
||||
@@ -119,10 +119,7 @@ fn cost_basis_formatted(
|
||||
FxHashMap::with_capacity_and_hasher(raw.map.len(), Default::default());
|
||||
for (&price_cents, &sats) in &raw.map {
|
||||
let price = Cents::from(price_cents);
|
||||
let key = match agg {
|
||||
UrpdAggregation::Raw => price,
|
||||
_ => agg.bucket_floor(price).unwrap_or(price),
|
||||
};
|
||||
let key = agg.bucket_floor(price);
|
||||
let entry = bucketed.entry(key).or_insert((Sats::ZERO, Dollars::ZERO));
|
||||
entry.0 += sats;
|
||||
if needs_realized {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
ops::{Add, AddAssign, Div},
|
||||
ops::{Add, AddAssign, Div, Mul},
|
||||
};
|
||||
|
||||
use schemars::JsonSchema;
|
||||
@@ -24,9 +24,35 @@ pub struct FeeRate(f64);
|
||||
impl FeeRate {
|
||||
pub const MIN: Self = Self(0.1);
|
||||
|
||||
pub fn new(fr: f64) -> Self {
|
||||
pub const fn new(fr: f64) -> Self {
|
||||
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 {
|
||||
@@ -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 {
|
||||
#[inline]
|
||||
fn from(value: usize) -> Self {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use schemars::JsonSchema;
|
||||
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.
|
||||
///
|
||||
/// 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)`
|
||||
/// pairs: supply in BTC, realized cap contribution in USD (`price_floor * supply`),
|
||||
/// and unrealized P&L against that date's close in USD
|
||||
/// (`(close - price_floor) * supply`, can be negative).
|
||||
/// Each bucket exposes three values: supply in BTC, realized cap contribution
|
||||
/// in USD (sum of `realized_price * supply` over the coins in the bucket), and
|
||||
/// unrealized P&L in USD (`close * supply - realized_cap`, can be negative).
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct Urpd {
|
||||
pub cohort: Cohort,
|
||||
@@ -26,6 +26,12 @@ pub struct Urpd {
|
||||
pub buckets: Vec<UrpdBucket>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct BucketAccum {
|
||||
supply: Sats,
|
||||
realized_cap: CentsSats,
|
||||
}
|
||||
|
||||
impl Urpd {
|
||||
/// Build from the raw on-disk distribution plus context.
|
||||
pub fn build(
|
||||
@@ -35,39 +41,44 @@ impl Urpd {
|
||||
raw: &UrpdRaw,
|
||||
aggregation: UrpdAggregation,
|
||||
) -> Self {
|
||||
let mut agg: FxHashMap<Cents, Sats> =
|
||||
let mut agg: FxHashMap<Cents, BucketAccum> =
|
||||
FxHashMap::with_capacity_and_hasher(raw.map.len(), Default::default());
|
||||
for (&price_cents, &sats) in &raw.map {
|
||||
let price = Cents::from(price_cents);
|
||||
let key = match aggregation {
|
||||
UrpdAggregation::Raw => price,
|
||||
_ => aggregation.bucket_floor(price).unwrap_or(price),
|
||||
};
|
||||
*agg.entry(key).or_insert(Sats::ZERO) += sats;
|
||||
let key = aggregation.bucket_floor(price);
|
||||
let slot = agg.entry(key).or_default();
|
||||
slot.supply += sats;
|
||||
slot.realized_cap += CentsSats::from_price_sats(price, 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 total_supply: Sats = raw.map.values().copied().sum();
|
||||
|
||||
let mut total_sats = Sats::ZERO;
|
||||
let mut buckets = Vec::with_capacity(sorted.len());
|
||||
for (price_floor_cents, supply) in sorted {
|
||||
total_sats += supply;
|
||||
let price_floor = Dollars::from(price_floor_cents);
|
||||
buckets.push(UrpdBucket {
|
||||
price_floor,
|
||||
supply: Bitcoin::from(supply),
|
||||
realized_cap: price_floor * supply,
|
||||
unrealized_pnl: (close - price_floor) * supply,
|
||||
});
|
||||
}
|
||||
let buckets = sorted
|
||||
.into_iter()
|
||||
.map(|(price_floor_cents, slot)| {
|
||||
let realized_cap_cents = slot.realized_cap.to_cents();
|
||||
let close_mc_cents = CentsSats::from_price_sats(close_cents, slot.supply).to_cents();
|
||||
let pnl = CentsSigned::from(close_mc_cents.inner())
|
||||
- CentsSigned::from(realized_cap_cents.inner());
|
||||
UrpdBucket {
|
||||
price_floor: Dollars::from(price_floor_cents),
|
||||
supply: Bitcoin::from(slot.supply),
|
||||
realized_cap: Dollars::from(realized_cap_cents),
|
||||
unrealized_pnl: Dollars::from(pnl),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
cohort,
|
||||
date,
|
||||
aggregation,
|
||||
close,
|
||||
total_supply: Bitcoin::from(total_sats),
|
||||
total_supply: Bitcoin::from(total_supply),
|
||||
buckets,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,26 +46,24 @@ impl UrpdAggregation {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute bucket floor for a given price in cents.
|
||||
/// Returns None for Raw (no bucketing).
|
||||
pub fn bucket_floor(&self, price_cents: Cents) -> Option<Cents> {
|
||||
/// Compute the bucket floor for a price in cents.
|
||||
/// `Raw` is the identity (no bucketing).
|
||||
pub fn bucket_floor(&self, price_cents: Cents) -> Cents {
|
||||
match self {
|
||||
Self::Raw => None,
|
||||
Self::Raw => price_cents,
|
||||
Self::Lin200 | Self::Lin500 | Self::Lin1000 => {
|
||||
let size = self.linear_size_cents().unwrap();
|
||||
Some((price_cents / size) * size)
|
||||
(price_cents / size) * size
|
||||
}
|
||||
Self::Log10 | Self::Log50 | Self::Log100 | Self::Log200 => {
|
||||
if price_cents == Cents::ZERO {
|
||||
return Some(Cents::ZERO);
|
||||
return Cents::ZERO;
|
||||
}
|
||||
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 bucket_idx = (n as f64 * log_price).floor() as i32;
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ use crate::{Bitcoin, Dollars};
|
||||
/// A single bucket in a URPD snapshot.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
|
||||
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,
|
||||
/// Supply held with a last-move price inside this bucket, in BTC.
|
||||
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,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user