global: MASSIVE snapshot

This commit is contained in:
nym21
2026-02-23 17:22:12 +01:00
parent be0d749f9c
commit 3b7aa8242a
703 changed files with 29130 additions and 30779 deletions

View File

@@ -1,6 +1,6 @@
use std::ops::{Add, AddAssign, SubAssign};
use brk_types::{CentsUnsigned, SupplyState, Timestamp};
use brk_types::{Cents, SupplyState, Timestamp};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
@@ -8,7 +8,7 @@ pub struct BlockState {
#[serde(flatten)]
pub supply: SupplyState,
#[serde(skip)]
pub price: Option<CentsUnsigned>,
pub price: Cents,
#[serde(skip)]
pub timestamp: Timestamp,
}

View File

@@ -1,7 +1,7 @@
use std::path::Path;
use brk_error::Result;
use brk_types::{Age, CentsUnsigned, FundedAddressData, Height, Sats, SupplyState};
use brk_types::{Age, Cents, FundedAddressData, Sats, SupplyState};
use vecdb::unlikely;
use super::{super::cost_basis::RealizedState, base::CohortState};
@@ -9,52 +9,45 @@ use super::{super::cost_basis::RealizedState, base::CohortState};
/// Significant digits for address cost basis prices (after rounding to dollars).
const COST_BASIS_PRICE_DIGITS: i32 = 4;
#[derive(Clone)]
pub struct AddressCohortState {
pub addr_count: u64,
pub inner: CohortState,
}
impl AddressCohortState {
pub fn new(path: &Path, name: &str, compute_dollars: bool) -> Self {
pub(crate) fn new(path: &Path, name: &str) -> Self {
Self {
addr_count: 0,
inner: CohortState::new(path, name, compute_dollars)
inner: CohortState::new(path, name)
.with_price_rounding(COST_BASIS_PRICE_DIGITS),
}
}
/// Reset state for fresh start.
pub fn reset(&mut self) {
pub(crate) fn reset(&mut self) {
self.addr_count = 0;
self.inner.supply = SupplyState::default();
self.inner.sent = Sats::ZERO;
self.inner.satblocks_destroyed = Sats::ZERO;
self.inner.satdays_destroyed = Sats::ZERO;
if let Some(realized) = self.inner.realized.as_mut() {
*realized = RealizedState::default();
}
self.inner.realized = RealizedState::default();
}
pub fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
pub(crate) fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
self.inner.reset_cost_basis_data_if_needed()
}
pub fn reset_single_iteration_values(&mut self) {
self.inner.reset_single_iteration_values();
}
pub fn send(
pub(crate) fn send(
&mut self,
addressdata: &mut FundedAddressData,
value: Sats,
current_price: CentsUnsigned,
prev_price: CentsUnsigned,
ath: CentsUnsigned,
current_price: Cents,
prev_price: Cents,
ath: Cents,
age: Age,
) -> Result<()> {
let prev = addressdata.cost_basis_snapshot();
addressdata.send(value, Some(prev_price))?;
addressdata.send(value, prev_price)?;
let current = addressdata.cost_basis_snapshot();
self.inner.send_address(
@@ -73,24 +66,15 @@ impl AddressCohortState {
Ok(())
}
pub fn receive(
pub(crate) fn receive_outputs(
&mut self,
address_data: &mut FundedAddressData,
value: Sats,
price: CentsUnsigned,
) {
self.receive_outputs(address_data, value, price, 1);
}
pub fn receive_outputs(
&mut self,
address_data: &mut FundedAddressData,
value: Sats,
price: CentsUnsigned,
price: Cents,
output_count: u32,
) {
let prev = address_data.cost_basis_snapshot();
address_data.receive_outputs(value, Some(price), output_count);
address_data.receive_outputs(value, price, output_count);
let current = address_data.cost_basis_snapshot();
self.inner.receive_address(
@@ -104,13 +88,13 @@ impl AddressCohortState {
);
}
pub fn add(&mut self, addressdata: &FundedAddressData) {
pub(crate) fn add(&mut self, addressdata: &FundedAddressData) {
self.addr_count += 1;
self.inner
.increment_snapshot(&addressdata.cost_basis_snapshot());
}
pub fn subtract(&mut self, addressdata: &FundedAddressData) {
pub(crate) fn subtract(&mut self, addressdata: &FundedAddressData) {
let snapshot = addressdata.cost_basis_snapshot();
// Check for potential underflow before it happens
@@ -157,7 +141,4 @@ impl AddressCohortState {
self.inner.decrement_snapshot(&snapshot);
}
pub fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
self.inner.write(height, cleanup)
}
}

View File

@@ -1,110 +1,89 @@
use std::path::Path;
use brk_error::Result;
use brk_types::{Age, CentsSats, CentsUnsigned, CostBasisSnapshot, Height, Sats, SupplyState};
use brk_types::{Age, CentsSats, Cents, CostBasisSnapshot, Height, Sats, SupplyState};
use super::super::cost_basis::{CostBasisData, Percentiles, RealizedState, UnrealizedState};
#[derive(Clone)]
pub struct CohortState {
pub supply: SupplyState,
pub realized: Option<RealizedState>,
pub realized: RealizedState,
pub sent: Sats,
pub satblocks_destroyed: Sats,
pub satdays_destroyed: Sats,
cost_basis_data: Option<CostBasisData>,
cost_basis_data: CostBasisData,
}
impl CohortState {
pub fn new(path: &Path, name: &str, compute_dollars: bool) -> Self {
pub(crate) fn new(path: &Path, name: &str) -> Self {
Self {
supply: SupplyState::default(),
realized: compute_dollars.then_some(RealizedState::default()),
realized: RealizedState::default(),
sent: Sats::ZERO,
satblocks_destroyed: Sats::ZERO,
satdays_destroyed: Sats::ZERO,
cost_basis_data: compute_dollars.then_some(CostBasisData::create(path, name)),
cost_basis_data: CostBasisData::create(path, name),
}
}
/// Enable price rounding for cost basis data.
pub fn with_price_rounding(mut self, digits: i32) -> Self {
if let Some(data) = self.cost_basis_data.take() {
self.cost_basis_data = Some(data.with_price_rounding(digits));
}
pub(crate) fn with_price_rounding(mut self, digits: i32) -> Self {
self.cost_basis_data = self.cost_basis_data.with_price_rounding(digits);
self
}
pub fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
match self.cost_basis_data.as_mut() {
Some(p) => p.import_at_or_before(height),
None => Ok(height),
}
pub(crate) fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
self.cost_basis_data.import_at_or_before(height)
}
/// Restore realized cap from cost_basis_data after import.
/// Uses the exact persisted values instead of recomputing from the map.
pub fn restore_realized_cap(&mut self) {
if let Some(cost_basis_data) = self.cost_basis_data.as_ref()
&& let Some(realized) = self.realized.as_mut()
{
realized.set_cap_raw(cost_basis_data.cap_raw());
realized.set_investor_cap_raw(cost_basis_data.investor_cap_raw());
}
pub(crate) fn restore_realized_cap(&mut self) {
self.realized.set_cap_raw(self.cost_basis_data.cap_raw());
self.realized
.set_investor_cap_raw(self.cost_basis_data.investor_cap_raw());
}
pub fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
if let Some(p) = self.cost_basis_data.as_mut() {
p.clean()?;
p.init();
}
pub(crate) fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
self.cost_basis_data.clean()?;
self.cost_basis_data.init();
Ok(())
}
pub fn apply_pending(&mut self) {
if let Some(p) = self.cost_basis_data.as_mut() {
p.apply_pending();
}
pub(crate) fn apply_pending(&mut self) {
self.cost_basis_data.apply_pending();
}
pub fn cost_basis_data_first_key_value(&self) -> Option<(CentsUnsigned, &Sats)> {
pub(crate) fn cost_basis_data_first_key_value(&self) -> Option<(Cents, &Sats)> {
self.cost_basis_data
.as_ref()?
.first_key_value()
.map(|(k, v)| (k.into(), v))
}
pub fn cost_basis_data_last_key_value(&self) -> Option<(CentsUnsigned, &Sats)> {
pub(crate) fn cost_basis_data_last_key_value(&self) -> Option<(Cents, &Sats)> {
self.cost_basis_data
.as_ref()?
.last_key_value()
.map(|(k, v)| (k.into(), v))
}
pub fn reset_single_iteration_values(&mut self) {
pub(crate) fn reset_single_iteration_values(&mut self) {
self.sent = Sats::ZERO;
self.satdays_destroyed = Sats::ZERO;
self.satblocks_destroyed = Sats::ZERO;
if let Some(realized) = self.realized.as_mut() {
realized.reset_single_iteration_values();
}
self.realized.reset_single_iteration_values();
}
pub fn increment(&mut self, supply: &SupplyState, price: Option<CentsUnsigned>) {
match price {
Some(p) => self.increment_snapshot(&CostBasisSnapshot::from_utxo(p, supply)),
None => self.supply += supply,
}
pub(crate) fn increment(&mut self, supply: &SupplyState, price: Cents) {
self.increment_snapshot(&CostBasisSnapshot::from_utxo(price, supply));
}
pub fn increment_snapshot(&mut self, s: &CostBasisSnapshot) {
pub(crate) fn increment_snapshot(&mut self, s: &CostBasisSnapshot) {
self.supply += &s.supply_state;
if s.supply_state.value > Sats::ZERO
&& let Some(realized) = self.realized.as_mut()
{
realized.increment_snapshot(s.price_sats, s.investor_cap);
self.cost_basis_data.as_mut().unwrap().increment(
if s.supply_state.value > Sats::ZERO {
self.realized
.increment_snapshot(s.price_sats, s.investor_cap);
self.cost_basis_data.increment(
s.realized_price,
s.supply_state.value,
s.price_sats,
@@ -113,21 +92,17 @@ impl CohortState {
}
}
pub fn decrement(&mut self, supply: &SupplyState, price: Option<CentsUnsigned>) {
match price {
Some(p) => self.decrement_snapshot(&CostBasisSnapshot::from_utxo(p, supply)),
None => self.supply -= supply,
}
pub(crate) fn decrement(&mut self, supply: &SupplyState, price: Cents) {
self.decrement_snapshot(&CostBasisSnapshot::from_utxo(price, supply));
}
pub fn decrement_snapshot(&mut self, s: &CostBasisSnapshot) {
pub(crate) fn decrement_snapshot(&mut self, s: &CostBasisSnapshot) {
self.supply -= &s.supply_state;
if s.supply_state.value > Sats::ZERO
&& let Some(realized) = self.realized.as_mut()
{
realized.decrement_snapshot(s.price_sats, s.investor_cap);
self.cost_basis_data.as_mut().unwrap().decrement(
if s.supply_state.value > Sats::ZERO {
self.realized
.decrement_snapshot(s.price_sats, s.investor_cap);
self.cost_basis_data.decrement(
s.realized_price,
s.supply_state.value,
s.price_sats,
@@ -136,44 +111,37 @@ impl CohortState {
}
}
pub fn receive_utxo(&mut self, supply: &SupplyState, price: Option<CentsUnsigned>) {
pub(crate) fn receive_utxo(&mut self, supply: &SupplyState, price: Cents) {
self.supply += supply;
if supply.value > Sats::ZERO
&& let Some(realized) = self.realized.as_mut()
{
let price = price.unwrap();
if supply.value > Sats::ZERO {
let sats = supply.value;
// Compute once using typed values
let price_sats = CentsSats::from_price_sats(price, sats);
let investor_cap = price_sats.to_investor_cap(price);
realized.receive(price, sats);
self.realized.receive(price, sats);
self.cost_basis_data
.as_mut()
.unwrap()
.increment(price, sats, price_sats, investor_cap);
}
}
pub fn receive_address(
pub(crate) fn receive_address(
&mut self,
supply: &SupplyState,
price: CentsUnsigned,
price: Cents,
current: &CostBasisSnapshot,
prev: &CostBasisSnapshot,
) {
self.supply += supply;
if supply.value > Sats::ZERO
&& let Some(realized) = self.realized.as_mut()
{
realized.receive(price, supply.value);
if supply.value > Sats::ZERO {
self.realized.receive(price, supply.value);
if current.supply_state.value.is_not_zero() {
self.cost_basis_data.as_mut().unwrap().increment(
self.cost_basis_data.increment(
current.realized_price,
current.supply_state.value,
current.price_sats,
@@ -182,7 +150,7 @@ impl CohortState {
}
if prev.supply_state.value.is_not_zero() {
self.cost_basis_data.as_mut().unwrap().decrement(
self.cost_basis_data.decrement(
prev.realized_price,
prev.supply_state.value,
prev.price_sats,
@@ -192,12 +160,12 @@ impl CohortState {
}
}
pub fn send_utxo(
pub(crate) fn send_utxo(
&mut self,
supply: &SupplyState,
current_price: Option<CentsUnsigned>,
prev_price: Option<CentsUnsigned>,
ath: Option<CentsUnsigned>,
current_price: Cents,
prev_price: Cents,
ath: Cents,
age: Age,
) {
if supply.utxo_count == 0 {
@@ -211,37 +179,32 @@ impl CohortState {
self.satblocks_destroyed += age.satblocks_destroyed(supply.value);
self.satdays_destroyed += age.satdays_destroyed(supply.value);
if let Some(realized) = self.realized.as_mut() {
let cp = current_price.unwrap();
let pp = prev_price.unwrap();
let ath_price = ath.unwrap();
let sats = supply.value;
let cp = current_price;
let pp = prev_price;
let ath_price = ath;
let sats = supply.value;
// Compute ONCE using typed values
let current_ps = CentsSats::from_price_sats(cp, sats);
let prev_ps = CentsSats::from_price_sats(pp, sats);
let ath_ps = CentsSats::from_price_sats(ath_price, sats);
let prev_investor_cap = prev_ps.to_investor_cap(pp);
// Compute ONCE using typed values
let current_ps = CentsSats::from_price_sats(cp, sats);
let prev_ps = CentsSats::from_price_sats(pp, sats);
let ath_ps = CentsSats::from_price_sats(ath_price, sats);
let prev_investor_cap = prev_ps.to_investor_cap(pp);
realized.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
self.realized
.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
self.cost_basis_data.as_mut().unwrap().decrement(
pp,
sats,
prev_ps,
prev_investor_cap,
);
}
self.cost_basis_data
.decrement(pp, sats, prev_ps, prev_investor_cap);
}
}
#[allow(clippy::too_many_arguments)]
pub fn send_address(
pub(crate) fn send_address(
&mut self,
supply: &SupplyState,
current_price: CentsUnsigned,
prev_price: CentsUnsigned,
ath: CentsUnsigned,
current_price: Cents,
prev_price: Cents,
ath: Cents,
age: Age,
current: &CostBasisSnapshot,
prev: &CostBasisSnapshot,
@@ -257,80 +220,55 @@ impl CohortState {
self.satblocks_destroyed += age.satblocks_destroyed(supply.value);
self.satdays_destroyed += age.satdays_destroyed(supply.value);
if let Some(realized) = self.realized.as_mut() {
let sats = supply.value;
let sats = supply.value;
// Compute once for realized.send using typed values
let current_ps = CentsSats::from_price_sats(current_price, sats);
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
let ath_ps = CentsSats::from_price_sats(ath, sats);
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
// Compute once for realized.send using typed values
let current_ps = CentsSats::from_price_sats(current_price, sats);
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
let ath_ps = CentsSats::from_price_sats(ath, sats);
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
realized.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
self.realized
.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
if current.supply_state.value.is_not_zero() {
self.cost_basis_data.as_mut().unwrap().increment(
current.realized_price,
current.supply_state.value,
current.price_sats,
current.investor_cap,
);
}
if current.supply_state.value.is_not_zero() {
self.cost_basis_data.increment(
current.realized_price,
current.supply_state.value,
current.price_sats,
current.investor_cap,
);
}
if prev.supply_state.value.is_not_zero() {
self.cost_basis_data.as_mut().unwrap().decrement(
prev.realized_price,
prev.supply_state.value,
prev.price_sats,
prev.investor_cap,
);
}
if prev.supply_state.value.is_not_zero() {
self.cost_basis_data.decrement(
prev.realized_price,
prev.supply_state.value,
prev.price_sats,
prev.investor_cap,
);
}
}
}
pub fn compute_percentiles(&self) -> Option<Percentiles> {
self.cost_basis_data.as_ref()?.compute_percentiles()
pub(crate) fn compute_percentiles(&mut self) -> Option<Percentiles> {
self.cost_basis_data.compute_percentiles()
}
pub fn compute_unrealized_states(
pub(crate) fn compute_unrealized_states(
&mut self,
height_price: CentsUnsigned,
date_price: Option<CentsUnsigned>,
height_price: Cents,
date_price: Option<Cents>,
) -> (UnrealizedState, Option<UnrealizedState>) {
match self.cost_basis_data.as_mut() {
Some(p) => p.compute_unrealized_states(height_price, date_price),
None => (
UnrealizedState::ZERO,
date_price.map(|_| UnrealizedState::ZERO),
),
}
}
pub fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
if let Some(p) = self.cost_basis_data.as_mut() {
p.write(height, cleanup)?;
}
Ok(())
}
pub fn min_price(&self) -> Option<CentsUnsigned> {
self.cost_basis_data
.as_ref()?
.first_key_value()
.map(|(k, _)| k.into())
.compute_unrealized_states(height_price, date_price)
}
pub fn max_price(&self) -> Option<CentsUnsigned> {
self.cost_basis_data
.as_ref()?
.last_key_value()
.map(|(k, _)| k.into())
pub(crate) fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
self.cost_basis_data.write(height, cleanup)
}
pub fn cost_basis_data_iter(&self) -> Option<impl Iterator<Item = (CentsUnsigned, &Sats)>> {
self.cost_basis_data
.as_ref()
.map(|p| p.iter().map(|(k, v)| (k.into(), v)))
pub(crate) fn cost_basis_data_iter(&self) -> impl Iterator<Item = (Cents, &Sats)> {
self.cost_basis_data.iter().map(|(k, v)| (k.into(), v))
}
}

View File

@@ -6,26 +6,24 @@ use derive_more::{Deref, DerefMut};
use super::{super::cost_basis::RealizedState, base::CohortState};
#[derive(Clone, Deref, DerefMut)]
#[derive(Deref, DerefMut)]
pub struct UTXOCohortState(CohortState);
impl UTXOCohortState {
pub fn new(path: &Path, name: &str, compute_dollars: bool) -> Self {
Self(CohortState::new(path, name, compute_dollars))
pub(crate) fn new(path: &Path, name: &str) -> Self {
Self(CohortState::new(path, name))
}
pub fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
pub(crate) fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
self.0.reset_cost_basis_data_if_needed()
}
/// Reset state for fresh start.
pub fn reset(&mut self) {
pub(crate) fn reset(&mut self) {
self.0.supply = SupplyState::default();
self.0.sent = Sats::ZERO;
self.0.satblocks_destroyed = Sats::ZERO;
self.0.satdays_destroyed = Sats::ZERO;
if let Some(realized) = self.0.realized.as_mut() {
*realized = RealizedState::default();
}
self.0.realized = RealizedState::default();
}
}

View File

@@ -6,8 +6,7 @@ use std::{
use brk_error::{Error, Result};
use brk_types::{
CentsSats, CentsSquaredSats, CentsUnsigned, CentsUnsignedCompact, CostBasisDistribution,
Height, Sats,
CentsCompact, CentsSats, CentsSquaredSats, Cents, CostBasisDistribution, Height, Sats,
};
use rustc_hash::FxHashMap;
use vecdb::Bytes;
@@ -17,7 +16,7 @@ use crate::utils::OptionExt;
use super::{CachedUnrealizedState, Percentiles, UnrealizedState};
/// Type alias for the price-to-sats map used in cost basis data.
pub(super) type CostBasisMap = BTreeMap<CentsUnsignedCompact, Sats>;
pub(super) type CostBasisMap = BTreeMap<CentsCompact, Sats>;
#[derive(Clone, Debug, Default)]
struct PendingRaw {
@@ -31,40 +30,44 @@ struct PendingRaw {
pub struct CostBasisData {
pathbuf: PathBuf,
state: Option<State>,
pending: FxHashMap<CentsUnsignedCompact, (Sats, Sats)>,
pending: FxHashMap<CentsCompact, (Sats, Sats)>,
pending_raw: PendingRaw,
cache: Option<CachedUnrealizedState>,
percentiles_dirty: bool,
cached_percentiles: Option<Percentiles>,
rounding_digits: Option<i32>,
}
const STATE_TO_KEEP: usize = 10;
impl CostBasisData {
pub fn create(path: &Path, name: &str) -> Self {
pub(crate) fn create(path: &Path, name: &str) -> Self {
Self {
pathbuf: path.join(format!("{name}_cost_basis")),
state: None,
pending: FxHashMap::default(),
pending_raw: PendingRaw::default(),
cache: None,
percentiles_dirty: true,
cached_percentiles: None,
rounding_digits: None,
}
}
pub fn with_price_rounding(mut self, digits: i32) -> Self {
pub(crate) fn with_price_rounding(mut self, digits: i32) -> Self {
self.rounding_digits = Some(digits);
self
}
#[inline]
fn round_price(&self, price: CentsUnsigned) -> CentsUnsigned {
fn round_price(&self, price: Cents) -> Cents {
match self.rounding_digits {
Some(digits) => price.round_to_dollar(digits),
None => price,
}
}
pub fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
pub(crate) fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
let files = self.read_dir(None)?;
let (&height, path) = files.range(..=height).next_back().ok_or(Error::NotFound(
"No cost basis state found at or before height".into(),
@@ -73,6 +76,8 @@ impl CostBasisData {
self.pending.clear();
self.pending_raw = PendingRaw::default();
self.cache = None;
self.percentiles_dirty = true;
self.cached_percentiles = None;
Ok(height)
}
@@ -90,16 +95,16 @@ impl CostBasisData {
&& self.pending_raw.investor_cap_dec == CentsSquaredSats::ZERO
}
pub fn iter(&self) -> impl Iterator<Item = (CentsUnsignedCompact, &Sats)> {
pub(crate) fn iter(&self) -> impl Iterator<Item = (CentsCompact, &Sats)> {
self.assert_pending_empty();
self.state.u().base.map.iter().map(|(&k, v)| (k, v))
}
pub fn is_empty(&self) -> bool {
pub(crate) fn is_empty(&self) -> bool {
self.pending.is_empty() && self.state.u().base.map.is_empty()
}
pub fn first_key_value(&self) -> Option<(CentsUnsignedCompact, &Sats)> {
pub(crate) fn first_key_value(&self) -> Option<(CentsCompact, &Sats)> {
self.assert_pending_empty();
self.state
.u()
@@ -109,7 +114,7 @@ impl CostBasisData {
.map(|(&k, v)| (k, v))
}
pub fn last_key_value(&self) -> Option<(CentsUnsignedCompact, &Sats)> {
pub(crate) fn last_key_value(&self) -> Option<(CentsCompact, &Sats)> {
self.assert_pending_empty();
self.state
.u()
@@ -120,22 +125,22 @@ impl CostBasisData {
}
/// Get the exact cap_raw value (not recomputed from map).
pub fn cap_raw(&self) -> CentsSats {
pub(crate) fn cap_raw(&self) -> CentsSats {
self.assert_pending_empty();
self.state.u().cap_raw
}
/// Get the exact investor_cap_raw value (not recomputed from map).
pub fn investor_cap_raw(&self) -> CentsSquaredSats {
pub(crate) fn investor_cap_raw(&self) -> CentsSquaredSats {
self.assert_pending_empty();
self.state.u().investor_cap_raw
}
/// Increment with pre-computed typed values.
/// Handles rounding and cache update.
pub fn increment(
pub(crate) fn increment(
&mut self,
price: CentsUnsigned,
price: Cents,
sats: Sats,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
@@ -153,9 +158,9 @@ impl CostBasisData {
/// Decrement with pre-computed typed values.
/// Handles rounding and cache update.
pub fn decrement(
pub(crate) fn decrement(
&mut self,
price: CentsUnsigned,
price: Cents,
sats: Sats,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
@@ -171,7 +176,10 @@ impl CostBasisData {
}
}
pub fn apply_pending(&mut self) {
pub(crate) fn apply_pending(&mut self) {
if !self.pending.is_empty() {
self.percentiles_dirty = true;
}
for (cents, (inc, dec)) in self.pending.drain() {
let entry = self.state.um().base.map.entry(cents).or_default();
*entry += inc;
@@ -232,25 +240,35 @@ impl CostBasisData {
self.pending_raw = PendingRaw::default();
}
pub fn init(&mut self) {
pub(crate) fn init(&mut self) {
self.state.replace(State::default());
self.pending.clear();
self.pending_raw = PendingRaw::default();
self.cache = None;
self.percentiles_dirty = true;
self.cached_percentiles = None;
}
pub fn compute_percentiles(&self) -> Option<Percentiles> {
pub(crate) fn compute_percentiles(&mut self) -> Option<Percentiles> {
self.assert_pending_empty();
Percentiles::compute(self.iter().map(|(k, &v)| (k, v)))
if !self.percentiles_dirty {
return self.cached_percentiles;
}
self.cached_percentiles = Percentiles::compute(self.iter().map(|(k, &v)| (k, v)));
self.percentiles_dirty = false;
self.cached_percentiles
}
pub fn compute_unrealized_states(
pub(crate) fn compute_unrealized_states(
&mut self,
height_price: CentsUnsigned,
date_price: Option<CentsUnsigned>,
height_price: Cents,
date_price: Option<Cents>,
) -> (UnrealizedState, Option<UnrealizedState>) {
if self.is_empty() {
return (UnrealizedState::ZERO, date_price.map(|_| UnrealizedState::ZERO));
return (
UnrealizedState::ZERO,
date_price.map(|_| UnrealizedState::ZERO),
);
}
let map = &self.state.u().base.map;
@@ -270,7 +288,7 @@ impl CostBasisData {
(height_state, date_state)
}
pub fn clean(&mut self) -> Result<()> {
pub(crate) fn clean(&mut self) -> Result<()> {
let _ = fs::remove_dir_all(&self.pathbuf);
fs::create_dir_all(self.path_by_height())?;
self.cache = None;
@@ -304,7 +322,7 @@ impl CostBasisData {
.collect::<BTreeMap<Height, PathBuf>>())
}
pub fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
pub(crate) fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
self.apply_pending();
if cleanup {

View File

@@ -1,19 +1,19 @@
use brk_types::{CentsUnsigned, CentsUnsignedCompact, Sats};
use brk_types::{Cents, CentsCompact, Sats};
use crate::internal::{PERCENTILES, PERCENTILES_LEN};
#[derive(Clone, Copy, Debug)]
pub struct Percentiles {
/// Sat-weighted: percentiles by coin count
pub sat_weighted: [CentsUnsigned; PERCENTILES_LEN],
pub sat_weighted: [Cents; PERCENTILES_LEN],
/// USD-weighted: percentiles by invested capital (sats × price)
pub usd_weighted: [CentsUnsigned; PERCENTILES_LEN],
pub usd_weighted: [Cents; PERCENTILES_LEN],
}
impl Percentiles {
/// Compute both sat-weighted and USD-weighted percentiles in a single pass.
/// Takes an iterator over (price, sats) pairs, assumed sorted by price ascending.
pub fn compute(iter: impl Iterator<Item = (CentsUnsignedCompact, Sats)>) -> Option<Self> {
pub(crate) fn compute(iter: impl Iterator<Item = (CentsCompact, Sats)>) -> Option<Self> {
// Collect to allow two passes: one for totals, one for percentiles
let entries: Vec<_> = iter.collect();
if entries.is_empty() {
@@ -32,8 +32,8 @@ impl Percentiles {
return None;
}
let mut sat_weighted = [CentsUnsigned::ZERO; PERCENTILES_LEN];
let mut usd_weighted = [CentsUnsigned::ZERO; PERCENTILES_LEN];
let mut sat_weighted = [Cents::ZERO; PERCENTILES_LEN];
let mut usd_weighted = [Cents::ZERO; PERCENTILES_LEN];
let mut cumsum_sats: u64 = 0;
let mut cumsum_usd: u128 = 0;
let mut sat_idx = 0;

View File

@@ -1,6 +1,6 @@
use std::cmp::Ordering;
use brk_types::{CentsSats, CentsSquaredSats, CentsUnsigned, Sats};
use brk_types::{CentsSats, CentsSquaredSats, Cents, Sats};
/// Realized state using u128 for raw cent*sat values internally.
/// This avoids overflow and defers division to output time for efficiency.
@@ -34,19 +34,19 @@ pub struct RealizedState {
impl RealizedState {
/// Get realized cap as CentsUnsigned (divides by ONE_BTC).
#[inline]
pub fn cap(&self) -> CentsUnsigned {
CentsUnsigned::new((self.cap_raw / Sats::ONE_BTC_U128) as u64)
pub(crate) fn cap(&self) -> Cents {
Cents::new((self.cap_raw / Sats::ONE_BTC_U128) as u64)
}
/// Set cap_raw directly from persisted value.
#[inline]
pub fn set_cap_raw(&mut self, cap_raw: CentsSats) {
pub(crate) fn set_cap_raw(&mut self, cap_raw: CentsSats) {
self.cap_raw = cap_raw.inner();
}
/// Set investor_cap_raw directly from persisted value.
#[inline]
pub fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats) {
pub(crate) fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats) {
self.investor_cap_raw = investor_cap_raw;
}
@@ -54,114 +54,84 @@ impl RealizedState {
/// investor_price = Σ(price² × sats) / Σ(price × sats)
/// This is the dollar-weighted average acquisition price.
#[inline]
pub fn investor_price(&self) -> CentsUnsigned {
pub(crate) fn investor_price(&self) -> Cents {
if self.cap_raw == 0 {
return CentsUnsigned::ZERO;
return Cents::ZERO;
}
CentsUnsigned::new((self.investor_cap_raw / self.cap_raw) as u64)
Cents::new((self.investor_cap_raw / self.cap_raw) as u64)
}
/// Get raw realized cap for aggregation.
#[inline]
pub fn cap_raw(&self) -> CentsSats {
pub(crate) fn cap_raw(&self) -> CentsSats {
CentsSats::new(self.cap_raw)
}
/// Get raw investor cap for aggregation.
#[inline]
pub fn investor_cap_raw(&self) -> CentsSquaredSats {
pub(crate) fn investor_cap_raw(&self) -> CentsSquaredSats {
self.investor_cap_raw
}
/// Get realized profit as CentsUnsigned.
#[inline]
pub fn profit(&self) -> CentsUnsigned {
CentsUnsigned::new((self.profit_raw / Sats::ONE_BTC_U128) as u64)
pub(crate) fn profit(&self) -> Cents {
Cents::new((self.profit_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get realized loss as CentsUnsigned.
#[inline]
pub fn loss(&self) -> CentsUnsigned {
CentsUnsigned::new((self.loss_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get value created as CentsUnsigned (derived from profit + loss splits).
#[inline]
pub fn value_created(&self) -> CentsUnsigned {
let raw = self.profit_value_created_raw + self.loss_value_created_raw;
CentsUnsigned::new((raw / Sats::ONE_BTC_U128) as u64)
}
/// Get value destroyed as CentsUnsigned (derived from profit + loss splits).
#[inline]
pub fn value_destroyed(&self) -> CentsUnsigned {
let raw = self.profit_value_destroyed_raw + self.loss_value_destroyed_raw;
CentsUnsigned::new((raw / Sats::ONE_BTC_U128) as u64)
pub(crate) fn loss(&self) -> Cents {
Cents::new((self.loss_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get profit value created as CentsUnsigned (sell_price × sats for profit cases).
#[inline]
pub fn profit_value_created(&self) -> CentsUnsigned {
CentsUnsigned::new((self.profit_value_created_raw / Sats::ONE_BTC_U128) as u64)
pub(crate) fn profit_value_created(&self) -> Cents {
Cents::new((self.profit_value_created_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get profit value destroyed as CentsUnsigned (cost_basis × sats for profit cases).
/// This is also known as profit_flow.
#[inline]
pub fn profit_value_destroyed(&self) -> CentsUnsigned {
CentsUnsigned::new((self.profit_value_destroyed_raw / Sats::ONE_BTC_U128) as u64)
pub(crate) fn profit_value_destroyed(&self) -> Cents {
Cents::new((self.profit_value_destroyed_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get loss value created as CentsUnsigned (sell_price × sats for loss cases).
#[inline]
pub fn loss_value_created(&self) -> CentsUnsigned {
CentsUnsigned::new((self.loss_value_created_raw / Sats::ONE_BTC_U128) as u64)
pub(crate) fn loss_value_created(&self) -> Cents {
Cents::new((self.loss_value_created_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get loss value destroyed as CentsUnsigned (cost_basis × sats for loss cases).
/// This is also known as capitulation_flow.
#[inline]
pub fn loss_value_destroyed(&self) -> CentsUnsigned {
CentsUnsigned::new((self.loss_value_destroyed_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get capitulation flow as CentsUnsigned.
/// This is the invested capital (cost_basis × sats) sold at a loss.
/// Alias for loss_value_destroyed.
#[inline]
pub fn capitulation_flow(&self) -> CentsUnsigned {
self.loss_value_destroyed()
}
/// Get profit flow as CentsUnsigned.
/// This is the invested capital (cost_basis × sats) sold at a profit.
/// Alias for profit_value_destroyed.
#[inline]
pub fn profit_flow(&self) -> CentsUnsigned {
self.profit_value_destroyed()
pub(crate) fn loss_value_destroyed(&self) -> Cents {
Cents::new((self.loss_value_destroyed_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get realized peak regret as CentsUnsigned.
/// This is Σ((peak - sell_price) × sats) - how much more could have been made
/// by selling at peak instead of when actually sold.
#[inline]
pub fn peak_regret(&self) -> CentsUnsigned {
CentsUnsigned::new((self.peak_regret_raw / Sats::ONE_BTC_U128) as u64)
pub(crate) fn peak_regret(&self) -> Cents {
Cents::new((self.peak_regret_raw / Sats::ONE_BTC_U128) as u64)
}
/// Get sats sent in profit.
#[inline]
pub fn sent_in_profit(&self) -> Sats {
pub(crate) fn sent_in_profit(&self) -> Sats {
self.sent_in_profit
}
/// Get sats sent in loss.
#[inline]
pub fn sent_in_loss(&self) -> Sats {
pub(crate) fn sent_in_loss(&self) -> Sats {
self.sent_in_loss
}
pub fn reset_single_iteration_values(&mut self) {
pub(crate) fn reset_single_iteration_values(&mut self) {
self.profit_raw = 0;
self.loss_raw = 0;
self.profit_value_created_raw = 0;
@@ -175,7 +145,7 @@ impl RealizedState {
/// Increment using pre-computed values (for UTXO path)
#[inline]
pub fn increment(&mut self, price: CentsUnsigned, sats: Sats) {
pub(crate) fn increment(&mut self, price: Cents, sats: Sats) {
if sats.is_zero() {
return;
}
@@ -186,26 +156,26 @@ impl RealizedState {
/// Increment using pre-computed snapshot values (for address path)
#[inline]
pub fn increment_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) {
pub(crate) fn increment_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) {
self.cap_raw += price_sats.as_u128();
self.investor_cap_raw += investor_cap;
}
/// Decrement using pre-computed snapshot values (for address path)
#[inline]
pub fn decrement_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) {
pub(crate) fn decrement_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) {
self.cap_raw -= price_sats.as_u128();
self.investor_cap_raw -= investor_cap;
}
#[inline]
pub fn receive(&mut self, price: CentsUnsigned, sats: Sats) {
pub(crate) fn receive(&mut self, price: Cents, sats: Sats) {
self.increment(price, sats);
}
/// Send with pre-computed typed values. Inlines decrement to avoid recomputation.
#[inline]
pub fn send(
pub(crate) fn send(
&mut self,
sats: Sats,
current_ps: CentsSats,

View File

@@ -1,6 +1,6 @@
use std::ops::Bound;
use brk_types::{CentsUnsigned, CentsUnsignedCompact, Sats};
use brk_types::{Cents, CentsCompact, Sats};
use super::CostBasisMap;
@@ -8,10 +8,10 @@ use super::CostBasisMap;
pub struct UnrealizedState {
pub supply_in_profit: Sats,
pub supply_in_loss: Sats,
pub unrealized_profit: CentsUnsigned,
pub unrealized_loss: CentsUnsigned,
pub invested_capital_in_profit: CentsUnsigned,
pub invested_capital_in_loss: CentsUnsigned,
pub unrealized_profit: Cents,
pub unrealized_loss: Cents,
pub invested_capital_in_profit: Cents,
pub invested_capital_in_loss: Cents,
/// Raw Σ(price² × sats) for UTXOs in profit. Used for aggregation.
pub investor_cap_in_profit_raw: u128,
/// Raw Σ(price² × sats) for UTXOs in loss. Used for aggregation.
@@ -26,39 +26,16 @@ impl UnrealizedState {
pub const ZERO: Self = Self {
supply_in_profit: Sats::ZERO,
supply_in_loss: Sats::ZERO,
unrealized_profit: CentsUnsigned::ZERO,
unrealized_loss: CentsUnsigned::ZERO,
invested_capital_in_profit: CentsUnsigned::ZERO,
invested_capital_in_loss: CentsUnsigned::ZERO,
unrealized_profit: Cents::ZERO,
unrealized_loss: Cents::ZERO,
invested_capital_in_profit: Cents::ZERO,
invested_capital_in_loss: Cents::ZERO,
investor_cap_in_profit_raw: 0,
investor_cap_in_loss_raw: 0,
invested_capital_in_profit_raw: 0,
invested_capital_in_loss_raw: 0,
};
/// Compute pain_index from raw values.
/// pain_index = investor_price_of_losers - spot
#[inline]
pub fn pain_index(&self, spot: CentsUnsigned) -> CentsUnsigned {
if self.invested_capital_in_loss_raw == 0 {
return CentsUnsigned::ZERO;
}
let investor_price_losers =
self.investor_cap_in_loss_raw / self.invested_capital_in_loss_raw;
CentsUnsigned::new((investor_price_losers - spot.as_u128()) as u64)
}
/// Compute greed_index from raw values.
/// greed_index = spot - investor_price_of_winners
#[inline]
pub fn greed_index(&self, spot: CentsUnsigned) -> CentsUnsigned {
if self.invested_capital_in_profit_raw == 0 {
return CentsUnsigned::ZERO;
}
let investor_price_winners =
self.investor_cap_in_profit_raw / self.invested_capital_in_profit_raw;
CentsUnsigned::new((spot.as_u128() - investor_price_winners) as u64)
}
}
/// Internal cache state using u128 for raw cent*sat values.
@@ -88,14 +65,12 @@ impl CachedStateRaw {
UnrealizedState {
supply_in_profit: self.supply_in_profit,
supply_in_loss: self.supply_in_loss,
unrealized_profit: CentsUnsigned::new(
(self.unrealized_profit / Sats::ONE_BTC_U128) as u64,
),
unrealized_loss: CentsUnsigned::new((self.unrealized_loss / Sats::ONE_BTC_U128) as u64),
invested_capital_in_profit: CentsUnsigned::new(
unrealized_profit: Cents::new((self.unrealized_profit / Sats::ONE_BTC_U128) as u64),
unrealized_loss: Cents::new((self.unrealized_loss / Sats::ONE_BTC_U128) as u64),
invested_capital_in_profit: Cents::new(
(self.invested_capital_in_profit / Sats::ONE_BTC_U128) as u64,
),
invested_capital_in_loss: CentsUnsigned::new(
invested_capital_in_loss: Cents::new(
(self.invested_capital_in_loss / Sats::ONE_BTC_U128) as u64,
),
investor_cap_in_profit_raw: self.investor_cap_in_profit,
@@ -109,12 +84,12 @@ impl CachedStateRaw {
#[derive(Debug, Clone)]
pub struct CachedUnrealizedState {
state: CachedStateRaw,
at_price: CentsUnsignedCompact,
at_price: CentsCompact,
}
impl CachedUnrealizedState {
pub fn compute_fresh(price: CentsUnsigned, map: &CostBasisMap) -> Self {
let price: CentsUnsignedCompact = price.into();
pub(crate) fn compute_fresh(price: Cents, map: &CostBasisMap) -> Self {
let price: CentsCompact = price.into();
let state = Self::compute_raw(price, map);
Self {
state,
@@ -123,24 +98,20 @@ impl CachedUnrealizedState {
}
/// Get the current cached state as output (without price update).
pub fn current_state(&self) -> UnrealizedState {
pub(crate) fn current_state(&self) -> UnrealizedState {
self.state.to_output()
}
pub fn get_at_price(
&mut self,
new_price: CentsUnsigned,
map: &CostBasisMap,
) -> UnrealizedState {
let new_price: CentsUnsignedCompact = new_price.into();
pub(crate) fn get_at_price(&mut self, new_price: Cents, map: &CostBasisMap) -> UnrealizedState {
let new_price: CentsCompact = new_price.into();
if new_price != self.at_price {
self.update_for_price_change(new_price, map);
}
self.state.to_output()
}
pub fn on_receive(&mut self, price: CentsUnsigned, sats: Sats) {
let price: CentsUnsignedCompact = price.into();
pub(crate) fn on_receive(&mut self, price: Cents, sats: Sats) {
let price: CentsCompact = price.into();
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
let invested_capital = price_u128 * sats_u128;
@@ -163,8 +134,8 @@ impl CachedUnrealizedState {
}
}
pub fn on_send(&mut self, price: CentsUnsigned, sats: Sats) {
let price: CentsUnsignedCompact = price.into();
pub(crate) fn on_send(&mut self, price: Cents, sats: Sats) {
let price: CentsCompact = price.into();
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
let invested_capital = price_u128 * sats_u128;
@@ -187,7 +158,7 @@ impl CachedUnrealizedState {
}
}
fn update_for_price_change(&mut self, new_price: CentsUnsignedCompact, map: &CostBasisMap) {
fn update_for_price_change(&mut self, new_price: CentsCompact, map: &CostBasisMap) {
let old_price = self.at_price;
if new_price > old_price {
@@ -198,7 +169,8 @@ impl CachedUnrealizedState {
// First, process UTXOs crossing from loss to profit
// Range (old_price, new_price] means: old_price < price <= new_price
for (&price, &sats) in map.range((Bound::Excluded(old_price), Bound::Included(new_price)))
for (&price, &sats) in
map.range((Bound::Excluded(old_price), Bound::Included(new_price)))
{
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
@@ -239,7 +211,8 @@ impl CachedUnrealizedState {
// First, process UTXOs crossing from profit to loss
// Range (new_price, old_price] means: new_price < price <= old_price
for (&price, &sats) in map.range((Bound::Excluded(new_price), Bound::Included(old_price)))
for (&price, &sats) in
map.range((Bound::Excluded(new_price), Bound::Included(old_price)))
{
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
@@ -278,7 +251,7 @@ impl CachedUnrealizedState {
}
/// Compute raw cached state from the map.
fn compute_raw(current_price: CentsUnsignedCompact, map: &CostBasisMap) -> CachedStateRaw {
fn compute_raw(current_price: CentsCompact, map: &CostBasisMap) -> CachedStateRaw {
let mut state = CachedStateRaw::default();
for (&price, &sats) in map.iter() {
@@ -309,8 +282,8 @@ impl CachedUnrealizedState {
/// Compute final UnrealizedState directly (not cached).
/// Used for date_state which doesn't use the cache.
pub fn compute_full_standalone(
current_price: CentsUnsignedCompact,
pub(crate) fn compute_full_standalone(
current_price: CentsCompact,
map: &CostBasisMap,
) -> UnrealizedState {
Self::compute_raw(current_price, map).to_output()

View File

@@ -12,7 +12,7 @@ pub struct Transacted {
impl Transacted {
#[allow(clippy::inconsistent_digit_grouping)]
pub fn iterate(&mut self, value: Sats, _type: OutputType) {
pub(crate) fn iterate(&mut self, value: Sats, _type: OutputType) {
let supply = SupplyState {
utxo_count: 1,
value,