mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-30 17:40:00 -07:00
computer: snapshot
This commit is contained in:
@@ -3,9 +3,7 @@ use std::path::Path;
|
||||
use brk_error::Result;
|
||||
use brk_types::{Age, CentsSats, CentsUnsigned, CostBasisSnapshot, Height, Sats, SupplyState};
|
||||
|
||||
use super::super::cost_basis::{
|
||||
CachedUnrealizedState, CostBasisData, Percentiles, RealizedState, UnrealizedState,
|
||||
};
|
||||
use super::super::cost_basis::{CostBasisData, Percentiles, RealizedState, UnrealizedState};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CohortState {
|
||||
@@ -15,9 +13,6 @@ pub struct CohortState {
|
||||
pub satblocks_destroyed: Sats,
|
||||
pub satdays_destroyed: Sats,
|
||||
cost_basis_data: Option<CostBasisData>,
|
||||
cached_unrealized: Option<CachedUnrealizedState>,
|
||||
/// If set, prices are rounded to nearest dollar with N significant digits.
|
||||
price_rounding_digits: Option<i32>,
|
||||
}
|
||||
|
||||
impl CohortState {
|
||||
@@ -29,28 +24,18 @@ impl CohortState {
|
||||
satblocks_destroyed: Sats::ZERO,
|
||||
satdays_destroyed: Sats::ZERO,
|
||||
cost_basis_data: compute_dollars.then_some(CostBasisData::create(path, name)),
|
||||
cached_unrealized: None,
|
||||
price_rounding_digits: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable price rounding for cost basis data.
|
||||
pub fn with_price_rounding(mut self, digits: i32) -> Self {
|
||||
self.price_rounding_digits = Some(digits);
|
||||
if let Some(data) = self.cost_basis_data.take() {
|
||||
self.cost_basis_data = Some(data.with_price_rounding(digits));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Round price if rounding is enabled.
|
||||
#[inline]
|
||||
fn round_price(&self, price: CentsUnsigned) -> CentsUnsigned {
|
||||
match self.price_rounding_digits {
|
||||
Some(digits) => price.round_to_dollar(digits),
|
||||
None => price,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
|
||||
self.cached_unrealized = None;
|
||||
match self.cost_basis_data.as_mut() {
|
||||
Some(p) => p.import_at_or_before(height),
|
||||
None => Ok(height),
|
||||
@@ -73,7 +58,6 @@ impl CohortState {
|
||||
p.clean()?;
|
||||
p.init();
|
||||
}
|
||||
self.cached_unrealized = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -111,21 +95,16 @@ impl CohortState {
|
||||
self.supply += &s.supply_state;
|
||||
|
||||
if s.supply_state.value > Sats::ZERO && self.realized.is_some() {
|
||||
let rounded_price = self.round_price(s.realized_price);
|
||||
self.realized
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.increment_snapshot(s.price_sats, s.investor_cap);
|
||||
self.cost_basis_data.as_mut().unwrap().increment(
|
||||
rounded_price,
|
||||
s.realized_price,
|
||||
s.supply_state.value,
|
||||
s.price_sats,
|
||||
s.investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_receive(rounded_price, s.supply_state.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,21 +119,16 @@ impl CohortState {
|
||||
self.supply -= &s.supply_state;
|
||||
|
||||
if s.supply_state.value > Sats::ZERO && self.realized.is_some() {
|
||||
let rounded_price = self.round_price(s.realized_price);
|
||||
self.realized
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.decrement_snapshot(s.price_sats, s.investor_cap);
|
||||
self.cost_basis_data.as_mut().unwrap().decrement(
|
||||
rounded_price,
|
||||
s.realized_price,
|
||||
s.supply_state.value,
|
||||
s.price_sats,
|
||||
s.investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_send(rounded_price, s.supply_state.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,10 +153,6 @@ impl CohortState {
|
||||
price_sats,
|
||||
investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_receive(price, sats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,37 +165,27 @@ impl CohortState {
|
||||
) {
|
||||
self.supply += supply;
|
||||
|
||||
if supply.value > Sats::ZERO && self.realized.is_some() {
|
||||
// Pre-compute rounded prices before mutable borrows
|
||||
let current_rounded = self.round_price(current.realized_price);
|
||||
let prev_rounded = self.round_price(prev.realized_price);
|
||||
|
||||
self.realized.as_mut().unwrap().receive(price, supply.value);
|
||||
if supply.value > Sats::ZERO
|
||||
&& let Some(realized) = self.realized.as_mut()
|
||||
{
|
||||
realized.receive(price, supply.value);
|
||||
|
||||
if current.supply_state.value.is_not_zero() {
|
||||
self.cost_basis_data.as_mut().unwrap().increment(
|
||||
current_rounded,
|
||||
current.realized_price,
|
||||
current.supply_state.value,
|
||||
current.price_sats,
|
||||
current.investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_receive(current_rounded, current.supply_state.value);
|
||||
}
|
||||
}
|
||||
|
||||
if prev.supply_state.value.is_not_zero() {
|
||||
self.cost_basis_data.as_mut().unwrap().decrement(
|
||||
prev_rounded,
|
||||
prev.realized_price,
|
||||
prev.supply_state.value,
|
||||
prev.price_sats,
|
||||
prev.investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_send(prev_rounded, prev.supply_state.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,10 +229,6 @@ impl CohortState {
|
||||
prev_ps,
|
||||
prev_investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_send(pp, sats);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,7 +255,7 @@ impl CohortState {
|
||||
self.satblocks_destroyed += age.satblocks_destroyed(supply.value);
|
||||
self.satdays_destroyed += age.satdays_destroyed(supply.value);
|
||||
|
||||
if self.realized.is_some() {
|
||||
if let Some(realized) = self.realized.as_mut() {
|
||||
let sats = supply.value;
|
||||
|
||||
// Compute once for realized.send using typed values
|
||||
@@ -308,39 +264,24 @@ impl CohortState {
|
||||
let ath_ps = CentsSats::from_price_sats(ath, sats);
|
||||
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
|
||||
|
||||
// Pre-compute rounded prices before mutable borrows
|
||||
let current_rounded = self.round_price(current.realized_price);
|
||||
let prev_rounded = self.round_price(prev.realized_price);
|
||||
|
||||
self.realized
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
|
||||
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_rounded,
|
||||
current.realized_price,
|
||||
current.supply_state.value,
|
||||
current.price_sats,
|
||||
current.investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_receive(current_rounded, current.supply_state.value);
|
||||
}
|
||||
}
|
||||
|
||||
if prev.supply_state.value.is_not_zero() {
|
||||
self.cost_basis_data.as_mut().unwrap().decrement(
|
||||
prev_rounded,
|
||||
prev.realized_price,
|
||||
prev.supply_state.value,
|
||||
prev.price_sats,
|
||||
prev.investor_cap,
|
||||
);
|
||||
|
||||
if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.on_send(prev_rounded, prev.supply_state.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,25 +296,10 @@ impl CohortState {
|
||||
height_price: CentsUnsigned,
|
||||
date_price: Option<CentsUnsigned>,
|
||||
) -> (UnrealizedState, Option<UnrealizedState>) {
|
||||
let cost_basis_data = match self.cost_basis_data.as_ref() {
|
||||
Some(p) if !p.is_empty() => p,
|
||||
_ => return (UnrealizedState::ZERO, date_price.map(|_| UnrealizedState::ZERO)),
|
||||
};
|
||||
|
||||
let date_state = date_price.map(|date_price| {
|
||||
CachedUnrealizedState::compute_full_standalone(date_price.into(), cost_basis_data)
|
||||
});
|
||||
|
||||
let height_state = if let Some(cache) = self.cached_unrealized.as_mut() {
|
||||
cache.get_at_price(height_price, cost_basis_data)
|
||||
} else {
|
||||
let cache = CachedUnrealizedState::compute_fresh(height_price, cost_basis_data);
|
||||
let state = cache.current_state();
|
||||
self.cached_unrealized = Some(cache);
|
||||
state
|
||||
};
|
||||
|
||||
(height_state, date_state)
|
||||
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<()> {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs,
|
||||
ops::Bound,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
@@ -15,7 +14,10 @@ use vecdb::Bytes;
|
||||
|
||||
use crate::utils::OptionExt;
|
||||
|
||||
use super::Percentiles;
|
||||
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>;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct PendingRaw {
|
||||
@@ -31,6 +33,8 @@ pub struct CostBasisData {
|
||||
state: Option<State>,
|
||||
pending: FxHashMap<CentsUnsignedCompact, (Sats, Sats)>,
|
||||
pending_raw: PendingRaw,
|
||||
cache: Option<CachedUnrealizedState>,
|
||||
rounding_digits: Option<i32>,
|
||||
}
|
||||
|
||||
const STATE_TO_KEEP: usize = 10;
|
||||
@@ -42,6 +46,21 @@ impl CostBasisData {
|
||||
state: None,
|
||||
pending: FxHashMap::default(),
|
||||
pending_raw: PendingRaw::default(),
|
||||
cache: None,
|
||||
rounding_digits: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_price_rounding(mut self, digits: i32) -> Self {
|
||||
self.rounding_digits = Some(digits);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn round_price(&self, price: CentsUnsigned) -> CentsUnsigned {
|
||||
match self.rounding_digits {
|
||||
Some(digits) => price.round_to_dollar(digits),
|
||||
None => price,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +72,7 @@ impl CostBasisData {
|
||||
self.state = Some(State::deserialize(&fs::read(path)?)?);
|
||||
self.pending.clear();
|
||||
self.pending_raw = PendingRaw::default();
|
||||
self.cache = None;
|
||||
Ok(height)
|
||||
}
|
||||
|
||||
@@ -75,14 +95,6 @@ impl CostBasisData {
|
||||
self.state.u().base.map.iter().map(|(&k, v)| (k, v))
|
||||
}
|
||||
|
||||
pub fn range(
|
||||
&self,
|
||||
bounds: (Bound<CentsUnsignedCompact>, Bound<CentsUnsignedCompact>),
|
||||
) -> impl Iterator<Item = (CentsUnsignedCompact, &Sats)> {
|
||||
self.assert_pending_empty();
|
||||
self.state.u().base.map.range(bounds).map(|(&k, v)| (k, v))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.pending.is_empty() && self.state.u().base.map.is_empty()
|
||||
}
|
||||
@@ -119,7 +131,8 @@ impl CostBasisData {
|
||||
self.state.u().investor_cap_raw
|
||||
}
|
||||
|
||||
/// Increment with pre-computed typed values
|
||||
/// Increment with pre-computed typed values.
|
||||
/// Handles rounding and cache update.
|
||||
pub fn increment(
|
||||
&mut self,
|
||||
price: CentsUnsigned,
|
||||
@@ -127,14 +140,19 @@ impl CostBasisData {
|
||||
price_sats: CentsSats,
|
||||
investor_cap: CentsSquaredSats,
|
||||
) {
|
||||
let price = self.round_price(price);
|
||||
self.pending.entry(price.into()).or_default().0 += sats;
|
||||
self.pending_raw.cap_inc += price_sats;
|
||||
if investor_cap != CentsSquaredSats::ZERO {
|
||||
self.pending_raw.investor_cap_inc += investor_cap;
|
||||
}
|
||||
if let Some(cache) = self.cache.as_mut() {
|
||||
cache.on_receive(price, sats);
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrement with pre-computed typed values
|
||||
/// Decrement with pre-computed typed values.
|
||||
/// Handles rounding and cache update.
|
||||
pub fn decrement(
|
||||
&mut self,
|
||||
price: CentsUnsigned,
|
||||
@@ -142,11 +160,15 @@ impl CostBasisData {
|
||||
price_sats: CentsSats,
|
||||
investor_cap: CentsSquaredSats,
|
||||
) {
|
||||
let price = self.round_price(price);
|
||||
self.pending.entry(price.into()).or_default().1 += sats;
|
||||
self.pending_raw.cap_dec += price_sats;
|
||||
if investor_cap != CentsSquaredSats::ZERO {
|
||||
self.pending_raw.investor_cap_dec += investor_cap;
|
||||
}
|
||||
if let Some(cache) = self.cache.as_mut() {
|
||||
cache.on_send(price, sats);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_pending(&mut self) {
|
||||
@@ -214,6 +236,7 @@ impl CostBasisData {
|
||||
self.state.replace(State::default());
|
||||
self.pending.clear();
|
||||
self.pending_raw = PendingRaw::default();
|
||||
self.cache = None;
|
||||
}
|
||||
|
||||
pub fn compute_percentiles(&self) -> Option<Percentiles> {
|
||||
@@ -221,9 +244,36 @@ impl CostBasisData {
|
||||
Percentiles::compute(self.iter().map(|(k, &v)| (k, v)))
|
||||
}
|
||||
|
||||
pub fn compute_unrealized_states(
|
||||
&mut self,
|
||||
height_price: CentsUnsigned,
|
||||
date_price: Option<CentsUnsigned>,
|
||||
) -> (UnrealizedState, Option<UnrealizedState>) {
|
||||
if self.is_empty() {
|
||||
return (UnrealizedState::ZERO, date_price.map(|_| UnrealizedState::ZERO));
|
||||
}
|
||||
|
||||
let map = &self.state.u().base.map;
|
||||
|
||||
let date_state =
|
||||
date_price.map(|p| CachedUnrealizedState::compute_full_standalone(p.into(), map));
|
||||
|
||||
let height_state = if let Some(cache) = self.cache.as_mut() {
|
||||
cache.get_at_price(height_price, map)
|
||||
} else {
|
||||
let cache = CachedUnrealizedState::compute_fresh(height_price, map);
|
||||
let state = cache.current_state();
|
||||
self.cache = Some(cache);
|
||||
state
|
||||
};
|
||||
|
||||
(height_state, date_state)
|
||||
}
|
||||
|
||||
pub fn clean(&mut self) -> Result<()> {
|
||||
let _ = fs::remove_dir_all(&self.pathbuf);
|
||||
fs::create_dir_all(self.path_by_height())?;
|
||||
self.cache = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,7 @@ mod unrealized;
|
||||
pub use data::*;
|
||||
pub use percentiles::*;
|
||||
pub use realized::*;
|
||||
pub use unrealized::*;
|
||||
pub use unrealized::UnrealizedState;
|
||||
|
||||
// Internal use only
|
||||
pub(super) use unrealized::CachedUnrealizedState;
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::ops::Bound;
|
||||
|
||||
use brk_types::{CentsUnsigned, CentsUnsignedCompact, Sats};
|
||||
|
||||
use super::data::CostBasisData;
|
||||
use super::CostBasisMap;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct UnrealizedState {
|
||||
@@ -113,9 +113,9 @@ pub struct CachedUnrealizedState {
|
||||
}
|
||||
|
||||
impl CachedUnrealizedState {
|
||||
pub fn compute_fresh(price: CentsUnsigned, cost_basis_data: &CostBasisData) -> Self {
|
||||
pub fn compute_fresh(price: CentsUnsigned, map: &CostBasisMap) -> Self {
|
||||
let price: CentsUnsignedCompact = price.into();
|
||||
let state = Self::compute_raw(price, cost_basis_data);
|
||||
let state = Self::compute_raw(price, map);
|
||||
Self {
|
||||
state,
|
||||
at_price: price,
|
||||
@@ -130,11 +130,11 @@ impl CachedUnrealizedState {
|
||||
pub fn get_at_price(
|
||||
&mut self,
|
||||
new_price: CentsUnsigned,
|
||||
cost_basis_data: &CostBasisData,
|
||||
map: &CostBasisMap,
|
||||
) -> UnrealizedState {
|
||||
let new_price: CentsUnsignedCompact = new_price.into();
|
||||
if new_price != self.at_price {
|
||||
self.update_for_price_change(new_price, cost_basis_data);
|
||||
self.update_for_price_change(new_price, map);
|
||||
}
|
||||
self.state.to_output()
|
||||
}
|
||||
@@ -187,11 +187,7 @@ impl CachedUnrealizedState {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_for_price_change(
|
||||
&mut self,
|
||||
new_price: CentsUnsignedCompact,
|
||||
cost_basis_data: &CostBasisData,
|
||||
) {
|
||||
fn update_for_price_change(&mut self, new_price: CentsUnsignedCompact, map: &CostBasisMap) {
|
||||
let old_price = self.at_price;
|
||||
|
||||
if new_price > old_price {
|
||||
@@ -202,8 +198,7 @@ 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
|
||||
cost_basis_data.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();
|
||||
@@ -244,8 +239,7 @@ 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
|
||||
cost_basis_data.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();
|
||||
@@ -283,14 +277,11 @@ impl CachedUnrealizedState {
|
||||
self.at_price = new_price;
|
||||
}
|
||||
|
||||
/// Compute raw cached state from cost_basis_data.
|
||||
fn compute_raw(
|
||||
current_price: CentsUnsignedCompact,
|
||||
cost_basis_data: &CostBasisData,
|
||||
) -> CachedStateRaw {
|
||||
/// Compute raw cached state from the map.
|
||||
fn compute_raw(current_price: CentsUnsignedCompact, map: &CostBasisMap) -> CachedStateRaw {
|
||||
let mut state = CachedStateRaw::default();
|
||||
|
||||
for (price, &sats) in cost_basis_data.iter() {
|
||||
for (&price, &sats) in map.iter() {
|
||||
let sats_u128 = sats.as_u128();
|
||||
let price_u128 = price.as_u128();
|
||||
let invested_capital = price_u128 * sats_u128;
|
||||
@@ -320,8 +311,8 @@ impl CachedUnrealizedState {
|
||||
/// Used for date_state which doesn't use the cache.
|
||||
pub fn compute_full_standalone(
|
||||
current_price: CentsUnsignedCompact,
|
||||
cost_basis_data: &CostBasisData,
|
||||
map: &CostBasisMap,
|
||||
) -> UnrealizedState {
|
||||
Self::compute_raw(current_price, cost_basis_data).to_output()
|
||||
Self::compute_raw(current_price, map).to_output()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user