mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-27 08:09:58 -07:00
website: snapshot
This commit is contained in:
@@ -8,7 +8,7 @@ mod total_addr_count;
|
||||
mod type_map;
|
||||
|
||||
pub use activity::{AddressActivityVecs, AddressTypeToActivityCounts};
|
||||
pub use address_count::{AddrCountVecs, AddrCountsVecs, AddressTypeToAddressCount};
|
||||
pub use address_count::{AddrCountsVecs, AddressTypeToAddressCount};
|
||||
pub use data::AddressesDataVecs;
|
||||
pub use growth_rate::GrowthRateVecs;
|
||||
pub use indexes::AnyAddressIndexesVecs;
|
||||
|
||||
@@ -56,52 +56,44 @@ impl AddressCohorts {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Apply a function to each aggregate cohort with its source cohorts.
|
||||
fn for_each_aggregate<F>(&mut self, mut f: F) -> Result<()>
|
||||
where
|
||||
F: FnMut(&mut AddressCohortVecs, Vec<&AddressCohortVecs>) -> Result<()>,
|
||||
{
|
||||
let by_amount_range = &self.0.amount_range;
|
||||
|
||||
let pairs: Vec<_> = self
|
||||
.0
|
||||
.ge_amount
|
||||
.iter_mut()
|
||||
.chain(self.0.lt_amount.iter_mut())
|
||||
.map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (vecs, sources) in pairs {
|
||||
f(vecs, sources)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute overlapping cohorts from component amount_range cohorts.
|
||||
///
|
||||
/// For example, ">=1 BTC" cohort is computed from sum of amount_range cohorts that match.
|
||||
pub fn compute_overlapping_vecs(
|
||||
&mut self,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let by_amount_range = &self.0.amount_range;
|
||||
|
||||
// ge_amount cohorts computed from matching amount_range cohorts
|
||||
[
|
||||
self.0
|
||||
.ge_amount
|
||||
.par_iter_mut()
|
||||
.map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
// lt_amount cohorts computed from matching amount_range cohorts
|
||||
self.0
|
||||
.lt_amount
|
||||
.par_iter_mut()
|
||||
.map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.try_for_each(|(vecs, components)| {
|
||||
vecs.compute_from_stateful(starting_indexes, &components, exit)
|
||||
self.for_each_aggregate(|vecs, sources| {
|
||||
vecs.compute_from_stateful(starting_indexes, &sources, exit)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -117,6 +109,20 @@ impl AddressCohorts {
|
||||
.try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit))
|
||||
}
|
||||
|
||||
/// Recompute net_sentiment for aggregate cohorts as weighted average of source cohorts.
|
||||
pub fn compute_aggregate_net_sentiment(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.for_each_aggregate(|vecs, sources| {
|
||||
let metrics: Vec<_> = sources.iter().map(|v| &v.metrics).collect();
|
||||
vecs.metrics
|
||||
.compute_net_sentiment_from_others(starting_indexes, &metrics, indexes, exit)
|
||||
})
|
||||
}
|
||||
|
||||
/// Second phase of post-processing: compute relative metrics.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn compute_rest_part2<HM, DM>(
|
||||
|
||||
@@ -152,70 +152,84 @@ impl UTXOCohorts {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Apply a function to each aggregate cohort with its source cohorts.
|
||||
fn for_each_aggregate<F>(&mut self, mut f: F) -> Result<()>
|
||||
where
|
||||
F: FnMut(&mut UTXOCohortVecs, Vec<&UTXOCohortVecs>) -> Result<()>,
|
||||
{
|
||||
let by_age_range = &self.0.age_range;
|
||||
let by_amount_range = &self.0.amount_range;
|
||||
|
||||
// Build (aggregate, sources) pairs
|
||||
let pairs: Vec<_> = [(&mut self.0.all, by_age_range.iter().collect::<Vec<_>>())]
|
||||
.into_iter()
|
||||
.chain(self.0.min_age.iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.max_age.iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.term.iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.ge_amount.iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.lt_amount.iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect(),
|
||||
)
|
||||
}))
|
||||
.collect();
|
||||
|
||||
for (vecs, sources) in pairs {
|
||||
f(vecs, sources)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute overlapping cohorts from component age/amount range cohorts.
|
||||
pub fn compute_overlapping_vecs(
|
||||
&mut self,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let by_age_range = &self.0.age_range;
|
||||
let by_amount_range = &self.0.amount_range;
|
||||
|
||||
[(&mut self.0.all, by_age_range.iter().collect::<Vec<_>>())]
|
||||
.into_par_iter()
|
||||
.chain(self.0.min_age.par_iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.max_age.par_iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.term.par_iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_age_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.ge_amount.par_iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}))
|
||||
.chain(self.0.lt_amount.par_iter_mut().map(|vecs| {
|
||||
let filter = vecs.filter().clone();
|
||||
(
|
||||
vecs,
|
||||
by_amount_range
|
||||
.iter()
|
||||
.filter(|other| filter.includes(other.filter()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}))
|
||||
.try_for_each(|(vecs, components)| {
|
||||
vecs.compute_from_stateful(starting_indexes, &components, exit)
|
||||
})
|
||||
self.for_each_aggregate(|vecs, sources| {
|
||||
vecs.compute_from_stateful(starting_indexes, &sources, exit)
|
||||
})
|
||||
}
|
||||
|
||||
/// First phase of post-processing: compute index transforms.
|
||||
@@ -230,6 +244,24 @@ impl UTXOCohorts {
|
||||
.try_for_each(|v| v.compute_rest_part1(indexes, price, starting_indexes, exit))
|
||||
}
|
||||
|
||||
/// Recompute net_sentiment for aggregate cohorts as weighted average of source cohorts.
|
||||
pub fn compute_aggregate_net_sentiment(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.for_each_aggregate(|vecs, sources| {
|
||||
let metrics: Vec<_> = sources.iter().map(|v| &v.metrics).collect();
|
||||
vecs.metrics.compute_net_sentiment_from_others(
|
||||
starting_indexes,
|
||||
&metrics,
|
||||
indexes,
|
||||
exit,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Second phase of post-processing: compute relative metrics.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn compute_rest_part2<HM, DM>(
|
||||
|
||||
@@ -42,6 +42,10 @@ pub fn compute_rest_part1(
|
||||
utxo_cohorts.compute_rest_part1(indexes, price, starting_indexes, exit)?;
|
||||
address_cohorts.compute_rest_part1(indexes, price, starting_indexes, exit)?;
|
||||
|
||||
// Recompute net_sentiment for aggregate cohorts as weighted average
|
||||
utxo_cohorts.compute_aggregate_net_sentiment(indexes, starting_indexes, exit)?;
|
||||
address_cohorts.compute_aggregate_net_sentiment(indexes, starting_indexes, exit)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -296,6 +296,45 @@ impl CohortMetrics {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute net_sentiment as capital-weighted average of component cohorts.
|
||||
///
|
||||
/// For aggregate cohorts, the simple greed-pain formula produces values outside
|
||||
/// the range of components due to asymmetric weighting. This recomputes net_sentiment
|
||||
/// as a proper weighted average using realized_cap as weight.
|
||||
pub fn compute_net_sentiment_from_others(
|
||||
&mut self,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
others: &[&Self],
|
||||
indexes: &indexes::Vecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let Some(unrealized) = self.unrealized.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let weights: Vec<_> = others
|
||||
.iter()
|
||||
.filter_map(|o| Some(&o.realized.as_ref()?.realized_cap.height))
|
||||
.collect();
|
||||
let values: Vec<_> = others
|
||||
.iter()
|
||||
.filter_map(|o| Some(&o.unrealized.as_ref()?.net_sentiment.height))
|
||||
.collect();
|
||||
|
||||
if weights.len() != others.len() || values.len() != others.len() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
unrealized
|
||||
.net_sentiment
|
||||
.height
|
||||
.compute_weighted_average_of_others(starting_indexes.height, &weights, &values, exit)?;
|
||||
|
||||
unrealized
|
||||
.net_sentiment
|
||||
.compute_rest(indexes, starting_indexes, exit)
|
||||
}
|
||||
|
||||
/// First phase of computed metrics (indexes from height).
|
||||
pub fn compute_rest_part1(
|
||||
&mut self,
|
||||
|
||||
@@ -5,8 +5,8 @@ use brk_types::{Dollars, Sats, StoredF32, StoredF64, Version};
|
||||
use vecdb::IterableCloneableVec;
|
||||
|
||||
use crate::internal::{
|
||||
LazyBinaryFromHeightLast, LazyBinaryFromDateLast, NegPercentageDollarsF32, NegRatio32,
|
||||
PercentageDollarsF32, PercentageSatsF64, Ratio32,
|
||||
LazyBinaryFromDateLast, LazyBinaryFromHeightLast, NegPercentageDollarsF32,
|
||||
PercentageDollarsF32, PercentageSatsF64,
|
||||
};
|
||||
|
||||
use super::{ImportConfig, RealizedMetrics, SupplyMetrics, UnrealizedMetrics};
|
||||
@@ -337,33 +337,33 @@ impl RelativeMetrics {
|
||||
|
||||
// === Unrealized vs Own Total Unrealized PnL (lazy, optional) ===
|
||||
unrealized_profit_rel_to_own_total_unrealized_pnl: extended.then(|| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<Ratio32, _, _>(
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<PercentageDollarsF32, _, _>(
|
||||
&cfg.name("unrealized_profit_rel_to_own_total_unrealized_pnl"),
|
||||
cfg.version,
|
||||
cfg.version + v1,
|
||||
&unrealized.unrealized_profit,
|
||||
&unrealized.total_unrealized_pnl,
|
||||
)
|
||||
}),
|
||||
unrealized_loss_rel_to_own_total_unrealized_pnl: extended.then(|| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<Ratio32, _, _>(
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<PercentageDollarsF32, _, _>(
|
||||
&cfg.name("unrealized_loss_rel_to_own_total_unrealized_pnl"),
|
||||
cfg.version,
|
||||
cfg.version + v1,
|
||||
&unrealized.unrealized_loss,
|
||||
&unrealized.total_unrealized_pnl,
|
||||
)
|
||||
}),
|
||||
neg_unrealized_loss_rel_to_own_total_unrealized_pnl: extended.then(|| {
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<NegRatio32, _, _>(
|
||||
LazyBinaryFromHeightLast::from_computed_height_date_and_binary_block::<NegPercentageDollarsF32, _, _>(
|
||||
&cfg.name("neg_unrealized_loss_rel_to_own_total_unrealized_pnl"),
|
||||
cfg.version,
|
||||
cfg.version + v1,
|
||||
&unrealized.unrealized_loss,
|
||||
&unrealized.total_unrealized_pnl,
|
||||
)
|
||||
}),
|
||||
net_unrealized_pnl_rel_to_own_total_unrealized_pnl: extended.then(|| {
|
||||
LazyBinaryFromHeightLast::from_both_binary_block::<Ratio32, _, _, _, _>(
|
||||
LazyBinaryFromHeightLast::from_both_binary_block::<PercentageDollarsF32, _, _, _, _>(
|
||||
&cfg.name("net_unrealized_pnl_rel_to_own_total_unrealized_pnl"),
|
||||
cfg.version + v1,
|
||||
cfg.version + v2,
|
||||
&unrealized.net_unrealized_pnl,
|
||||
&unrealized.total_unrealized_pnl,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{CentsSats, CentsSquaredSats, CentsUnsigned, DateIndex, Dollars, Height};
|
||||
use brk_types::{CentsSats, CentsSquaredSats, CentsUnsigned, DateIndex, Dollars, Height, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, BytesVec, Exit, GenericStoredVec, ImportableVec, Negate,
|
||||
@@ -150,7 +150,7 @@ impl UnrealizedMetrics {
|
||||
let net_sentiment = ComputedFromHeightLast::forced_import(
|
||||
cfg.db,
|
||||
&cfg.name("net_sentiment"),
|
||||
cfg.version,
|
||||
cfg.version + Version::ONE, // v1: weighted average for aggregate cohorts
|
||||
cfg.indexes,
|
||||
)?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user