mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-27 01:54:47 -07:00
global: MASSIVE snapshot
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user