use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{
Bitcoin, Cents, CentsSats, CentsSigned, CentsSquaredSats, Height, PartsPerMillion32,
PartsPerMillion64, PartsPerMillionSigned64, StoredF64, Version,
};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, AnyVec, BytesVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::{
distribution::AllChainCache,
distribution::state::{CohortState, CostBasisData, RealizedState, WithCapital},
internal::{
FiatPerBlockCumulativeWithSums, LazyPercentPerBlock, PercentPerBlock,
PercentRollingWindows, PriceWithRatioPerBlock, RatioCents, RatioCents64,
RatioCentsSignedCents, RollingWindows, RollingWindowsFrom1w,
ValuePerBlockCumulativeRolling,
},
price,
};
use crate::distribution::metrics::ImportConfig;
use super::RealizedCore;
#[derive(Traversable)]
pub struct RealizedNetPnl<M: StorageMode = Rw> {
#[traversable(wrap = "change_1m", rename = "to_rcap")]
pub change_1m_to_rcap: PercentPerBlock<PartsPerMillionSigned64, M>,
#[traversable(wrap = "change_1m", rename = "to_mcap")]
pub change_1m_to_mcap: LazyPercentPerBlock<PartsPerMillionSigned64>,
}
#[derive(Traversable)]
pub struct RealizedSopr<M: StorageMode = Rw> {
#[traversable(rename = "ratio")]
pub ratio_extended: RollingWindowsFrom1w<StoredF64, M>,
}
#[derive(Traversable)]
pub struct RealizedPeakRegret<M: StorageMode = Rw> {
#[traversable(flatten)]
pub value: FiatPerBlockCumulativeWithSums<Cents, M>,
}
#[derive(Traversable)]
pub struct RealizedCapitalized<M: StorageMode = Rw> {
pub price: PriceWithRatioPerBlock<M>,
#[traversable(hidden)]
cap_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
}
#[derive(Deref, DerefMut, Traversable)]
pub struct RealizedFull<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub core: RealizedCore<M>,
#[traversable(wrap = "cap", rename = "to_own_mcap")]
pub cap_to_own_mcap: LazyPercentPerBlock<PartsPerMillion32>,
pub gross_pnl: FiatPerBlockCumulativeWithSums<Cents, M>,
pub sell_side_risk_ratio: PercentRollingWindows<PartsPerMillion32, M>,
pub net_pnl: RealizedNetPnl<M>,
pub sopr: RealizedSopr<M>,
pub peak_regret: RealizedPeakRegret<M>,
pub capitalized: RealizedCapitalized<M>,
pub profit_to_loss_ratio: RollingWindows<StoredF64, M>,
#[traversable(hidden)]
cap_raw: M::Stored<BytesVec<Height, CentsSats>>,
}
impl RealizedFull {
pub(crate) fn forced_import(cfg: &ImportConfig, all_chain: &AllChainCache) -> Result<Self> {
let v0 = Version::ZERO;
let v1 = Version::ONE;
let core = RealizedCore::forced_import(cfg)?;
let cap_to_own_mcap = LazyPercentPerBlock::from_indexed_source(
&cfg.name("realized_cap_to_own_mcap"),
cfg.version + Version::TWO,
&core.minimal.price.ppm.height,
mvrv_to_realized_cap_ratio,
cfg.indexes,
);
let gross_pnl: FiatPerBlockCumulativeWithSums<Cents> =
cfg.import("realized_gross_pnl", v1)?;
let sell_side_risk_ratio = cfg.import("sell_side_risk_ratio", Version::new(2))?;
let mcap_name = cfg.name("net_pnl_change_1m_to_mcap");
let mcap_version = Version::new(5);
let mcap_source = all_chain.with_market_cap(
&format!("{mcap_name}_ppm_source"),
mcap_version,
&core.net_pnl.delta.absolute._1m.cents.height,
|_, net_pnl, market_cap| Self::net_pnl_to_market_cap(net_pnl, market_cap),
);
let net_pnl = RealizedNetPnl {
change_1m_to_rcap: cfg.import("net_pnl_change_1m_to_rcap", Version::new(5))?,
change_1m_to_mcap: LazyPercentPerBlock::from_uncached_height_source(
&mcap_name,
mcap_version,
mcap_source,
cfg.indexes,
),
};
let sopr = RealizedSopr {
ratio_extended: cfg.import("sopr", v1)?,
};
let peak_regret = RealizedPeakRegret {
value: cfg.import("realized_peak_regret", Version::new(3))?,
};
let capitalized = RealizedCapitalized {
price: cfg.import("capitalized_price", v0)?,
cap_raw: cfg.import("capitalized_cap_raw", v0)?,
};
Ok(Self {
core,
cap_to_own_mcap,
gross_pnl,
sell_side_risk_ratio,
net_pnl,
sopr,
peak_regret,
capitalized,
profit_to_loss_ratio: cfg.import("realized_profit_to_loss_ratio", v1)?,
cap_raw: cfg.import("cap_raw", v0)?,
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.capitalized
.price
.cents
.height
.len()
.min(self.cap_raw.len())
.min(self.capitalized.cap_raw.len())
.min(self.peak_regret.value.cumulative.cents.height.len())
}
fn net_pnl_to_market_cap(net_pnl: CentsSigned, market_cap: Cents) -> PartsPerMillionSigned64 {
let market_cap = f64::from(market_cap);
if market_cap > 0.0 {
PartsPerMillionSigned64::from(net_pnl.inner() as f64 / market_cap)
} else {
PartsPerMillionSigned64::default()
}
}
#[inline(always)]
pub(crate) fn push_state(
&mut self,
state: &CohortState<RealizedState, CostBasisData<WithCapital>>,
) {
self.core.push_state(state);
self.capitalized
.price
.cents
.height
.push(state.realized.capitalized_price());
self.cap_raw.push(state.realized.cap_raw());
self.capitalized
.cap_raw
.push(state.realized.capitalized_cap_raw());
self.peak_regret
.value
.push_block(state.realized.peak_regret());
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = self.core.collect_vecs_mut();
vecs.push(&mut self.capitalized.price.cents.height);
vecs.push(&mut self.cap_raw as &mut dyn AnyStoredVec);
vecs.push(&mut self.capitalized.cap_raw as &mut dyn AnyStoredVec);
vecs.push(self.peak_regret.value.stored_mut());
vecs
}
pub(crate) fn compute_from_stateful(
&mut self,
starting_lengths: &Lengths,
others: &[&RealizedCore],
exit: &Exit,
) -> Result<()> {
self.core
.compute_from_stateful(starting_lengths, others, exit)
}
#[inline(always)]
pub(crate) fn push_accum(&mut self, accum: &RealizedFullAccum) -> Cents {
self.cap_raw.push(accum.cap_raw);
self.capitalized.cap_raw.push(accum.capitalized_cap_raw);
let capitalized_price = {
let cap = accum.cap_raw.as_u128();
if cap == 0 {
Cents::ZERO
} else {
Cents::new((accum.capitalized_cap_raw / cap) as u64)
}
};
self.capitalized.price.cents.height.push(capitalized_price);
self.peak_regret.value.push_block(accum.peak_regret());
capitalized_price
}
pub(crate) fn compute_rest_part1(
&mut self,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
self.core.compute_rest_part1(starting_lengths, exit)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest_part2(
&mut self,
prices: &price::Vecs,
starting_lengths: &Lengths,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
activity_transfer_volume: &ValuePerBlockCumulativeRolling,
exit: &Exit,
) -> Result<()> {
self.core.compute_rest_part2(
prices,
starting_lengths,
height_to_supply,
&activity_transfer_volume.sum._24h.cents.height,
exit,
)?;
for ((sopr, vc), vd) in self
.sopr
.ratio_extended
.as_mut_array()
.into_iter()
.zip(activity_transfer_volume.sum.0.as_array()[1..].iter())
.zip(self.core.sopr.value_destroyed.sum.as_array()[1..].iter())
{
sopr.compute_binary::<Cents, Cents, RatioCents64>(
starting_lengths.height,
&vc.cents.height,
&vd.height,
exit,
)?;
}
self.gross_pnl.compute_from_cumulative_pair(
starting_lengths.height,
&self.core.minimal.profit.cumulative.cents.height,
&self.core.minimal.loss.cumulative.cents.height,
|_, profit, loss| profit + loss,
exit,
)?;
self.net_pnl
.change_1m_to_rcap
.compute_binary::<CentsSigned, Cents, RatioCentsSignedCents<PartsPerMillionSigned64>>(
starting_lengths.height,
&self.core.net_pnl.delta.absolute._1m.cents.height,
&self.core.minimal.cap.cents.height,
exit,
)?;
for (ssrr, rv) in self
.sell_side_risk_ratio
.as_mut_array()
.into_iter()
.zip(self.gross_pnl.sum.as_array())
{
ssrr.compute_binary::<Cents, Cents, RatioCents<PartsPerMillion32>>(
starting_lengths.height,
&rv.cents.height,
&self.core.minimal.cap.cents.height,
exit,
)?;
}
for ((ratio, profit), loss) in self
.profit_to_loss_ratio
.as_mut_array()
.into_iter()
.zip(self.core.minimal.profit.sum.as_array())
.zip(self.core.minimal.loss.sum.as_array())
{
ratio.compute_binary::<Cents, Cents, RatioCents64>(
starting_lengths.height,
&profit.cents.height,
&loss.cents.height,
exit,
)?;
}
Ok(())
}
}
#[inline(always)]
fn mvrv_to_realized_cap_ratio(_: Height, mvrv: PartsPerMillion64) -> PartsPerMillion32 {
PartsPerMillion32::from(1.0 / f64::from(mvrv))
}
#[derive(Default)]
pub struct RealizedFullAccum {
pub(crate) cap_raw: CentsSats,
pub(crate) capitalized_cap_raw: CentsSquaredSats,
peak_regret: CentsSats,
}
impl RealizedFullAccum {
pub(crate) fn add(&mut self, state: &RealizedState) {
self.cap_raw += state.cap_raw();
self.capitalized_cap_raw += state.capitalized_cap_raw();
self.peak_regret += CentsSats::new(state.peak_regret_raw());
}
pub(crate) fn peak_regret(&self) -> Cents {
self.peak_regret.to_cents()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn realized_cap_ratio_is_inverse_mvrv() {
assert_eq!(
mvrv_to_realized_cap_ratio(Height::ZERO, PartsPerMillion64::from(2.0)),
PartsPerMillion32::from(0.5),
);
assert_eq!(
mvrv_to_realized_cap_ratio(Height::ZERO, PartsPerMillion64::from(1.0)),
PartsPerMillion32::from(1.0),
);
assert!(mvrv_to_realized_cap_ratio(Height::ZERO, PartsPerMillion64::NAN).is_nan());
assert!(mvrv_to_realized_cap_ratio(Height::ZERO, PartsPerMillion64::ZERO).is_nan());
}
}