use brk_cohort::{Loss, Profit, ProfitabilityRange};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPointsSigned32, Bitcoin, Cents, Dollars, Indexes, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
use crate::{
indexes,
internal::{
AmountPerBlock, AmountPerBlockWithDeltas, CachedWindowStarts, PerBlock, RatioPerBlock,
},
prices,
};
#[derive(Traversable)]
pub struct WithSth<All, Sth = All> {
pub all: All,
pub sth: Sth,
}
#[derive(Traversable)]
pub struct ProfitabilityBucket<M: StorageMode = Rw> {
pub supply: WithSth<AmountPerBlockWithDeltas<M>, AmountPerBlock<M>>,
pub realized_cap: WithSth<PerBlock<Dollars, M>>,
pub unrealized_pnl: WithSth<PerBlock<Dollars, M>>,
pub nupl: RatioPerBlock<BasisPointsSigned32, M>,
}
impl<M: StorageMode> ProfitabilityBucket<M> {
fn min_len(&self) -> usize {
self.supply
.all
.sats
.height
.len()
.min(self.realized_cap.all.height.len())
}
}
impl ProfitabilityBucket {
fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
) -> Result<Self> {
Ok(Self {
supply: WithSth {
all: AmountPerBlockWithDeltas::forced_import(
db,
&format!("{name}_supply"),
version,
indexes,
cached_starts,
)?,
sth: AmountPerBlock::forced_import(
db,
&format!("{name}_sth_supply"),
version,
indexes,
)?,
},
realized_cap: WithSth {
all: PerBlock::forced_import(
db,
&format!("{name}_realized_cap"),
version,
indexes,
)?,
sth: PerBlock::forced_import(
db,
&format!("{name}_sth_realized_cap"),
version,
indexes,
)?,
},
unrealized_pnl: WithSth {
all: PerBlock::forced_import(
db,
&format!("{name}_unrealized_pnl"),
version,
indexes,
)?,
sth: PerBlock::forced_import(
db,
&format!("{name}_sth_unrealized_pnl"),
version,
indexes,
)?,
},
nupl: RatioPerBlock::forced_import_raw(
db,
&format!("{name}_nupl"),
version + Version::ONE,
indexes,
)?,
})
}
#[inline(always)]
pub(crate) fn push(
&mut self,
supply: Sats,
sth_supply: Sats,
realized_cap: Dollars,
sth_realized_cap: Dollars,
) {
self.supply.all.sats.height.push(supply);
self.supply.sth.sats.height.push(sth_supply);
self.realized_cap.all.height.push(realized_cap);
self.realized_cap.sth.height.push(sth_realized_cap);
}
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
is_profit: bool,
exit: &Exit,
) -> Result<()> {
let max_from = starting_indexes.height;
self.supply.all.compute(prices, max_from, exit)?;
self.supply.sth.compute(prices, max_from, exit)?;
self.unrealized_pnl.all.height.compute_transform3(
max_from,
&prices.spot.cents.height,
&self.realized_cap.all.height,
&self.supply.all.sats.height,
|(i, spot, cap, supply, ..)| {
let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply));
let rc = f64::from(cap);
let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0);
(i, Dollars::from(pnl))
},
exit,
)?;
self.unrealized_pnl.sth.height.compute_transform3(
max_from,
&prices.spot.cents.height,
&self.realized_cap.sth.height,
&self.supply.sth.sats.height,
|(i, spot, cap, supply, ..)| {
let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply));
let rc = f64::from(cap);
let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0);
(i, Dollars::from(pnl))
},
exit,
)?;
self.nupl.bps.height.compute_transform3(
max_from,
&prices.spot.cents.height,
&self.realized_cap.all.height,
&self.supply.all.sats.height,
|(i, spot, cap_dollars, supply_sats, ..)| {
let p = spot.as_u128();
let supply = supply_sats.as_u128();
if p == 0 || supply == 0 {
(i, BasisPointsSigned32::ZERO)
} else {
let rp = Cents::from(cap_dollars).as_u128() * Sats::ONE_BTC_U128 / supply;
let bps = ((p as i128 - rp as i128) * 10000) / p as i128;
(i, BasisPointsSigned32::from(bps as i32))
}
},
exit,
)?;
Ok(())
}
pub(crate) fn compute_from_ranges(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
is_profit: bool,
sources: &[&ProfitabilityBucket],
exit: &Exit,
) -> Result<()> {
let max_from = starting_indexes.height;
self.supply.all.sats.height.compute_sum_of_others(
max_from,
&sources
.iter()
.map(|s| &s.supply.all.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
self.supply.sth.sats.height.compute_sum_of_others(
max_from,
&sources
.iter()
.map(|s| &s.supply.sth.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
self.realized_cap.all.height.compute_sum_of_others(
max_from,
&sources
.iter()
.map(|s| &s.realized_cap.all.height)
.collect::<Vec<_>>(),
exit,
)?;
self.realized_cap.sth.height.compute_sum_of_others(
max_from,
&sources
.iter()
.map(|s| &s.realized_cap.sth.height)
.collect::<Vec<_>>(),
exit,
)?;
self.compute(prices, starting_indexes, is_profit, exit)
}
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.supply.all.inner.sats.height as &mut dyn AnyStoredVec,
&mut self.supply.all.inner.cents.height,
&mut self.supply.sth.sats.height,
&mut self.supply.sth.cents.height,
&mut self.realized_cap.all.height,
&mut self.realized_cap.sth.height,
&mut self.unrealized_pnl.all.height,
&mut self.unrealized_pnl.sth.height,
&mut self.nupl.bps.height,
]
}
}
#[derive(Traversable)]
pub struct ProfitabilityMetrics<M: StorageMode = Rw> {
pub range: ProfitabilityRange<ProfitabilityBucket<M>>,
pub profit: Profit<ProfitabilityBucket<M>>,
pub loss: Loss<ProfitabilityBucket<M>>,
}
impl<M: StorageMode> ProfitabilityMetrics<M> {
pub fn iter(&self) -> impl Iterator<Item = &ProfitabilityBucket<M>> {
self.range
.iter()
.chain(self.profit.iter())
.chain(self.loss.iter())
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ProfitabilityBucket<M>> {
self.range
.iter_mut()
.chain(self.profit.iter_mut())
.chain(self.loss.iter_mut())
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.range.iter().map(|b| b.min_len()).min().unwrap_or(0)
}
}
impl ProfitabilityMetrics {
pub(crate) fn forced_import(
db: &Database,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
) -> Result<Self> {
let range = ProfitabilityRange::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
})?;
let aggregate_version = version + Version::ONE;
let profit = Profit::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
})?;
let loss = Loss::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, aggregate_version, indexes, cached_starts)
})?;
Ok(Self {
range,
profit,
loss,
})
}
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
exit: &Exit,
) -> Result<()> {
for (is_profit, bucket) in self.range.iter_mut_with_is_profit() {
bucket.compute(prices, starting_indexes, is_profit, exit)?;
}
let range_arr = self.range.as_array();
for (threshold, sources) in self.profit.iter_mut_with_growing_prefix(&range_arr) {
threshold.compute_from_ranges(prices, starting_indexes, true, sources, exit)?;
}
for (threshold, sources) in self.loss.iter_mut_with_growing_suffix(&range_arr) {
threshold.compute_from_ranges(prices, starting_indexes, false, sources, exit)?;
}
Ok(())
}
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = Vec::new();
for bucket in self.iter_mut() {
vecs.extend(bucket.collect_all_vecs_mut());
}
vecs
}
}