brk_computer 0.2.5

A Bitcoin dataset computer built on top of brk_indexer
Documentation
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::CentsSquaredSats;
use brk_types::{BasisPoints16, Cents, Height, Indexes, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};

use crate::internal::{PERCENTILES_LEN, PerBlock, PercentPerBlock, PercentilesVecs, Price};

use super::ImportConfig;

#[derive(Traversable)]
pub struct CostBasisSide<M: StorageMode = Rw> {
    pub per_coin: Price<PerBlock<Cents, M>>,
    pub per_dollar: Price<PerBlock<Cents, M>>,
}

/// Cost basis metrics: min/max + profit/loss splits + percentiles + supply density.
/// Used by all/sth/lth cohorts only.
#[derive(Traversable)]
pub struct CostBasis<M: StorageMode = Rw> {
    pub in_profit: CostBasisSide<M>,
    pub in_loss: CostBasisSide<M>,
    pub min: Price<PerBlock<Cents, M>>,
    pub max: Price<PerBlock<Cents, M>>,
    pub per_coin: PercentilesVecs<M>,
    pub per_dollar: PercentilesVecs<M>,
    pub supply_density: PercentPerBlock<BasisPoints16, M>,
}

impl CostBasis {
    pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
        Ok(Self {
            in_profit: CostBasisSide {
                per_coin: Price::forced_import(
                    cfg.db,
                    &cfg.name("cost_basis_in_profit_per_coin"),
                    cfg.version + Version::ONE,
                    cfg.indexes,
                )?,
                per_dollar: Price::forced_import(
                    cfg.db,
                    &cfg.name("cost_basis_in_profit_per_dollar"),
                    cfg.version + Version::ONE,
                    cfg.indexes,
                )?,
            },
            in_loss: CostBasisSide {
                per_coin: Price::forced_import(
                    cfg.db,
                    &cfg.name("cost_basis_in_loss_per_coin"),
                    cfg.version + Version::ONE,
                    cfg.indexes,
                )?,
                per_dollar: Price::forced_import(
                    cfg.db,
                    &cfg.name("cost_basis_in_loss_per_dollar"),
                    cfg.version + Version::ONE,
                    cfg.indexes,
                )?,
            },
            min: cfg.import("cost_basis_min", Version::ZERO)?,
            max: cfg.import("cost_basis_max", Version::ZERO)?,
            per_coin: PercentilesVecs::forced_import(
                cfg.db,
                &cfg.name("cost_basis_per_coin"),
                cfg.version,
                cfg.indexes,
            )?,
            per_dollar: PercentilesVecs::forced_import(
                cfg.db,
                &cfg.name("cost_basis_per_dollar"),
                cfg.version,
                cfg.indexes,
            )?,
            supply_density: PercentPerBlock::forced_import(
                cfg.db,
                &cfg.name("supply_density"),
                cfg.version,
                cfg.indexes,
            )?,
        })
    }

    pub(crate) fn min_stateful_len(&self) -> usize {
        self.min
            .cents
            .height
            .len()
            .min(self.max.cents.height.len())
            .min(self.supply_density.bps.height.len())
    }

    #[inline(always)]
    pub(crate) fn push_minmax(&mut self, min_price: Cents, max_price: Cents) {
        self.min.cents.height.push(min_price);
        self.max.cents.height.push(max_price);
    }

    #[inline(always)]
    pub(crate) fn push_percentiles(
        &mut self,
        sat_prices: &[Cents; PERCENTILES_LEN],
        usd_prices: &[Cents; PERCENTILES_LEN],
    ) {
        self.per_coin.push(sat_prices);
        self.per_dollar.push(usd_prices);
    }

    #[inline(always)]
    pub(crate) fn push_density(&mut self, density_bps: BasisPoints16) {
        self.supply_density.bps.height.push(density_bps);
    }

    pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
        self.per_coin
            .validate_computed_version_or_reset(base_version)?;
        self.per_dollar
            .validate_computed_version_or_reset(base_version)?;
        Ok(())
    }

    pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
        let mut vecs: Vec<&mut dyn AnyStoredVec> = vec![
            &mut self.in_profit.per_coin.cents.height,
            &mut self.in_profit.per_dollar.cents.height,
            &mut self.in_loss.per_coin.cents.height,
            &mut self.in_loss.per_dollar.cents.height,
            &mut self.min.cents.height,
            &mut self.max.cents.height,
            &mut self.supply_density.bps.height,
        ];
        vecs.extend(
            self.per_coin
                .vecs
                .iter_mut()
                .map(|v| &mut v.cents.height as &mut dyn AnyStoredVec),
        );
        vecs.extend(
            self.per_dollar
                .vecs
                .iter_mut()
                .map(|v| &mut v.cents.height as &mut dyn AnyStoredVec),
        );
        vecs
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn compute_prices(
        &mut self,
        starting_indexes: &Indexes,
        spot: &impl ReadableVec<Height, Cents>,
        invested_cap_in_profit: &impl ReadableVec<Height, Cents>,
        invested_cap_in_loss: &impl ReadableVec<Height, Cents>,
        supply_in_profit_sats: &impl ReadableVec<Height, Sats>,
        supply_in_loss_sats: &impl ReadableVec<Height, Sats>,
        investor_cap_in_profit_raw: &impl ReadableVec<Height, CentsSquaredSats>,
        investor_cap_in_loss_raw: &impl ReadableVec<Height, CentsSquaredSats>,
        exit: &Exit,
    ) -> Result<()> {
        self.in_profit.per_coin.cents.height.compute_transform3(
            starting_indexes.height,
            invested_cap_in_profit,
            supply_in_profit_sats,
            spot,
            |(h, invested_cents, supply_sats, spot, ..)| {
                let supply = supply_sats.as_u128();
                if supply == 0 {
                    return (h, spot);
                }
                (
                    h,
                    Cents::new((invested_cents.as_u128() * Sats::ONE_BTC_U128 / supply) as u64),
                )
            },
            exit,
        )?;
        self.in_loss.per_coin.cents.height.compute_transform3(
            starting_indexes.height,
            invested_cap_in_loss,
            supply_in_loss_sats,
            spot,
            |(h, invested_cents, supply_sats, spot, ..)| {
                let supply = supply_sats.as_u128();
                if supply == 0 {
                    return (h, spot);
                }
                (
                    h,
                    Cents::new((invested_cents.as_u128() * Sats::ONE_BTC_U128 / supply) as u64),
                )
            },
            exit,
        )?;
        self.in_profit.per_dollar.cents.height.compute_transform3(
            starting_indexes.height,
            investor_cap_in_profit_raw,
            invested_cap_in_profit,
            spot,
            |(h, investor_cap, invested_cents, spot, ..)| {
                let invested_raw = invested_cents.as_u128() * Sats::ONE_BTC_U128;
                if invested_raw == 0 {
                    return (h, spot);
                }
                (h, Cents::new((investor_cap.inner() / invested_raw) as u64))
            },
            exit,
        )?;
        self.in_loss.per_dollar.cents.height.compute_transform3(
            starting_indexes.height,
            investor_cap_in_loss_raw,
            invested_cap_in_loss,
            spot,
            |(h, investor_cap, invested_cents, spot, ..)| {
                let invested_raw = invested_cents.as_u128() * Sats::ONE_BTC_U128;
                if invested_raw == 0 {
                    return (h, spot);
                }
                (h, Cents::new((investor_cap.inner() / invested_raw) as u64))
            },
            exit,
        )?;
        Ok(())
    }
}